0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Amazon Connect のコンタクトフローを CDK で管理しつつ、AWS アカウントに依存させない方法

0
Posted at

Amazon Connect の主軸となるリソースの1つに「コンタクトフロー」がありますが、これを CDK で管理しようとすると、一筋縄ではいきません。

理由は、コンタクトフローの実体が JSON ファイルであり、その中で他の AWS リソースを ID(多くの場合 ARN)によって直接参照する構造になっているためです。例えば、フロー内で Lambda 関数を呼び出す場合、JSON 上にはその Lambda 関数の ARN がそのまま埋め込まれます。

このまま CDK リポジトリで管理してしまうと、複数の AWS アカウントにデプロイする際に、その都度 JSON 内の ARN を手動で書き換える必要が生じます。

この記事では、コンタクトフローを CDK で管理しつつ、特定の AWS アカウントに依存させない方法を紹介します。

なお、本記事で紹介する実装のサンプルは以下のリポジトリで公開しています。あわせてご参照ください。

前提:使用する技術や環境情報

  • AWS CDK (TypeScript) v2 系(aws-cdk-lib)
  • Node.js / ts-node
  • Amazon Connect インスタンスは CDK 外で作成済みのものを利用する(インスタンス自体は CDK 管理対象外)
  • コンタクトフローのリソースには L1 construct の CfnContactFlow を使用
  • 対象のコンタクトフローは以下の2つで、ChildFlow1ParentFlow の順に参照し合う構成
    • ChildFlow1:単独で完結する子フロー
    • ParentFlow:Lambda 関数を呼び出したあと ChildFlow1 に遷移する親フロー

String.prototype.replaceAll を使うため、tsconfig.jsontarget / libES2021 以上にしておく必要があります。

実装

ディレクトリ構成

コンタクトフローの「中身(JSON)」と「デプロイ定義(CDKコード)」を分離し、以下の構成にしました。

.
├── bin/
│   └── app.ts                     # エントリポイント
├── lib/
│   └── connect-contact-flow-stack.ts  # 複数フローをループしてデプロイするスタック
├── config/
│   └── contact-flows.ts           # デプロイ対象フローの一覧(マニフェスト)
└── contact-flows/
    ├── ChildFlow1.json
    └── ParentFlow.json

JSON 側:アカウント固有の値をプレースホルダー化

Connect のフローデザイナーからエクスポートした JSON には、Lambda の ARN や他フローの ARN がそのまま埋め込まれています。これをあらかじめ ${KEY} 形式のプレースホルダーに置き換えておきます。

// ParentFlow.json(抜粋)
{
  "Parameters": {
    "LambdaFunctionARN": "${SAMPLE_FUNCTION_1_ARN}"
  },
  "Type": "InvokeLambdaFunction"
},
{
  "Parameters": {
    "ContactFlowId": "${CHILD_FLOW_1_ARN}"
  },
  "Type": "TransferToFlow"
}

config 側:プレースホルダーの解決方法を2種類用意する

アカウントに依存する値には、性質の異なる2パターンがあります。

  1. CDK 管理外の既存リソース(Lambda 関数など)の ARN
    → アカウントID・リージョンを cdk.Aws.ACCOUNT_ID / cdk.Aws.REGION という CloudFormation の擬似パラメータトークンから組み立てる
  2. 同じデプロイ内で CDK が新規作成する別リソース(他のコンタクトフロー)の ARN
    → そのリソースの construct が実際に作成されたあとでないと値が確定しないため、関数として遅延評価する
// config/contact-flows.ts(抜粋)
export type FlowMap = Record<string, connect.CfnContactFlow>;
export type SubstitutionValue = string | ((flows: FlowMap) => string);

// ① 既存Lambdaの場合:アカウントID/リージョンをトークンから組み立てる
export function lambdaArn(functionName: string): SubstitutionValue {
  return `arn:${cdk.Aws.PARTITION}:lambda:${cdk.Aws.REGION}:${cdk.Aws.ACCOUNT_ID}:function:${functionName}`;
}

