@exabugs/dynamodb-client 1.19.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +1 -1
  3. package/bin/dynamodb-system-change.js +6 -0
  4. package/dist/server/handler.cjs +3 -3
  5. package/dist/server/media-handler.cjs +2 -2
  6. package/dist/server/media-process-handler-dlq.cjs +2 -2
  7. package/dist/server/media-process-handler.cjs +2 -2
  8. package/dist/tools/dynamodb-system-change/cli.d.ts +35 -0
  9. package/dist/tools/dynamodb-system-change/cli.d.ts.map +1 -0
  10. package/dist/tools/dynamodb-system-change/cli.js +226 -0
  11. package/dist/tools/dynamodb-system-change/cli.js.map +1 -0
  12. package/dist/tools/dynamodb-system-change/details.d.ts +42 -0
  13. package/dist/tools/dynamodb-system-change/details.d.ts.map +1 -0
  14. package/dist/tools/dynamodb-system-change/details.js +59 -0
  15. package/dist/tools/dynamodb-system-change/details.js.map +1 -0
  16. package/dist/tools/dynamodb-system-change/index.d.ts +13 -0
  17. package/dist/tools/dynamodb-system-change/index.d.ts.map +1 -0
  18. package/dist/tools/dynamodb-system-change/index.js +9 -0
  19. package/dist/tools/dynamodb-system-change/index.js.map +1 -0
  20. package/dist/tools/dynamodb-system-change/releases.d.ts +34 -0
  21. package/dist/tools/dynamodb-system-change/releases.d.ts.map +1 -0
  22. package/dist/tools/dynamodb-system-change/releases.js +108 -0
  23. package/dist/tools/dynamodb-system-change/releases.js.map +1 -0
  24. package/dist/tools/dynamodb-system-change/run.d.ts +2 -0
  25. package/dist/tools/dynamodb-system-change/run.d.ts.map +1 -0
  26. package/dist/tools/dynamodb-system-change/run.js +6 -0
  27. package/dist/tools/dynamodb-system-change/run.js.map +1 -0
  28. package/dist/tools/dynamodb-system-change/send.d.ts +43 -0
  29. package/dist/tools/dynamodb-system-change/send.d.ts.map +1 -0
  30. package/dist/tools/dynamodb-system-change/send.js +97 -0
  31. package/dist/tools/dynamodb-system-change/send.js.map +1 -0
  32. package/docs/DYNAMODB_ACCESS.md +1 -1
  33. package/docs/NOTIFY.md +2 -2
  34. package/docs/SYSTEM_CHANGES.md +68 -1
  35. package/package.json +7 -2
  36. package/terraform/README.md +9 -0
  37. package/terraform/alerts-cleanup/README.md +65 -0
  38. package/terraform/alerts-cleanup/main.tf +111 -0
  39. package/terraform/alerts-cleanup/outputs.tf +14 -0
  40. package/terraform/alerts-cleanup/variables.tf +42 -0
  41. package/terraform/alerts-cleanup/versions.tf +16 -0
  42. package/terraform/ops-notify/README.md +109 -0
  43. package/terraform/ops-notify/main.tf +295 -0
  44. package/terraform/ops-notify/outputs.tf +19 -0
  45. package/terraform/ops-notify/us-east-1.tf +68 -0
  46. package/terraform/ops-notify/variables.tf +105 -0
  47. package/terraform/ops-notify/versions.tf +18 -0
  48. package/terraform/system-change-recorder/README.md +71 -0
  49. package/terraform/system-change-recorder/main.tf +122 -0
  50. package/terraform/system-change-recorder/outputs.tf +14 -0
  51. package/terraform/system-change-recorder/variables.tf +53 -0
  52. package/terraform/system-change-recorder/versions.tf +16 -0
