diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl index acb84d2..6907acc 100644 --- a/.terraform.lock.hcl +++ b/.terraform.lock.hcl @@ -1,8 +1,28 @@ # This file is maintained automatically by "terraform init". # Manual edits may be lost in future updates. +provider "registry.terraform.io/hashicorp/archive" { + version = "2.4.1" + hashes = [ + "h1:3mCpFxc6HwDIETCFHNENlxBUgKdsW2S1EmVHARn9Lgk=", + "zh:00240c042740d18d6ba545b211ff7ed5a9e8490d30be3f865e71dba90d7a34cf", + "zh:230c285beafaffd8d60da3446157b95f8fb43b359ba94b09214c1822bf310c3d", + "zh:726672a0e61a1d39695ce5e330aa3e6caa97f2a9438cf8125360e80f4cb52fa5", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7bc8f4a4fc7059ec01e767246df7937603dbc6ec49cb3eedffe6ecb68dbe9cb4", + "zh:800d898ce8ac96b244746c5a41f4107bd3c883fe6093d9a972a28b138ac02c4e", + "zh:9a8ea216af3840af48c08ef5ed998606c556b15be30d7b42c89a62df54285903", + "zh:b9905d0ac55b61ea78ecf0e6b07d54a9863a9f02e249d0d492e68cfcede0d89f", + "zh:c822495ba01ab7cee66c892f941097971c3be122a6200d556f462a751d446df8", + "zh:e05c31f2f4dca9eaada2726d16d2ffb03d6441b4eb55547b93d62d81383cd0ef", + "zh:ec14c68ca5d881bac73dbbd298f0ca84444001a81d473f51e36c4e29df040983", + "zh:ed32ebccb20b21c112f01d73d138ba5ada28cf8ede175441738a30711c79119a", + ] +} + provider "registry.terraform.io/hashicorp/aws" { - version = "5.30.0" + version = "5.30.0" + constraints = ">= 5.24.0" hashes = [ "h1:6SZLydYMDqhA4A+Fh0oZswJ+McOBf2q+XdSuMFbPzHI=", "h1:6ZRzAlt5BT1wD7NlRWdKJT5l4DXzMtpHcgEi/xskozM=", diff --git a/README.md b/README.md index 59a86ca..2408472 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,15 @@ This project creates and manages resources within an AWS account for infrastruct | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.6.3 | -| [aws](#requirement\_aws) | >= 5.24.0 | +| [terraform](#requirement\_terraform) | >= 1.6.5 | +| [archive](#requirement\_archive) | >= 2.4.1 | +| [aws](#requirement\_aws) | >= 5.30.0 | ## Providers | Name | Version | |------|---------| +| [archive](#provider\_archive) | 2.4.1 | | [aws](#provider\_aws) | 5.30.0 | | [aws.awsroute53root](#provider\_aws.awsroute53root) | 5.30.0 | @@ -25,18 +27,43 @@ This project creates and manages resources within an AWS account for infrastruct | Name | Type | |------|------| | [aws_athena_workgroup.infrastructure_vpc_flow_logs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/athena_workgroup) | resource | +| [aws_autoscaling_group.infrastructure_ecs_cluster](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/autoscaling_group) | resource | +| [aws_autoscaling_lifecycle_hook.infrastructure_ecs_cluster_termination](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/autoscaling_lifecycle_hook) | resource | +| [aws_cloudwatch_log_group.ecs_cluster_infrastructure_draining_lambda_log_group](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | | [aws_cloudwatch_log_group.infrastructure_vpc_flow_logs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | | [aws_default_network_acl.infrastructure](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/default_network_acl) | resource | +| [aws_ecs_cluster.infrastructure](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_cluster) | resource | | [aws_eip.infrastructure_nat](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eip) | resource | | [aws_flow_log.infrastructure_vpc_flow_logs_cloudwatch](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/flow_log) | resource | | [aws_flow_log.infrastructure_vpc_flow_logs_s3](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/flow_log) | resource | | [aws_glue_catalog_database.infrastructure_vpc_flow_logs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/glue_catalog_database) | resource | | [aws_glue_catalog_table.infrastructure_vpc_flow_logs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/glue_catalog_table) | resource | +| [aws_iam_instance_profile.infrastructure_ecs_cluster](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_instance_profile) | resource | +| [aws_iam_policy.ecs_cluster_infrastructure_draining_ecs_container_instance_state_update_lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.ecs_cluster_infrastructure_draining_kms_encrypt](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.ecs_cluster_infrastructure_draining_lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.ecs_cluster_infrastructure_draining_sns_publish_lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.infrastructure_ecs_cluster_autoscaling_lifecycle_termination_kms_encrypt](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.infrastructure_ecs_cluster_autoscaling_lifecycle_termination_sns_publish](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_policy.infrastructure_ecs_cluster_ec2_ecs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role.ecs_cluster_infrastructure_draining_lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role.infrastructure_ecs_cluster](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role.infrastructure_ecs_cluster_autoscaling_lifecycle_termination](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role.infrastructure_vpc_flow_logs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role_policy.infrastructure_vpc_flow_logs_allow_cloudwatch_rw](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy_attachment.ecs_cluster_infrastructure_draining_ecs_container_instance_state_update_lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.ecs_cluster_infrastructure_draining_kms_encrypt](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.ecs_cluster_infrastructure_draining_lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.ecs_cluster_infrastructure_draining_sns_publish_lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.infrastructure_ecs_cluster_autoscaling_lifecycle_termination_kms_encrypt](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.infrastructure_ecs_cluster_autoscaling_lifecycle_termination_sns_publish](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_role_policy_attachment.infrastructure_ecs_cluster_ec2_ecs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_internet_gateway.infrastructure_public](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/internet_gateway) | resource | | [aws_kms_alias.infrastructure](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_alias) | resource | | [aws_kms_key.infrastructure](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key) | resource | +| [aws_lambda_function.ecs_cluster_infrastructure_draining](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource | +| [aws_lambda_permission.ecs_cluster_infrastructure_draining_allow_sns_execution](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource | +| [aws_launch_template.infrastructure_ecs_cluster](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/launch_template) | resource | | [aws_nat_gateway.infrastructure](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/nat_gateway) | resource | | [aws_network_acl.infrastructure_private](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/network_acl) | resource | | [aws_network_acl.infrastructure_public](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/network_acl) | resource | @@ -50,6 +77,7 @@ This project creates and manages resources within an AWS account for infrastruct | [aws_network_acl_rule.ingress_allow_all_public](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/network_acl_rule) | resource | | [aws_network_acl_rule.ingress_private](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/network_acl_rule) | resource | | [aws_network_acl_rule.ingress_public](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/network_acl_rule) | resource | +| [aws_placement_group.infrastructure_ecs_cluster](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/placement_group) | resource | | [aws_route.infrustructure_public_internet_gateway](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route) | resource | | [aws_route.private_nat_gateway](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route) | resource | | [aws_route53_record.infrastructure_ns](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record) | resource | @@ -64,9 +92,20 @@ This project creates and manages resources within an AWS account for infrastruct | [aws_s3_bucket_public_access_block.infrastructure_logs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource | | [aws_s3_bucket_server_side_encryption_configuration.infrastructure_logs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_server_side_encryption_configuration) | resource | | [aws_s3_bucket_versioning.infrastructure_logs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_versioning) | resource | +| [aws_security_group.infrastructure_ecs_cluster_container_instances](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | +| [aws_security_group_rule.infrastructure_ecs_cluster_container_instances_egress_dns_tcp](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [aws_security_group_rule.infrastructure_ecs_cluster_container_instances_egress_dns_udp](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [aws_security_group_rule.infrastructure_ecs_cluster_container_instances_egress_https_tcp](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [aws_security_group_rule.infrastructure_ecs_cluster_container_instances_egress_https_udp](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [aws_security_group_rule.infrastructure_ecs_cluster_container_instances_ingress_tcp](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [aws_security_group_rule.infrastructure_ecs_cluster_container_instances_ingress_udp](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [aws_sns_topic.infrastructure_ecs_cluster_autoscaling_lifecycle_termination](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic) | resource | +| [aws_sns_topic_subscription.ecs_cluster_infrastructure_draining_autoscaling_lifecycle_termination](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic_subscription) | resource | | [aws_subnet.infrastructure_private](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/subnet) | resource | | [aws_subnet.infrastructure_public](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/subnet) | resource | | [aws_vpc.infrastructure](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc) | resource | +| [archive_file.ecs_cluster_infrastructure_draining_lambda](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | +| [aws_ami.ecs_cluster_ami](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ami) | data source | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | | [aws_route53_zone.root](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/route53_zone) | data source | @@ -76,8 +115,22 @@ This project creates and manages resources within an AWS account for infrastruct |------|-------------|------|---------|:--------:| | [aws\_profile\_name\_route53\_root](#input\_aws\_profile\_name\_route53\_root) | AWS Profile name which is configured for the account in which the root Route53 Hosted Zone exists. | `string` | n/a | yes | | [aws\_region](#input\_aws\_region) | AWS region in which to launch resources | `string` | n/a | yes | +| [enable\_infrastructure\_ecs\_cluster](#input\_enable\_infrastructure\_ecs\_cluster) | Enable creation of infrastructure ECS cluster, to place ECS services | `bool` | n/a | yes | | [enable\_infrastructure\_route53\_hosted\_zone](#input\_enable\_infrastructure\_route53\_hosted\_zone) | Creates a Route53 hosted zone, where DNS records will be created for resources launched within this module. | `bool` | n/a | yes | | [environment](#input\_environment) | The environment name to be used as part of the resource prefix | `string` | n/a | yes | +| [infrastructure\_dockerhub\_email](#input\_infrastructure\_dockerhub\_email) | Dockerhub email | `string` | n/a | yes | +| [infrastructure\_dockerhub\_token](#input\_infrastructure\_dockerhub\_token) | Dockerhub token which has permissions to pull images | `string` | n/a | yes | +| [infrastructure\_ecs\_cluster\_ami\_version](#input\_infrastructure\_ecs\_cluster\_ami\_version) | AMI version for ECS cluster instances (amzn2-ami-ecs-hvm-) | `string` | n/a | yes | +| [infrastructure\_ecs\_cluster\_draining\_lambda\_enabled](#input\_infrastructure\_ecs\_cluster\_draining\_lambda\_enabled) | Enable the Lambda which ensures all containers have drained before terminating ECS cluster instances | `bool` | n/a | yes | +| [infrastructure\_ecs\_cluster\_draining\_lambda\_log\_retention](#input\_infrastructure\_ecs\_cluster\_draining\_lambda\_log\_retention) | Log retention for the ECS cluster draining Lambda | `number` | n/a | yes | +| [infrastructure\_ecs\_cluster\_ebs\_docker\_storage\_volume\_size](#input\_infrastructure\_ecs\_cluster\_ebs\_docker\_storage\_volume\_size) | Size of EBS volume for Docker storage on the infrastructure ECS instances | `number` | n/a | yes | +| [infrastructure\_ecs\_cluster\_ebs\_docker\_storage\_volume\_type](#input\_infrastructure\_ecs\_cluster\_ebs\_docker\_storage\_volume\_type) | Type of EBS volume for Docker storage on the infrastructure ECS instances (eg. gp3) | `string` | n/a | yes | +| [infrastructure\_ecs\_cluster\_instance\_type](#input\_infrastructure\_ecs\_cluster\_instance\_type) | The instance type for EC2 instances launched in the ECS cluster | `string` | n/a | yes | +| [infrastructure\_ecs\_cluster\_max\_instance\_lifetime](#input\_infrastructure\_ecs\_cluster\_max\_instance\_lifetime) | Maximum lifetime in seconds of an instance within the ECS cluster | `number` | n/a | yes | +| [infrastructure\_ecs\_cluster\_max\_size](#input\_infrastructure\_ecs\_cluster\_max\_size) | Maximum number of instances for the ECS cluster | `number` | n/a | yes | +| [infrastructure\_ecs\_cluster\_min\_size](#input\_infrastructure\_ecs\_cluster\_min\_size) | Minimum number of instances for the ECS cluster | `number` | n/a | yes | +| [infrastructure\_ecs\_cluster\_publicly\_avaialble](#input\_infrastructure\_ecs\_cluster\_publicly\_avaialble) | Conditionally launch the ECS cluster EC2 instances into the Public subnet | `bool` | n/a | yes | +| [infrastructure\_ecs\_cluster\_termination\_timeout](#input\_infrastructure\_ecs\_cluster\_termination\_timeout) | The timeout for the terminiation lifecycle hook | `number` | n/a | yes | | [infrastructure\_kms\_encryption](#input\_infrastructure\_kms\_encryption) | Enable infrastructure KMS encryption. This will create a single KMS key to be used across all resources that support KMS encryption. | `bool` | n/a | yes | | [infrastructure\_logging\_bucket\_retention](#input\_infrastructure\_logging\_bucket\_retention) | Retention in days for the infrasrtucture S3 logs. This is for the default S3 logs bucket, where all AWS service logs will be delivered | `number` | n/a | yes | | [infrastructure\_name](#input\_infrastructure\_name) | The infrastructure name to be used as part of the resource prefix | `string` | n/a | yes | diff --git a/data.tf b/data.tf index 028fab1..921742f 100644 --- a/data.tf +++ b/data.tf @@ -5,3 +5,24 @@ data "aws_route53_zone" "root" { name = local.route53_root_hosted_zone_domain_name } + +data "aws_ami" "ecs_cluster_ami" { + count = local.enable_infrastructure_ecs_cluster ? 1 : 0 + + most_recent = true + owners = ["amazon"] + + filter { + name = "name" + values = [ + "amzn2-ami-ecs-hvm-${local.infrastructure_ecs_cluster_ami_version}" + ] + } + + filter { + name = "architecture" + values = [ + "x86_64" + ] + } +} diff --git a/ec2-userdata/ecs-instance.tpl b/ec2-userdata/ecs-instance.tpl new file mode 100644 index 0000000..d87d551 --- /dev/null +++ b/ec2-userdata/ecs-instance.tpl @@ -0,0 +1,30 @@ +#!/bin/bash + +# Mount docker storage volume +sudo mkfs -t xfs ${docker_storage_volume_device_name} +sudo mkdir -p /var/lib/docker +sudo mount -o prjquota ${docker_storage_volume_device_name} /var/lib/docker + +# Configure ECS with Docker +echo ECS_CLUSTER="${ecs_cluster_name}" >> /etc/ecs/ecs.config +echo ECS_ENGINE_AUTH_TYPE=dockercfg >> /etc/ecs/ecs.config +echo 'ECS_ENGINE_AUTH_DATA={"https://index.docker.io/v1/": { "auth": "${dockerhub_token}", "email": "${dockerhub_email}"}}' >> /etc/ecs/ecs.config +# Set low task cleanup - reduces chance of docker thin pool running out of free space +echo "ECS_ENGINE_TASK_CLEANUP_WAIT_DURATION=15m" >> /etc/ecs/ecs.config + +# Configure Docker options +sed -i s/OPTIONS/#OPTIONS/ /etc/sysconfig/docker +echo 'OPTIONS="--default-ulimit nofile=1024:4096 --storage-opt overlay2.size=${docker_storage_size}G"' >> /etc/sysconfig/docker +sudo service docker restart + +# Install useful packages +sudo yum update -y + +if ! command -v aws &> /dev/null +then + sudo yum install -y aws-cli +fi + +sudo yum install -y \ + jq \ + rsync diff --git a/ecs-cluster-infrastructure-draining-lambda.tf b/ecs-cluster-infrastructure-draining-lambda.tf new file mode 100644 index 0000000..6d22d21 --- /dev/null +++ b/ecs-cluster-infrastructure-draining-lambda.tf @@ -0,0 +1,140 @@ +resource "aws_cloudwatch_log_group" "ecs_cluster_infrastructure_draining_lambda_log_group" { + count = local.infrastructure_ecs_cluster_draining_lambda_enabled ? 1 : 0 + + name = "/aws/lambda/${local.project_name}-ecs-cluster-infrastructure-draining" + kms_key_id = local.infrastructure_kms_encryption ? aws_kms_key.infrastructure[0].arn : null + retention_in_days = local.infrastructure_ecs_cluster_draining_lambda_log_retention +} + +resource "aws_iam_role" "ecs_cluster_infrastructure_draining_lambda" { + count = local.infrastructure_ecs_cluster_draining_lambda_enabled ? 1 : 0 + + name = "${local.project_name}-ecs-cluster-infrastructure-draining-lambda" + assume_role_policy = templatefile( + "${path.root}/policies/assume-roles/service-principle-standard.json.tpl", + { services = jsonencode(["lambda.amazonaws.com"]) } + ) +} + +resource "aws_iam_policy" "ecs_cluster_infrastructure_draining_lambda" { + count = local.infrastructure_ecs_cluster_draining_lambda_enabled ? 1 : 0 + + name = "${local.project_name}-ecs-cluster-infrastructure-draining-lambda" + policy = templatefile( + "${path.root}/policies/lambda-default.json.tpl", + { + region = local.aws_region + account_id = local.aws_account_id + function_name = "${local.project_name}-ecs-cluster-infrastructure-draining" + } + ) +} + +resource "aws_iam_role_policy_attachment" "ecs_cluster_infrastructure_draining_lambda" { + count = local.infrastructure_ecs_cluster_draining_lambda_enabled ? 1 : 0 + + role = aws_iam_role.ecs_cluster_infrastructure_draining_lambda[0].name + policy_arn = aws_iam_policy.ecs_cluster_infrastructure_draining_lambda[0].arn +} + +resource "aws_iam_policy" "ecs_cluster_infrastructure_draining_ecs_container_instance_state_update_lambda" { + count = local.infrastructure_ecs_cluster_draining_lambda_enabled ? 1 : 0 + + name = "${local.project_name}-ecs-cluster-infrastructure-ecs-container-instance-state-update" + policy = templatefile( + "${path.root}/policies/ecs-container-instance-state-update.json.tpl", {} + ) +} + +resource "aws_iam_role_policy_attachment" "ecs_cluster_infrastructure_draining_ecs_container_instance_state_update_lambda" { + count = local.infrastructure_ecs_cluster_draining_lambda_enabled ? 1 : 0 + + role = aws_iam_role.ecs_cluster_infrastructure_draining_lambda[0].name + policy_arn = aws_iam_policy.ecs_cluster_infrastructure_draining_ecs_container_instance_state_update_lambda[0].arn +} + +resource "aws_iam_policy" "ecs_cluster_infrastructure_draining_sns_publish_lambda" { + count = local.infrastructure_ecs_cluster_draining_lambda_enabled ? 1 : 0 + + name = "${local.project_name}-ecs-cluster-infrastructure-sns-publish" + policy = templatefile( + "${path.root}/policies/sns-publish.json.tpl", + { sns_topic_arn = aws_sns_topic.infrastructure_ecs_cluster_autoscaling_lifecycle_termination[0].arn } + ) +} + +resource "aws_iam_role_policy_attachment" "ecs_cluster_infrastructure_draining_sns_publish_lambda" { + count = local.infrastructure_ecs_cluster_draining_lambda_enabled ? 1 : 0 + + role = aws_iam_role.ecs_cluster_infrastructure_draining_lambda[0].name + policy_arn = aws_iam_policy.ecs_cluster_infrastructure_draining_sns_publish_lambda[0].arn +} + +resource "aws_iam_policy" "ecs_cluster_infrastructure_draining_kms_encrypt" { + count = local.infrastructure_ecs_cluster_draining_lambda_enabled && local.infrastructure_kms_encryption ? 1 : 0 + + name = "${local.project_name}-ecs-cluster-infrastructure-kms-encrypt" + policy = templatefile( + "${path.root}/policies/kms-encrypt.json.tpl", + { kms_key_arn = aws_kms_key.infrastructure[0].arn } + ) +} + +resource "aws_iam_role_policy_attachment" "ecs_cluster_infrastructure_draining_kms_encrypt" { + count = local.infrastructure_ecs_cluster_draining_lambda_enabled && local.infrastructure_kms_encryption ? 1 : 0 + + role = aws_iam_role.ecs_cluster_infrastructure_draining_lambda[0].name + policy_arn = aws_iam_policy.ecs_cluster_infrastructure_draining_kms_encrypt[0].arn +} + +data "archive_file" "ecs_cluster_infrastructure_draining_lambda" { + count = local.infrastructure_ecs_cluster_draining_lambda_enabled ? 1 : 0 + + type = "zip" + source_dir = "lambdas/ecs-ec2-draining" + output_path = "lambdas/.zip-cache/ecs-ec2-draining.zip" +} + +resource "aws_lambda_function" "ecs_cluster_infrastructure_draining" { + count = local.infrastructure_ecs_cluster_draining_lambda_enabled ? 1 : 0 + + filename = data.archive_file.ecs_cluster_infrastructure_draining_lambda[0].output_path + function_name = "${local.project_name}-ecs-cluster-infrastructure-draining" + description = "${local.project_name} ECS Cluster Infrastructure Draining" + handler = "function.lambda_handler" + runtime = "python3.11" + role = aws_iam_role.ecs_cluster_infrastructure_draining_lambda[0].arn + source_code_hash = data.archive_file.ecs_cluster_infrastructure_draining_lambda[0].output_base64sha256 + memory_size = 128 + package_type = "Zip" + timeout = 900 + + environment { + variables = { + ecsClusterName = local.infrastructure_ecs_cluster_name + awsRegion = local.aws_region + } + } + + tracing_config { + mode = "Active" + } +} + +resource "aws_lambda_permission" "ecs_cluster_infrastructure_draining_allow_sns_execution" { + count = local.infrastructure_ecs_cluster_draining_lambda_enabled ? 1 : 0 + + statement_id = "AllowExecutionFromSNS" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.ecs_cluster_infrastructure_draining[0].function_name + principal = "sns.amazonaws.com" + source_arn = aws_sns_topic.infrastructure_ecs_cluster_autoscaling_lifecycle_termination[0].arn +} + +resource "aws_sns_topic_subscription" "ecs_cluster_infrastructure_draining_autoscaling_lifecycle_termination" { + count = local.infrastructure_ecs_cluster_draining_lambda_enabled ? 1 : 0 + + topic_arn = aws_sns_topic.infrastructure_ecs_cluster_autoscaling_lifecycle_termination[0].arn + protocol = "lambda" + endpoint = aws_lambda_function.ecs_cluster_infrastructure_draining[0].arn +} diff --git a/ecs-cluster-infrastructure.tf b/ecs-cluster-infrastructure.tf new file mode 100644 index 0000000..587bade --- /dev/null +++ b/ecs-cluster-infrastructure.tf @@ -0,0 +1,319 @@ +resource "aws_ecs_cluster" "infrastructure" { + count = local.enable_infrastructure_ecs_cluster ? 1 : 0 + + name = local.infrastructure_ecs_cluster_name + + setting { + name = "containerInsights" + value = "enabled" + } +} + +resource "aws_security_group" "infrastructure_ecs_cluster_container_instances" { + count = local.enable_infrastructure_ecs_cluster ? 1 : 0 + + name = "${local.resource_prefix}-infrastructure-ecs-cluster-container-instances" + description = "Infrastructure ECS cluster container instances" + vpc_id = aws_vpc.infrastructure[0].id +} + +resource "aws_security_group_rule" "infrastructure_ecs_cluster_container_instances_ingress_tcp" { + count = local.enable_infrastructure_ecs_cluster && local.infrastructure_vpc_network_enable_public ? 1 : 0 + + description = "Allow container port tcp ingress from public subnet (TO BE CHANGED TO ONLY ALLOW ALB)" + type = "ingress" + from_port = 32768 + to_port = 65535 + protocol = "tcp" + # TODO: Update to `source_security_group_id`, using the ECS service ALB's security group id + cidr_blocks = [for subnet in aws_subnet.infrastructure_public : subnet.cidr_block] + security_group_id = aws_security_group.infrastructure_ecs_cluster_container_instances[0].id +} + +resource "aws_security_group_rule" "infrastructure_ecs_cluster_container_instances_ingress_udp" { + count = local.enable_infrastructure_ecs_cluster && local.infrastructure_vpc_network_enable_public ? 1 : 0 + + description = "Allow container port udp ingress from public subnet (TO BE CHANGED TO ONLY ALLOW ALB)" + type = "ingress" + from_port = 32768 + to_port = 65535 + protocol = "udp" + # TODO: Update to `source_security_group_id`, using the ECS service ALB's security group id + cidr_blocks = [for subnet in aws_subnet.infrastructure_public : subnet.cidr_block] + security_group_id = aws_security_group.infrastructure_ecs_cluster_container_instances[0].id +} + +resource "aws_security_group_rule" "infrastructure_ecs_cluster_container_instances_egress_https_tcp" { + count = local.enable_infrastructure_ecs_cluster ? 1 : 0 + + description = "Allow HTTPS tcp outbound" + type = "egress" + from_port = 443 + to_port = 443 + protocol = "tcp" + # tfsec:ignore:aws-ec2-no-public-egress-sgr + cidr_blocks = ["0.0.0.0/0"] + security_group_id = aws_security_group.infrastructure_ecs_cluster_container_instances[0].id +} + +resource "aws_security_group_rule" "infrastructure_ecs_cluster_container_instances_egress_https_udp" { + count = local.enable_infrastructure_ecs_cluster ? 1 : 0 + + description = "Allow HTTPS udp outbound" + type = "egress" + from_port = 443 + to_port = 443 + protocol = "udp" + # tfsec:ignore:aws-ec2-no-public-egress-sgr + cidr_blocks = ["0.0.0.0/0"] + security_group_id = aws_security_group.infrastructure_ecs_cluster_container_instances[0].id +} + +resource "aws_security_group_rule" "infrastructure_ecs_cluster_container_instances_egress_dns_tcp" { + count = local.enable_infrastructure_ecs_cluster ? 1 : 0 + + description = "Allow DNS tcp outbound to AWS" + type = "egress" + from_port = 53 + to_port = 53 + protocol = "tcp" + cidr_blocks = local.infrastructure_ecs_cluster_publicly_avaialble ? [ + for subnet in aws_subnet.infrastructure_public : subnet.cidr_block + ] : [ + for subnet in aws_subnet.infrastructure_private : subnet.cidr_block + ] + security_group_id = aws_security_group.infrastructure_ecs_cluster_container_instances[0].id +} + +resource "aws_security_group_rule" "infrastructure_ecs_cluster_container_instances_egress_dns_udp" { + count = local.enable_infrastructure_ecs_cluster ? 1 : 0 + + description = "Allow DNS udp outbound to AWS" + type = "egress" + from_port = 53 + to_port = 53 + protocol = "udp" + cidr_blocks = local.infrastructure_ecs_cluster_publicly_avaialble ? [ + for subnet in aws_subnet.infrastructure_public : subnet.cidr_block + ] : [ + for subnet in aws_subnet.infrastructure_private : subnet.cidr_block + ] + security_group_id = aws_security_group.infrastructure_ecs_cluster_container_instances[0].id +} + +resource "aws_iam_role" "infrastructure_ecs_cluster" { + count = local.enable_infrastructure_ecs_cluster ? 1 : 0 + + name = "${local.resource_prefix}-infrastructure-ecs-cluster" + assume_role_policy = templatefile( + "${path.root}/policies/assume-roles/service-principle-standard.json.tpl", + { services = jsonencode(["ecs.amazonaws.com", "ec2.amazonaws.com"]) } + ) +} + +resource "aws_iam_policy" "infrastructure_ecs_cluster_ec2_ecs" { + count = local.enable_infrastructure_ecs_cluster ? 1 : 0 + + name = "${local.project_name}-ec2-ecs" + policy = templatefile("${path.root}/policies/ec2-ecs.json.tpl", {}) +} + +resource "aws_iam_role_policy_attachment" "infrastructure_ecs_cluster_ec2_ecs" { + count = local.enable_infrastructure_ecs_cluster ? 1 : 0 + + role = aws_iam_role.infrastructure_ecs_cluster[0].name + policy_arn = aws_iam_policy.infrastructure_ecs_cluster_ec2_ecs[0].arn +} + +resource "aws_iam_instance_profile" "infrastructure_ecs_cluster" { + count = local.enable_infrastructure_ecs_cluster ? 1 : 0 + + name = "${local.resource_prefix}-infrastructure-ecs-cluster" + role = aws_iam_role.infrastructure_ecs_cluster[0].name +} + +resource "aws_launch_template" "infrastructure_ecs_cluster" { + count = local.enable_infrastructure_ecs_cluster ? 1 : 0 + + name = "${local.resource_prefix}-infrastructure-ecs-cluster" + description = "Infrastructure ECS Cluster (${local.resource_prefix})" + + block_device_mappings { + # Root EBS volume + device_name = "/dev/xvda" + + ebs { + volume_size = 40 + encrypted = true + delete_on_termination = true + } + } + + block_device_mappings { + # Docker Storage EBS volume + device_name = local.infrastructure_ecs_cluster_ebs_docker_storage_volume_device_name + + ebs { + volume_size = local.infrastructure_ecs_cluster_ebs_docker_storage_volume_size + volume_type = local.infrastructure_ecs_cluster_ebs_docker_storage_volume_type + encrypted = true + delete_on_termination = true + } + } + + capacity_reservation_specification { + capacity_reservation_preference = "open" + } + + network_interfaces { + associate_public_ip_address = local.infrastructure_ecs_cluster_publicly_avaialble + security_groups = [aws_security_group.infrastructure_ecs_cluster_container_instances[0].id] + } + + iam_instance_profile { + name = aws_iam_instance_profile.infrastructure_ecs_cluster[0].name + } + + metadata_options { + http_endpoint = "enabled" + http_tokens = "required" + } + + monitoring { + enabled = true + } + + disable_api_termination = false + disable_api_stop = false + ebs_optimized = true + image_id = data.aws_ami.ecs_cluster_ami[0].id + instance_initiated_shutdown_behavior = "stop" + instance_type = local.infrastructure_ecs_cluster_instance_type + + user_data = local.infrastructure_ecs_cluster_user_data +} + +resource "aws_placement_group" "infrastructure_ecs_cluster" { + count = local.enable_infrastructure_ecs_cluster ? 1 : 0 + + name = "${local.resource_prefix}-infrastructure-ecs-cluster" + + strategy = "spread" + spread_level = "rack" +} + +resource "aws_autoscaling_group" "infrastructure_ecs_cluster" { + count = local.enable_infrastructure_ecs_cluster ? 1 : 0 + + name = "${local.resource_prefix}-infrastructure-ecs-cluster" + + launch_template { + id = aws_launch_template.infrastructure_ecs_cluster[0].id + version = aws_launch_template.infrastructure_ecs_cluster[0].latest_version + } + + vpc_zone_identifier = local.infrastructure_ecs_cluster_publicly_avaialble ? [ + for subnet in aws_subnet.infrastructure_public : subnet.id + ] : [ + for subnet in aws_subnet.infrastructure_private : subnet.id + ] + placement_group = aws_placement_group.infrastructure_ecs_cluster[0].id + + min_size = local.infrastructure_ecs_cluster_min_size + max_size = local.infrastructure_ecs_cluster_max_size + desired_capacity = local.infrastructure_ecs_cluster_min_size + max_instance_lifetime = local.infrastructure_ecs_cluster_max_instance_lifetime + + termination_policies = ["OldestLaunchConfiguration", "ClosestToNextInstanceHour", "Default"] + + tag { + key = "Name" + value = "${local.resource_prefix}-infrastructure-ecs-cluster" + propagate_at_launch = true + } + + dynamic "tag" { + for_each = local.default_tags + + content { + key = tag.key + value = tag.value + propagate_at_launch = true + } + } + + instance_refresh { + strategy = "Rolling" + preferences { + min_healthy_percentage = 100 + } + triggers = ["tag"] + } + + timeouts { + delete = "15m" + } +} + +resource "aws_sns_topic" "infrastructure_ecs_cluster_autoscaling_lifecycle_termination" { + count = local.enable_infrastructure_ecs_cluster ? 1 : 0 + + name = "${local.resource_prefix}-ecs-termination-hook" + kms_master_key_id = local.infrastructure_kms_encryption ? aws_kms_alias.infrastructure[0].name : null +} + +resource "aws_iam_role" "infrastructure_ecs_cluster_autoscaling_lifecycle_termination" { + count = local.enable_infrastructure_ecs_cluster ? 1 : 0 + + name = "${local.resource_prefix}-ecs-termination-hook" + assume_role_policy = templatefile( + "${path.root}/policies/assume-roles/service-principle-standard.json.tpl", + { services = jsonencode(["autoscaling.amazonaws.com"]) } + ) +} + +resource "aws_iam_policy" "infrastructure_ecs_cluster_autoscaling_lifecycle_termination_sns_publish" { + count = local.enable_infrastructure_ecs_cluster ? 1 : 0 + + name = "${local.resource_prefix}-ecs-termination-hook-sns-publish" + policy = templatefile( + "${path.root}/policies/sns-publish.json.tpl", + { sns_topic_arn = aws_sns_topic.infrastructure_ecs_cluster_autoscaling_lifecycle_termination[0].arn } + ) +} + +resource "aws_iam_role_policy_attachment" "infrastructure_ecs_cluster_autoscaling_lifecycle_termination_sns_publish" { + count = local.enable_infrastructure_ecs_cluster ? 1 : 0 + + role = aws_iam_role.infrastructure_ecs_cluster_autoscaling_lifecycle_termination[0].id + policy_arn = aws_iam_policy.infrastructure_ecs_cluster_autoscaling_lifecycle_termination_sns_publish[0].arn +} + +resource "aws_iam_policy" "infrastructure_ecs_cluster_autoscaling_lifecycle_termination_kms_encrypt" { + count = local.enable_infrastructure_ecs_cluster && local.infrastructure_kms_encryption ? 1 : 0 + + name = "${local.resource_prefix}-ecs-termination-hook-kms-encrypt" + policy = templatefile( + "${path.root}/policies/kms-encrypt.json.tpl", + { kms_key_arn = aws_kms_key.infrastructure[0].arn } + ) +} + +resource "aws_iam_role_policy_attachment" "infrastructure_ecs_cluster_autoscaling_lifecycle_termination_kms_encrypt" { + count = local.enable_infrastructure_ecs_cluster && local.infrastructure_kms_encryption ? 1 : 0 + + role = aws_iam_role.infrastructure_ecs_cluster_autoscaling_lifecycle_termination[0].id + policy_arn = aws_iam_policy.infrastructure_ecs_cluster_autoscaling_lifecycle_termination_kms_encrypt[0].arn +} + +resource "aws_autoscaling_lifecycle_hook" "infrastructure_ecs_cluster_termination" { + count = local.enable_infrastructure_ecs_cluster ? 1 : 0 + + name = local.infrastructure_ecs_cluster_termination_sns_topic_name + autoscaling_group_name = aws_autoscaling_group.infrastructure_ecs_cluster[0].name + default_result = local.infrastructure_ecs_cluster_draining_lambda_enabled ? "ABANDON" : "CONTINUE" + heartbeat_timeout = local.infrastructure_ecs_cluster_termination_timeout + lifecycle_transition = "autoscaling:EC2_INSTANCE_TERMINATING" + role_arn = aws_iam_role.infrastructure_ecs_cluster_autoscaling_lifecycle_termination[0].arn + notification_target_arn = aws_sns_topic.infrastructure_ecs_cluster_autoscaling_lifecycle_termination[0].arn +} diff --git a/lambdas/ecs-ec2-draining/function.py b/lambdas/ecs-ec2-draining/function.py new file mode 100644 index 0000000..37e24cd --- /dev/null +++ b/lambdas/ecs-ec2-draining/function.py @@ -0,0 +1,71 @@ +import json +import time +import boto3 +import os + +CLUSTER = os.environ['ecsClusterName'] +REGION = os.environ['awsRegion'] + +ECS = boto3.client('ecs', region_name=REGION) +ASG = boto3.client('autoscaling', region_name=REGION) +SNS = boto3.client('sns', region_name=REGION) + +def find_ecs_instance_info(instance_id): + paginator = ECS.get_paginator('list_container_instances') + for list_resp in paginator.paginate(cluster=CLUSTER): + arns = list_resp['containerInstanceArns'] + desc_resp = ECS.describe_container_instances(cluster=CLUSTER, + containerInstances=arns) + for container_instance in desc_resp['containerInstances']: + if container_instance['ec2InstanceId'] != instance_id: + continue + + print('Found instance: id=%s, arn=%s, status=%s, runningTasksCount=%s' % + (instance_id, container_instance['containerInstanceArn'], + container_instance['status'], container_instance['runningTasksCount'])) + + return (container_instance['containerInstanceArn'], + container_instance['status'], container_instance['runningTasksCount']) + + return None, None, 0 + +def instance_has_running_tasks(instance_id): + (instance_arn, container_status, running_tasks) = find_ecs_instance_info(instance_id) + if instance_arn is None: + print('Could not find instance ID %s. Letting autoscaling kill the instance.' % + (instance_id)) + return False + + if container_status != 'DRAINING': + print('Setting container instance %s (%s) to DRAINING' % + (instance_id, instance_arn)) + ECS.update_container_instances_state(cluster=CLUSTER, + containerInstances=[instance_arn], + status='DRAINING') + + return running_tasks > 0 + +def lambda_handler(event, context): + msg = json.loads(event['Records'][0]['Sns']['Message']) + + if 'LifecycleTransition' not in msg.keys() or \ + msg['LifecycleTransition'].find('autoscaling:EC2_INSTANCE_TERMINATING') == -1: + print('Exiting since the lifecycle transition is not EC2_INSTANCE_TERMINATING.') + return + + if instance_has_running_tasks(msg['EC2InstanceId']): + print('Tasks are still running on instance %s; posting msg to SNS topic %s' % + (msg['EC2InstanceId'], event['Records'][0]['Sns']['TopicArn'])) + time.sleep(5) + sns_resp = SNS.publish(TopicArn=event['Records'][0]['Sns']['TopicArn'], + Message=json.dumps(msg), + Subject='Publishing SNS msg to invoke Lambda again.') + print('Posted msg %s to SNS topic.' % (sns_resp['MessageId'])) + else: + print('No tasks are running on instance %s; setting lifecycle to complete' % + (msg['EC2InstanceId'])) + + ASG.complete_lifecycle_action(LifecycleHookName=msg['LifecycleHookName'], + AutoScalingGroupName=msg['AutoScalingGroupName'], + LifecycleActionResult='CONTINUE', + InstanceId=msg['EC2InstanceId']) diff --git a/locals.tf b/locals.tf index d8add67..38f9f1d 100644 --- a/locals.tf +++ b/locals.tf @@ -89,6 +89,34 @@ locals { hour = "string" } + infrastructure_dockerhub_email = var.infrastructure_dockerhub_email + infrastructure_dockerhub_token = var.infrastructure_dockerhub_token + + enable_infrastructure_ecs_cluster = var.enable_infrastructure_ecs_cluster && local.infrastructure_vpc + infrastructure_ecs_cluster_name = "${local.resource_prefix}-infrastructure" + infrastructure_ecs_cluster_ami_version = var.infrastructure_ecs_cluster_ami_version + infrastructure_ecs_cluster_ebs_docker_storage_volume_device_name = "/dev/xvdcz" + infrastructure_ecs_cluster_ebs_docker_storage_volume_size = var.infrastructure_ecs_cluster_ebs_docker_storage_volume_size + infrastructure_ecs_cluster_ebs_docker_storage_volume_type = var.infrastructure_ecs_cluster_ebs_docker_storage_volume_type + infrastructure_ecs_cluster_publicly_avaialble = var.infrastructure_ecs_cluster_publicly_avaialble && local.infrastructure_vpc_network_enable_public + infrastructure_ecs_cluster_instance_type = var.infrastructure_ecs_cluster_instance_type + infrastructure_ecs_cluster_termination_timeout = var.infrastructure_ecs_cluster_termination_timeout + infrastructure_ecs_cluster_draining_lambda_enabled = var.infrastructure_ecs_cluster_draining_lambda_enabled && local.enable_infrastructure_ecs_cluster + infrastructure_ecs_cluster_draining_lambda_log_retention = var.infrastructure_ecs_cluster_draining_lambda_log_retention + infrastructure_ecs_cluster_termination_sns_topic_name = "${local.resource_prefix}-infrastructure-ecs-cluster-termination" + infrastructure_ecs_cluster_min_size = var.infrastructure_ecs_cluster_min_size + infrastructure_ecs_cluster_max_size = var.infrastructure_ecs_cluster_max_size + infrastructure_ecs_cluster_max_instance_lifetime = var.infrastructure_ecs_cluster_max_instance_lifetime + infrastructure_ecs_cluster_user_data = base64encode( + templatefile("ec2-userdata/ecs-instance.tpl", { + docker_storage_volume_device_name = local.infrastructure_ecs_cluster_ebs_docker_storage_volume_device_name, + ecs_cluster_name = local.infrastructure_ecs_cluster_name, + dockerhub_token = local.infrastructure_dockerhub_token, + dockerhub_email = local.infrastructure_dockerhub_email, + docker_storage_size = local.infrastructure_ecs_cluster_ebs_docker_storage_volume_size + }) + ) + default_tags = { Project = local.project_name, Infrastructure = local.infrastructure_name, diff --git a/policies/ec2-ecs.json.tpl b/policies/ec2-ecs.json.tpl new file mode 100644 index 0000000..caa3be4 --- /dev/null +++ b/policies/ec2-ecs.json.tpl @@ -0,0 +1,39 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "ec2:DescribeTags", + "ecs:CreateCluster", + "ecs:DeregisterContainerInstance", + "ecs:DiscoverPollEndpoint", + "ecs:Poll", + "ecs:RegisterContainerInstance", + "ecs:StartTelemetrySession", + "ecs:UpdateContainerInstancesState", + "ecs:Submit*", + "ecr:GetAuthorizationToken", + "ecr:BatchCheckLayerAvailability", + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": "ecs:TagResource", + "Resource": "*", + "Condition": { + "StringEquals": { + "ecs:CreateAction": [ + "CreateCluster", + "RegisterContainerInstance" + ] + } + } + } + ] +} diff --git a/policies/ecs-container-instance-state-update.json.tpl b/policies/ecs-container-instance-state-update.json.tpl new file mode 100644 index 0000000..d82ab75 --- /dev/null +++ b/policies/ecs-container-instance-state-update.json.tpl @@ -0,0 +1,17 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "autoscaling:CompleteLifecycleAction", + "ecs:ListContainerInstances", + "ecs:DescribeContainerInstances", + "ecs:UpdateContainerInstancesState" + ], + "Resource": [ + "*" + ] + } + ] +} diff --git a/policies/kms-encrypt.json.tpl b/policies/kms-encrypt.json.tpl new file mode 100644 index 0000000..a72039b --- /dev/null +++ b/policies/kms-encrypt.json.tpl @@ -0,0 +1,13 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "kms:GenerateDataKey", + "kms:Decrypt" + ], + "Resource": "${kms_key_arn}" + } + ] +} diff --git a/policies/kms-key-policy-statements/service-allow-encrypt.json.tpl b/policies/kms-key-policy-statements/service-allow-encrypt.json.tpl new file mode 100644 index 0000000..db8a873 --- /dev/null +++ b/policies/kms-key-policy-statements/service-allow-encrypt.json.tpl @@ -0,0 +1,11 @@ +%{if services != "[]"}{ + "Effect": "Allow", + "Principal": { + "Service": ${services} + }, + "Action": [ + "kms:GenerateDataKey*", + "kms:Decrypt" + ], + "Resource": "*" +}%{endif} diff --git a/policies/kms-key-policy-statements/service-sns-allow-encrypt.json.tpl b/policies/kms-key-policy-statements/service-sns-allow-encrypt.json.tpl new file mode 100644 index 0000000..c89d2e1 --- /dev/null +++ b/policies/kms-key-policy-statements/service-sns-allow-encrypt.json.tpl @@ -0,0 +1,16 @@ +%{if sns_topic_arn != ""}{ + "Effect": "Allow", + "Principal": { + "Service": ${services} + }, + "Action": [ + "kms:GenerateDataKey*", + "kms:Decrypt" + ], + "Resource": "*", + "Condition": { + "StringEquals": { + "kms:EncryptionContext:aws:sns:topicArn": "${sns_topic_arn}" + } + } +}%{endif} diff --git a/policies/lambda-default.json.tpl b/policies/lambda-default.json.tpl new file mode 100644 index 0000000..f3b72d5 --- /dev/null +++ b/policies/lambda-default.json.tpl @@ -0,0 +1,22 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogGroup" + ], + "Resource": "arn:aws:logs:${region}:${account_id}:*" + }, + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Resource": [ + "arn:aws:logs:${region}:${account_id}:log-group:/aws/lambda/${function_name}:*" + ] + } + ] +} diff --git a/policies/sns-publish.json.tpl b/policies/sns-publish.json.tpl new file mode 100644 index 0000000..1da083d --- /dev/null +++ b/policies/sns-publish.json.tpl @@ -0,0 +1,12 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "sns:Publish" + ], + "Resource": "${sns_topic_arn}" + } + ] +} diff --git a/variables.tf b/variables.tf index 3923192..cbdf658 100644 --- a/variables.tf +++ b/variables.tf @@ -18,6 +18,17 @@ variable "aws_region" { type = string } +variable "infrastructure_dockerhub_email" { + description = "Dockerhub email" + type = string +} + +variable "infrastructure_dockerhub_token" { + description = "Dockerhub token which has permissions to pull images" + type = string + sensitive = true +} + variable "infrastructure_kms_encryption" { description = "Enable infrastructure KMS encryption. This will create a single KMS key to be used across all resources that support KMS encryption." type = bool @@ -193,3 +204,63 @@ variable "enable_infrastructure_route53_hosted_zone" { description = "Creates a Route53 hosted zone, where DNS records will be created for resources launched within this module." type = bool } + +variable "enable_infrastructure_ecs_cluster" { + description = "Enable creation of infrastructure ECS cluster, to place ECS services" + type = bool +} + +variable "infrastructure_ecs_cluster_ami_version" { + description = "AMI version for ECS cluster instances (amzn2-ami-ecs-hvm-)" + type = string +} + +variable "infrastructure_ecs_cluster_ebs_docker_storage_volume_size" { + description = "Size of EBS volume for Docker storage on the infrastructure ECS instances" + type = number +} + +variable "infrastructure_ecs_cluster_ebs_docker_storage_volume_type" { + description = "Type of EBS volume for Docker storage on the infrastructure ECS instances (eg. gp3)" + type = string +} + +variable "infrastructure_ecs_cluster_publicly_avaialble" { + description = "Conditionally launch the ECS cluster EC2 instances into the Public subnet" + type = bool +} + +variable "infrastructure_ecs_cluster_instance_type" { + description = "The instance type for EC2 instances launched in the ECS cluster" + type = string +} + +variable "infrastructure_ecs_cluster_termination_timeout" { + description = "The timeout for the terminiation lifecycle hook" + type = number +} + +variable "infrastructure_ecs_cluster_draining_lambda_enabled" { + description = "Enable the Lambda which ensures all containers have drained before terminating ECS cluster instances" + type = bool +} + +variable "infrastructure_ecs_cluster_draining_lambda_log_retention" { + description = "Log retention for the ECS cluster draining Lambda" + type = number +} + +variable "infrastructure_ecs_cluster_min_size" { + description = "Minimum number of instances for the ECS cluster" + type = number +} + +variable "infrastructure_ecs_cluster_max_size" { + description = "Maximum number of instances for the ECS cluster" + type = number +} + +variable "infrastructure_ecs_cluster_max_instance_lifetime" { + description = "Maximum lifetime in seconds of an instance within the ECS cluster" + type = number +} diff --git a/versions.tf b/versions.tf index 4589b65..de5eeef 100644 --- a/versions.tf +++ b/versions.tf @@ -1,9 +1,13 @@ terraform { - required_version = ">= 1.6.3" + required_version = ">= 1.6.5" required_providers { aws = { source = "hashicorp/aws" - version = ">= 5.24.0" + version = ">= 5.30.0" + } + archive = { + source = "hashicorp/archive" + version = ">= 2.4.1" } } }