Three resources for AWS AMIs.

AWS provides three different ways to create AMIs that each have different
inputs, but once they are complete the same management operations apply.

Thus these three resources each have a different "Create" implementation
but then share the same "Read", "Update" and "Delete" implementations.
This commit is contained in:
Martin Atkins 2015-08-29 21:35:45 -07:00
parent 7d142134f2
commit 7f64327663
11 changed files with 1236 additions and 0 deletions

View File

@ -156,6 +156,9 @@ func Provider() terraform.ResourceProvider {
},
ResourcesMap: map[string]*schema.Resource{
"aws_ami": resourceAwsAmi(),
"aws_ami_copy": resourceAwsAmiCopy(),
"aws_ami_from_instance": resourceAwsAmiFromInstance(),
"aws_app_cookie_stickiness_policy": resourceAwsAppCookieStickinessPolicy(),
"aws_autoscaling_group": resourceAwsAutoscalingGroup(),
"aws_autoscaling_notification": resourceAwsAutoscalingNotification(),

View File

@ -0,0 +1,521 @@
package aws
import (
"bytes"
"errors"
"fmt"
"log"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsAmi() *schema.Resource {
// Our schema is shared also with aws_ami_copy and aws_ami_from_instance
resourceSchema := resourceAwsAmiCommonSchema(false)
return &schema.Resource{
Create: resourceAwsAmiCreate,
Schema: resourceSchema,
// The Read, Update and Delete operations are shared with aws_ami_copy
// and aws_ami_from_instance, since they differ only in how the image
// is created.
Read: resourceAwsAmiRead,
Update: resourceAwsAmiUpdate,
Delete: resourceAwsAmiDelete,
}
}
func resourceAwsAmiCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*AWSClient).ec2conn
req := &ec2.RegisterImageInput{
Name: aws.String(d.Get("name").(string)),
Description: aws.String(d.Get("description").(string)),
Architecture: aws.String(d.Get("architecture").(string)),
ImageLocation: aws.String(d.Get("image_location").(string)),
RootDeviceName: aws.String(d.Get("root_device_name").(string)),
SriovNetSupport: aws.String(d.Get("sriov_net_support").(string)),
VirtualizationType: aws.String(d.Get("virtualization_type").(string)),
}
if kernelId := d.Get("kernel_id").(string); kernelId != "" {
req.KernelId = aws.String(kernelId)
}
if ramdiskId := d.Get("ramdisk_id").(string); ramdiskId != "" {
req.RamdiskId = aws.String(ramdiskId)
}
ebsBlockDevsSet := d.Get("ebs_block_device").(*schema.Set)
ephemeralBlockDevsSet := d.Get("ephemeral_block_device").(*schema.Set)
for _, ebsBlockDevI := range ebsBlockDevsSet.List() {
ebsBlockDev := ebsBlockDevI.(map[string]interface{})
blockDev := &ec2.BlockDeviceMapping{
DeviceName: aws.String(ebsBlockDev["device_name"].(string)),
Ebs: &ec2.EbsBlockDevice{
DeleteOnTermination: aws.Bool(ebsBlockDev["delete_on_termination"].(bool)),
VolumeSize: aws.Int64(int64(ebsBlockDev["volume_size"].(int))),
VolumeType: aws.String(ebsBlockDev["volume_type"].(string)),
},
}
if iops := ebsBlockDev["iops"].(int); iops != 0 {
blockDev.Ebs.Iops = aws.Int64(int64(iops))
}
encrypted := ebsBlockDev["encrypted"].(bool)
if snapshotId := ebsBlockDev["snapshot_id"].(string); snapshotId != "" {
blockDev.Ebs.SnapshotId = aws.String(snapshotId)
if encrypted {
return errors.New("can't set both 'snapshot_id' and 'encrypted'")
}
} else if encrypted {
blockDev.Ebs.Encrypted = aws.Bool(true)
}
req.BlockDeviceMappings = append(req.BlockDeviceMappings, blockDev)
}
for _, ephemeralBlockDevI := range ephemeralBlockDevsSet.List() {
ephemeralBlockDev := ephemeralBlockDevI.(map[string]interface{})
blockDev := &ec2.BlockDeviceMapping{
DeviceName: aws.String(ephemeralBlockDev["device_name"].(string)),
VirtualName: aws.String(ephemeralBlockDev["virtual_name"].(string)),
}
req.BlockDeviceMappings = append(req.BlockDeviceMappings, blockDev)
}
res, err := client.RegisterImage(req)
if err != nil {
return err
}
id := *res.ImageId
d.SetId(id)
d.Partial(true) // make sure we record the id even if the rest of this gets interrupted
d.Set("id", id)
d.Set("manage_ebs_block_devices", false)
d.SetPartial("id")
d.SetPartial("manage_ebs_block_devices")
d.Partial(false)
_, err = resourceAwsAmiWaitForAvailable(id, client)
if err != nil {
return err
}
return resourceAwsAmiUpdate(d, meta)
}
func resourceAwsAmiRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*AWSClient).ec2conn
id := d.Id()
req := &ec2.DescribeImagesInput{
ImageIds: []*string{aws.String(id)},
}
res, err := client.DescribeImages(req)
if err != nil {
return err
}
if len(res.Images) != 1 {
d.SetId("")
return nil
}
image := res.Images[0]
state := *(image.State)
if state == "pending" {
// This could happen if a user manually adds an image we didn't create
// to the state. We'll wait for the image to become available
// before we continue. We should never take this branch in normal
// circumstances since we would've waited for availability during
// the "Create" step.
image, err = resourceAwsAmiWaitForAvailable(id, client)
if err != nil {
return err
}
state = *(image.State)
}
if state == "deregistered" {
d.SetId("")
return nil
}
if state != "available" {
return fmt.Errorf("AMI has become %s", state)
}
d.Set("name", image.Name)
d.Set("description", image.Description)
d.Set("image_location", image.ImageLocation)
d.Set("architecture", image.Architecture)
d.Set("kernel_id", image.KernelId)
d.Set("ramdisk_id", image.RamdiskId)
d.Set("root_device_name", image.RootDeviceName)
d.Set("sriov_net_support", image.SriovNetSupport)
d.Set("virtualization_type", image.VirtualizationType)
var ebsBlockDevs []map[string]interface{}
var ephemeralBlockDevs []map[string]interface{}
for _, blockDev := range image.BlockDeviceMappings {
if blockDev.Ebs != nil {
ebsBlockDev := map[string]interface{}{
"device_name": *(blockDev.DeviceName),
"delete_on_termination": *(blockDev.Ebs.DeleteOnTermination),
"encrypted": *(blockDev.Ebs.Encrypted),
"iops": 0,
"snapshot_id": *(blockDev.Ebs.SnapshotId),
"volume_size": int(*(blockDev.Ebs.VolumeSize)),
"volume_type": *(blockDev.Ebs.VolumeType),
}
if blockDev.Ebs.Iops != nil {
ebsBlockDev["iops"] = int(*(blockDev.Ebs.Iops))
}
ebsBlockDevs = append(ebsBlockDevs, ebsBlockDev)
} else {
ephemeralBlockDevs = append(ephemeralBlockDevs, map[string]interface{}{
"device_name": *(blockDev.DeviceName),
"virtual_name": *(blockDev.VirtualName),
})
}
}
d.Set("ebs_block_device", ebsBlockDevs)
d.Set("ephemeral_block_device", ephemeralBlockDevs)
d.Set("tags", tagsToMap(image.Tags))
return nil
}
func resourceAwsAmiUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*AWSClient).ec2conn
d.Partial(true)
if err := setTags(client, d); err != nil {
return err
} else {
d.SetPartial("tags")
}
if d.Get("description").(string) != "" {
_, err := client.ModifyImageAttribute(&ec2.ModifyImageAttributeInput{
ImageId: aws.String(d.Id()),
Description: &ec2.AttributeValue{
Value: aws.String(d.Get("description").(string)),
},
})
if err != nil {
return err
}
d.SetPartial("description")
}
d.Partial(false)
return resourceAwsAmiRead(d, meta)
}
func resourceAwsAmiDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*AWSClient).ec2conn
req := &ec2.DeregisterImageInput{
ImageId: aws.String(d.Id()),
}
_, err := client.DeregisterImage(req)
if err != nil {
return err
}
// If we're managing the EBS snapshots then we need to delete those too.
if d.Get("manage_ebs_snapshots").(bool) {
errs := map[string]error{}
ebsBlockDevsSet := d.Get("ebs_block_device").(*schema.Set)
req := &ec2.DeleteSnapshotInput{}
for _, ebsBlockDevI := range ebsBlockDevsSet.List() {
ebsBlockDev := ebsBlockDevI.(map[string]interface{})
snapshotId := ebsBlockDev["snapshot_id"].(string)
if snapshotId != "" {
req.SnapshotId = aws.String(snapshotId)
_, err := client.DeleteSnapshot(req)
if err != nil {
errs[snapshotId] = err
}
}
}
if len(errs) > 0 {
errParts := []string{"Errors while deleting associated EBS snapshots:"}
for snapshotId, err := range errs {
errParts = append(errParts, fmt.Sprintf("%s: %s", snapshotId, err))
}
errParts = append(errParts, "These are no longer managed by Terraform and must be deleted manually.")
return errors.New(strings.Join(errParts, "\n"))
}
}
d.SetId("")
return nil
}
func resourceAwsAmiWaitForAvailable(id string, client *ec2.EC2) (*ec2.Image, error) {
log.Printf("Waiting for AMI %s to become available...", id)
req := &ec2.DescribeImagesInput{
ImageIds: []*string{aws.String(id)},
}
pollsWhereNotFound := 0
for {
res, err := client.DescribeImages(req)
if err != nil {
// When using RegisterImage (for aws_ami) the AMI sometimes isn't available at all
// right after the API responds, so we need to tolerate a couple Not Found errors
// before an available AMI shows up.
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidAMIID.NotFound" {
pollsWhereNotFound++
// We arbitrarily stop polling after getting a "not found" error five times,
// assuming that the AMI has been deleted by something other than Terraform.
if pollsWhereNotFound > 5 {
return nil, fmt.Errorf("gave up waiting for AMI to be created: %s", err)
}
time.Sleep(4 * time.Second)
continue
}
return nil, fmt.Errorf("error reading AMI: %s", err)
}
if len(res.Images) != 1 {
return nil, fmt.Errorf("new AMI vanished while pending")
}
state := *(res.Images[0].State)
if state == "pending" {
// Give it a few seconds before we poll again.
time.Sleep(4 * time.Second)
continue
}
if state == "available" {
// We're done!
return res.Images[0], nil
}
// If we're not pending or available then we're in one of the invalid/error
// states, so stop polling and bail out.
stateReason := *(res.Images[0].StateReason)
return nil, fmt.Errorf("new AMI became %s while pending: %s", state, stateReason)
}
}
func resourceAwsAmiCommonSchema(computed bool) map[string]*schema.Schema {
// The "computed" parameter controls whether we're making
// a schema for an AMI that's been implicitly registered (aws_ami_copy, aws_ami_from_instance)
// or whether we're making a schema for an explicit registration (aws_ami).
// When set, almost every attribute is marked as "computed".
// When not set, only the "id" attribute is computed.
// "name" and "description" are never computed, since they must always
// be provided by the user.
var virtualizationTypeDefault interface{}
var deleteEbsOnTerminationDefault interface{}
var sriovNetSupportDefault interface{}
var architectureDefault interface{}
var volumeTypeDefault interface{}
if !computed {
virtualizationTypeDefault = "paravirtual"
deleteEbsOnTerminationDefault = true
sriovNetSupportDefault = "simple"
architectureDefault = "x86_64"
volumeTypeDefault = "standard"
}
return map[string]*schema.Schema{
"id": &schema.Schema{
Type: schema.TypeString,
Computed: true,
},
"image_location": &schema.Schema{
Type: schema.TypeString,
Optional: !computed,
Computed: true,
ForceNew: !computed,
},
"architecture": &schema.Schema{
Type: schema.TypeString,
Optional: !computed,
Computed: computed,
ForceNew: !computed,
Default: architectureDefault,
},
"description": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"kernel_id": &schema.Schema{
Type: schema.TypeString,
Optional: !computed,
Computed: computed,
ForceNew: !computed,
},
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"ramdisk_id": &schema.Schema{
Type: schema.TypeString,
Optional: !computed,
Computed: computed,
ForceNew: !computed,
},
"root_device_name": &schema.Schema{
Type: schema.TypeString,
Optional: !computed,
Computed: computed,
ForceNew: !computed,
},
"sriov_net_support": &schema.Schema{
Type: schema.TypeString,
Optional: !computed,
Computed: computed,
ForceNew: !computed,
Default: sriovNetSupportDefault,
},
"virtualization_type": &schema.Schema{
Type: schema.TypeString,
Optional: !computed,
Computed: computed,
ForceNew: !computed,
Default: virtualizationTypeDefault,
},
// The following block device attributes intentionally mimick the
// corresponding attributes on aws_instance, since they have the
// same meaning.
// However, we don't use root_block_device here because the constraint
// on which root device attributes can be overridden for an instance to
// not apply when registering an AMI.
"ebs_block_device": &schema.Schema{
Type: schema.TypeSet,
Optional: true,
Computed: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"delete_on_termination": &schema.Schema{
Type: schema.TypeBool,
Optional: !computed,
Default: deleteEbsOnTerminationDefault,
ForceNew: !computed,
Computed: computed,
},
"device_name": &schema.Schema{
Type: schema.TypeString,
Required: !computed,
ForceNew: !computed,
Computed: computed,
},
"encrypted": &schema.Schema{
Type: schema.TypeBool,
Optional: !computed,
Computed: computed,
ForceNew: !computed,
},
"iops": &schema.Schema{
Type: schema.TypeInt,
Optional: !computed,
Computed: computed,
ForceNew: !computed,
},
"snapshot_id": &schema.Schema{
Type: schema.TypeString,
Optional: !computed,
Computed: computed,
ForceNew: !computed,
},
"volume_size": &schema.Schema{
Type: schema.TypeInt,
Optional: !computed,
Computed: true,
ForceNew: !computed,
},
"volume_type": &schema.Schema{
Type: schema.TypeString,
Optional: !computed,
Computed: computed,
ForceNew: !computed,
Default: volumeTypeDefault,
},
},
},
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["snapshot_id"].(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: !computed,
Computed: computed,
},
"virtual_name": &schema.Schema{
Type: schema.TypeString,
Required: !computed,
Computed: computed,
},
},
},
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())
},
},
"tags": tagsSchema(),
// Not a public attribute; used to let the aws_ami_copy and aws_ami_from_instance
// resources record that they implicitly created new EBS snapshots that we should
// now manage. Not set by aws_ami, since the snapshots used there are presumed to
// be independently managed.
"manage_ebs_snapshots": &schema.Schema{
Type: schema.TypeBool,
Computed: true,
ForceNew: true,
},
}
}

