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 を便利にする Web アプリ開発:第2回

0
Posted at

フロー定義 JSON を再帰探索してリソースを逆引きする

本連載は3部作です

回 テーマ 内容
第1回 フロントエンドのみで AWS 接続 Cognito で一時認証情報を取得し、バックエンドなしで AWS SDK を叩く
第2回(本記事) リソース関連性の逆引き解析 フロー定義の JSON を再帰探索し、リソース同士の関連を逆引きする
第3回 性能・UX の最適化 大量の API 呼び出しを捌くキャッシュ・並列制御・段階ロードの工夫

この記事について

Amazon Connect のリソース(キュー、エージェント、モジュールなど)が「どのフローから使われているか」を自動で探し出す、逆引き解析の仕組みを解説します。

Connect の管理コンソールは、フローを開けば「そのフローが何を使っているか」は分かります。しかし逆に「このキューを使っているフローはどれか」を一覧するのは苦手です。この記事では、フロー定義の JSON を読み解いてその逆引きを実現する考え方を扱います。

この記事で分かること

  • なぜ Connect では「逆引き」が難しいのか
  • フロー定義(Content)の正体と、そこから情報を取り出す方法
  • ネスト構造を取りこぼさず探索する「再帰探索」の考え方
  • ID と ARN が混在する現実への対処(正規化・重複排除)
  • 逆引き機能を他のリソースへ横展開するパターン

⚠️ 記事中のコードは要点を抜き出した抜粋です。実際のコードにあるデバッグ用ログなどは省略し、ロジックの骨子だけを示しています。そのまま動く完成形ではありません。

前提(事前に把握しておくこと)

  • 第1回の構成(Cognito 経由で ConnectClient を利用できる状態)ができていること
  • Amazon Connect の基本概念(フロー、モジュール、キュー、エージェント、ルーティングプロファイル)を一通り知っていること
  • 利用する IAM ロールに connect:ListContactFlows / connect:DescribeContactFlow などの参照権限があること

1. 課題:Connect は「順引き」はできても「逆引き」ができない

Amazon Connect の設定は、おおまかに次のような関係でつながっています。

  • ユーザー → ルーティングプロファイル → キュー
  • フロー / モジュール → (内部で)キュー・エージェント・別モジュール・クイック接続を参照

コンソールでは上から下(順引き)はたどれます。しかし運用では逆方向の質問が頻発します。

  • 「このキューを廃止したいが、どのフローが参照している?」
  • 「このモジュールを修正したら、どのフローに影響する?」
  • 「このルーティングプロファイルは、誰に割り当てられている?」

これらに答えるには、全リソースを横断的に調べて関連を割り出す必要があります。とくにフロー・モジュールは設定が 定義 JSON の中に埋もれている ため、単純な一覧 API では分かりません。ここが逆引きの肝になります。

2. フロー定義(Content)の正体

DescribeContactFlow でフローの詳細を取得すると、Content というフィールドに フロー定義が JSON 文字列として 入っています。文字列なので、まず JSON.parse してオブジェクトにします。

const flow = await describeContactFlow(flowId);
if (flow?.Content) {
  const parsed = JSON.parse(flow.Content); // ← ここで構造化データになる
  // parsed.Actions に、フロー内の各ブロックが配列で入っている
}

パース後のオブジェクトは、おおまかに次のような形をしています。

{
  "Actions": [
    {
      "Identifier": "abc-123",
      "Type": "TransferToQueue",
      "Parameters": { "QueueId": "arn:aws:connect:...:queue/xxxx" }
    },
    {
      "Type": "InvokeFlowModule",
      "Parameters": { "FlowModuleId": "yyyy" }
    }
    // ...(多数のアクションが続く)
  ],
  "Metadata": { /* レイアウト情報など */ }
}

つまり「フローが何を使っているか」は、この Actions[].Parameters の中に QueueId や FlowModuleId といったキーで書かれています。ここを拾えばよい、というのが出発点です。

