providers/aws: rework instance block devices

Instance block devices are now managed by three distinct sub-resources:

 * `root_block_device` - introduced previously
 * `ebs_block_device` - all additional ebs-backed volumes
 * `ephemeral_block_device` - instance store / ephemeral devices

The AWS API support around BlockDeviceMapping is pretty confusing. It's
a single collection type that supports these three members each of which
has different fields and different behavior.

My biggest hiccup came from the fact that Instance Store volumes do not
show up in any response BlockDeviceMapping for any EC2 `Describe*` API
calls. They're only available from the instance meta-data service as
queried from inside the node.

This removes `block_device` altogether for a clean break from old
configs. New configs will need to sort their `block_device`
declarations into the three new types. The field has been marked
`Removed` to indicate this to users.

With the new block device format being introduced, we need to ensure
Terraform is able to properly read statefiles written in the old format.
So we use the new `helper/schema` facility of "state migrations" to
transform statefiles in the old format to something that the current
version of the schema can use.

Fixes #858
This commit is contained in:
Paul Hinze 2015-02-24 11:00:22 -06:00
parent 3ba8ed536b
commit 2b23c402ee
5 changed files with 587 additions and 154 deletions

View File

@ -24,6 +24,9 @@ func resourceAwsInstance() *schema.Resource {
Update: resourceAwsInstanceUpdate, Update: resourceAwsInstanceUpdate,
Delete: resourceAwsInstanceDelete, Delete: resourceAwsInstanceDelete,
SchemaVersion: 1,
MigrateState: resourceAwsInstanceMigrateState,
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"ami": &schema.Schema{ "ami": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
@ -127,53 +130,28 @@ func resourceAwsInstance() *schema.Resource {
ForceNew: true, ForceNew: true,
Optional: true, Optional: true,
}, },
"tenancy": &schema.Schema{ "tenancy": &schema.Schema{
Type: schema.TypeString, Type: schema.TypeString,
Optional: true, Optional: true,
Computed: true, Computed: true,
ForceNew: true, ForceNew: true,
}, },
"tags": tagsSchema(), "tags": tagsSchema(),
"block_device": &schema.Schema{ "block_device": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
Removed: "Split out into three sub-types; see Changelog and Docs",
},
"ebs_block_device": &schema.Schema{
Type: schema.TypeSet, Type: schema.TypeSet,
Optional: true, Optional: true,
Computed: true, Computed: true,
Elem: &schema.Resource{ Elem: &schema.Resource{
Schema: map[string]*schema.Schema{ Schema: map[string]*schema.Schema{
"device_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"virtual_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"snapshot_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"volume_type": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
"volume_size": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
ForceNew: true,
},
"delete_on_termination": &schema.Schema{ "delete_on_termination": &schema.Schema{
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
@ -181,6 +159,12 @@ func resourceAwsInstance() *schema.Resource {
ForceNew: true, ForceNew: true,
}, },
"device_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"encrypted": &schema.Schema{ "encrypted": &schema.Schema{
Type: schema.TypeBool, Type: schema.TypeBool,
Optional: true, Optional: true,
@ -194,17 +178,79 @@ func resourceAwsInstance() *schema.Resource {
Computed: true, Computed: true,
ForceNew: 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: resourceAwsInstanceBlockDevicesHash, },
Set: func(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool)))
buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string)))
buf.WriteString(fmt.Sprintf("%t-", m["encrypted"].(bool)))
// NOTE: Not considering IOPS in hash; when using gp2, IOPS can come
// back set to something like "33", which throws off the set
// calculation and generates an unresolvable diff.
// buf.WriteString(fmt.Sprintf("%d-", m["iops"].(int)))
buf.WriteString(fmt.Sprintf("%s-", m["snapshot_id"].(string)))
buf.WriteString(fmt.Sprintf("%d-", m["volume_size"].(int)))
buf.WriteString(fmt.Sprintf("%s-", m["volume_type"].(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{ "root_block_device": &schema.Schema{
// TODO: This is a list because we don't support singleton // TODO: This is a set because we don't support singleton
// sub-resources today. We'll enforce that the list only ever has // sub-resources today. We'll enforce that the set only ever has
// length zero or one below. When TF gains support for // length zero or one below. When TF gains support for
// sub-resources this can be converted. // sub-resources this can be converted.
Type: schema.TypeList, Type: schema.TypeSet,
Optional: true, Optional: true,
Computed: true, Computed: true,
Elem: &schema.Resource{ Elem: &schema.Resource{
@ -226,6 +272,13 @@ func resourceAwsInstance() *schema.Resource {
Default: "/dev/sda1", Default: "/dev/sda1",
}, },
"iops": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
ForceNew: true,
},
"volume_size": &schema.Schema{ "volume_size": &schema.Schema{
Type: schema.TypeInt, Type: schema.TypeInt,
Optional: true, Optional: true,
@ -239,14 +292,18 @@ func resourceAwsInstance() *schema.Resource {
Computed: true, Computed: true,
ForceNew: true, ForceNew: true,
}, },
"iops": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Computed: true,
ForceNew: true,
}, },
}, },
Set: func(v interface{}) int {
var buf bytes.Buffer
m := v.(map[string]interface{})
buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool)))
buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string)))
// See the NOTE in "ebs_block_device" for why we skip iops here.
// buf.WriteString(fmt.Sprintf("%d-", m["iops"].(int)))
buf.WriteString(fmt.Sprintf("%d-", m["volume_size"].(int)))
buf.WriteString(fmt.Sprintf("%s-", m["volume_type"].(string)))
return hashcode.String(buf.String())
}, },
}, },
}, },
@ -347,44 +404,85 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error {
runOpts.KeyName = aws.String(v.(string)) runOpts.KeyName = aws.String(v.(string))
} }
blockDevices := make([]interface{}, 0) blockDevices := make([]ec2.BlockDeviceMapping, 0)
if v := d.Get("block_device"); v != nil { if v, ok := d.GetOk("ebs_block_device"); ok {
blockDevices = append(blockDevices, v.(*schema.Set).List()...) vL := v.(*schema.Set).List()
} for _, v := range vL {
if v := d.Get("root_block_device"); v != nil {
rootBlockDevices := v.([]interface{})
if len(rootBlockDevices) > 1 {
return fmt.Errorf("Cannot specify more than one root_block_device.")
}
blockDevices = append(blockDevices, rootBlockDevices...)
}
if len(blockDevices) > 0 {
runOpts.BlockDeviceMappings = make([]ec2.BlockDeviceMapping, len(blockDevices))
for i, v := range blockDevices {
bd := v.(map[string]interface{}) bd := v.(map[string]interface{})
runOpts.BlockDeviceMappings[i].DeviceName = aws.String(bd["device_name"].(string)) ebs := &ec2.EBSBlockDevice{
runOpts.BlockDeviceMappings[i].EBS = &ec2.EBSBlockDevice{
VolumeType: aws.String(bd["volume_type"].(string)),
VolumeSize: aws.Integer(bd["volume_size"].(int)),
DeleteOnTermination: aws.Boolean(bd["delete_on_termination"].(bool)), DeleteOnTermination: aws.Boolean(bd["delete_on_termination"].(bool)),
} }
if v, ok := bd["virtual_name"].(string); ok {
runOpts.BlockDeviceMappings[i].VirtualName = aws.String(v)
}
if v, ok := bd["snapshot_id"].(string); ok && v != "" { if v, ok := bd["snapshot_id"].(string); ok && v != "" {
runOpts.BlockDeviceMappings[i].EBS.SnapshotID = aws.String(v) ebs.SnapshotID = aws.String(v)
} }
if v, ok := bd["encrypted"].(bool); ok {
runOpts.BlockDeviceMappings[i].EBS.Encrypted = aws.Boolean(v) if v, ok := bd["volume_size"].(int); ok && v != 0 {
ebs.VolumeSize = aws.Integer(v)
} }
if v, ok := bd["volume_type"].(string); ok && v != "" {
ebs.VolumeType = aws.String(v)
}
if v, ok := bd["iops"].(int); ok && v > 0 { if v, ok := bd["iops"].(int); ok && v > 0 {
runOpts.BlockDeviceMappings[i].EBS.IOPS = aws.Integer(v) ebs.IOPS = aws.Integer(v)
}
blockDevices = append(blockDevices, ec2.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, ec2.BlockDeviceMapping{
DeviceName: aws.String(bd["device_name"].(string)),
VirtualName: aws.String(bd["virtual_name"].(string)),
})
}
// if err := d.Set("ephemeral_block_device", vL); err != nil {
// return err
// }
}
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 := &ec2.EBSBlockDevice{
DeleteOnTermination: aws.Boolean(bd["delete_on_termination"].(bool)),
}
if v, ok := bd["volume_size"].(int); ok && v != 0 {
ebs.VolumeSize = aws.Integer(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.Integer(v)
}
blockDevices = append(blockDevices, ec2.BlockDeviceMapping{
DeviceName: aws.String(bd["device_name"].(string)),
EBS: ebs,
})
}
}
if len(blockDevices) > 0 {
runOpts.BlockDeviceMappings = blockDevices
} }
// Create the instance // Create the instance
@ -518,50 +616,10 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
} }
d.Set("security_groups", sgs) d.Set("security_groups", sgs)
blockDevices := make(map[string]ec2.InstanceBlockDeviceMapping) if err := readBlockDevices(d, instance, ec2conn); err != nil {
for _, bd := range instance.BlockDeviceMappings {
blockDevices[*bd.EBS.VolumeID] = bd
}
volIDs := make([]string, 0, len(blockDevices))
for _, vol := range blockDevices {
volIDs = append(volIDs, *vol.EBS.VolumeID)
}
volResp, err := ec2conn.DescribeVolumes(&ec2.DescribeVolumesRequest{
VolumeIDs: volIDs,
})
if err != nil {
return err return err
} }
nonRootBlockDevices := make([]map[string]interface{}, 0)
rootBlockDevice := make([]interface{}, 0, 1)
for _, vol := range volResp.Volumes {
blockDevice := make(map[string]interface{})
blockDevice["device_name"] = *blockDevices[*vol.VolumeID].DeviceName
blockDevice["volume_type"] = *vol.VolumeType
blockDevice["volume_size"] = *vol.Size
if vol.IOPS != nil {
blockDevice["iops"] = *vol.IOPS
}
blockDevice["delete_on_termination"] =
*blockDevices[*vol.VolumeID].EBS.DeleteOnTermination
// If this is the root device, save it. We stop here since we
// can't put invalid keys into this map.
if blockDevice["device_name"] == *instance.RootDeviceName {
rootBlockDevice = []interface{}{blockDevice}
continue
}
blockDevice["snapshot_id"] = *vol.SnapshotID
blockDevice["encrypted"] = *vol.Encrypted
nonRootBlockDevices = append(nonRootBlockDevices, blockDevice)
}
d.Set("block_device", nonRootBlockDevices)
d.Set("root_block_device", rootBlockDevice)
return nil return nil
} }
@ -656,11 +714,89 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, instanceID string) resource.StateRe
} }
} }
func resourceAwsInstanceBlockDevicesHash(v interface{}) int { func readBlockDevices(d *schema.ResourceData, instance *ec2.Instance, ec2conn *ec2.EC2) error {
var buf bytes.Buffer ibds, err := readBlockDevicesFromInstance(instance, ec2conn)
m := v.(map[string]interface{}) if err != nil {
buf.WriteString(fmt.Sprintf("%s-", m["device_name"].(string))) return err
buf.WriteString(fmt.Sprintf("%s-", m["virtual_name"].(string))) }
buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool)))
return hashcode.String(buf.String()) if err := d.Set("ebs_block_device", ibds["ebs"]); err != nil {
return err
}
if ibds["root"] != nil {
if err := d.Set("root_block_device", []interface{}{ibds["root"]}); err != nil {
return err
}
}
return nil
}
func readBlockDevicesFromInstance(instance *ec2.Instance, ec2conn *ec2.EC2) (map[string]interface{}, error) {
blockDevices := make(map[string]interface{})
blockDevices["ebs"] = make([]map[string]interface{}, 0)
blockDevices["root"] = nil
instanceBlockDevices := make(map[string]ec2.InstanceBlockDeviceMapping)
for _, bd := range instance.BlockDeviceMappings {
if bd.EBS != nil {
instanceBlockDevices[*(bd.EBS.VolumeID)] = bd
}
}
volIDs := make([]string, 0, len(instanceBlockDevices))
for volID := range instanceBlockDevices {
volIDs = append(volIDs, volID)
}
// Need to call DescribeVolumes to get volume_size and volume_type for each
// EBS block device
volResp, err := ec2conn.DescribeVolumes(&ec2.DescribeVolumesRequest{
VolumeIDs: volIDs,
})
if err != nil {
return nil, err
}
for _, vol := range volResp.Volumes {
instanceBd := instanceBlockDevices[*vol.VolumeID]
bd := make(map[string]interface{})
if instanceBd.EBS != nil && instanceBd.EBS.DeleteOnTermination != nil {
bd["delete_on_termination"] = *instanceBd.EBS.DeleteOnTermination
}
if instanceBd.DeviceName != nil {
bd["device_name"] = *instanceBd.DeviceName
}
if vol.Size != nil {
bd["volume_size"] = *vol.Size
}
if vol.VolumeType != nil {
bd["volume_type"] = *vol.VolumeType
}
if vol.IOPS != nil {
bd["iops"] = *vol.IOPS
}
if blockDeviceIsRoot(instanceBd, instance) {
blockDevices["root"] = bd
} else {
if vol.Encrypted != nil {
bd["encrypted"] = *vol.Encrypted
}
if vol.SnapshotID != nil {
bd["snapshot_id"] = *vol.SnapshotID
}
blockDevices["ebs"] = append(blockDevices["ebs"].([]map[string]interface{}), bd)
}
}
return blockDevices, nil
}
func blockDeviceIsRoot(bd ec2.InstanceBlockDeviceMapping, instance *ec2.Instance) bool {
return (bd.DeviceName != nil &&
instance.RootDeviceName != nil &&
*bd.DeviceName == *instance.RootDeviceName)
} }

