diff --git a/.circleci/config.yml b/.circleci/config.yml index 422e58cf..9b8a5cb7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,7 +4,7 @@ orbs: executors: consul-ecs-test: docker: - - image: docker.mirror.hashicorp.services/hashicorpdev/consul-ecs-test:0.3.0 + - image: docker.mirror.hashicorp.services/hashicorpdev/consul-ecs-test:0.3.1 environment: TEST_RESULTS: &TEST_RESULTS /tmp/test-results # path to where test results are saved @@ -17,7 +17,7 @@ jobs: # Restore go module cache if there is one - restore_cache: keys: - - consul-ecs-acceptance-modcache-v1-{{ checksum "test/acceptance/go.mod" }} + - consul-ecs-acceptance-modcache-v2-{{ checksum "test/acceptance/go.mod" }} - run: name: go mod download @@ -26,9 +26,9 @@ jobs: # Save go module cache if the go.mod file has changed - save_cache: - key: consul-ecs-acceptance-modcache-v1-{{ checksum "test/acceptance/go.mod" }} + key: consul-ecs-acceptance-modcache-v2-{{ checksum "test/acceptance/go.mod" }} paths: - - "/go/pkg/mod" + - "/home/circleci/go/pkg/mod" # check go fmt output because it does not report non-zero when there are fmt changes - run: @@ -99,7 +99,7 @@ jobs: # Restore go module cache if there is one - restore_cache: keys: - - consul-ecs-acceptance-modcache-v1-{{ checksum "test/acceptance/go.mod" }} + - consul-ecs-acceptance-modcache-v2-{{ checksum "test/acceptance/go.mod" }} - run: mkdir -p $TEST_RESULTS @@ -108,7 +108,7 @@ jobs: working_directory: test/acceptance/tests no_output_timeout: 1h command: | - gotestsum --junitfile "$TEST_RESULTS/gotestsum-report.xml" -- ./... -p 1 -timeout 1h -v -failfast + gotestsum --junitfile "$TEST_RESULTS/gotestsum-report.xml" --format standard-verbose -- ./... -p 1 -timeout 1h -v -failfast - store_test_results: path: *TEST_RESULTS diff --git a/CHANGELOG.md b/CHANGELOG.md index 3da97850..a5537133 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ FEATURES * modules/dev-server: Add `consul_license` input variable to support passing a Consul enterprise license. [[GH-96](https://github.com/hashicorp/terraform-aws-consul-ecs/pull/96)] +* modules/mesh-task, modules/acl-controller: Support the Consul AWS IAM auth method. This requires + Consul 1.12.0+. Add `consul_http_addr`, `client_token_auth_method_name`, and + `service_token_auth_method_name` variables to `mesh-task`. Add `iam_role_path` variable to + `acl-controller`. Add an `iam:GetRole` permission to the task role. Set the tags + `consul.hashicorp.com.service-name` and `consul.hashicorp.com.namespace` on the task role. + [[GH-100](https://github.com/hashicorp/terraform-aws-consul-ecs/pull/100)] ## 0.4.1 (April 8, 2022) diff --git a/modules/acl-controller/main.tf b/modules/acl-controller/main.tf index 718c528b..d4b97657 100644 --- a/modules/acl-controller/main.tf +++ b/modules/acl-controller/main.tf @@ -36,15 +36,20 @@ resource "aws_ecs_task_definition" "this" { image = var.consul_ecs_image essential = true logConfiguration = var.log_configuration, - command = concat([ - "acl-controller", - "-consul-client-secret-arn", aws_secretsmanager_secret.client_token.arn, - "-secret-name-prefix", var.name_prefix + command = concat( + [ + "acl-controller", + "-consul-client-secret-arn", aws_secretsmanager_secret.client_token.arn, + "-secret-name-prefix", var.name_prefix ], var.consul_partitions_enabled ? [ "-partitions-enabled", "-partition", var.consul_partition - ] : []) + ] : [], + var.iam_role_path != "" ? [ + "-iam-role-path", var.iam_role_path, + ] : [], + ) linuxParameters = { initProcessEnabled = true } diff --git a/modules/acl-controller/variables.tf b/modules/acl-controller/variables.tf index 7c495811..52a46d6c 100644 --- a/modules/acl-controller/variables.tf +++ b/modules/acl-controller/variables.tf @@ -37,6 +37,12 @@ variable "log_configuration" { default = null } +variable "iam_role_path" { + description = "IAM roles at this path will be permitted to login to the Consul AWS IAM auth method configured by this controller." + type = string + default = "" +} + variable "subnets" { description = "Subnets where the controller task should be deployed. If these are private subnets then there must be a NAT gateway for image pulls to work. If these are public subnets then you must also set assign_public_ip for image pulls to work." type = list(string) diff --git a/modules/dev-server/variables.tf b/modules/dev-server/variables.tf index 69cec862..da8271f3 100644 --- a/modules/dev-server/variables.tf +++ b/modules/dev-server/variables.tf @@ -58,7 +58,7 @@ variable "lb_ingress_rule_security_groups" { variable "consul_image" { description = "Consul Docker image." type = string - default = "public.ecr.aws/hashicorp/consul:1.11.4" + default = "public.ecr.aws/hashicorp/consul:1.12.0" } variable "consul_license" { diff --git a/modules/mesh-task/config.tf b/modules/mesh-task/config.tf index 34540733..df3fe38b 100644 --- a/modules/mesh-task/config.tf +++ b/modules/mesh-task/config.tf @@ -3,7 +3,18 @@ locals { serviceExtra = lookup(var.consul_ecs_config, "service", {}) proxyExtra = lookup(var.consul_ecs_config, "proxy", {}) + consulLogin = var.acls ? { + // TODO: Switch this to `enabled = var.acls` once the auth method is fully supported. + enabled = var.service_token_auth_method_name != "" + method = var.service_token_auth_method_name + // TODO: Move this to a top-level partition field in the CONSUL_ECS_CONFIG_JSON + extraLoginFlags = var.consul_partition != "" ? ["-partition", var.consul_partition] : [] + } : null + config = { + consulHTTPAddr = var.consul_http_addr + consulCACertFile = "/consul/consul-ca-cert.pem" + consulLogin = local.consulLogin service = merge( { name = local.service_name diff --git a/modules/mesh-task/iam.tf b/modules/mesh-task/iam.tf index c37c3791..f29db497 100644 --- a/modules/mesh-task/iam.tf +++ b/modules/mesh-task/iam.tf @@ -25,6 +25,7 @@ locals { // Create the task role resource "aws_iam_role" "task" { count = local.create_task_role ? 1 : 0 + path = "/ecs/" name = "${var.family}-task" assume_role_policy = jsonencode({ @@ -39,6 +40,44 @@ resource "aws_iam_role" "task" { } ] }) + + tags = { + "consul.hashicorp.com.service-name" = local.service_name + "consul.hashicorp.com.namespace" = var.consul_namespace + } +} + +// If acls are enabled, the task role must be configured with an `iam:GetRole` permission +// to fetch itself, in order to be compatbile with the auth method. +resource "aws_iam_policy" "task" { + count = var.acls ? 1 : 0 + name = "${var.family}-task" + path = "/ecs/" + description = "${var.family} mesh-task task policy" + + policy = < /tmp/consul-ca-cert.pem +echo "$CONSUL_CACERT" > /consul/consul-ca-cert.pem +%{ endif ~} + +%{ if acls && client_token_auth_method_name != "" ~} + +login() { + echo "Logging into auth method: name=${ client_token_auth_method_name }" + consul login \ + -http-addr ${ consul_http_addr } \ + %{ if tls ~} + -ca-file /consul/consul-ca-cert.pem \ + %{ endif ~} + %{ if consul_partition != "" ~} + -partition ${ consul_partition } \ + %{ endif ~} + -type aws -method ${ client_token_auth_method_name } \ + -aws-region "$TASK_REGION" \ + -aws-auto-bearer-token -aws-include-entity \ + -token-sink-file /consul/client-token +} + +while ! login; do + sleep 2 +done + +# This is an env var which is interpolated into the agent-defaults.hcl +export AGENT_TOKEN=$(cat /consul/client-token) %{ endif ~} cat << EOF > /consul/agent-defaults.hcl diff --git a/modules/mesh-task/variables.tf b/modules/mesh-task/variables.tf index 17c50892..fc17c8c3 100644 --- a/modules/mesh-task/variables.tf +++ b/modules/mesh-task/variables.tf @@ -108,7 +108,7 @@ variable "outbound_only" { variable "consul_image" { description = "Consul Docker image." type = string - default = "public.ecr.aws/hashicorp/consul:1.11.4" + default = "public.ecr.aws/hashicorp/consul:1.12.0" } variable "consul_ecs_image" { @@ -230,6 +230,24 @@ variable "retry_join" { type = list(string) } +variable "consul_http_addr" { + description = "Consul HTTP Address. Required when using the IAM Auth Method to obtain ACL tokens." + type = string + default = "" +} + +variable "client_token_auth_method_name" { + description = "The name of the Consul Auth Method to login to for client tokens." + type = string + default = "" +} + +variable "service_token_auth_method_name" { + description = "The name of the Consul Auth Method to login to for service tokens." + type = string + default = "" +} + variable "tags" { description = "A map of tags to add to all resources." type = map(string) diff --git a/test/acceptance/tests/basic/basic_test.go b/test/acceptance/tests/basic/basic_test.go index 798d71ff..df114c96 100644 --- a/test/acceptance/tests/basic/basic_test.go +++ b/test/acceptance/tests/basic/basic_test.go @@ -409,18 +409,30 @@ func TestValidation_NamespaceAndPartitionRequired(t *testing.T) { } func TestBasic(t *testing.T) { - cases := []bool{false, true} + cases := []struct { + secure bool + authMethod bool + }{ + {secure: false}, + {secure: true}, + {secure: true, authMethod: true}, + } - for _, secure := range cases { - t.Run(fmt.Sprintf("secure: %t", secure), func(t *testing.T) { + for _, c := range cases { + name := fmt.Sprintf("secure: %t", c.secure) + if c.authMethod { + name += ",authMethod: true" + } + t.Run(name, func(t *testing.T) { randomSuffix := strings.ToLower(random.UniqueId()) tfVars := suite.Config().TFVars("route_table_ids") - tfVars["secure"] = secure + tfVars["secure"] = c.secure + tfVars["auth_method"] = c.authMethod tfVars["suffix"] = randomSuffix clientServiceName := "test_client" serverServiceName := "test_server" - if secure { + if c.secure { // This uses the explicitly passed service name rather than the task's family name. serverServiceName = "custom_test_server" } @@ -482,7 +494,7 @@ func TestBasic(t *testing.T) { // Wait for passing health check for test_server and test_client tokenHeader := "" - if secure { + if c.secure { tokenHeader = `-H "X-Consul-Token: $CONSUL_HTTP_TOKEN"` } @@ -531,7 +543,7 @@ func TestBasic(t *testing.T) { testClientTaskID := arnParts[len(arnParts)-1] // Create an intention. - if secure { + if c.secure { // First check that connection between apps is unsuccessful. retry.RunWith(&retry.Timer{Timeout: 3 * time.Minute, Wait: 20 * time.Second}, t, func(r *retry.R) { curlOut, err := helpers.ExecuteRemoteCommand(t, suite.Config(), testClientTaskARN, "basic", `/bin/sh -c "curl localhost:1234"`) diff --git a/test/acceptance/tests/basic/terraform/basic-install/main.tf b/test/acceptance/tests/basic/terraform/basic-install/main.tf index 8bff6aa6..3c0fd7af 100644 --- a/test/acceptance/tests/basic/terraform/basic-install/main.tf +++ b/test/acceptance/tests/basic/terraform/basic-install/main.tf @@ -41,6 +41,13 @@ variable "secure" { default = false } +// TODO: Remove this once switched over to the auth method. +variable "auth_method" { + description = "When secure=true, whether the secure configuration uses the auth method." + type = bool + default = false +} + variable "launch_type" { description = "Whether to launch tasks on Fargate or EC2" type = string @@ -143,6 +150,8 @@ module "acl_controller" { subnets = var.subnets name_prefix = var.suffix consul_ecs_image = var.consul_ecs_image + + iam_role_path = var.secure && var.auth_method ? "/ecs" : "" } resource "aws_ecs_service" "test_client" { @@ -198,6 +207,9 @@ module "test_client" { name = "GOLANG_MAIN_B64" value = base64encode(file("${path.module}/shutdown-monitor.go")) }] + linuxParameters = { + initProcessEnabled = true + } # NOTE: `go run ` signal handling is different: https://github.com/golang/go/issues/40467 entryPoint = ["/bin/sh", "-c", < main.go @@ -229,6 +241,12 @@ EOT additional_task_role_policies = [aws_iam_policy.execute-command.arn] + consul_http_addr = var.secure && var.auth_method ? "https://${module.consul_server.server_dns}:8501" : "" + // TODO: Remove these once fully switched over to the auth method. + // These should default to the correct value. + client_token_auth_method_name = var.secure && var.auth_method ? "iam-ecs-client-token" : "" + service_token_auth_method_name = var.secure && var.auth_method ? "iam-ecs-service-token" : "" + consul_agent_configuration = <<-EOT log_level = "debug" EOT @@ -281,6 +299,12 @@ module "test_server" { acl_secret_name_prefix = var.suffix consul_ecs_image = var.consul_ecs_image + consul_http_addr = var.secure && var.auth_method ? "https://${module.consul_server.server_dns}:8501" : "" + // TODO: Remove these once fully switched over to the auth method. + // These should default to the correct value. + client_token_auth_method_name = var.secure && var.auth_method ? "iam-ecs-client-token" : "" + service_token_auth_method_name = var.secure && var.auth_method ? "iam-ecs-service-token" : "" + additional_task_role_policies = [aws_iam_policy.execute-command.arn] } @@ -299,6 +323,13 @@ resource "aws_iam_role" "task" { }, ] }) + + // Terraform does not support managing individual tags for IAM roles yet. + // These tags must be set when a role is passed in to mesh-task, if acls are enabled. + tags = { + "consul.hashicorp.com.service-name" = "${var.server_service_name}_${var.suffix}" + "consul.hashicorp.com.namespace" = "" + } }