@aws/nx-plugin 0.46.0 → 0.48.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.
- package/package.json +1 -1
- package/src/infra/app/generator.js +4 -1
- package/src/infra/app/generator.js.map +1 -1
- package/src/mcp-server/tools/create-workspace-command.js +4 -0
- package/src/mcp-server/tools/create-workspace-command.js.map +1 -1
- package/src/mcp-server/tools/general-guidance.js +3 -1
- package/src/mcp-server/tools/general-guidance.js.map +1 -1
- package/src/preset/__snapshots__/generator.spec.ts.snap +9 -0
- package/src/preset/generator.d.ts +1 -1
- package/src/preset/generator.js +4 -1
- package/src/preset/generator.js.map +1 -1
- package/src/preset/schema.d.ts +3 -0
- package/src/preset/schema.json +9 -1
- package/src/py/fast-api/__snapshots__/generator.spec.ts.snap +60 -0
- package/src/py/fast-api/generator.js +5 -3
- package/src/py/fast-api/generator.js.map +1 -1
- package/src/py/fast-api/react/__snapshots__/generator.spec.ts.snap +1 -0
- package/src/py/fast-api/schema.d.ts +3 -1
- package/src/py/fast-api/schema.json +4 -4
- package/src/py/lambda-function/__snapshots__/generator.spec.ts.snap +310 -0
- package/src/py/lambda-function/generator.js +17 -25
- package/src/py/lambda-function/generator.js.map +1 -1
- package/src/py/lambda-function/schema.d.ts +3 -0
- package/src/py/lambda-function/schema.json +8 -0
- package/src/py/mcp-server/generator.js +4 -2
- package/src/py/mcp-server/generator.js.map +1 -1
- package/src/py/mcp-server/schema.d.ts +4 -1
- package/src/py/mcp-server/schema.json +4 -4
- package/src/py/strands-agent/generator.js +4 -2
- package/src/py/strands-agent/generator.js.map +1 -1
- package/src/py/strands-agent/schema.d.ts +3 -1
- package/src/py/strands-agent/schema.json +4 -4
- package/src/terraform/project/generator.js +2 -1
- package/src/terraform/project/generator.js.map +1 -1
- package/src/trpc/backend/__snapshots__/generator.spec.ts.snap +60 -0
- package/src/trpc/backend/generator.js +4 -2
- package/src/trpc/backend/generator.js.map +1 -1
- package/src/trpc/backend/schema.d.ts +2 -1
- package/src/trpc/backend/schema.json +4 -4
- package/src/trpc/react/__snapshots__/generator.spec.ts.snap +1 -0
- package/src/ts/lambda-function/__snapshots__/generator.spec.ts.snap +310 -0
- package/src/ts/lambda-function/generator.js +17 -27
- package/src/ts/lambda-function/generator.js.map +1 -1
- package/src/ts/lambda-function/schema.d.ts +3 -0
- package/src/ts/lambda-function/schema.json +8 -0
- package/src/ts/mcp-server/generator.js +4 -2
- package/src/ts/mcp-server/generator.js.map +1 -1
- package/src/ts/mcp-server/schema.d.ts +3 -1
- package/src/ts/mcp-server/schema.json +4 -4
- package/src/ts/nx-generator/__snapshots__/generator.spec.ts.snap +27 -1
- package/src/ts/nx-generator/files/nx-plugin-for-aws/docs/__nameKebabCase__.mdx.template +31 -0
- package/src/ts/nx-generator/files/nx-plugin-for-aws/generator/generator.spec.ts.template +1 -1
- package/src/ts/react-website/app/__snapshots__/generator.spec.ts.snap +756 -0
- package/src/ts/react-website/app/generator.js +44 -43
- package/src/ts/react-website/app/generator.js.map +1 -1
- package/src/ts/react-website/app/schema.d.ts +2 -0
- package/src/ts/react-website/app/schema.json +8 -0
- package/src/ts/react-website/cognito-auth/__snapshots__/generator.spec.ts.snap +259 -0
- package/src/ts/react-website/cognito-auth/generator.js +12 -13
- package/src/ts/react-website/cognito-auth/generator.js.map +1 -1
- package/src/ts/react-website/cognito-auth/schema.d.ts +4 -0
- package/src/ts/react-website/cognito-auth/schema.json +8 -0
- package/src/ts/react-website/runtime-config/__snapshots__/generator.spec.ts.snap +0 -40
- package/src/ts/react-website/runtime-config/generator.js +0 -2
- package/src/ts/react-website/runtime-config/generator.js.map +1 -1
- package/src/utils/agent-core-constructs/agent-core-constructs.d.ts +4 -2
- package/src/utils/agent-core-constructs/agent-core-constructs.js.map +1 -1
- package/src/utils/api-constructs/api-constructs.d.ts +2 -1
- package/src/utils/api-constructs/api-constructs.js.map +1 -1
- package/src/utils/api-constructs/files/terraform/app/apis/http/__apiNameKebabCase__/__apiNameKebabCase__.tf.template +10 -0
- package/src/utils/api-constructs/files/terraform/app/apis/rest/__apiNameKebabCase__/__apiNameKebabCase__.tf.template +10 -0
- package/src/utils/config/index.d.ts +6 -0
- package/src/utils/config/index.js.map +1 -1
- package/src/utils/files/terraform/src/core/runtime-config/entry/entry.tf.template +119 -0
- package/src/utils/files/terraform/src/core/runtime-config/read/read.tf.template +28 -0
- package/src/utils/files/terraform/src/metrics/metrics.tf.template +7 -1
- package/src/{py/lambda-function/files/common/constructs/src/app/lambda-functions/__constructFunctionKebabCase__.ts.template → utils/function-constructs/files/cdk/app/lambda-functions/__functionNameKebabCase__.ts.template} +5 -5
- package/src/utils/function-constructs/files/terraform/app/lambda-functions/__functionNameKebabCase__/__functionNameKebabCase__.tf.template +150 -0
- package/src/utils/function-constructs/function-constructs.d.ts +20 -0
- package/src/utils/function-constructs/function-constructs.js +57 -0
- package/src/utils/function-constructs/function-constructs.js.map +1 -0
- package/src/utils/iac.d.ts +21 -0
- package/src/utils/iac.js +25 -0
- package/src/utils/iac.js.map +1 -0
- package/src/utils/identity-constructs/files/terraform/core/user-identity/add-callback-url/add-callback-url.tf.template +123 -0
- package/src/utils/identity-constructs/files/terraform/core/user-identity/identity/identity.tf.template +421 -0
- package/src/utils/identity-constructs/files/terraform/core/user-identity/main.tf.template +47 -0
- package/src/utils/identity-constructs/identity-constructs.d.ts +16 -0
- package/src/utils/identity-constructs/identity-constructs.js +84 -0
- package/src/utils/identity-constructs/identity-constructs.js.map +1 -0
- package/src/utils/metrics.js +1 -1
- package/src/utils/metrics.js.map +1 -1
- package/src/utils/shared-constructs.d.ts +3 -2
- package/src/utils/shared-constructs.js +27 -3
- package/src/utils/shared-constructs.js.map +1 -1
- package/src/utils/website-constructs/files/terraform/app/static-websites/__websiteNameKebabCase__/__websiteNameKebabCase__.tf.template +42 -0
- package/src/utils/website-constructs/files/terraform/core/static-website/static-website.tf.template +709 -0
- package/src/utils/website-constructs/website-constructs.d.ts +19 -0
- package/src/utils/website-constructs/website-constructs.js +61 -0
- package/src/utils/website-constructs/website-constructs.js.map +1 -0
- package/src/ts/lambda-function/files/common/constructs/src/app/lambda-functions/__constructFunctionNameKebabCase__.ts.template +0 -24
- /package/src/{ts/react-website/cognito-auth/files/common/constructs/src → utils/identity-constructs/files/cdk}/core/user-identity.ts.template +0 -0
- /package/src/{ts/react-website/app/files/common/constructs/src → utils/website-constructs/files/cdk}/app/static-websites/__websiteNameKebabCase__.ts.template +0 -0
- /package/src/{ts/react-website/app/files/common/constructs/src → utils/website-constructs/files/cdk}/core/static-website.ts.template +0 -0
package/src/utils/website-constructs/files/terraform/core/static-website/static-website.tf.template
ADDED
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
terraform {
|
|
2
|
+
required_providers {
|
|
3
|
+
aws = {
|
|
4
|
+
source = "hashicorp/aws"
|
|
5
|
+
version = "~> 6.0"
|
|
6
|
+
configuration_aliases = [aws.us_east_1]
|
|
7
|
+
}
|
|
8
|
+
random = {
|
|
9
|
+
source = "hashicorp/random"
|
|
10
|
+
version = "~> 3.1"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
# Variables
|
|
16
|
+
variable "website_name" {
|
|
17
|
+
description = "Name of the website"
|
|
18
|
+
type = string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
variable "website_file_path" {
|
|
22
|
+
description = "Path to the website files"
|
|
23
|
+
type = string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Data sources
|
|
28
|
+
data "aws_caller_identity" "current" {}
|
|
29
|
+
data "aws_region" "current" {}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# KMS Key for encryption
|
|
33
|
+
resource "aws_kms_key" "website_key" {
|
|
34
|
+
description = "KMS key for ${var.website_name} website encryption"
|
|
35
|
+
deletion_window_in_days = 7
|
|
36
|
+
enable_key_rotation = true
|
|
37
|
+
|
|
38
|
+
policy = jsonencode({
|
|
39
|
+
Version = "2012-10-17"
|
|
40
|
+
Statement = [
|
|
41
|
+
{
|
|
42
|
+
Sid = "Enable IAM User Permissions"
|
|
43
|
+
Effect = "Allow"
|
|
44
|
+
Principal = {
|
|
45
|
+
AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
|
|
46
|
+
}
|
|
47
|
+
Action = "kms:*"
|
|
48
|
+
Resource = "*"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
Sid = "Allow S3 Service"
|
|
52
|
+
Effect = "Allow"
|
|
53
|
+
Principal = {
|
|
54
|
+
Service = "s3.amazonaws.com"
|
|
55
|
+
}
|
|
56
|
+
Action = [
|
|
57
|
+
"kms:Decrypt",
|
|
58
|
+
"kms:GenerateDataKey"
|
|
59
|
+
]
|
|
60
|
+
Resource = "*"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
Sid = "AllowCloudFrontServicePrincipalSSE-KMS"
|
|
64
|
+
Effect = "Allow"
|
|
65
|
+
Principal = {
|
|
66
|
+
Service = "cloudfront.amazonaws.com"
|
|
67
|
+
}
|
|
68
|
+
Action = [
|
|
69
|
+
"kms:Decrypt",
|
|
70
|
+
"kms:Encrypt",
|
|
71
|
+
"kms:GenerateDataKey*"
|
|
72
|
+
]
|
|
73
|
+
Resource = "*"
|
|
74
|
+
Condition = {
|
|
75
|
+
StringEquals = {
|
|
76
|
+
"AWS:SourceArn" = aws_cloudfront_distribution.website.arn
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
]
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
tags = {
|
|
84
|
+
Name = "${lower(var.website_name)}-website-key-${random_id.unique_suffix.hex}"
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
resource "aws_kms_alias" "website_key_alias" {
|
|
89
|
+
name = "alias/${lower(var.website_name)}-website-key-${random_id.unique_suffix.hex}"
|
|
90
|
+
target_key_id = aws_kms_key.website_key.key_id
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
# Access Logs Bucket
|
|
94
|
+
resource "aws_s3_bucket" "access_logs" {
|
|
95
|
+
#checkov:skip=CKV2_AWS_61:Lifecycle configuration not required for access logs bucket
|
|
96
|
+
#checkov:skip=CKV_AWS_144:Cross-region replication not required for access logs
|
|
97
|
+
#checkov:skip=CKV2_AWS_62:Event notifications not required for access logs bucket
|
|
98
|
+
#checkov:skip=CKV_AWS_21:Versioning disabled for access logs to reduce storage costs
|
|
99
|
+
bucket = "${lower(var.website_name)}-access-logs-${random_id.bucket_suffix.hex}"
|
|
100
|
+
force_destroy = true
|
|
101
|
+
|
|
102
|
+
tags = {
|
|
103
|
+
Name = "${lower(var.website_name)}-access-logs"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
resource "random_id" "unique_suffix" {
|
|
108
|
+
byte_length = 4
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
resource "random_id" "bucket_suffix" {
|
|
112
|
+
byte_length = 8
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
resource "aws_s3_bucket_versioning" "access_logs_versioning" {
|
|
116
|
+
bucket = aws_s3_bucket.access_logs.id
|
|
117
|
+
versioning_configuration {
|
|
118
|
+
status = "Disabled"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
resource "aws_s3_bucket_server_side_encryption_configuration" "access_logs_encryption" {
|
|
123
|
+
bucket = aws_s3_bucket.access_logs.id
|
|
124
|
+
|
|
125
|
+
rule {
|
|
126
|
+
apply_server_side_encryption_by_default {
|
|
127
|
+
kms_master_key_id = aws_kms_key.website_key.arn
|
|
128
|
+
sse_algorithm = "aws:kms"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
resource "aws_s3_bucket_public_access_block" "access_logs_pab" {
|
|
134
|
+
bucket = aws_s3_bucket.access_logs.id
|
|
135
|
+
|
|
136
|
+
block_public_acls = true
|
|
137
|
+
block_public_policy = true
|
|
138
|
+
ignore_public_acls = true
|
|
139
|
+
restrict_public_buckets = true
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
resource "aws_s3_bucket_policy" "access_logs_ssl_policy" {
|
|
143
|
+
bucket = aws_s3_bucket.access_logs.id
|
|
144
|
+
|
|
145
|
+
policy = jsonencode({
|
|
146
|
+
Version = "2012-10-17"
|
|
147
|
+
Statement = [
|
|
148
|
+
{
|
|
149
|
+
Sid = "DenyInsecureConnections"
|
|
150
|
+
Effect = "Deny"
|
|
151
|
+
Principal = "*"
|
|
152
|
+
Action = "s3:*"
|
|
153
|
+
Resource = [
|
|
154
|
+
aws_s3_bucket.access_logs.arn,
|
|
155
|
+
"${aws_s3_bucket.access_logs.arn}/*"
|
|
156
|
+
]
|
|
157
|
+
Condition = {
|
|
158
|
+
Bool = {
|
|
159
|
+
"aws:SecureTransport" = "false"
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# Website Bucket
|
|
168
|
+
resource "aws_s3_bucket" "website" {
|
|
169
|
+
#checkov:skip=CKV2_AWS_61:Lifecycle configuration not required for static website content
|
|
170
|
+
#checkov:skip=CKV_AWS_144:Cross-region replication not required for static website
|
|
171
|
+
#checkov:skip=CKV2_AWS_62:Event notifications not required for static website bucket
|
|
172
|
+
bucket = "${lower(var.website_name)}-website-${random_id.bucket_suffix.hex}"
|
|
173
|
+
force_destroy = true
|
|
174
|
+
|
|
175
|
+
tags = {
|
|
176
|
+
Name = "${lower(var.website_name)}-website"
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
resource "aws_s3_bucket_versioning" "website_versioning" {
|
|
181
|
+
bucket = aws_s3_bucket.website.id
|
|
182
|
+
versioning_configuration {
|
|
183
|
+
status = "Enabled"
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
resource "aws_s3_bucket_server_side_encryption_configuration" "website_encryption" {
|
|
188
|
+
bucket = aws_s3_bucket.website.id
|
|
189
|
+
|
|
190
|
+
rule {
|
|
191
|
+
apply_server_side_encryption_by_default {
|
|
192
|
+
kms_master_key_id = aws_kms_key.website_key.arn
|
|
193
|
+
sse_algorithm = "aws:kms"
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
resource "aws_s3_bucket_public_access_block" "website_pab" {
|
|
199
|
+
bucket = aws_s3_bucket.website.id
|
|
200
|
+
|
|
201
|
+
block_public_acls = true
|
|
202
|
+
block_public_policy = true
|
|
203
|
+
ignore_public_acls = true
|
|
204
|
+
restrict_public_buckets = true
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
resource "aws_s3_bucket_ownership_controls" "website_ownership" {
|
|
208
|
+
bucket = aws_s3_bucket.website.id
|
|
209
|
+
|
|
210
|
+
rule {
|
|
211
|
+
object_ownership = "BucketOwnerEnforced"
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
resource "aws_s3_bucket_logging" "website_logging" {
|
|
216
|
+
bucket = aws_s3_bucket.website.id
|
|
217
|
+
|
|
218
|
+
target_bucket = aws_s3_bucket.access_logs.id
|
|
219
|
+
target_prefix = "website-access-logs/"
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
# Distribution Log Bucket
|
|
224
|
+
resource "aws_s3_bucket" "distribution_logs" {
|
|
225
|
+
#checkov:skip=CKV2_AWS_61:Lifecycle configuration not required for CloudFront logs
|
|
226
|
+
#checkov:skip=CKV_AWS_144:Cross-region replication not required for CloudFront logs
|
|
227
|
+
#checkov:skip=CKV2_AWS_62:Event notifications not required for CloudFront logs bucket
|
|
228
|
+
#checkov:skip=CKV_AWS_21:Versioning not required for CloudFront access logs
|
|
229
|
+
bucket = "${lower(var.website_name)}-distribution-logs-${random_id.bucket_suffix.hex}"
|
|
230
|
+
force_destroy = true
|
|
231
|
+
|
|
232
|
+
tags = {
|
|
233
|
+
Name = "${lower(var.website_name)}-distribution-logs"
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
resource "aws_s3_bucket_server_side_encryption_configuration" "distribution_logs_encryption" {
|
|
238
|
+
bucket = aws_s3_bucket.distribution_logs.id
|
|
239
|
+
|
|
240
|
+
rule {
|
|
241
|
+
apply_server_side_encryption_by_default {
|
|
242
|
+
kms_master_key_id = aws_kms_key.website_key.arn
|
|
243
|
+
sse_algorithm = "aws:kms"
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
resource "aws_s3_bucket_public_access_block" "distribution_logs_pab" {
|
|
249
|
+
bucket = aws_s3_bucket.distribution_logs.id
|
|
250
|
+
|
|
251
|
+
block_public_acls = true
|
|
252
|
+
block_public_policy = true
|
|
253
|
+
ignore_public_acls = true
|
|
254
|
+
restrict_public_buckets = true
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
resource "aws_s3_bucket_ownership_controls" "distribution_logs_ownership" {
|
|
258
|
+
#checkov:skip=CKV2_AWS_65:BucketOwnerPreferred required for CloudFront logging compatibility
|
|
259
|
+
bucket = aws_s3_bucket.distribution_logs.id
|
|
260
|
+
|
|
261
|
+
rule {
|
|
262
|
+
object_ownership = "BucketOwnerPreferred"
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
resource "aws_s3_bucket_logging" "distribution_logs_logging" {
|
|
268
|
+
bucket = aws_s3_bucket.distribution_logs.id
|
|
269
|
+
|
|
270
|
+
target_bucket = aws_s3_bucket.access_logs.id
|
|
271
|
+
target_prefix = "distribution-access-logs/"
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
resource "aws_s3_bucket_policy" "distribution_logs_ssl_policy" {
|
|
275
|
+
bucket = aws_s3_bucket.distribution_logs.id
|
|
276
|
+
|
|
277
|
+
policy = jsonencode({
|
|
278
|
+
Version = "2012-10-17"
|
|
279
|
+
Statement = [
|
|
280
|
+
{
|
|
281
|
+
Sid = "DenyInsecureConnections"
|
|
282
|
+
Effect = "Deny"
|
|
283
|
+
Principal = "*"
|
|
284
|
+
Action = "s3:*"
|
|
285
|
+
Resource = [
|
|
286
|
+
aws_s3_bucket.distribution_logs.arn,
|
|
287
|
+
"${aws_s3_bucket.distribution_logs.arn}/*"
|
|
288
|
+
]
|
|
289
|
+
Condition = {
|
|
290
|
+
Bool = {
|
|
291
|
+
"aws:SecureTransport" = "false"
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
]
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
# WAF Web ACL (must be in us-east-1 for CloudFront)
|
|
300
|
+
resource "aws_wafv2_web_acl" "cloudfront_waf" {
|
|
301
|
+
#checkov:skip=CKV2_AWS_31:WAF logging disabled
|
|
302
|
+
provider = aws.us_east_1
|
|
303
|
+
name = "${lower(var.website_name)}-cloudfront-waf-${random_id.unique_suffix.hex}"
|
|
304
|
+
scope = "CLOUDFRONT"
|
|
305
|
+
|
|
306
|
+
default_action {
|
|
307
|
+
allow {}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
rule {
|
|
311
|
+
name = "CRSRule"
|
|
312
|
+
priority = 0
|
|
313
|
+
|
|
314
|
+
override_action {
|
|
315
|
+
none {}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
statement {
|
|
319
|
+
managed_rule_group_statement {
|
|
320
|
+
name = "AWSManagedRulesCommonRuleSet"
|
|
321
|
+
vendor_name = "AWS"
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
visibility_config {
|
|
326
|
+
cloudwatch_metrics_enabled = true
|
|
327
|
+
metric_name = "MetricForWebACLCDK-CRS"
|
|
328
|
+
sampled_requests_enabled = true
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
rule {
|
|
333
|
+
name = "KnownBadInputsRule"
|
|
334
|
+
priority = 1
|
|
335
|
+
|
|
336
|
+
override_action {
|
|
337
|
+
none {}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
statement {
|
|
341
|
+
managed_rule_group_statement {
|
|
342
|
+
name = "AWSManagedRulesKnownBadInputsRuleSet"
|
|
343
|
+
vendor_name = "AWS"
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
visibility_config {
|
|
348
|
+
cloudwatch_metrics_enabled = true
|
|
349
|
+
metric_name = "MetricForWebACLCDK-KnownBadInputs"
|
|
350
|
+
sampled_requests_enabled = true
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
visibility_config {
|
|
355
|
+
cloudwatch_metrics_enabled = true
|
|
356
|
+
metric_name = "${lower(var.website_name)}-waf"
|
|
357
|
+
sampled_requests_enabled = true
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
tags = {
|
|
361
|
+
Name = "${lower(var.website_name)}-cloudfront-waf-${random_id.unique_suffix.hex}"
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
lifecycle {
|
|
365
|
+
create_before_destroy = true
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
# Origin Access Control
|
|
371
|
+
resource "aws_cloudfront_origin_access_control" "website_oac" {
|
|
372
|
+
name = "${lower(var.website_name)}-oac-${random_id.unique_suffix.hex}"
|
|
373
|
+
description = "Origin Access Control for ${lower(var.website_name)}"
|
|
374
|
+
origin_access_control_origin_type = "s3"
|
|
375
|
+
signing_behavior = "always"
|
|
376
|
+
signing_protocol = "sigv4"
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
# CloudFront Distribution
|
|
380
|
+
resource "aws_cloudfront_distribution" "website" {
|
|
381
|
+
#checkov:skip=CKV_AWS_174:Using CloudFront default certificate which does not support TLS v1.2
|
|
382
|
+
#checkov:skip=CKV_AWS_310:Origin failover not required for single S3 origin static website
|
|
383
|
+
#checkov:skip=CKV_AWS_374:Geo restrictions not required for global web application
|
|
384
|
+
#checkov:skip=CKV2_AWS_42:Custom SSL certificate not required for development - using CloudFront default
|
|
385
|
+
#checkov:skip=CKV2_AWS_32:Response headers policy not required for basic static website
|
|
386
|
+
#checkov:skip=CKV2_AWS_47:WAF includes AWSManagedRulesKnownBadInputsRuleSet which provides Log4j protection
|
|
387
|
+
origin {
|
|
388
|
+
domain_name = aws_s3_bucket.website.bucket_regional_domain_name
|
|
389
|
+
origin_access_control_id = aws_cloudfront_origin_access_control.website_oac.id
|
|
390
|
+
origin_id = "S3-${aws_s3_bucket.website.bucket}"
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
enabled = true
|
|
394
|
+
is_ipv6_enabled = true
|
|
395
|
+
default_root_object = "index.html"
|
|
396
|
+
web_acl_id = aws_wafv2_web_acl.cloudfront_waf.arn
|
|
397
|
+
|
|
398
|
+
logging_config {
|
|
399
|
+
include_cookies = false
|
|
400
|
+
bucket = aws_s3_bucket.distribution_logs.bucket_regional_domain_name
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
default_cache_behavior {
|
|
404
|
+
allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
|
|
405
|
+
cached_methods = ["GET", "HEAD"]
|
|
406
|
+
target_origin_id = "S3-${aws_s3_bucket.website.bucket}"
|
|
407
|
+
|
|
408
|
+
forwarded_values {
|
|
409
|
+
query_string = false
|
|
410
|
+
cookies {
|
|
411
|
+
forward = "none"
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
viewer_protocol_policy = "redirect-to-https"
|
|
416
|
+
min_ttl = 0
|
|
417
|
+
default_ttl = 3600
|
|
418
|
+
max_ttl = 86400
|
|
419
|
+
compress = true
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
# Custom error responses for SPA routing
|
|
423
|
+
custom_error_response {
|
|
424
|
+
error_code = 404
|
|
425
|
+
response_code = 200
|
|
426
|
+
response_page_path = "/index.html"
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
custom_error_response {
|
|
430
|
+
error_code = 403
|
|
431
|
+
response_code = 200
|
|
432
|
+
response_page_path = "/index.html"
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
restrictions {
|
|
436
|
+
geo_restriction {
|
|
437
|
+
restriction_type = "none"
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
viewer_certificate {
|
|
442
|
+
cloudfront_default_certificate = true
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
tags = {
|
|
446
|
+
Name = "${lower(var.website_name)}-distribution-${random_id.unique_suffix.hex}"
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
lifecycle {
|
|
450
|
+
replace_triggered_by = [
|
|
451
|
+
aws_wafv2_web_acl.cloudfront_waf
|
|
452
|
+
]
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
# S3 Bucket Policy for CloudFront OAC
|
|
457
|
+
resource "aws_s3_bucket_policy" "website_cloudfront_policy" {
|
|
458
|
+
bucket = aws_s3_bucket.website.id
|
|
459
|
+
|
|
460
|
+
policy = jsonencode({
|
|
461
|
+
Version = "2012-10-17"
|
|
462
|
+
Statement = [
|
|
463
|
+
{
|
|
464
|
+
Sid = "AllowCloudFrontServicePrincipal"
|
|
465
|
+
Effect = "Allow"
|
|
466
|
+
Principal = {
|
|
467
|
+
Service = "cloudfront.amazonaws.com"
|
|
468
|
+
}
|
|
469
|
+
Action = "s3:GetObject"
|
|
470
|
+
Resource = "${aws_s3_bucket.website.arn}/*"
|
|
471
|
+
Condition = {
|
|
472
|
+
StringEquals = {
|
|
473
|
+
"AWS:SourceArn" = aws_cloudfront_distribution.website.arn
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
Sid = "DenyInsecureConnections"
|
|
479
|
+
Effect = "Deny"
|
|
480
|
+
Principal = "*"
|
|
481
|
+
Action = "s3:*"
|
|
482
|
+
Resource = [
|
|
483
|
+
aws_s3_bucket.website.arn,
|
|
484
|
+
"${aws_s3_bucket.website.arn}/*"
|
|
485
|
+
]
|
|
486
|
+
Condition = {
|
|
487
|
+
Bool = {
|
|
488
|
+
"aws:SecureTransport" = "false"
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
]
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
depends_on = [aws_cloudfront_distribution.website]
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
# Read runtime config using the reader module
|
|
499
|
+
module "runtime_config_reader" {
|
|
500
|
+
source = "../runtime-config/read"
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
# Upload website files to S3
|
|
504
|
+
resource "null_resource" "upload_website_files" {
|
|
505
|
+
triggers = {
|
|
506
|
+
# Trigger on any change to the website directory
|
|
507
|
+
website_path = var.website_file_path
|
|
508
|
+
# Trigger if any file in the directory changes using directory hash
|
|
509
|
+
directory_hash = sha256(join("", [for f in fileset(var.website_file_path, "**") : filesha256("${var.website_file_path}/${f}")]))
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
provisioner "local-exec" {
|
|
513
|
+
command = <<-EOT
|
|
514
|
+
cd "${path.root}"
|
|
515
|
+
uv run --with boto3 python3 -c "
|
|
516
|
+
import os
|
|
517
|
+
import sys
|
|
518
|
+
import boto3
|
|
519
|
+
import mimetypes
|
|
520
|
+
from pathlib import Path
|
|
521
|
+
from botocore.exceptions import ClientError, NoCredentialsError
|
|
522
|
+
|
|
523
|
+
def sync_to_s3(local_path, bucket_name):
|
|
524
|
+
try:
|
|
525
|
+
s3_client = boto3.client('s3')
|
|
526
|
+
|
|
527
|
+
# Check if local directory exists
|
|
528
|
+
if not os.path.isdir(local_path):
|
|
529
|
+
print(f'Error: Website directory not found at {local_path}')
|
|
530
|
+
sys.exit(1)
|
|
531
|
+
|
|
532
|
+
# Get existing objects in bucket (for deletion)
|
|
533
|
+
try:
|
|
534
|
+
existing_objects = set()
|
|
535
|
+
paginator = s3_client.get_paginator('list_objects_v2')
|
|
536
|
+
for page in paginator.paginate(Bucket=bucket_name):
|
|
537
|
+
if 'Contents' in page:
|
|
538
|
+
for obj in page['Contents']:
|
|
539
|
+
existing_objects.add(obj['Key'])
|
|
540
|
+
except ClientError as e:
|
|
541
|
+
if e.response['Error']['Code'] != 'NoSuchBucket':
|
|
542
|
+
raise
|
|
543
|
+
existing_objects = set()
|
|
544
|
+
|
|
545
|
+
# Upload files
|
|
546
|
+
uploaded_objects = set()
|
|
547
|
+
local_path_obj = Path(local_path)
|
|
548
|
+
|
|
549
|
+
for file_path in local_path_obj.rglob('*'):
|
|
550
|
+
if file_path.is_file():
|
|
551
|
+
# Skip runtime-config.json as it's handled separately
|
|
552
|
+
if file_path.name == 'runtime-config.json':
|
|
553
|
+
continue
|
|
554
|
+
|
|
555
|
+
# Calculate S3 key (relative path from local_path)
|
|
556
|
+
relative_path = file_path.relative_to(local_path_obj)
|
|
557
|
+
s3_key = str(relative_path).replace('\\\\', '/')
|
|
558
|
+
uploaded_objects.add(s3_key)
|
|
559
|
+
|
|
560
|
+
# Determine content type
|
|
561
|
+
content_type, _ = mimetypes.guess_type(str(file_path))
|
|
562
|
+
if content_type is None:
|
|
563
|
+
content_type = 'binary/octet-stream'
|
|
564
|
+
|
|
565
|
+
# Upload file
|
|
566
|
+
try:
|
|
567
|
+
s3_client.upload_file(
|
|
568
|
+
str(file_path),
|
|
569
|
+
bucket_name,
|
|
570
|
+
s3_key,
|
|
571
|
+
ExtraArgs={'ContentType': content_type}
|
|
572
|
+
)
|
|
573
|
+
print(f'Uploaded: {s3_key}')
|
|
574
|
+
except ClientError as e:
|
|
575
|
+
print(f'Error uploading {s3_key}: {e}')
|
|
576
|
+
sys.exit(1)
|
|
577
|
+
|
|
578
|
+
# Delete objects that no longer exist locally (excluding runtime-config.json)
|
|
579
|
+
objects_to_delete = existing_objects - uploaded_objects - {'runtime-config.json'}
|
|
580
|
+
if objects_to_delete:
|
|
581
|
+
delete_objects = [{'Key': key} for key in objects_to_delete]
|
|
582
|
+
try:
|
|
583
|
+
s3_client.delete_objects(
|
|
584
|
+
Bucket=bucket_name,
|
|
585
|
+
Delete={'Objects': delete_objects}
|
|
586
|
+
)
|
|
587
|
+
for obj in delete_objects:
|
|
588
|
+
print(f'Deleted: {obj[\"Key\"]}')
|
|
589
|
+
except ClientError as e:
|
|
590
|
+
print(f'Error deleting objects: {e}')
|
|
591
|
+
sys.exit(1)
|
|
592
|
+
|
|
593
|
+
print(f'Website files synced to s3://{bucket_name}/')
|
|
594
|
+
|
|
595
|
+
except NoCredentialsError:
|
|
596
|
+
print('Error: AWS credentials not found')
|
|
597
|
+
sys.exit(1)
|
|
598
|
+
except Exception as e:
|
|
599
|
+
print(f'Error: {e}')
|
|
600
|
+
sys.exit(1)
|
|
601
|
+
|
|
602
|
+
# Execute sync
|
|
603
|
+
sync_to_s3('${var.website_file_path}', '${aws_s3_bucket.website.bucket}')
|
|
604
|
+
"
|
|
605
|
+
EOT
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
depends_on = [aws_s3_bucket_policy.website_cloudfront_policy]
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
# Upload runtime config file
|
|
612
|
+
resource "aws_s3_object" "runtime_config" {
|
|
613
|
+
bucket = aws_s3_bucket.website.id
|
|
614
|
+
key = "runtime-config.json"
|
|
615
|
+
content = module.runtime_config_reader.config_json
|
|
616
|
+
content_type = "application/json"
|
|
617
|
+
etag = md5(module.runtime_config_reader.config_json)
|
|
618
|
+
|
|
619
|
+
depends_on = [null_resource.upload_website_files]
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
# Invalidate CloudFront cache after uploads
|
|
623
|
+
resource "null_resource" "cloudfront_invalidation" {
|
|
624
|
+
triggers = {
|
|
625
|
+
# Trigger when files or runtime config change
|
|
626
|
+
files_trigger = null_resource.upload_website_files.id
|
|
627
|
+
config_trigger = aws_s3_object.runtime_config.etag
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
provisioner "local-exec" {
|
|
631
|
+
command = <<-EOT
|
|
632
|
+
uv run --with boto3 python3 -c "
|
|
633
|
+
import boto3
|
|
634
|
+
import sys
|
|
635
|
+
from botocore.exceptions import ClientError, NoCredentialsError
|
|
636
|
+
|
|
637
|
+
def create_invalidation(distribution_id):
|
|
638
|
+
try:
|
|
639
|
+
cloudfront_client = boto3.client('cloudfront')
|
|
640
|
+
|
|
641
|
+
# Create invalidation for all paths
|
|
642
|
+
response = cloudfront_client.create_invalidation(
|
|
643
|
+
DistributionId=distribution_id,
|
|
644
|
+
InvalidationBatch={
|
|
645
|
+
'Paths': {
|
|
646
|
+
'Quantity': 1,
|
|
647
|
+
'Items': ['/*']
|
|
648
|
+
},
|
|
649
|
+
'CallerReference': f'terraform-invalidation-{distribution_id}-{hash(distribution_id) % 1000000}'
|
|
650
|
+
}
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
invalidation_id = response['Invalidation']['Id']
|
|
654
|
+
print(f'CloudFront cache invalidation created: {invalidation_id}')
|
|
655
|
+
print(f'Distribution: ${aws_cloudfront_distribution.website.id}')
|
|
656
|
+
print(f'Status: {response[\"Invalidation\"][\"Status\"]}')
|
|
657
|
+
|
|
658
|
+
except NoCredentialsError:
|
|
659
|
+
print('Error: AWS credentials not found')
|
|
660
|
+
sys.exit(1)
|
|
661
|
+
except ClientError as e:
|
|
662
|
+
print(f'Error creating CloudFront invalidation: {e}')
|
|
663
|
+
sys.exit(1)
|
|
664
|
+
except Exception as e:
|
|
665
|
+
print(f'Error: {e}')
|
|
666
|
+
sys.exit(1)
|
|
667
|
+
|
|
668
|
+
# Execute invalidation
|
|
669
|
+
create_invalidation('${aws_cloudfront_distribution.website.id}')
|
|
670
|
+
"
|
|
671
|
+
EOT
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
depends_on = [
|
|
675
|
+
null_resource.upload_website_files,
|
|
676
|
+
aws_s3_object.runtime_config
|
|
677
|
+
]
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
# Outputs
|
|
681
|
+
output "website_bucket_name" {
|
|
682
|
+
description = "Name of the S3 bucket hosting the website"
|
|
683
|
+
value = aws_s3_bucket.website.bucket
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
output "website_bucket_arn" {
|
|
687
|
+
description = "ARN of the S3 bucket hosting the website"
|
|
688
|
+
value = aws_s3_bucket.website.arn
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
output "cloudfront_distribution_id" {
|
|
692
|
+
description = "ID of the CloudFront distribution"
|
|
693
|
+
value = aws_cloudfront_distribution.website.id
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
output "cloudfront_distribution_arn" {
|
|
697
|
+
description = "ARN of the CloudFront distribution"
|
|
698
|
+
value = aws_cloudfront_distribution.website.arn
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
output "cloudfront_domain_name" {
|
|
702
|
+
description = "Domain name of the CloudFront distribution"
|
|
703
|
+
value = aws_cloudfront_distribution.website.domain_name
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
output "waf_web_acl_arn" {
|
|
707
|
+
description = "ARN of the WAF Web ACL"
|
|
708
|
+
value = aws_wafv2_web_acl.cloudfront_waf.arn
|
|
709
|
+
}
|