メインコンテンツへスキップ

CLI でフローをコピーする

CLI はフローをポータブルなバンドルにシリアライズし、別の場所で再作成できます。dci export-cloudflow-flow は、そのフローに加えて subflow ノード経由で到達可能なすべてのフローを 1 つの JSON ドキュメントとして書き出し、dci import-cloudflow-flow は、認証済みのテナント内にそれらのフローを作成します。

バンドルはテナントに中立で、クレデンシャルを含みません。Connections、Datastore テーブル、global variables は、インポート時に実際のリソースへ再バインドする名前付きの_requirements_ として移動します。元のテナントに関するそれ以外の情報(クレデンシャル、テナント識別子、スケジュール、実行履歴)は一切含まれません。

このチュートリアルでは After-Hours Instance Sweep というフローを使用します。これは、本番以外の EC2 インスタンスを停止するスケジュール済みフローです。ただし、ここでの各コマンドはどのフローに対しても同じように動作します。

注意

Export と import には最新の CLI が必要です。まず dci update を実行し、コマンドが dci --help に表示されない場合は Keep it up to date を参照してください。

目的と到達目標

  • 目的 (Goal): コンソール上でノードごとに作り直すことなく、稼働中のフローをあるテナントから別のテナントへ移動すること。

  • 到達目標 (Objectives): このチュートリアルでは次の内容を学びます。

    • フローをバンドルファイルにエクスポートし、その中身を確認する方法

    • ドライランを使って、何も書き込まずにインポートを検証する方法

    • ターゲットテナント内の Connections やテーブルに、バンドルの requirements をバインドする方法

    • フローをインポートし、コンソールで仕上げる方法

始める前に

  1. CLI をインストールし、サインインしてください。dci validate を実行して両方を確認してください。

  2. エクスポートおよびインポートを行う各テナントで、DoiT アカウントに CloudFlow Editor または CloudFlow Manager 権限が付与されていることを確認してください。詳細は CloudFlow permissions を参照してください。

  3. 省略可能ですが、以下でバンドルの一部を読み取ったりインポートオプションを追加したりするために使用する jq をインストールしてください。

フロー ID を探す

Export はフロー名ではなくフロー ID を受け取ります。フロー一覧は、ID を表示するよう指定した場合に ID を表示します。

dci list-cloudflows --fields id,name
╔══════════════════════╤══════════════════════════════════════════════════════╗
║ id │ name ║
╟━━━━━━━━━━━━━━━━━━━━━━┼━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╢
║ q8RR0Ea9KqbMtgAxKWiW │ Datastore SQL Testing ║
║ G2zdE9inbvwxBc38FCVc │ Draft Delete unused Oracle Cloud reserved public IPs ║
║ EBnC9oHaZpcH3hgWGoSp │ After-Hours Instance Sweep ║
╚══════════════════════╧══════════════════════════════════════════════════════╝

ID は、コンソールでフローを開いたときの URL の最後のセグメントでもあります。

フローをエクスポートする

Export はバンドルを標準出力に書き出すため、リダイレクトしてファイルに保存します。

dci export-cloudflow-flow EBnC9oHaZpcH3hgWGoSp > after-hours-sweep.json

結果は、フロー全体を表す 1 つの JSON ドキュメントです。そのトップレベルを見ると、どのような内容かが分かります。

jq '{kind, schemaVersion, rootFlow, requirements, flows: [.flows[] | {key, name, nodes: (.nodes | length)}]}' after-hours-sweep.json
{
"kind": "cloudflow.doit.com/FlowBundle",
"schemaVersion": 1,
"rootFlow": "EBnC9oHaZpcH3hgWGoSp",
"requirements": {},
"flows": [
{
"key": "EBnC9oHaZpcH3hgWGoSp",
"name": "After-Hours Instance Sweep",
"nodes": 7
}
]
}
  • rootFlow: エクスポートしたフローです。そこから呼び出される subflow は、flows 内に並んで格納されます。

  • requirements: フローが必要とする、テナントにスコープされたリソースです。このフローでは宣言されていません。requirements を持つフローについては、Bind requirements で説明します。

  • flows: フローそのもの(ノード、トランジション、パラメータ、変数定義)です。

