Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

providers/aws: Add ElastiCache replication group support (fixes #1799) #2945

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ website/node_modules
*.bak
*~
.*.swp
config/lang/y.go
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be in the .gitignore. There's some oddness with go 1.4 and 1.5 but for now this file should just not be included in any changes, more ignored. Sorry for the hassle

1 change: 1 addition & 0 deletions builtin/providers/aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ func Provider() terraform.ResourceProvider {
"aws_eip": resourceAwsEip(),
"aws_elasticache_cluster": resourceAwsElasticacheCluster(),
"aws_elasticache_parameter_group": resourceAwsElasticacheParameterGroup(),
"aws_elasticache_replication_group":resourceAwsElasticacheReplicationGroup(),
"aws_elasticache_security_group": resourceAwsElasticacheSecurityGroup(),
"aws_elasticache_subnet_group": resourceAwsElasticacheSubnetGroup(),
"aws_elb": resourceAwsElb(),
Expand Down
7 changes: 7 additions & 0 deletions builtin/providers/aws/resource_aws_elasticache_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ func resourceAwsElasticacheCluster() *schema.Resource {
Computed: true,
ForceNew: true,
},
"replication_group_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like a Computed attribute to me, what do you think? It seems we would have a aws_elasticache_cluster resource, and then a elasticache_replication_group which has a primary_cluster_id that points to the aws_elasticache_cluster. The aws_elasticache_cluster can exist by itself, but it may have a replication group associated with it, which comes later. Does that make sense?

"security_group_names": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Expand Down Expand Up @@ -155,6 +160,7 @@ func resourceAwsElasticacheClusterCreate(d *schema.ResourceData, meta interface{
subnetGroupName := d.Get("subnet_group_name").(string)
securityNameSet := d.Get("security_group_names").(*schema.Set)
securityIdSet := d.Get("security_group_ids").(*schema.Set)
replicationGroupID := d.Get("replication_group_id").(string)

securityNames := expandStringList(securityNameSet.List())
securityIds := expandStringList(securityIdSet.List())
Expand All @@ -171,6 +177,7 @@ func resourceAwsElasticacheClusterCreate(d *schema.ResourceData, meta interface{
CacheSecurityGroupNames: securityNames,
SecurityGroupIds: securityIds,
Tags: tags,
ReplicationGroupId: aws.String(replicationGroupID),
}

// parameter groups are optional and can be defaulted by AWS
Expand Down
322 changes: 322 additions & 0 deletions builtin/providers/aws/resource_aws_elasticache_replication_group.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,322 @@
package aws

import (
"fmt"
"log"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/elasticache"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
)

func resourceAwsElasticacheReplicationGroup() *schema.Resource {
return &schema.Resource{
Create: resourceAwsElasticacheReplicationGroupCreate,
Read: resourceAwsElasticacheReplicationGroupRead,
Update: resourceAwsElasticacheReplicationGroupUpdate,
Delete: resourceAwsElasticacheReplicationGroupDelete,

Schema: map[string]*schema.Schema{
"replication_group_id": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"description": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"cache_node_type": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"automatic_failover": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
"num_cache_clusters": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
ForceNew: true,
},
"primary_cluster_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"parameter_group_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"subnet_group_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"security_group_names": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
"security_group_ids": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
"engine": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Default: "redis",
},
"engine_version": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"primary_endpoint": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"preferred_cache_cluster_azs": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
ForceNew: true,
Elem: &schema.Schema{Type: schema.TypeString},
Set: schema.HashString,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we should add some of the other parameters – like snapshot info, preferred maintenance window, and anything else in CreateReplicationGroupInput that currently isn't here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thegedge That makes sense to me.

},
}
}

func resourceAwsElasticacheReplicationGroupCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).elasticacheconn

replicationGroupID := d.Get("replication_group_id").(string)
description := d.Get("description").(string)
cacheNodeType := d.Get("cache_node_type").(string)
automaticFailover := d.Get("automatic_failover").(bool)
numCacheClusters := d.Get("num_cache_clusters").(int)
primaryClusterID := d.Get("primary_cluster_id").(string)
engine := d.Get("engine").(string)
engineVersion := d.Get("engine_version").(string)
securityNameSet := d.Get("security_group_names").(*schema.Set)
securityIDSet := d.Get("security_group_ids").(*schema.Set)
subnetGroupName := d.Get("subnet_group_name").(string)
prefferedCacheClusterAZs := d.Get("preferred_cache_cluster_azs").(*schema.Set)

securityNames := expandStringList(securityNameSet.List())
securityIds := expandStringList(securityIDSet.List())
prefferedAZs := expandStringList(prefferedCacheClusterAZs.List())

req := &elasticache.CreateReplicationGroupInput{
ReplicationGroupId: aws.String(replicationGroupID),
ReplicationGroupDescription: aws.String(description),
CacheNodeType: aws.String(cacheNodeType),
AutomaticFailoverEnabled: aws.Bool(automaticFailover),
NumCacheClusters: aws.Int64(int64(numCacheClusters)),
PrimaryClusterId: aws.String(primaryClusterID),
Engine: aws.String(engine),
CacheSubnetGroupName: aws.String(subnetGroupName),
EngineVersion: aws.String(engineVersion),
CacheSecurityGroupNames: securityNames,
SecurityGroupIds: securityIds,
PreferredCacheClusterAZs: prefferedAZs,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if any of these above attributes are omitted because they are optional, this CreateReplicationGroupInput will contain empty strings, which is typically not accepted in the API. Does this work when they are omitted?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discovered on my own that the answer is "no", but we can fix this easily 😄 Looks like engine_version, preferred_cache_cluster_azs and security_group_names specifically need to be conditionally added


// parameter groups are optional and can be defaulted by AWS
if v, ok := d.GetOk("parameter_group_name"); ok {
req.CacheParameterGroupName = aws.String(v.(string))
}

if v, ok := d.GetOk("maintenance_window"); ok {
req.PreferredMaintenanceWindow = aws.String(v.(string))
}

_, err := conn.CreateReplicationGroup(req)
if err != nil {
return fmt.Errorf("Error creating Elasticache replication group: %s", err)
}

d.SetId(replicationGroupID)

pending := []string{"creating"}
stateConf := &resource.StateChangeConf{
Pending: pending,
Target: "available",
Refresh: replicationGroupStateRefreshFunc(conn, d.Id(), "available", pending),
Timeout: 60 * time.Minute,
Delay: 20 * time.Second,
MinTimeout: 5 * time.Second,
}

log.Printf("[DEBUG] Waiting for state to become available: %v", d.Id())
_, sterr := stateConf.WaitForState()
if sterr != nil {
return fmt.Errorf("Error waiting for elasticache (%s) to be created: %s", d.Id(), sterr)
}

return nil
}

func resourceAwsElasticacheReplicationGroupRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).elasticacheconn

req := &elasticache.DescribeReplicationGroupsInput{
ReplicationGroupId: aws.String(d.Id()),
}

res, err := conn.DescribeReplicationGroups(req)
if err != nil {
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "ReplicationGroupNotFoundFault" {
// Update state to indicate the replication group no longer exists.
d.SetId("")
return nil
}

return err
}

if len(res.ReplicationGroups) == 1 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block will cause a panic if ReplicationGroups[0] is in the deleting state (none of those attributes are set, so you get nil reference issues). Also, why do we set primary_endpoint outside of this block?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do I check the state?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ReplicationGroup status is in the output struct of the DescribeReplicationGroups method

If I recall, the issue appeared when you reference res.ReplicationGroups[0].NodeGroups[0].PrimaryEndpoint.Address... but it could also be that some of the other elements simply weren't present. I was only able to trigger it by manually deleting the group in the console, then running terraform refresh

c := res.ReplicationGroups[0]
if *c.Status != "available" {
return nil
}
d.Set("replication_group_id", c.ReplicationGroupId)
d.Set("description", c.Description)
d.Set("automatic_failover", c.AutomaticFailover)
d.Set("num_cache_clusters", len(c.MemberClusters))
if len(c.NodeGroups) >= 1 && c.NodeGroups[0].PrimaryEndpoint != nil {
d.Set("primary_endpoint", c.NodeGroups[0].PrimaryEndpoint.Address)
}
}

