Skip to content

Commit

Permalink
feat!: dbtp 928 add cdn endpoint module (#141)
Browse files Browse the repository at this point in the history
Co-authored-by: Will Gibson <[email protected]>
Co-authored-by: Gabe Naughton <[email protected]>
  • Loading branch information
3 people authored Jun 4, 2024
1 parent ad478e9 commit 20d6f5b
Show file tree
Hide file tree
Showing 15 changed files with 362 additions and 7 deletions.
2 changes: 1 addition & 1 deletion .checkov.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -338,4 +338,4 @@
]
}
]
}
}
37 changes: 34 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ For non-production: `internal.<application_name>.uktrade.digital`

For production: `internal.<application_name>.prod.uktrade.digital`

Additional domains (`cdn_domains_list`) are the domain names that will be configured in CloudFront. In the map the key is the fully qualified domain name and the value is the application's base domain (the application's Route 53 zone).


If there are multiple web services on the application, you can add the additional domain to your certificate by adding the prefix name (eg. `internal.static`) to the variable `additional_address_list` see extension.yml example below. `Note: this is just the prefix, no need to add env.uktrade.digital`

Expand All @@ -102,17 +102,48 @@ The R53 domains for non-production and production are stored in different AWS ac

example `extensions.yml` config.

```yaml
my-application-alb:
type: alb
environments:
dev:
additional_address_list:
- internal.my-web-service-2
```

## CDN

This module will create the CloudFront (CDN) endpoints for the application if enabled.

