Skip to content

Commit bdeae70

Browse files
authored
Accept map or list for policy arns, fix race condition (#198)
1 parent c9f96f1 commit bdeae70

File tree

10 files changed

+752
-122
lines changed

10 files changed

+752
-122
lines changed

README.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -367,13 +367,13 @@ Available targets:
367367
| <a name="input_tags"></a> [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).<br>Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no |
368368
| <a name="input_task_cpu"></a> [task\_cpu](#input\_task\_cpu) | The number of CPU units used by the task. If using `FARGATE` launch type `task_cpu` must match [supported memory values](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#task_size) | `number` | `256` | no |
369369
| <a name="input_task_definition"></a> [task\_definition](#input\_task\_definition) | Reuse an existing task definition family and revision for the ecs service instead of creating one | `string` | `null` | no |
370-
| <a name="input_task_exec_policy_arns"></a> [task\_exec\_policy\_arns](#input\_task\_exec\_policy\_arns) | A list of IAM Policy ARNs to attach to the generated task execution role. | `list(string)` | `[]` | no |
371-
| <a name="input_task_exec_policy_arns_map"></a> [task\_exec\_policy\_arns\_map](#input\_task\_exec\_policy\_arns\_map) | A map of name to IAM Policy ARNs to attach to the generated task execution role. | `map(string)` | `{}` | no |
370+
| <a name="input_task_exec_policy_arns"></a> [task\_exec\_policy\_arns](#input\_task\_exec\_policy\_arns) | A list of IAM Policy ARNs to attach to the generated task execution role.<br>Changes to the list will have ripple effects, so use `task_exec_policy_arns_map` if possible. | `list(string)` | `[]` | no |
371+
| <a name="input_task_exec_policy_arns_map"></a> [task\_exec\_policy\_arns\_map](#input\_task\_exec\_policy\_arns\_map) | A map of name to IAM Policy ARNs to attach to the generated task execution role.<br>The names are arbitrary, but must be known at plan time. The purpose of the name<br>is so that changes to one ARN do not cause a ripple effect on the other ARNs.<br>If you cannot provide unique names known at plan time, use `task_exec_policy_arns` instead. | `map(string)` | `{}` | no |
372372
| <a name="input_task_exec_role_arn"></a> [task\_exec\_role\_arn](#input\_task\_exec\_role\_arn) | A `list(string)` of zero or one ARNs of IAM roles that allows the<br>ECS/Fargate agent to make calls to the ECS API on your behalf.<br>If the list is empty, a role will be created for you.<br>DEPRECATED: you can also pass a `string` with the ARN, but that<br>string must be known a "plan" time. | `any` | `[]` | no |
373373
| <a name="input_task_memory"></a> [task\_memory](#input\_task\_memory) | The amount of memory (in MiB) used by the task. If using Fargate launch type `task_memory` must match [supported cpu value](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#task_size) | `number` | `512` | no |
374374
| <a name="input_task_placement_constraints"></a> [task\_placement\_constraints](#input\_task\_placement\_constraints) | A set of placement constraints rules that are taken into consideration during task placement.<br>Maximum number of placement\_constraints is 10. See [`placement_constraints`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_task_definition#placement-constraints-arguments) | <pre>list(object({<br> type = string<br> expression = string<br> }))</pre> | `[]` | no |
375-
| <a name="input_task_policy_arns"></a> [task\_policy\_arns](#input\_task\_policy\_arns) | A list of IAM Policy ARNs to attach to the generated task role. | `list(string)` | `[]` | no |
376-
| <a name="input_task_policy_arns_map"></a> [task\_policy\_arns\_map](#input\_task\_policy\_arns\_map) | A map of name to IAM Policy ARNs to attach to the generated task role. | `map(string)` | `{}` | no |
375+
| <a name="input_task_policy_arns"></a> [task\_policy\_arns](#input\_task\_policy\_arns) | A list of IAM Policy ARNs to attach to the generated task role.<br>Changes to the list will have ripple effects, so use `task_policy_arns_map` if possible. | `list(string)` | `[]` | no |
376+
| <a name="input_task_policy_arns_map"></a> [task\_policy\_arns\_map](#input\_task\_policy\_arns\_map) | A map of name to IAM Policy ARNs to attach to the generated task role.<br>The names are arbitrary, but must be known at plan time. The purpose of the name<br>is so that changes to one ARN do not cause a ripple effect on the other ARNs.<br>If you cannot provide unique names known at plan time, use `task_policy_arns` instead. | `map(string)` | `{}` | no |
377377
| <a name="input_task_role_arn"></a> [task\_role\_arn](#input\_task\_role\_arn) | A `list(string)` of zero or one ARNs of IAM roles that allows<br>your Amazon ECS container task to make calls to other AWS services.<br>If the list is empty, a role will be created for you.<br>DEPRECATED: you can also pass a `string` with the ARN, but that<br>string must be known a "plan" time. | `any` | `[]` | no |
378378
| <a name="input_tenant"></a> [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no |
379379
| <a name="input_use_alb_security_group"></a> [use\_alb\_security\_group](#input\_use\_alb\_security\_group) | A flag to enable/disable allowing traffic from the ALB security group to the service security group | `bool` | `false` | no |

docs/terraform.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,13 @@
120120
| <a name="input_tags"></a> [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).<br>Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no |
121121
| <a name="input_task_cpu"></a> [task\_cpu](#input\_task\_cpu) | The number of CPU units used by the task. If using `FARGATE` launch type `task_cpu` must match [supported memory values](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#task_size) | `number` | `256` | no |
122122
| <a name="input_task_definition"></a> [task\_definition](#input\_task\_definition) | Reuse an existing task definition family and revision for the ecs service instead of creating one | `string` | `null` | no |
123-
| <a name="input_task_exec_policy_arns"></a> [task\_exec\_policy\_arns](#input\_task\_exec\_policy\_arns) | A list of IAM Policy ARNs to attach to the generated task execution role. | `list(string)` | `[]` | no |
124-
| <a name="input_task_exec_policy_arns_map"></a> [task\_exec\_policy\_arns\_map](#input\_task\_exec\_policy\_arns\_map) | A map of name to IAM Policy ARNs to attach to the generated task execution role. | `map(string)` | `{}` | no |
123+
| <a name="input_task_exec_policy_arns"></a> [task\_exec\_policy\_arns](#input\_task\_exec\_policy\_arns) | A list of IAM Policy ARNs to attach to the generated task execution role.<br>Changes to the list will have ripple effects, so use `task_exec_policy_arns_map` if possible. | `list(string)` | `[]` | no |
124+
| <a name="input_task_exec_policy_arns_map"></a> [task\_exec\_policy\_arns\_map](#input\_task\_exec\_policy\_arns\_map) | A map of name to IAM Policy ARNs to attach to the generated task execution role.<br>The names are arbitrary, but must be known at plan time. The purpose of the name<br>is so that changes to one ARN do not cause a ripple effect on the other ARNs.<br>If you cannot provide unique names known at plan time, use `task_exec_policy_arns` instead. | `map(string)` | `{}` | no |
125125
| <a name="input_task_exec_role_arn"></a> [task\_exec\_role\_arn](#input\_task\_exec\_role\_arn) | A `list(string)` of zero or one ARNs of IAM roles that allows the<br>ECS/Fargate agent to make calls to the ECS API on your behalf.<br>If the list is empty, a role will be created for you.<br>DEPRECATED: you can also pass a `string` with the ARN, but that<br>string must be known a "plan" time. | `any` | `[]` | no |
126126
| <a name="input_task_memory"></a> [task\_memory](#input\_task\_memory) | The amount of memory (in MiB) used by the task. If using Fargate launch type `task_memory` must match [supported cpu value](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html#task_size) | `number` | `512` | no |
127127
| <a name="input_task_placement_constraints"></a> [task\_placement\_constraints](#input\_task\_placement\_constraints) | A set of placement constraints rules that are taken into consideration during task placement.<br>Maximum number of placement\_constraints is 10. See [`placement_constraints`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_task_definition#placement-constraints-arguments) | <pre>list(object({<br> type = string<br> expression = string<br> }))</pre> | `[]` | no |
128-
| <a name="input_task_policy_arns"></a> [task\_policy\_arns](#input\_task\_policy\_arns) | A list of IAM Policy ARNs to attach to the generated task role. | `list(string)` | `[]` | no |
129-
| <a name="input_task_policy_arns_map"></a> [task\_policy\_arns\_map](#input\_task\_policy\_arns\_map) | A map of name to IAM Policy ARNs to attach to the generated task role. | `map(string)` | `{}` | no |
128+
| <a name="input_task_policy_arns"></a> [task\_policy\_arns](#input\_task\_policy\_arns) | A list of IAM Policy ARNs to attach to the generated task role.<br>Changes to the list will have ripple effects, so use `task_policy_arns_map` if possible. | `list(string)` | `[]` | no |
129+
| <a name="input_task_policy_arns_map"></a> [task\_policy\_arns\_map](#input\_task\_policy\_arns\_map) | A map of name to IAM Policy ARNs to attach to the generated task role.<br>The names are arbitrary, but must be known at plan time. The purpose of the name<br>is so that changes to one ARN do not cause a ripple effect on the other ARNs.<br>If you cannot provide unique names known at plan time, use `task_policy_arns` instead. | `map(string)` | `{}` | no |
130130
| <a name="input_task_role_arn"></a> [task\_role\_arn](#input\_task\_role\_arn) | A `list(string)` of zero or one ARNs of IAM roles that allows<br>your Amazon ECS container task to make calls to other AWS services.<br>If the list is empty, a role will be created for you.<br>DEPRECATED: you can also pass a `string` with the ARN, but that<br>string must be known a "plan" time. | `any` | `[]` | no |
131131
| <a name="input_tenant"></a> [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no |
132132
| <a name="input_use_alb_security_group"></a> [use\_alb\_security\_group](#input\_use\_alb\_security\_group) | A flag to enable/disable allowing traffic from the ALB security group to the service security group | `bool` | `false` | no |

examples/complete/main.tf

+20-12
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,45 @@ provider "aws" {
22
region = var.region
33
}
44

5+
locals {
6+
enabled = module.this.enabled
7+
}
8+
59
module "vpc" {
610
source = "cloudposse/vpc/aws"
7-
version = "0.28.1"
11+
version = "2.0.0"
812

9-
cidr_block = var.vpc_cidr_block
13+
ipv4_primary_cidr_block = var.vpc_cidr_block
1014

1115
context = module.this.context
1216
}
1317

1418
module "subnets" {
1519
source = "cloudposse/dynamic-subnets/aws"
16-
version = "0.39.8"
20+
version = "2.1.0"
1721

1822
availability_zones = var.availability_zones
1923
vpc_id = module.vpc.vpc_id
20-
igw_id = module.vpc.igw_id
21-
cidr_block = module.vpc.vpc_cidr_block
22-
nat_gateway_enabled = true
24+
igw_id = [module.vpc.igw_id]
25+
ipv4_cidr_block = [module.vpc.vpc_cidr_block]
26+
nat_gateway_enabled = false
2327
nat_instance_enabled = false
2428

2529
context = module.this.context
2630
}
2731

2832
resource "aws_ecs_cluster" "default" {
29-
name = module.this.id
30-
tags = module.this.tags
33+
#bridgecrew:skip=BC_AWS_LOGGING_11: not required for testing
34+
count = local.enabled ? 1 : 0
35+
name = module.this.id
36+
tags = module.this.tags
3137
}
3238

3339
module "container_definition" {
40+
count = local.enabled ? 1 : 0
41+
3442
source = "cloudposse/ecs-container-definition/aws"
35-
version = "0.58.1"
43+
version = "0.58.2"
3644

3745
container_name = var.container_name
3846
container_image = var.container_image
@@ -71,8 +79,8 @@ module "test_policy" {
7179
module "ecs_alb_service_task" {
7280
source = "../.."
7381
alb_security_group = module.vpc.vpc_default_security_group_id
74-
container_definition_json = module.container_definition.json_map_encoded_list
75-
ecs_cluster_arn = aws_ecs_cluster.default.arn
82+
container_definition_json = one(module.container_definition.*.json_map_encoded_list)
83+
ecs_cluster_arn = one(aws_ecs_cluster.default.*.id)
7684
launch_type = var.ecs_launch_type
7785
vpc_id = module.vpc.vpc_id
7886
security_group_ids = [module.vpc.vpc_default_security_group_id]
@@ -91,7 +99,7 @@ module "ecs_alb_service_task" {
9199
force_new_deployment = var.force_new_deployment
92100
redeploy_on_apply = var.redeploy_on_apply
93101
task_policy_arns = [module.test_policy.policy_arn]
94-
# task_policy_arns_map = { test = module.test_policy.policy_arn }
102+
task_exec_policy_arns_map = { test = module.test_policy.policy_arn }
95103

96104
context = module.this.context
97105
}

examples/complete/outputs.tf

+4-4
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,22 @@ output "vpc_cidr" {
1414
}
1515

1616
output "container_definition_json" {
17-
value = module.container_definition.json_map_encoded_list
17+
value = one(module.container_definition.*.json_map_encoded_list)
1818
description = "JSON encoded list of container definitions for use with other terraform resources such as aws_ecs_task_definition"
1919
}
2020

2121
output "container_definition_json_map" {
22-
value = module.container_definition.json_map_encoded
22+
value = one(module.container_definition.*.json_map_encoded)
2323
description = "JSON encoded container definitions for use with other terraform resources such as aws_ecs_task_definition"
2424
}
2525

2626
output "ecs_cluster_id" {
27-
value = aws_ecs_cluster.default.id
27+
value = one(aws_ecs_cluster.default.*.id)
2828
description = "ECS cluster ID"
2929
}
3030

3131
output "ecs_cluster_arn" {
32-
value = aws_ecs_cluster.default.arn
32+
value = one(aws_ecs_cluster.default.*.arn)
3333
description = "ECS cluster ARN"
3434
}
3535

main.tf

+19-2
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ locals {
1414
redeployment = timestamp()
1515
} : {}
1616

17-
task_policy_arns_map = length(var.task_policy_arns) > 0 ? { for i, a in var.task_policy_arns : i => a } : var.task_policy_arns_map
17+
task_policy_arns_map = merge({ for i, a in var.task_policy_arns : format("_#%v_", i) => a }, var.task_policy_arns_map)
1818

19-
task_exec_policy_arns_map = length(var.task_exec_policy_arns) > 0 ? { for i, a in var.task_exec_policy_arns : i => a } : var.task_exec_policy_arns_map
19+
task_exec_policy_arns_map = merge({ for i, a in var.task_exec_policy_arns : format("_#%v_", i) => a }, var.task_exec_policy_arns_map)
2020
}
2121

2222
module "task_label" {
@@ -450,6 +450,10 @@ resource "aws_ecs_service" "ignore_changes_task_definition" {
450450
lifecycle {
451451
ignore_changes = [task_definition]
452452
}
453+
454+
# Avoid race condition on destroy.
455+
# See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service
456+
depends_on = [aws_iam_role.ecs_service, aws_iam_role_policy.ecs_service]
453457
}
454458

455459
resource "aws_ecs_service" "ignore_changes_task_definition_and_desired_count" {
@@ -545,6 +549,10 @@ resource "aws_ecs_service" "ignore_changes_task_definition_and_desired_count" {
545549
lifecycle {
546550
ignore_changes = [task_definition, desired_count]
547551
}
552+
553+
# Avoid race condition on destroy.
554+
# See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service
555+
depends_on = [aws_iam_role.ecs_service, aws_iam_role_policy.ecs_service]
548556
}
549557

550558
resource "aws_ecs_service" "ignore_changes_desired_count" {
@@ -640,6 +648,10 @@ resource "aws_ecs_service" "ignore_changes_desired_count" {
640648
lifecycle {
641649
ignore_changes = [desired_count]
642650
}
651+
652+
# Avoid race condition on destroy.
653+
# See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service
654+
depends_on = [aws_iam_role.ecs_service, aws_iam_role_policy.ecs_service]
643655
}
644656

645657
resource "aws_ecs_service" "default" {
@@ -731,4 +743,9 @@ resource "aws_ecs_service" "default" {
731743
}
732744

733745
triggers = local.redeployment_trigger
746+
747+
# Avoid race condition on destroy.
748+
# See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ecs_service
749+
depends_on = [aws_iam_role.ecs_service, aws_iam_role_policy.ecs_service]
750+
734751
}

test/src/examples_complete_test.go

+59-9
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,53 @@ package test
22

33
import (
44
"encoding/json"
5+
"os"
6+
"regexp"
7+
"strings"
8+
"testing"
9+
510
"github.com/gruntwork-io/terratest/modules/random"
611
"github.com/gruntwork-io/terratest/modules/terraform"
12+
testStructure "github.com/gruntwork-io/terratest/modules/test-structure"
713
"github.com/stretchr/testify/assert"
8-
"strings"
9-
"testing"
1014
)
1115

16+
func cleanup(t *testing.T, terraformOptions *terraform.Options, tempTestFolder string) {
17+
terraform.Destroy(t, terraformOptions)
18+
os.RemoveAll(tempTestFolder)
19+
}
20+
1221
// Test the Terraform module in examples/complete using Terratest.
1322
func TestExamplesComplete(t *testing.T) {
14-
// This test is not configured to run in parallel (due to shared Terraform workspace)
15-
// t.Parallel()
16-
23+
t.Parallel()
1724
randID := strings.ToLower(random.UniqueId())
1825
attributes := []string{randID}
19-
basename := "eg-test-ecs-alb-service-task-" + randID
26+
27+
rootFolder := "../../"
28+
terraformFolderRelativeToRoot := "examples/complete"
29+
varFiles := []string{"fixtures.us-east-2.tfvars"}
30+
31+
tempTestFolder := testStructure.CopyTerraformFolderToTemp(t, rootFolder, terraformFolderRelativeToRoot)
2032

2133
terraformOptions := &terraform.Options{
2234
// The path to where our Terraform code is located
23-
TerraformDir: "../../examples/complete",
35+
TerraformDir: tempTestFolder,
2436
Upgrade: true,
2537
// Variables to pass to our Terraform code using -var-file options
26-
VarFiles: []string{"fixtures.us-east-2.tfvars"},
38+
VarFiles: varFiles,
2739
Vars: map[string]interface{}{
2840
"attributes": attributes,
2941
},
3042
}
3143

3244
// At the end of the test, run `terraform destroy` to clean up any resources that were created
33-
defer terraform.Destroy(t, terraformOptions)
45+
defer cleanup(t, terraformOptions, tempTestFolder)
3446

3547
// This will run `terraform init` and `terraform apply` and fail the test if there are any errors
3648
terraform.InitAndApply(t, terraformOptions)
3749

50+
basename := "eg-test-ecs-alb-service-task-" + randID
51+
3852
// Run `terraform output` to get the value of an output variable
3953
jsonMap := terraform.OutputRequired(t, terraformOptions, "container_definition_json_map")
4054
// Verify we're getting back the outputs we expect
@@ -109,3 +123,39 @@ func TestExamplesComplete(t *testing.T) {
109123
// Verify we're getting back the outputs we expect
110124
assert.Equal(t, "arn:aws:iam::126450723953:role/"+basename+"-task", taskRoleArn)
111125
}
126+
127+
func TestExamplesCompleteDisabled(t *testing.T) {
128+
t.Parallel()
129+
randID := strings.ToLower(random.UniqueId())
130+
attributes := []string{randID}
131+
132+
rootFolder := "../../"
133+
terraformFolderRelativeToRoot := "examples/complete"
134+
varFiles := []string{"fixtures.us-east-2.tfvars"}
135+
136+
tempTestFolder := testStructure.CopyTerraformFolderToTemp(t, rootFolder, terraformFolderRelativeToRoot)
137+
138+
terraformOptions := &terraform.Options{
139+
// The path to where our Terraform code is located
140+
TerraformDir: tempTestFolder,
141+
Upgrade: true,
142+
// Variables to pass to our Terraform code using -var-file options
143+
VarFiles: varFiles,
144+
Vars: map[string]interface{}{
145+
"attributes": attributes,
146+
"enabled": "false",
147+
},
148+
}
149+
150+
// At the end of the test, run `terraform destroy` to clean up any resources that were created
151+
defer cleanup(t, terraformOptions, tempTestFolder)
152+
153+
// This will run `terraform init` and `terraform apply` and fail the test if there are any errors
154+
results := terraform.InitAndApply(t, terraformOptions)
155+
156+
// Should complete successfully without creating or changing any resources.
157+
// Extract the "Resources:" section of the output to make the error message more readable.
158+
re := regexp.MustCompile(`Resources: [^.]+\.`)
159+
match := re.FindString(results)
160+
assert.Equal(t, "Resources: 0 added, 0 changed, 0 destroyed.", match, "Re-applying the same configuration should not change any resources")
161+
}

0 commit comments

Comments
 (0)