View File

@ -0,0 +1,70 @@
package aws
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsAmiCopy() *schema.Resource {
// Inherit all of the common AMI attributes from aws_ami, since we're
// implicitly creating an aws_ami resource.
resourceSchema := resourceAwsAmiCommonSchema(true)
// Additional attributes unique to the copy operation.
resourceSchema["source_ami_id"] = &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
}
resourceSchema["source_ami_region"] = &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
}
return &schema.Resource{
Create: resourceAwsAmiCopyCreate,
Schema: resourceSchema,
// The remaining operations are shared with the generic aws_ami resource,
// since the aws_ami_copy resource only differs in how it's created.
Read: resourceAwsAmiRead,
Update: resourceAwsAmiUpdate,
Delete: resourceAwsAmiDelete,
}
}
func resourceAwsAmiCopyCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*AWSClient).ec2conn
req := &ec2.CopyImageInput{
Name: aws.String(d.Get("name").(string)),
Description: aws.String(d.Get("description").(string)),
SourceImageId: aws.String(d.Get("source_ami_id").(string)),
SourceRegion: aws.String(d.Get("source_ami_region").(string)),
}
res, err := client.CopyImage(req)
if err != nil {
return err
}
id := *res.ImageId
d.SetId(id)
d.Partial(true) // make sure we record the id even if the rest of this gets interrupted
d.Set("id", id)
d.Set("manage_ebs_snapshots", true)
d.SetPartial("id")
d.SetPartial("manage_ebs_snapshots")
d.Partial(false)
_, err = resourceAwsAmiWaitForAvailable(id, client)
if err != nil {
return err
}
return resourceAwsAmiUpdate(d, meta)
}