View File

@ -0,0 +1,107 @@
package aws
import (
"fmt"
"log"
"strconv"
"strings"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/terraform"
)
func resourceAwsInstanceMigrateState(
v int, is *terraform.InstanceState, meta interface{}) (*terraform.InstanceState, error) {
switch v {
case 0:
log.Println("[INFO] Found AWS Instance State v0; migrating to v1")
return migrateStateV0toV1(is)
default:
return is, fmt.Errorf("Unexpected schema version: %d", v)
}
return is, nil
}
func migrateStateV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) {
log.Printf("[DEBUG] Attributes before migration: %#v", is.Attributes)
// Delete old count
delete(is.Attributes, "block_device.#")
oldBds, err := readV0BlockDevices(is)
if err != nil {
return is, err
}
// seed count fields for new types
is.Attributes["ebs_block_device.#"] = "0"
is.Attributes["ephemeral_block_device.#"] = "0"
// depending on if state was v0.3.7 or an earlier version, it might have
// root_block_device defined already
if _, ok := is.Attributes["root_block_device.#"]; !ok {
is.Attributes["root_block_device.#"] = "0"
}
for _, oldBd := range oldBds {
if err := writeV1BlockDevice(is, oldBd); err != nil {
return is, err
}
}
log.Printf("[DEBUG] Attributes after migration: %#v", is.Attributes)
return is, nil
}
func readV0BlockDevices(is *terraform.InstanceState) (map[string]map[string]string, error) {
oldBds := make(map[string]map[string]string)
for k, v := range is.Attributes {
if !strings.HasPrefix(k, "block_device.") {
continue
}
path := strings.Split(k, ".")
if len(path) != 3 {
return oldBds, fmt.Errorf("Found unexpected block_device field: %#v", k)
}
hashcode, attribute := path[1], path[2]
oldBd, ok := oldBds[hashcode]
if !ok {
oldBd = make(map[string]string)
oldBds[hashcode] = oldBd
}
oldBd[attribute] = v
delete(is.Attributes, k)
}
return oldBds, nil
}
func writeV1BlockDevice(
is *terraform.InstanceState, oldBd map[string]string) error {
code := hashcode.String(oldBd["device_name"])
bdType := "ebs_block_device"
if vn, ok := oldBd["virtual_name"]; ok && strings.HasPrefix(vn, "ephemeral") {
bdType = "ephemeral_block_device"
} else if dn, ok := oldBd["device_name"]; ok && dn == "/dev/sda1" {
bdType = "root_block_device"
}
switch bdType {
case "ebs_block_device":
delete(oldBd, "virtual_name")
case "root_block_device":
delete(oldBd, "virtual_name")
delete(oldBd, "encrypted")
delete(oldBd, "snapshot_id")
case "ephemeral_block_device":
delete(oldBd, "delete_on_termination")
delete(oldBd, "encrypted")
delete(oldBd, "iops")
delete(oldBd, "volume_size")
delete(oldBd, "volume_type")
}
for attr, val := range oldBd {
attrKey := fmt.Sprintf("%s.%d.%s", bdType, code, attr)
is.Attributes[attrKey] = val
}
countAttr := fmt.Sprintf("%s.#", bdType)
count, _ := strconv.Atoi(is.Attributes[countAttr])
is.Attributes[countAttr] = strconv.Itoa(count + 1)
return nil
}