@@ -0,0 +1,108 @@
1
+ /**
2
+ * terraform の plan / apply のログから、Lambda のコードのリリース(作成・置き換え・コードの更新)の成功・失敗を取り出す。
3
+ *
4
+ * asanowa(ADR 0044)・astro(ADR-0011)の `infra/scripts/parse-lambda-releases.py` を Node に移植したもの
5
+ * (両アプリの版は、ロジックが同一。違いは docstring だけ)。出力は Python 版と同じ(テストで固定)。
6
+ *
7
+ * 取り出す条件(Terraform 1.x と AWS プロバイダ 6.x の実際の出力で確認済み):
8
+ * - 計画の `# <アドレス> will be created / must be replaced / will be replaced, as requested / will be updated in-place`
9
+ * のうち、aws_lambda_function で、作成・置き換え、または更新で `source_code_hash` の差分があるもの(= コードが変わる)。
10
+ * 設定だけの更新(環境変数・同時実行数など)は、対象外
11
+ * - 成功: `<アドレス>: Modifications complete after … [id=<関数名>]`(更新)/ `Creation complete after … [id=<関数名>]`(作成)
12
+ * - 失敗: `Error: creating Lambda Function (<名前>)` / `Error: updating Lambda Function (<名前>) code` /
13
+ * `Error: waiting for Lambda Function (<名前>) code update|create`(設定の更新の失敗は、対象外)
14
+ */
15
+ /** 取り出す上限(Python 版の MAX_ITEMS) */
16
+ export const EXTRACT_MAX_ITEMS = 50;
17
+ /** 色コード(Python 版の ANSI) */
18
+ export const ANSI_PATTERN = /\x1b\[[0-9;]*m/g;
19
+ const ADDR = String.raw `\S*aws_lambda_function\.[A-Za-z0-9_]+(?:\[[^\]\n]*\])?`;
20
+ // 見出しの言い方: will be created / will be updated in-place / must be replaced / will be replaced, as requested(-replace)/
21
+ // is tainted, so must be replaced / will be destroyed
22
+ const HEADER = new RegExp(String.raw `^\s*# (` +
23
+ ADDR +
24
+ String.raw `) (will be created|will be updated in-place|will be destroyed|` +
25
+ String.raw `(?:is tainted, so )?must be replaced|will be replaced, as requested)`);
26
+ const ANY_HEADER = /^\s*# \S+ (?:will|must|is) /;
27
+ const DONE = new RegExp('^(' + ADDR + String.raw `): (Modifications|Creation) complete after \S+ \[id=([^\]\s]+)\]`);
28
+ const FAILED = [
29
+ [/Error: creating Lambda Function \(([^)\s]+)\)/, 'create'],
30
+ [/Error: updating Lambda Function \(([^)\s]+)\) code/, 'code'],
31
+ [/Error: waiting for Lambda Function \(([^)\s]+)\) code update/, 'code'],
32
+ [/Error: waiting for Lambda Function \(([^)\s]+)\) create/, 'create'],
33
+ ];
34
+ /**
35
+ * Python の `str.splitlines()` と同じ区切りで行に分ける(\r\n、\n、\r、\v、\f、\x1c〜\x1e、\x85、U+2028、U+2029)。
36
+ * 末尾の区切りの後ろに空の行を作らない
37
+ */
38
+ export function splitLines(text) {
39
+ const lines = text.split(/\r\n|[\n\r\v\f\x1c\x1d\x1e\x85\u2028\u2029]/);
40
+ if (lines.length > 0 && lines[lines.length - 1] === '')
41
+ lines.pop();
42
+ return lines;
43
+ }
44
+ /** 色コードを除く */
45
+ export function stripAnsi(text) {
46
+ return text.replace(ANSI_PATTERN, '');
47
+ }
48
+ /** 計画の見出しから「コードが変わる Lambda のアドレス → 変更の種類」を作る */
49
+ function codeChangesOf(lines) {
50
+ const changes = new Map();
51
+ let i = 0;
52
+ while (i < lines.length) {
53
+ const m = HEADER.exec(lines[i]);
54
+ if (!m) {
55
+ i += 1;
56
+ continue;
57
+ }
58
+ const [, addr, action] = m;
59
+ let j = i + 1;
60
+ while (j < lines.length &&
61
+ !ANY_HEADER.test(lines[j]) &&
62
+ !lines[j].startsWith('Plan:') &&
63
+ !lines[j].startsWith('Do you want')) {
64
+ j += 1;
65
+ }
66
+ const block = lines.slice(i + 1, j);
67
+ if (action === 'will be created')
68
+ changes.set(addr, 'create');
69
+ else if (action.includes('replaced'))
70
+ changes.set(addr, 'replace');
71
+ else if (action === 'will be updated in-place' &&
72
+ block.some((l) => l.includes('source_code_hash'))) {
73
+ changes.set(addr, 'code');
74
+ }
75
+ i = j;
76
+ }
77
+ return changes;
78
+ }
79
+ /** ログから Lambda のリリースを取り出す(該当が無ければ空の配列。最大 EXTRACT_MAX_ITEMS 件) */
80
+ export function extractLambdaReleases(text) {
81
+ const lines = splitLines(text).map(stripAnsi);
82
+ const codeChanges = codeChangesOf(lines);
83
+ const out = [];
84
+ const seen = new Set();
85
+ for (const raw of lines) {
86
+ const line = raw.replace(/^[│ ]+/, '').trimEnd();
87
+ const done = DONE.exec(line);
88
+ const change = done ? codeChanges.get(done[1]) : undefined;
89
+ if (done && change) {
90
+ const name = done[3];
91
+ if (!seen.has(name)) {
92
+ seen.add(name);
93
+ out.push({ target: name, result: 'succeeded', change });
94
+ }
95
+ continue;
96
+ }
97
+ for (const [rx, failedChange] of FAILED) {
98
+ const m = rx.exec(line);
99
+ if (m && !seen.has(m[1])) {
100
+ seen.add(m[1]);
101
+ out.push({ target: m[1], result: 'failed', change: failedChange });
102
+ break;
103
+ }
104
+ }
105
+ }
106
+ return out.slice(0, EXTRACT_MAX_ITEMS);
107
+ }
108
+ //# sourceMappingURL=releases.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"releases.js","sourceRoot":"","sources":["../../../src/tools/dynamodb-system-change/releases.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/tools/dynamodb-system-change/run.ts"],"names":[],"mappings":""}
@@ -0,0 +1,6 @@
1
+ import { main } from './cli.js';
2
+ process.exitCode = main(process.argv.slice(2), {
3
+ out: (s) => process.stdout.write(s),
4
+ err: (s) => process.stderr.write(s),
5
+ });
6
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../../../src/tools/dynamodb-system-change/run.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAEhC,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IAC7C,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACnC,GAAG,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;CACpC,CAAC,CAAC"}
@@ -0,0 +1,43 @@
1
+ /** 外部コマンドの実行(テストで差し替える) */
2
+ export type RunCommand = (command: string, args: string[], options?: {
3
+ cwd?: string;
4
+ }) => {
5
+ status: number | null;
6
+ stdout: Buffer;
7
+ stderr: Buffer;
8
+ error?: Error;
9
+ };
10
+ export declare const runCommand: RunCommand;
11
+ /** 記録 Lambda に送る本文(記録 Lambda の入力。キーの順は現行と同じ) */
12
+ export interface SystemChangePayload {
13
+ kind: string;
14
+ result: 'started' | 'succeeded' | 'failed';
15
+ target: string;
16
+ commit: string;
17
+ actor: string;
18
+ details: Record<string, unknown>;
19
+ changeId?: string;
20
+ }
21
+ /**
22
+ * Python の `json.dumps(x, ensure_ascii=False)` と同じ文字列(区切りは `, ` と `: `)。
23
+ * 送る内容(--payload)と、RECORD_DRY_RUN の表示を、現行と一字一句同じにするため
24
+ */
25
+ export declare function pythonJsonDumps(value: unknown): string;
26
+ /**
27
+ * コミット(`git rev-parse --short HEAD`。取れなければ unknown)。dirtyPaths に未コミットの変更があれば `-dirty`
28
+ * (`git diff-index --quiet HEAD -- <paths>` が 0 以外。git が無い・失敗も -dirty。現行と同じ)
29
+ */
30
+ export declare function commitOf(repo: string | undefined, dirtyPaths: string[], run?: RunCommand): string;
31
+ /** 実行者(`$(whoami)@$(hostname -s)`) */
32
+ export declare function actorOf(): string;
33
+ export interface SendOptions {
34
+ functionName: string;
35
+ region: string;
36
+ dryRun: boolean;
37
+ /** 警告の出力先(標準エラー。バイト列も受ける) */
38
+ warn: (message: string | Buffer) => void;
39
+ run?: RunCommand;
40
+ }
41
+ /** 記録 Lambda を呼ぶ。失敗は警告だけ(例外にしない)。記録 Lambda が無い環境は黙ってスキップ */
42
+ export declare function sendSystemChange(payload: SystemChangePayload, options: SendOptions): void;
43
+ //# sourceMappingURL=send.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"send.d.ts","sourceRoot":"","sources":["../../../src/tools/dynamodb-system-change/send.ts"],"names":[],"mappings":"AAUA,2BAA2B;AAC3B,MAAM,MAAM,UAAU,GAAG,CACvB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,CAAC,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,KACvB;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,KAAK,CAAA;CAAE,CAAC;AAE9E,eAAO,MAAM,UAAU,EAAE,UAQxB,CAAC;AAEF,gDAAgD;AAChD,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,CAAC;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAOtD;AAED;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,GAAG,aAAa,GAAG,MAAM,CAOjG;AAED,sCAAsC;AACtC,wBAAgB,OAAO,IAAI,MAAM,CAEhC;AAED,MAAM,WAAW,WAAW;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,6BAA6B;IAC7B,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,KAAK,IAAI,CAAC;IACzC,GAAG,CAAC,EAAE,UAAU,CAAC;CAClB;AAYD,4DAA4D;AAC5D,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,mBAAmB,EAAE,OAAO,EAAE,WAAW,GAAG,IAAI,CAsCzF"}
@@ -0,0 +1,97 @@
1
+ /**
2
+ * 記録 Lambda(`<app>-<env>-system-change-recorder`)への送信(両アプリの terraform-wrapper.sh の record_change を
3
+ * 移植したもの)。
4
+ *
5
+ * 経路は現行のまま: AWS CLI の `aws lambda invoke`(認証は AWS CLI の認証情報。`AWS_PROFILE` などは環境変数で
6
+ * 引き継ぐ)。記録の失敗は、警告を出すだけで、例外にしない(apply・destroy を止めない)。
7
+ */
8
+ import { spawnSync } from 'node:child_process';
9
+ import { hostname, userInfo } from 'node:os';
10
+ export const runCommand = (command, args, options) => {
11
+ const r = spawnSync(command, args, { cwd: options?.cwd, stdio: ['ignore', 'pipe', 'pipe'] });
12
+ return {
13
+ status: r.status,
14
+ stdout: r.stdout ?? Buffer.alloc(0),
15
+ stderr: r.stderr ?? Buffer.alloc(0),
16
+ error: r.error,
17
+ };
18
+ };
19
+ /**
20
+ * Python の `json.dumps(x, ensure_ascii=False)` と同じ文字列(区切りは `, ` と `: `)。
21
+ * 送る内容(--payload)と、RECORD_DRY_RUN の表示を、現行と一字一句同じにするため
22
+ */
23
+ export function pythonJsonDumps(value) {
24
+ if (Array.isArray(value))
25
+ return `[${value.map(pythonJsonDumps).join(', ')}]`;
26
+ if (value !== null && typeof value === 'object') {
27
+ const entries = Object.entries(value).filter(([, v]) => v !== undefined);
28
+ return `{${entries.map(([k, v]) => `${JSON.stringify(k)}: ${pythonJsonDumps(v)}`).join(', ')}}`;
29
+ }
30
+ return JSON.stringify(value);
31
+ }
32
+ /**
33
+ * コミット(`git rev-parse --short HEAD`。取れなければ unknown)。dirtyPaths に未コミットの変更があれば `-dirty`
34
+ * (`git diff-index --quiet HEAD -- <paths>` が 0 以外。git が無い・失敗も -dirty。現行と同じ)
35
+ */
36
+ export function commitOf(repo, dirtyPaths, run = runCommand) {
37
+ const head = run('git', ['rev-parse', '--short', 'HEAD'], { cwd: repo });
38
+ let commit = head.status === 0 && !head.error ? head.stdout.toString('utf8').replace(/\n+$/, '') : 'unknown';
39
+ const diff = run('git', ['diff-index', '--quiet', 'HEAD', '--', ...dirtyPaths], { cwd: repo });
40
+ if (diff.status !== 0 || diff.error)
41
+ commit = `${commit}-dirty`;
42
+ return commit;
43
+ }
44
+ /** 実行者(`$(whoami)@$(hostname -s)`) */
45
+ export function actorOf() {
46
+ return `${userInfo().username}@${hostname().split('.')[0]}`;
47
+ }
48
+ const PREFIX = '[change-record]';
49
+ const NL = Buffer.from('\n');
50
+ /** 末尾の改行(0x0a)を除く(bash の `$(...)` と同じ) */
51
+ function trimTrailingNewlines(buf) {
52
+ let end = buf.length;
53
+ while (end > 0 && buf[end - 1] === 0x0a)
54
+ end -= 1;
55
+ return buf.subarray(0, end);
56
+ }
57
+ /** 記録 Lambda を呼ぶ。失敗は警告だけ(例外にしない)。記録 Lambda が無い環境は黙ってスキップ */
58
+ export function sendSystemChange(payload, options) {
59
+ const body = pythonJsonDumps(payload);
60
+ if (options.dryRun) {
61
+ options.warn(`${PREFIX}[dry-run] ${options.functionName} <- ${body}\n`);
62
+ return;
63
+ }
64
+ const run = options.run ?? runCommand;
65
+ const r = run('aws', [
66
+ 'lambda',
67
+ 'invoke',
68
+ '--function-name',
69
+ options.functionName,
70
+ '--region',
71
+ options.region,
72
+ '--cli-binary-format',
73
+ 'raw-in-base64-out',
74
+ '--cli-connect-timeout',
75
+ '5',
76
+ '--cli-read-timeout',
77
+ '30',
78
+ '--payload',
79
+ body,
80
+ '/dev/null',
81
+ ]);
82
+ // 現行は `out="$(… 2>&1)"`(標準出力と標準エラーをまとめ、末尾の改行を除く)
83
+ const errText = r.error ? Buffer.from(`${r.error.message}\n`) : Buffer.alloc(0);
84
+ const out = trimTrailingNewlines(Buffer.concat([r.stdout, r.stderr, errText]));
85
+ const text = out.toString('utf8');
86
+ if (r.status !== 0 || r.error) {
87
+ if (text.includes('ResourceNotFoundException'))
88
+ return; // 記録 Lambda が無い環境は、スキップ
89
+ // 現行は `$(echo "$out" | head -c 200)`(バイトで 200。末尾の改行は $(...) が除く)
90
+ const head = trimTrailingNewlines(Buffer.concat([out, NL]).subarray(0, 200));
91
+ options.warn(Buffer.concat([Buffer.from(`${PREFIX} 記録に失敗しました(処理は続行): `), head, NL]));
92
+ }
93
+ else if (text.includes('"FunctionError"')) {
94
+ options.warn(`${PREFIX} 記録 Lambda がエラーを返しました(処理は続行)\n`);
95
+ }
96
+ }
97
+ //# sourceMappingURL=send.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"send.js","sourceRoot":"","sources":["../../../src/tools/dynamodb-system-change/send.ts"],"names":[],"mappings":"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"}
@@ -138,7 +138,7 @@ ESLint は、エディタでも違反をその場で示す。CLI は ESLint の
138
138
 
