From 77a5e6746c59fdea95691d26be293b0e7f117747 Mon Sep 17 00:00:00 2001 From: Kai Siren Date: Wed, 8 Jan 2025 10:33:58 -0800 Subject: [PATCH 1/3] Update IAM roles before running database migrations --- infra/modules/service/main.tf | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infra/modules/service/main.tf b/infra/modules/service/main.tf index 147a1b495..df5b0b4ee 100644 --- a/infra/modules/service/main.tf +++ b/infra/modules/service/main.tf @@ -155,6 +155,11 @@ resource "aws_ecs_task_definition" "app" { # Reference https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-networking.html network_mode = "awsvpc" + + depends_on = [ + aws_iam_role_policy.task_executor, + aws_iam_role_policy_attachment.extra_policies, + ] } resource "aws_ecs_cluster" "cluster" { From 7666c22aa6f5316a01cf0e3c3e5423e53f10041a Mon Sep 17 00:00:00 2001 From: "kai [they]" Date: Wed, 8 Jan 2025 10:30:33 -0800 Subject: [PATCH 2/3] [Issue #3160] Add transfer bucket env vars (#3451) ## Summary Relates to #3160 ### Time to review: __2 mins__ ## Changes Adds env vars for both API and analytics sides of the transfer bucket ## Testing I ran a test deploy on both API and analytics --- .../analytics/service/api_analytics_bucket.tf | 35 +++++++++++++++++++ infra/analytics/service/main.tf | 6 +++- infra/analytics/service/transfer_access.tf | 28 --------------- infra/api/app-config/env-config/s3_buckets.tf | 7 +++- 4 files changed, 46 insertions(+), 30 deletions(-) create mode 100644 infra/analytics/service/api_analytics_bucket.tf delete mode 100644 infra/analytics/service/transfer_access.tf diff --git a/infra/analytics/service/api_analytics_bucket.tf b/infra/analytics/service/api_analytics_bucket.tf new file mode 100644 index 000000000..4667eafab --- /dev/null +++ b/infra/analytics/service/api_analytics_bucket.tf @@ -0,0 +1,35 @@ +locals { + api_analytics_bucket_environment_variables = { + "API_ANALYTICS_BUCKET" : "s3://${data.aws_ssm_parameter.api_analytics_bucket_id.value}" + "API_ANALYTICS_DB_EXTRACTS_PATH" : "s3://${data.aws_ssm_parameter.api_analytics_bucket_id.value}/db-extracts" + } +} + +data "aws_ssm_parameter" "api_analytics_bucket_arn" { + name = "/buckets/api-${var.environment_name}/api-analytics-transfer/arn" +} + +data "aws_ssm_parameter" "api_analytics_bucket_id" { + name = "/buckets/api-${var.environment_name}/api-analytics-transfer/id" +} + +data "aws_iam_policy_document" "api_analytics_bucket_access" { + statement { + effect = "Allow" + resources = [ + data.aws_ssm_parameter.api_analytics_bucket_arn.value, + "${data.aws_ssm_parameter.api_analytics_bucket_arn.value}/*", + ] + actions = ["s3:Get*", "s3:List*"] + + principals { + type = "AWS" + identifiers = [module.service.app_service_arn] + } + } +} + +resource "aws_s3_bucket_policy" "api_analytics_bucket_access" { + bucket = data.aws_ssm_parameter.api_analytics_bucket_id.value + policy = data.aws_iam_policy_document.api_analytics_bucket_access.json +} diff --git a/infra/analytics/service/main.tf b/infra/analytics/service/main.tf index 8acae803a..ccc052f03 100644 --- a/infra/analytics/service/main.tf +++ b/infra/analytics/service/main.tf @@ -138,7 +138,11 @@ module "service" { } } - extra_environment_variables = merge(local.service_config.extra_environment_variables, { "ENVIRONMENT" : var.environment_name }) + extra_environment_variables = merge( + local.service_config.extra_environment_variables, + local.api_analytics_bucket_environment_variables, + { "ENVIRONMENT" : var.environment_name }, + ) secrets = concat( [for secret_name in keys(local.service_config.secrets) : { diff --git a/infra/analytics/service/transfer_access.tf b/infra/analytics/service/transfer_access.tf deleted file mode 100644 index 0788075cc..000000000 --- a/infra/analytics/service/transfer_access.tf +++ /dev/null @@ -1,28 +0,0 @@ -data "aws_ssm_parameter" "transfer_bucket_arn" { - name = "/buckets/api-${var.environment_name}/api-analytics-transfer/arn" -} - -data "aws_ssm_parameter" "transfer_bucket_id" { - name = "/buckets/api-${var.environment_name}/api-analytics-transfer/id" -} - -data "aws_iam_policy_document" "transfer_bucket_access" { - statement { - effect = "Allow" - resources = [ - data.aws_ssm_parameter.transfer_bucket_arn.value, - "${data.aws_ssm_parameter.transfer_bucket_arn.value}/*", - ] - actions = ["s3:Get*", "s3:List*"] - - principals { - type = "AWS" - identifiers = [module.service.app_service_arn] - } - } -} - -resource "aws_s3_bucket_policy" "transfer_bucket_access" { - bucket = data.aws_ssm_parameter.transfer_bucket_id.value - policy = data.aws_iam_policy_document.transfer_bucket_access.json -} diff --git a/infra/api/app-config/env-config/s3_buckets.tf b/infra/api/app-config/env-config/s3_buckets.tf index de2c55c00..97fedc0ab 100644 --- a/infra/api/app-config/env-config/s3_buckets.tf +++ b/infra/api/app-config/env-config/s3_buckets.tf @@ -30,7 +30,12 @@ locals { api-analytics-transfer = { env_var = "API_ANALYTICS_BUCKET" public = false - paths = [] + paths = [ + { + path = "/db-extracts" + env_var = "API_ANALYTICS_DB_EXTRACTS_PATH" + }, + ] } } } From dc07a1c51643f4483bb79ed47f48bd26c3d2905d Mon Sep 17 00:00:00 2001 From: Kai Siren Date: Wed, 8 Jan 2025 23:35:11 -0800 Subject: [PATCH 3/3] s3 cdn --- infra/api/service/main.tf | 2 + infra/frontend/service/main.tf | 2 +- infra/modules/service/cdn-logs.tf | 88 ------------- infra/modules/service/cdn.tf | 116 ----------------- infra/modules/service/cdn_alb.tf | 195 +++++++++++++++++++++++++++++ infra/modules/service/cdn_s3.tf | 195 +++++++++++++++++++++++++++++ infra/modules/service/main.tf | 4 + infra/modules/service/variables.tf | 16 ++- 8 files changed, 411 insertions(+), 207 deletions(-) delete mode 100644 infra/modules/service/cdn-logs.tf delete mode 100644 infra/modules/service/cdn.tf create mode 100644 infra/modules/service/cdn_alb.tf create mode 100644 infra/modules/service/cdn_s3.tf diff --git a/infra/api/service/main.tf b/infra/api/service/main.tf index f539ba78d..f6a0324b5 100644 --- a/infra/api/service/main.tf +++ b/infra/api/service/main.tf @@ -123,6 +123,8 @@ module "service" { max_capacity = local.service_config.instance_scaling_max_capacity min_capacity = local.service_config.instance_scaling_min_capacity enable_autoscaling = true + enable_s3_cdn = true + s3_cdn_bucket_name = "public-files" cpu = local.service_config.instance_cpu memory = local.service_config.instance_memory environment_name = var.environment_name diff --git a/infra/frontend/service/main.tf b/infra/frontend/service/main.tf index 7dd4f0425..7c54ad063 100644 --- a/infra/frontend/service/main.tf +++ b/infra/frontend/service/main.tf @@ -131,7 +131,7 @@ module "service" { cpu = local.service_config.instance_cpu memory = local.service_config.instance_memory enable_autoscaling = true - enable_cdn = true + enable_alb_cdn = true app_access_policy_arn = null migrator_access_policy_arn = null diff --git a/infra/modules/service/cdn-logs.tf b/infra/modules/service/cdn-logs.tf deleted file mode 100644 index 21b0a292a..000000000 --- a/infra/modules/service/cdn-logs.tf +++ /dev/null @@ -1,88 +0,0 @@ -resource "aws_s3_bucket" "cdn" { - count = var.enable_cdn ? 1 : 0 - - bucket_prefix = "${var.service_name}-cdn-access-logs" - force_destroy = false - # checkov:skip=CKV2_AWS_62:Event notification not necessary for this bucket especially due to likely use of lifecycle rules - # checkov:skip=CKV_AWS_18:Access logging was not considered necessary for this bucket - # checkov:skip=CKV_AWS_144:Not considered critical to the point of cross region replication - # checkov:skip=CKV_AWS_300:Known issue where Checkov gets confused by multiple rules - # checkov:skip=CKV_AWS_21:Bucket versioning is not worth it in this use case - # checkov:skip=CKV_AWS_145:Use KMS in future work - # checkov:skip=CKV2_AWS_65:We need ACLs for Cloudfront - # checkov:skip=CKV2_AWS_6:False positive - # checkov:skip=CKV2_AWS_61:False positive -} - -resource "aws_s3_bucket_ownership_controls" "cdn" { - count = var.enable_cdn ? 1 : 0 - - bucket = aws_s3_bucket.cdn[0].id - rule { - object_ownership = "BucketOwnerPreferred" - } - # checkov:skip=CKV2_AWS_65:We need ACLs for Cloudfront -} - -resource "aws_s3_bucket_acl" "cdn" { - count = var.enable_cdn ? 1 : 0 - - bucket = aws_s3_bucket.cdn[0].id - - acl = "log-delivery-write" - - depends_on = [aws_s3_bucket_ownership_controls.cdn[0]] -} - -resource "aws_s3_bucket_public_access_block" "cdn" { - count = var.enable_cdn ? 1 : 0 - - bucket = aws_s3_bucket.cdn[0].id - - block_public_acls = true - block_public_policy = true - ignore_public_acls = true - restrict_public_buckets = true -} - -data "aws_iam_policy_document" "cdn" { - count = var.enable_cdn ? 1 : 0 - - statement { - actions = [ - "s3:GetObject", - ] - - resources = [ - "${aws_s3_bucket.cdn[0].arn}/*", - ] - - principals { - type = "AWS" - identifiers = [aws_cloudfront_origin_access_identity.cdn[0].iam_arn] - } - } -} - -resource "aws_s3_bucket_policy" "cdn" { - count = var.enable_cdn ? 1 : 0 - - bucket = aws_s3_bucket.cdn[0].id - policy = data.aws_iam_policy_document.cdn[0].json -} - -resource "aws_s3_bucket_lifecycle_configuration" "cdn" { - count = var.enable_cdn ? 1 : 0 - - bucket = aws_s3_bucket.cdn[0].id - - rule { - id = "AbortIncompleteUpload" - status = "Enabled" - abort_incomplete_multipart_upload { - days_after_initiation = 7 - } - } - - # checkov:skip=CKV_AWS_300:There is a known issue where this check brings up false positives -} diff --git a/infra/modules/service/cdn.tf b/infra/modules/service/cdn.tf deleted file mode 100644 index 97019cd71..000000000 --- a/infra/modules/service/cdn.tf +++ /dev/null @@ -1,116 +0,0 @@ -locals { - # We have the option to route the CDN to multiple origins based on the origin id. - # We do not currently do this, though. - default_origin_id = "default" - ssl_protocols = ["TLSv1.2"] - minimum_protocol_version = "TLSv1.2_2021" - -} - -resource "aws_cloudfront_origin_access_identity" "cdn" { - count = var.enable_cdn ? 1 : 0 - - comment = "Origin Access Identity for CloudFront to access S3 bucket" -} - -resource "aws_cloudfront_cache_policy" "default" { - count = var.enable_cdn ? 1 : 0 - - name = var.service_name - - # Default to caching for 1 hour. - # The default TTL can be overriden by the `Cache-Control max-age` or `Expires` headers - # There's also a `max_ttl` option, which can be used to override the above headers. - min_ttl = 0 - default_ttl = 3600 - - parameters_in_cache_key_and_forwarded_to_origin { - cookies_config { - cookie_behavior = "all" - } - headers_config { - # The only options are "none" and "whitelist", there is no "all" option - header_behavior = "none" - } - query_strings_config { - query_string_behavior = "all" - } - } -} - -resource "aws_cloudfront_distribution" "cdn" { - count = var.enable_cdn ? 1 : 0 - - enabled = var.enable_cdn ? true : false - aliases = var.domain == null ? null : [var.domain] - - origin { - domain_name = aws_lb.alb[0].dns_name - origin_id = local.default_origin_id - custom_origin_config { - http_port = 80 - https_port = 443 - origin_protocol_policy = var.cert_arn == null ? "http-only" : "https-only" - - # See possible values here: - # https://docs.aws.amazon.com/cloudfront/latest/APIReference/API_OriginSslProtocols.html - origin_ssl_protocols = local.ssl_protocols - } - # https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/origin-shield.html - dynamic "origin_shield" { - for_each = var.cert_arn == null ? [1] : [] - content { - enabled = true - origin_shield_region = data.aws_region.current.name - } - } - } - - logging_config { - include_cookies = false - bucket = aws_s3_bucket.cdn[0].bucket_domain_name - } - - default_cache_behavior { - allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"] - cached_methods = ["GET", "HEAD", "OPTIONS"] - target_origin_id = local.default_origin_id - cache_policy_id = aws_cloudfront_cache_policy.default[0].id - compress = true - viewer_protocol_policy = var.cert_arn == null ? "allow-all" : "redirect-to-https" - - # Default to caching for 1 hour. - # The default TTL can be overriden by the `Cache-Control max-age` or `Expires` headers - # There's also a `max_ttl` option, which can be used to override the above headers. - min_ttl = 0 - default_ttl = 3600 - } - - restrictions { - geo_restriction { - restriction_type = "none" - } - } - - viewer_certificate { - acm_certificate_arn = var.cert_arn == null ? null : var.cert_arn - cloudfront_default_certificate = var.cert_arn == null ? true : false - minimum_protocol_version = local.minimum_protocol_version - ssl_support_method = "sni-only" - } - - depends_on = [ - aws_s3_bucket_public_access_block.cdn[0], - aws_s3_bucket_policy.cdn[0], - aws_s3_bucket.cdn[0], - ] - - #checkov:skip=CKV2_AWS_46:We aren't using a S3 origin - #checkov:skip=CKV_AWS_174:False positive - #checkov:skip=CKV_AWS_310:Configure a failover in future work - #checkov:skip=CKV_AWS_68:Configure WAF in future work - #checkov:skip=CKV2_AWS_47:Configure WAF in future work - #checkov:skip=CKV2_AWS_32:Configure response headers policy in future work - #checkov:skip=CKV_AWS_374:Ignore the geo restriction - #checkov:skip=CKV_AWS_305:We don't need a default root object... we don't need to redirect / to index.html. -} diff --git a/infra/modules/service/cdn_alb.tf b/infra/modules/service/cdn_alb.tf new file mode 100644 index 000000000..c08aa60ed --- /dev/null +++ b/infra/modules/service/cdn_alb.tf @@ -0,0 +1,195 @@ +resource "aws_cloudfront_origin_access_identity" "alb_cdn" { + count = var.enable_alb_cdn ? 1 : 0 + comment = "Origin Access Identity for CloudFront to access S3 bucket" +} + +resource "aws_cloudfront_cache_policy" "alb_cdn" { + count = var.enable_alb_cdn ? 1 : 0 + + name = var.service_name + + # Default to caching for 1 hour. + # The default TTL can be overriden by the `Cache-Control max-age` or `Expires` headers + # There's also a `max_ttl` option, which can be used to override the above headers. + min_ttl = 0 + default_ttl = 3600 + + parameters_in_cache_key_and_forwarded_to_origin { + cookies_config { + cookie_behavior = "all" + } + headers_config { + # The only options are "none" and "whitelist", there is no "all" option + header_behavior = "none" + } + query_strings_config { + query_string_behavior = "all" + } + } +} + +resource "aws_cloudfront_distribution" "alb_cdn" { + count = var.enable_alb_cdn ? 1 : 0 + + enabled = var.enable_alb_cdn ? true : false + aliases = var.domain == null ? null : [var.domain] + + origin { + domain_name = aws_lb.alb[0].dns_name + origin_id = local.cdn_default_origin_id + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = var.cert_arn == null ? "http-only" : "https-only" + + # See possible values here: + # https://docs.aws.amazon.com/cloudfront/latest/APIReference/API_OriginSslProtocols.html + origin_ssl_protocols = local.cdn_ssl_protocols + } + # https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/origin-shield.html + dynamic "origin_shield" { + for_each = var.cert_arn == null ? [1] : [] + content { + enabled = true + origin_shield_region = data.aws_region.current.name + } + } + } + + logging_config { + include_cookies = false + bucket = aws_s3_bucket.alb_cdn[0].bucket_domain_name + } + + default_cache_behavior { + allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"] + cached_methods = ["GET", "HEAD", "OPTIONS"] + target_origin_id = local.cdn_default_origin_id + cache_policy_id = aws_cloudfront_cache_policy.alb_cdn[0].id + compress = true + viewer_protocol_policy = var.cert_arn == null ? "allow-all" : "redirect-to-https" + + # Default to caching for 1 hour. + # The default TTL can be overriden by the `Cache-Control max-age` or `Expires` headers + # There's also a `max_ttl` option, which can be used to override the above headers. + min_ttl = 0 + default_ttl = 3600 + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + acm_certificate_arn = var.cert_arn == null ? null : var.cert_arn + cloudfront_default_certificate = var.cert_arn == null ? true : false + minimum_protocol_version = local.cdn_minimum_protocol_version + ssl_support_method = "sni-only" + } + + depends_on = [ + aws_s3_bucket_public_access_block.alb_cdn[0], + aws_s3_bucket_policy.alb_cdn[0], + aws_s3_bucket.alb_cdn[0], + ] + + #checkov:skip=CKV2_AWS_46:We aren't using a S3 origin + #checkov:skip=CKV_AWS_174:False positive + #checkov:skip=CKV_AWS_310:Configure a failover in future work + #checkov:skip=CKV_AWS_68:Configure WAF in future work + #checkov:skip=CKV2_AWS_47:Configure WAF in future work + #checkov:skip=CKV2_AWS_32:Configure response headers policy in future work + #checkov:skip=CKV_AWS_374:Ignore the geo restriction + #checkov:skip=CKV_AWS_305:We don't need a default root object... we don't need to redirect / to index.html. +} + +resource "aws_s3_bucket" "alb_cdn" { + count = var.enable_alb_cdn ? 1 : 0 + + bucket_prefix = "${var.service_name}-cdn-access-logs" + force_destroy = false + # checkov:skip=CKV2_AWS_62:Event notification not necessary for this bucket especially due to likely use of lifecycle rules + # checkov:skip=CKV_AWS_18:Access logging was not considered necessary for this bucket + # checkov:skip=CKV_AWS_144:Not considered critical to the point of cross region replication + # checkov:skip=CKV_AWS_300:Known issue where Checkov gets confused by multiple rules + # checkov:skip=CKV_AWS_21:Bucket versioning is not worth it in this use case + # checkov:skip=CKV_AWS_145:Use KMS in future work + # checkov:skip=CKV2_AWS_65:We need ACLs for Cloudfront + # checkov:skip=CKV2_AWS_6:False positive + # checkov:skip=CKV2_AWS_61:False positive +} + +resource "aws_s3_bucket_ownership_controls" "alb_cdn" { + count = var.enable_alb_cdn ? 1 : 0 + + bucket = aws_s3_bucket.alb_cdn[0].id + rule { + object_ownership = "BucketOwnerPreferred" + } + # checkov:skip=CKV2_AWS_65:We need ACLs for Cloudfront +} + +resource "aws_s3_bucket_acl" "alb_cdn" { + count = var.enable_alb_cdn ? 1 : 0 + + bucket = aws_s3_bucket.alb_cdn[0].id + + acl = "log-delivery-write" + + depends_on = [aws_s3_bucket_ownership_controls.alb_cdn[0]] +} + +resource "aws_s3_bucket_public_access_block" "alb_cdn" { + count = var.enable_alb_cdn ? 1 : 0 + + bucket = aws_s3_bucket.alb_cdn[0].id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +data "aws_iam_policy_document" "alb_cdn" { + count = var.enable_alb_cdn ? 1 : 0 + + statement { + actions = [ + "s3:GetObject", + ] + + resources = [ + "${aws_s3_bucket.alb_cdn[0].arn}/*", + ] + + principals { + type = "AWS" + identifiers = [aws_cloudfront_origin_access_identity.alb_cdn[0].iam_arn] + } + } +} + +resource "aws_s3_bucket_policy" "alb_cdn" { + count = var.enable_alb_cdn ? 1 : 0 + + bucket = aws_s3_bucket.alb_cdn[0].id + policy = data.aws_iam_policy_document.alb_cdn[0].json +} + +resource "aws_s3_bucket_lifecycle_configuration" "alb_cdn" { + count = var.enable_alb_cdn ? 1 : 0 + + bucket = aws_s3_bucket.alb_cdn[0].id + + rule { + id = "AbortIncompleteUpload" + status = "Enabled" + abort_incomplete_multipart_upload { + days_after_initiation = 7 + } + } + + # checkov:skip=CKV_AWS_300:There is a known issue where this check brings up false positives +} diff --git a/infra/modules/service/cdn_s3.tf b/infra/modules/service/cdn_s3.tf new file mode 100644 index 000000000..585bfb6a2 --- /dev/null +++ b/infra/modules/service/cdn_s3.tf @@ -0,0 +1,195 @@ +resource "aws_cloudfront_origin_access_identity" "s3_cdn" { + count = var.enable_s3_cdn ? 1 : 0 + comment = "Origin Access Identity for CloudFront to access S3 bucket" +} + +resource "aws_cloudfront_cache_policy" "s3_cdn" { + count = var.enable_s3_cdn ? 1 : 0 + + name = var.service_name + + # Default to caching for 1 hour. + # The default TTL can be overriden by the `Cache-Control max-age` or `Expires` headers + # There's also a `max_ttl` option, which can be used to override the above headers. + min_ttl = 0 + default_ttl = 3600 + + parameters_in_cache_key_and_forwarded_to_origin { + cookies_config { + cookie_behavior = "all" + } + headers_config { + # The only options are "none" and "whitelist", there is no "all" option + header_behavior = "none" + } + query_strings_config { + query_string_behavior = "all" + } + } +} + +resource "aws_cloudfront_distribution" "s3_cdn" { + count = var.enable_s3_cdn ? 1 : 0 + + enabled = var.enable_s3_cdn ? true : false + aliases = var.domain == null ? null : [var.domain] + + origin { + domain_name = aws_s3_bucket.s3_buckets[var.s3_cdn_bucket_name].bucket_regional_domain_name + origin_id = local.cdn_default_origin_id + custom_origin_config { + http_port = 80 + https_port = 443 + origin_protocol_policy = var.cert_arn == null ? "http-only" : "https-only" + + # See possible values here: + # https://docs.aws.amazon.com/cloudfront/latest/APIReference/API_OriginSslProtocols.html + origin_ssl_protocols = local.cdn_ssl_protocols + } + # https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/origin-shield.html + dynamic "origin_shield" { + for_each = var.cert_arn == null ? [1] : [] + content { + enabled = true + origin_shield_region = data.aws_region.current.name + } + } + } + + logging_config { + include_cookies = false + bucket = aws_s3_bucket.s3_cdn[0].bucket_domain_name + } + + default_cache_behavior { + allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"] + cached_methods = ["GET", "HEAD", "OPTIONS"] + target_origin_id = local.cdn_default_origin_id + cache_policy_id = aws_cloudfront_cache_policy.s3_cdn[0].id + compress = true + viewer_protocol_policy = var.cert_arn == null ? "allow-all" : "redirect-to-https" + + # Default to caching for 1 hour. + # The default TTL can be overriden by the `Cache-Control max-age` or `Expires` headers + # There's also a `max_ttl` option, which can be used to override the above headers. + min_ttl = 0 + default_ttl = 3600 + } + + restrictions { + geo_restriction { + restriction_type = "none" + } + } + + viewer_certificate { + acm_certificate_arn = var.cert_arn == null ? null : var.cert_arn + cloudfront_default_certificate = var.cert_arn == null ? true : false + minimum_protocol_version = local.cdn_minimum_protocol_version + ssl_support_method = "sni-only" + } + + depends_on = [ + aws_s3_bucket_public_access_block.s3_cdn[0], + aws_s3_bucket_policy.s3_cdn[0], + aws_s3_bucket.s3_cdn[0], + ] + + #checkov:skip=CKV2_AWS_46:We aren't using a S3 origin + #checkov:skip=CKV_AWS_174:False positive + #checkov:skip=CKV_AWS_310:Configure a failover in future work + #checkov:skip=CKV_AWS_68:Configure WAF in future work + #checkov:skip=CKV2_AWS_47:Configure WAF in future work + #checkov:skip=CKV2_AWS_32:Configure response headers policy in future work + #checkov:skip=CKV_AWS_374:Ignore the geo restriction + #checkov:skip=CKV_AWS_305:We don't need a default root object... we don't need to redirect / to index.html. +} + +resource "aws_s3_bucket" "s3_cdn" { + count = var.enable_s3_cdn ? 1 : 0 + + bucket_prefix = "${var.service_name}-cdn-access-logs" + force_destroy = false + # checkov:skip=CKV2_AWS_62:Event notification not necessary for this bucket especially due to likely use of lifecycle rules + # checkov:skip=CKV_AWS_18:Access logging was not considered necessary for this bucket + # checkov:skip=CKV_AWS_144:Not considered critical to the point of cross region replication + # checkov:skip=CKV_AWS_300:Known issue where Checkov gets confused by multiple rules + # checkov:skip=CKV_AWS_21:Bucket versioning is not worth it in this use case + # checkov:skip=CKV_AWS_145:Use KMS in future work + # checkov:skip=CKV2_AWS_65:We need ACLs for Cloudfront + # checkov:skip=CKV2_AWS_6:False positive + # checkov:skip=CKV2_AWS_61:False positive +} + +resource "aws_s3_bucket_ownership_controls" "s3_cdn" { + count = var.enable_s3_cdn ? 1 : 0 + + bucket = aws_s3_bucket.s3_cdn[0].id + rule { + object_ownership = "BucketOwnerPreferred" + } + # checkov:skip=CKV2_AWS_65:We need ACLs for Cloudfront +} + +resource "aws_s3_bucket_acl" "s3_cdn" { + count = var.enable_s3_cdn ? 1 : 0 + + bucket = aws_s3_bucket.s3_cdn[0].id + + acl = "log-delivery-write" + + depends_on = [aws_s3_bucket_ownership_controls.s3_cdn[0]] +} + +resource "aws_s3_bucket_public_access_block" "s3_cdn" { + count = var.enable_s3_cdn ? 1 : 0 + + bucket = aws_s3_bucket.s3_cdn[0].id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +data "aws_iam_policy_document" "s3_cdn" { + count = var.enable_s3_cdn ? 1 : 0 + + statement { + actions = [ + "s3:GetObject", + ] + + resources = [ + "${aws_s3_bucket.s3_cdn[0].arn}/*", + ] + + principals { + type = "AWS" + identifiers = [aws_cloudfront_origin_access_identity.s3_cdn[0].iam_arn] + } + } +} + +resource "aws_s3_bucket_policy" "s3_cdn" { + count = var.enable_s3_cdn ? 1 : 0 + + bucket = aws_s3_bucket.s3_cdn[0].id + policy = data.aws_iam_policy_document.s3_cdn[0].json +} + +resource "aws_s3_bucket_lifecycle_configuration" "s3_cdn" { + count = var.enable_s3_cdn ? 1 : 0 + + bucket = aws_s3_bucket.s3_cdn[0].id + + rule { + id = "AbortIncompleteUpload" + status = "Enabled" + abort_incomplete_multipart_upload { + days_after_initiation = 7 + } + } + + # checkov:skip=CKV_AWS_300:There is a known issue where this check brings up false positives +} diff --git a/infra/modules/service/main.tf b/infra/modules/service/main.tf index df5b0b4ee..a115881a8 100644 --- a/infra/modules/service/main.tf +++ b/infra/modules/service/main.tf @@ -27,6 +27,10 @@ locals { image_url = var.image_repository_url != null ? "${var.image_repository_url}:${var.image_tag}" : "${data.aws_ecr_repository.app[0].repository_url}:${var.image_tag}" hostname = var.hostname != null ? [{ name = "HOSTNAME", value = var.hostname }] : [] + cdn_default_origin_id = "default" + cdn_ssl_protocols = ["TLSv1.2"] + cdn_minimum_protocol_version = "TLSv1.2_2021" + base_environment_variables = concat([ { name : "PORT", value : tostring(var.container_port) }, { name : "AWS_REGION", value : data.aws_region.current.name }, diff --git a/infra/modules/service/variables.tf b/infra/modules/service/variables.tf index ad6dd31d5..0722bc9f0 100644 --- a/infra/modules/service/variables.tf +++ b/infra/modules/service/variables.tf @@ -204,12 +204,24 @@ variable "enable_load_balancer" { default = true } -variable "enable_cdn" { - description = "Whether to enable a CDN for the service" +variable "enable_alb_cdn" { + description = "Whether to enable an ALB backed CDN for the service" type = bool default = false } +variable "enable_s3_cdn" { + description = "Whether to enable a S3 backed CDN for the service" + type = bool + default = false +} + +variable "s3_cdn_bucket_name" { + description = "The name of the S3 bucket to use for the S3 CDN" + type = string + default = null +} + variable "healthcheck_command" { description = "The command to run to check the health of the container, used on the container health check" type = list(string)