Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Issue #680] Add basic CDN configuration #3082

Merged
merged 22 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions infra/frontend/service/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ module "service" {
public_subnet_ids = data.aws_subnets.public.ids
private_subnet_ids = data.aws_subnets.private.ids
cert_arn = local.domain != null ? data.aws_acm_certificate.cert[0].arn : null
domain = local.domain
hostname = module.app_config.hostname
desired_instance_count = local.service_config.instance_desired_instance_count
max_capacity = local.service_config.instance_scaling_max_capacity
Expand All @@ -131,6 +132,10 @@ module "service" {
cpu = local.service_config.instance_cpu
memory = local.service_config.instance_memory

# Enable the CDN for prod and staging environments, disable it for dev.
# This allows us to test the impact of the CDN by diffing staging and dev.
enable_cdn = contains(["prod", "staging"], var.environment_name) ? true : false

app_access_policy_arn = null
migrator_access_policy_arn = null

Expand Down
5 changes: 5 additions & 0 deletions infra/frontend/service/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ output "service_endpoint" {
value = module.service.public_endpoint
}

output "cdn_endpoint" {
description = "The CDN endpoint for the service."
value = module.service.cdn_endpoint
}

output "service_cluster_name" {
value = module.service.cluster_name
}
Expand Down
86 changes: 86 additions & 0 deletions infra/modules/service/cdn-logs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
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
}

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
}
111 changes: 111 additions & 0 deletions infra/modules/service/cdn.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
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"
}

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 = "default"

# Default to caching for 1 hour, with a minimum of 1 minute.
# 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 = 60
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]
default_root_object = "/"

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 = ["TLSv1.2"]
}
# 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, with a minimum of 1 minute.
# 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 = 60
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think we might want this as 0 is we set those in the app with the s-maxage= https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Expiration.html#ExpirationDownloadDist

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had this as 0 at first! I'll put it back

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
}

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
}
5 changes: 5 additions & 0 deletions infra/modules/service/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ output "public_endpoint" {
value = var.enable_load_balancer ? "http://${aws_lb.alb[0].dns_name}" : null
}

output "cdn_endpoint" {
description = "The CDN endpoint for the service."
value = var.enable_cdn ? aws_cloudfront_distribution.cdn[0].domain_name : null
}

output "cluster_name" {
value = aws_ecs_cluster.cluster.name
}
Expand Down
12 changes: 12 additions & 0 deletions infra/modules/service/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,12 @@ variable "readonly_root_filesystem" {
default = true
}

variable "domain" {
description = "The domain name for the service"
type = string
default = null
}

variable "drop_linux_capabilities" {
description = "Whether to drop linux parameters"
type = bool
Expand All @@ -174,6 +180,12 @@ variable "enable_load_balancer" {
default = true
}

variable "enable_cdn" {
description = "Whether to enable a CDN for the service"
type = bool
default = false
}

variable "healthcheck_command" {
description = "The command to run to check the health of the container, used on the container health check"
type = list(string)
Expand Down