return nil
}

func resourceAwsElasticacheReplicationGroupUpdate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).elasticacheconn

req := &elasticache.ModifyReplicationGroupInput{
ApplyImmediately: aws.Bool(true),
ReplicationGroupId: aws.String(d.Id()),
}

if d.HasChange("automatic_failover") {
automaticFailover := d.Get("automatic_failover").(bool)
req.AutomaticFailoverEnabled = aws.Bool(automaticFailover)
}

if d.HasChange("description") {
description := d.Get("description").(string)
req.ReplicationGroupDescription = aws.String(description)
}

if d.HasChange("engine_version") {
engineVersion := d.Get("engine_version").(string)
req.EngineVersion = aws.String(engineVersion)
}

if d.HasChange("security_group_ids") {
securityIDSet := d.Get("security_group_ids").(*schema.Set)
securityIds := expandStringList(securityIDSet.List())
req.SecurityGroupIds = securityIds
}

if d.HasChange("security_group_names") {
securityNameSet := d.Get("security_group_names").(*schema.Set)
securityNames := expandStringList(securityNameSet.List())
req.CacheSecurityGroupNames = securityNames
}

_, err := conn.ModifyReplicationGroup(req)
if err != nil {
return fmt.Errorf("Error updating Elasticache replication group: %s", err)
}

