diff --git a/builder/amazon/common/block_device.go b/builder/amazon/common/block_device.go index e97cd4107..a01dfc83d 100644 --- a/builder/amazon/common/block_device.go +++ b/builder/amazon/common/block_device.go @@ -42,7 +42,7 @@ func buildBlockDevices(b []BlockDevice) []*ec2.BlockDeviceMapping { // You cannot specify Encrypted if you specify a Snapshot ID if blockDevice.SnapshotId != "" { ebsBlockDevice.SnapshotID = &blockDevice.SnapshotId - } else { + } else if blockDevice.Encrypted { ebsBlockDevice.Encrypted = &blockDevice.Encrypted } diff --git a/builder/amazon/ebs/builder.go b/builder/amazon/ebs/builder.go index 692485701..a572bfeba 100644 --- a/builder/amazon/ebs/builder.go +++ b/builder/amazon/ebs/builder.go @@ -99,6 +99,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe CommConfig: &b.config.RunConfig.Comm, VpcId: b.config.VpcId, }, + &stepCleanupVolumes{ + BlockDevices: b.config.BlockDevices, + }, &awscommon.StepRunSourceInstance{ Debug: b.config.PackerDebug, ExpectedRootDevice: "ebs", diff --git a/builder/amazon/ebs/step_cleanup_volumes.go b/builder/amazon/ebs/step_cleanup_volumes.go new file mode 100644 index 000000000..56ebe5527 --- /dev/null +++ b/builder/amazon/ebs/step_cleanup_volumes.go @@ -0,0 +1,118 @@ +package ebs + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/mitchellh/multistep" + "github.com/mitchellh/packer/builder/amazon/common" + "github.com/mitchellh/packer/packer" +) + +// stepCleanupVolumes cleans up any orphaned volumes that were not designated to +// remain after termination of the instance. These volumes are typically ones +// that are marked as "delete on terminate:false" in the source_ami of a build. +type stepCleanupVolumes struct { + BlockDevices common.BlockDevices +} + +func (s *stepCleanupVolumes) Run(state multistep.StateBag) multistep.StepAction { + // stepCleanupVolumes is for Cleanup only + return multistep.ActionContinue +} + +func (s *stepCleanupVolumes) Cleanup(state multistep.StateBag) { + ec2conn := state.Get("ec2").(*ec2.EC2) + instanceRaw := state.Get("instance") + var instance *ec2.Instance + if instanceRaw != nil { + instance = instanceRaw.(*ec2.Instance) + } + ui := state.Get("ui").(packer.Ui) + amisRaw := state.Get("amis") + if amisRaw == nil { + ui.Say("No AMIs to cleanup") + return + } + + if instance == nil { + ui.Say("No volumes to clean up, skipping") + return + } + + ui.Say("Cleaning up any extra volumes...") + + // We don't actually care about the value here, but we need Set behavior + save := make(map[string]struct{}) + for _, b := range s.BlockDevices.AMIMappings { + if !b.DeleteOnTermination { + save[b.DeviceName] = struct{}{} + } + } + + for _, b := range s.BlockDevices.LaunchMappings { + if !b.DeleteOnTermination { + save[b.DeviceName] = struct{}{} + } + } + + // Collect Volume information from the cached Instance as a map of volume-id + // to device name, to compare with save list above + var vl []*string + volList := make(map[string]string) + for _, bdm := range instance.BlockDeviceMappings { + if bdm.EBS != nil { + vl = append(vl, bdm.EBS.VolumeID) + volList[*bdm.EBS.VolumeID] = *bdm.DeviceName + } + } + + // Using the volume list from the cached Instance, check with AWS for up to + // date information on them + resp, err := ec2conn.DescribeVolumes(&ec2.DescribeVolumesInput{ + Filters: []*ec2.Filter{ + &ec2.Filter{ + Name: aws.String("volume-id"), + Values: vl, + }, + }, + }) + + if err != nil { + ui.Say(fmt.Sprintf("Error describing volumes: %s", err)) + return + } + + // If any of the returned volumes are in a "deleting" stage or otherwise not + // available, remove them from the list of volumes + for _, v := range resp.Volumes { + if v.State != nil && *v.State != "available" { + delete(volList, *v.VolumeID) + } + } + + if len(resp.Volumes) == 0 { + ui.Say("No volumes to clean up, skipping") + return + } + + // Filter out any devices marked for saving + for saveName, _ := range save { + for volKey, volName := range volList { + if volName == saveName { + delete(volList, volKey) + } + } + } + + // Destroy remaining volumes + for k, _ := range volList { + ui.Say(fmt.Sprintf("Destroying volume (%s)...", k)) + _, err := ec2conn.DeleteVolume(&ec2.DeleteVolumeInput{VolumeID: aws.String(k)}) + if err != nil { + ui.Say(fmt.Sprintf("Error deleting volume: %s", k)) + } + + } +} diff --git a/website/.ruby-version b/website/.ruby-version new file mode 100644 index 000000000..b1b25a5ff --- /dev/null +++ b/website/.ruby-version @@ -0,0 +1 @@ +2.2.2 diff --git a/website/source/docs/builders/amazon-ebs.html.markdown b/website/source/docs/builders/amazon-ebs.html.markdown index 5420c10fd..b7f16eef9 100644 --- a/website/source/docs/builders/amazon-ebs.html.markdown +++ b/website/source/docs/builders/amazon-ebs.html.markdown @@ -278,6 +278,13 @@ Here is an example using the optional AMI tags. This will add the tags } ``` +-> **Note:** Packer uses pre-built AMIs as the source for building images. +These source AMIs may include volumes that are not flagged to be destroyed on +termiation of the instance building the new image. Packer will attempt to clean +up all residual volumes that are not designated by the user to remain after +termination. If you need to preserve those source volumes, you can overwrite the +termination setting by specifying `delete_on_termination=false` in the +`launch_device_mappings` block for the device. [1]: http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_BlockDeviceMapping.html [2]: http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_EbsBlockDevice.html