139
139
  ## 許可リストの種別と約束
140
140
 
141
- - **恒久(permanent)**: 構造上必要な例外。このライブラリでは、Records の本体、Records が使う共有部品、別テーブル `notices`(`src/notify/store.ts`)。`notices` は例外ではなく原則の**対象外**(構造が違う別テーブルで、通知の重複抑制・流量制限の内部用。ビジネスのデータではない)。検査が IAM をテーブルごとに区別できないため permanent として載せる(ADR 0005)
141
+ - **恒久(permanent)**: 構造上必要な例外。このライブラリでは、Records の本体、Records が使う共有部品、別テーブル `notices`(`src/notify/store.ts` と、通知 Lambda のロール `terraform/ops-notify` の `UpdateItem`)。`notices` は例外ではなく原則の**対象外**(構造が違う別テーブルで、通知の重複抑制・流量制限の内部用。ビジネスのデータではない)。検査が IAM をテーブルごとに区別できないため permanent として載せる(ADR 0005)
142
142
  - **負債(debt)**: 期限つき。消す作業が決まっている
143
143
 
144
144
  運用の約束:
package/docs/NOTIFY.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # 運用通知(`@exabugs/dynamodb-client/notify`)
2
2
 
3
- ログ(`warn` 以上)・CloudWatch アラーム・直接呼び出しを、管理者へ通知するための**部品**。配線(購読フィルタ・Lambda・Terraform・秘密の置き場)は、各アプリが行う。既存の挙動は変えない(サブパス `./notify` の追加のみ)。
3
+ ログ(`warn` 以上)・CloudWatch アラーム・直接呼び出しを、管理者へ通知するための**部品**。配線のうち、Lambda のエントリと秘密の置き場は各アプリが行う。購読フィルタ・Lambda・テーブルの Terraform は `terraform/ops-notify`(1.21.0)。既存の挙動は変えない(サブパス `./notify` の追加のみ)。
4
4
 
