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