View File

@ -0,0 +1,195 @@
package aws
import (
"errors"
"fmt"
"strings"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSAMICopy(t *testing.T) {
var amiId string
snapshots := []string{}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSAMICopyConfig,
Check: func(state *terraform.State) error {
rs, ok := state.RootModule().Resources["aws_ami_copy.test"]
if !ok {
return fmt.Errorf("AMI resource not found")
}
amiId = rs.Primary.ID
if amiId == "" {
return fmt.Errorf("AMI id is not set")
}
conn := testAccProvider.Meta().(*AWSClient).ec2conn
req := &ec2.DescribeImagesInput{
ImageIds: []*string{aws.String(amiId)},
}
describe, err := conn.DescribeImages(req)
if err != nil {
return err
}
if len(describe.Images) != 1 ||
*describe.Images[0].ImageId != rs.Primary.ID {
return fmt.Errorf("AMI not found")
}
image := describe.Images[0]
if expected := "available"; *image.State != expected {
return fmt.Errorf("invalid image state; expected %v, got %v", expected, image.State)
}
if expected := "machine"; *image.ImageType != expected {
return fmt.Errorf("wrong image type; expected %v, got %v", expected, image.ImageType)
}
if expected := "terraform-acc-ami-copy"; *image.Name != expected {
return fmt.Errorf("wrong name; expected %v, got %v", expected, image.Name)
}
for _, bdm := range image.BlockDeviceMappings {
if bdm.Ebs != nil && bdm.Ebs.SnapshotId != nil {
snapshots = append(snapshots, *bdm.Ebs.SnapshotId)
}
}
if expected := 1; len(snapshots) != expected {
return fmt.Errorf("wrong number of snapshots; expected %v, got %v", expected, len(snapshots))
}
return nil
},
},
},
CheckDestroy: func(state *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ec2conn
diReq := &ec2.DescribeImagesInput{
ImageIds: []*string{aws.String(amiId)},
}
diRes, err := conn.DescribeImages(diReq)
if err != nil {
return err
}
if len(diRes.Images) > 0 {
state := diRes.Images[0].State
return fmt.Errorf("AMI %v remains in state %v", amiId, state)
}
stillExist := make([]string, 0, len(snapshots))
checkErrors := make(map[string]error)
for _, snapshotId := range snapshots {
dsReq := &ec2.DescribeSnapshotsInput{
SnapshotIds: []*string{aws.String(snapshotId)},
}
_, err := conn.DescribeSnapshots(dsReq)
if err == nil {
stillExist = append(stillExist, snapshotId)
continue
}
awsErr, ok := err.(awserr.Error)
if !ok {
checkErrors[snapshotId] = err
continue
}
if awsErr.Code() != "InvalidSnapshot.NotFound" {
checkErrors[snapshotId] = err
continue
}
}
if len(stillExist) > 0 || len(checkErrors) > 0 {
errParts := []string{
"Expected all snapshots to be gone, but:",
}
for _, snapshotId := range stillExist {
errParts = append(
errParts,
fmt.Sprintf("- %v still exists", snapshotId),
)
}
for snapshotId, err := range checkErrors {
errParts = append(
errParts,
fmt.Sprintf("- checking %v gave error: %v", snapshotId, err),
)
}
return errors.New(strings.Join(errParts, "\n"))
}
return nil
},
})
}
var testAccAWSAMICopyConfig = `
provider "aws" {
region = "us-east-1"
}
// An AMI can't be directly copied from one account to another, and
// we can't rely on any particular AMI being available since anyone
// can run this test in whatever account they like.
// Therefore we jump through some hoops here:
// - Spin up an EC2 instance based on a public AMI
// - Create an AMI by snapshotting that EC2 instance, using
// aws_ami_from_instance .
// - Copy the new AMI using aws_ami_copy .
//
// Thus this test can only succeed if the aws_ami_from_instance resource
// is working. If it's misbehaving it will likely cause this test to fail too.
// Since we're booting a t2.micro HVM instance we need a VPC for it to boot
// up into.
resource "aws_vpc" "foo" {
cidr_block = "10.1.0.0/16"
}
resource "aws_subnet" "foo" {
cidr_block = "10.1.1.0/24"
vpc_id = "${aws_vpc.foo.id}"
}
resource "aws_instance" "test" {
// This AMI has one block device mapping, so we expect to have
// one snapshot in our created AMI.
// This is an Amazon Linux HVM AMI. A public HVM AMI is required
// because paravirtual images cannot be copied between accounts.
ami = "ami-8fff43e4"
instance_type = "t2.micro"
tags {
Name = "terraform-acc-ami-copy-victim"
}
subnet_id = "${aws_subnet.foo.id}"
}
resource "aws_ami_from_instance" "test" {
name = "terraform-acc-ami-copy-victim"
description = "Testing Terraform aws_ami_from_instance resource"
source_instance_id = "${aws_instance.test.id}"
}
resource "aws_ami_copy" "test" {
name = "terraform-acc-ami-copy"
description = "Testing Terraform aws_ami_copy resource"
source_ami_id = "${aws_ami_from_instance.test.id}"
source_ami_region = "us-east-1"
}
`

