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

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 を参照してください。

参照