Skip to content

Commit

Permalink
Merge pull request #68 from dxw/custom-cloudformation-stacks
Browse files Browse the repository at this point in the history
Custom CloudFormation stacks
  • Loading branch information
Stretch96 authored Mar 22, 2024
2 parents 2fb265f + 7b15c76 commit 2773f2b
Show file tree
Hide file tree
Showing 8 changed files with 152 additions and 3 deletions.
10 changes: 10 additions & 0 deletions README.md

Large diffs are not rendered by default.

67 changes: 67 additions & 0 deletions cloudformation-custom-stack-s3-template-store.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
resource "aws_s3_bucket" "cloudformation_custom_stack_template_store" {
count = local.enable_cloudformatian_s3_template_store ? 1 : 0

bucket = "${local.resource_prefix_hash}-cloudformation-custom-stack-templates"
}

resource "aws_s3_bucket_policy" "cloudformation_custom_stack_template_store" {
count = local.enable_cloudformatian_s3_template_store ? 1 : 0

bucket = aws_s3_bucket.cloudformation_custom_stack_template_store[0].id
policy = templatefile(
"${path.module}/policies/s3-bucket-policy.json.tpl",
{
statement = <<EOT
[
${templatefile("${path.root}/policies/s3-bucket-policy-statements/enforce-tls.json.tpl",
{
bucket_arn = aws_s3_bucket.cloudformation_custom_stack_template_store[0].arn
}
)}
]
EOT
}
)
}

resource "aws_s3_bucket_public_access_block" "cloudformation_custom_stack_template_store" {
count = local.enable_cloudformatian_s3_template_store ? 1 : 0

bucket = aws_s3_bucket.cloudformation_custom_stack_template_store[0].id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}

resource "aws_s3_bucket_versioning" "cloudformation_custom_stack_template_store" {
count = local.enable_cloudformatian_s3_template_store ? 1 : 0

bucket = aws_s3_bucket.cloudformation_custom_stack_template_store[0].id

versioning_configuration {
status = "Enabled"
}
}

resource "aws_s3_bucket_logging" "cloudformation_custom_stack_template_store" {
count = local.enable_cloudformatian_s3_template_store ? 1 : 0

bucket = aws_s3_bucket.cloudformation_custom_stack_template_store[0].id

target_bucket = aws_s3_bucket.infrastructure_logs[0].id
target_prefix = "s3/cloudformation-custom-stack-templates"
}

resource "aws_s3_bucket_server_side_encryption_configuration" "cloudformation_custom_stack_template_store" {
count = local.enable_cloudformatian_s3_template_store ? 1 : 0

bucket = aws_s3_bucket.cloudformation_custom_stack_template_store[0].id

rule {
apply_server_side_encryption_by_default {
kms_master_key_id = local.infrastructure_kms_encryption ? aws_kms_key.infrastructure[0].arn : null
sse_algorithm = local.infrastructure_kms_encryption ? "aws:kms" : "AES256"
}
}
}
11 changes: 11 additions & 0 deletions cloudformation-custom-stack.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
resource "aws_cloudformation_stack" "custom" {
for_each = local.custom_cloudformation_stacks

name = "${local.resource_prefix_hash}-${each.key}"
parameters = each.value["parameters"]
template_body = each.value["template_body"]
template_url = local.enable_cloudformatian_s3_template_store && each.value["s3_template_store_key"] != null ? sensitive(data.external.s3_presigned_url["${aws_s3_bucket.cloudformation_custom_stack_template_store[0].id}/${each.value["s3_template_store_key"]}"].result.url) : null
on_failure = each.value["on_failure"] != null ? each.value["on_failure"] : "DO_NOTHING"
notification_arns = []
capabilities = each.value["capabilities"] != null ? each.value["capabilities"] : []
}
13 changes: 13 additions & 0 deletions data.tf
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,16 @@ data "external" "ssm_dhmc_setting" {
setting_id = "arn:aws:ssm:${local.aws_region}:${local.aws_account_id}:servicesetting/ssm/managed-instance/default-ec2-instance-management-role"
}
}

data "external" "s3_presigned_url" {
for_each = local.enable_cloudformatian_s3_template_store ? local.s3_object_presign : []

program = ["/bin/bash", "external-data-scripts/s3-object-presign.sh"]
query = {
s3_path = each.value
}

depends_on = [
aws_s3_bucket.cloudformation_custom_stack_template_store,
]
}
13 changes: 13 additions & 0 deletions external-data-scripts/s3-object-presign.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/bash

set -e
set -o pipefail

eval "$(jq -r '@sh "S3_PATH=\(.s3_path)"')"