`cdn_domains_list` is a map of the domain names that will be configured in CloudFront.
* the key is the fully qualified domain name
* the value is an array containing the internal prefix and the base domain (the application's Route 53 zone).

### Optional settings:

To create a R53 record pointing to the CloudFront endpoint, set this to true. If not set, in non production this is set to true by default and set to false in production.
- enable_cdn_record: true

To turn on CloudFront logging to a S3 bucket, set this to true.
- enable_logging: true

example `extensions.yml` config.

```yaml
my-application-alb:
type: alb
environments:
dev:
cdn_domains_list:
dev.my-application.uktrade.digital: my-application.uktrade.digital
- dev.my-application.uktrade.digital: [ "internal", "my-application.uktrade.digital" ]
- dev.my-web-service-2.my-application.uktrade.digital: [ "internal.my-web-service-2", "my-application.uktrade.digital" ]
additional_address_list:
- internal.my-web-service-2
enable_cdn_record: false
enable_logging: true
prod:
domain: {my-application.great.gov.uk: "great.gov.uk"}
cdn_domains_list:
- my-application.prod.uktrade.digital: [ "internal", "my-application.prod.uktrade.digital" ]
```

## Monitoring
Expand Down
4 changes: 3 additions & 1 deletion application-load-balancer/locals.tf
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ locals {
additional_address_fqdn = try({ for k in var.config.additional_address_list : "${k}.${local.additional_address_domain}" => "${var.application}.${local.domain_suffix}" }, {})

# A List of domains that can be used in the Subject Alternative Name (SAN) part of the certificate.
san_list = merge(local.additional_address_fqdn, var.config.cdn_domains_list)
# Only select the domain from the value field of cdn_domain_list (drop "internal")
culled_san_list = try({ for k, v in var.config.cdn_domains_list : "${k}" => "${v[1]}" }, {})
san_list = merge(local.additional_address_fqdn, local.culled_san_list)

# Create a complete domain list, primary domain plus all CDN/SAN domains.
full_list = merge({ (local.domain_name) = "${var.application}.${local.domain_suffix}" }, local.san_list)
Expand Down
2 changes: 1 addition & 1 deletion application-load-balancer/tests/unit.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ variables {
vpc_name = "vpc-name"
config = {
domain_prefix = "dom-prefix",
cdn_domains_list = { "dev.my-application.uktrade.digital" : "my-application.uktrade.digital" },
cdn_domains_list = { "dev.my-application.uktrade.digital" : ["internal", "my-application.uktrade.digital"] }
}
}

Expand Down
2 changes: 1 addition & 1 deletion application-load-balancer/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ variable "config" {
type = object({
domain_prefix = optional(string)
env_root = optional(string)
cdn_domains_list = optional(map(string))
cdn_domains_list = optional(map(list(string)))
additional_address_list = optional(list(string))
})
}
57 changes: 57 additions & 0 deletions cdn/locals.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
locals {
tags = {
application = var.application
environment = var.environment
managed-by = "DBT Platform - Terraform"
copilot-application = var.application
copilot-environment = var.environment
}

# The primary domain for every application follows the naming standard documented under https://github.com/uktrade/terraform-platform-modules/blob/main/README.md#application-load-balancer-module
domain_suffix = var.environment == "prod" ? coalesce(var.config.env_root, "${var.application}.prod.uktrade.digital") : coalesce(var.config.env_root, "${var.environment}.${var.application}.uktrade.digital")

cdn_domains_list = coalesce(var.config.cdn_domains_list, {})

# To avoid overwrites in prod we do not want to update R53 records by default.
enable_cdn_record = coalesce(var.config.enable_cdn_record, var.environment == "prod" ? false : true)
cdn_records = local.enable_cdn_record ? local.cdn_domains_list : {}

# CDN logging buckets
logging_bucket = var.environment == "prod" ? "dbt-cloudfront-logs-prod.s3-eu-west-2.amazonaws.com" : "dbt-cloudfront-logs.s3-eu-west-2.amazonaws.com"

# Default configuration for CDN.
cdn_defaults = {
viewer_protocol_policy = coalesce(var.config.viewer_protocol_policy, "redirect-to-https")
viewer_certificate = {
minimum_protocol_version = coalesce(var.config.viewer_certificate_minimum_protocol_version, "TLSv1.2_2021")
ssl_support_method = coalesce(var.config.viewer_certificate_ssl_support_method, "sni-only")
}
forwarded_values = {
query_string = coalesce(var.config.forwarded_values_query_string, true)
headers = coalesce(var.config.forwarded_values_headers, ["*"])
cookies = {
forward = coalesce(var.config.forwarded_values_forward, "all")
}
}
allowed_methods = coalesce(var.config.allowed_methods, ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"])
cached_methods = coalesce(var.config.cached_methods, ["GET", "HEAD"])

origin = {
custom_origin_config = {
origin_protocol_policy = coalesce(var.config.origin_protocol_policy, "https-only")
origin_ssl_protocols = coalesce(var.config.origin_ssl_protocols, ["TLSv1.2"])
}
}
compress = coalesce(var.config.cdn_compress, true)

geo_restriction = {
restriction_type = coalesce(var.config.cdn_geo_restriction_type, "none")
locations = coalesce(var.config.cdn_geo_locations, [])
}

# By default logging is off on all distros.
logging_config = coalesce(var.config.enable_logging, false) ? { bucket = local.logging_bucket } : {}

default_waf = var.environment == "prod" ? coalesce(var.config.default_waf, "waf_sentinel_684092750218_default") : coalesce(var.config.default_waf, "waf_sentinel_011755346992_default")
}
}
127 changes: 127 additions & 0 deletions cdn/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
data "aws_wafv2_web_acl" "waf-default" {
provider = aws.domain-cdn
name = local.cdn_defaults.default_waf
scope = "CLOUDFRONT"
}

resource "aws_acm_certificate" "certificate" {
provider = aws.domain-cdn
for_each = local.cdn_domains_list

domain_name = each.key
validation_method = "DNS"
key_algorithm = "RSA_2048"
tags = local.tags
lifecycle {
create_before_destroy = true
}
}

resource "aws_acm_certificate_validation" "cert-validate" {
provider = aws.domain-cdn
for_each = local.cdn_domains_list
certificate_arn = aws_acm_certificate.certificate[each.key].arn
validation_record_fqdns = [for record in aws_route53_record.validation-record : record.fqdn]
}

data "aws_route53_zone" "domain-root" {
provider = aws.domain-cdn
for_each = local.cdn_domains_list
name = each.value[1]
}

resource "aws_route53_record" "validation-record" {
provider = aws.domain-cdn
for_each = local.cdn_domains_list
zone_id = data.aws_route53_zone.domain-root[each.key].zone_id
name = tolist(aws_acm_certificate.certificate[each.key].domain_validation_options)[0].resource_record_name
type = tolist(aws_acm_certificate.certificate[each.key].domain_validation_options)[0].resource_record_type
records = [tolist(aws_acm_certificate.certificate[each.key].domain_validation_options)[0].resource_record_value]
ttl = 300
}

resource "aws_cloudfront_distribution" "standard" {
# checkov:skip=CKV_AWS_305:This is managed in the application.
# checkov:skip=CKV_AWS_310:No fail-over origin required.
# checkov:skip=CKV2_AWS_32:Response headers policy not required.
# checkov:skip=CKV2_AWS_47:WAFv2 WebACL rules are set in https://gitlab.ci.uktrade.digital/webops/terraform-waf
depends_on = [aws_acm_certificate_validation.cert-validate]

provider = aws.domain-cdn
for_each = local.cdn_domains_list
enabled = true
is_ipv6_enabled = true
web_acl_id = data.aws_wafv2_web_acl.waf-default.arn
aliases = [each.key]

origin {
domain_name = "${each.value[0]}.${local.domain_suffix}"
origin_id = "${each.value[0]}.${local.domain_suffix}"
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = local.cdn_defaults.origin.custom_origin_config.origin_protocol_policy
origin_ssl_protocols = local.cdn_defaults.origin.custom_origin_config.origin_ssl_protocols
}
}

default_cache_behavior {
allowed_methods = local.cdn_defaults.allowed_methods
cached_methods = local.cdn_defaults.cached_methods
target_origin_id = "${each.value[0]}.${local.domain_suffix}"
forwarded_values {
query_string = local.cdn_defaults.forwarded_values.query_string
headers = local.cdn_defaults.forwarded_values.headers
cookies {
forward = local.cdn_defaults.forwarded_values.cookies.forward
}
}
compress = local.cdn_defaults.compress
viewer_protocol_policy = local.cdn_defaults.viewer_protocol_policy
min_ttl = 0
default_ttl = 86400
max_ttl = 31536000
}

viewer_certificate {
cloudfront_default_certificate = false
acm_certificate_arn = aws_acm_certificate.certificate[each.key].arn
minimum_protocol_version = local.cdn_defaults.viewer_certificate.minimum_protocol_version
ssl_support_method = local.cdn_defaults.viewer_certificate.ssl_support_method
}

restrictions {
geo_restriction {
restriction_type = local.cdn_defaults.geo_restriction.restriction_type
locations = local.cdn_defaults.geo_restriction.locations
}
}

dynamic "logging_config" {
for_each = local.cdn_defaults.logging_config
content {
bucket = local.cdn_defaults.logging_config.bucket
include_cookies = false
prefix = each.key
}
}

tags = local.tags
}

# This is only run if enable_cdn_record is set to true.
# Production default is false.
# Non prod this is true.
resource "aws_route53_record" "cdn-address" {
provider = aws.domain-cdn

for_each = local.cdn_records
zone_id = data.aws_route53_zone.domain-root[each.key].zone_id
name = each.key
type = "A"
alias {
name = aws_cloudfront_distribution.standard[each.key].domain_name
zone_id = aws_cloudfront_distribution.standard[each.key].hosted_zone_id
evaluate_target_health = false
}
}
12 changes: 12 additions & 0 deletions cdn/providers.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
terraform {
required_version = "~> 1.7"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5"
configuration_aliases = [
aws.domain-cdn,
]
}
}
}
56 changes: 56 additions & 0 deletions cdn/tests/unit.tftest.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
mock_provider "aws" {
alias = "domain-cdn"
}