デフォルトでは変数の値も一緒に移動します。変数名・型・必須かどうかだけを残し、値をバンドルに含めたくない場合は、--include-variable-values=false を付けてエクスポートしてください。

ポリシーおよび Slack チャンネルの参照は、テナント間では移動できません。Export はそれらをサポートされていない参照として記録し、Import はそれらを使用しているノードを未完了としてフラグします。その後、コンソールで参照先を付け替えてください。

バンドルを YAML として読む

バンドルはマシン向けですが、フローの構成を俯瞰したい場合は YAML の方が読みやすいことが多いです。less でページングしたり、先頭行だけを抜き出して全体像をつかんだりしてください。

dci export-cloudflow-flow EBnC9oHaZpcH3hgWGoSp --output yaml | head -13
exportedAt: "2026-08-25T17:58:31.696566271Z"
flows:
- description: Stops non-prod instances on weekday evenings. Skips production. Flags
untagged instances to their owners.
firstNode: 4e10b33c-5ff9-4667-b8d9-594317c55e7d
key: EBnC9oHaZpcH3hgWGoSp
name: After-Hours Instance Sweep
nodes:
- key: 0421485f-0a3a-4f64-a587-fe1b0fb361e0
name: 'Production: skip'
parameters:
outputs: []
type: flowOutput

フロー内のすべてのノードは nodes の下に、firstNode と各トランジションが参照する内部 ID をキーとして並びます。同じビューはどの出力フォーマットでも利用でき、--fields でさらに絞り込めます。

注意

YAML は閲覧専用として使用してください。Import が受け付けるのは JSON なので、インポートに使うファイルはデフォルトのフォーマットのままにしてください。

インポートをドライランする

インポート前には必ず検証してください。ドライランは何も書き込まず、インポートプランだけを返します。

dci import-cloudflow-flow --dry-run < after-hours-sweep.json
{
"action": {
"command": "import-cloudflow-flow",
"status": "simulated",
"dry_run": true
},
"result": {
"errors": [],
"flowsToCreate": [
{
"incompleteNodeCount": 0,
"key": "EBnC9oHaZpcH3hgWGoSp",
"name": "After-Hours Instance Sweep",
"nodeCount": 7
}
],
"requirements": [],
"valid": true
}
}

valid: true かつ errors 配列が空、incompleteNodeCount: 0 の場合、そのインポートは完全に成功する見込みです。検証は問題を一度にすべて報告するため、拒否されたバンドルでも、試行ごとに 1 件ずつではなく、一度に全ての修正点を把握できます。

バンドルはエクスポート時と同じ形で標準入力から渡します。CLI が、API が期待するリクエスト形状にラップします。

requirements をバインドする

Connection、Datastore テーブル、global variable を使用するフローは、それらを requirement として宣言します。これらのリソースは 1 つのテナント内にだけ存在するためです。次は、未使用の Oracle Cloud の reserved public IP を削除するフローの requirements の例です。

dci export-cloudflow-flow G2zdE9inbvwxBc38FCVc > oci-flow.json
jq '.requirements' oci-flow.json
{
"connections": [
{
"key": "omni-oci",
"name": "omni-oci",
"provider": "Oracle",
"usedByNodes": [
"G2zdE9inbvwxBc38FCVc/delete-resources",
"G2zdE9inbvwxBc38FCVc/list-resources"
]
}
]
}

ドライランは、各 requirement をターゲットテナントに対して解決し、候補を提案します。

dci import-cloudflow-flow --dry-run < oci-flow.json | jq '.result'
{
"errors": [],
"flowsToCreate": [
{
"incompleteNodeCount": 2,
"key": "G2zdE9inbvwxBc38FCVc",
"name": "Draft Delete unused Oracle Cloud reserved public IPs",
"nodeCount": 4
}
],
"requirements": [
{
"candidates": [
{
"id": "CWGMz3ndg9WihPZnUXAi",
"name": "omni-oci",
"provider": "Oracle"
}
],
"consequenceIfUnbound": "referencing nodes are imported with the connection cleared and flagged incomplete",
"key": "omni-oci",
"resolution": "suggested",
"section": "connections"
}
],
"valid": true
}