View File

@ -0,0 +1,70 @@
package aws
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsAmiFromInstance() *schema.Resource {
// Inherit all of the common AMI attributes from aws_ami, since we're
// implicitly creating an aws_ami resource.
resourceSchema := resourceAwsAmiCommonSchema(true)
// Additional attributes unique to the copy operation.
resourceSchema["source_instance_id"] = &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
}
resourceSchema["snapshot_without_reboot"] = &schema.Schema{
Type: schema.TypeBool,
Optional: true,
ForceNew: true,
}
return &schema.Resource{
Create: resourceAwsAmiFromInstanceCreate,
Schema: resourceSchema,
// The remaining operations are shared with the generic aws_ami resource,
// since the aws_ami_copy resource only differs in how it's created.
Read: resourceAwsAmiRead,
Update: resourceAwsAmiUpdate,
Delete: resourceAwsAmiDelete,
}
}
func resourceAwsAmiFromInstanceCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*AWSClient).ec2conn
req := &ec2.CreateImageInput{
Name: aws.String(d.Get("name").(string)),
Description: aws.String(d.Get("description").(string)),
InstanceId: aws.String(d.Get("source_instance_id").(string)),
NoReboot: aws.Bool(d.Get("snapshot_without_reboot").(bool)),
}
res, err := client.CreateImage(req)
if err != nil {
return err
}
id := *res.ImageId
d.SetId(id)
d.Partial(true) // make sure we record the id even if the rest of this gets interrupted
d.Set("id", id)
d.Set("manage_ebs_snapshots", true)
d.SetPartial("id")
d.SetPartial("manage_ebs_snapshots")
d.Partial(false)
_, err = resourceAwsAmiWaitForAvailable(id, client)
if err != nil {
return err
}
return resourceAwsAmiUpdate(d, meta)
}

