diff --git a/.changelog/26987.txt b/.changelog/26987.txt new file mode 100644 index 000000000000..b9ce5f7b025a --- /dev/null +++ b/.changelog/26987.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_elaticache_cluster: Add `transit_encryption_enabled` argument, enabling in-transit encryption for Memcached clusters inside a VPC +``` diff --git a/internal/service/elasticache/cluster.go b/internal/service/elasticache/cluster.go index 3328e291c463..8622ff4324b3 100644 --- a/internal/service/elasticache/cluster.go +++ b/internal/service/elasticache/cluster.go @@ -326,6 +326,12 @@ func ResourceCluster() *schema.Resource { Computed: true, ForceNew: true, }, + "transit_encryption_enabled": { + Type: schema.TypeBool, + ForceNew: true, + Optional: true, + Default: false, + }, names.AttrTags: tftags.TagsSchema(), names.AttrTagsAll: tftags.TagsSchemaComputed(), }, @@ -337,6 +343,7 @@ func ResourceCluster() *schema.Resource { CustomizeDiffValidateClusterNumCacheNodes, CustomizeDiffClusterMemcachedNodeType, CustomizeDiffValidateClusterMemcachedSnapshotIdentifier, + CustomizeDiffValidateTransitEncryptionEnabled, verify.SetTagsDiff, ), } @@ -437,6 +444,10 @@ func resourceClusterCreate(ctx context.Context, d *schema.ResourceData, meta int input.SnapshotName = aws.String(v.(string)) } + if v, ok := d.GetOk("transit_encryption_enabled"); ok { + input.TransitEncryptionEnabled = aws.Bool(v.(bool)) + } + if v, ok := d.GetOk("az_mode"); ok { input.AZMode = aws.String(v.(string)) } @@ -543,6 +554,7 @@ func resourceClusterRead(ctx context.Context, d *schema.ResourceData, meta inter d.Set("ip_discovery", c.IpDiscovery) d.Set("network_type", c.NetworkType) d.Set("preferred_outpost_arn", c.PreferredOutpostArn) + d.Set("transit_encryption_enabled", c.TransitEncryptionEnabled) return diags } diff --git a/internal/service/elasticache/cluster_test.go b/internal/service/elasticache/cluster_test.go index ec496d9ece24..6c88ea9a5116 100644 --- a/internal/service/elasticache/cluster_test.go +++ b/internal/service/elasticache/cluster_test.go @@ -1228,6 +1228,42 @@ func TestAccElastiCacheCluster_tagWithOtherModification(t *testing.T) { }) } +func TestAccElastiCacheCluster_TransitEncryption(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + var cluster elasticache.CacheCluster + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_elasticache_cluster.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, elasticache.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckClusterDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccClusterConfig_transitEncryption(rName, "memcached", "1.6.11"), + ExpectError: regexache.MustCompile(`Transit encryption is not supported for memcached version 1.6.11`), + }, + { + Config: testAccClusterConfig_transitEncryption(rName, "redis", "6.2"), + ExpectError: regexache.MustCompile(`aws_elasticache_cluster does not support transit encryption using the redis engine, use aws_elasticache_replication_group instead`), + }, + { + Config: testAccClusterConfig_transitEncryption(rName, "memcached", "1.6.12"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckClusterExists(ctx, resourceName, &cluster), + resource.TestCheckResourceAttr(resourceName, "engine", "memcached"), + resource.TestCheckResourceAttr(resourceName, "engine_version", "1.6.12"), + resource.TestCheckResourceAttr(resourceName, "transit_encryption_enabled", "true"), + ), + }, + }, + }) +} + func TestAccElastiCacheCluster_outpost_memcached(t *testing.T) { ctx := acctest.Context(t) if testing.Short() { @@ -2153,3 +2189,17 @@ resource "aws_elasticache_cluster" "test" { } `, rName, version, tagKey1, tagValue1) } + +func testAccClusterConfig_transitEncryption(rName, engine, version string) string { + return fmt.Sprintf(` +resource "aws_elasticache_cluster" "test" { + apply_immediately = true + cluster_id = "%[1]s" + engine = "%[2]s" + engine_version = "%[3]s" + node_type = "cache.t3.medium" + num_cache_nodes = 1 + transit_encryption_enabled = true +} +`, rName, engine, version) +} diff --git a/internal/service/elasticache/diff.go b/internal/service/elasticache/diff.go index dfedae91963a..ebc58cd50f2d 100644 --- a/internal/service/elasticache/diff.go +++ b/internal/service/elasticache/diff.go @@ -6,11 +6,15 @@ package elasticache import ( "context" "errors" + "fmt" "github.com/aws/aws-sdk-go/service/elasticache" + gversion "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) +var minMemcachedTransitEncryptionVersion = gversion.Must(gversion.NewVersion("1.6.12")) + // CustomizeDiffValidateClusterAZMode validates that `num_cache_nodes` is greater than 1 when `az_mode` is "cross-az" func CustomizeDiffValidateClusterAZMode(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { if v, ok := diff.GetOk("az_mode"); !ok || v.(string) != elasticache.AZModeCrossAz { @@ -69,3 +73,27 @@ func CustomizeDiffValidateReplicationGroupAutomaticFailover(_ context.Context, d } return nil } + +// CustomizeDiffValidateTransitEncryptionEnabled validates that an appropriate engine type and version +// are utilized when in-transit encryption is enabled +func CustomizeDiffValidateTransitEncryptionEnabled(_ context.Context, diff *schema.ResourceDiff, _ interface{}) error { + if v, ok := diff.GetOk("transit_encryption_enabled"); ok && v.(bool) { + if engine := diff.Get("engine").(string); engine == engineRedis { + return errors.New("aws_elasticache_cluster does not support transit encryption using the redis engine, use aws_elasticache_replication_group instead") + } + + engineVersion, ok := diff.GetOk("engine_version") + if !ok { + return nil + } + version, err := normalizeEngineVersion(engineVersion.(string)) + if err != nil { + return err + } + if version.LessThan(minMemcachedTransitEncryptionVersion) { + return fmt.Errorf("Transit encryption is not supported for memcached version %v", version) + } + } + + return nil +} diff --git a/website/docs/r/elasticache_cluster.html.markdown b/website/docs/r/elasticache_cluster.html.markdown index ea74559428a4..0c8cd878095d 100644 --- a/website/docs/r/elasticache_cluster.html.markdown +++ b/website/docs/r/elasticache_cluster.html.markdown @@ -181,6 +181,7 @@ The minimum maintenance window is a 60 minute period. Example: `sun:05:00-sun:09 * `snapshot_window` - (Optional, Redis only) Daily time range (in UTC) during which ElastiCache will begin taking a daily snapshot of your cache cluster. Example: 05:00-09:00 * `subnet_group_name` – (Optional, VPC only) Name of the subnet group to be used for the cache cluster. Changing this value will re-create the resource. * `tags` - (Optional) Map of tags to assign to the resource. 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. +* `transit_encryption_enabled` - (Optional) Enable encryption in-transit. Supported only with Memcached versions `1.6.12` and later, running in a VPC. See the [ElastiCache in-transit encryption](https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/in-transit-encryption-mc.html) documentation for more details. ### Log Delivery Configuration