3. 素直に拾うと取りこぼす ― だから「再帰探索」

最初は「Actions を回して Parameters.QueueId を見ればいい」と考えます。しかし実際のフロー定義は、

  • アクションの種類によって Parameters の構造が違う
  • 参照が Parameters の さらに深いネスト に入っていることがある
  • 同じ「キュー参照」でも QueueId(ID形式)の場合と QueueArn(ARN形式)の場合がある

といった事情で、決め打ちのパスでは漏れます。

そこで、オブジェクトを再帰的に全走査し、目的のキー名に当たったら値を回収する 方針にします。配列なら各要素へ、オブジェクトなら各値へ潜っていくだけのシンプルな関数です。

// queues / modules / agents をそれぞれ Set に集める(重複を自然に排除)
const searchInObject = (obj: any) => {
  if (!obj || typeof obj !== 'object') return;

  if (Array.isArray(obj)) {
    obj.forEach(searchInObject);   // 配列 → 各要素へ
    return;
  }

  for (const key of Object.keys(obj)) {
    const value = obj[key];

    // キュー参照(ID形式)
    if (key === 'QueueId' && typeof value === 'string' && value.trim()) {
      queues.add(value);
    }
    // エージェント参照(複数のキー名に対応)
    if ((key === 'AgentId' || key === 'UserId' || key === 'UserArn') && typeof value === 'string') {
      // ARN なら user/ 以降の ID を抜き出す
      const m = value.match(/user\/(.+)$/);
      agents.add(m ? m[1] : value);
    }
    // モジュール参照(呼び出し方によってキー名が違う)
    if ((key === 'FlowModuleId' || key === 'ContactFlowModuleId' || key === 'ModuleId') && typeof value === 'string') {
      modules.add(value);
    }
    // キュー参照(ARN形式)
    if (key === 'QueueArn' && typeof value === 'string' && value.includes('queue/')) {
      queues.add(value);
    }

    // それ以外でも、オブジェクト/配列ならさらに潜る
    if (value && typeof value === 'object') searchInObject(value);
  }
};

ポイントは2つです。

  1. 複数のキー名を許容する:エージェントは AgentId / UserId / UserArn、モジュールは FlowModuleId / ContactFlowModuleId / ModuleId のように、文脈によって名前が揺れます。OR で受けておきます
  2. Set に集める:同じ参照が複数箇所に出ても、Set なら自然に重複排除されます

実際には Actions だけでなく Metadata やその他のトップレベルプロパティも走査対象にして、取りこぼしを減らしています。

4. ID と ARN の混在をどう正規化するか

現実のフロー定義では、同じキューが ある箇所では ID、別の箇所では ARN で書かれていることがあります。

  • ID 形式:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
  • ARN 形式:arn:aws:connect:<region>:<account>:instance/.../queue/xxxxxxxx-...

これを別物として数えると、同じキューが二重に出てしまいます。そこで、回収した後に ARN から ID 部分を抜き出して ID に寄せる 正規化を行います。

const finalQueueIds = new Set<string>();

// ID 形式はそのまま
collected.filter(q => !q.startsWith('arn:')).forEach(id => finalQueueIds.add(id));

// ARN 形式は queue/ 以降の ID を抽出して寄せる(既にあれば追加しない)
collected.filter(q => q.startsWith('arn:')).forEach(arn => {
  const m = arn.match(/queue\/(.+)$/);
  if (m) finalQueueIds.add(m[1]);
  else finalQueueIds.add(arn); // ID が取れなければ ARN のまま保持
});

こうして「フロー1つが参照しているキュー / モジュール / エージェントの ID 一覧」を、形式の揺れを吸収した上で取り出せます。ここまでが 抽出器(あるフローの中身を読む処理)です。

5. 逆引きの実装パターン

抽出器ができれば、逆引きは「全フローを取得し、1つずつ抽出して、目的のリソースを含むか判定する」だけです。例として「あるキューを使っているフロー」を探す処理を示します。