View File

@ -0,0 +1,157 @@
package aws
import (
"errors"
"fmt"
"strings"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)
func TestAccAWSAMIFromInstance(t *testing.T) {
var amiId string
snapshots := []string{}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccAWSAMIFromInstanceConfig,
Check: func(state *terraform.State) error {
rs, ok := state.RootModule().Resources["aws_ami_from_instance.test"]
if !ok {
return fmt.Errorf("AMI resource not found")
}
amiId = rs.Primary.ID
if amiId == "" {
return fmt.Errorf("AMI id is not set")
}
conn := testAccProvider.Meta().(*AWSClient).ec2conn
req := &ec2.DescribeImagesInput{
ImageIds: []*string{aws.String(amiId)},
}
describe, err := conn.DescribeImages(req)
if err != nil {
return err
}
if len(describe.Images) != 1 ||
*describe.Images[0].ImageId != rs.Primary.ID {
return fmt.Errorf("AMI not found")
}
image := describe.Images[0]
if expected := "available"; *image.State != expected {
return fmt.Errorf("invalid image state; expected %v, got %v", expected, image.State)
}
if expected := "machine"; *image.ImageType != expected {
return fmt.Errorf("wrong image type; expected %v, got %v", expected, image.ImageType)
}
if expected := "terraform-acc-ami-from-instance"; *image.Name != expected {
return fmt.Errorf("wrong name; expected %v, got %v", expected, image.Name)
}
for _, bdm := range image.BlockDeviceMappings {
if bdm.Ebs != nil && bdm.Ebs.SnapshotId != nil {
snapshots = append(snapshots, *bdm.Ebs.SnapshotId)
}
}
if expected := 1; len(snapshots) != expected {
return fmt.Errorf("wrong number of snapshots; expected %v, got %v", expected, len(snapshots))
}
return nil
},
},
},
CheckDestroy: func(state *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ec2conn
diReq := &ec2.DescribeImagesInput{
ImageIds: []*string{aws.String(amiId)},
}
diRes, err := conn.DescribeImages(diReq)
if err != nil {
return err
}
if len(diRes.Images) > 0 {
state := diRes.Images[0].State
return fmt.Errorf("AMI %v remains in state %v", amiId, state)
}
stillExist := make([]string, 0, len(snapshots))
checkErrors := make(map[string]error)
for _, snapshotId := range snapshots {
dsReq := &ec2.DescribeSnapshotsInput{
SnapshotIds: []*string{aws.String(snapshotId)},
}
_, err := conn.DescribeSnapshots(dsReq)
if err == nil {
stillExist = append(stillExist, snapshotId)
continue
}
awsErr, ok := err.(awserr.Error)
if !ok {
checkErrors[snapshotId] = err
continue
}
if awsErr.Code() != "InvalidSnapshot.NotFound" {
checkErrors[snapshotId] = err
continue
}
}
if len(stillExist) > 0 || len(checkErrors) > 0 {
errParts := []string{
"Expected all snapshots to be gone, but:",
}
for _, snapshotId := range stillExist {
errParts = append(
errParts,
fmt.Sprintf("- %v still exists", snapshotId),
)
}
for snapshotId, err := range checkErrors {
errParts = append(
errParts,
fmt.Sprintf("- checking %v gave error: %v", snapshotId, err),
)
}
return errors.New(strings.Join(errParts, "\n"))
}
return nil
},
})
}
var testAccAWSAMIFromInstanceConfig = `
provider "aws" {
region = "us-east-1"
}
resource "aws_instance" "test" {
// This AMI has one block device mapping, so we expect to have
// one snapshot in our created AMI.
ami = "ami-408c7f28"
instance_type = "t1.micro"
}
resource "aws_ami_from_instance" "test" {
name = "terraform-acc-ami-from-instance"
description = "Testing Terraform aws_ami_from_instance resource"
source_instance_id = "${aws_instance.test.id}"
}
`

