shoc-frontend-new/terraform/live/modules/environment-owned/main.tf
Adam Moussa 7ed0743b90
fix(terraform): pin SSM deploy parameter tier and data_type
HCP tried to replace the live parameters on refresh because those
attributes were computed. The apply role cannot DeleteParameter.
2026-09-18 15:09:56 -04:00

407 lines
10 KiB
HCL

locals {
bucket_arn = "arn:aws:s3:::${var.bucket_name}"
distribution_arn = "arn:aws:cloudfront::${var.aws_account_id}:distribution/${var.distribution_id}"
resource_tags = var.adoption_complete ? var.ownership_tags : var.pre_adoption_tags
bucket_tags = var.adoption_complete ? var.ownership_tags : var.pre_adoption_bucket_tags
deploy_role_tags = var.adoption_complete ? var.post_adoption_deploy_role_tags : var.pre_adoption_deploy_role_tags
ssm_prefix = "/shoc-frontend-new/${var.environment}"
github_subject = "repo:${var.github_repo}:environment:${var.environment}"
spa_rewrite_code = join("\n", [
"function handler(event) {",
" var request = event.request;",
" var uri = request.uri;",
" // No file extension after the last slash -> a client-side route.",
" if (uri.lastIndexOf('.') <= uri.lastIndexOf('/')) {",
" request.uri = '/index.html';",
" }",
" return request;",
"}",
])
}
data "aws_iam_policy_document" "site_bucket" {
dynamic "statement" {
for_each = var.adoption_complete ? [] : [1]
content {
effect = "Allow"
principals {
type = "AWS"
identifiers = [var.bucket_auto_delete_helper_role_arn]
}
actions = [
"s3:DeleteObject*",
"s3:GetBucket*",
"s3:List*",
"s3:PutBucketPolicy",
]
resources = [
local.bucket_arn,
"${local.bucket_arn}/*",
]
}
}
statement {
effect = "Allow"
principals {
type = "Service"
identifiers = ["cloudfront.amazonaws.com"]
}
actions = ["s3:GetObject"]
resources = ["${local.bucket_arn}/*"]
condition {
test = "StringEquals"
variable = "AWS:SourceArn"
values = [local.distribution_arn]
}
}
statement {
effect = "Deny"
principals {
type = "AWS"
identifiers = ["*"]
}
actions = ["s3:*"]
resources = [
local.bucket_arn,
"${local.bucket_arn}/*",
]
condition {
test = "Bool"
variable = "aws:SecureTransport"
values = ["false"]
}
}
}
data "aws_iam_policy_document" "github_deploy_assume" {
statement {
sid = "GithubDeployOidc"
effect = "Allow"
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [var.github_oidc_provider_arn]
}
condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:aud"
values = ["sts.amazonaws.com"]
}
condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:sub"
values = [local.github_subject]
}
# StringLike: a release-triggered job loads the workflow file from the tag,
# so job_workflow_ref ends in @refs/tags/vX.Y.Z-staging there and
# @refs/heads/main on push and workflow_dispatch. The environment claim in
# sub is the gate.
condition {
test = "StringLike"
variable = "token.actions.githubusercontent.com:job_workflow_ref"
values = [
"${var.github_repo}/.github/workflows/deploy-web.yaml@refs/heads/main",
"${var.github_repo}/.github/workflows/deploy-web.yaml@refs/tags/v*",
]
}
}
}
data "aws_iam_policy_document" "github_deploy" {
statement {
sid = "ListWebBucket"
effect = "Allow"
actions = [
"s3:GetBucketLocation",
"s3:ListBucket",
]
resources = [local.bucket_arn]
}
statement {
sid = "SyncWebBucket"
effect = "Allow"
actions = [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
]
resources = ["${local.bucket_arn}/*"]
}
statement {
sid = "InvalidateDistribution"
effect = "Allow"
actions = [
"cloudfront:CreateInvalidation",
"cloudfront:GetInvalidation",
"cloudfront:GetDistribution",
]
resources = [local.distribution_arn]
}
statement {
sid = "DeployParams"
effect = "Allow"
actions = [
"ssm:GetParameter",
]
resources = [
aws_ssm_parameter.deploy_bucket.arn,
aws_ssm_parameter.deploy_distribution_id.arn,
]
}
}
resource "aws_s3_bucket" "site" {
bucket = var.bucket_name
force_destroy = false
tags = local.bucket_tags
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_public_access_block" "site" {
bucket = aws_s3_bucket.site.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_ownership_controls" "site" {
bucket = aws_s3_bucket.site.id
rule {
object_ownership = "BucketOwnerEnforced"
}
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "site" {
bucket = aws_s3_bucket.site.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
bucket_key_enabled = false
}
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_versioning" "site" {
bucket = aws_s3_bucket.site.id
versioning_configuration {
status = "Enabled"
}
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_policy" "site" {
bucket = aws_s3_bucket.site.id
policy = data.aws_iam_policy_document.site_bucket.json
lifecycle {
prevent_destroy = true
}
}
# Pointer leftover from Terraform-promoted content CD. Forgotten, not destroyed.
# deploy-web.yaml syncs dist/ to the bucket root with --delete.
removed {
from = aws_s3_object.release_pointer
lifecycle {
destroy = false
}
}
resource "aws_cloudfront_origin_access_control" "site" {
name = var.origin_access_control_name
description = var.origin_access_control_description
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
lifecycle {
prevent_destroy = true
}
}
resource "aws_cloudfront_function" "spa_rewrite" {
name = var.function_name
runtime = "cloudfront-js-1.0"
comment = "SPA routing: rewrite extensionless paths to /index.html"
publish = true
code = local.spa_rewrite_code
tags = local.resource_tags
lifecycle {
prevent_destroy = true
ignore_changes = [publish]
}
}
resource "aws_cloudfront_distribution" "site" {
aliases = [var.domain_name]
comment = "SeaHaven SHOC frontend (${var.environment})"
default_root_object = "index.html"
enabled = true
http_version = "http2and3"
is_ipv6_enabled = true
price_class = "PriceClass_100"
tags = local.resource_tags
origin {
connection_attempts = 3
connection_timeout = 10
domain_name = aws_s3_bucket.site.bucket_regional_domain_name
origin_access_control_id = aws_cloudfront_origin_access_control.site.id
origin_id = var.origin_id
origin_path = ""
}
default_cache_behavior {
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cache_policy_id = var.cache_policy_id
cached_methods = ["GET", "HEAD"]
compress = true
target_origin_id = var.origin_id
viewer_protocol_policy = "redirect-to-https"
function_association {
event_type = "viewer-request"
function_arn = aws_cloudfront_function.spa_rewrite.arn
}
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
acm_certificate_arn = var.certificate_arn
minimum_protocol_version = "TLSv1.2_2021"
ssl_support_method = "sni-only"
}
lifecycle {
prevent_destroy = true
}
}
resource "aws_route53_record" "site_a" {
zone_id = var.hosted_zone_id
name = var.domain_name
type = "A"
alias {
name = aws_cloudfront_distribution.site.domain_name
zone_id = aws_cloudfront_distribution.site.hosted_zone_id
evaluate_target_health = false
}
lifecycle {
prevent_destroy = true
}
}
resource "aws_route53_record" "site_aaaa" {
zone_id = var.hosted_zone_id
name = var.domain_name
type = "AAAA"
alias {
name = aws_cloudfront_distribution.site.domain_name
zone_id = aws_cloudfront_distribution.site.hosted_zone_id
evaluate_target_health = false
}
lifecycle {
prevent_destroy = true
}
}
resource "aws_iam_role" "github_deploy" {
name = var.deploy_role_name
path = "/"
description = "GitHub Actions SPA deploy role for ${var.github_repo} Environment ${var.environment}"
assume_role_policy = data.aws_iam_policy_document.github_deploy_assume.json
max_session_duration = 3600
permissions_boundary = var.deploy_permissions_boundary_arn
tags = local.deploy_role_tags
lifecycle {
prevent_destroy = true
# SCP ProtectDeploymentPrincipalLifecycle denies UpdateRoleDescription on
# githubdeploy-* for HCP apply roles.
ignore_changes = [description]
}
}
resource "aws_iam_role_policy" "github_deploy" {
name = var.deploy_inline_policy_name
role = aws_iam_role.github_deploy.id
policy = data.aws_iam_policy_document.github_deploy.json
lifecycle {
prevent_destroy = true
}
}
resource "aws_ssm_parameter" "deploy_bucket" {
name = "${local.ssm_prefix}/deploy/bucket"
type = "String"
data_type = "text"
tier = "Standard"
value = aws_s3_bucket.site.id
description = "SPA origin bucket; deploy-web syncs dist/ to the bucket root"
tags = local.resource_tags
}
resource "aws_ssm_parameter" "deploy_distribution_id" {
name = "${local.ssm_prefix}/deploy/distribution-id"
type = "String"
data_type = "text"
tier = "Standard"
value = aws_cloudfront_distribution.site.id
description = "CloudFront distribution ID; deploy-web invalidates /* after sync"
tags = local.resource_tags
}