5
5
  ```text
6
6
  ログ(購読フィルタ) ─┐
@@ -177,7 +177,7 @@ export const handler = createNotifyHandler({
177
177
  - 環境変数: 既存の `NOTICE_TABLE` / `RECORDS_IAM_URL` に加えて、`ADMIN_BASE_URL`(任意。無ければ URL を載せない)
178
178
  - IAM: 通知ストアへは `dynamodb:UpdateItem` だけ(既存の権限のまま。`GetItem` は要らない)。Records の IAM URL の呼び出し(`lambda:InvokeFunctionUrl`)は、管理者の一覧の取得で既に持っている
179
179
  - 同時実行数: 同じレコードへの同時の書き込みはシャドーを孤立させうるので、**記録を有効にするときだけ**、通知 Lambda の予約同時実行数を 1 にする(今の通知 Lambda は未設定。入れると、記録なしでも遅れ方が変わる)。直列化は努力目標で、強制はされない(打ち切った HTTP は止まらない・削除の定時処理と重なる・管理画面から書ける)。孤立したシャドーは `src/scripts/repair-shadows.ts` で修復できる
180
- - ライブラリは Terraform を提供しない(`alerts` の定時削除・同時実行数・環境変数は各アプリの infra で)
180
+ - Terraform は `terraform/ops-notify`・`terraform/alerts-cleanup`(1.21.0。[README](../terraform/ops-notify/README.md))。Lambda の成果物(エントリ)と、秘密の置き場は各アプリ
181
181
 
182
182
  ### 削除(定時 Lambda)
183
183
 
@@ -104,10 +104,77 @@ export const handler = createSystemChangeHandler({
104
104
  - 管理画面の一覧・詳細は `SystemChangesList` / `SystemChangesShow`(`@exabugs/dynamodb-client/integrations/react-admin`。1.19.0。[react-admin-integration.md](react-admin-integration.md#運用の画面alertssystemchanges))
105
105
  - 投稿は notify の `postSlackText`(`createSlackChannel` と同じ送信。URL の接頭辞の確認、URL を出さないエラー)
106
106
 
107
+ ## 記録の CLI(`dynamodb-system-change`。1.20.0)
108
+
109
+ terraform の apply・destroy の開始と終了を、記録 Lambda(`<app>-<env>-system-change-recorder`)に送る CLI([ADR 0006](adr/0006-library-owns-table-operations.md) の段階 5 = (c))。asanowa・astro の `terraform-wrapper.sh` の「記録に送る部分」(開始・終了の呼び出し、`details` の組み立て、`parse-lambda-releases.py`)を Node にした。**bash のラッパーは各アプリに残し**(state のバックエンド、plan の保存と確認、環境の検査はアプリごとに違う)、記録だけをこの CLI に委ねる。
110
+
111
+ - 経路は従来のまま: AWS CLI の `aws lambda invoke`(`--cli-connect-timeout 5 --cli-read-timeout 30`。認証は AWS CLI の認証情報で、`AWS_PROFILE` などは環境変数で引き継ぐ)。送る本文は従来の Python の `json.dumps` と同じ文字列
112
+ - **記録の失敗で apply を止めない**: `start` / `finish` は、記録の失敗・引数の誤りでも終了コード 0(警告は標準エラーに `[change-record] …`)。記録 Lambda が無い環境(`ResourceNotFoundException`)は黙ってスキップ。サブコマンドの誤りだけ 2
113
+ - 受け渡し: `start` は変更 ID(ULID)を標準出力に 1 行で出す(記録の成否によらず。`SKIP_CHANGE_RECORD=1` では何も出さない)。ラッパーはシェルの変数に持ち、`finish --change-id` に渡す
114
+ - 環境変数は従来と同じ: `RECORD_DRY_RUN=1`(呼ばずに、送る内容を表示)、`SKIP_CHANGE_RECORD=1`(記録しない)、`AWS_REGION`(無ければ `ap-northeast-1`)
115
+
116
+ ```text
117
+ dynamodb-system-change start --app <app> --env <env> --command <cmd> [共通]
118
+ dynamodb-system-change finish --app <app> --env <env> --command <cmd> --exit-code <n> [--change-id <id>] [--log <file>] [共通]
119
+ 共通: [--repo <dir>] [--function <name>] [--target <target>] [--region <region>]
120
+ [--releases-max-items <n|none>] [--releases-max-bytes <n|none>] [--dirty-path <path>...]
121
+ ```
122
+
123
+ | 引数 | 内容 |
124
+ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
125
+ | `--app` / `--env` | `details.app` / `details.env`。既定の関数名 `<app>-<env>-system-change-recorder` と対象 `terraform:<app>` にも使う |
126
+ | `--command` | `details.command`(`apply` / `apply-auto` / `destroy` など)。`destroy` では Lambda のリリースを取り出さない |
127
+ | `--exit-code` | terraform の終了コード。0 は `succeeded`、それ以外は `failed` と `details.exit_code` |
128
+ | `--log` | plan / apply のログ。成功なら最後の完了の行から `add` / `change` / `destroy`、Lambda のリリースから `lambdaReleases` |
129
+ | `--repo` | git を実行するディレクトリ(既定: カレント)。`commit` は `git rev-parse --short HEAD`(取れなければ `unknown`) |
130
+ | `--dirty-path` | 未コミットの変更を調べる場所(`git diff-index --quiet HEAD -- <path>...`)。あれば `commit` に `-dirty`。最後に書く |
131
+ | `--releases-max-items` | `lambdaReleases` の件数の上限(既定 `none`) |
132
+ | `--releases-max-bytes` | `lambdaReleases` の JSON のバイト数の上限(既定 3000。details の上限 4096 に収める)。失敗を先に残し、省いた件数は `lambdaReleasesOmitted` |
133
+
134
+ `actor` は `<ユーザー名>@<ホスト名の先頭>`(従来の `$(whoami)@$(hostname -s)`)。
135
+
136
+ ### ラッパーからの呼び方
137
+
138
+ `set -euo pipefail` のラッパーでも止まらないよう、呼び出しには `|| true` を付ける(CLI が入っていない・node が無いときも apply を進める)。CLI が無いときは、記録が欠けたことに気づけるよう警告を出す。記録の部分は次のとおり(`$CHANGE_LOG` と `trap` の扱い、plan の確認はアプリのまま。`<…>` はアプリごとの値):
139
+
140
+ ```bash
141
+ SYSTEM_CHANGE="<node_modules の .bin>/dynamodb-system-change" # npx は標準出力に余計な行を出しうるので使わない
142
+ CHANGE_ARGS=(--app "<アプリ名>" --env "$ENV" --command "$COMMAND" [--repo "<git を実行する場所>"] --dirty-path <調べる場所>...)
143
+
144
+ begin_change() {
145
+ [ -x "$SYSTEM_CHANGE" ] || echo "[change-record] CLI がありません(記録をスキップ)" >&2
146
+ CHANGE_ID="$("$SYSTEM_CHANGE" start "${CHANGE_ARGS[@]}" || true)"
147
+ CHANGE_OPEN=1
148
+ }
149
+
150
+ finish_change() {
151
+ local rc=$?
152
+ trap - EXIT
153
+ if [ "$CHANGE_OPEN" = "1" ]; then
154
+ CHANGE_OPEN=0
155
+ "$SYSTEM_CHANGE" finish "${CHANGE_ARGS[@]}" --change-id "$CHANGE_ID" --exit-code "$rc" --log "$CHANGE_LOG" || true
156
+ fi
157
+ rm -f "$CHANGE_LOG"
158
+ return "$rc"
159
+ }
160
+ ```
161
+
162
+ アプリごとの `CHANGE_ARGS`(従来の記録と同じ commit・details になる。`lambdaReleases` の上限は、両アプリとも既定の 3000 バイトを使う):
163
+
164
+ - **asanowa**: `--app "$APP" --env "$ENV" --command "$COMMAND" --dirty-path "$INFRA_ROOT" "$INFRA_ROOT/../functions" "$INFRA_ROOT/../packages"`(`--repo` は不要。ラッパーが `cd` した先で git を実行する)
165
+ - **astro**: `--app "$PROJECT" --env "$ENV" --command "$COMMAND" --repo "$REPO_ROOT" --dirty-path infra apps functions packages`。上限は asanowa の既定(3000 バイト)に揃える(従来の「30 件まで」はやめる。件数の上限は付けない。64 文字の名前が並ぶと 30 件 → 約 27 件になり、超えた分は `lambdaReleasesOmitted` に残る)
166
+
167
+ `--releases-max-items` / `--releases-max-bytes` は、既定と違う上限が要るときだけ使う。
168
+
169
+ 置き換えた後は、`record_change`・`new_change_id`・`finish_change` の Python と `parse-lambda-releases.py` を消せる。両アプリの `test-terraform-wrapper.sh` は、偽の `aws` を PATH に置く方式なので、そのまま使える(1.20.0 の開発時に、両アプリの検査がこの形のラッパーで全て通ることを確かめた)。
170
+
171
+ 部品(`@exabugs/dynamodb-client/system-changes/cli`): `extractLambdaReleases(logText)`(`parse-lambda-releases.py` の移植。出力は同じ)、`buildFinishDetails` / `limitReleases` / `resourceCountsOf`、`main(argv, io, deps?)`。
172
+
107
173
  ## テスト
108
174
 
109
175
  - 単体: `src/system-changes/__tests__/`。ハンドラは、本物の `DynamoClient` に `fetch` だけを差し替えて固定する(`connect()` の呼び忘れを見逃さない)
110
176
  - 統合(DynamoDB Local。CI には入っていない): `src/system-changes/__tests__/system-changes.integration.test.ts`(実行方法は PUBLISHING.md)
177
+ - CLI: `__tests__/tools/dynamodb-system-change-*.test.ts`。Lambda のリリースの取り出しは、両アプリの本物のログ(`__tests__/tools/testdata/terraform-logs`。アカウント ID・URL・Cognito のユーザープール ID・ドメインは伏せた)で Python 版と同じ出力を固定する。送り方は偽の `aws` / `git`、止めないことは `set -euo pipefail` の bash のラッパーから CLI をプロセスとして呼んで確かめる
111
178
 
112
179
  ## アプリの移行(公開後の別作業)
113
180
 
@@ -116,4 +183,4 @@ ADR 0006 の段階 2 の手順: astro で先に置き換えて dev で確認し
116
183
  - Slack の文言は asanowa に揃えた。astro の「記録を省いた Lambda」は「ほかに記録しきれなかった Lambda」になる
117
184
  - Slack の送信は `postSlackText` を通る: webhook の URL が `https://hooks.slack.com/` で始まらないと送らない(`slack.urlPrefix`)。失敗の warn の理由は従来と同じ `HTTP 500` やエラー名(時間切れは `AbortError` ではなく `TimeoutError`)