// ② 同じ配列内の別フローの場合:作成済みフローのARNを遅延評価で参照する
export function contactFlowArn(flowId: string): SubstitutionValue {
  return (flows: FlowMap) => flows[flowId].attrContactFlowArn;
}

export const CONTACT_FLOWS: ContactFlowDefinition[] = [
  { id: "ChildFlow1", name: "Child Flow 1", type: "CONTACT_FLOW", filePath: "ChildFlow1.json" },
  {
    id: "ParentFlow",
    name: "Parent Flow",
    type: "CONTACT_FLOW",
    filePath: "ParentFlow.json",
    substitutions: {
      // ParentFlow.json 内の "${CHILD_FLOW_1_ARN}" を、ChildFlow1 のARNに置換する
      CHILD_FLOW_1_ARN: contactFlowArn("ChildFlow1"),
      // ParentFlow.json 内の "${SAMPLE_FUNCTION_1_ARN}" を、Lambda関数 sample-function-1 のARNに置換する
      SAMPLE_FUNCTION_1_ARN: lambdaArn("sample-function-1"),
    },
  },
];

ポイントは、contactFlowArn が返すのは値そのものではなく「あとで呼び出す関数」だということです。CfnContactFlow の ARN は CDK 上ではトークン(実際の値は CloudFormation デプロイ時に解決される仮の文字列)として扱われるため、参照先の construct が作られたタイミングで初めて評価できるようにしています。

スタック側:配列を順番に処理し、作成済みフローをマップに貯めていく

// lib/connect-contact-flow-stack.ts(抜粋)

// 作成済みフローを id で引けるようにしておく(後続フローからのARN参照に使う)
const flows: FlowMap = {};

// CONTACT_FLOWS を定義順に処理し、1件ずつコンタクトフローを作成していく
for (const def of CONTACT_FLOWS) {
  // ① フロー本体のJSONを読み込む
  let content = fs.readFileSync(path.join(CONTACT_FLOWS_DIR, def.filePath), "utf-8");

  // ② JSON内のプレースホルダー("${KEY}")を、対応する値に置換する
  for (const [key, value] of Object.entries(def.substitutions ?? {})) {
    // 値が関数の場合は、ここまでに作成済みのフロー情報(flows)を渡して実行する
    const resolved = typeof value === "function" ? value(flows) : value;
    content = content.replaceAll(`\${${key}}`, resolved);
  }

  // ③ 置換後のJSONを content として渡し、コンタクトフローのリソースを作成する
  const flow = new connect.CfnContactFlow(this, def.id, {
    instanceArn: props.connectInstanceArn,
    name: def.name,
    type: def.type,
    content: JSON.stringify(JSON.parse(content)),
  });

  // ④ 作成したフローをマップに登録する(後続フローから参照できるようにしておく)
  flows[def.id] = flow;
}

CONTACT_FLOWS を配列の先頭から順に処理するだけなので、ChildFlow1ParentFlow の順で定義しておけば、後ろのフローから前のフローの ARN を参照できます。逆に、参照先を後ろに書いてしまうとその時点ではまだ construct が存在しないため、実装上はエラーになります。

これにより、cdk deploy -c connectInstanceArn=... の1回の実行の中で、

  1. ChildFlow1 を作成
  2. その ARN と Lambda の ARN を使って ParentFlow を作成

という一連の流れが自動的に完結します。別アカウントにデプロイする場合も、変更が必要なのは基本的に connectInstanceArn の値だけです。

まとめ

コンタクトフローを CDK で管理する際にネックになるのは、JSON の中に「アカウントに依存する値」と「デプロイして初めて決まる値」が混在している点でした。

今回の実装では、この2種類の値を JSON から追い出してプレースホルダー化した上で、

  • 既存リソースの ARN は CloudFormation の擬似パラメータトークンから組み立てる
  • CDK が新規作成するリソースの ARN は construct 作成後に遅延評価する

という形で、値の性質ごとに解決方法を分けて実装しました。この考え方さえ押さえておけば、コンタクトフローに限らず「JSON や外部設定ファイルの中に環境依存の値が埋め込まれているリソースを CDK 管理下に置く」というケース全般に応用できると思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?