From efa5c223f02d3ae49d6248842b744c19e4cb8551 Mon Sep 17 00:00:00 2001 From: Michael Kania Date: Tue, 31 Aug 2021 16:51:30 -0700 Subject: [PATCH] add IAM for exec and terratest --- README.md | 5 ++- examples/no-load-balancer/main.tf | 1 + examples/no-load-balancer/variables.tf | 5 +++ main.tf | 60 ++++++++++++++++++++++++-- test/terraform_aws_ecs_service_test.go | 41 ++++++++++++++++-- test/test_helper.go | 32 ++++++++++++++ variables.tf | 12 +++--- 7 files changed, 142 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index ae2d94f..21110f7 100644 --- a/README.md +++ b/README.md @@ -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,7 +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\_enable\_execute\_command](#input\_ecs\_enable\_execute\_command) | Whether to enable Amazon ECS Exec for the tasks within the service | `bool` | `false` | no | +| [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 c4ec83a..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,10 +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 - - enable_execute_command = var.ecs_enable_execute_command + 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 953c488..da1dfad 100644 --- a/variables.tf +++ b/variables.tf @@ -76,12 +76,6 @@ variable "ecs_cluster" { }) } -variable "ecs_enable_execute_command" { - description = "Whether to enable Amazon ECS Exec for the tasks within the service" - default = false - type = bool -} - variable "ecs_instance_role" { description = "The name of the ECS instance role." default = "" @@ -240,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 +}