mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-30 10:47:14 -06:00
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:
parent
3ba8ed536b
commit
2b23c402ee
@ -24,6 +24,9 @@ func resourceAwsInstance() *schema.Resource {
|
||||
Update: resourceAwsInstanceUpdate,
|
||||
Delete: resourceAwsInstanceDelete,
|
||||
|
||||
SchemaVersion: 1,
|
||||
MigrateState: resourceAwsInstanceMigrateState,
|
||||
|
||||
Schema: map[string]*schema.Schema{
|
||||
"ami": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
@ -127,53 +130,28 @@ func resourceAwsInstance() *schema.Resource {
|
||||
ForceNew: true,
|
||||
Optional: true,
|
||||
},
|
||||
|
||||
"tenancy": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
|
||||
"tags": tagsSchema(),
|
||||
|
||||
"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,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Elem: &schema.Resource{
|
||||
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{
|
||||
Type: schema.TypeBool,
|
||||
Optional: true,
|
||||
@ -181,6 +159,12 @@ func resourceAwsInstance() *schema.Resource {
|
||||
ForceNew: true,
|
||||
},
|
||||
|
||||
"device_name": &schema.Schema{
|
||||
Type: schema.TypeString,
|
||||
Required: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
|
||||
"encrypted": &schema.Schema{
|
||||
Type: schema.TypeBool,
|
||||
Optional: true,
|
||||
@ -194,17 +178,79 @@ func resourceAwsInstance() *schema.Resource {
|
||||
Computed: 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{
|
||||
// TODO: This is a list because we don't support singleton
|
||||
// sub-resources today. We'll enforce that the list only ever has
|
||||
// TODO: This is a set because we don't support singleton
|
||||
// sub-resources today. We'll enforce that the set only ever has
|
||||
// length zero or one below. When TF gains support for
|
||||
// sub-resources this can be converted.
|
||||
Type: schema.TypeList,
|
||||
Type: schema.TypeSet,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Elem: &schema.Resource{
|
||||
@ -226,6 +272,13 @@ func resourceAwsInstance() *schema.Resource {
|
||||
Default: "/dev/sda1",
|
||||
},
|
||||
|
||||
"iops": &schema.Schema{
|
||||
Type: schema.TypeInt,
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
ForceNew: true,
|
||||
},
|
||||
|
||||
"volume_size": &schema.Schema{
|
||||
Type: schema.TypeInt,
|
||||
Optional: true,
|
||||
@ -239,15 +292,19 @@ func resourceAwsInstance() *schema.Resource {
|
||||
Computed: 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,46 +404,87 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error {
|
||||
runOpts.KeyName = aws.String(v.(string))
|
||||
}
|
||||
|
||||
blockDevices := make([]interface{}, 0)
|
||||
blockDevices := make([]ec2.BlockDeviceMapping, 0)
|
||||
|
||||
if v := d.Get("block_device"); v != nil {
|
||||
blockDevices = append(blockDevices, v.(*schema.Set).List()...)
|
||||
}
|
||||
|
||||
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 {
|
||||
if v, ok := d.GetOk("ebs_block_device"); ok {
|
||||
vL := v.(*schema.Set).List()
|
||||
for _, v := range vL {
|
||||
bd := v.(map[string]interface{})
|
||||
runOpts.BlockDeviceMappings[i].DeviceName = aws.String(bd["device_name"].(string))
|
||||
runOpts.BlockDeviceMappings[i].EBS = &ec2.EBSBlockDevice{
|
||||
VolumeType: aws.String(bd["volume_type"].(string)),
|
||||
VolumeSize: aws.Integer(bd["volume_size"].(int)),
|
||||
ebs := &ec2.EBSBlockDevice{
|
||||
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 != "" {
|
||||
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 {
|
||||
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
|
||||
log.Printf("[DEBUG] Run configuration: %#v", runOpts)
|
||||
runResp, err := ec2conn.RunInstances(runOpts)
|
||||
@ -518,50 +616,10 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
|
||||
}
|
||||
d.Set("security_groups", sgs)
|
||||
|
||||
blockDevices := make(map[string]ec2.InstanceBlockDeviceMapping)
|
||||
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 {
|
||||
if err := readBlockDevices(d, instance, ec2conn); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
@ -656,11 +714,89 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, instanceID string) resource.StateRe
|
||||
}
|
||||
}
|
||||
|
||||
func resourceAwsInstanceBlockDevicesHash(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)))
|
||||
buf.WriteString(fmt.Sprintf("%t-", m["delete_on_termination"].(bool)))
|
||||
return hashcode.String(buf.String())
|
||||
func readBlockDevices(d *schema.ResourceData, instance *ec2.Instance, ec2conn *ec2.EC2) error {
|
||||
ibds, err := readBlockDevicesFromInstance(instance, ec2conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
107
builtin/providers/aws/resource_aws_instance_migrate.go
Normal file
107
builtin/providers/aws/resource_aws_instance_migrate.go
Normal 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
|
||||
}
|
135
builtin/providers/aws/resource_aws_instance_migrate_test.go
Normal file
135
builtin/providers/aws/resource_aws_instance_migrate_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -111,31 +111,33 @@ func TestAccAWSInstance_blockDevices(t *testing.T) {
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_instance.foo", "root_block_device.#", "1"),
|
||||
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(
|
||||
"aws_instance.foo", "root_block_device.0.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
|
||||
"aws_instance.foo", "root_block_device.3018388612.volume_size", "11"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_instance.foo", "root_block_device.0.volume_type", "gp2"),
|
||||
"aws_instance.foo", "root_block_device.3018388612.volume_type", "gp2"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_instance.foo", "block_device.#", "2"),
|
||||
"aws_instance.foo", "ebs_block_device.#", "2"),
|
||||
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(
|
||||
"aws_instance.foo", "block_device.172787947.volume_size", "9"),
|
||||
"aws_instance.foo", "ebs_block_device.418220885.volume_size", "9"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_instance.foo", "block_device.172787947.iops", "0"),
|
||||
// Check provisioned SSD device
|
||||
"aws_instance.foo", "ebs_block_device.418220885.volume_type", "standard"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_instance.foo", "block_device.3336996981.volume_type", "io1"),
|
||||
"aws_instance.foo", "ebs_block_device.1877654467.device_name", "/dev/sdc"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_instance.foo", "block_device.3336996981.device_name", "/dev/sdc"),
|
||||
"aws_instance.foo", "ebs_block_device.1877654467.volume_size", "10"),
|
||||
resource.TestCheckResourceAttr(
|
||||
"aws_instance.foo", "block_device.3336996981.volume_size", "10"),
|
||||
"aws_instance.foo", "ebs_block_device.1877654467.volume_type", "io1"),
|
||||
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(),
|
||||
),
|
||||
},
|
||||
@ -420,21 +422,26 @@ resource "aws_instance" "foo" {
|
||||
# us-west-2
|
||||
ami = "ami-55a7ea65"
|
||||
instance_type = "m1.small"
|
||||
|
||||
root_block_device {
|
||||
device_name = "/dev/sda1"
|
||||
volume_type = "gp2"
|
||||
volume_size = 11
|
||||
}
|
||||
block_device {
|
||||
ebs_block_device {
|
||||
device_name = "/dev/sdb"
|
||||
volume_size = 9
|
||||
}
|
||||
block_device {
|
||||
ebs_block_device {
|
||||
device_name = "/dev/sdc"
|
||||
volume_size = 10
|
||||
volume_type = "io1"
|
||||
iops = 100
|
||||
}
|
||||
ephemeral_block_device {
|
||||
device_name = "/dev/sde"
|
||||
virtual_name = "ephemeral0"
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
|
@ -14,7 +14,8 @@ and deleted. Instances also support [provisioning](/docs/provisioners/index.html
|
||||
## 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" {
|
||||
ami = "ami-1234"
|
||||
instance_type = "m1.small"
|
||||
@ -47,32 +48,79 @@ The following arguments are supported:
|
||||
* `iam_instance_profile` - (Optional) The IAM Instance Profile to
|
||||
launch the instance with.
|
||||
* `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
|
||||
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.
|
||||
* `virtual_name` - (Optional) The virtual device name.
|
||||
* `snapshot_id` - (Optional) The Snapshot ID to mount.
|
||||
* `volume_type` - (Optional) The type of volume. Can be standard, gp2, or io1. Defaults to standard.
|
||||
* `volume_size` - (Optional) The size of the volume in gigabytes.
|
||||
* `iops` - (Optional) The amount of provisioned IOPS. Setting this implies a
|
||||
volume_type of "io1".
|
||||
* `delete_on_termination` - (Optional) Should the volume be destroyed on instance termination (defaults true).
|
||||
* `encrypted` - (Optional) Should encryption be enabled (defaults false).
|
||||
<a id="block-devices"></a>
|
||||
## Block devices
|
||||
|
||||
Each of the `*_block_device` attributes controls a portion of the AWS
|
||||
Instance's "Block Device Mapping". It's a good idea to familiarize yourself with [AWS's Block Device
|
||||
Mapping docs](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/block-device-mapping-concepts.html)
|
||||
to understand the implications of using these attributes.
|
||||
|
||||
The `root_block_device` mapping supports the following:
|
||||
|
||||
* `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.
|
||||
* `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.
|
||||
* `iops` - (Optional) The amount of provisioned IOPS. Setting this implies a
|
||||
volume_type of "io1".
|
||||
* `delete_on_termination` - (Optional) Should the volume be destroyed on instance termination (defaults true).
|
||||
* `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`).
|
||||
|
||||
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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user