View File

@ -0,0 +1,8 @@
package aws
// FIXME: The aws_ami resource doesn't currently have any acceptance tests,
// since creating an AMI requires having an EBS snapshot and we don't yet
// have a resource type for creating those.
// Once there is an aws_ebs_snapshot resource we can use it to implement
// a reasonable acceptance test for aws_ami. Until then it's necessary to
// test manually using a pre-existing EBS snapshot.

View File

@ -0,0 +1,92 @@
---
layout: "aws"
page_title: "AWS: aws_ami"
sidebar_current: "docs-aws-resource-ami"
description: |-
Creates and manages a custom Amazon Machine Image (AMI).
---
# aws\_ami
The AMI resource allows the creation and management of a completely-custom
*Amazon Machine Image* (AMI).
If you just want to duplicate an existing AMI, possibly copying it to another
region, it's better to use `aws_ami_copy` instead.
## Example Usage
```
# Create an AMI that will start a machine whose root device is backed by
# an EBS volume populated from a snapshot. It is assumed that such a snapshot
# already exists with the id "snap-xxxxxxxx".
resource "aws_ami" "example" {
name = "terraform-example"
virtualization_type = "hvm"
root_device_name = "/dev/xvda"
ebs_block_device {
device_name = "/dev/xvda"
snapshot_id = "snap-xxxxxxxx"
volume_size = 8
}
}
```
## Argument Reference
The following arguments are supported:
* `name` - (Required) A region-unique name for the AMI.
* `description` - (Optional) A longer, human-readable description for the AMI.
* `virtualization_type` - (Optional) Keyword to choose what virtualization mode created instances
will use. Can be either "paravirtual" (the default) or "hvm". The choice of virtualization type
changes the set of further arguments that are required, as described below.
* `architecture` - (Optional) Machine architecture for created instances. Defaults to "x86_64".
* `ebs_block_device` - (Optional) Nested block describing an EBS block device that should be
attached to created instances. The structure of this block is described below.
* `ephemeral_block_device` - (Optional) Nested block describing an ephemeral block device that
should be attached to created instances. The structure of this block is described below.
When `virtualization_type` is "paravirtual" the following additional arguments apply:
* `image_location` - (Required) Path to an S3 object containing an image manifest, e.g. created
by the `ec2-upload-bundle` command in the EC2 command line tools.
* `kernel_id` - (Required) The id of the kernel image (AKI) that will be used as the paravirtual
kernel in created instances.
* `ramdisk_id` - (Optional) The id of an initrd image (ARI) that will be used when booting the
created instances.
When `virtualization_type` is "hvm" the following additional arguments apply:
* `sriov_net_support` - (Optional) When set to "simple" (the default), enables enhanced networking
for created instances. No other value is supported at this time.
Nested `ebs_block_device` blocks have the following structure:
* `device_name` - (Required) The path at which the device is exposed to created instances.
* `delete_on_termination` - (Optional) Boolean controlling whether the EBS volumes created to
support each created instance will be deleted once that instance is terminated.
* `encrypted` - (Optional) Boolean controlling whether the created EBS volumes will be encrypted.
* `iops` - (Required only when `volume_type` is "io1") Number of I/O operations per second the
created volumes will support.
* `snapshot_id` - (Optional) The id of an EBS snapshot that will be used to initialize the created
EBS volumes. If set, the `volume_size` attribute must be at least as large as the referenced
snapshot.
* `volume_size` - (Required unless `snapshot_id` is set) The size of created volumes in GiB.
If `snapshot_id` is set and `volume_size` is omitted then the volume will have the same size
as the selected snapshot.
* `volume_type` - (Optional) The type of EBS volume to create. Can be one of "standard" (the
default), "io1" or "gp2".
Nested `ephemeral_block_device` blocks have the following structure:
* `device_name` - (Required) The path at which the device is exposed to created instances.
* `virtual_name` - (Required) A name for the ephemeral device, of the form "ephemeralN" where
*N* is a volume number starting from zero.
## Attributes Reference
The following attributes are exported:
* `id` - The ID of the created AMI.

