diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index e8f95784e..6b301f942 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -213,6 +213,7 @@ func Provider() terraform.ResourceProvider { "aws_opsworks_mysql_layer": resourceAwsOpsworksMysqlLayer(), "aws_opsworks_ganglia_layer": resourceAwsOpsworksGangliaLayer(), "aws_opsworks_custom_layer": resourceAwsOpsworksCustomLayer(), + "aws_opsworks_instance": resourceAwsOpsworksInstance(), "aws_placement_group": resourceAwsPlacementGroup(), "aws_proxy_protocol_policy": resourceAwsProxyProtocolPolicy(), "aws_rds_cluster": resourceAwsRDSCluster(), diff --git a/builtin/providers/aws/resource_aws_opsworks_instance.go b/builtin/providers/aws/resource_aws_opsworks_instance.go new file mode 100644 index 000000000..bf1dbfdf3 --- /dev/null +++ b/builtin/providers/aws/resource_aws_opsworks_instance.go @@ -0,0 +1,1013 @@ +package aws + +import ( + "bytes" + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/opsworks" +) + +func resourceAwsOpsworksInstance() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsOpsworksInstanceCreate, + Read: resourceAwsOpsworksInstanceRead, + Update: resourceAwsOpsworksInstanceUpdate, + Delete: resourceAwsOpsworksInstanceDelete, + + Schema: map[string]*schema.Schema{ + "id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + + "agent_version": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "INHERIT", + }, + + "ami_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "architecture": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Default: "x86_64", + ValidateFunc: validateArchitecture, + }, + + "auto_scaling_type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateAutoScalingType, + }, + + "availability_zone": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "created_at": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "delete_ebs": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "delete_eip": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "ebs_optimized": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + ForceNew: true, + }, + + "ec2_instance_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "ecs_cluster_arn": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "elastic_ip": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "hostname": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "infrastructure_class": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "install_updates_on_boot": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + + "instance_profile_arn": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "instance_type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "last_service_error_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "layer_ids": &schema.Schema{ + Type: schema.TypeList, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + + "os": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "platform": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "private_dns": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "private_ip": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "public_dns": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "public_ip": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "registered_by": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "reported_agent_version": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "reported_os_family": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "reported_os_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "reported_os_version": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "root_device_type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Computed: true, + ValidateFunc: validateRootDeviceType, + }, + + "root_device_volume_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "security_group_ids": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + + "ssh_host_dsa_key_fingerprint": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "ssh_host_rsa_key_fingerprint": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "ssh_key_name": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "stack_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "state": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateState, + }, + + "status": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + + "subnet_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "virtualization_type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validateVirtualizationType, + }, + + "ebs_block_device": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Computed: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "delete_on_termination": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + ForceNew: true, + }, + + "device_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "iops": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "snapshot_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "volume_size": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "volume_type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + }, + }, + Set: func(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["snapshot_id"].(string))) + return hashcode.String(buf.String()) + }, + }, + "ephemeral_block_device": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Computed: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "device_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "virtual_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + }, + }, + Set: func(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string))) + return hashcode.String(buf.String()) + }, + }, + + "root_block_device": &schema.Schema{ + // TODO: This is a set because we don't support singleton + // sub-resources today. We'll enforce that the set only ever has + // length zero or one below. When TF gains support for + // sub-resources this can be converted. + Type: schema.TypeSet, + Optional: true, + Computed: true, + ForceNew: true, + Elem: &schema.Resource{ + // "You can only modify the volume size, volume type, and Delete on + // Termination flag on the block device mapping entry for the root + // device volume." - bit.ly/ec2bdmap + Schema: map[string]*schema.Schema{ + "delete_on_termination": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: true, + ForceNew: true, + }, + + "iops": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "volume_size": &schema.Schema{ + Type: schema.TypeInt, + Optional: true, + Computed: true, + ForceNew: true, + }, + + "volume_type": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + }, + }, + Set: func(v interface{}) int { + // there can be only one root device; no need to hash anything + return 0 + }, + }, + }, + } +} + +func validateArchitecture(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if value != "x86_64" && value != "i386" { + errors = append(errors, fmt.Errorf( + "%q must be one of \"x86_64\" or \"i386\"", k)) + } + return +} + +func validateAutoScalingType(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if value != "load" && value != "timer" { + errors = append(errors, fmt.Errorf( + "%q must be one of \"load\" or \"timer\"", k)) + } + return +} + +func validateRootDeviceType(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if value != "ebs" && value != "instance-store" { + errors = append(errors, fmt.Errorf( + "%q must be one of \"ebs\" or \"instance-store\"", k)) + } + return +} + +func validateState(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if value != "running" && value != "stopped" { + errors = append(errors, fmt.Errorf( + "%q must be one of \"running\" or \"stopped\"", k)) + } + return +} + +func validateVirtualizationType(v interface{}, k string) (ws []string, errors []error) { + value := v.(string) + if value != "paravirtual" && value != "hvm" { + errors = append(errors, fmt.Errorf( + "%q must be one of \"paravirtual\" or \"hvm\"", k)) + } + return +} + +func resourceAwsOpsworksInstanceValidate(d *schema.ResourceData) error { + if d.HasChange("ami_id") { + if v, ok := d.GetOk("os"); ok { + if v.(string) != "Custom" { + return fmt.Errorf("OS must be \"Custom\" when using using a custom ami_id") + } + } + + if _, ok := d.GetOk("root_block_device"); ok { + return fmt.Errorf("Cannot specify root_block_device when using a custom ami_id.") + } + + if _, ok := d.GetOk("ebs_block_device"); ok { + return fmt.Errorf("Cannot specify ebs_block_device when using a custom ami_id.") + } + + if _, ok := d.GetOk("ephemeral_block_device"); ok { + return fmt.Errorf("Cannot specify ephemeral_block_device when using a custom ami_id.") + } + } + return nil +} + +func resourceAwsOpsworksInstanceRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*AWSClient).opsworksconn + + req := &opsworks.DescribeInstancesInput{ + InstanceIds: []*string{ + aws.String(d.Id()), + }, + } + + log.Printf("[DEBUG] Reading OpsWorks instance: %s", d.Id()) + + resp, err := client.DescribeInstances(req) + if err != nil { + if awserr, ok := err.(awserr.Error); ok { + if awserr.Code() == "ResourceNotFoundException" { + d.SetId("") + return nil + } + } + return err + } + + // If nothing was found, then return no state + if len(resp.Instances) == 0 { + d.SetId("") + return nil + } + instance := resp.Instances[0] + + if instance.InstanceId == nil { + d.SetId("") + return nil + } + instanceId := *instance.InstanceId + + d.SetId(instanceId) + d.Set("agent_version", instance.AgentVersion) + d.Set("ami_id", instance.AmiId) + d.Set("architecture", instance.Architecture) + d.Set("auto_scaling_type", instance.AutoScalingType) + d.Set("availability_zone", instance.AvailabilityZone) + d.Set("created_at", instance.CreatedAt) + d.Set("ebs_optimized", instance.EbsOptimized) + d.Set("ec2_instance_id", instance.Ec2InstanceId) + d.Set("ecs_cluster_arn", instance.EcsClusterArn) + d.Set("elastic_ip", instance.ElasticIp) + d.Set("hostname", instance.Hostname) + d.Set("infrastructure_class", instance.InfrastructureClass) + d.Set("install_updates_on_boot", instance.InstallUpdatesOnBoot) + d.Set("id", instanceId) + d.Set("instance_profile_arn", instance.InstanceProfileArn) + d.Set("instance_type", instance.InstanceType) + d.Set("last_service_error_id", instance.LastServiceErrorId) + d.Set("layer_ids", instance.LayerIds) + d.Set("os", instance.Os) + d.Set("platform", instance.Platform) + d.Set("private_dns", instance.PrivateDns) + d.Set("private_ip", instance.PrivateIp) + d.Set("public_dns", instance.PublicDns) + d.Set("public_ip", instance.PublicIp) + d.Set("registered_by", instance.RegisteredBy) + d.Set("reported_agent_version", instance.ReportedAgentVersion) + d.Set("reported_os_family", instance.ReportedOs.Family) + d.Set("reported_os_name", instance.ReportedOs.Name) + d.Set("reported_os_version", instance.ReportedOs.Version) + d.Set("root_device_type", instance.RootDeviceType) + d.Set("root_device_volume_id", instance.RootDeviceVolumeId) + d.Set("ssh_host_dsa_key_fingerprint", instance.SshHostDsaKeyFingerprint) + d.Set("ssh_host_rsa_key_fingerprint", instance.SshHostRsaKeyFingerprint) + d.Set("ssh_key_name", instance.SshKeyName) + d.Set("stack_id", instance.StackId) + d.Set("status", instance.Status) + d.Set("subnet_id", instance.SubnetId) + d.Set("virtualization_type", instance.VirtualizationType) + + // Read BlockDeviceMapping + ibds, err := readOpsworksBlockDevices(d, instance, meta) + if err != nil { + return err + } + + if err := d.Set("ebs_block_device", ibds["ebs"]); err != nil { + return err + } + if err := d.Set("ephemeral_block_device", ibds["ephemeral"]); err != nil { + return err + } + if ibds["root"] != nil { + if err := d.Set("root_block_device", []interface{}{ibds["root"]}); err != nil { + return err + } + } else { + d.Set("root_block_device", []interface{}{}) + } + + // Read Security Groups + sgs := make([]string, 0, len(instance.SecurityGroupIds)) + for _, sg := range instance.SecurityGroupIds { + sgs = append(sgs, *sg) + } + if err := d.Set("security_group_ids", sgs); err != nil { + return err + } + + return nil +} + +func resourceAwsOpsworksInstanceCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*AWSClient).opsworksconn + + err := resourceAwsOpsworksInstanceValidate(d) + if err != nil { + return err + } + + req := &opsworks.CreateInstanceInput{ + AgentVersion: aws.String(d.Get("agent_version").(string)), + Architecture: aws.String(d.Get("architecture").(string)), + EbsOptimized: aws.Bool(d.Get("ebs_optimized").(bool)), + InstallUpdatesOnBoot: aws.Bool(d.Get("install_updates_on_boot").(bool)), + InstanceType: aws.String(d.Get("instance_type").(string)), + LayerIds: expandStringList(d.Get("layer_ids").([]interface{})), + StackId: aws.String(d.Get("stack_id").(string)), + } + + if v, ok := d.GetOk("ami_id"); ok { + req.AmiId = aws.String(v.(string)) + req.Os = aws.String("Custom") + } + + if v, ok := d.GetOk("auto_scaling_type"); ok { + req.AutoScalingType = aws.String(v.(string)) + } + + if v, ok := d.GetOk("availability_zone"); ok { + req.AvailabilityZone = aws.String(v.(string)) + } + + if v, ok := d.GetOk("hostname"); ok { + req.Hostname = aws.String(v.(string)) + } + + if v, ok := d.GetOk("os"); ok { + req.Os = aws.String(v.(string)) + } + + if v, ok := d.GetOk("root_device_type"); ok { + req.RootDeviceType = aws.String(v.(string)) + } + + if v, ok := d.GetOk("ssh_key_name"); ok { + req.SshKeyName = aws.String(v.(string)) + } + + if v, ok := d.GetOk("subnet_id"); ok { + req.SubnetId = aws.String(v.(string)) + } + + if v, ok := d.GetOk("virtualization_type"); ok { + req.VirtualizationType = aws.String(v.(string)) + } + + var blockDevices []*opsworks.BlockDeviceMapping + + if v, ok := d.GetOk("ebs_block_device"); ok { + vL := v.(*schema.Set).List() + for _, v := range vL { + bd := v.(map[string]interface{}) + ebs := &opsworks.EbsBlockDevice{ + DeleteOnTermination: aws.Bool(bd["delete_on_termination"].(bool)), + } + + if v, ok := bd["snapshot_id"].(string); ok && v != "" { + ebs.SnapshotId = aws.String(v) + } + + if v, ok := bd["volume_size"].(int); ok && v != 0 { + ebs.VolumeSize = aws.Int64(int64(v)) + } + + if v, ok := bd["volume_type"].(string); ok && v != "" { + ebs.VolumeType = aws.String(v) + } + + if v, ok := bd["iops"].(int); ok && v > 0 { + ebs.Iops = aws.Int64(int64(v)) + } + + blockDevices = append(blockDevices, &opsworks.BlockDeviceMapping{ + DeviceName: aws.String(bd["device_name"].(string)), + Ebs: ebs, + }) + } + } + + if v, ok := d.GetOk("ephemeral_block_device"); ok { + vL := v.(*schema.Set).List() + for _, v := range vL { + bd := v.(map[string]interface{}) + blockDevices = append(blockDevices, &opsworks.BlockDeviceMapping{ + DeviceName: aws.String(bd["device_name"].(string)), + VirtualName: aws.String(bd["virtual_name"].(string)), + }) + } + } + + if v, ok := d.GetOk("root_block_device"); ok { + vL := v.(*schema.Set).List() + if len(vL) > 1 { + return fmt.Errorf("Cannot specify more than one root_block_device.") + } + for _, v := range vL { + bd := v.(map[string]interface{}) + ebs := &opsworks.EbsBlockDevice{ + DeleteOnTermination: aws.Bool(bd["delete_on_termination"].(bool)), + } + + if v, ok := bd["volume_size"].(int); ok && v != 0 { + ebs.VolumeSize = aws.Int64(int64(v)) + } + + if v, ok := bd["volume_type"].(string); ok && v != "" { + ebs.VolumeType = aws.String(v) + } + + if v, ok := bd["iops"].(int); ok && v > 0 { + ebs.Iops = aws.Int64(int64(v)) + } + + blockDevices = append(blockDevices, &opsworks.BlockDeviceMapping{ + DeviceName: aws.String("ROOT_DEVICE"), + Ebs: ebs, + }) + } + } + + if len(blockDevices) > 0 { + req.BlockDeviceMappings = blockDevices + } + + log.Printf("[DEBUG] Creating OpsWorks instance") + + var resp *opsworks.CreateInstanceOutput + + resp, err = client.CreateInstance(req) + if err != nil { + return err + } + + if resp.InstanceId == nil { + return fmt.Errorf("Error launching instance: no instance returned in response") + } + + instanceId := *resp.InstanceId + d.SetId(instanceId) + d.Set("id", instanceId) + + if v, ok := d.GetOk("state"); ok && v.(string) == "running" { + err := startOpsworksInstance(d, meta, false) + if err != nil { + return err + } + } + + return resourceAwsOpsworksInstanceRead(d, meta) +} + +func resourceAwsOpsworksInstanceUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*AWSClient).opsworksconn + + err := resourceAwsOpsworksInstanceValidate(d) + if err != nil { + return err + } + + req := &opsworks.UpdateInstanceInput{ + AgentVersion: aws.String(d.Get("agent_version").(string)), + Architecture: aws.String(d.Get("architecture").(string)), + InstanceId: aws.String(d.Get("id").(string)), + InstallUpdatesOnBoot: aws.Bool(d.Get("install_updates_on_boot").(bool)), + } + + if v, ok := d.GetOk("ami_id"); ok { + req.AmiId = aws.String(v.(string)) + req.Os = aws.String("Custom") + } + + if v, ok := d.GetOk("auto_scaling_type"); ok { + req.AutoScalingType = aws.String(v.(string)) + } + + if v, ok := d.GetOk("hostname"); ok { + req.Hostname = aws.String(v.(string)) + } + + if v, ok := d.GetOk("instance_type"); ok { + req.InstanceType = aws.String(v.(string)) + } + + if v, ok := d.GetOk("layer_ids"); ok { + req.LayerIds = expandStringList(v.([]interface{})) + + } + + if v, ok := d.GetOk("os"); ok { + req.Os = aws.String(v.(string)) + } + + if v, ok := d.GetOk("ssh_key_name"); ok { + req.SshKeyName = aws.String(v.(string)) + } + + log.Printf("[DEBUG] Updating OpsWorks instance: %s", d.Id()) + + _, err = client.UpdateInstance(req) + if err != nil { + return err + } + + var status string + + if v, ok := d.GetOk("status"); ok { + status = v.(string) + } else { + status = "stopped" + } + + if v, ok := d.GetOk("state"); ok { + state := v.(string) + if state == "running" { + if status == "stopped" || status == "stopping" || status == "shutting_down" { + err := startOpsworksInstance(d, meta, false) + if err != nil { + return err + } + } + } else { + if status != "stopped" && status != "stopping" && status != "shutting_down" { + err := stopOpsworksInstance(d, meta, false) + if err != nil { + return err + } + } + } + } + + return resourceAwsOpsworksInstanceRead(d, meta) +} + +func resourceAwsOpsworksInstanceDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*AWSClient).opsworksconn + + if v, ok := d.GetOk("status"); ok && v.(string) != "stopped" { + err := stopOpsworksInstance(d, meta, true) + if err != nil { + return err + } + } + + req := &opsworks.DeleteInstanceInput{ + InstanceId: aws.String(d.Id()), + DeleteElasticIp: aws.Bool(d.Get("delete_eip").(bool)), + DeleteVolumes: aws.Bool(d.Get("delete_ebs").(bool)), + } + + log.Printf("[DEBUG] Deleting OpsWorks instance: %s", d.Id()) + + _, err := client.DeleteInstance(req) + if err != nil { + return err + } + + d.SetId("") + return nil +} + +func startOpsworksInstance(d *schema.ResourceData, meta interface{}, wait bool) error { + client := meta.(*AWSClient).opsworksconn + + instanceId := d.Get("id").(string) + + req := &opsworks.StartInstanceInput{ + InstanceId: aws.String(instanceId), + } + + log.Printf("[DEBUG] Starting OpsWorks instance: %s", instanceId) + + _, err := client.StartInstance(req) + + if err != nil { + return err + } + + if wait { + log.Printf("[DEBUG] Waiting for instance (%s) to become running", instanceId) + + stateConf := &resource.StateChangeConf{ + Pending: []string{"requested", "pending", "booting", "running_setup"}, + Target: []string{"online"}, + Refresh: OpsworksInstanceStateRefreshFunc(client, instanceId), + Timeout: 10 * time.Minute, + Delay: 10 * time.Second, + MinTimeout: 3 * time.Second, + } + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for instance (%s) to become stopped: %s", + instanceId, err) + } + } + + return nil +} + +func stopOpsworksInstance(d *schema.ResourceData, meta interface{}, wait bool) error { + client := meta.(*AWSClient).opsworksconn + + instanceId := d.Get("id").(string) + + req := &opsworks.StopInstanceInput{ + InstanceId: aws.String(instanceId), + } + + log.Printf("[DEBUG] Stopping OpsWorks instance: %s", instanceId) + + _, err := client.StopInstance(req) + + if err != nil { + return err + } + + if wait { + log.Printf("[DEBUG] Waiting for instance (%s) to become stopped", instanceId) + + stateConf := &resource.StateChangeConf{ + Pending: []string{"stopping", "terminating", "shutting_down", "terminated"}, + Target: []string{"stopped"}, + Refresh: OpsworksInstanceStateRefreshFunc(client, instanceId), + Timeout: 10 * time.Minute, + Delay: 10 * time.Second, + MinTimeout: 3 * time.Second, + } + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for instance (%s) to become stopped: %s", + instanceId, err) + } + } + + return nil +} + +func readOpsworksBlockDevices(d *schema.ResourceData, instance *opsworks.Instance, meta interface{}) ( + map[string]interface{}, error) { + + blockDevices := make(map[string]interface{}) + blockDevices["ebs"] = make([]map[string]interface{}, 0) + blockDevices["ephemeral"] = make([]map[string]interface{}, 0) + blockDevices["root"] = nil + + if len(instance.BlockDeviceMappings) == 0 { + return nil, nil + } + + for _, bdm := range instance.BlockDeviceMappings { + bd := make(map[string]interface{}) + if bdm.Ebs != nil && bdm.Ebs.DeleteOnTermination != nil { + bd["delete_on_termination"] = *bdm.Ebs.DeleteOnTermination + } + if bdm.Ebs != nil && bdm.Ebs.VolumeSize != nil { + bd["volume_size"] = *bdm.Ebs.VolumeSize + } + if bdm.Ebs != nil && bdm.Ebs.VolumeType != nil { + bd["volume_type"] = *bdm.Ebs.VolumeType + } + if bdm.Ebs != nil && bdm.Ebs.Iops != nil { + bd["iops"] = *bdm.Ebs.Iops + } + if bdm.DeviceName != nil && *bdm.DeviceName == "ROOT_DEVICE" { + blockDevices["root"] = bd + } else { + if bdm.DeviceName != nil { + bd["device_name"] = *bdm.DeviceName + } + if bdm.VirtualName != nil { + bd["virtual_name"] = *bdm.VirtualName + blockDevices["ephemeral"] = append(blockDevices["ephemeral"].([]map[string]interface{}), bd) + } else { + if bdm.Ebs != nil && bdm.Ebs.SnapshotId != nil { + bd["snapshot_id"] = *bdm.Ebs.SnapshotId + } + blockDevices["ebs"] = append(blockDevices["ebs"].([]map[string]interface{}), bd) + } + } + } + return blockDevices, nil +} + +func OpsworksInstanceStateRefreshFunc(conn *opsworks.OpsWorks, instanceID string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + resp, err := conn.DescribeInstances(&opsworks.DescribeInstancesInput{ + InstanceIds: []*string{aws.String(instanceID)}, + }) + if err != nil { + if awserr, ok := err.(awserr.Error); ok && awserr.Code() == "ResourceNotFoundException" { + // Set this to nil as if we didn't find anything. + resp = nil + } else { + log.Printf("Error on OpsworksInstanceStateRefresh: %s", err) + return nil, "", err + } + } + + if resp == nil || len(resp.Instances) == 0 { + // Sometimes AWS just has consistency issues and doesn't see + // our instance yet. Return an empty state. + return nil, "", nil + } + + i := resp.Instances[0] + return i, *i.Status, nil + } +} diff --git a/builtin/providers/aws/resource_aws_opsworks_instance_test.go b/builtin/providers/aws/resource_aws_opsworks_instance_test.go new file mode 100644 index 000000000..e79f8bb45 --- /dev/null +++ b/builtin/providers/aws/resource_aws_opsworks_instance_test.go @@ -0,0 +1,274 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/opsworks" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSOpsworksInstance(t *testing.T) { + stackName := fmt.Sprintf("tf-%d", acctest.RandInt()) + var opsinst opsworks.Instance + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsOpsworksInstanceDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAwsOpsworksInstanceConfigCreate(stackName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSOpsworksInstanceExists( + "aws_opsworks_instance.tf-acc", &opsinst), + testAccCheckAWSOpsworksInstanceAttributes(&opsinst), + resource.TestCheckResourceAttr( + "aws_opsworks_instance.tf-acc", "hostname", "tf-acc1", + ), + resource.TestCheckResourceAttr( + "aws_opsworks_instance.tf-acc", "instance_type", "t2.micro", + ), + resource.TestCheckResourceAttr( + "aws_opsworks_instance.tf-acc", "state", "stopped", + ), + resource.TestCheckResourceAttr( + "aws_opsworks_instance.tf-acc", "layer_ids.#", "1", + ), + resource.TestCheckResourceAttr( + "aws_opsworks_instance.tf-acc", "install_updates_on_boot", "true", + ), + resource.TestCheckResourceAttr( + "aws_opsworks_instance.tf-acc", "architecture", "x86_64", + ), + resource.TestCheckResourceAttr( + "aws_opsworks_instance.tf-acc", "os", "Amazon Linux 2014.09", // inherited from opsworks_stack_test + ), + resource.TestCheckResourceAttr( + "aws_opsworks_instance.tf-acc", "root_device_type", "ebs", // inherited from opsworks_stack_test + ), + resource.TestCheckResourceAttr( + "aws_opsworks_instance.tf-acc", "availability_zone", "us-west-2a", // inherited from opsworks_stack_test + ), + ), + }, + resource.TestStep{ + Config: testAccAwsOpsworksInstanceConfigUpdate(stackName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSOpsworksInstanceExists( + "aws_opsworks_instance.tf-acc", &opsinst), + testAccCheckAWSOpsworksInstanceAttributes(&opsinst), + resource.TestCheckResourceAttr( + "aws_opsworks_instance.tf-acc", "hostname", "tf-acc1", + ), + resource.TestCheckResourceAttr( + "aws_opsworks_instance.tf-acc", "instance_type", "t2.small", + ), + resource.TestCheckResourceAttr( + "aws_opsworks_instance.tf-acc", "layer_ids.#", "2", + ), + resource.TestCheckResourceAttr( + "aws_opsworks_instance.tf-acc", "os", "Amazon Linux 2015.09", + ), + ), + }, + }, + }) +} + +func testAccCheckAWSOpsworksInstanceExists( + n string, opsinst *opsworks.Instance) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Opsworks Instance is set") + } + + conn := testAccProvider.Meta().(*AWSClient).opsworksconn + + params := &opsworks.DescribeInstancesInput{ + InstanceIds: []*string{&rs.Primary.ID}, + } + resp, err := conn.DescribeInstances(params) + + if err != nil { + return err + } + + if v := len(resp.Instances); v != 1 { + return fmt.Errorf("Expected 1 request returned, got %d", v) + } + + *opsinst = *resp.Instances[0] + + return nil + } +} + +func testAccCheckAWSOpsworksInstanceAttributes( + opsinst *opsworks.Instance) resource.TestCheckFunc { + return func(s *terraform.State) error { + // Depending on the timing, the state could be requested or stopped + if *opsinst.Status != "stopped" && *opsinst.Status != "requested" { + return fmt.Errorf("Unexpected request status: %s", *opsinst.Status) + } + if *opsinst.AvailabilityZone != "us-west-2a" { + return fmt.Errorf("Unexpected availability zone: %s", *opsinst.AvailabilityZone) + } + if *opsinst.Architecture != "x86_64" { + return fmt.Errorf("Unexpected architecture: %s", *opsinst.Architecture) + } + if *opsinst.InfrastructureClass != "ec2" { + return fmt.Errorf("Unexpected infrastructure class: %s", *opsinst.InfrastructureClass) + } + if *opsinst.RootDeviceType != "ebs" { + return fmt.Errorf("Unexpected root device type: %s", *opsinst.RootDeviceType) + } + if *opsinst.VirtualizationType != "hvm" { + return fmt.Errorf("Unexpected virtualization type: %s", *opsinst.VirtualizationType) + } + return nil + } +} + +func testAccCheckAwsOpsworksInstanceDestroy(s *terraform.State) error { + opsworksconn := testAccProvider.Meta().(*AWSClient).opsworksconn + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_opsworks_instance" { + continue + } + req := &opsworks.DescribeInstancesInput{ + InstanceIds: []*string{ + aws.String(rs.Primary.ID), + }, + } + + _, err := opsworksconn.DescribeInstances(req) + if err != nil { + if awserr, ok := err.(awserr.Error); ok { + if awserr.Code() == "ResourceNotFoundException" { + // not found, good to go + return nil + } + } + return err + } + } + + return fmt.Errorf("Fall through error on OpsWorks instance test") +} + +func testAccAwsOpsworksInstanceConfigCreate(name string) string { + return fmt.Sprintf(` +resource "aws_security_group" "tf-ops-acc-web" { + name = "%s-web" + ingress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_security_group" "tf-ops-acc-php" { + name = "%s-php" + ingress { + from_port = 8080 + to_port = 8080 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_opsworks_static_web_layer" "tf-acc" { + stack_id = "${aws_opsworks_stack.tf-acc.id}" + + custom_security_group_ids = [ + "${aws_security_group.tf-ops-acc-web.id}", + ] +} + +resource "aws_opsworks_php_app_layer" "tf-acc" { + stack_id = "${aws_opsworks_stack.tf-acc.id}" + + custom_security_group_ids = [ + "${aws_security_group.tf-ops-acc-php.id}", + ] +} + +resource "aws_opsworks_instance" "tf-acc" { + stack_id = "${aws_opsworks_stack.tf-acc.id}" + layer_ids = [ + "${aws_opsworks_static_web_layer.tf-acc.id}", + ] + instance_type = "t2.micro" + state = "stopped" + hostname = "tf-acc1" +} + +%s + +`, name, name, testAccAwsOpsworksStackConfigVpcCreate(name)) +} + +func testAccAwsOpsworksInstanceConfigUpdate(name string) string { + return fmt.Sprintf(` +resource "aws_security_group" "tf-ops-acc-web" { + name = "%s-web" + ingress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_security_group" "tf-ops-acc-php" { + name = "%s-php" + ingress { + from_port = 8080 + to_port = 8080 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_opsworks_static_web_layer" "tf-acc" { + stack_id = "${aws_opsworks_stack.tf-acc.id}" + + custom_security_group_ids = [ + "${aws_security_group.tf-ops-acc-web.id}", + ] +} + +resource "aws_opsworks_php_app_layer" "tf-acc" { + stack_id = "${aws_opsworks_stack.tf-acc.id}" + + custom_security_group_ids = [ + "${aws_security_group.tf-ops-acc-php.id}", + ] +} + +resource "aws_opsworks_instance" "tf-acc" { + stack_id = "${aws_opsworks_stack.tf-acc.id}" + layer_ids = [ + "${aws_opsworks_static_web_layer.tf-acc.id}", + "${aws_opsworks_php_app_layer.tf-acc.id}", + ] + instance_type = "t2.small" + state = "stopped" + hostname = "tf-acc1" + os = "Amazon Linux 2015.09" +} + +%s + +`, name, name, testAccAwsOpsworksStackConfigVpcCreate(name)) +} diff --git a/website/source/docs/providers/aws/r/opsworks_instance.html.markdown b/website/source/docs/providers/aws/r/opsworks_instance.html.markdown new file mode 100644 index 000000000..c855d559f --- /dev/null +++ b/website/source/docs/providers/aws/r/opsworks_instance.html.markdown @@ -0,0 +1,136 @@ +--- +layout: "aws" +page_title: "AWS: aws_opsworks_instance" +sidebar_current: "docs-aws-resource-opsworks-instance" +description: |- + Provides an OpsWorks instance resource. +--- + +# aws\_opsworks\_instance + +Provides an OpsWorks instance resource. + +## Example Usage + +``` +aws_opsworks_instance" "my-instance" { + stack_id = "${aws_opsworks_stack.my-stack.id}" + + layer_ids = [ + "${aws_opsworks_custom_layer.my-layer.id}", + ] + + instance_type = "t2.micro" + os = "Amazon Linux 2015.09" + state = "stopped" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `instance_type` - (Required) The type of instance to start +* `stack_id` - (Required) The id of the stack the instance will belong to. +* `layer_ids` - (Required) The ids of the layers the instance will belong to. +* `state` - (Optional) The desired state of the instance. Can be either `"running"` or `"stopped"`. +* `install_updates_on_boot` - (Optional) Controls where to install OS and package updates when the instance boots. Defaults to `true`. +* `auto_scaling_type` - (Optional) Creates load-based or time-based instances. If set, can be either: `"load"` or `"timer"`. +* `availability_zone` - (Optional) Name of the availability zone where instances will be created + by default. +* `ebs_optimized` - (Optional) If true, the launched EC2 instance will be EBS-optimized. +* `hostname` - (Optional) The instance's host name. +* `architecture` - (Optional) Machine architecture for created instances. Can be either `"x86_64"` (the default) or `"i386"` +* `ami_id` - (Optional) The AMI to use for the instance. If an AMI is specified, `os` must be `"Custom"`. +* `os` - (Optional) Name of operating system that will be installed. +* `root_device_type` - (Optional) Name of the type of root device instances will have by default. Can be either `"ebs"` or `"instance-store"` +* `ssh_key_name` - (Optional) Name of the SSH keypair that instances will have by default. +* `agent_version` - (Optional) The AWS OpsWorks agent to install. Defaults to `"INHERIT"`. +* `subnet_id` - (Optional) Subnet ID to attach to +* `virtualization_type` - (Optional) Keyword to choose what virtualization mode created instances + will use. Can be either `"paravirtual"` or `"hvm"`. +* `root_block_device` - (Optional) Customize details about the root block + device of the instance. See [Block Devices](#block-devices) below for details. +* `ebs_block_device` - (Optional) Additional EBS block devices to attach to the + instance. See [Block Devices](#block-devices) below for details. +* `ephemeral_block_device` - (Optional) Customize Ephemeral (also known as + "Instance Store") volumes on the instance. See [Block Devices](#block-devices) below for details. + + + +## Block devices + +Each of the `*_block_device` attributes controls a portion of the AWS +Instance's "Block Device Mapping". It's a good idea to familiarize yourself with [AWS's Block Device +Mapping docs](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html) +to understand the implications of using these attributes. + +The `root_block_device` mapping supports the following: + +* `volume_type` - (Optional) The type of volume. Can be `"standard"`, `"gp2"`, + or `"io1"`. (Default: `"standard"`). +* `volume_size` - (Optional) The size of the volume in gigabytes. +* `iops` - (Optional) The amount of provisioned + [IOPS](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-io-characteristics.html). + This must be set with a `volume_type` of `"io1"`. +* `delete_on_termination` - (Optional) Whether the volume should be destroyed + on instance termination (Default: `true`). + +Modifying any of the `root_block_device` settings requires resource +replacement. + +Each `ebs_block_device` supports the following: + +* `device_name` - The name of the device to mount. +* `snapshot_id` - (Optional) The Snapshot ID to mount. +* `volume_type` - (Optional) The type of volume. Can be `"standard"`, `"gp2"`, + or `"io1"`. (Default: `"standard"`). +* `volume_size` - (Optional) The size of the volume in gigabytes. +* `iops` - (Optional) The amount of provisioned + [IOPS](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-io-characteristics.html). + This must be set with a `volume_type` of `"io1"`. +* `delete_on_termination` - (Optional) Whether the volume should be destroyed + on instance termination (Default: `true`). +* `encrypted` - (Optional) Enables [EBS + encryption](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html) + on the volume (Default: `false`). Cannot be used with `snapshot_id`. + +Modifying any `ebs_block_device` currently requires resource replacement. + +Each `ephemeral_block_device` supports the following: + +* `device_name` - The name of the block device to mount on the instance. +* `virtual_name` - The [Instance Store Device + Name](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/InstanceStorage.html#InstanceStoreDeviceNames) + (e.g. `"ephemeral0"`) + +Each AWS Instance type has a different set of Instance Store block devices +available for attachment. AWS [publishes a +list](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/InstanceStorage.html#StorageOnInstanceTypes) +of which ephemeral devices are available on each type. The devices are always +identified by the `virtual_name` in the format `"ephemeral{0..N}"`. + +~> **NOTE:** Currently, changes to `*_block_device` configuration of _existing_ +resources cannot be automatically detected by Terraform. After making updates +to block device configuration, resource recreation can be manually triggered by +using the [`taint` command](/docs/commands/taint.html). + + +## Attributes Reference + +The following attributes are exported: + +* `id` - The id of the OpsWorks instance. +* `agent_version` - The AWS OpsWorks agent version. +* `availability_zone` - The availability zone of the instance. +* `ssh_key_name` - The key name of the instance +* `public_dns` - The public DNS name assigned to the instance. For EC2-VPC, this + is only available if you've enabled DNS hostnames for your VPC +* `public_ip` - The public IP address assigned to the instance, if applicable. +* `private_dns` - The private DNS name assigned to the instance. Can only be + used inside the Amazon EC2, and only available if you've enabled DNS hostnames + for your VPC +* `private_ip` - The private IP address assigned to the instance +* `subnet_id` - The VPC subnet ID. +* `security_group_ids` - The associated security groups. + diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index 8b6f4d1b5..5e358243e 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -478,6 +478,10 @@ aws_opsworks_haproxy_layer +