View File

@ -0,0 +1,135 @@
package aws
import (
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestAWSInstanceMigrateState(t *testing.T) {
cases := map[string]struct {
StateVersion int
Attributes map[string]string
Expected map[string]string
Meta interface{}
}{
"v0.3.6 and earlier": {
StateVersion: 0,
Attributes: map[string]string{
// EBS
"block_device.#": "2",
"block_device.3851383343.delete_on_termination": "true",
"block_device.3851383343.device_name": "/dev/sdx",
"block_device.3851383343.encrypted": "false",
"block_device.3851383343.snapshot_id": "",
"block_device.3851383343.virtual_name": "",
"block_device.3851383343.volume_size": "5",
"block_device.3851383343.volume_type": "standard",
// Ephemeral
"block_device.3101711606.delete_on_termination": "false",
"block_device.3101711606.device_name": "/dev/sdy",
"block_device.3101711606.encrypted": "false",
"block_device.3101711606.snapshot_id": "",
"block_device.3101711606.virtual_name": "ephemeral0",
"block_device.3101711606.volume_size": "",
"block_device.3101711606.volume_type": "",
// Root
"block_device.56575650.delete_on_termination": "true",
"block_device.56575650.device_name": "/dev/sda1",
"block_device.56575650.encrypted": "false",
"block_device.56575650.snapshot_id": "",
"block_device.56575650.volume_size": "10",
"block_device.56575650.volume_type": "standard",
},
Expected: map[string]string{
"ebs_block_device.#": "1",
"ebs_block_device.3851383343.delete_on_termination": "true",
"ebs_block_device.3851383343.device_name": "/dev/sdx",
"ebs_block_device.3851383343.encrypted": "false",
"ebs_block_device.3851383343.snapshot_id": "",
"ebs_block_device.3851383343.volume_size": "5",
"ebs_block_device.3851383343.volume_type": "standard",
"ephemeral_block_device.#": "1",
"ephemeral_block_device.2458403513.device_name": "/dev/sdy",
"ephemeral_block_device.2458403513.virtual_name": "ephemeral0",
"root_block_device.#": "1",
"root_block_device.3018388612.delete_on_termination": "true",
"root_block_device.3018388612.device_name": "/dev/sda1",
"root_block_device.3018388612.snapshot_id": "",
"root_block_device.3018388612.volume_size": "10",
"root_block_device.3018388612.volume_type": "standard",
},
},
"v0.3.7": {
StateVersion: 0,
Attributes: map[string]string{
// EBS
"block_device.#": "2",
"block_device.3851383343.delete_on_termination": "true",
"block_device.3851383343.device_name": "/dev/sdx",
"block_device.3851383343.encrypted": "false",
"block_device.3851383343.snapshot_id": "",
"block_device.3851383343.virtual_name": "",
"block_device.3851383343.volume_size": "5",
"block_device.3851383343.volume_type": "standard",
"block_device.3851383343.iops": "",
// Ephemeral
"block_device.3101711606.delete_on_termination": "false",
"block_device.3101711606.device_name": "/dev/sdy",
"block_device.3101711606.encrypted": "false",
"block_device.3101711606.snapshot_id": "",
"block_device.3101711606.virtual_name": "ephemeral0",
"block_device.3101711606.volume_size": "",
"block_device.3101711606.volume_type": "",
"block_device.3101711606.iops": "",
// Root
"root_block_device.#": "1",
"root_block_device.3018388612.delete_on_termination": "true",
"root_block_device.3018388612.device_name": "/dev/sda1",
"root_block_device.3018388612.snapshot_id": "",
"root_block_device.3018388612.volume_size": "10",
"root_block_device.3018388612.volume_type": "io1",
"root_block_device.3018388612.iops": "1000",
},
Expected: map[string]string{
"ebs_block_device.#": "1",
"ebs_block_device.3851383343.delete_on_termination": "true",
"ebs_block_device.3851383343.device_name": "/dev/sdx",
"ebs_block_device.3851383343.encrypted": "false",
"ebs_block_device.3851383343.snapshot_id": "",
"ebs_block_device.3851383343.volume_size": "5",
"ebs_block_device.3851383343.volume_type": "standard",
"ephemeral_block_device.#": "1",
"ephemeral_block_device.2458403513.device_name": "/dev/sdy",
"ephemeral_block_device.2458403513.virtual_name": "ephemeral0",
"root_block_device.#": "1",
"root_block_device.3018388612.delete_on_termination": "true",
"root_block_device.3018388612.device_name": "/dev/sda1",
"root_block_device.3018388612.snapshot_id": "",
"root_block_device.3018388612.volume_size": "10",
"root_block_device.3018388612.volume_type": "io1",
"root_block_device.3018388612.iops": "1000",
},
},
}
for tn, tc := range cases {
is := &terraform.InstanceState{
Attributes: tc.Attributes,
}
is, err := resourceAwsInstanceMigrateState(
tc.StateVersion, is, tc.Meta)
if err != nil {
t.Fatalf("bad: %s, err: %#v", tn, err)
}
for k, v := range tc.Expected {
if is.Attributes[k] != v {
t.Fatalf(
"bad: %s\n\n expected: %#v -> %#v\n got: %#v -> %#v\n in: %#v",
tn, k, v, k, is.Attributes[k], is.Attributes)
}
}
}
}

View File

@ -111,31 +111,33 @@ func TestAccAWSInstance_blockDevices(t *testing.T) {
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "root_block_device.#", "1"), "aws_instance.foo", "root_block_device.#", "1"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "root_block_device.0.device_name", "/dev/sda1"), "aws_instance.foo", "root_block_device.3018388612.device_name", "/dev/sda1"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "root_block_device.0.volume_size", "11"), "aws_instance.foo", "root_block_device.3018388612.volume_size", "11"),
// this one is important because it's the only root_block_device
// attribute that comes back from the API. so checking it verifies
// that we set state properly
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "root_block_device.0.volume_type", "gp2"), "aws_instance.foo", "root_block_device.3018388612.volume_type", "gp2"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.#", "2"), "aws_instance.foo", "ebs_block_device.#", "2"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.172787947.device_name", "/dev/sdb"), "aws_instance.foo", "ebs_block_device.418220885.device_name", "/dev/sdb"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.172787947.volume_size", "9"), "aws_instance.foo", "ebs_block_device.418220885.volume_size", "9"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.172787947.iops", "0"), "aws_instance.foo", "ebs_block_device.418220885.volume_type", "standard"),
// Check provisioned SSD device
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.3336996981.volume_type", "io1"), "aws_instance.foo", "ebs_block_device.1877654467.device_name", "/dev/sdc"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.3336996981.device_name", "/dev/sdc"), "aws_instance.foo", "ebs_block_device.1877654467.volume_size", "10"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.3336996981.volume_size", "10"), "aws_instance.foo", "ebs_block_device.1877654467.volume_type", "io1"),
resource.TestCheckResourceAttr( resource.TestCheckResourceAttr(
"aws_instance.foo", "block_device.3336996981.iops", "100"), "aws_instance.foo", "ebs_block_device.1877654467.iops", "100"),
resource.TestCheckResourceAttr(
"aws_instance.foo", "ephemeral_block_device.#", "1"),
resource.TestCheckResourceAttr(
"aws_instance.foo", "ephemeral_block_device.2087552357.device_name", "/dev/sde"),
resource.TestCheckResourceAttr(
"aws_instance.foo", "ephemeral_block_device.2087552357.virtual_name", "ephemeral0"),
testCheck(), testCheck(),
), ),
}, },
@ -420,21 +422,26 @@ resource "aws_instance" "foo" {
# us-west-2 # us-west-2
ami = "ami-55a7ea65" ami = "ami-55a7ea65"
instance_type = "m1.small" instance_type = "m1.small"
root_block_device { root_block_device {
device_name = "/dev/sda1" device_name = "/dev/sda1"
volume_type = "gp2" volume_type = "gp2"
volume_size = 11 volume_size = 11
} }
block_device { ebs_block_device {
device_name = "/dev/sdb" device_name = "/dev/sdb"
volume_size = 9 volume_size = 9
} }
block_device { ebs_block_device {
device_name = "/dev/sdc" device_name = "/dev/sdc"
volume_size = 10 volume_size = 10
volume_type = "io1" volume_type = "io1"
iops = 100 iops = 100
} }
ephemeral_block_device {
device_name = "/dev/sde"
virtual_name = "ephemeral0"
}
} }
` `