async function findFlowsUsingQueue(queueId: string) {
  // 1. 対象キューの ARN も取得しておく(ID / ARN 両方で照合するため)
  const queueArn = (await describeQueue(queueId))?.QueueArn;

  // 2. 全フローを取得
  const allFlows = await listContactFlows();

  // 3. 各フローを解析して判定(※並列実行の工夫は第3回で扱う)
  const results = await Promise.all(allFlows.map(async (flow) => {
    const detail = await describeContactFlowWithContent(flow.Id);
    if (!detail?.parsedContent) return null;

    const { queues } = extractResourcesFromFlowContent(detail.parsedContent);

    // 4. ID / ARN / ARN内ID の3パターンで照合
    const used = queues.some(q =>
      q === queueId ||
      (queueArn && q === queueArn) ||
      q.includes(queueId) ||
      (q.startsWith('arn:') && q.match(/queue\/(.+)$/)?.[1] === queueId)
    );

    return used ? { id: flow.Id, name: flow.Name } : null;
  }));

  return results.filter(Boolean); // ヒットしたフローだけ返す
}

照合を「ID 一致」だけにせず、ARN 一致・ARN から抽出した ID の一致 まで見ているのがポイントです。抽出側で正規化していても、フロー定義の書かれ方は一様ではないため、判定側でも複数パターンを許容して取りこぼしを防いでいます。

モジュールの中身も同じ JSON 構造なので、DescribeContactFlowModule の Content をパースして 同じ抽出器を再利用 できます。「キューを使っているモジュール」も同じ要領で探せます。

6. 同じ仕組みを横展開する

抽出器+「全件取得して判定」という型ができると、逆引き機能を次々と量産できます。本ツールでは以下を実装しています。

逆引き やりたいこと 調べ方
キュー → フロー このキューを使うフロー一覧 全フローの Content を解析
キュー → モジュール このキューを使うモジュール一覧 全モジュールの Content を解析
キュー → ルーティングプロファイル このキューを持つRP一覧 各RPの ListRoutingProfileQueues を確認
モジュール → フロー このモジュールを呼ぶフロー一覧 全フローの Content を解析
エージェント → フロー このエージェントへ転送するフロー一覧 全フローの Content を解析
ルーティングプロファイル → ユーザー このRPを割り当てられたユーザー 各ユーザーの RoutingProfileId を確認
クイック接続 → キュー このQCを設定したキュー一覧 各キューの ListQueueQuickConnects を確認

注目したいのは、逆引きには2タイプある ことです。

  • JSON 解析型:フロー・モジュールのように設定が定義 JSON に埋もれているもの(再帰探索が必要)
  • 専用 API 型:ルーティングプロファイルとキューの関係や、キューとクイック接続の関係のように、関係を返す API が用意されているもの(解析不要で素直に照合)

後者は AWS の API(ListRoutingProfileQueues や ListQueueQuickConnects など)が関係を直接返してくれるので、JSON を読む必要はありません。「JSON を解析するしかないのはどこか」を見極めるのが設計のコツです。

7. 全体像

ここまでで扱ったリソースの関連を図にすると、次のようになります。実線は順引き(コンソールでも見える方向)、点線が本ツールで実現した逆引きの主な方向です。

まとめ

  • Connect の逆引きが難しいのは、設定が フロー定義 JSON の中に埋もれている から
  • Content は JSON 文字列。JSON.parse して 再帰探索 すれば、ネストや表記揺れに強く拾える
  • ID と ARN の混在は、ARN から ID を抜き出して寄せる 正規化で吸収する
  • 逆引きは「全件取得 → 抽出 → 照合」の型で量産でき、関係を返す専用 API があるものはそれを使う

ただし「全フローを取得して1つずつ解析する」という素朴な実装は、リソースが増えると 大量の API 呼び出しで遅くなり、スロットリングの危険 もあります。次回(最適化編)では、この重い処理を実用的な速度で動かすためのキャッシュ・並列制御・段階的ロードの工夫を解説します。

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?