github.com/chalford/terraform@v0.3.7-0.20150113080010-a78c69a8c81f/builtin/providers/aws/resource_aws_instance.go (about)

     1  package aws
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/sha1"
     6  	"encoding/hex"
     7  	"fmt"
     8  	"log"
     9  	"strconv"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/hashicorp/terraform/helper/hashcode"
    14  	"github.com/hashicorp/terraform/helper/resource"
    15  	"github.com/hashicorp/terraform/helper/schema"
    16  	"github.com/mitchellh/goamz/ec2"
    17  )
    18  
    19  func resourceAwsInstance() *schema.Resource {
    20  	return &schema.Resource{
    21  		Create: resourceAwsInstanceCreate,
    22  		Read:   resourceAwsInstanceRead,
    23  		Update: resourceAwsInstanceUpdate,
    24  		Delete: resourceAwsInstanceDelete,
    25  
    26  		Schema: map[string]*schema.Schema{
    27  			"ami": &schema.Schema{
    28  				Type:     schema.TypeString,
    29  				Required: true,
    30  				ForceNew: true,
    31  			},
    32  
    33  			"associate_public_ip_address": &schema.Schema{
    34  				Type:     schema.TypeBool,
    35  				Optional: true,
    36  				ForceNew: true,
    37  			},
    38  
    39  			"availability_zone": &schema.Schema{
    40  				Type:     schema.TypeString,
    41  				Optional: true,
    42  				Computed: true,
    43  				ForceNew: true,
    44  			},
    45  
    46  			"instance_type": &schema.Schema{
    47  				Type:     schema.TypeString,
    48  				Required: true,
    49  				ForceNew: true,
    50  			},
    51  
    52  			"key_name": &schema.Schema{
    53  				Type:     schema.TypeString,
    54  				Optional: true,
    55  				ForceNew: true,
    56  				Computed: true,
    57  			},
    58  
    59  			"subnet_id": &schema.Schema{
    60  				Type:     schema.TypeString,
    61  				Optional: true,
    62  				Computed: true,
    63  				ForceNew: true,
    64  			},
    65  
    66  			"private_ip": &schema.Schema{
    67  				Type:     schema.TypeString,
    68  				Optional: true,
    69  				ForceNew: true,
    70  				Computed: true,
    71  			},
    72  
    73  			"source_dest_check": &schema.Schema{
    74  				Type:     schema.TypeBool,
    75  				Optional: true,
    76  			},
    77  
    78  			"user_data": &schema.Schema{
    79  				Type:     schema.TypeString,
    80  				Optional: true,
    81  				ForceNew: true,
    82  				StateFunc: func(v interface{}) string {
    83  					switch v.(type) {
    84  					case string:
    85  						hash := sha1.Sum([]byte(v.(string)))
    86  						return hex.EncodeToString(hash[:])
    87  					default:
    88  						return ""
    89  					}
    90  				},
    91  			},
    92  
    93  			"security_groups": &schema.Schema{
    94  				Type:     schema.TypeSet,
    95  				Optional: true,
    96  				Computed: true,
    97  				ForceNew: true,
    98  				Elem:     &schema.Schema{Type: schema.TypeString},
    99  				Set: func(v interface{}) int {
   100  					return hashcode.String(v.(string))
   101  				},
   102  			},
   103  
   104  			"public_dns": &schema.Schema{
   105  				Type:     schema.TypeString,
   106  				Computed: true,
   107  			},
   108  
   109  			"public_ip": &schema.Schema{
   110  				Type:     schema.TypeString,
   111  				Computed: true,
   112  			},
   113  
   114  			"private_dns": &schema.Schema{
   115  				Type:     schema.TypeString,
   116  				Computed: true,
   117  			},
   118  
   119  			"ebs_optimized": &schema.Schema{
   120  				Type:     schema.TypeBool,
   121  				Optional: true,
   122  			},
   123  
   124  			"iam_instance_profile": &schema.Schema{
   125  				Type:     schema.TypeString,
   126  				ForceNew: true,
   127  				Optional: true,
   128  			},
   129  			"tenancy": &schema.Schema{
   130  				Type:     schema.TypeString,
   131  				Optional: true,
   132  				Computed: true,
   133  				ForceNew: true,
   134  			},
   135  			"tags": tagsSchema(),
   136  
   137  			"block_device": &schema.Schema{
   138  				Type:     schema.TypeSet,
   139  				Optional: true,
   140  				Computed: true,
   141  				Elem: &schema.Resource{
   142  					Schema: map[string]*schema.Schema{
   143  						"device_name": &schema.Schema{
   144  							Type:     schema.TypeString,
   145  							Required: true,
   146  							ForceNew: true,
   147  						},
   148  
   149  						"virtual_name": &schema.Schema{
   150  							Type:     schema.TypeString,
   151  							Optional: true,
   152  							ForceNew: true,
   153  						},
   154  
   155  						"snapshot_id": &schema.Schema{
   156  							Type:     schema.TypeString,
   157  							Optional: true,
   158  							Computed: true,
   159  							ForceNew: true,
   160  						},
   161  
   162  						"volume_type": &schema.Schema{
   163  							Type:     schema.TypeString,
   164  							Optional: true,
   165  							Computed: true,
   166  							ForceNew: true,
   167  						},
   168  
   169  						"volume_size": &schema.Schema{
   170  							Type:     schema.TypeInt,
   171  							Optional: true,
   172  							Computed: true,
   173  							ForceNew: true,
   174  						},
   175  
   176  						"delete_on_termination": &schema.Schema{
   177  							Type:     schema.TypeBool,
   178  							Optional: true,
   179  							Default:  true,
   180  							ForceNew: true,
   181  						},
   182  
   183  						"encrypted": &schema.Schema{
   184  							Type:     schema.TypeBool,
   185  							Optional: true,
   186  							Computed: true,
   187  							ForceNew: true,
   188  						},
   189  					},
   190  				},
   191  				Set: resourceAwsInstanceBlockDevicesHash,
   192  			},
   193  		},
   194  	}
   195  }
   196  
   197  func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error {
   198  	ec2conn := meta.(*AWSClient).ec2conn
   199  
   200  	// Figure out user data
   201  	userData := ""
   202  	if v := d.Get("user_data"); v != nil {
   203  		userData = v.(string)
   204  	}
   205  
   206  	associatePublicIPAddress := false
   207  	if v := d.Get("associate_public_ip_address"); v != nil {
   208  		associatePublicIPAddress = v.(bool)
   209  	}
   210  
   211  	// Build the creation struct
   212  	runOpts := &ec2.RunInstances{
   213  		ImageId:                  d.Get("ami").(string),
   214  		AvailZone:                d.Get("availability_zone").(string),
   215  		InstanceType:             d.Get("instance_type").(string),
   216  		KeyName:                  d.Get("key_name").(string),
   217  		SubnetId:                 d.Get("subnet_id").(string),
   218  		PrivateIPAddress:         d.Get("private_ip").(string),
   219  		AssociatePublicIpAddress: associatePublicIPAddress,
   220  		UserData:                 []byte(userData),
   221  		EbsOptimized:             d.Get("ebs_optimized").(bool),
   222  		IamInstanceProfile:       d.Get("iam_instance_profile").(string),
   223  		Tenancy:                  d.Get("tenancy").(string),
   224  	}
   225  
   226  	if v := d.Get("security_groups"); v != nil {
   227  		for _, v := range v.(*schema.Set).List() {
   228  			str := v.(string)
   229  
   230  			var g ec2.SecurityGroup
   231  			if runOpts.SubnetId != "" {
   232  				g.Id = str
   233  			} else {
   234  				g.Name = str
   235  			}
   236  
   237  			runOpts.SecurityGroups = append(runOpts.SecurityGroups, g)
   238  		}
   239  	}
   240  
   241  	if v := d.Get("block_device"); v != nil {
   242  		vs := v.(*schema.Set).List()
   243  		if len(vs) > 0 {
   244  			runOpts.BlockDevices = make([]ec2.BlockDeviceMapping, len(vs))
   245  			for i, v := range vs {
   246  				bd := v.(map[string]interface{})
   247  				runOpts.BlockDevices[i].DeviceName = bd["device_name"].(string)
   248  				runOpts.BlockDevices[i].VirtualName = bd["virtual_name"].(string)
   249  				runOpts.BlockDevices[i].SnapshotId = bd["snapshot_id"].(string)
   250  				runOpts.BlockDevices[i].VolumeType = bd["volume_type"].(string)
   251  				runOpts.BlockDevices[i].VolumeSize = int64(bd["volume_size"].(int))
   252  				runOpts.BlockDevices[i].DeleteOnTermination = bd["delete_on_termination"].(bool)
   253  				runOpts.BlockDevices[i].Encrypted = bd["encrypted"].(bool)
   254  			}
   255  		}
   256  	}
   257  
   258  	// Create the instance
   259  	log.Printf("[DEBUG] Run configuration: %#v", runOpts)
   260  	runResp, err := ec2conn.RunInstances(runOpts)
   261  	if err != nil {
   262  		return fmt.Errorf("Error launching source instance: %s", err)
   263  	}
   264  
   265  	instance := &runResp.Instances[0]
   266  	log.Printf("[INFO] Instance ID: %s", instance.InstanceId)
   267  
   268  	// Store the resulting ID so we can look this up later
   269  	d.SetId(instance.InstanceId)
   270  
   271  	// Wait for the instance to become running so we can get some attributes
   272  	// that aren't available until later.
   273  	log.Printf(
   274  		"[DEBUG] Waiting for instance (%s) to become running",
   275  		instance.InstanceId)
   276  
   277  	stateConf := &resource.StateChangeConf{
   278  		Pending:    []string{"pending"},
   279  		Target:     "running",
   280  		Refresh:    InstanceStateRefreshFunc(ec2conn, instance.InstanceId),
   281  		Timeout:    10 * time.Minute,
   282  		Delay:      10 * time.Second,
   283  		MinTimeout: 3 * time.Second,
   284  	}
   285  
   286  	instanceRaw, err := stateConf.WaitForState()
   287  	if err != nil {
   288  		return fmt.Errorf(
   289  			"Error waiting for instance (%s) to become ready: %s",
   290  			instance.InstanceId, err)
   291  	}
   292  
   293  	instance = instanceRaw.(*ec2.Instance)
   294  
   295  	// Initialize the connection info
   296  	d.SetConnInfo(map[string]string{
   297  		"type": "ssh",
   298  		"host": instance.PublicIpAddress,
   299  	})
   300  
   301  	// Set our attributes
   302  	if err := resourceAwsInstanceRead(d, meta); err != nil {
   303  		return err
   304  	}
   305  
   306  	// Update if we need to
   307  	return resourceAwsInstanceUpdate(d, meta)
   308  }
   309  
   310  func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
   311  	ec2conn := meta.(*AWSClient).ec2conn
   312  
   313  	resp, err := ec2conn.Instances([]string{d.Id()}, ec2.NewFilter())
   314  	if err != nil {
   315  		// If the instance was not found, return nil so that we can show
   316  		// that the instance is gone.
   317  		if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidInstanceID.NotFound" {
   318  			d.SetId("")
   319  			return nil
   320  		}
   321  
   322  		// Some other error, report it
   323  		return err
   324  	}
   325  
   326  	// If nothing was found, then return no state
   327  	if len(resp.Reservations) == 0 {
   328  		d.SetId("")
   329  		return nil
   330  	}
   331  
   332  	instance := &resp.Reservations[0].Instances[0]
   333  
   334  	// If the instance is terminated, then it is gone
   335  	if instance.State.Name == "terminated" {
   336  		d.SetId("")
   337  		return nil
   338  	}
   339  
   340  	d.Set("availability_zone", instance.AvailZone)
   341  	d.Set("key_name", instance.KeyName)
   342  	d.Set("public_dns", instance.DNSName)
   343  	d.Set("public_ip", instance.PublicIpAddress)
   344  	d.Set("private_dns", instance.PrivateDNSName)
   345  	d.Set("private_ip", instance.PrivateIpAddress)
   346  	d.Set("subnet_id", instance.SubnetId)
   347  	d.Set("ebs_optimized", instance.EbsOptimized)
   348  	d.Set("tags", tagsToMap(instance.Tags))
   349  	d.Set("tenancy", instance.Tenancy)
   350  
   351  	// Determine whether we're referring to security groups with
   352  	// IDs or names. We use a heuristic to figure this out. By default,
   353  	// we use IDs if we're in a VPC. However, if we previously had an
   354  	// all-name list of security groups, we use names. Or, if we had any
   355  	// IDs, we use IDs.
   356  	useID := instance.SubnetId != ""
   357  	if v := d.Get("security_groups"); v != nil {
   358  		match := false
   359  		for _, v := range v.(*schema.Set).List() {
   360  			if strings.HasPrefix(v.(string), "sg-") {
   361  				match = true
   362  				break
   363  			}
   364  		}
   365  
   366  		useID = match
   367  	}
   368  
   369  	// Build up the security groups
   370  	sgs := make([]string, len(instance.SecurityGroups))
   371  	for i, sg := range instance.SecurityGroups {
   372  		if useID {
   373  			sgs[i] = sg.Id
   374  		} else {
   375  			sgs[i] = sg.Name
   376  		}
   377  	}
   378  	d.Set("security_groups", sgs)
   379  
   380  	volIDs := make([]string, len(instance.BlockDevices))
   381  	bdByVolID := make(map[string]ec2.BlockDevice)
   382  	for i, bd := range instance.BlockDevices {
   383  		volIDs[i] = bd.VolumeId
   384  		bdByVolID[bd.VolumeId] = bd
   385  	}
   386  
   387  	volResp, err := ec2conn.Volumes(volIDs, ec2.NewFilter())
   388  	if err != nil {
   389  		return err
   390  	}
   391  
   392  	bds := make([]map[string]interface{}, len(volResp.Volumes))
   393  	for i, vol := range volResp.Volumes {
   394  		volSize, err := strconv.Atoi(vol.Size)
   395  		if err != nil {
   396  			return err
   397  		}
   398  		bds[i] = make(map[string]interface{})
   399  		bds[i]["device_name"] = bdByVolID[vol.VolumeId].DeviceName
   400  		bds[i]["snapshot_id"] = vol.SnapshotId
   401  		bds[i]["volume_type"] = vol.VolumeType
   402  		bds[i]["volume_size"] = volSize
   403  		bds[i]["delete_on_termination"] = bdByVolID[vol.VolumeId].DeleteOnTermination
   404  		bds[i]["encrypted"] = vol.Encrypted
   405  	}
   406  	d.Set("block_device", bds)
   407  
   408  	return nil
   409  }
   410  
   411  func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
   412  	ec2conn := meta.(*AWSClient).ec2conn
   413  
   414  	modify := false
   415  	opts := new(ec2.ModifyInstance)
   416  
   417  	if v, ok := d.GetOk("source_dest_check"); ok {
   418  		opts.SourceDestCheck = v.(bool)
   419  		opts.SetSourceDestCheck = true
   420  		modify = true
   421  	}
   422  
   423  	if modify {
   424  		log.Printf("[INFO] Modifing instance %s: %#v", d.Id(), opts)
   425  		if _, err := ec2conn.ModifyInstance(d.Id(), opts); err != nil {
   426  			return err
   427  		}
   428  
   429  		// TODO(mitchellh): wait for the attributes we modified to
   430  		// persist the change...
   431  	}
   432  
   433  	if err := setTags(ec2conn, d); err != nil {
   434  		return err
   435  	} else {
   436  		d.SetPartial("tags")
   437  	}
   438  
   439  	return nil
   440  }
   441  
   442  func resourceAwsInstanceDelete(d *schema.ResourceData, meta interface{}) error {
   443  	ec2conn := meta.(*AWSClient).ec2conn
   444  
   445  	log.Printf("[INFO] Terminating instance: %s", d.Id())
   446  	if _, err := ec2conn.TerminateInstances([]string{d.Id()}); err != nil {
   447  		return fmt.Errorf("Error terminating instance: %s", err)
   448  	}
   449  
   450  	log.Printf(
   451  		"[DEBUG] Waiting for instance (%s) to become terminated",
   452  		d.Id())
   453  
   454  	stateConf := &resource.StateChangeConf{
   455  		Pending:    []string{"pending", "running", "shutting-down", "stopped", "stopping"},
   456  		Target:     "terminated",
   457  		Refresh:    InstanceStateRefreshFunc(ec2conn, d.Id()),
   458  		Timeout:    10 * time.Minute,
   459  		Delay:      10 * time.Second,
   460  		MinTimeout: 3 * time.Second,
   461  	}
   462  
   463  	_, err := stateConf.WaitForState()
   464  	if err != nil {
   465  		return fmt.Errorf(
   466  			"Error waiting for instance (%s) to terminate: %s",
   467  			d.Id(), err)
   468  	}
   469  
   470  	d.SetId("")
   471  	return nil
   472  }
   473  
   474  // InstanceStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch
   475  // an EC2 instance.
   476  func InstanceStateRefreshFunc(conn *ec2.EC2, instanceID string) resource.StateRefreshFunc {
   477  	return func() (interface{}, string, error) {
   478  		resp, err := conn.Instances([]string{instanceID}, ec2.NewFilter())
   479  		if err != nil {
   480  			if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidInstanceID.NotFound" {
   481  				// Set this to nil as if we didn't find anything.
   482  				resp = nil
   483  			} else {
   484  				log.Printf("Error on InstanceStateRefresh: %s", err)
   485  				return nil, "", err
   486  			}
   487  		}
   488  
   489  		if resp == nil || len(resp.Reservations) == 0 || len(resp.Reservations[0].Instances) == 0 {
   490  			// Sometimes AWS just has consistency issues and doesn't see
   491  			// our instance yet. Return an empty state.
   492  			return nil, "", nil
   493  		}
   494  
   495  		i := &resp.Reservations[0].Instances[0]
   496  		return i, i.State.Name, nil
   497  	}
   498  }
   499  
   500  func resourceAwsInstanceBlockDevicesHash(v interface{}) int {
   501  	var buf bytes.Buffer
   502  	m := v.(map[string]interface{})
   503  	buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string)))
   504  	buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string)))
   505  	buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool)))
   506  	return hashcode.String(buf.String())
   507  }