View File

@ -14,7 +14,8 @@ and deleted. Instances also support [provisioning](/docs/provisioners/index.html
## Example Usage ## Example Usage
``` ```
# Create a new instance of the ami-1234 on an m1.small node with an AWS Tag naming it "HelloWorld" # Create a new instance of the ami-1234 on an m1.small node
# with an AWS Tag naming it "HelloWorld"
resource "aws_instance" "web" { resource "aws_instance" "web" {
ami = "ami-1234" ami = "ami-1234"
instance_type = "m1.small" instance_type = "m1.small"
@ -47,32 +48,79 @@ The following arguments are supported:
* `iam_instance_profile` - (Optional) The IAM Instance Profile to * `iam_instance_profile` - (Optional) The IAM Instance Profile to
launch the instance with. launch the instance with.
* `tags` - (Optional) A mapping of tags to assign to the resource. * `tags` - (Optional) A mapping of tags to assign to the resource.
* `block_device` - (Optional) A list of block devices to add. Their keys are documented below.
* `root_block_device` - (Optional) Customize details about the root block * `root_block_device` - (Optional) Customize details about the root block
device of the instance. Available keys are documented below. 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.
Each `block_device` supports the following:
* `device_name` - The name of the device to mount. <a id="block-devices"></a>
* `virtual_name` - (Optional) The virtual device name. ## Block devices
* `snapshot_id` - (Optional) The Snapshot ID to mount.
* `volume_type` - (Optional) The type of volume. Can be standard, gp2, or io1. Defaults to standard. Each of the `*_block_device` attributes controls a portion of the AWS
* `volume_size` - (Optional) The size of the volume in gigabytes. Instance's "Block Device Mapping". It's a good idea to familiarize yourself with [AWS's Block Device
* `iops` - (Optional) The amount of provisioned IOPS. Setting this implies a Mapping docs](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html)
volume_type of "io1". to understand the implications of using these attributes.
* `delete_on_termination` - (Optional) Should the volume be destroyed on instance termination (defaults true).
* `encrypted` - (Optional) Should encryption be enabled (defaults false).
The `root_block_device` mapping supports the following: The `root_block_device` mapping supports the following:
* `device_name` - The name of the root device on the target instance. Must * `device_name` - The name of the root device on the target instance. Must
match the root device as defined in the AMI. Defaults to "/dev/sda1", which match the root device as defined in the AMI. Defaults to `"/dev/sda1"`, which
is the typical root volume for Linux instances. is the typical root volume for Linux instances.
* `volume_type` - (Optional) The type of volume. Can be standard, gp2, or io1. Defaults to standard. * `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. * `volume_size` - (Optional) The size of the volume in gigabytes.
* `iops` - (Optional) The amount of provisioned IOPS. Setting this implies a * `iops` - (Optional) The amount of provisioned
volume_type of "io1". [IOPS](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-io-characteristics.html).
* `delete_on_termination` - (Optional) Should the volume be destroyed on instance termination (defaults true). 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`).
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:** Because AWS [does not expose Instance Store mapping
details](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html#bdm-instance-metadata)
via an externally accessible API, `ephemeral_block_device` configuration may
only be applied at instance creation time, and changes to configuration of
existing resources cannot be detected by Terraform. Updates to Instance Store
block device configuration can be manually triggered by using the [`taint`
command](/docs/commands/taint.html).
## Attributes Reference ## Attributes Reference