@staticforge/aws 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +6 -2
- package/dist/.tsbuildinfo +1 -0
- package/dist/clients.d.ts +22 -0
- package/dist/clients.d.ts.map +1 -0
- package/dist/clients.js +64 -0
- package/dist/clients.js.map +1 -0
- package/dist/deployer.d.ts +6 -0
- package/dist/deployer.d.ts.map +1 -0
- package/dist/deployer.js +379 -0
- package/dist/deployer.js.map +1 -0
- package/dist/iac.d.ts +26 -0
- package/dist/iac.d.ts.map +1 -0
- package/dist/iac.js +223 -0
- package/dist/iac.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/routing.d.ts +32 -0
- package/dist/routing.d.ts.map +1 -0
- package/dist/routing.js +111 -0
- package/dist/routing.js.map +1 -0
- package/package.json +59 -3
- package/terraform/modules/site/cdn.tf +191 -0
- package/terraform/modules/site/ci.tf +117 -0
- package/terraform/modules/site/domain.tf +77 -0
- package/terraform/modules/site/logging.tf +82 -0
- package/terraform/modules/site/outputs.tf +69 -0
- package/terraform/modules/site/router.js +136 -0
- package/terraform/modules/site/router.tf +33 -0
- package/terraform/modules/site/storage.tf +107 -0
- package/terraform/modules/site/variables.tf +106 -0
- package/terraform/modules/site/versions.tf +11 -0
- package/terraform/state-bootstrap/main.tf +104 -0
- package/terraform/state-bootstrap/outputs.tf +11 -0
- package/terraform/state-bootstrap/variables.tf +21 -0
- package/terraform/state-bootstrap/versions.tf +10 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Routing at the edge: a viewer-request function maps clean URLs to objects and
|
|
2
|
+
# prefixes them with the active version, which lives in a KeyValueStore. A
|
|
3
|
+
# deploy or rollback is one KVS key update: no distribution change, and since
|
|
4
|
+
# the rewritten URI is the cache key, no invalidation either.
|
|
5
|
+
|
|
6
|
+
resource "aws_cloudfront_key_value_store" "site" {
|
|
7
|
+
name = substr("sforge-${local.id}", 0, 64)
|
|
8
|
+
comment = "staticforge ${local.id}: active version, previews and routes"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
# Seeded once; deploys and rollbacks change the value with the SDK, so the
|
|
12
|
+
# value is ignored here and never shows up as drift.
|
|
13
|
+
resource "aws_cloudfrontkeyvaluestore_key" "active" {
|
|
14
|
+
key_value_store_arn = aws_cloudfront_key_value_store.site.arn
|
|
15
|
+
key = "active"
|
|
16
|
+
value = "none"
|
|
17
|
+
|
|
18
|
+
lifecycle {
|
|
19
|
+
ignore_changes = [value]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
resource "aws_cloudfront_function" "router" {
|
|
24
|
+
name = substr("sforge-${local.id}-router", 0, 64)
|
|
25
|
+
comment = "staticforge URL routing and versions"
|
|
26
|
+
runtime = "cloudfront-js-2.0"
|
|
27
|
+
publish = true
|
|
28
|
+
code = replace(file("${path.module}/router.js"), "__SFORGE_CONFIG__", jsonencode({
|
|
29
|
+
trailingSlash = var.trailing_slash
|
|
30
|
+
previewSuffix = var.preview_domain == null ? "" : ".${var.preview_domain}"
|
|
31
|
+
}))
|
|
32
|
+
key_value_store_associations = [aws_cloudfront_key_value_store.site.arn]
|
|
33
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
locals {
|
|
2
|
+
id = "${var.name}-${var.target}"
|
|
3
|
+
# bucket_prefix may be at most 37 characters; a random suffix makes the name unique.
|
|
4
|
+
bucket_prefix = "${trimsuffix(substr("sforge-${local.id}", 0, 36), "-")}-"
|
|
5
|
+
tags = merge(var.tags, { "sforge:site" = var.name, "sforge:target" = var.target })
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
# Private content bucket. Deploys write v/<versionId>/...; there is no website
|
|
9
|
+
# endpoint and no public access: only this distribution can read it (OAC).
|
|
10
|
+
resource "aws_s3_bucket" "site" {
|
|
11
|
+
bucket_prefix = local.bucket_prefix
|
|
12
|
+
force_destroy = var.force_destroy
|
|
13
|
+
tags = local.tags
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
resource "aws_s3_bucket_public_access_block" "site" {
|
|
17
|
+
bucket = aws_s3_bucket.site.id
|
|
18
|
+
block_public_acls = true
|
|
19
|
+
block_public_policy = true
|
|
20
|
+
ignore_public_acls = true
|
|
21
|
+
restrict_public_buckets = true
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
resource "aws_s3_bucket_ownership_controls" "site" {
|
|
25
|
+
bucket = aws_s3_bucket.site.id
|
|
26
|
+
rule {
|
|
27
|
+
object_ownership = "BucketOwnerEnforced"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
resource "aws_s3_bucket_versioning" "site" {
|
|
32
|
+
bucket = aws_s3_bucket.site.id
|
|
33
|
+
versioning_configuration {
|
|
34
|
+
status = "Enabled"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
resource "aws_s3_bucket_server_side_encryption_configuration" "site" {
|
|
39
|
+
bucket = aws_s3_bucket.site.id
|
|
40
|
+
rule {
|
|
41
|
+
apply_server_side_encryption_by_default {
|
|
42
|
+
sse_algorithm = "AES256"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Deploy prefixes beyond retainVersions are deleted by sforge; this only
|
|
48
|
+
# cleans up overwritten object versions and failed uploads.
|
|
49
|
+
resource "aws_s3_bucket_lifecycle_configuration" "site" {
|
|
50
|
+
bucket = aws_s3_bucket.site.id
|
|
51
|
+
|
|
52
|
+
rule {
|
|
53
|
+
id = "expire-noncurrent-versions"
|
|
54
|
+
status = "Enabled"
|
|
55
|
+
filter {}
|
|
56
|
+
noncurrent_version_expiration {
|
|
57
|
+
noncurrent_days = 30
|
|
58
|
+
}
|
|
59
|
+
abort_incomplete_multipart_upload {
|
|
60
|
+
days_after_initiation = 1
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
depends_on = [aws_s3_bucket_versioning.site]
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
locals {
|
|
68
|
+
cloudfront_only = {
|
|
69
|
+
StringEquals = { "AWS:SourceArn" = aws_cloudfront_distribution.site.arn }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
resource "aws_s3_bucket_policy" "site" {
|
|
74
|
+
bucket = aws_s3_bucket.site.id
|
|
75
|
+
policy = jsonencode({
|
|
76
|
+
Version = "2012-10-17"
|
|
77
|
+
Statement = [
|
|
78
|
+
{
|
|
79
|
+
Sid = "CloudFrontRead"
|
|
80
|
+
Effect = "Allow"
|
|
81
|
+
Principal = { Service = "cloudfront.amazonaws.com" }
|
|
82
|
+
Action = "s3:GetObject"
|
|
83
|
+
Resource = "${aws_s3_bucket.site.arn}/*"
|
|
84
|
+
Condition = local.cloudfront_only
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
# Lets S3 answer 404 (not 403) for missing objects.
|
|
88
|
+
Sid = "CloudFrontList"
|
|
89
|
+
Effect = "Allow"
|
|
90
|
+
Principal = { Service = "cloudfront.amazonaws.com" }
|
|
91
|
+
Action = "s3:ListBucket"
|
|
92
|
+
Resource = aws_s3_bucket.site.arn
|
|
93
|
+
Condition = local.cloudfront_only
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
Sid = "DenyInsecureTransport"
|
|
97
|
+
Effect = "Deny"
|
|
98
|
+
Principal = "*"
|
|
99
|
+
Action = "s3:*"
|
|
100
|
+
Resource = [aws_s3_bucket.site.arn, "${aws_s3_bucket.site.arn}/*"]
|
|
101
|
+
Condition = { Bool = { "aws:SecureTransport" = "false" } }
|
|
102
|
+
},
|
|
103
|
+
]
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
depends_on = [aws_s3_bucket_public_access_block.site]
|
|
107
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
variable "name" {
|
|
2
|
+
description = "Project name (iac.name), used in resource names and tags."
|
|
3
|
+
type = string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
variable "target" {
|
|
7
|
+
description = "Target name, e.g. staging."
|
|
8
|
+
type = string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
variable "trailing_slash" {
|
|
12
|
+
description = "build.trailingSlash: how the router maps extensionless URLs."
|
|
13
|
+
type = string
|
|
14
|
+
default = "always"
|
|
15
|
+
validation {
|
|
16
|
+
condition = contains(["always", "never"], var.trailing_slash)
|
|
17
|
+
error_message = "trailing_slash must be \"always\" or \"never\"."
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
variable "domain" {
|
|
22
|
+
description = "Custom domain. Null serves the site from the *.cloudfront.net domain."
|
|
23
|
+
type = string
|
|
24
|
+
default = null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
variable "hosted_zone" {
|
|
28
|
+
description = "Route 53 hosted zone for DNS validation and alias records. Null prints the records instead."
|
|
29
|
+
type = string
|
|
30
|
+
default = null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
variable "preview_domain" {
|
|
34
|
+
description = "Parent domain of PR previews (pr-<n>.<preview_domain>). Needs domain."
|
|
35
|
+
type = string
|
|
36
|
+
default = null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
variable "wait_for_validation" {
|
|
40
|
+
description = "Wait for the ACM certificate to be validated (false only creates the certificate)."
|
|
41
|
+
type = bool
|
|
42
|
+
default = true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
variable "price_class" {
|
|
46
|
+
type = string
|
|
47
|
+
default = "PriceClass_100"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
variable "logging" {
|
|
51
|
+
description = "Write CloudFront standard access logs to a separate bucket."
|
|
52
|
+
type = bool
|
|
53
|
+
default = false
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
variable "waf_web_acl_arn" {
|
|
57
|
+
type = string
|
|
58
|
+
default = null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
variable "force_destroy" {
|
|
62
|
+
description = "Allow the content bucket to be deleted while it holds objects (set by sforge destroy)."
|
|
63
|
+
type = bool
|
|
64
|
+
default = false
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
variable "security_headers" {
|
|
68
|
+
description = "Security headers sent with every response."
|
|
69
|
+
type = object({
|
|
70
|
+
content_security_policy = optional(string)
|
|
71
|
+
search_content_security_policy = optional(string)
|
|
72
|
+
hsts_max_age = optional(number)
|
|
73
|
+
hsts_include_subdomains = optional(bool, false)
|
|
74
|
+
hsts_preload = optional(bool, false)
|
|
75
|
+
referrer_policy = optional(string, "strict-origin-when-cross-origin")
|
|
76
|
+
frame_options = optional(string, "SAMEORIGIN")
|
|
77
|
+
permissions_policy = optional(string)
|
|
78
|
+
})
|
|
79
|
+
default = {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
variable "search_paths" {
|
|
83
|
+
description = "Path patterns of the search page, which gets search_content_security_policy (Pagefind needs WebAssembly)."
|
|
84
|
+
type = list(string)
|
|
85
|
+
default = []
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
variable "ci" {
|
|
89
|
+
description = "GitHub Actions OIDC deploy role. Null creates none."
|
|
90
|
+
type = object({
|
|
91
|
+
repo = string
|
|
92
|
+
branches = list(string)
|
|
93
|
+
environments = list(string)
|
|
94
|
+
pull_requests = bool
|
|
95
|
+
create_oidc_provider = bool
|
|
96
|
+
oidc_provider_arn = optional(string)
|
|
97
|
+
state_bucket = optional(string)
|
|
98
|
+
state_key = optional(string)
|
|
99
|
+
})
|
|
100
|
+
default = null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
variable "tags" {
|
|
104
|
+
type = map(string)
|
|
105
|
+
default = {}
|
|
106
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Remote state for every staticforge site in one AWS account and region.
|
|
2
|
+
# State files are stored at <site>/<target>.tfstate and locked with S3 native
|
|
3
|
+
# locking (<key>.tflock objects, no DynamoDB table).
|
|
4
|
+
|
|
5
|
+
resource "aws_s3_bucket" "state" {
|
|
6
|
+
bucket = var.bucket_name
|
|
7
|
+
tags = merge(var.tags, { "sforge:role" = "state" })
|
|
8
|
+
|
|
9
|
+
lifecycle {
|
|
10
|
+
prevent_destroy = true
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
resource "aws_s3_bucket_public_access_block" "state" {
|
|
15
|
+
bucket = aws_s3_bucket.state.id
|
|
16
|
+
block_public_acls = true
|
|
17
|
+
block_public_policy = true
|
|
18
|
+
ignore_public_acls = true
|
|
19
|
+
restrict_public_buckets = true
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
resource "aws_s3_bucket_ownership_controls" "state" {
|
|
23
|
+
bucket = aws_s3_bucket.state.id
|
|
24
|
+
rule {
|
|
25
|
+
object_ownership = "BucketOwnerEnforced"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
resource "aws_s3_bucket_versioning" "state" {
|
|
30
|
+
bucket = aws_s3_bucket.state.id
|
|
31
|
+
versioning_configuration {
|
|
32
|
+
status = "Enabled"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
resource "aws_s3_bucket_server_side_encryption_configuration" "state" {
|
|
37
|
+
bucket = aws_s3_bucket.state.id
|
|
38
|
+
rule {
|
|
39
|
+
apply_server_side_encryption_by_default {
|
|
40
|
+
sse_algorithm = var.kms_key_arn == null ? "AES256" : "aws:kms"
|
|
41
|
+
kms_master_key_id = var.kms_key_arn
|
|
42
|
+
}
|
|
43
|
+
bucket_key_enabled = var.kms_key_arn != null
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
resource "aws_s3_bucket_lifecycle_configuration" "state" {
|
|
48
|
+
bucket = aws_s3_bucket.state.id
|
|
49
|
+
|
|
50
|
+
rule {
|
|
51
|
+
id = "expire-old-state-versions"
|
|
52
|
+
status = "Enabled"
|
|
53
|
+
filter {}
|
|
54
|
+
noncurrent_version_expiration {
|
|
55
|
+
noncurrent_days = var.noncurrent_version_days
|
|
56
|
+
}
|
|
57
|
+
abort_incomplete_multipart_upload {
|
|
58
|
+
days_after_initiation = 7
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
depends_on = [aws_s3_bucket_versioning.state]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
data "aws_iam_policy_document" "state" {
|
|
66
|
+
statement {
|
|
67
|
+
sid = "DenyInsecureTransport"
|
|
68
|
+
effect = "Deny"
|
|
69
|
+
actions = ["s3:*"]
|
|
70
|
+
resources = [aws_s3_bucket.state.arn, "${aws_s3_bucket.state.arn}/*"]
|
|
71
|
+
principals {
|
|
72
|
+
type = "*"
|
|
73
|
+
identifiers = ["*"]
|
|
74
|
+
}
|
|
75
|
+
condition {
|
|
76
|
+
test = "Bool"
|
|
77
|
+
variable = "aws:SecureTransport"
|
|
78
|
+
values = ["false"]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
statement {
|
|
83
|
+
sid = "DenyOutdatedTLS"
|
|
84
|
+
effect = "Deny"
|
|
85
|
+
actions = ["s3:*"]
|
|
86
|
+
resources = [aws_s3_bucket.state.arn, "${aws_s3_bucket.state.arn}/*"]
|
|
87
|
+
principals {
|
|
88
|
+
type = "*"
|
|
89
|
+
identifiers = ["*"]
|
|
90
|
+
}
|
|
91
|
+
condition {
|
|
92
|
+
test = "NumericLessThan"
|
|
93
|
+
variable = "s3:TlsVersion"
|
|
94
|
+
values = ["1.2"]
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
resource "aws_s3_bucket_policy" "state" {
|
|
100
|
+
bucket = aws_s3_bucket.state.id
|
|
101
|
+
policy = data.aws_iam_policy_document.state.json
|
|
102
|
+
|
|
103
|
+
depends_on = [aws_s3_bucket_public_access_block.state]
|
|
104
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
variable "bucket_name" {
|
|
2
|
+
description = "Name of the state bucket, e.g. sforge-state-<accountId>-<region>."
|
|
3
|
+
type = string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
variable "kms_key_arn" {
|
|
7
|
+
description = "KMS key for SSE-KMS. Null uses SSE-S3."
|
|
8
|
+
type = string
|
|
9
|
+
default = null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
variable "noncurrent_version_days" {
|
|
13
|
+
description = "Days to keep previous versions of state files."
|
|
14
|
+
type = number
|
|
15
|
+
default = 90
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
variable "tags" {
|
|
19
|
+
type = map(string)
|
|
20
|
+
default = {}
|
|
21
|
+
}
|