diff --git a/.changelog/18920.txt b/.changelog/18920.txt new file mode 100644 index 000000000000..b6856076dfe8 --- /dev/null +++ b/.changelog/18920.txt @@ -0,0 +1,11 @@ +```release-note:bug +resource/aws_elasticache_cluster: Allows specifying Redis 6.x +``` + +```release-note:bug +resource/aws_elasticache_replication_group: Allows specifying Redis 6.x +``` + +```release-note:enhancement +resource/aws_elasticache_global_replication_group: Adds parameter `engine_version_actual` to match other ElastiCache resources +``` diff --git a/aws/elasticache_validation.go b/aws/elasticache_validation.go new file mode 100644 index 000000000000..e5acc5603101 --- /dev/null +++ b/aws/elasticache_validation.go @@ -0,0 +1,156 @@ +package aws + +import ( + "context" + "errors" + "fmt" + "regexp" + + "github.com/aws/aws-sdk-go/service/elasticache" + multierror "github.com/hashicorp/go-multierror" + gversion "github.com/hashicorp/go-version" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + tfelasticache "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/elasticache" +) + +const ( + redisVersionPreV6RegexpRaw = `[1-5](\.[[:digit:]]+){2}` + redisVersionPostV6RegexpRaw = `([6-9]|[[:digit:]]{2})\.x` + + redisVersionRegexpRaw = redisVersionPreV6RegexpRaw + "|" + redisVersionPostV6RegexpRaw +) + +const ( + redisVersionRegexpPattern = "^" + redisVersionRegexpRaw + "$" + redisVersionPostV6RegexpPattern = "^" + redisVersionPostV6RegexpRaw + "$" +) + +var ( + redisVersionRegexp = regexp.MustCompile(redisVersionRegexpPattern) + redisVersionPostV6Regexp = regexp.MustCompile(redisVersionPostV6RegexpPattern) +) + +func ValidateElastiCacheRedisVersionString(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + + if !redisVersionRegexp.MatchString(value) { + errors = append(errors, fmt.Errorf("%s: Redis versions must match .x when using version 6 or higher, or ..", k)) + } + + return +} + +// NormalizeElastiCacheEngineVersion returns a github.com/hashicorp/go-version Version +// that can handle a regular 1.2.3 version number or a 6.x version number used for +// ElastiCache Redis version 6 and higher +func NormalizeElastiCacheEngineVersion(version string) (*gversion.Version, error) { + if matches := redisVersionPostV6Regexp.FindStringSubmatch(version); matches != nil { + version = matches[1] + } + return gversion.NewVersion(version) +} + +// CustomizeDiffElastiCacheEngineVersion causes re-creation of the resource if the version is being downgraded +func CustomizeDiffElastiCacheEngineVersion(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { + if diff.Id() == "" || !diff.HasChange("engine_version") { + return nil + } + + o, n := diff.GetChange("engine_version") + oVersion, err := NormalizeElastiCacheEngineVersion(o.(string)) + if err != nil { + return fmt.Errorf("error parsing old engine_version: %w", err) + } + nVersion, err := NormalizeElastiCacheEngineVersion(n.(string)) + if err != nil { + return fmt.Errorf("error parsing new engine_version: %w", err) + } + + if nVersion.GreaterThan(oVersion) { + return nil + } + + return diff.ForceNew("engine_version") +} + +// 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 { + return nil + } + if v, ok := diff.GetOk("num_cache_nodes"); !ok || v.(int) != 1 { + return nil + } + + return errors.New(`az_mode "cross-az" is not supported with num_cache_nodes = 1`) +} + +// CustomizeDiffValidateClusterEngineVersion validates the correct format for `engine_version`, based on `engine` +func CustomizeDiffValidateClusterEngineVersion(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { + // Memcached: Versions in format .. + // Redis: Starting with version 6, must match .x, prior to version 6, .. + engineVersion, ok := diff.GetOk("engine_version") + if !ok { + return nil + } + + var validator schema.SchemaValidateFunc + if v, ok := diff.GetOk("engine"); !ok || v.(string) == tfelasticache.EngineMemcached { + validator = validateVersionString + } else { + validator = ValidateElastiCacheRedisVersionString + } + + _, errs := validator(engineVersion, "engine_version") + + var err *multierror.Error + err = multierror.Append(err, errs...) + return err.ErrorOrNil() +} + +// CustomizeDiffValidateClusterNumCacheNodes validates that `num_cache_nodes` is 1 when `engine` is "redis" +func CustomizeDiffValidateClusterNumCacheNodes(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { + if v, ok := diff.GetOk("engine"); !ok || v.(string) == tfelasticache.EngineMemcached { + return nil + } + + if v, ok := diff.GetOk("num_cache_nodes"); !ok || v.(int) == 1 { + return nil + } + return errors.New(`engine "redis" does not support num_cache_nodes > 1`) +} + +// CustomizeDiffClusterMemcachedNodeType causes re-creation when `node_type` is changed and `engine` is "memcached" +func CustomizeDiffClusterMemcachedNodeType(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { + // Engine memcached does not currently support vertical scaling + // https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/Scaling.html#Scaling.Memcached.Vertically + if diff.Id() == "" || !diff.HasChange("node_type") { + return nil + } + if v, ok := diff.GetOk("engine"); !ok || v.(string) == tfelasticache.EngineRedis { + return nil + } + return diff.ForceNew("node_type") +} + +// CustomizeDiffValidateClusterMemcachedSnapshotIdentifier validates that `final_snapshot_identifier` is not set when `engine` is "memcached" +func CustomizeDiffValidateClusterMemcachedSnapshotIdentifier(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { + if v, ok := diff.GetOk("engine"); !ok || v.(string) == tfelasticache.EngineRedis { + return nil + } + if _, ok := diff.GetOk("final_snapshot_identifier"); !ok { + return nil + } + return errors.New(`engine "memcached" does not support final_snapshot_identifier`) +} + +// CustomizeDiffValidateReplicationGroupAutomaticFailover validates that `automatic_failover_enabled` is set when `multi_az_enabled` is true +func CustomizeDiffValidateReplicationGroupAutomaticFailover(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { + if v := diff.Get("multi_az_enabled").(bool); !v { + return nil + } + if v := diff.Get("automatic_failover_enabled").(bool); !v { + return errors.New(`automatic_failover_enabled must be true if multi_az_enabled is true`) + } + return nil +} diff --git a/aws/internal/service/elasticache/service.go b/aws/internal/service/elasticache/service.go new file mode 100644 index 000000000000..4cdc87d0e1bd --- /dev/null +++ b/aws/internal/service/elasticache/service.go @@ -0,0 +1,14 @@ +package elasticache + +const ( + EngineMemcached = "memcached" + EngineRedis = "redis" +) + +// Engine_Values returns all elements of the Engine enum +func Engine_Values() []string { + return []string{ + EngineMemcached, + EngineRedis, + } +} diff --git a/aws/resource_aws_elasticache_cluster.go b/aws/resource_aws_elasticache_cluster.go index 600991609219..df67db37d2e7 100644 --- a/aws/resource_aws_elasticache_cluster.go +++ b/aws/resource_aws_elasticache_cluster.go @@ -1,7 +1,6 @@ package aws import ( - "context" "errors" "fmt" "log" @@ -19,6 +18,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + tfelasticache "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/elasticache" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/elasticache/finder" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/elasticache/waiter" "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" @@ -115,14 +115,19 @@ func resourceAwsElasticacheCluster() *schema.Resource { Computed: true, }, "engine": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(tfelasticache.Engine_Values(), false), + }, + "engine_version": { Type: schema.TypeString, Optional: true, Computed: true, - ForceNew: true, }, - "engine_version": { + "engine_version_actual": { Type: schema.TypeString, - Optional: true, Computed: true, }, "maintenance_window": { @@ -257,71 +262,12 @@ func resourceAwsElasticacheCluster() *schema.Resource { }, CustomizeDiff: customdiff.Sequence( - func(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { - // Plan time validation for az_mode - // InvalidParameterCombination: Must specify at least two cache nodes in order to specify AZ Mode of 'cross-az'. - if v, ok := diff.GetOk("az_mode"); !ok || v.(string) != elasticache.AZModeCrossAz { - return nil - } - if v, ok := diff.GetOk("num_cache_nodes"); !ok || v.(int) != 1 { - return nil - } - return errors.New(`az_mode "cross-az" is not supported with num_cache_nodes = 1`) - }, - func(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { - // Plan time validation for engine_version - // InvalidParameterCombination: Cannot modify memcached from 1.4.33 to 1.4.24 - // InvalidParameterCombination: Cannot modify redis from 3.2.6 to 3.2.4 - if diff.Id() == "" || !diff.HasChange("engine_version") { - return nil - } - o, n := diff.GetChange("engine_version") - oVersion, err := gversion.NewVersion(o.(string)) - if err != nil { - return err - } - nVersion, err := gversion.NewVersion(n.(string)) - if err != nil { - return err - } - if nVersion.GreaterThan(oVersion) { - return nil - } - return diff.ForceNew("engine_version") - }, - func(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { - // Plan time validation for num_cache_nodes - // InvalidParameterValue: Cannot create a Redis cluster with a NumCacheNodes parameter greater than 1. - if v, ok := diff.GetOk("engine"); !ok || v.(string) == "memcached" { - return nil - } - if v, ok := diff.GetOk("num_cache_nodes"); !ok || v.(int) == 1 { - return nil - } - return errors.New(`engine "redis" does not support num_cache_nodes > 1`) - }, - func(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { - // Engine memcached does not currently support vertical scaling - // InvalidParameterCombination: Scaling is not supported for engine memcached - // https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/Scaling.html#Scaling.Memcached.Vertically - if diff.Id() == "" || !diff.HasChange("node_type") { - return nil - } - if v, ok := diff.GetOk("engine"); !ok || v.(string) == "redis" { - return nil - } - return diff.ForceNew("node_type") - }, - func(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { - if v, ok := diff.GetOk("engine"); !ok || v.(string) == "redis" { - return nil - } - if _, ok := diff.GetOk("final_snapshot_identifier"); !ok { - return nil - } - return errors.New(`engine "memcached" does not support final_snapshot_identifier`) - }, - SetTagsDiff, + CustomizeDiffValidateClusterAZMode, + CustomizeDiffValidateClusterEngineVersion, + CustomizeDiffElastiCacheEngineVersion, + CustomizeDiffValidateClusterNumCacheNodes, + CustomizeDiffClusterMemcachedNodeType, + CustomizeDiffValidateClusterMemcachedSnapshotIdentifier, ), } } @@ -443,10 +389,16 @@ func resourceAwsElasticacheClusterRead(d *schema.ResourceData, meta interface{}) } d.Set("cluster_id", c.CacheClusterId) - d.Set("node_type", c.CacheNodeType) + + if err := elasticacheSetResourceDataFromCacheCluster(d, c); err != nil { + return err + } + + d.Set("snapshot_window", c.SnapshotWindow) + d.Set("snapshot_retention_limit", c.SnapshotRetentionLimit) + d.Set("num_cache_nodes", c.NumCacheNodes) - d.Set("engine", c.Engine) - d.Set("engine_version", c.EngineVersion) + if c.ConfigurationEndpoint != nil { d.Set("port", c.ConfigurationEndpoint.Port) d.Set("configuration_endpoint", aws.String(fmt.Sprintf("%s:%d", aws.StringValue(c.ConfigurationEndpoint.Address), aws.Int64Value(c.ConfigurationEndpoint.Port)))) @@ -459,15 +411,6 @@ func resourceAwsElasticacheClusterRead(d *schema.ResourceData, meta interface{}) d.Set("replication_group_id", c.ReplicationGroupId) } - d.Set("subnet_group_name", c.CacheSubnetGroupName) - d.Set("security_group_names", flattenElastiCacheSecurityGroupNames(c.CacheSecurityGroups)) - d.Set("security_group_ids", flattenElastiCacheSecurityGroupIds(c.SecurityGroups)) - if c.CacheParameterGroup != nil { - d.Set("parameter_group_name", c.CacheParameterGroup.CacheParameterGroupName) - } - d.Set("maintenance_window", c.PreferredMaintenanceWindow) - d.Set("snapshot_window", c.SnapshotWindow) - d.Set("snapshot_retention_limit", c.SnapshotRetentionLimit) if c.NotificationConfiguration != nil { if *c.NotificationConfiguration.TopicStatus == "active" { d.Set("notification_topic_arn", c.NotificationConfiguration.TopicArn) @@ -506,6 +449,46 @@ func resourceAwsElasticacheClusterRead(d *schema.ResourceData, meta interface{}) return nil } +func elasticacheSetResourceDataFromCacheCluster(d *schema.ResourceData, c *elasticache.CacheCluster) error { + d.Set("node_type", c.CacheNodeType) + + d.Set("engine", c.Engine) + if err := elasticacheSetResourceDataEngineVersionFromCacheCluster(d, c); err != nil { + return err + } + + d.Set("subnet_group_name", c.CacheSubnetGroupName) + if err := d.Set("security_group_names", flattenElastiCacheSecurityGroupNames(c.CacheSecurityGroups)); err != nil { + return fmt.Errorf("error setting security_group_names: %w", err) + } + if err := d.Set("security_group_ids", flattenElastiCacheSecurityGroupIds(c.SecurityGroups)); err != nil { + return fmt.Errorf("error setting security_group_ids: %w", err) + } + + if c.CacheParameterGroup != nil { + d.Set("parameter_group_name", c.CacheParameterGroup.CacheParameterGroupName) + } + + d.Set("maintenance_window", c.PreferredMaintenanceWindow) + + return nil +} + +func elasticacheSetResourceDataEngineVersionFromCacheCluster(d *schema.ResourceData, c *elasticache.CacheCluster) error { + engineVersion, err := gversion.NewVersion(aws.StringValue(c.EngineVersion)) + if err != nil { + return fmt.Errorf("error reading ElastiCache Cache Cluster (%s) engine version: %w", d.Id(), err) + } + if engineVersion.Segments()[0] < 6 { + d.Set("engine_version", engineVersion.String()) + } else { + d.Set("engine_version", fmt.Sprintf("%d.x", engineVersion.Segments()[0])) + } + d.Set("engine_version_actual", engineVersion.String()) + + return nil +} + func resourceAwsElasticacheClusterUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).elasticacheconn diff --git a/aws/resource_aws_elasticache_cluster_test.go b/aws/resource_aws_elasticache_cluster_test.go index 40f72d8639e7..c42ad008528e 100644 --- a/aws/resource_aws_elasticache_cluster_test.go +++ b/aws/resource_aws_elasticache_cluster_test.go @@ -179,6 +179,7 @@ func TestAccAWSElasticacheCluster_ParameterGroupName_Default(t *testing.T) { testAccCheckAWSElasticacheClusterExists(resourceName, &ec), resource.TestCheckResourceAttr(resourceName, "engine", "memcached"), resource.TestCheckResourceAttr(resourceName, "engine_version", "1.4.34"), + resource.TestCheckResourceAttr(resourceName, "engine_version_actual", "1.4.34"), resource.TestCheckResourceAttr(resourceName, "parameter_group_name", "default.memcached1.4"), ), }, @@ -500,6 +501,7 @@ func TestAccAWSElasticacheCluster_EngineVersion_Memcached(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckAWSElasticacheClusterExists(resourceName, &pre), resource.TestCheckResourceAttr(resourceName, "engine_version", "1.4.33"), + resource.TestCheckResourceAttr(resourceName, "engine_version_actual", "1.4.33"), ), }, { @@ -508,6 +510,7 @@ func TestAccAWSElasticacheCluster_EngineVersion_Memcached(t *testing.T) { testAccCheckAWSElasticacheClusterExists(resourceName, &mid), testAccCheckAWSElasticacheClusterRecreated(&pre, &mid), resource.TestCheckResourceAttr(resourceName, "engine_version", "1.4.24"), + resource.TestCheckResourceAttr(resourceName, "engine_version_actual", "1.4.24"), ), }, { @@ -516,6 +519,7 @@ func TestAccAWSElasticacheCluster_EngineVersion_Memcached(t *testing.T) { testAccCheckAWSElasticacheClusterExists(resourceName, &post), testAccCheckAWSElasticacheClusterNotRecreated(&mid, &post), resource.TestCheckResourceAttr(resourceName, "engine_version", "1.4.34"), + resource.TestCheckResourceAttr(resourceName, "engine_version_actual", "1.4.34"), ), }, }, @@ -523,7 +527,7 @@ func TestAccAWSElasticacheCluster_EngineVersion_Memcached(t *testing.T) { } func TestAccAWSElasticacheCluster_EngineVersion_Redis(t *testing.T) { - var pre, mid, post elasticache.CacheCluster + var v1, v2, v3, v4, v5 elasticache.CacheCluster rName := acctest.RandomWithPrefix("tf-acc-test") resourceName := "aws_elasticache_cluster.test" @@ -536,24 +540,45 @@ func TestAccAWSElasticacheCluster_EngineVersion_Redis(t *testing.T) { { Config: testAccAWSElasticacheClusterConfig_EngineVersion_Redis(rName, "3.2.6"), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSElasticacheClusterExists(resourceName, &pre), + testAccCheckAWSElasticacheClusterExists(resourceName, &v1), resource.TestCheckResourceAttr(resourceName, "engine_version", "3.2.6"), + resource.TestCheckResourceAttr(resourceName, "engine_version_actual", "3.2.6"), ), }, { Config: testAccAWSElasticacheClusterConfig_EngineVersion_Redis(rName, "3.2.4"), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSElasticacheClusterExists(resourceName, &mid), - testAccCheckAWSElasticacheClusterRecreated(&pre, &mid), + testAccCheckAWSElasticacheClusterExists(resourceName, &v2), + testAccCheckAWSElasticacheClusterRecreated(&v1, &v2), resource.TestCheckResourceAttr(resourceName, "engine_version", "3.2.4"), + resource.TestCheckResourceAttr(resourceName, "engine_version_actual", "3.2.4"), ), }, { Config: testAccAWSElasticacheClusterConfig_EngineVersion_Redis(rName, "3.2.10"), Check: resource.ComposeTestCheckFunc( - testAccCheckAWSElasticacheClusterExists(resourceName, &post), - testAccCheckAWSElasticacheClusterNotRecreated(&mid, &post), + testAccCheckAWSElasticacheClusterExists(resourceName, &v3), + testAccCheckAWSElasticacheClusterNotRecreated(&v2, &v3), resource.TestCheckResourceAttr(resourceName, "engine_version", "3.2.10"), + resource.TestCheckResourceAttr(resourceName, "engine_version_actual", "3.2.10"), + ), + }, + { + Config: testAccAWSElasticacheClusterConfig_EngineVersion_Redis(rName, "6.x"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheClusterExists(resourceName, &v4), + testAccCheckAWSElasticacheClusterNotRecreated(&v3, &v4), + resource.TestCheckResourceAttr(resourceName, "engine_version", "6.x"), + resource.TestMatchResourceAttr(resourceName, "engine_version_actual", regexp.MustCompile(`^6\.[[:digit:]]+\.[[:digit:]]+$`)), + ), + }, + { + Config: testAccAWSElasticacheClusterConfig_EngineVersion_Redis(rName, "5.0.6"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheClusterExists(resourceName, &v5), + testAccCheckAWSElasticacheClusterRecreated(&v4, &v5), + resource.TestCheckResourceAttr(resourceName, "engine_version", "5.0.6"), + resource.TestCheckResourceAttr(resourceName, "engine_version_actual", "5.0.6"), ), }, }, diff --git a/aws/resource_aws_elasticache_global_replication_group.go b/aws/resource_aws_elasticache_global_replication_group.go index d7816b3d2a55..0d88b6c70630 100644 --- a/aws/resource_aws_elasticache_global_replication_group.go +++ b/aws/resource_aws_elasticache_global_replication_group.go @@ -70,13 +70,18 @@ func resourceAwsElasticacheGlobalReplicationGroup() *schema.Resource { }, // Leaving space for `engine_version` for creation and updating. // `engine_version` cannot be used for returning the version because, starting with Redis 6, - // version configuration is major-version-only: `engine_version = "6.x"`, while `actual_engine_version` + // version configuration is major-version-only: `engine_version = "6.x"`, while `engine_version_actual` // will be e.g. `6.0.5` // See also https://github.com/hashicorp/terraform-provider-aws/issues/15625 - "actual_engine_version": { + "engine_version_actual": { Type: schema.TypeString, Computed: true, }, + "actual_engine_version": { + Type: schema.TypeString, + Computed: true, + Deprecated: "Use engine_version_actual instead", + }, "global_replication_group_id": { Type: schema.TypeString, Computed: true, @@ -194,6 +199,7 @@ func resourceAwsElasticacheGlobalReplicationGroupRead(d *schema.ResourceData, me d.Set("cache_node_type", globalReplicationGroup.CacheNodeType) d.Set("cluster_enabled", globalReplicationGroup.ClusterEnabled) d.Set("engine", globalReplicationGroup.Engine) + d.Set("engine_version_actual", globalReplicationGroup.EngineVersion) d.Set("actual_engine_version", globalReplicationGroup.EngineVersion) d.Set("global_replication_group_description", globalReplicationGroup.GlobalReplicationGroupDescription) d.Set("global_replication_group_id", globalReplicationGroup.GlobalReplicationGroupId) diff --git a/aws/resource_aws_elasticache_global_replication_group_test.go b/aws/resource_aws_elasticache_global_replication_group_test.go index 2265971cb0c7..dd5d98f51514 100644 --- a/aws/resource_aws_elasticache_global_replication_group_test.go +++ b/aws/resource_aws_elasticache_global_replication_group_test.go @@ -96,6 +96,7 @@ func TestAccAWSElasticacheGlobalReplicationGroup_basic(t *testing.T) { resource.TestCheckResourceAttrPair(resourceName, "cache_node_type", primaryReplicationGroupResourceName, "node_type"), resource.TestCheckResourceAttrPair(resourceName, "cluster_enabled", primaryReplicationGroupResourceName, "cluster_enabled"), resource.TestCheckResourceAttrPair(resourceName, "engine", primaryReplicationGroupResourceName, "engine"), + resource.TestCheckResourceAttrPair(resourceName, "engine_version_actual", primaryReplicationGroupResourceName, "engine_version"), resource.TestCheckResourceAttrPair(resourceName, "actual_engine_version", primaryReplicationGroupResourceName, "engine_version"), resource.TestCheckResourceAttr(resourceName, "global_replication_group_id_suffix", rName), resource.TestMatchResourceAttr(resourceName, "global_replication_group_id", regexp.MustCompile(elasticacheGlobalReplicationGroupRegionPrefixFormat+rName)), diff --git a/aws/resource_aws_elasticache_replication_group.go b/aws/resource_aws_elasticache_replication_group.go index d33ca9e82110..d4a8f0b4f9ad 100644 --- a/aws/resource_aws_elasticache_replication_group.go +++ b/aws/resource_aws_elasticache_replication_group.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + tfelasticache "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/elasticache" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/elasticache/finder" "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/elasticache/waiter" "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" @@ -106,12 +107,17 @@ func resourceAwsElasticacheReplicationGroup() *schema.Resource { Type: schema.TypeString, Optional: true, ForceNew: true, - Default: "redis", - ValidateFunc: validation.StringInSlice([]string{"redis"}, true), + Default: tfelasticache.EngineRedis, + ValidateFunc: validation.StringInSlice([]string{tfelasticache.EngineRedis}, true), }, "engine_version": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: ValidateElastiCacheRedisVersionString, + }, + "engine_version_actual": { Type: schema.TypeString, - Optional: true, Computed: true, }, "global_replication_group_id": { @@ -293,15 +299,8 @@ func resourceAwsElasticacheReplicationGroup() *schema.Resource { }, CustomizeDiff: customdiff.Sequence( - func(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { - if v := diff.Get("multi_az_enabled").(bool); !v { - return nil - } - if v := diff.Get("automatic_failover_enabled").(bool); !v { - return errors.New(`automatic_failover_enabled must be true if multi_az_enabled is true`) - } - return nil - }, + CustomizeDiffValidateReplicationGroupAutomaticFailover, + CustomizeDiffElastiCacheEngineVersion, customdiff.ComputedIf("member_clusters", func(ctx context.Context, diff *schema.ResourceDiff, meta interface{}) bool { return diff.HasChange("number_cache_clusters") || diff.HasChange("cluster_mode.0.num_node_groups") || @@ -520,18 +519,11 @@ func resourceAwsElasticacheReplicationGroupRead(d *schema.ResourceData, meta int } c := res.CacheClusters[0] - d.Set("node_type", c.CacheNodeType) - d.Set("engine", c.Engine) - d.Set("engine_version", c.EngineVersion) - d.Set("subnet_group_name", c.CacheSubnetGroupName) - d.Set("security_group_names", flattenElastiCacheSecurityGroupNames(c.CacheSecurityGroups)) - d.Set("security_group_ids", flattenElastiCacheSecurityGroupIds(c.SecurityGroups)) - if c.CacheParameterGroup != nil { - d.Set("parameter_group_name", c.CacheParameterGroup.CacheParameterGroupName) + if err := elasticacheSetResourceDataFromCacheCluster(d, c); err != nil { + return err } - d.Set("maintenance_window", c.PreferredMaintenanceWindow) d.Set("snapshot_window", rgp.SnapshotWindow) d.Set("snapshot_retention_limit", rgp.SnapshotRetentionLimit) diff --git a/aws/resource_aws_elasticache_replication_group_test.go b/aws/resource_aws_elasticache_replication_group_test.go index 3a42a5453eab..398a5acb3c5f 100644 --- a/aws/resource_aws_elasticache_replication_group_test.go +++ b/aws/resource_aws_elasticache_replication_group_test.go @@ -109,6 +109,8 @@ func TestAccAWSElasticacheReplicationGroup_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "cluster_mode.0.replicas_per_node_group", "1"), resource.TestCheckResourceAttr(resourceName, "cluster_mode.0.num_node_groups", "1"), resource.TestCheckResourceAttr(resourceName, "cluster_enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "engine_version", "6.x"), + resource.TestMatchResourceAttr(resourceName, "engine_version_actual", regexp.MustCompile(`^6\.[[:digit:]]+\.[[:digit:]]+$`)), ), }, { @@ -150,6 +152,71 @@ func TestAccAWSElasticacheReplicationGroup_Uppercase(t *testing.T) { }) } +func TestAccAWSElasticacheReplicationGroup_EngineVersion_Update(t *testing.T) { + var v1, v2, v3, v4, v5 elasticache.ReplicationGroup + var c1, c2, c3, c4, c5 map[string]*elasticache.CacheCluster + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_elasticache_replication_group.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, elasticache.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSElasticacheReplicationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSElasticacheReplicationGroupConfig_EngineVersion(rName, "3.2.6"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheReplicationGroupExists(resourceName, &v1), + testAccCheckAWSElastiCacheReplicationGroupMemberClusters(resourceName, &c1), + resource.TestCheckResourceAttr(resourceName, "engine_version", "3.2.6"), + resource.TestCheckResourceAttr(resourceName, "engine_version_actual", "3.2.6"), + ), + }, + { + Config: testAccAWSElasticacheReplicationGroupConfig_EngineVersion(rName, "3.2.4"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheReplicationGroupExists(resourceName, &v2), + testAccCheckAWSElastiCacheReplicationGroupMemberClusters(resourceName, &c2), + testAccCheckAWSElastiCacheReplicationGroupRecreated(&c1, &c2), + resource.TestCheckResourceAttr(resourceName, "engine_version", "3.2.4"), + resource.TestCheckResourceAttr(resourceName, "engine_version_actual", "3.2.4"), + ), + }, + { + Config: testAccAWSElasticacheReplicationGroupConfig_EngineVersion(rName, "3.2.10"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheReplicationGroupExists(resourceName, &v3), + testAccCheckAWSElastiCacheReplicationGroupMemberClusters(resourceName, &c3), + testAccCheckAWSElastiCacheReplicationGroupNotRecreated(&c2, &c3), + resource.TestCheckResourceAttr(resourceName, "engine_version", "3.2.10"), + resource.TestCheckResourceAttr(resourceName, "engine_version_actual", "3.2.10"), + ), + }, + { + Config: testAccAWSElasticacheReplicationGroupConfig_EngineVersion(rName, "6.x"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheReplicationGroupExists(resourceName, &v4), + testAccCheckAWSElastiCacheReplicationGroupMemberClusters(resourceName, &c4), + testAccCheckAWSElastiCacheReplicationGroupNotRecreated(&c3, &c4), + resource.TestCheckResourceAttr(resourceName, "engine_version", "6.x"), + resource.TestMatchResourceAttr(resourceName, "engine_version_actual", regexp.MustCompile(`^6\.[[:digit:]]+\.[[:digit:]]+$`)), + ), + }, + { + Config: testAccAWSElasticacheReplicationGroupConfig_EngineVersion(rName, "5.0.6"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSElasticacheReplicationGroupExists(resourceName, &v5), + testAccCheckAWSElastiCacheReplicationGroupMemberClusters(resourceName, &c5), + testAccCheckAWSElastiCacheReplicationGroupRecreated(&c4, &c5), + resource.TestCheckResourceAttr(resourceName, "engine_version", "5.0.6"), + resource.TestCheckResourceAttr(resourceName, "engine_version_actual", "5.0.6"), + ), + }, + }, + }) +} + func TestAccAWSElasticacheReplicationGroup_disappears(t *testing.T) { var rg elasticache.ReplicationGroup rName := acctest.RandomWithPrefix("tf-acc-test") @@ -1624,6 +1691,67 @@ func testAccCheckAWSElasticacheReplicationDestroy(s *terraform.State) error { return nil } +func testAccCheckAWSElastiCacheReplicationGroupMemberClusters(n string, v *map[string]*elasticache.CacheCluster) resource.TestCheckFunc { + return func(s *terraform.State) error { + var rg elasticache.ReplicationGroup + + err := testAccCheckAWSElasticacheReplicationGroupExists(n, &rg)(s) + if err != nil { + return err + } + + conn := testAccProvider.Meta().(*AWSClient).elasticacheconn + + clusters := make(map[string]*elasticache.CacheCluster, len(rg.MemberClusters)) + for _, clusterID := range rg.MemberClusters { + c, err := finder.CacheClusterWithNodeInfoByID(conn, aws.StringValue(clusterID)) + if err != nil { + return fmt.Errorf("could not read ElastiCache replication group (%s) member cluster (%s): %w", n, aws.StringValue(clusterID), err) + } + + clusters[aws.StringValue(c.CacheClusterId)] = c + } + + *v = clusters + + return nil + } +} + +func testAccCheckAWSElastiCacheReplicationGroupRecreated(i, j *map[string]*elasticache.CacheCluster) resource.TestCheckFunc { + return func(s *terraform.State) error { + for key, iv := range *i { + jv, ok := (*j)[key] + if !ok { + continue + } + + if aws.TimeValue(iv.CacheClusterCreateTime).Equal(aws.TimeValue(jv.CacheClusterCreateTime)) { + return fmt.Errorf("ElastiCache replication group not recreated: member cluster (%s) not recreated", key) + } + } + + return nil + } +} + +func testAccCheckAWSElastiCacheReplicationGroupNotRecreated(i, j *map[string]*elasticache.CacheCluster) resource.TestCheckFunc { + return func(s *terraform.State) error { + for key, iv := range *i { + jv, ok := (*j)[key] + if !ok { + continue + } + + if !aws.TimeValue(iv.CacheClusterCreateTime).Equal(aws.TimeValue(jv.CacheClusterCreateTime)) { + return fmt.Errorf("ElastiCache replication group recreated: member cluster (%s) recreated", key) + } + } + + return nil + } +} + func testAccAWSElasticacheReplicationGroupConfig(rName string) string { return fmt.Sprintf(` resource "aws_elasticache_replication_group" "test" { @@ -1687,6 +1815,23 @@ resource "aws_elasticache_replication_group" "test" { `, rName) } +func testAccAWSElasticacheReplicationGroupConfig_EngineVersion(rName, engineVersion string) string { + return fmt.Sprintf(` +resource "aws_elasticache_replication_group" "test" { + replication_group_id = %[1]q + replication_group_description = "test description" + + node_type = "cache.t3.small" + number_cache_clusters = 2 + + engine_version = %[2]q + apply_immediately = true + maintenance_window = "tue:06:30-tue:07:30" + snapshot_window = "01:00-02:00" +} +`, rName, engineVersion) +} + func testAccAWSElasticacheReplicationGroupConfigEnableSnapshotting(rName string) string { return fmt.Sprintf(` resource "aws_elasticache_replication_group" "test" { diff --git a/aws/validators.go b/aws/validators.go index 2aa681a254f0..c74f4019dab4 100644 --- a/aws/validators.go +++ b/aws/validators.go @@ -26,18 +26,24 @@ const ( awsAccountIDRegexpInternalPattern = `(aws|\d{12})` awsPartitionRegexpInternalPattern = `aws(-[a-z]+)*` awsRegionRegexpInternalPattern = `[a-z]{2}(-[a-z]+)+-\d` + + versionStringRegexpInternalPattern = `[[:digit:]]+(\.[[:digit:]]+){2}` ) const ( awsAccountIDRegexpPattern = "^" + awsAccountIDRegexpInternalPattern + "$" awsPartitionRegexpPattern = "^" + awsPartitionRegexpInternalPattern + "$" awsRegionRegexpPattern = "^" + awsRegionRegexpInternalPattern + "$" + + versionStringRegexpPattern = "^" + versionStringRegexpInternalPattern + "$" ) var awsAccountIDRegexp = regexp.MustCompile(awsAccountIDRegexpPattern) var awsPartitionRegexp = regexp.MustCompile(awsPartitionRegexpPattern) var awsRegionRegexp = regexp.MustCompile(awsRegionRegexpPattern) +var versionStringRegexp = regexp.MustCompile(versionStringRegexpPattern) + // validateTypeStringNullableBoolean provides custom error messaging for TypeString booleans // Some arguments require three values: true, false, and "" (unspecified). // This ValidateFunc returns a custom message since the message with @@ -2426,3 +2432,13 @@ var validateTypeStringIsDateOrPositiveInt = validation.Any( validation.IsRFC3339Time, validation.StringMatch(regexp.MustCompile(`^\d+$`), "must be a positive integer value"), ) + +func validateVersionString(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + + if !versionStringRegexp.MatchString(value) { + errors = append(errors, fmt.Errorf("%s: must be a version string matching x.y.z", k)) + } + + return +} diff --git a/website/docs/r/elasticache_cluster.html.markdown b/website/docs/r/elasticache_cluster.html.markdown index 348facc7d2a1..c64ccb6de344 100644 --- a/website/docs/r/elasticache_cluster.html.markdown +++ b/website/docs/r/elasticache_cluster.html.markdown @@ -74,18 +74,22 @@ The following arguments are required: * `cluster_id` – (Required) Group identifier. ElastiCache converts this name to lowercase. Changing this value will re-create the resource. * `engine` – (Required unless `replication_group_id` is provided) Name of the cache engine to be used for this cache cluster. Valid values are `memcached` or `redis`. -* `node_type` – (Required unless `replication_group_id` is provided) Instance class used. See AWS documentation for information on [supported node types for Redis](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/CacheNodes.SupportedTypes.html) and [guidance on selecting node types for Redis](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/nodes-select-size.html). See AWS documentation for information on [supported node types for Memcached](https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/CacheNodes.SupportedTypes.html) and [guidance on selecting node types for Memcached](https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/nodes-select-size.html). For Memcached, changing this value will re-create the resource. -* `num_cache_nodes` – (Required unless `replication_group_id` is provided) Initial number of cache nodes that the cache cluster will have. For Redis, this value must be 1. For Memcached, this value must be between 1 and 20. If this number is reduced on subsequent runs, the highest numbered nodes will be removed. -* `parameter_group_name` – (Required unless `replication_group_id` is provided) Name of the parameter group to associate with this cache cluster +* `node_type` – (Required unless `replication_group_id` is provided) The instance class used. See AWS documentation for information on [supported node types for Redis](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/CacheNodes.SupportedTypes.html) and [guidance on selecting node types for Redis](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/nodes-select-size.html). See AWS documentation for information on [supported node types for Memcached](https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/CacheNodes.SupportedTypes.html) and [guidance on selecting node types for Memcached](https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/nodes-select-size.html). For Memcached, changing this value will re-create the resource. +* `num_cache_nodes` – (Required unless `replication_group_id` is provided) The initial number of cache nodes that the cache cluster will have. For Redis, this value must be 1. For Memcached, this value must be between 1 and 20. If this number is reduced on subsequent runs, the highest numbered nodes will be removed. +* `parameter_group_name` – (Required unless `replication_group_id` is provided) The name of the parameter group to associate with this cache cluster. The following arguments are optional: -* `apply_immediately` - (Optional) Whether any database modifications are applied immediately, or during the next maintenance window. Default is `false`. See [Amazon ElastiCache Documentation for more information.](https://docs.aws.amazon.com/AmazonElastiCache/latest/APIReference/API_ModifyCacheCluster.html) +* `apply_immediately` - (Optional) Whether any database modifications are applied immediately, or during the next maintenance window. Default is `false`. See [Amazon ElastiCache Documentation for more information.](https://docs.aws.amazon.com/AmazonElastiCache/latest/APIReference/API_ModifyCacheCluster.html). * `availability_zone` - (Optional) Availability Zone for the cache cluster. If you want to create cache nodes in multi-az, use `preferred_availability_zones` instead. Default: System chosen Availability Zone. Changing this value will re-create the resource. -* `az_mode` - (Optional, Memcached only) Whether the nodes in this Memcached node group are created in a single Availability Zone or created across multiple Availability Zones in the cluster's region. Valid values for this parameter are `single-az` or `cross-az`, default is `single-az`. If you want to choose `cross-az`, `num_cache_nodes` must be greater than `1` -* `engine_version` – (Optional) Version number of the cache engine to be used. See [Describe Cache Engine Versions](https://docs.aws.amazon.com/cli/latest/reference/elasticache/describe-cache-engine-versions.html) in the AWS Documentation center for supported versions +* `az_mode` - (Optional, Memcached only) Whether the nodes in this Memcached node group are created in a single Availability Zone or created across multiple Availability Zones in the cluster's region. Valid values for this parameter are `single-az` or `cross-az`, default is `single-az`. If you want to choose `cross-az`, `num_cache_nodes` must be greater than `1`. +* `engine_version` – (Optional) Version number of the cache engine to be used. +See [Describe Cache Engine Versions](https://docs.aws.amazon.com/cli/latest/reference/elasticache/describe-cache-engine-versions.html) +in the AWS Documentation for supported versions. When `engine` is `redis` and the version is 6 or higher, only the major version can be set, e.g. `6.x`, otherwise, specify the full version desired, e.g. `5.0.6`. The actual engine version used is returned in the attribute `engine_version_actual`, [defined below](#engine_version_actual). * `final_snapshot_identifier` - (Optional, Redis only) Name of your final cluster snapshot. If omitted, no final snapshot will be made. -* `maintenance_window` – (Optional) Weekly time range for when maintenance on the cache cluster is performed. The format is `ddd:hh24:mi-ddd:hh24:mi` (24H Clock UTC). The minimum maintenance window is a 60 minute period. Example: `sun:05:00-sun:09:00` +* `maintenance_window` – (Optional) Specifies the weekly time range for when maintenance +on the cache cluster is performed. The format is `ddd:hh24:mi-ddd:hh24:mi` (24H Clock UTC). +The minimum maintenance window is a 60 minute period. Example: `sun:05:00-sun:09:00`. * `notification_topic_arn` – (Optional) ARN of an SNS topic to send ElastiCache notifications to. Example: `arn:aws:sns:us-east-1:012345678999:my_sns_topic`. * `port` – (Optional) The port number on which each of the cache nodes will accept connections. For Memcached the default is 11211, and for Redis the default port is 6379. Cannot be provided with `replication_group_id`. Changing this value will re-create the resource. * `preferred_availability_zones` - (Optional, Memcached only) List of the Availability Zones in which cache nodes are created. If you are creating your cluster in an Amazon VPC you can only locate nodes in Availability Zones that are associated with the subnets in the selected subnet group. The number of Availability Zones listed must equal the value of `num_cache_nodes`. If you want all the nodes in the same Availability Zone, use `availability_zone` instead, or repeat the Availability Zone multiple times in the list. Default: System chosen Availability Zones. Detecting drift of existing node availability zone is not currently supported. Updating this argument by itself to migrate existing node availability zones is not currently supported and will show a perpetual difference. @@ -103,7 +107,8 @@ The following arguments are optional: In addition to all arguments above, the following attributes are exported: -* `arn` - ARN of the created ElastiCache Cluster. +* `arn` - The ARN of the created ElastiCache Cluster. +* `engine_version_actual` - The running version of the cache engine. * `cache_nodes` - List of node objects including `id`, `address`, `port` and `availability_zone`. * `cluster_address` - (Memcached only) DNS name of the cache cluster without the port appended. * `configuration_endpoint` - (Memcached only) Configuration endpoint to allow host discovery. diff --git a/website/docs/r/elasticache_global_replication_group.html.markdown b/website/docs/r/elasticache_global_replication_group.html.markdown index 8a8962b94415..4dd8a04fea32 100644 --- a/website/docs/r/elasticache_global_replication_group.html.markdown +++ b/website/docs/r/elasticache_global_replication_group.html.markdown @@ -58,7 +58,8 @@ In addition to all arguments above, the following attributes are exported: * `id` - The ID of the ElastiCache Global Replication Group. * `arn` - The ARN of the ElastiCache Global Replication Group. -* `actual_engine_version` - The full version number of the cache engine running on the members of this global replication group. +* `actual_engine_version` - (**DEPRECATED** use `engine_version_actual` instead) The full version number of the cache engine running on the members of this global replication group. +* `engine_version_actual` - The full version number of the cache engine running on the members of this global replication group. * `at_rest_encryption_enabled` - A flag that indicate whether the encryption at rest is enabled. * `auth_token_enabled` - A flag that indicate whether AuthToken (password) is enabled. * `cache_node_type` - The instance class used. See AWS documentation for information on [supported node types](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/CacheNodes.SupportedTypes.html) and [guidance on selecting node types](https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/nodes-select-size.html). diff --git a/website/docs/r/elasticache_replication_group.html.markdown b/website/docs/r/elasticache_replication_group.html.markdown index c1e604fa0dc2..b43066a85bad 100644 --- a/website/docs/r/elasticache_replication_group.html.markdown +++ b/website/docs/r/elasticache_replication_group.html.markdown @@ -153,8 +153,8 @@ The following arguments are optional: * `automatic_failover_enabled` - (Optional) Specifies whether a read-only replica will be automatically promoted to read/write primary if the existing primary fails. If enabled, `number_cache_clusters` must be greater than 1. Must be enabled for Redis (cluster mode enabled) replication groups. Defaults to `false`. * `availability_zones` - (Optional) A list of EC2 availability zones in which the replication group's cache clusters will be created. The order of the availability zones in the list is not important. * `cluster_mode` - (Optional) Create a native Redis cluster. `automatic_failover_enabled` must be set to true. Cluster Mode documented below. Only 1 `cluster_mode` block is allowed. One of `number_cache_clusters` or `cluster_mode` is required. Note that configuring this block does not enable cluster mode, i.e. data sharding, this requires using a parameter group that has the parameter `cluster-enabled` set to true. -* `engine_version` - (Optional) The version number of the cache engine to be used for the cache clusters in this replication group. * `engine` - (Optional) The name of the cache engine to be used for the clusters in this replication group. The only valid value is `redis`. +* `engine_version` - (Optional) The version number of the cache engine to be used for the cache clusters in this replication group. If the version is 6 or higher, only the major version can be set, e.g. `6.x`, otherwise, specify the full version desired, e.g. `5.0.6`. The actual engine version used is returned in the attribute `engine_version_actual`, [defined below](#engine_version_actual). * `final_snapshot_identifier` - (Optional) The name of your final node group (shard) snapshot. ElastiCache creates the snapshot from the primary node in the cluster. If omitted, no final snapshot will be made. * `global_replication_group_id` - (Optional) The ID of the global replication group to which this replication group should belong. If this parameter is specified, the replication group is added to the specified global replication group as a secondary replication group; otherwise, the replication group is not part of any global replication group. * `kms_key_id` - (Optional) The ARN of the key that you wish to use if encrypting at rest. If not supplied, uses service managed encryption. Can be specified only if `at_rest_encryption_enabled = true`. @@ -185,13 +185,14 @@ The following arguments are optional: In addition to all arguments above, the following attributes are exported: -* `arn` - ARN of the created ElastiCache Replication Group. -* `cluster_enabled` - Whether cluster mode is enabled. -* `configuration_endpoint_address` - Address of the replication group configuration endpoint when cluster mode is enabled. -* `id` - ID of the ElastiCache Replication Group. -* `member_clusters` - Identifiers of all the nodes that are part of this replication group. -* `primary_endpoint_address` - (Redis only) Address of the endpoint for the primary node in the replication group, if the cluster mode is disabled. -* `reader_endpoint_address` - (Redis only) Address of the endpoint for the reader node in the replication group, if the cluster mode is disabled. +* `arn` - The Amazon Resource Name (ARN) of the created ElastiCache Replication Group. +* `engine_version_actual` - The running version of the cache engine. +* `cluster_enabled` - Indicates if cluster mode is enabled. +* `configuration_endpoint_address` - The address of the replication group configuration endpoint when cluster mode is enabled. +* `id` - The ID of the ElastiCache Replication Group. +* `member_clusters` - The identifiers of all the nodes that are part of this replication group. +* `primary_endpoint_address` - (Redis only) The address of the endpoint for the primary node in the replication group, if the cluster mode is disabled. +* `reader_endpoint_address` - (Redis only) The address of the endpoint for the reader node in the replication group, if the cluster mode is disabled. * `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](https://www.terraform.io/docs/providers/aws/index.html#default_tags-configuration-block). ## Timeouts