diff --git a/README.md b/README.md index f9ae24f..21110f7 100644 --- a/README.md +++ b/README.md @@ -106,13 +106,13 @@ module "app_ecs_service" { | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 0.13 | -| [aws](#requirement\_aws) | >= 3.0 | +| [aws](#requirement\_aws) | >= 3.34 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | >= 3.0 | +| [aws](#provider\_aws) | >= 3.34 | ## Modules @@ -127,10 +127,12 @@ No modules. | [aws_cloudwatch_metric_alarm.alarm_mem](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_metric_alarm) | resource | | [aws_ecs_service.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service) | resource | | [aws_ecs_task_definition.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_task_definition) | resource | +| [aws_iam_policy.task_role_ecs_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [aws_iam_role.task_execution_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role.task_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role_policy.instance_role_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | | [aws_iam_role_policy.task_execution_role_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource | +| [aws_iam_role_policy_attachment.task_role_ecs_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_security_group.ecs_sg](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | | [aws_security_group_rule.app_ecs_allow_health_check_from_alb](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | | [aws_security_group_rule.app_ecs_allow_health_check_from_nlb](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | @@ -141,6 +143,7 @@ No modules. | [aws_iam_policy_document.ecs_assume_role_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.instance_role_policy_doc](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.task_execution_role_policy_doc](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.task_role_ecs_exec](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | ## Inputs @@ -163,6 +166,7 @@ No modules. | [ec2\_create\_task\_execution\_role](#input\_ec2\_create\_task\_execution\_role) | Set to true to create ecs task execution role to ECS EC2 Tasks. | `bool` | `false` | no | | [ecr\_repo\_arns](#input\_ecr\_repo\_arns) | The ARNs of the ECR repos. By default, allows all repositories. | `list(string)` |
[
"*"
]
| no | | [ecs\_cluster](#input\_ecs\_cluster) | ECS cluster object for this task. |
object({
arn = string
name = string
})
| n/a | yes | +| [ecs\_exec\_enable](#input\_ecs\_exec\_enable) | Enable the ability to execute commands on the containers via Amazon ECS Exec | `bool` | `false` | no | | [ecs\_instance\_role](#input\_ecs\_instance\_role) | The name of the ECS instance role. | `string` | `""` | no | | [ecs\_subnet\_ids](#input\_ecs\_subnet\_ids) | Subnet IDs for the ECS tasks. | `list(string)` | n/a | yes | | [ecs\_use\_fargate](#input\_ecs\_use\_fargate) | Whether to use Fargate for the task definition. | `bool` | `false` | no | diff --git a/examples/no-load-balancer/main.tf b/examples/no-load-balancer/main.tf index a5fc438..b19997a 100644 --- a/examples/no-load-balancer/main.tf +++ b/examples/no-load-balancer/main.tf @@ -83,6 +83,7 @@ module "ecs-service" { ecs_subnet_ids = module.vpc.public_subnets ecs_vpc_id = module.vpc.vpc_id ecs_use_fargate = true + ecs_exec_enable = var.ecs_exec_enable assign_public_ip = true additional_security_group_ids = [ aws_security_group.ecs_allow_http.id diff --git a/examples/no-load-balancer/variables.tf b/examples/no-load-balancer/variables.tf index 5e61846..97a5f34 100644 --- a/examples/no-load-balancer/variables.tf +++ b/examples/no-load-balancer/variables.tf @@ -9,3 +9,8 @@ variable "test_name" { variable "vpc_azs" { type = list(string) } + +variable "ecs_exec_enable" { + type = bool +} + diff --git a/main.tf b/main.tf index 2fc6d30..c3a3625 100644 --- a/main.tf +++ b/main.tf @@ -374,6 +374,59 @@ resource "aws_iam_role_policy" "task_execution_role_policy" { policy = data.aws_iam_policy_document.task_execution_role_policy_doc.json } +# +# ECS Exec +# + +data "aws_iam_policy_document" "task_role_ecs_exec" { + count = var.ecs_exec_enable ? 1 : 0 + statement { + sid = "AllowECSExec" + effect = "Allow" + + actions = [ + "ssmmessages:CreateControlChannel", + "ssmmessages:CreateDataChannel", + "ssmmessages:OpenControlChannel", + "ssmmessages:OpenDataChannel" + ] + + resources = ["*"] + } + + statement { + sid = "AllowDescribeLogGroups" + actions = [ + "logs:DescribeLogGroups", + ] + + resources = ["*"] + } + + statement { + sid = "AllowECSExecLogging" + actions = [ + "logs:CreateLogStream", + "logs:DescribeLogStreams", + "logs:PutLogEvents", + ] + resources = ["${aws_cloudwatch_log_group.main.arn}:*"] + } +} + +resource "aws_iam_policy" "task_role_ecs_exec" { + count = var.ecs_exec_enable ? 1 : 0 + name = "${aws_iam_role.task_role.name}-ecs-exec" + description = "Allow ECS Exec with Cloudwatch logging when attached to an ECS task role" + policy = join("", data.aws_iam_policy_document.task_role_ecs_exec.*.json) +} + +resource "aws_iam_role_policy_attachment" "task_role_ecs_exec" { + count = var.ecs_exec_enable ? 1 : 0 + role = join("", aws_iam_role.task_role.*.name) + policy_arn = join("", aws_iam_policy.task_role_ecs_exec.*.arn) +} + # # ECS # @@ -447,8 +500,9 @@ resource "aws_ecs_service" "main" { name = var.name cluster = var.ecs_cluster.arn - launch_type = local.ecs_service_launch_type - platform_version = local.fargate_platform_version + launch_type = local.ecs_service_launch_type + platform_version = local.fargate_platform_version + enable_execute_command = var.ecs_exec_enable # Use latest active revision task_definition = "${aws_ecs_task_definition.main.family}:${max( diff --git a/test/terraform_aws_ecs_service_test.go b/test/terraform_aws_ecs_service_test.go index d145b20..676a799 100644 --- a/test/terraform_aws_ecs_service_test.go +++ b/test/terraform_aws_ecs_service_test.go @@ -12,6 +12,7 @@ import ( "github.com/gruntwork-io/terratest/modules/random" "github.com/gruntwork-io/terratest/modules/terraform" test_structure "github.com/gruntwork-io/terratest/modules/test-structure" + "github.com/stretchr/testify/require" ) func TestTerraformAwsEcsServiceNoLoadBalancer(t *testing.T) { @@ -29,9 +30,10 @@ func TestTerraformAwsEcsServiceNoLoadBalancer(t *testing.T) { // Variables to pass to our Terraform code using -var options Vars: map[string]interface{}{ - "test_name": ecsServiceName, - "vpc_azs": vpcAzs, - "region": awsRegion, + "test_name": ecsServiceName, + "vpc_azs": vpcAzs, + "region": awsRegion, + "ecs_exec_enable": false, }, EnvVars: map[string]string{ "AWS_DEFAULT_REGION": awsRegion, @@ -192,3 +194,36 @@ func TestTerraformAwsEcsServiceNlb(t *testing.T) { timeBetweenRetries, ) } + +func TestTerraformAwsEcsServiceEcsExec(t *testing.T) { + t.Parallel() + + tempTestFolder := test_structure.CopyTerraformFolderToTemp(t, "../", "examples/no-load-balancer") + + ecsServiceName := fmt.Sprintf("terratest-simple-%s", strings.ToLower(random.UniqueId())) + awsRegion := "us-west-2" + vpcAzs := aws.GetAvailabilityZones(t, awsRegion)[:3] + + terraformOptions := &terraform.Options{ + // The path to where our Terraform code is located + TerraformDir: tempTestFolder, + + // Variables to pass to our Terraform code using -var options + Vars: map[string]interface{}{ + "test_name": ecsServiceName, + "vpc_azs": vpcAzs, + "region": awsRegion, + "ecs_exec_enable": true, + }, + EnvVars: map[string]string{ + "AWS_DEFAULT_REGION": awsRegion, + }, + } + + defer terraform.Destroy(t, terraformOptions) + terraform.InitAndApply(t, terraformOptions) + + // Test by execing uname on the running container + err := EcsExecCommand(t, awsRegion, ecsServiceName, "uname") + require.Nil(t, err, err) +} diff --git a/test/test_helper.go b/test/test_helper.go index 2da2d5e..c6de9ed 100644 --- a/test/test_helper.go +++ b/test/test_helper.go @@ -2,6 +2,7 @@ package test import ( "fmt" + "strings" "testing" "time" @@ -90,3 +91,34 @@ func GetPublicIP(t *testing.T, region string, enis []string) *string { publicIP := eniDetail.NetworkInterfaces[0].Association.PublicIp return publicIP } + +func EcsExecCommand(t *testing.T, region string, cluster string, command string) error { + ecsClient, err := aws.NewEcsClientE(t, region) + if err != nil { + return err + } + + tasksOutput := GetTasks(t, region, cluster) + taskSplit := strings.Split(*tasksOutput.TaskArns[0], "/") + task := taskSplit[len(taskSplit)-1] + + params := &ecs.ExecuteCommandInput{ + Cluster: awssdk.String(cluster), + Command: awssdk.String(command), + Task: awssdk.String(task), + Interactive: awssdk.Bool(true), + } + + maxRetries := 3 + retryDuration, _ := time.ParseDuration("30s") + _, err = retry.DoWithRetryE(t, fmt.Sprintf("Execute ECS command with params %v", params), maxRetries, retryDuration, func() (string, error) { + req, _ := ecsClient.ExecuteCommandRequest(params) + err = req.Send() + if err != nil { + return "failed to execute command", err + } + return fmt.Sprintf("Executed command %s", command), nil + }, + ) + return err +} diff --git a/variables.tf b/variables.tf index e243541..da1dfad 100644 --- a/variables.tf +++ b/variables.tf @@ -234,3 +234,9 @@ variable "health_check_grace_period_seconds" { default = null type = number } + +variable "ecs_exec_enable" { + description = "Enable the ability to execute commands on the containers via Amazon ECS Exec" + default = false + type = bool +} diff --git a/versions.tf b/versions.tf index 34260e6..366705a 100644 --- a/versions.tf +++ b/versions.tf @@ -4,7 +4,7 @@ terraform { required_providers { aws = { source = "hashicorp/aws" - version = ">= 3.0" + version = ">= 3.34" } } }