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

feat!: dbtp 928 add cdn endpoint module #141

Merged
merged 20 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from 12 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: 3 additions & 2 deletions .checkov.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@
{
"resource": "module.extensions-staging.module.alb.aws_security_group.alb-security-group",
"check_ids": [
"CKV_AWS_23"
"CKV_AWS_23",
"CKV2_AWS_5"
ejayesh marked this conversation as resolved.
Show resolved Hide resolved
]
}
]
Expand Down Expand Up @@ -337,4 +338,4 @@
]
}
]
}
}
39 changes: 36 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,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 @@ -92,19 +92,52 @@ 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.

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).
ejayesh marked this conversation as resolved.
Show resolved Hide resolved

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.great.gov.uk: "great.gov.uk"}
ejayesh marked this conversation as resolved.
Show resolved Hide resolved
```

Each item in the `cdn_domain_list` must include:
- Key: The endpoint name <myapp.mysite.com>
- Values: application internal prefix and base domain <"internal", "mysite.com">
ejayesh marked this conversation as resolved.
Show resolved Hide resolved

### 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.
ejayesh marked this conversation as resolved.
Show resolved Hide resolved
- enable_cdn_record: true

To turn on CloudFront logging to a S3 bucket set this to true.
ejayesh marked this conversation as resolved.
Show resolved Hide resolved
- enable_logging: true
ejayesh marked this conversation as resolved.
Show resolved Hide resolved


## Monitoring

This will provision a CloudWatch Compute Dashboard and Application Insights for `<application>-<environment>`.
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 these naming standard. See README.md
ejayesh marked this conversation as resolved.
Show resolved Hide resolved
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 not set here.
ejayesh marked this conversation as resolved.
Show resolved Hide resolved
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