return resourceAwsElasticacheReplicationGroupRead(d, meta)
}

func resourceAwsElasticacheReplicationGroupDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).elasticacheconn

req := &elasticache.DeleteReplicationGroupInput{
ReplicationGroupId: aws.String(d.Id()),
}

_, err := conn.DeleteReplicationGroup(req)
if err != nil {
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "ReplicationGroupNotFoundFault" {
// Update state to indicate the replication group no longer exists.
d.SetId("")
return nil
}

return fmt.Errorf("Error deleting Elasticache replication group: %s", err)
}

log.Printf("[DEBUG] Waiting for deletion: %v", d.Id())
stateConf := &resource.StateChangeConf{
Pending: []string{"creating", "available", "deleting"},
Target: "",
Refresh: replicationGroupStateRefreshFunc(conn, d.Id(), "", []string{}),
Timeout: 15 * time.Minute,
Delay: 20 * time.Second,
MinTimeout: 5 * time.Second,
}

_, sterr := stateConf.WaitForState()
if sterr != nil {
return fmt.Errorf("Error waiting for replication group (%s) to delete: %s", d.Id(), sterr)
}

return nil
}

func replicationGroupStateRefreshFunc(conn *elasticache.ElastiCache, replicationGroupID, givenState string, pending []string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
resp, err := conn.DescribeReplicationGroups(&elasticache.DescribeReplicationGroupsInput{
ReplicationGroupId: aws.String(replicationGroupID),
})
if err != nil {
ec2err, ok := err.(awserr.Error)

if ok {
log.Printf("[DEBUG] message: %v, code: %v", ec2err.Message(), ec2err.Code())
if ec2err.Code() == "ReplicationGroupNotFoundFault" {
log.Printf("[DEBUG] Detect deletion")
return nil, "", nil
}
}

log.Printf("[ERROR] replicationGroupStateRefreshFunc: %s", err)
return nil, "", err
}

c := resp.ReplicationGroups[0]
log.Printf("[DEBUG] status: %v", *c.Status)

// return the current state if it's in the pending array
for _, p := range pending {
s := *c.Status
if p == s {
log.Printf("[DEBUG] Return with status: %v", *c.Status)
return c, p, nil
}
}

// return given state if it's not in pending
if givenState != "" {
return c, givenState, nil
}
log.Printf("[DEBUG] current status: %v", *c.Status)
return c, *c.Status, nil
}
}
Loading