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