View File

@ -0,0 +1,51 @@
---
layout: "aws"
page_title: "AWS: aws_ami_copy"
sidebar_current: "docs-aws-resource-ami-copy"
description: |-
Duplicates an existing Amazon Machine Image (AMI)
---
# aws\_ami\_copy
The "AMI copy" resource allows duplication of an Amazon Machine Image (AMI),
including cross-region copies.
If the source AMI has associated EBS snapshots, those will also be duplicated
along with the AMI.
This is useful for taking a single AMI provisioned in one region and making
it available in another for a multi-region deployment.
Copying an AMI can take several minutes. The creation of this resource will
block until the new AMI is available for use on new instances.
## Example Usage
```
resource "aws_ami_copy" "example" {
name = "terraform-example"
source_ami_id = "ami-xxxxxxxx"
source_ami_region = "us-west-1"
}
```
## Argument Reference
The following arguments are supported:
* `name` - (Required) A region-unique name for the AMI.
* `source_ami_id` - (Required) The id of the AMI to copy. This id must be valid in the region
given by `source_ami_region`.
* `source_region` - (Required) The region from which the AMI will be copied. This may be the
same as the AWS provider region in order to create a copy within the same region.
## Attributes Reference
The following attributes are exported:
* `id` - The ID of the created AMI.
This resource also exports a full set of attributes corresponding to the arguments of the
`aws_ami` resource, allowing the properties of the created AMI to be used elsewhere in the
configuration.