URL="$(aws s3 presign "s3://$S3_PATH")"
jq -n \
--arg url "$URL" \
'{
url: $url
}'
4 changes: 2 additions & 2 deletions kms-infrastructure.tf
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ resource "aws_kms_key" "infrastructure" {
[for k, v in local.custom_s3_buckets : aws_cloudfront_distribution.custom_s3_buckets[k].arn if v["cloudfront_dedicated_distribution"] == true && v["create_dedicated_kms_key"] == false],
[for k, v in local.custom_s3_buckets : aws_cloudfront_distribution.infrastructure_ecs_cluster_service_cloudfront[v["cloudfront_infrastructure_ecs_cluster_service"]].arn if v["cloudfront_infrastructure_ecs_cluster_service"] != null && v["create_dedicated_kms_key"] == false]
)))
})}${(local.infrastructure_vpc_flow_logs_s3_with_athena || contains([for service in local.infrastructure_ecs_cluster_services : service["cloudfront_access_logging_enabled"]], true)) && local.infrastructure_kms_encryption ? "," : ""}
})}${(local.infrastructure_vpc_flow_logs_s3_with_athena || local.enable_cloudformatian_s3_template_store || contains([for service in local.infrastructure_ecs_cluster_services : service["cloudfront_access_logging_enabled"]], true)) && local.infrastructure_kms_encryption ? "," : ""}
${templatefile("${path.root}/policies/kms-key-policy-statements/log-delivery-allow.json.tpl",
{
account_id = (local.infrastructure_vpc_flow_logs_s3_with_athena || contains([for service in local.infrastructure_ecs_cluster_services : service["cloudfront_access_logging_enabled"]], true)) || length(local.custom_s3_buckets) > 0 && local.infrastructure_kms_encryption ? local.aws_account_id : ""
account_id = (local.infrastructure_vpc_flow_logs_s3_with_athena || local.enable_cloudformatian_s3_template_store || contains([for service in local.infrastructure_ecs_cluster_services : service["cloudfront_access_logging_enabled"]], true)) || length(local.custom_s3_buckets) > 0 && local.infrastructure_kms_encryption ? local.aws_account_id : ""
region = local.aws_region
}
)}
Expand Down
10 changes: 9 additions & 1 deletion locals.tf
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ locals {
enable_infrastructure_logs_bucket = (
local.infrastructure_vpc_flow_logs_s3_with_athena ||
length(local.infrastructure_ecs_cluster_services) != 0 ||
length(local.custom_s3_buckets) != 0
length(local.custom_s3_buckets) != 0 ||
local.enable_cloudformatian_s3_template_store
)
logs_bucket_source_arns = concat(
local.infrastructure_vpc_flow_logs_s3_with_athena ? ["arn:aws:logs:${local.aws_region}:${local.aws_account_id}:*"] : [],
Expand Down Expand Up @@ -194,6 +195,13 @@ locals {

custom_s3_buckets = var.custom_s3_buckets

enable_cloudformatian_s3_template_store = var.enable_cloudformatian_s3_template_store != null ? var.enable_cloudformatian_s3_template_store : false
custom_cloudformation_stacks = var.custom_cloudformation_stacks

s3_object_presign = local.enable_cloudformatian_s3_template_store ? toset([
for k, v in local.custom_cloudformation_stacks : "${aws_s3_bucket.cloudformation_custom_stack_template_store[0].id}/${v["s3_template_store_key"]}" if v["s3_template_store_key"] != null
]) : []

default_tags = {
Project = local.project_name,
Infrastructure = local.infrastructure_name,
Expand Down
27 changes: 27 additions & 0 deletions variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -615,3 +615,30 @@ variable "custom_s3_buckets" {
cloudfront_infrastructure_ecs_cluster_service_path = optional(string, null)
}))
}

variable "enable_cloudformatian_s3_template_store" {
description = "Creates an S3 bucket to store custom CloudFormation templates, which can then be referenced in `custom_cloudformation_stacks`. A user with RW access to the bucket is also created."
type = bool
}

variable "custom_cloudformation_stacks" {
description = <<EOT
Map of CloudFormation stacks to deploy
{
stack-name = {
s3_template_store_key: The filename of a CloudFormation template that is stored within the S3 bucket, created by the `enable_cloudformatian_s3_template_store`
template_body: (Optional - use of s3_template_store_key is preferred) The CloudFormation template body
parameters: The CloudFormation template parameters ({ parameter-name = parameter-value, ... })
on_failure: What to do on failure, either 'DO_NOTHING', 'ROLLBACK' or 'DELETE'
capabilities: A list of capabilities. Valid values: `CAPABILITY_NAMED_IAM`, `CAPABILITY_IAM`, `CAPABILITY_AUTO_EXPAND`
}
}
EOT
type = map(object({
s3_template_store_key = optional(string, null)
template_body = optional(string, null)
parameters = optional(map(string), null)
on_failure = optional(string, null)
capabilities = optional(list(string), null)
}))
}

0 comments on commit 2773f2b

Please sign in to comment.