各 requirement には resolution が付きます。

  • bound: あなたがバインディングを指定しており、検証も通過しています。

  • suggested: ターゲットテナントに妥当と思われる候補があり、その一覧が candidates に示されています。suggestion は自動では適用されないため、明示的にバインドしてください。

  • willCreate: Import がリソースを新規作成します。global variables はこの方法で解決され、options.createMissingTables を指定した場合の Datastore テーブルも同様です。

  • unbound: 一致するものがありません。インポート自体は成功しますが、consequenceIfUnbound が、その後に修正が必要な内容を示します。この例では、2 つのノードが接続先をクリアした状態で到着することを意味します。

requirement をバインドするには、生のバンドルではなく、完全なリクエスト形状を送信します。キーはプランから取得し、ID はターゲットテナントから取得します(dci list-cloudflow-connections)。

jq '{bundle: ., bindings: {connections: {"omni-oci": "CWGMz3ndg9WihPZnUXAi"}}}' oci-flow.json \
| dci import-cloudflow-flow --dry-run | jq '.result.requirements[0]'
{
"boundTo": "CWGMz3ndg9WihPZnUXAi",
"consequenceIfUnbound": "referencing nodes are imported with the connection cleared and flagged incomplete",
"key": "omni-oci",
"resolution": "bound",
"section": "connections"
}

同じリクエスト形状にインポートオプションも含めます。options.createMissingTables: true は、バンドル内のスキーマから不足している Datastore テーブルを作成します(構造のみで、行データは含まれません)。options.namePrefix は、インポートによって作成されるすべてのフロー名の先頭に付与されます。

jq '{bundle: ., options: {namePrefix: "Copy of "}}' after-hours-sweep.json \
| dci import-cloudflow-flow --dry-run | jq '.result.flowsToCreate[0]'
{
"incompleteNodeCount": 0,
"key": "EBnC9oHaZpcH3hgWGoSp",
"name": "Copy of After-Hours Instance Sweep",
"nodeCount": 7
}

フローをインポートする

プランに問題がなければ、--dry-run を外し、冪等性キーを追加します。インポートごとに新しいキーを生成してください。

dci import-cloudflow-flow --idempotency-key "$(uuidgen)" < after-hours-sweep.json
{
"flows": [
{
"id": "YT57P7OhP2nLwZMo9QSw",
"key": "EBnC9oHaZpcH3hgWGoSp",
"name": "After-Hours Instance Sweep",
"status": "draft"
}
]
}

key はバンドル内でのフロー ID、id はターゲットテナント内での新しいフロー ID です。Import は作成専用であり、呼び出しのたびに新しいドラフトフローを新しい ID で作成し、既存のフローを更新することはありません。同じ冪等性キーを再送すると 2 つ目のコピーは作られず、代わりに同じレスポンスが返されます。そのため、中断されたインポートを安全に再試行できます。

インポートされたフローはドラフトとして到着します。誰かがコンソールで公開するまでは、何も実行されず、スケジュールも有効になりません。

別のテナントへインポートする

テナント間でのコピーも、各コマンドにcustomer context を付与する以外は同じ 2 つのコマンドです。ソースからエクスポートし、ターゲットにインポートします。

dci export-cloudflow-flow EBnC9oHaZpcH3hgWGoSp -D <source-customer-id> > after-hours-sweep.json
dci import-cloudflow-flow --idempotency-key "$(uuidgen)" -D <target-customer-id> < after-hours-sweep.json

テナントをまたぐコピーでは、requirements に特に注意が必要です。元のテナントでは suggested として解決していた接続が、ターゲットテナントでは unbound になる場合があります。本番インポートの前にターゲットに対してドライランを行い、プランが列挙した items をバインドしてください。

注意

CloudFlow のコマンドで -D に渡すのは customer の ID であり、ドメインや URL 表示名ではありません。他のコマンドは 3 種類すべてを受け付けます。

次のステップ

  1. コンソールでインポートしたフローを開き、すべてのノードが完了していることを確認してください。requirements が unbound のまま残ったノードにはフラグが付いています。

  2. バンドルでは移動できないものを再接続してください。未バインドのままにした connections、Slack チャンネル、ポリシー参照などです。

  3. ターゲットテナントで必要なスケジュールまたはトリガーを設定し、その後フローを公開してください。詳細は Manage a flow を参照してください。

参照