View File

@ -0,0 +1,57 @@
---
layout: "aws"
page_title: "AWS: aws_ami_from_instance"
sidebar_current: "docs-aws-resource-ami-from-instance"
description: |-
Creates an Amazon Machine Image (AMI) from an EBS-backed EC2 instance
---
# aws\_ami\_from\_instance
The "AMI from instance" resource allows the creation of an Amazon Machine
Image (AMI) modelled after an existing EBS-backed EC2 instance.
The created AMI will refer to implicitly-created snapshots of the instance's
EBS volumes and mimick its assigned block device configuration at the time
the resource is created.
This resource is best applied to an instance that is stopped when this instance
is created, so that the contents of the created image are predictable. When
applied to an instance that is running, *the instance will be stopped before taking
the snapshots and then started back up again*, resulting in a period of
downtime.
Note that the source instance is inspected only at the initial creation of this
resource. Ongoing updates to the referenced instance will not be propagated into
the generated AMI. Users may taint or otherwise recreate the resource in order
to produce a fresh snapshot.
## Example Usage
```
resource "aws_ami_from_instance" "example" {
name = "terraform-example"
source_instance_id = "i-xxxxxxxx"
}
```
## Argument Reference
The following arguments are supported:
* `name` - (Required) A region-unique name for the AMI.
* `source_instance_id` - (Required) The id of the instance to use as the basis of the AMI.
* `snapshot_without_reboot` - (Optional) Boolean that overrides the behavior of stopping
the instance before snapshotting. This is risky since it may cause a snapshot of an
inconsistent filesystem state, but can be used to avoid downtime if the user otherwise
guarantees that no filesystem writes will be underway at the time of snapshot.
## Attributes Reference
The following attributes are exported:
* `id` - The ID of the created AMI.
This resource also exports a full set of attributes corresponding to the arguments of the
`aws_ami` resource, allowing the properties of the created AMI to be used elsewhere in the
configuration.

View File

@ -39,6 +39,18 @@
<a href="#">EC2 Resources</a>
<ul class="nav nav-visible">
<li<%= sidebar_current("docs-aws-resource-ami") %>>
<a href="/docs/providers/aws/r/ami.html">aws_ami</a>
</li>
<li<%= sidebar_current("docs-aws-resource-ami-copy") %>>
<a href="/docs/providers/aws/r/ami_copy.html">aws_ami_copy</a>
</li>
<li<%= sidebar_current("docs-aws-resource-ami-from-instance") %>>
<a href="/docs/providers/aws/r/ami_from_instance.html">aws_ami_from_instance</a>
</li>
<li<%= sidebar_current("docs-aws-resource-app-cookie-stickiness-policy") %>>
<a href="/docs/providers/aws/r/app_cookie_stickiness_policy.html">aws_app_cookie_stickiness_policy</a>
</li>