@exabugs/dynamodb-client 1.5.2 → 1.5.3

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 (50) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/server/handler.cjs +10855 -6862
  3. package/dist/server/handler.cjs.map +4 -4
  4. package/dist/server/media/magicBytes.d.ts +25 -0
  5. package/dist/server/media/magicBytes.d.ts.map +1 -0
  6. package/dist/server/media/magicBytes.js +50 -0
  7. package/dist/server/media/magicBytes.js.map +1 -0
  8. package/dist/server/media/media-handler.d.ts +13 -0
  9. package/dist/server/media/media-handler.d.ts.map +1 -0
  10. package/dist/server/media/media-handler.js +143 -0
  11. package/dist/server/media/media-handler.js.map +1 -0
  12. package/dist/server/media/presign.d.ts +9 -0
  13. package/dist/server/media/presign.d.ts.map +1 -0
  14. package/dist/server/media/presign.js +73 -0
  15. package/dist/server/media/presign.js.map +1 -0
  16. package/dist/server/media/process-handler-dlq.d.ts +16 -0
  17. package/dist/server/media/process-handler-dlq.d.ts.map +1 -0
  18. package/dist/server/media/process-handler-dlq.js +52 -0
  19. package/dist/server/media/process-handler-dlq.js.map +1 -0
  20. package/dist/server/media/process-handler.d.ts +12 -0
  21. package/dist/server/media/process-handler.d.ts.map +1 -0
  22. package/dist/server/media/process-handler.js +138 -0
  23. package/dist/server/media/process-handler.js.map +1 -0
  24. package/dist/server/media/sign.d.ts +11 -0
  25. package/dist/server/media/sign.d.ts.map +1 -0
  26. package/dist/server/media/sign.js +49 -0
  27. package/dist/server/media/sign.js.map +1 -0
  28. package/dist/server/media/ssmParams.d.ts +29 -0
  29. package/dist/server/media/ssmParams.d.ts.map +1 -0
  30. package/dist/server/media/ssmParams.js +74 -0
  31. package/dist/server/media/ssmParams.js.map +1 -0
  32. package/dist/server/media/types.d.ts +109 -0
  33. package/dist/server/media/types.d.ts.map +1 -0
  34. package/dist/server/media/types.js +2 -0
  35. package/dist/server/media/types.js.map +1 -0
  36. package/dist/server/media-handler.cjs +57450 -0
  37. package/dist/server/media-handler.cjs.map +7 -0
  38. package/dist/server/media-process-handler-dlq.cjs +36195 -0
  39. package/dist/server/media-process-handler-dlq.cjs.map +7 -0
  40. package/dist/server/media-process-handler.cjs +60568 -0
  41. package/dist/server/media-process-handler.cjs.map +7 -0
  42. package/dist/sharp-layer.zip +0 -0
  43. package/package.json +34 -3
  44. package/terraform/main.tf +3 -2
  45. package/terraform/media/README.md +64 -0
  46. package/terraform/media/main.tf +718 -0
  47. package/terraform/media/outputs.tf +66 -0
  48. package/terraform/media/variables.tf +84 -0
  49. package/terraform/modules/parameter-store/main.tf +26 -1
  50. package/terraform/modules/parameter-store/variables.tf +7 -1
