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 github_subject_operator = var.pre_adoption_github_subject_operator 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 { 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 = local.github_subject_operator variable = "token.actions.githubusercontent.com:sub" values = [var.github_subject] } } } data "aws_iam_policy_document" "github_deploy" { dynamic "statement" { for_each = !var.adoption_complete && var.environment == "dev" ? [1] : [] content { sid = "AssumeCdkBootstrapRoles" effect = "Allow" actions = ["sts:AssumeRole"] resources = ["arn:aws:iam::${var.aws_account_id}:role/cdk-hnb659fds-*"] } } dynamic "statement" { for_each = var.adoption_complete ? [] : [1] content { sid = "DescribeStack" effect = "Allow" actions = ["cloudformation:DescribeStacks"] resources = ["arn:aws:cloudformation:${var.aws_region}:${var.aws_account_id}:stack/${var.cloudformation_stack_name}/*"] } } dynamic "statement" { for_each = var.adoption_complete ? [] : [1] content { effect = "Allow" actions = [ "s3:Abort*", "s3:DeleteObject*", "s3:GetBucket*", "s3:GetObject*", "s3:List*", "s3:PutObject", "s3:PutObjectLegalHold", "s3:PutObjectRetention", "s3:PutObjectTagging", "s3:PutObjectVersionTagging", ] resources = [ local.bucket_arn, "${local.bucket_arn}/*", ] } } dynamic "statement" { for_each = var.adoption_complete ? [1] : [] content { sid = "ReadDeploymentBucket" effect = "Allow" actions = [ "s3:GetBucketLocation", "s3:GetBucketVersioning", "s3:ListBucket", "s3:ListBucketVersions", ] resources = [local.bucket_arn] } } dynamic "statement" { for_each = var.adoption_complete ? [1] : [] content { sid = "PublishAndRollbackSiteObjects" effect = "Allow" actions = [ "s3:DeleteObject", "s3:DeleteObjectVersion", "s3:GetObject", "s3:GetObjectVersion", "s3:PutObject", ] resources = ["${local.bucket_arn}/*"] } } statement { sid = "InvalidateDistribution" effect = "Allow" actions = [ "cloudfront:CreateInvalidation", "cloudfront:GetInvalidation", ] resources = [local.distribution_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 } } 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 } 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 deploy role for Sea-Haven-Industries/shoc-frontend-new@${var.deploy_branch}" 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 } } 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 } }