Amazon Connect を使った IVR(自動音声案内)を運用すると、通話のコンタクトレコードは取得できるものの、「お客様が入力した番号(DTMF)」をコンソール上で一括ダウンロードする機能が無く、個別にコンタクトを開いて確認する必要がある、という課題に直面します。
この記事では、筆者が実際に運用している Lambda 実装(Connect フローで Name に入力番号をセット → Lambda で受け取り S3 に日次 CSV を追記)を紹介し、コードの解説と運用時の注意点、より良い代替案までまとめます。
結論(最短)
- Connect フロー側で入力値を「ContactData.Name」へ渡す設定にすれば、Lambda 側で event.Details.ContactData.Name から取得可能。
- 単純で早い実装は「既存の日次 CSV を GetObject → 行追加 → PutObject」で上書き保存する方法(下記コード参照)。
- ただし同時書き込み(競合)やスケールを考えると、DynamoDB / Kinesis Firehose / S3 に1件ずつオブジェクトを書く方式など、堅牢な設計に改めることを推奨。
※前提として、AmazonConnectのブロックに、顧客の入力を保存するやコンタクト属性の設定を追加して、顧客が番号を入力する必要があります。
サンプルコード
以下はそのまま Lambda(Node.js、AWS SDK v3)に置いて動く主要コードです。細かい説明は後述します。
import { ConnectClient, GetContactAttributesCommand } from "@aws-sdk/client-connect";
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { Readable } from "stream";
//Connect リージョン名 を設定してください。
const connect = new ConnectClient({ region: "" });
//S3 リージョン名 を設定してください。
const s3 = new S3Client({ region: "" });
// S3 GetObject の Body を文字列化
const streamToString = (stream) =>
new Promise((resolve, reject) => {
const chunks = [];
stream.on("data", (chunk) => chunks.push(chunk));
stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
stream.on("error", reject);
});
// 必要情報(固定でOK)
//Connect のインスタンスID を設定してください。または環境変数
//S3 バケット名 を設定してください。または環境変数
const INSTANCE_ID = "";
const BUCKET_NAME = "";
export const handler = async (event) => {
console.log("Event:", event);
// exports.handler = async (event) => {
try {
// --- Connect から渡された通話 ID ---
const contactId = event.Details.ContactData.ContactId;
// --- Connect フローで渡される「Name」には入力番号が格納されている ---
const inputNumber = event.Details.ContactData.Name;
if (!inputNumber) {
// 入力番号が取れない場合はエラー終了
return { status: "error", message: "Input number missing" };
}
// 日付を "2025/09/22 11:51:27" 形式に変換
const formatDate = () => {
return new Date().toLocaleString("ja-JP", {
timeZone: "Asia/Tokyo",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
}).replace(/\//g, "/"); // 明示的にスラッシュで区切り
};
// --- 保存する 1 件分のレコード ---
const record = {
Date: formatDate(), // 例: 2025/09/22 11:52:10
ContactId: contactId,
InputNumber: inputNumber,
};
// --- 日付ごとに 1 ファイルとして管理するためのキー ---
// --- ファイル名は日付(YYYY-MM-DD)単位にする ---
const dateKey = new Date().toISOString().slice(0, 10); // 例: 2025-09-24
const key = `connect-attributes/${dateKey}.csv`;
let records = [];
try {
// --- 既存ファイルを取得して追記する ---
const getRes = await s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: key }));
const body = await streamToString(getRes.Body);
// records = JSON.parse(body);
records = body.split("\n").slice(1).filter(line => line.trim() !== "").map(line => {
const [Date, ContactId, InputNumber] = line.split(",");
return { Date, ContactId, InputNumber };
});
} catch (err) {
// --- ファイルが存在しない場合は新規作成(エラーは握り潰す) ---
console.log("No existing file, creating new one:", err.message);
}
// --- 新しいレコードを追加 ---
records.push(record);
// --- S3 に保存(JSON 配列形式で保存) ---
const csvHeader = "Date,ContactId,InputNumber\n";
const csvRows = records.map(r => `${r.Date},${r.ContactId},${r.InputNumber}`);
const body = csvHeader + csvRows.join("\n");
console.log(">>> Saving to S3:", { Bucket: BUCKET_NAME, Key: key, Body: body });
await s3.send(
new PutObjectCommand({
Bucket: BUCKET_NAME,
Key: key,
Body: body,
ContentType: "text/csv",
})
);
console.log(">>> Save completed:", key);
// --- 正常終了レスポンス ---
return { status: "ok", s3_key: key, count: records.length };
} catch (e) {
// --- エラー処理 ---
console.error(e);
return { status: "error", message: e.message };
}
};JavaScriptnode_modules を同梱する理由と配置ルール
Node の組み込みモジュールや Lambda が標準で提供するライブラリは実行時に既に存在するため node_modules にファイルが無くても動きます。一方、外部パッケージ(今回の @aws-sdk/client-s3 や @aws-sdk/client-connect のような v3 モジュール)はランタイムに入っていないので、デプロイ時に node_modules に同梱する必要があります。
ただし、毎回大きな node_modules をアップロードするとデプロイが重くなるので、Lambda Layer に依存を置くか、esbuild 等でバンドルして依存を縮小する運用も検討してください。どちらも同梱を避ける代替手段として有効です。

