From ecbc4ff5439ca66845acdd38ffc501dffbdb63a4 Mon Sep 17 00:00:00 2001 From: Anthony Wat Date: Wed, 12 Jun 2024 02:40:31 -0400 Subject: [PATCH] feat: Add managed_storage_configuration block to aws_ecs_cluster --- .changelog/37932.txt | 3 + internal/service/ecs/cluster.go | 62 +++++++++ internal/service/ecs/cluster_test.go | 139 ++++++++++++++++++++ website/docs/r/ecs_cluster.html.markdown | 157 +++++++++++++++++++---- 4 files changed, 338 insertions(+), 23 deletions(-) create mode 100644 .changelog/37932.txt diff --git a/.changelog/37932.txt b/.changelog/37932.txt new file mode 100644 index 00000000000..22155d47f59 --- /dev/null +++ b/.changelog/37932.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_ecs_cluster: Add `configuration.managed_storage_configuration` argument +``` \ No newline at end of file diff --git a/internal/service/ecs/cluster.go b/internal/service/ecs/cluster.go index 366d29aae7a..36414875ea6 100644 --- a/internal/service/ecs/cluster.go +++ b/internal/service/ecs/cluster.go @@ -99,6 +99,23 @@ func ResourceCluster() *schema.Resource { }, }, }, + "managed_storage_configuration": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "fargate_ephemeral_storage_kms_key_id": { + Type: schema.TypeString, + Optional: true, + }, + names.AttrKMSKeyID: { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, }, }, }, @@ -526,6 +543,11 @@ func flattenClusterConfiguration(apiObject *ecs.ClusterConfiguration) []interfac if apiObject.ExecuteCommandConfiguration != nil { tfMap["execute_command_configuration"] = flattenClusterConfigurationExecuteCommandConfiguration(apiObject.ExecuteCommandConfiguration) } + + if apiObject.ManagedStorageConfiguration != nil { + tfMap["managed_storage_configuration"] = flattenClusterConfigurationManagedStorageConfiguration(apiObject.ManagedStorageConfiguration) + } + return []interface{}{tfMap} } @@ -576,6 +598,24 @@ func flattenClusterConfigurationExecuteCommandConfigurationLogConfiguration(apiO return []interface{}{tfMap} } +func flattenClusterConfigurationManagedStorageConfiguration(apiObject *ecs.ManagedStorageConfiguration) []interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + if apiObject.FargateEphemeralStorageKmsKeyId != nil { + tfMap["fargate_ephemeral_storage_kms_key_id"] = aws.StringValue(apiObject.FargateEphemeralStorageKmsKeyId) + } + + if apiObject.KmsKeyId != nil { + tfMap[names.AttrKMSKeyID] = aws.StringValue(apiObject.KmsKeyId) + } + + return []interface{}{tfMap} +} + func expandClusterConfiguration(nc []interface{}) *ecs.ClusterConfiguration { if len(nc) == 0 || nc[0] == nil { return &ecs.ClusterConfiguration{} @@ -587,6 +627,10 @@ func expandClusterConfiguration(nc []interface{}) *ecs.ClusterConfiguration { config.ExecuteCommandConfiguration = expandClusterConfigurationExecuteCommandConfiguration(v) } + if v, ok := raw["managed_storage_configuration"].([]interface{}); ok && len(v) > 0 { + config.ManagedStorageConfiguration = expandClusterConfigurationManagedStorageConfiguration(v) + } + return config } @@ -642,3 +686,21 @@ func expandClusterConfigurationExecuteCommandLogConfiguration(nc []interface{}) return config } + +func expandClusterConfigurationManagedStorageConfiguration(nc []interface{}) *ecs.ManagedStorageConfiguration { + if len(nc) == 0 || nc[0] == nil { + return &ecs.ManagedStorageConfiguration{} + } + raw := nc[0].(map[string]interface{}) + + config := &ecs.ManagedStorageConfiguration{} + if v, ok := raw["fargate_ephemeral_storage_kms_key_id"].(string); ok && v != "" { + config.FargateEphemeralStorageKmsKeyId = aws.String(v) + } + + if v, ok := raw[names.AttrKMSKeyID].(string); ok && v != "" { + config.KmsKeyId = aws.String(v) + } + + return config +} diff --git a/internal/service/ecs/cluster_test.go b/internal/service/ecs/cluster_test.go index 305b38a7109..f600554079a 100644 --- a/internal/service/ecs/cluster_test.go +++ b/internal/service/ecs/cluster_test.go @@ -265,6 +265,54 @@ func TestAccECSCluster_configuration(t *testing.T) { }) } +func TestAccECSCluster_managedStorageConfiguration(t *testing.T) { + ctx := acctest.Context(t) + var cluster1 ecs.Cluster + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_ecs_cluster.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ECSServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckClusterDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccClusterConfig_managedStorageConfiguration(rName, "aws_kms_key.test.arn", "null"), + Check: resource.ComposeTestCheckFunc( + testAccCheckClusterExists(ctx, resourceName, &cluster1), + resource.TestCheckResourceAttr(resourceName, "configuration.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "configuration.0.managed_storage_configuration.#", acctest.Ct1), + resource.TestCheckResourceAttrPair(resourceName, "configuration.0.managed_storage_configuration.0.fargate_ephemeral_storage_kms_key_id", "aws_kms_key.test", names.AttrARN), + resource.TestCheckResourceAttr(resourceName, "configuration.0.managed_storage_configuration.0.kms_key_id", ""), + ), + }, + { + ResourceName: resourceName, + ImportStateId: rName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccClusterConfig_managedStorageConfiguration(rName, "null", "aws_kms_key.test.arn"), + Check: resource.ComposeTestCheckFunc( + testAccCheckClusterExists(ctx, resourceName, &cluster1), + resource.TestCheckResourceAttr(resourceName, "configuration.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "configuration.0.managed_storage_configuration.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "configuration.0.managed_storage_configuration.0.fargate_ephemeral_storage_kms_key_id", ""), + resource.TestCheckResourceAttrPair(resourceName, "configuration.0.managed_storage_configuration.0.kms_key_id", "aws_kms_key.test", names.AttrARN), + ), + }, + { + ResourceName: resourceName, + ImportStateId: rName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func testAccCheckClusterDestroy(ctx context.Context) resource.TestCheckFunc { return func(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).ECSConn(ctx) @@ -408,3 +456,94 @@ resource "aws_ecs_cluster" "test" { } `, rName, enable) } + +func testAccClusterConfig_managedStorageConfiguration(rName, fargateEphemeralStorageKmsKeyId, kmsKeyId string) string { + return fmt.Sprintf(` +data "aws_caller_identity" "current" {} + +resource "aws_kms_key" "test" { + description = %[1]q + deletion_window_in_days = 7 +} + +resource "aws_kms_key_policy" "test" { + key_id = aws_kms_key.test.id + policy = jsonencode({ + Id = "ECSClusterFargatePolicy" + Statement = [ + { + Sid = "Enable IAM User Permissions" + Effect = "Allow" + Principal = { + "AWS" : "*" + } + Action = "kms:*" + Resource = "*" + }, + { + Sid = "Allow generate data key access for Fargate tasks." + Effect = "Allow" + Principal = { + Service = "fargate.amazonaws.com" + } + Action = [ + "kms:GenerateDataKeyWithoutPlaintext" + ] + Condition = { + StringEquals = { + "kms:EncryptionContext:aws:ecs:clusterAccount" = [ + data.aws_caller_identity.current.account_id + ] + "kms:EncryptionContext:aws:ecs:clusterName" = [ + %[1]q + ] + } + } + Resource = "*" + }, + { + Sid = "Allow grant creation permission for Fargate tasks." + Effect = "Allow" + Principal = { + Service = "fargate.amazonaws.com" + } + Action = [ + "kms:CreateGrant" + ] + Condition = { + StringEquals = { + "kms:EncryptionContext:aws:ecs:clusterAccount" = [ + data.aws_caller_identity.current.account_id + ] + "kms:EncryptionContext:aws:ecs:clusterName" = [ + %[1]q + ] + } + "ForAllValues:StringEquals" = { + "kms:GrantOperations" = [ + "Decrypt" + ] + } + } + Resource = "*" + } + ] + Version = "2012-10-17" + }) +} + +resource "aws_ecs_cluster" "test" { + name = %[1]q + + configuration { + managed_storage_configuration { + fargate_ephemeral_storage_kms_key_id = %[2]s + kms_key_id = %[3]s + } + } + depends_on = [ + aws_kms_key_policy.test + ] +} +`, rName, fargateEphemeralStorageKmsKeyId, kmsKeyId) +} diff --git a/website/docs/r/ecs_cluster.html.markdown b/website/docs/r/ecs_cluster.html.markdown index 1fa1a01a290..d318da77f63 100644 --- a/website/docs/r/ecs_cluster.html.markdown +++ b/website/docs/r/ecs_cluster.html.markdown @@ -23,7 +23,7 @@ resource "aws_ecs_cluster" "foo" { } ``` -### Example with Log Configuration +### Execute Command Configuration with Override Logging ```terraform resource "aws_kms_key" "example" { @@ -52,42 +52,153 @@ resource "aws_ecs_cluster" "test" { } ``` +### Fargate Ephemeral Storage Encryption with Customer-Managed KMS Key + +```terraform +data "aws_caller_identity" "current" {} + +resource "aws_kms_key" "example" { + description = "example" + deletion_window_in_days = 7 +} + +resource "aws_kms_key_policy" "example" { + key_id = aws_kms_key.example.id + policy = jsonencode({ + Id = "ECSClusterFargatePolicy" + Statement = [ + { + Sid = "Enable IAM User Permissions" + Effect = "Allow" + Principal = { + "AWS" : "*" + } + Action = "kms:*" + Resource = "*" + }, + { + Sid = "Allow generate data key access for Fargate tasks." + Effect = "Allow" + Principal = { + Service = "fargate.amazonaws.com" + } + Action = [ + "kms:GenerateDataKeyWithoutPlaintext" + ] + Condition = { + StringEquals = { + "kms:EncryptionContext:aws:ecs:clusterAccount" = [ + data.aws_caller_identity.current.account_id + ] + "kms:EncryptionContext:aws:ecs:clusterName" = [ + "example" + ] + } + } + Resource = "*" + }, + { + Sid = "Allow grant creation permission for Fargate tasks." + Effect = "Allow" + Principal = { + Service = "fargate.amazonaws.com" + } + Action = [ + "kms:CreateGrant" + ] + Condition = { + StringEquals = { + "kms:EncryptionContext:aws:ecs:clusterAccount" = [ + data.aws_caller_identity.current.account_id + ] + "kms:EncryptionContext:aws:ecs:clusterName" = [ + "example" + ] + } + "ForAllValues:StringEquals" = { + "kms:GrantOperations" = [ + "Decrypt" + ] + } + } + Resource = "*" + } + ] + Version = "2012-10-17" + }) +} + +resource "aws_ecs_cluster" "test" { + name = "example" + + configuration { + managed_storage_configuration { + fargate_ephemeral_storage_kms_key_id = aws_kms_key.example.id + } + } + depends_on = [ + aws_kms_key_policy.example + ] +} +``` + ## Argument Reference -This resource supports the following arguments: +The following arguments are required: -* `configuration` - (Optional) The execute command configuration for the cluster. Detailed below. * `name` - (Required) Name of the cluster (up to 255 letters, numbers, hyphens, and underscores) -* `service_connect_defaults` - (Optional) Configures a default Service Connect namespace. Detailed below. -* `setting` - (Optional) Configuration block(s) with cluster settings. For example, this can be used to enable CloudWatch Container Insights for a cluster. Detailed below. + +The following arguments are optional: + +* `configuration` - (Optional) Execute command configuration for the cluster. See [`configueration` Block](#configuration-block) for details. +* `service_connect_defaults` - (Optional) Default Service Connect namespace. See [`service_connect_defaults` Block](#service_connect_defaults-block) for details. +* `setting` - (Optional) Configuration block(s) with cluster settings. For example, this can be used to enable CloudWatch Container Insights for a cluster. See [`setting` Block](#setting-block) for details. * `tags` - (Optional) Key-value map of resource tags. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. -### `configuration` +### `configuration` Block + +The `configuration` configuration block supports the following arguments: + +* `execute_command_configuration` - (Optional) Details of the execute command configuration. See [`execute_command_configuration` Block](#execute_command_configuration-block) for details. +* `managed_storage_configuration` - (Optional) Details of the managed storage configuration. See [`managed_storage_configuration` Block](#managed_storage_configuration-block) for details. -* `execute_command_configuration` - (Optional) The details of the execute command configuration. Detailed below. +### `execute_command_configuration` Block -#### `execute_command_configuration` +The `execute_command_configuration` configuration block supports the following arguments: -* `kms_key_id` - (Optional) The AWS Key Management Service key ID to encrypt the data between the local client and the container. -* `log_configuration` - (Optional) The log configuration for the results of the execute command actions Required when `logging` is `OVERRIDE`. Detailed below. -* `logging` - (Optional) The log setting to use for redirecting logs for your execute command results. Valid values are `NONE`, `DEFAULT`, and `OVERRIDE`. +* `kms_key_id` - (Optional) AWS Key Management Service key ID to encrypt the data between the local client and the container. +* `log_configuration` - (Optional) Log configuration for the results of the execute command actions. Required when `logging` is `OVERRIDE`. See [`log_configuration` Block](#log_configuration-block) for details. +* `logging` - (Optional) Log setting to use for redirecting logs for your execute command results. Valid values: `NONE`, `DEFAULT`, `OVERRIDE`. -##### `log_configuration` +#### `log_configuration` Block -* `cloud_watch_encryption_enabled` - (Optional) Whether or not to enable encryption on the CloudWatch logs. If not specified, encryption will be disabled. +The `log_configuration` configuration block supports the following arguments: + +* `cloud_watch_encryption_enabled` - (Optional) Whether to enable encryption on the CloudWatch logs. If not specified, encryption will be disabled. * `cloud_watch_log_group_name` - (Optional) The name of the CloudWatch log group to send logs to. -* `s3_bucket_name` - (Optional) The name of the S3 bucket to send logs to. -* `s3_bucket_encryption_enabled` - (Optional) Whether or not to enable encryption on the logs sent to S3. If not specified, encryption will be disabled. -* `s3_key_prefix` - (Optional) An optional folder in the S3 bucket to place logs in. +* `s3_bucket_name` - (Optional) Name of the S3 bucket to send logs to. +* `s3_bucket_encryption_enabled` - (Optional) Whether to enable encryption on the logs sent to S3. If not specified, encryption will be disabled. +* `s3_key_prefix` - (Optional) Optional folder in the S3 bucket to place logs in. -### `setting` +### `managed_storage_configuration` Block -* `name` - (Required) Name of the setting to manage. Valid values: `containerInsights`. -* `value` - (Required) The value to assign to the setting. Valid values are `enabled` and `disabled`. +The `managed_storage_configuration` configuration block supports the following arguments: + +* `fargate_ephemeral_storage_kms_key_id` - (Optional) AWS Key Management Service key ID for the Fargate ephemeral storage. +* `kms_key_id` - (Optional) AWS Key Management Service key ID to encrypt the managed storage. + +### `service_connect_defaults` Block -### `service_connect_defaults` +The `service_connect_defaults` configuration block supports the following arguments: -* `namespace` - (Required) The ARN of the [`aws_service_discovery_http_namespace`](/docs/providers/aws/r/service_discovery_http_namespace.html) that's used when you create a service and don't specify a Service Connect configuration. +* `namespace` - (Required) ARN of the [`aws_service_discovery_http_namespace`](/docs/providers/aws/r/service_discovery_http_namespace.html) that's used when you create a service and don't specify a Service Connect configuration. + +### `setting` Block + +The `setting` configuration block supports the following arguments: + +* `name` - (Required) Name of the setting to manage. Valid values: `containerInsights`. +* `value` - (Required) Value to assign to the setting. Valid values: `enabled`, `disabled`. ## Attribute Reference @@ -99,7 +210,7 @@ This resource exports the following attributes in addition to the arguments abov ## Import -In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import ECS clusters using the `name`. For example: +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import ECS clusters using the cluster name. For example: ```terraform import { @@ -108,7 +219,7 @@ import { } ``` -Using `terraform import`, import ECS clusters using the `name`. For example: +Using `terraform import`, import ECS clusters using the cluster name. For example: ```console % terraform import aws_ecs_cluster.stateless stateless-app