118
185
  - asanowa の `changeId` の読み取りは `findOne`(404 を null にする)から `find({ id }, { sort: { id: 'asc' }, limit: 1 })`(Records の ID の経路。PK/SK の Query 1 回) に変わる。Records の ITEM_NOT_FOUND の warn が出なくなる
119
- - 記録 Lambda の Terraform(`source_file`)は、アプリのバンドルのまま(Terraform のモジュール化は段階 6)
186
+ - 記録 Lambda の Terraform は `terraform/system-change-recorder`(1.21.0。段階 6。[README](../terraform/system-change-recorder/README.md))。成果物はアプリのバンドルのまま(`source_file` で渡す)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exabugs/dynamodb-client",
3
- "version": "1.19.0",
3
+ "version": "1.21.0",
4
4
  "description": "DynamoDB Single-Table Client SDK with MongoDB-like API, Shadow Records, and Lambda implementation for serverless applications",
5
5
  "author": "exabugs",
6
6
  "license": "MIT",
@@ -8,7 +8,8 @@
8
8
  "main": "./dist/index.js",
9
9
  "types": "./dist/index.d.ts",
10
10
  "bin": {
11
- "dynamodb-access-check": "./bin/dynamodb-access-check.js"
11
+ "dynamodb-access-check": "./bin/dynamodb-access-check.js",
12
+ "dynamodb-system-change": "./bin/dynamodb-system-change.js"
12
13
  },