配置は、プロジェクトルートにハンドラ(例 index.js/index.mjs)、package.json、node_modules/ を置き、Lambda のハンドラ設定(例 index.handler)と一致させる。
そのフォルダをそのまま ZIP 化してアップロードするか、VS Code の Deploy ボタン等で丸ごとデプロイし、機密値は必ず環境変数で渡す。
コード解説(ポイント別)
- 入力番号の受け渡し(Connect 側)
- Connect の Contact Flow 内で、ユーザーが入力した DTMF(数字)を Set contact attributes や Invoke AWS Lambda ブロックの
Nameフィールドに入れる(例:Name = $.Attributes.input_digits 等)。 - Lambda 実行時に event.Details.ContactData.Name から取り出せる設計にしています(コードでは event.Details.ContactData.Name)。
- Connect の Contact Flow 内で、ユーザーが入力した DTMF(数字)を Set contact attributes や Invoke AWS Lambda ブロックの
- 日次ファイルの設計
- コードは connect-attributes/YYYY-MM-DD.csv の日別ファイルに行を追加する形。簡便で閲覧しやすい一方、同時更新の競合が発生しやすい点に注意(後述)。
- S3 の読み書き
- 既存ファイルを GetObject で取得、文字列にして CSV をパース → 追記 →
PutObjectで上書きして保存します。 - streamToString は GetObject の Body(stream)を文字列化するユーティリティ。
- 既存ファイルを GetObject で取得、文字列にして CSV をパース → 追記 →
- エラー時の扱い
- 既存ファイルが無ければ新規作成というロジック。GetObject が 404 の際はログに出して続行します。
運用上の注意点
以下は実運用でつまずきやすい点です。実装前・運用中のチェックリストとして必ず確認してください。
- 同時書き込み(競合)
- Lambda が複数同時実行されると、GetObject → 追記 → PutObject の間に競合(ラストライトが勝つ)が発生します。結果、一部レコードが消える可能性あり。
- 対処:
- 単純な運用規模なら Lambda の同時実行数を制限する(しかし遅延のリスクあり)。
- DynamoDB(PutItem) を使って原子的に書き込む。
- 各レコードを S3 に個別オブジェクト(connect-attributes/YYYY-MM-DD/<timestamp>-<contactId>.json)として保存し、後で Athena/Glue で集計する。
- Kinesis Firehose を使って S3 に連携する(大量トラフィック向け)。
- S3 に CSV を毎回再生成するコスト
- ファイルが大きくなると毎回のダウンロード/アップロードが重くなる。日次ファイルが数MB〜数十MBを超える場合は分割(時間帯ファイル)や別方式を検討。
トラブルシュート(よくある失敗)
- Lambda に届く event の構造が想定と違う → Connect フローから渡される JSON を CloudWatch で確認してコードを合わせる。
- S3 に保存されない(403) → IAM ロールの権限を確認。バケットポリシーでブロックしていないか確認。
- CSV が壊れる(カンマや改行を含む入力) → 入力値を CSV エスケープする、あるいは JSON 保存に切り替える。
- 大量同時発生でデータ欠損 → 同時書き込み対策(後述の改善案)を検討。
まとめ(要点の再掲)
- Amazon Connect のコンソールから「IVR入力の一括取得」は制約があり、Lambda を用いて Connect のフローから渡された Name(入力番号)を受け取り日次 CSV 等で保存するのが実用的な回避策です。
- 提示したコードはシンプルですぐ動く利点があるものの、同時書き込みの競合やスケーラビリティ、個人情報保護など運用上の注意が必要です。
- 長期運用・高トラフィックを見込む場合は、各レコードを個別オブジェクトにする、DynamoDB や Firehose を使う等の設計変更を強くおすすめします。

