github.com/jefferai/terraform@v0.3.7-0.20150310153852-f7512ca29fcf/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  						"iops": &schema.Schema{
   191  							Type:     schema.TypeInt,
   192  							Optional: true,
   193  							Computed: true,
   194  							ForceNew: true,
   195  						},
   196  					},
   197  				},
   198  				Set: resourceAwsInstanceBlockDevicesHash,
   199  			},
   200  
   201  			"root_block_device": &schema.Schema{
   202  				// TODO: This is a list because we don't support singleton
   203  				//       sub-resources today. We'll enforce that the list only ever has
   204  				//       length zero or one below. When TF gains support for
   205  				//       sub-resources this can be converted.
   206  				Type:     schema.TypeList,
   207  				Optional: true,
   208  				Computed: true,
   209  				Elem: &schema.Resource{
   210  					// "You can only modify the volume size, volume type, and Delete on
   211  					// Termination flag on the block device mapping entry for the root
   212  					// device volume." - bit.ly/ec2bdmap
   213  					Schema: map[string]*schema.Schema{
   214  						"delete_on_termination": &schema.Schema{
   215  							Type:     schema.TypeBool,
   216  							Optional: true,
   217  							Default:  true,
   218  							ForceNew: true,
   219  						},
   220  
   221  						"device_name": &schema.Schema{
   222  							Type:     schema.TypeString,
   223  							Optional: true,
   224  							ForceNew: true,
   225  							Default:  "/dev/sda1",
   226  						},
   227  
   228  						"volume_size": &schema.Schema{
   229  							Type:     schema.TypeInt,
   230  							Optional: true,
   231  							Computed: true,
   232  							ForceNew: true,
   233  						},
   234  
   235  						"volume_type": &schema.Schema{
   236  							Type:     schema.TypeString,
   237  							Optional: true,
   238  							Computed: true,
   239  							ForceNew: true,
   240  						},
   241  
   242  						"iops": &schema.Schema{
   243  							Type:     schema.TypeInt,
   244  							Optional: true,
   245  							Computed: true,
   246  							ForceNew: true,
   247  						},
   248  					},
   249  				},
   250  			},
   251  		},
   252  	}
   253  }
   254  
   255  func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error {
   256  	ec2conn := meta.(*AWSClient).ec2conn
   257  
   258  	// Figure out user data
   259  	userData := ""
   260  	if v := d.Get("user_data"); v != nil {
   261  		userData = v.(string)
   262  	}
   263  
   264  	associatePublicIPAddress := false
   265  	if v := d.Get("associate_public_ip_address"); v != nil {
   266  		associatePublicIPAddress = v.(bool)
   267  	}
   268  
   269  	// Build the creation struct
   270  	runOpts := &ec2.RunInstances{
   271  		ImageId:                  d.Get("ami").(string),
   272  		AvailZone:                d.Get("availability_zone").(string),
   273  		InstanceType:             d.Get("instance_type").(string),
   274  		KeyName:                  d.Get("key_name").(string),
   275  		SubnetId:                 d.Get("subnet_id").(string),
   276  		PrivateIPAddress:         d.Get("private_ip").(string),
   277  		AssociatePublicIpAddress: associatePublicIPAddress,
   278  		UserData:                 []byte(userData),
   279  		EbsOptimized:             d.Get("ebs_optimized").(bool),
   280  		IamInstanceProfile:       d.Get("iam_instance_profile").(string),
   281  		Tenancy:                  d.Get("tenancy").(string),
   282  	}
   283  
   284  	if v := d.Get("security_groups"); v != nil {
   285  		for _, v := range v.(*schema.Set).List() {
   286  			str := v.(string)
   287  
   288  			var g ec2.SecurityGroup
   289  			if runOpts.SubnetId != "" {
   290  				g.Id = str
   291  			} else {
   292  				g.Name = str
   293  			}
   294  
   295  			runOpts.SecurityGroups = append(runOpts.SecurityGroups, g)
   296  		}
   297  	}
   298  
   299  	blockDevices := make([]interface{}, 0)
   300  
   301  	if v := d.Get("block_device"); v != nil {
   302  		blockDevices = append(blockDevices, v.(*schema.Set).List()...)
   303  	}
   304  
   305  	if v := d.Get("root_block_device"); v != nil {
   306  		rootBlockDevices := v.([]interface{})
   307  		if len(rootBlockDevices) > 1 {
   308  			return fmt.Errorf("Cannot specify more than one root_block_device.")
   309  		}
   310  		blockDevices = append(blockDevices, rootBlockDevices...)
   311  	}
   312  
   313  	if len(blockDevices) > 0 {
   314  		runOpts.BlockDevices = make([]ec2.BlockDeviceMapping, len(blockDevices))
   315  		for i, v := range blockDevices {
   316  			bd := v.(map[string]interface{})
   317  			runOpts.BlockDevices[i].DeviceName = bd["device_name"].(string)
   318  			runOpts.BlockDevices[i].VolumeType = bd["volume_type"].(string)
   319  			runOpts.BlockDevices[i].VolumeSize = int64(bd["volume_size"].(int))
   320  			runOpts.BlockDevices[i].DeleteOnTermination = bd["delete_on_termination"].(bool)
   321  			if v, ok := bd["virtual_name"].(string); ok {
   322  				runOpts.BlockDevices[i].VirtualName = v
   323  			}
   324  			if v, ok := bd["snapshot_id"].(string); ok {
   325  				runOpts.BlockDevices[i].SnapshotId = v
   326  			}
   327  			if v, ok := bd["encrypted"].(bool); ok {
   328  				runOpts.BlockDevices[i].Encrypted = v
   329  			}
   330  			if v, ok := bd["iops"].(int); ok {
   331  				runOpts.BlockDevices[i].IOPS = int64(v)
   332  			}
   333  		}
   334  	}
   335  
   336  	// Create the instance
   337  	log.Printf("[DEBUG] Run configuration: %#v", runOpts)
   338  	runResp, err := ec2conn.RunInstances(runOpts)
   339  	if err != nil {
   340  		return fmt.Errorf("Error launching source instance: %s", err)
   341  	}
   342  
   343  	instance := &runResp.Instances[0]
   344  	log.Printf("[INFO] Instance ID: %s", instance.InstanceId)
   345  
   346  	// Store the resulting ID so we can look this up later
   347  	d.SetId(instance.InstanceId)
   348  
   349  	// Wait for the instance to become running so we can get some attributes
   350  	// that aren't available until later.
   351  	log.Printf(
   352  		"[DEBUG] Waiting for instance (%s) to become running",
   353  		instance.InstanceId)
   354  
   355  	stateConf := &resource.StateChangeConf{
   356  		Pending:    []string{"pending"},
   357  		Target:     "running",
   358  		Refresh:    InstanceStateRefreshFunc(ec2conn, instance.InstanceId),
   359  		Timeout:    10 * time.Minute,
   360  		Delay:      10 * time.Second,
   361  		MinTimeout: 3 * time.Second,
   362  	}
   363  
   364  	instanceRaw, err := stateConf.WaitForState()
   365  	if err != nil {
   366  		return fmt.Errorf(
   367  			"Error waiting for instance (%s) to become ready: %s",
   368  			instance.InstanceId, err)
   369  	}
   370  
   371  	instance = instanceRaw.(*ec2.Instance)
   372  
   373  	// Initialize the connection info
   374  	d.SetConnInfo(map[string]string{
   375  		"type": "ssh",
   376  		"host": instance.PublicIpAddress,
   377  	})
   378  
   379  	// Set our attributes
   380  	if err := resourceAwsInstanceRead(d, meta); err != nil {
   381  		return err
   382  	}
   383  
   384  	// Update if we need to
   385  	return resourceAwsInstanceUpdate(d, meta)
   386  }
   387  
   388  func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
   389  	ec2conn := meta.(*AWSClient).ec2conn
   390  
   391  	resp, err := ec2conn.Instances([]string{d.Id()}, ec2.NewFilter())
   392  	if err != nil {
   393  		// If the instance was not found, return nil so that we can show
   394  		// that the instance is gone.
   395  		if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidInstanceID.NotFound" {
   396  			d.SetId("")
   397  			return nil
   398  		}
   399  
   400  		// Some other error, report it
   401  		return err
   402  	}
   403  
   404  	// If nothing was found, then return no state
   405  	if len(resp.Reservations) == 0 {
   406  		d.SetId("")
   407  		return nil
   408  	}
   409  
   410  	instance := &resp.Reservations[0].Instances[0]
   411  
   412  	// If the instance is terminated, then it is gone
   413  	if instance.State.Name == "terminated" {
   414  		d.SetId("")
   415  		return nil
   416  	}
   417  
   418  	d.Set("availability_zone", instance.AvailZone)
   419  	d.Set("key_name", instance.KeyName)
   420  	d.Set("public_dns", instance.DNSName)
   421  	d.Set("public_ip", instance.PublicIpAddress)
   422  	d.Set("private_dns", instance.PrivateDNSName)
   423  	d.Set("private_ip", instance.PrivateIpAddress)
   424  	d.Set("subnet_id", instance.SubnetId)
   425  	d.Set("ebs_optimized", instance.EbsOptimized)
   426  	d.Set("tags", tagsToMap(instance.Tags))
   427  	d.Set("tenancy", instance.Tenancy)
   428  
   429  	// Determine whether we're referring to security groups with
   430  	// IDs or names. We use a heuristic to figure this out. By default,
   431  	// we use IDs if we're in a VPC. However, if we previously had an
   432  	// all-name list of security groups, we use names. Or, if we had any
   433  	// IDs, we use IDs.
   434  	useID := instance.SubnetId != ""
   435  	if v := d.Get("security_groups"); v != nil {
   436  		match := false
   437  		for _, v := range v.(*schema.Set).List() {
   438  			if strings.HasPrefix(v.(string), "sg-") {
   439  				match = true
   440  				break
   441  			}
   442  		}
   443  
   444  		useID = match
   445  	}
   446  
   447  	// Build up the security groups
   448  	sgs := make([]string, len(instance.SecurityGroups))
   449  	for i, sg := range instance.SecurityGroups {
   450  		if useID {
   451  			sgs[i] = sg.Id
   452  		} else {
   453  			sgs[i] = sg.Name
   454  		}
   455  	}
   456  	d.Set("security_groups", sgs)
   457  
   458  	blockDevices := make(map[string]ec2.BlockDevice)
   459  	for _, bd := range instance.BlockDevices {
   460  		blockDevices[bd.VolumeId] = bd
   461  	}
   462  
   463  	volIDs := make([]string, 0, len(blockDevices))
   464  	for volID := range blockDevices {
   465  		volIDs = append(volIDs, volID)
   466  	}
   467  
   468  	volResp, err := ec2conn.Volumes(volIDs, ec2.NewFilter())
   469  	if err != nil {
   470  		return err
   471  	}
   472  
   473  	nonRootBlockDevices := make([]map[string]interface{}, 0)
   474  	rootBlockDevice := make([]interface{}, 0, 1)
   475  	for _, vol := range volResp.Volumes {
   476  		volSize, err := strconv.Atoi(vol.Size)
   477  		if err != nil {
   478  			return err
   479  		}
   480  
   481  		blockDevice := make(map[string]interface{})
   482  		blockDevice["device_name"] = blockDevices[vol.VolumeId].DeviceName
   483  		blockDevice["volume_type"] = vol.VolumeType
   484  		blockDevice["volume_size"] = volSize
   485  		blockDevice["delete_on_termination"] =
   486  			blockDevices[vol.VolumeId].DeleteOnTermination
   487  
   488  		// If this is the root device, save it. We stop here since we
   489  		// can't put invalid keys into this map.
   490  		if blockDevice["device_name"] == instance.RootDeviceName {
   491  			rootBlockDevice = []interface{}{blockDevice}
   492  			continue
   493  		}
   494  
   495  		blockDevice["snapshot_id"] = vol.SnapshotId
   496  		blockDevice["encrypted"] = vol.Encrypted
   497  		blockDevice["iops"] = vol.IOPS
   498  		nonRootBlockDevices = append(nonRootBlockDevices, blockDevice)
   499  	}
   500  	d.Set("block_device", nonRootBlockDevices)
   501  	d.Set("root_block_device", rootBlockDevice)
   502  
   503  	return nil
   504  }
   505  
   506  func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
   507  	ec2conn := meta.(*AWSClient).ec2conn
   508  	opts := new(ec2.ModifyInstance)
   509  
   510  	opts.SetSourceDestCheck = true
   511  	opts.SourceDestCheck = d.Get("source_dest_check").(bool)
   512  
   513  	log.Printf("[INFO] Modifying instance %s: %#v", d.Id(), opts)
   514  	if _, err := ec2conn.ModifyInstance(d.Id(), opts); err != nil {
   515  		return err
   516  	}
   517  
   518  	// TODO(mitchellh): wait for the attributes we modified to
   519  	// persist the change...
   520  
   521  	if err := setTags(ec2conn, d); err != nil {
   522  		return err
   523  	} else {
   524  		d.SetPartial("tags")
   525  	}
   526  
   527  	return nil
   528  }
   529  
   530  func resourceAwsInstanceDelete(d *schema.ResourceData, meta interface{}) error {
   531  	ec2conn := meta.(*AWSClient).ec2conn
   532  
   533  	log.Printf("[INFO] Terminating instance: %s", d.Id())
   534  	if _, err := ec2conn.TerminateInstances([]string{d.Id()}); err != nil {
   535  		return fmt.Errorf("Error terminating instance: %s", err)
   536  	}
   537  
   538  	log.Printf(
   539  		"[DEBUG] Waiting for instance (%s) to become terminated",
   540  		d.Id())
   541  
   542  	stateConf := &resource.StateChangeConf{
   543  		Pending:    []string{"pending", "running", "shutting-down", "stopped", "stopping"},
   544  		Target:     "terminated",
   545  		Refresh:    InstanceStateRefreshFunc(ec2conn, d.Id()),
   546  		Timeout:    10 * time.Minute,
   547  		Delay:      10 * time.Second,
   548  		MinTimeout: 3 * time.Second,
   549  	}
   550  
   551  	_, err := stateConf.WaitForState()
   552  	if err != nil {
   553  		return fmt.Errorf(
   554  			"Error waiting for instance (%s) to terminate: %s",
   555  			d.Id(), err)
   556  	}
   557  
   558  	d.SetId("")
   559  	return nil
   560  }
   561  
   562  // InstanceStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch
   563  // an EC2 instance.
   564  func InstanceStateRefreshFunc(conn *ec2.EC2, instanceID string) resource.StateRefreshFunc {
   565  	return func() (interface{}, string, error) {
   566  		resp, err := conn.Instances([]string{instanceID}, ec2.NewFilter())
   567  		if err != nil {
   568  			if ec2err, ok := err.(*ec2.Error); ok && ec2err.Code == "InvalidInstanceID.NotFound" {
   569  				// Set this to nil as if we didn't find anything.
   570  				resp = nil
   571  			} else {
   572  				log.Printf("Error on InstanceStateRefresh: %s", err)
   573  				return nil, "", err
   574  			}
   575  		}
   576  
   577  		if resp == nil || len(resp.Reservations) == 0 || len(resp.Reservations[0].Instances) == 0 {
   578  			// Sometimes AWS just has consistency issues and doesn't see
   579  			// our instance yet. Return an empty state.
   580  			return nil, "", nil
   581  		}
   582  
   583  		i := &resp.Reservations[0].Instances[0]
   584  		return i, i.State.Name, nil
   585  	}
   586  }
   587  
   588  func resourceAwsInstanceBlockDevicesHash(v interface{}) int {
   589  	var buf bytes.Buffer
   590  	m := v.(map[string]interface{})
   591  	buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string)))
   592  	buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string)))
   593  	buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool)))
   594  	return hashcode.String(buf.String())
   595  }