13
14
  "keywords": [
14
15
  "dynamodb",
@@ -129,6 +130,10 @@
129
130
  "./system-changes": {
130
131
  "import": "./dist/system-changes/index.js",
131
132
  "types": "./dist/system-changes/index.d.ts"
133
+ },
134
+ "./system-changes/cli": {
135
+ "import": "./dist/tools/dynamodb-system-change/index.js",
136
+ "types": "./dist/tools/dynamodb-system-change/index.d.ts"
132
137
  }
133
138
  },
134
139
  "peerDependencies": {
@@ -219,6 +219,15 @@ make logs-records ENV=dev
219
219
  aws logs tail /aws/lambda/my-project-dev-records --follow
220
220
  ```
221
221
 
222
+ ## 運用のモジュール(1.21.0)
223
+
224
+ Records のクライアントとして動く運用の Lambda を、別のモジュールとして持つ(ADR 0006 の (b))。Lambda の成果物(エントリ)はアプリが作り、`source_file` で渡す。
225
+
226
+ - [ops-notify](ops-notify/README.md): 運用通知(ログ・アラーム・生存確認 → Slack とメール。alerts の記録)
227
+ - [alerts-cleanup](alerts-cleanup/README.md): alerts の定時削除
228
+ - [system-change-recorder](system-change-recorder/README.md): システム変更履歴の記録
229
+ - [media](media/README.md): メディア(アップロード・処理・配信)
230
+
222
231
  ## 関連ドキュメント
223
232
 
224
233
  - [パッケージREADME](../README.md) - クライアントライブラリの使用方法
@@ -0,0 +1,65 @@
1
+ # alerts の定時削除 Lambda Terraformモジュール
2
+
3
+ ADR 0006 の (b)(1.21.0)。asanowa・astro の `infra/modules/alerts-cleanup` を 1 つにしたもの。使い方の詳細は [docs/NOTIFY.md](../../docs/NOTIFY.md) の「削除(定時 Lambda)」。
4
+
5
+ ## 作るもの
6
+
7
+ - Lambda `<project>-<env>-alerts-cleanup`(Node.js 22、arm64、120 秒、256 MB。構造化ログ JSON)と、そのロググループ・実行ロール
8
+ - EventBridge の定時ルール(既定 毎日 03:00 JST)とターゲット(再試行 0)、呼び出しの許可
9
+ - 権限: Records の IAM 版 URL の呼び出しだけ。DynamoDB には直接触れない
10
+ - 環境変数: `PROJECT_NAME`(`createAlertsCleanupHandler` の `app` の既定)、`RECORDS_IAM_URL`
11
+
12
+ ## 前提
13
+
14
+ - 参照は `node_modules` のパス(版は npm の版で固定される。git の `ref` ではない)。例: アプリの infra が `infra/` なら `source = "../node_modules/@exabugs/dynamodb-client/terraform/alerts-cleanup"`、`infra/apps/<app>/` なら `"../../../node_modules/..."`
15
+ - **Lambda の成果物はアプリが作り、`source_file`(バンドル済みの 1 ファイル)と `handler` で渡す**。エントリはアプリの数行(`createAlertsCleanupHandler`。アプリ名や、権限チェック層を通すアプリの包み方を渡す)なので、ライブラリの dist は使わない
16
+ - zip は `${path.root}/.terraform/archives/alerts-cleanup.zip` に作る(`path.module` を使わない。pnpm の版つきのパスが state に入ると、版を上げるたびに差分が出るため)
17
+ - pnpm では、版を上げた後に `.terraform/modules` を消してから `terraform init` する([../README.md](../README.md) の注意)
18
+ - モジュール全体の `depends_on` は使わない(依存先に変更があると、中のデータソースが apply 時の読み取りになり、既存のリソースが作り直しになる)。依存は、出力の値(`log_group_name` など)で渡す
19
+
20
+ ## 使い方
21
+
22
+ ```hcl
23
+ module "alerts_cleanup" {
24
+ source = "../node_modules/@exabugs/dynamodb-client/terraform/alerts-cleanup"
25
+
26
+ project_name = var.project_name
27
+ environment = var.environment
28
+ records_iam_function_url = module.records.iam_function_url
29
+ records_iam_alias_arn = module.records.iam_alias_arn
30
+ log_retention_days = var.log_retention_days
31
+
32
+ source_file = "${path.root}/../apps/server/dist/alerts-cleanup.cjs"
33
+ handler = "alerts-cleanup.handler"
34
+ }
35
+ ```
36
+
37
+ ## 変数
38
+
39
+ | 変数 | 既定 | 説明 |
40
+ | ---------------------------------------------------- | -------------------- | ------------------------------------------------------- |
41
+ | `project_name` / `environment` | 必須 | 名前の接頭辞 `<project>-<env>`。環境変数 `PROJECT_NAME` |
42
+ | `records_iam_function_url` / `records_iam_alias_arn` | 必須 | Records の IAM 版 Function URL と、そのエイリアスの ARN |
43
+ | `source_file` / `handler` | 必須 | アプリのバンドル(1 ファイル)と、ハンドラ |
44
+ | `schedule_expression` | `cron(0 18 * * ? *)` | 実行間隔(毎日 03:00 JST) |
45
+ | `log_retention_days` | 7 | ロググループの保持日数 |
46
+
47
+ ## 出力
48
+
49
+ `function_name`、`function_arn`、`log_group_name`(ops-notify に購読させる)
50
+
51
+ ## アプリに残るもの
52
+
53
+ - 上の module ブロック(`source`・`source_file`・`handler` と設定の値)
54
+ - Lambda のエントリ(`createAlertsCleanupHandler` を呼ぶ数行)とバンドルの作成(esbuild など)
55
+ - 秘密(webhook の URL など)の SSM パラメータの作成
56
+
57
+ ## アプリのモジュールからの移行(置き換えを起こさない)
58
+
59
+ リソースのアドレス・名前は、asanowa / astro の旧モジュール(`infra/modules/alerts-cleanup`)と同じ。**module ブロックの名前(`module "alerts_cleanup"`)を変えずに `source` だけを差し替え**、`source_file`・`handler` を足す。アドレスが変わらないので `moved` は要らない(ラッパーのモジュールで包むと、アドレスが変わり `moved` が要る)。
60
+
61
+ 1. `terraform init`(モジュールの取り直し)→ `terraform plan`
62
+ 2. **0 to add / 0 to destroy、`must be replaced` が無い**ことを確かめる。出てよいのは in-place の更新だけ(文言の差。astro ではルールの description が英語の文言に、Lambda に `PROJECT_NAME` が加わる)
63
+ 3. 確認のうえ apply。旧モジュールのディレクトリを消す
64
+
65
+ 詳細と実機の確認は [ADR 0006](../../docs/adr/0006-library-owns-table-operations.md) の段階 6。
@@ -0,0 +1,111 @@
1
+ # 運用通知の記録(alerts)の定時削除(ADR 0003・ADR 0006 の (b)。docs/NOTIFY.md の「削除(定時 Lambda)」)
2
+ # 最後の発生から 28 日を過ぎた alerts を、定時に消す(ネイティブ TTL は効かない)。
3
+ # Lambda の成果物はアプリが作り、source_file で渡す(実装は `createAlertsCleanupHandler`)。
4
+ #
5
+ # 失敗は構造化ログ(error)に出す。このロググループは、通知(../ops-notify)に購読させる。
6
+ # リソースのアドレス・名前は、asanowa / astro の旧モジュール(infra/modules/alerts-cleanup)と同じ。
7
+
8
+ locals {
9
+ prefix = "${var.project_name}-${var.environment}"
10
+ }
11
+
12
+ data "archive_file" "cleanup" {
13
+ type = "zip"
14
+ source_file = var.source_file
15
+ output_path = "${path.root}/.terraform/archives/alerts-cleanup.zip"
16
+ }
17
+
18
+ resource "aws_cloudwatch_log_group" "cleanup" {
19
+ name = "/aws/lambda/${local.prefix}-alerts-cleanup"
20
+ retention_in_days = var.log_retention_days
21
+ }
22
+
23
+ resource "aws_iam_role" "cleanup" {
24
+ name = "${local.prefix}-alerts-cleanup-role"
25
+
26
+ assume_role_policy = jsonencode({
27
+ Version = "2012-10-17"
28
+ Statement = [{
29
+ Action = "sts:AssumeRole"
30
+ Effect = "Allow"
31
+ Principal = { Service = "lambda.amazonaws.com" }
32
+ }]
33
+ })
34
+ }
35
+
36
+ resource "aws_iam_role_policy_attachment" "cleanup_logs" {
37
+ role = aws_iam_role.cleanup.name
38
+ policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
39
+ }
40
+
41
+ # Records Lambda の IAM 認証版 URL(エイリアス)を呼ぶ権限だけ(最小権限)。DynamoDB には直接触れない
42
+ resource "aws_iam_role_policy" "cleanup_invoke_records" {
43
+ name = "${local.prefix}-alerts-cleanup-invoke-records"
44
+ role = aws_iam_role.cleanup.id
45
+
46
+ policy = jsonencode({
47
+ Version = "2012-10-17"
48
+ Statement = [{
49
+ Effect = "Allow"
50
+ Action = ["lambda:InvokeFunctionUrl"]
51
+ Resource = [var.records_iam_alias_arn]
52
+ }]
53
+ })
54
+ }
55
+
56
+ resource "aws_lambda_function" "cleanup" {
57
+ function_name = "${local.prefix}-alerts-cleanup"
58
+ role = aws_iam_role.cleanup.arn
59
+ filename = data.archive_file.cleanup.output_path
60
+ source_code_hash = data.archive_file.cleanup.output_base64sha256
61
+ handler = var.handler
62
+ runtime = "nodejs22.x"
63
+ architectures = ["arm64"]
64
+ timeout = 120
65
+ memory_size = 256
66
+
67
+ environment {
68
+ # PROJECT_NAME は createAlertsCleanupHandler の app の既定(app を渡すアプリは読まない)
69
+ variables = {
70
+ PROJECT_NAME = var.project_name
71
+ RECORDS_IAM_URL = var.records_iam_function_url
72
+ }
73
+ }
74
+
75
+ logging_config {
76
+ log_format = "JSON"
77
+ log_group = aws_cloudwatch_log_group.cleanup.name
78
+ }
79
+
80
+ depends_on = [aws_iam_role_policy_attachment.cleanup_logs]
81
+ }
82
+
83
+ # 再試行は 0(翌日の定時実行で残りを消す。失敗はログ → 通知で知る)
84
+ resource "aws_lambda_function_event_invoke_config" "cleanup" {
85
+ function_name = aws_lambda_function.cleanup.function_name
86
+ maximum_retry_attempts = 0
87
+ }
88
+
89
+ resource "aws_cloudwatch_event_rule" "cleanup" {
90
+ name = "${local.prefix}-alerts-cleanup"
91
+ description = "Delete ops alerts records older than 28 days (daily 03:00 JST)"
92
+ schedule_expression = var.schedule_expression
93
+ }
94
+
95
+ resource "aws_cloudwatch_event_target" "cleanup" {
96
+ rule = aws_cloudwatch_event_rule.cleanup.name
97
+ arn = aws_lambda_function.cleanup.arn
98
+
99
+ retry_policy {
100
+ maximum_retry_attempts = 0
101
+ maximum_event_age_in_seconds = 3600
102
+ }
103
+ }
104
+
105
+ resource "aws_lambda_permission" "cleanup_events" {
106
+ statement_id = "AllowEventBridgeInvoke"
107
+ action = "lambda:InvokeFunction"
108
+ function_name = aws_lambda_function.cleanup.function_name
109
+ principal = "events.amazonaws.com"
110
+ source_arn = aws_cloudwatch_event_rule.cleanup.arn
111
+ }