mock_provider "aws" {
alias = "domain"
}

override_data {
target = data.aws_route53_zone.domain-root
values = {
count = 0
name = "my-application.uktrade.digital"
}
}

variables {
application = "app"
environment = "env"
vpc_name = "vpc-name"
config = {
domain_prefix = "dom-prefix",
cdn_domains_list = { "dev.my-application.uktrade.digital" : ["internal", "my-application.uktrade.digital"] }
}
}


run "aws_route53_record_unit_test" {
command = plan

assert {
condition = aws_route53_record.cdn-address["dev.my-application.uktrade.digital"].name == "dev.my-application.uktrade.digital"
error_message = "Should be: dev.my-application.uktrade.digital"
}

}

run "aws_acm_certificate_unit_test" {
command = plan

assert {
condition = aws_acm_certificate.certificate["dev.my-application.uktrade.digital"].domain_name == "dev.my-application.uktrade.digital"
error_message = "Should be: dev.my-application.uktrade.digital"
}

}

run "aws_cloudfront_distribution_unit_test" {
command = plan

assert {
condition = [for k in aws_cloudfront_distribution.standard["dev.my-application.uktrade.digital"].aliases : true if k == "dev.my-application.uktrade.digital"][0] == true
error_message = "Should be: [ dev.my-application.uktrade.digital, ]"
}

}
40 changes: 40 additions & 0 deletions cdn/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
variable "application" {
type = string
}

variable "environment" {
type = string
}

variable "vpc_name" {
type = string
}

variable "config" {
type = object({
domain_prefix = optional(string)
env_root = optional(string)
cdn_domains_list = optional(map(list(string)))
additional_address_list = optional(list(string))
enable_cdn_record = optional(bool)
enable_logging = optional(bool)

# CDN default overrides
viewer_certificate_minimum_protocol_version = optional(string)
viewer_certificate_ssl_support_method = optional(string)
forwarded_values_query_string = optional(bool)
forwarded_values_headers = optional(list(string))
forwarded_values_forward = optional(string)
viewer_protocol_policy = optional(string)
allowed_methods = optional(list(string))
cached_methods = optional(list(string))
default_waf = optional(string)
origin_protocol_policy = optional(string)
origin_ssl_protocols = optional(list(string))
cdn_compress = optional(bool)
cdn_geo_restriction_type = optional(string)
cdn_geo_locations = optional(list(string))
cdn_logging_bucket = optional(string)
cdn_logging_bucket_prefix = optional(string)
})
}
Loading

0 comments on commit 20d6f5b

Please sign in to comment.