@@ -0,0 +1,718 @@
1
+ # メディア(ファイルアップロード/処理/配信)機能モジュール
2
+ #
3
+ # 詳細設計: ../../docs/media-design.md
4
+ # 決定事項・トレードオフ: ../../docs/adr/0002-media-design.md
5
+ #
6
+ # 本モジュールは upload-handler(presign/sign発行)のLambdaを含まない。
7
+ # 「誰にpresign/署名URLを発行するか」の認可判断はアプリごとに異なるため、呼び出し側は
8
+ # 既存のHTTP APIレイヤー(認証・権限チェック済み)から `../../src/server/media/presign.ts`・
9
+ # `sign.ts` を直接呼び出して自前のLambda/ルートに組み込むこと。
10
+
11
+ locals {
12
+ name_prefix = "${var.project_name}-${var.environment}-media"
13
+ }
14
+
15
+ # ============================================================
16
+ # S3バケット(raw/master/cache の3プレフィックス、完全非公開)
17
+ # ============================================================
18
+
19
+ resource "aws_s3_bucket" "media" {
20
+ bucket = local.name_prefix
21
+
22
+ tags = {
23
+ Name = local.name_prefix
24
+ }
25
+ }
26
+
27
+ resource "aws_s3_bucket_public_access_block" "media" {
28
+ bucket = aws_s3_bucket.media.id
29
+
30
+ block_public_acls = true
31
+ block_public_policy = true
32
+ ignore_public_acls = true
33
+ restrict_public_buckets = true
34
+ }
35
+
36
+ # cache/* のみ自動削除。raw/*・master/*は恒久保存
37
+ resource "aws_s3_bucket_lifecycle_configuration" "media" {
38
+ bucket = aws_s3_bucket.media.id
39
+
40
+ rule {
41
+ id = "expire-cache"
42
+ status = "Enabled"
43
+
44
+ filter {
45
+ prefix = "cache/"
46
+ }
47
+
48
+ expiration {
49
+ days = var.image_cache_ttl_days
50
+ }
51
+ }
52
+ }
53
+
54
+ # CloudFront OAC経由のアクセスのみ許可。Resourceはmaster/*に限定し、
55
+ # raw/*・cache/*をバケットポリシー経由で公開しない(cache/*はmedia-handlerの実行ロール経由のみ)
56
+ resource "aws_s3_bucket_policy" "media" {
57
+ bucket = aws_s3_bucket.media.id
58
+
59
+ policy = jsonencode({
60
+ Version = "2012-10-17"
61
+ Statement = [
62
+ {
63
+ Sid = "AllowCloudFrontOACReadMasterOnly"
64
+ Effect = "Allow"
65
+ Principal = { Service = "cloudfront.amazonaws.com" }
66
+ Action = "s3:GetObject"
67
+ Resource = "${aws_s3_bucket.media.arn}/master/*"
68
+ Condition = {
69
+ StringEquals = {
70
+ "AWS:SourceArn" = aws_cloudfront_distribution.media.arn
71
+ }
72
+ }
73
+ }
74
+ ]
75
+ })
76
+ }
77
+
78
+ # ============================================================
79
+ # DLQ(process-handlerのon-failure宛先)
80
+ # ============================================================
81
+
82
+ resource "aws_sqs_queue" "process_handler_dlq" {
83
+ name = "${local.name_prefix}-process-dlq"
84
+ message_retention_seconds = 1209600 # 14日(SQS上限)
85
+
86
+ tags = {
87
+ Name = "${local.name_prefix}-process-dlq"
88
+ }
89
+ }
90
+
91
+ # ============================================================
92
+ # sharp Lambda Layer(scripts/build-sharp-layer.shで事前ビルド)
93
+ # ============================================================
94
+
95
+ resource "aws_lambda_layer_version" "sharp" {
96
+ layer_name = "${local.name_prefix}-sharp"
97
+ filename = "${path.module}/../../dist/sharp-layer.zip"
98
+ source_code_hash = filebase64sha256("${path.module}/../../dist/sharp-layer.zip")
99
+ compatible_runtimes = ["nodejs22.x"]
100
+ # sharp-layer.zipはlinux/arm64向けにビルドされる(build-sharp-layer.sh参照)
101
+ compatible_architectures = ["arm64"]
102
+ }
103
+
104
+ # ============================================================
105
+ # process-handler Lambda(S3 ObjectCreatedトリガー)
106
+ # ============================================================
107
+
108
+ resource "aws_iam_role" "process_handler" {
109
+ name = "${local.name_prefix}-process-handler-role"
110
+
111
+ assume_role_policy = jsonencode({
112
+ Version = "2012-10-17"
113
+ Statement = [
114
+ {
115
+ Action = "sts:AssumeRole"
116
+ Effect = "Allow"
117
+ Principal = { Service = "lambda.amazonaws.com" }
118
+ }
119
+ ]
120
+ })
121
+
122
+ tags = {
123
+ Name = "${local.name_prefix}-process-handler-role"
124
+ }
125
+ }
126
+
127
+ resource "aws_iam_role_policy_attachment" "process_handler_basic" {
128
+ role = aws_iam_role.process_handler.name
129
+ policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
130
+ }
131
+
132
+ resource "aws_iam_role_policy" "process_handler_s3" {
133
+ name = "s3-access"
134
+ role = aws_iam_role.process_handler.id
135
+
136
+ policy = jsonencode({
137
+ Version = "2012-10-17"
138
+ Statement = [
139
+ {
140
+ Effect = "Allow"
141
+ Action = ["s3:GetObject"]
142
+ Resource = "${aws_s3_bucket.media.arn}/raw/*"
143
+ },
144
+ {
145
+ Effect = "Allow"
146
+ Action = ["s3:PutObject", "s3:GetObject"]
147
+ Resource = "${aws_s3_bucket.media.arn}/master/*"
148
+ }
149
+ ]
150
+ })
151
+ }
152
+
153
+ resource "aws_iam_role_policy" "process_handler_dynamodb" {
154
+ name = "dynamodb-access"
155
+ role = aws_iam_role.process_handler.id
156
+
157
+ policy = jsonencode({
158
+ Version = "2012-10-17"
159
+ Statement = [
160
+ {
161
+ Effect = "Allow"
162
+ Action = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:BatchGetItem", "dynamodb:Query"]
163
+ Resource = [var.table_arn, "${var.table_arn}/index/*"]
164
+ }
165
+ ]
166
+ })
167
+ }
168
+
169
+ resource "aws_iam_role_policy" "process_handler_ssm" {
170
+ name = "ssm-access"
171
+ role = aws_iam_role.process_handler.id
172
+
173
+ policy = jsonencode({
174
+ Version = "2012-10-17"
175
+ Statement = [
176
+ {
177
+ Effect = "Allow"
178
+ Action = ["ssm:GetParameter"]
179
+ Resource = "arn:aws:ssm:${var.region}:*:parameter${var.image_policy_param}"
180
+ }
181
+ ]
182
+ })
183
+ }
184
+
185
+ # Lambda非同期呼び出し失敗時のon-failure宛先として送信する権限
186
+ resource "aws_iam_role_policy" "process_handler_dlq_send" {
187
+ name = "dlq-send"
188
+ role = aws_iam_role.process_handler.id
189
+
190
+ policy = jsonencode({
191
+ Version = "2012-10-17"
192
+ Statement = [
193
+ {
194
+ Effect = "Allow"
195
+ Action = ["sqs:SendMessage"]
196
+ Resource = aws_sqs_queue.process_handler_dlq.arn
197
+ }
198
+ ]
199
+ })
200
+ }
201
+
202
+ resource "aws_cloudwatch_log_group" "process_handler" {
203
+ name = "/aws/lambda/${local.name_prefix}-process-handler"
204
+ retention_in_days = var.log_retention_days
205
+
206
+ tags = {
207
+ Name = "${local.name_prefix}-process-handler-logs"
208
+ }
209
+ }
210
+
211
+ data "archive_file" "process_handler" {
212
+ type = "zip"
213
+ source_file = "${path.module}/../../dist/server/media-process-handler.cjs"
214
+ output_path = "${path.module}/../../dist/server/media-process-handler.zip"
215
+ }
216
+
217
+ resource "aws_lambda_function" "process_handler" {
218
+ function_name = "${local.name_prefix}-process-handler"
219
+ description = "メディア処理(EXIF除去・リサイズ・master配置)。S3 ObjectCreated(raw/*)トリガー"
220
+ role = aws_iam_role.process_handler.arn
221
+
222
+ filename = data.archive_file.process_handler.output_path
223
+ source_code_hash = data.archive_file.process_handler.output_base64sha256
224
+
225
+ runtime = "nodejs22.x"
226
+ architectures = ["arm64"] # sharp-layer.zip(linux/arm64)に合わせる
227
+ handler = "media-process-handler.handler"
228
+
229
+ timeout = var.process_handler_timeout
230
+ memory_size = var.process_handler_memory_size
231
+
232
+ layers = [aws_lambda_layer_version.sharp.arn]
233
+
234
+ environment {
235
+ variables = {
236
+ REGION = var.region
237
+ TABLE_NAME = var.table_name
238
+ IMAGE_POLICY_PARAM = var.image_policy_param
239
+ }
240
+ }
241
+
242
+ depends_on = [
243
+ aws_cloudwatch_log_group.process_handler,
244
+ aws_iam_role_policy.process_handler_s3,
245
+ aws_iam_role_policy.process_handler_dynamodb,
246
+ aws_iam_role_policy.process_handler_ssm,
247
+ ]
248
+
249
+ tags = {
250
+ Name = "${local.name_prefix}-process-handler"
251
+ }
252
+ }
253
+
254
+ resource "aws_lambda_function_event_invoke_config" "process_handler" {
255
+ function_name = aws_lambda_function.process_handler.function_name
256
+ maximum_retry_attempts = 2
257
+
258
+ destination_config {
259
+ on_failure {
260
+ destination = aws_sqs_queue.process_handler_dlq.arn
261
+ }
262
+ }
263
+ }
264
+
265
+ resource "aws_lambda_permission" "s3_invoke_process_handler" {
266
+ statement_id = "AllowS3Invoke"
267
+ action = "lambda:InvokeFunction"
268
+ function_name = aws_lambda_function.process_handler.function_name
269
+ principal = "s3.amazonaws.com"
270
+ source_arn = aws_s3_bucket.media.arn
271
+ }
272
+
273
+ resource "aws_s3_bucket_notification" "media" {
274
+ bucket = aws_s3_bucket.media.id
275
+
276
+ lambda_function {
277
+ lambda_function_arn = aws_lambda_function.process_handler.arn
278
+ events = ["s3:ObjectCreated:*"]
279
+ filter_prefix = "raw/"
280
+ }
281
+
282
+ depends_on = [aws_lambda_permission.s3_invoke_process_handler]
283
+ }
284
+
285
+ # ============================================================
286
+ # process-handler-dlq Lambda(SQS DLQトリガー)
287
+ # ============================================================
288
+
289
+ resource "aws_iam_role" "process_handler_dlq" {
290
+ name = "${local.name_prefix}-process-handler-dlq-role"
291
+
292
+ assume_role_policy = jsonencode({
293
+ Version = "2012-10-17"
294
+ Statement = [
295
+ {
296
+ Action = "sts:AssumeRole"
297
+ Effect = "Allow"
298
+ Principal = { Service = "lambda.amazonaws.com" }
299
+ }
300
+ ]
301
+ })
302
+
303
+ tags = {
304
+ Name = "${local.name_prefix}-process-handler-dlq-role"
305
+ }
306
+ }
307
+
308
+ resource "aws_iam_role_policy_attachment" "process_handler_dlq_basic" {
309
+ role = aws_iam_role.process_handler_dlq.name
310
+ policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
311
+ }
312
+
313
+ resource "aws_iam_role_policy" "process_handler_dlq_sqs" {
314
+ name = "sqs-access"
315
+ role = aws_iam_role.process_handler_dlq.id
316
+
317
+ policy = jsonencode({
318
+ Version = "2012-10-17"
319
+ Statement = [
320
+ {
321
+ Effect = "Allow"
322
+ Action = ["sqs:ReceiveMessage", "sqs:DeleteMessage", "sqs:GetQueueAttributes"]
323
+ Resource = aws_sqs_queue.process_handler_dlq.arn
324
+ }
325
+ ]
326
+ })
327
+ }
328
+
329
+ resource "aws_iam_role_policy" "process_handler_dlq_dynamodb" {
330
+ name = "dynamodb-access"
331
+ role = aws_iam_role.process_handler_dlq.id
332
+
333
+ policy = jsonencode({
334
+ Version = "2012-10-17"
335
+ Statement = [
336
+ {
337
+ Effect = "Allow"
338
+ Action = ["dynamodb:GetItem", "dynamodb:PutItem", "dynamodb:UpdateItem", "dynamodb:BatchGetItem"]
339
+ Resource = [var.table_arn, "${var.table_arn}/index/*"]
340
+ }
341
+ ]
342
+ })
343
+ }
344
+
345
+ resource "aws_cloudwatch_log_group" "process_handler_dlq" {
346
+ name = "/aws/lambda/${local.name_prefix}-process-handler-dlq"
347
+ retention_in_days = var.log_retention_days
348
+
349
+ tags = {
350
+ Name = "${local.name_prefix}-process-handler-dlq-logs"
351
+ }
352
+ }
353
+
354
+ data "archive_file" "process_handler_dlq" {
355
+ type = "zip"
356
+ source_file = "${path.module}/../../dist/server/media-process-handler-dlq.cjs"
357
+ output_path = "${path.module}/../../dist/server/media-process-handler-dlq.zip"
358
+ }
359
+
360
+ resource "aws_lambda_function" "process_handler_dlq" {
361
+ function_name = "${local.name_prefix}-process-handler-dlq"
362
+ description = "process-handlerのリトライが尽きた場合にfailedレコードを書き込む(SQS DLQトリガー)"
363
+ role = aws_iam_role.process_handler_dlq.arn
364
+
365
+ filename = data.archive_file.process_handler_dlq.output_path
366
+ source_code_hash = data.archive_file.process_handler_dlq.output_base64sha256
367
+
368
+ runtime = "nodejs22.x"
369
+ architectures = ["arm64"]
370
+ handler = "media-process-handler-dlq.handler"
371
+
372
+ timeout = 30
373
+ memory_size = 256
374
+
375
+ environment {
376
+ variables = {
377
+ REGION = var.region
378
+ TABLE_NAME = var.table_name
379
+ }
380
+ }
381
+
382
+ depends_on = [
383
+ aws_cloudwatch_log_group.process_handler_dlq,
384
+ aws_iam_role_policy.process_handler_dlq_sqs,
385
+ aws_iam_role_policy.process_handler_dlq_dynamodb,
386
+ ]
387
+
388
+ tags = {
389
+ Name = "${local.name_prefix}-process-handler-dlq"
390
+ }
391
+ }
392
+
393
+ resource "aws_lambda_event_source_mapping" "process_handler_dlq" {
394
+ event_source_arn = aws_sqs_queue.process_handler_dlq.arn
395
+ function_name = aws_lambda_function.process_handler_dlq.arn
396
+ batch_size = 1
397
+ }
398
+
399
+ # ============================================================
400
+ # media-handler Lambda(CloudFront /resize/* 専用オリジン)
401
+ # ============================================================
402
+
403
+ resource "aws_iam_role" "media_handler" {
404
+ name = "${local.name_prefix}-media-handler-role"
405
+
406
+ assume_role_policy = jsonencode({
407
+ Version = "2012-10-17"
408
+ Statement = [
409
+ {
410
+ Action = "sts:AssumeRole"
411
+ Effect = "Allow"
412
+ Principal = { Service = "lambda.amazonaws.com" }
413
+ }
414
+ ]
415
+ })
416
+
417
+ tags = {
418
+ Name = "${local.name_prefix}-media-handler-role"
419
+ }
420
+ }
421
+
422
+ resource "aws_iam_role_policy_attachment" "media_handler_basic" {
423
+ role = aws_iam_role.media_handler.name
424
+ policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
425
+ }
426
+
427
+ # 署名鍵へのSSM権限・DynamoDB権限は付与しない(最小権限。media-handlerに不要なため)
428
+ resource "aws_iam_role_policy" "media_handler_s3" {
429
+ name = "s3-access"
430
+ role = aws_iam_role.media_handler.id
431
+
432
+ policy = jsonencode({
433
+ Version = "2012-10-17"
434
+ Statement = [
435
+ {
436
+ Effect = "Allow"
437
+ Action = ["s3:GetObject"]
438
+ Resource = "${aws_s3_bucket.media.arn}/master/*"
439
+ },
440
+ {
441
+ Effect = "Allow"
442
+ Action = ["s3:GetObject", "s3:PutObject"]
443
+ Resource = "${aws_s3_bucket.media.arn}/cache/*"
444
+ },
445
+ {
446
+ # s3:ListBucket が無いロールでGetObjectが存在しないキーを指すと、S3は
447
+ # NoSuchKey(404相当)ではなく403 AccessDeniedを返す(存在確認防止のためのAWS仕様)。
448
+ # これを許可しないと tryGetFromS3() の NoSuchKey 判定に当たらず、
449
+ # cache未生成時の初回リクエストが常に500になる。
450
+ Effect = "Allow"
451
+ Action = ["s3:ListBucket"]
452
+ Resource = aws_s3_bucket.media.arn
453
+ Condition = {
454
+ StringLike = {
455
+ "s3:prefix" = ["master/*", "cache/*"]
456
+ }
457
+ }
458
+ }
459
+ ]
460
+ })
461
+ }
462
+
463
+ resource "aws_iam_role_policy" "media_handler_ssm" {
464
+ name = "ssm-access"
465
+ role = aws_iam_role.media_handler.id
466
+
467
+ policy = jsonencode({
468
+ Version = "2012-10-17"
469
+ Statement = [
470
+ {
471
+ Effect = "Allow"
472
+ Action = ["ssm:GetParameter"]
473
+ Resource = "arn:aws:ssm:${var.region}:*:parameter${var.image_policy_param}"
474
+ }
475
+ ]
476
+ })
477
+ }
478
+
479
+ resource "aws_cloudwatch_log_group" "media_handler" {
480
+ name = "/aws/lambda/${local.name_prefix}-handler"
481
+ retention_in_days = var.log_retention_days
482
+
483
+ tags = {
484
+ Name = "${local.name_prefix}-handler-logs"
485
+ }
486
+ }
487
+
488
+ data "archive_file" "media_handler" {
489
+ type = "zip"
490
+ source_file = "${path.module}/../../dist/server/media-handler.cjs"
491
+ output_path = "${path.module}/../../dist/server/media-handler.zip"
492
+ }
493
+
494
+ resource "aws_lambda_function" "media_handler" {
495
+ function_name = "${local.name_prefix}-handler"
496
+ description = "CloudFront /resize/* 専用オリジン。オンデマンドリサイズ"
497
+ role = aws_iam_role.media_handler.arn
498
+
499
+ filename = data.archive_file.media_handler.output_path
500
+ source_code_hash = data.archive_file.media_handler.output_base64sha256
501
+
502
+ runtime = "nodejs22.x"
503
+ architectures = ["arm64"] # sharp-layer.zip(linux/arm64)に合わせる
504
+ handler = "media-handler.handler"
505
+
506
+ timeout = 30
507
+ memory_size = var.media_handler_memory_size
508
+
509
+ layers = [aws_lambda_layer_version.sharp.arn]
510
+
511
+ environment {
512
+ variables = {
513
+ REGION = var.region
514
+ MEDIA_BUCKET = aws_s3_bucket.media.bucket
515
+ IMAGE_POLICY_PARAM = var.image_policy_param
516
+ }
517
+ }
518
+
519
+ depends_on = [
520
+ aws_cloudwatch_log_group.media_handler,
521
+ aws_iam_role_policy.media_handler_s3,
522
+ aws_iam_role_policy.media_handler_ssm,
523
+ ]
524
+
525
+ tags = {
526
+ Name = "${local.name_prefix}-handler"
527
+ }
528
+ }
529
+
530
+ # CloudFrontからのみ呼び出し可能にする(AWS_IAM認証、CloudFront OAC経由)
531
+ resource "aws_lambda_function_url" "media_handler" {
532
+ function_name = aws_lambda_function.media_handler.function_name
533
+ authorization_type = "AWS_IAM"
534
+ }
535
+
536
+ resource "aws_lambda_permission" "cloudfront_invoke_media_handler" {
537
+ statement_id = "AllowCloudFrontInvoke"
538
+ action = "lambda:InvokeFunctionUrl"
539
+ function_name = aws_lambda_function.media_handler.function_name
540
+ principal = "cloudfront.amazonaws.com"
541
+ source_arn = aws_cloudfront_distribution.media.arn
542
+ function_url_auth_type = "AWS_IAM"
543
+ }
544
+
545
+ # ============================================================
546
+ # CloudFront配信(署名付きURL必須、/master/*・/resize/*の2ビヘイビア)
547
+ # ============================================================
548
+
549
+ resource "aws_cloudfront_origin_access_control" "media_s3" {
550
+ name = "${local.name_prefix}-s3-oac"
551
+ origin_access_control_origin_type = "s3"
552
+ signing_behavior = "always"
553
+ signing_protocol = "sigv4"
554
+ }
555
+
556
+ resource "aws_cloudfront_origin_access_control" "media_handler" {
557
+ name = "${local.name_prefix}-lambda-oac"
558
+ origin_access_control_origin_type = "lambda"
559
+ signing_behavior = "always"
560
+ signing_protocol = "sigv4"
561
+ }
562
+
563
+ data "aws_ssm_parameter" "signing_public_key" {
564
+ name = var.signing_public_key_param
565
+ }
566
+
567
+ resource "aws_cloudfront_public_key" "media" {
568
+ name = "${local.name_prefix}-signing-key"
569
+ encoded_key = data.aws_ssm_parameter.signing_public_key.value
570
+ comment = "Media delivery signing key (2048bit RSA)"
571
+ }
572
+
573
+ resource "aws_cloudfront_key_group" "media" {
574
+ name = "${local.name_prefix}-key-group"
575
+ items = [aws_cloudfront_public_key.media.id]
576
+ }
577
+
578
+ # width クエリパラメータのみキャッシュキーに含め、署名パラメータは除外する
579
+ resource "aws_cloudfront_cache_policy" "resize" {
580
+ name = "${local.name_prefix}-resize-cache-policy"
581
+ comment = "width のみキャッシュキーに含める。署名付きURLのExpires/Signature/Key-Pair-Idは除外"
582
+
583
+ default_ttl = 86400
584
+ max_ttl = 31536000
585
+ min_ttl = 0
586
+
587
+ parameters_in_cache_key_and_forwarded_to_origin {
588
+ cookies_config {
589
+ cookie_behavior = "none"
590
+ }
591
+ headers_config {
592
+ header_behavior = "none"
593
+ }
594
+ query_strings_config {
595
+ query_string_behavior = "whitelist"
596
+ query_strings {
597
+ items = ["width"]
598
+ }
599
+ }
600
+ enable_accept_encoding_brotli = true
601
+ enable_accept_encoding_gzip = true
602
+ }
603
+ }
604
+
605
+ locals {
606
+ # AWSマネージドポリシー(キャッシュポリシー/オリジンリクエストポリシー)のID
607
+ # CachingOptimized: S3静的ファイル配信向けの標準キャッシュポリシー
608
+ managed_caching_optimized_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6"
609
+ # AllViewerExceptHostHeader: Hostヘッダーのみ転送しない(Function URLがHostヘッダー転送時に403を返すため)
610
+ managed_all_viewer_except_host_header_policy_id = "b689b0a8-53d0-40ab-baf2-68738e2966ac"
611
+
612
+ media_handler_function_url_domain = replace(
613
+ replace(aws_lambda_function_url.media_handler.function_url, "https://", ""),
614
+ "/",
615
+ ""
616
+ )
617
+
618
+ has_custom_domain = var.domain_name != null && var.acm_certificate_arn != null
619
+ }
620
+
621
+ resource "aws_cloudfront_distribution" "media" {
622
+ enabled = true
623
+ comment = "${local.name_prefix} delivery"
624
+ price_class = "PriceClass_200"
625
+
626
+ aliases = local.has_custom_domain ? [var.domain_name] : []
627
+
628
+ origin {
629
+ domain_name = aws_s3_bucket.media.bucket_regional_domain_name
630
+ origin_id = "s3-media"
631
+ origin_access_control_id = aws_cloudfront_origin_access_control.media_s3.id
632
+ }
633
+
634
+ origin {
635
+ domain_name = local.media_handler_function_url_domain
636
+ origin_id = "lambda-media-handler"
637
+ origin_access_control_id = aws_cloudfront_origin_access_control.media_handler.id
638
+
639
+ custom_origin_config {
640
+ http_port = 80
641
+ https_port = 443
642
+ origin_protocol_policy = "https-only"
643
+ origin_ssl_protocols = ["TLSv1.2"]
644
+ }
645
+ }
646
+
647
+ # デフォルトビヘイビア(想定外のパス)。masterと同じ扱いにしておく
648
+ default_cache_behavior {
649
+ target_origin_id = "s3-media"
650
+ viewer_protocol_policy = "redirect-to-https"
651
+ allowed_methods = ["GET", "HEAD"]
652
+ cached_methods = ["GET", "HEAD"]
653
+ cache_policy_id = local.managed_caching_optimized_policy_id
654
+ trusted_key_groups = [aws_cloudfront_key_group.media.id]
655
+ }
656
+
657
+ ordered_cache_behavior {
658
+ path_pattern = "/master/*"
659
+ target_origin_id = "s3-media"
660
+ viewer_protocol_policy = "redirect-to-https"
661
+ allowed_methods = ["GET", "HEAD"]
662
+ cached_methods = ["GET", "HEAD"]
663
+ cache_policy_id = local.managed_caching_optimized_policy_id
664
+ trusted_key_groups = [aws_cloudfront_key_group.media.id]
665
+ }
666
+
667
+ ordered_cache_behavior {
668
+ path_pattern = "/resize/*"
669
+ target_origin_id = "lambda-media-handler"
670
+ viewer_protocol_policy = "redirect-to-https"
671
+ allowed_methods = ["GET", "HEAD"]
672
+ cached_methods = ["GET", "HEAD"]
673
+ cache_policy_id = aws_cloudfront_cache_policy.resize.id
674
+ origin_request_policy_id = local.managed_all_viewer_except_host_header_policy_id
675
+ trusted_key_groups = [aws_cloudfront_key_group.media.id]
676
+ }
677
+
678
+ # 404/403応答がネガティブキャッシュ(既定約10秒)で長引かないようにする
679
+ custom_error_response {
680
+ error_code = 403
681
+ error_caching_min_ttl = 0
682
+ }
683
+ custom_error_response {
684
+ error_code = 404
685
+ error_caching_min_ttl = 0
686
+ }
687
+
688
+ restrictions {
689
+ geo_restriction {
690
+ restriction_type = "none"
691
+ }
692
+ }
693
+
694
+ viewer_certificate {
695
+ cloudfront_default_certificate = local.has_custom_domain ? null : true
696
+ acm_certificate_arn = local.has_custom_domain ? var.acm_certificate_arn : null
697
+ ssl_support_method = local.has_custom_domain ? "sni-only" : null
698
+ minimum_protocol_version = local.has_custom_domain ? "TLSv1.2_2021" : null
699
+ }
700
+
701
+ tags = {
702
+ Name = local.name_prefix
703
+ }
704
+ }
705
+
706
+ resource "aws_route53_record" "media" {
707
+ count = local.has_custom_domain && var.route53_zone_id != null ? 1 : 0
708
+
709
+ zone_id = var.route53_zone_id
710
+ name = var.domain_name
711
+ type = "A"
712
+
713
+ alias {
714
+ name = aws_cloudfront_distribution.media.domain_name
715
+ zone_id = aws_cloudfront_distribution.media.hosted_zone_id
716
+ evaluate_target_health = false
717
+ }
718
+ }