Modify ebssurrogate builder to snapshot all launch devices

Documentation for ebssurrogate states that all of the devices in
`launch_block_device_mappings` are snapshotted and included in the
image. In fact, only the device that was designated as the root
device was snapshotted. This patch modifies the builder to create
snapshots of all the devices and include them in the image. This
allows creating images with separate filesystems preconfigured,
rather than having to add volumes to `ami_block_device_mappings`
and configure them after boot.
This commit is contained in:
Joseph Wright 2018-03-25 19:25:53 -04:00
parent b5635ac393
commit cb3699a584
6 changed files with 404 additions and 166 deletions

View File

@ -176,6 +176,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}
}
amiDevices := b.config.BuildAMIDevices()
launchDevices := b.config.BuildLaunchDevices()
// Build the steps
steps := []multistep.Step{
&awscommon.StepPreValidate{
@ -227,8 +230,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
EnableAMISriovNetSupport: b.config.AMISriovNetSupport,
EnableAMIENASupport: b.config.AMIENASupport,
},
&StepSnapshotNewRootVolume{
NewRootMountPoint: b.config.RootDevice.SourceDeviceName,
&StepSnapshotVolumes{
LaunchDevices: launchDevices,
},
&awscommon.StepDeregisterAMI{
AccessConfig: &b.config.AccessConfig,
@ -239,7 +242,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
},
&StepRegisterAMI{
RootDevice: b.config.RootDevice,
BlockDevices: b.config.BlockDevices.BuildAMIDevices(),
AMIDevices: amiDevices,
LaunchDevices: launchDevices,
EnableAMISriovNetSupport: b.config.AMISriovNetSupport,
EnableAMIENASupport: b.config.AMIENASupport,
},

View File

@ -3,8 +3,6 @@ package ebssurrogate
import (
"errors"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/packer/template/interpolate"
)
@ -46,21 +44,3 @@ func (c *RootBlockDevice) Prepare(ctx *interpolate.Context) []error {
return nil
}
func (d *RootBlockDevice) createBlockDeviceMapping(snapshotId string) *ec2.BlockDeviceMapping {
rootBlockDevice := &ec2.EbsBlockDevice{
SnapshotId: aws.String(snapshotId),
VolumeType: aws.String(d.VolumeType),
VolumeSize: aws.Int64(d.VolumeSize),
DeleteOnTermination: aws.Bool(d.DeleteOnTermination),
}
if d.IOPS != 0 {
rootBlockDevice.Iops = aws.Int64(d.IOPS)
}
return &ec2.BlockDeviceMapping{
DeviceName: aws.String(d.DeviceName),
Ebs: rootBlockDevice,
}
}

View File

@ -14,7 +14,8 @@ import (
// StepRegisterAMI creates the AMI.
type StepRegisterAMI struct {
RootDevice RootBlockDevice
BlockDevices []*ec2.BlockDeviceMapping
AMIDevices []*ec2.BlockDeviceMapping
LaunchDevices []*ec2.BlockDeviceMapping
EnableAMIENASupport bool
EnableAMISriovNetSupport bool
image *ec2.Image
@ -23,19 +24,19 @@ type StepRegisterAMI struct {
func (s *StepRegisterAMI) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
ec2conn := state.Get("ec2").(*ec2.EC2)
snapshotId := state.Get("snapshot_id").(string)
snapshotIds := state.Get("snapshot_ids").(map[string]string)
ui := state.Get("ui").(packer.Ui)
ui.Say("Registering the AMI...")
blockDevicesExcludingRoot := DeduplicateRootVolume(s.BlockDevices, s.RootDevice, snapshotId)
blockDevices := s.combineDevices(snapshotIds)
registerOpts := &ec2.RegisterImageInput{
Name: &config.AMIName,
Architecture: aws.String(ec2.ArchitectureValuesX8664),
RootDeviceName: aws.String(s.RootDevice.DeviceName),
VirtualizationType: aws.String(config.AMIVirtType),
BlockDeviceMappings: blockDevicesExcludingRoot,
BlockDeviceMappings: blockDevices,
}
if s.EnableAMISriovNetSupport {
@ -120,17 +121,34 @@ func (s *StepRegisterAMI) Cleanup(state multistep.StateBag) {
}
}
func DeduplicateRootVolume(BlockDevices []*ec2.BlockDeviceMapping, RootDevice RootBlockDevice, snapshotId string) []*ec2.BlockDeviceMapping {
// Defensive coding to make sure we only add the root volume once
blockDevicesExcludingRoot := make([]*ec2.BlockDeviceMapping, 0, len(BlockDevices))
for _, blockDevice := range BlockDevices {
if *blockDevice.DeviceName == RootDevice.SourceDeviceName {
continue
}
func (s *StepRegisterAMI) combineDevices(snapshotIds map[string]string) []*ec2.BlockDeviceMapping {
devices := map[string]*ec2.BlockDeviceMapping{}
blockDevicesExcludingRoot = append(blockDevicesExcludingRoot, blockDevice)
for _, device := range s.AMIDevices {
devices[*device.DeviceName] = device
}
blockDevicesExcludingRoot = append(blockDevicesExcludingRoot, RootDevice.createBlockDeviceMapping(snapshotId))
return blockDevicesExcludingRoot
// Devices in launch_block_device_mappings override any with
// the same name in ami_block_device_mappings, except for the
// one designated as the root device in ami_root_device
for _, device := range s.LaunchDevices {
snapshotId, ok := snapshotIds[*device.DeviceName]
if ok {
device.Ebs.SnapshotId = aws.String(snapshotId)
// Block devices with snapshot inherit
// encryption settings from the snapshot
device.Ebs.Encrypted = nil
device.Ebs.KmsKeyId = nil
}
if *device.DeviceName == s.RootDevice.SourceDeviceName {
device.DeviceName = aws.String(s.RootDevice.DeviceName)
}
devices[*device.DeviceName] = device
}
blockDevices := []*ec2.BlockDeviceMapping{}
for _, device := range devices {
blockDevices = append(blockDevices, device)
}
return blockDevices
}

View File

@ -1,37 +1,247 @@
package ebssurrogate
import (
"reflect"
"sort"
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
)
func GetStringPointer() *string {
tmp := "/dev/name"
return &tmp
}
const sourceDeviceName = "/dev/xvdf"
const rootDeviceName = "/dev/xvda"
func GetTestDevice() *ec2.BlockDeviceMapping {
TestDev := ec2.BlockDeviceMapping{
DeviceName: GetStringPointer(),
}
return &TestDev
}
func TestStepRegisterAmi_DeduplicateRootVolume(t *testing.T) {
TestRootDevice := RootBlockDevice{}
TestRootDevice.SourceDeviceName = "/dev/name"
blockDevices := []*ec2.BlockDeviceMapping{}
blockDevicesExcludingRoot := DeduplicateRootVolume(blockDevices, TestRootDevice, "12342351")
if len(blockDevicesExcludingRoot) != 1 {
t.Fatalf("Unexpected length of block devices list")
}
TestBlockDevice := GetTestDevice()
blockDevices = append(blockDevices, TestBlockDevice)
blockDevicesExcludingRoot = DeduplicateRootVolume(blockDevices, TestRootDevice, "12342351")
if len(blockDevicesExcludingRoot) != 1 {
t.Fatalf("Unexpected length of block devices list")
func newStepRegisterAMI(amiDevices, launchDevices []*ec2.BlockDeviceMapping) *StepRegisterAMI {
return &StepRegisterAMI{
RootDevice: RootBlockDevice{
SourceDeviceName: sourceDeviceName,
DeviceName: rootDeviceName,
DeleteOnTermination: true,
VolumeType: "ebs",
VolumeSize: 10,
},
AMIDevices: amiDevices,
LaunchDevices: launchDevices,
}
}
func sorted(devices []*ec2.BlockDeviceMapping) []*ec2.BlockDeviceMapping {
sort.SliceStable(devices, func(i, j int) bool {
return *devices[i].DeviceName < *devices[j].DeviceName
})
return devices
}
func TestStepRegisterAmi_combineDevices(t *testing.T) {
cases := []struct {
snapshotIds map[string]string
amiDevices []*ec2.BlockDeviceMapping
launchDevices []*ec2.BlockDeviceMapping
allDevices []*ec2.BlockDeviceMapping
}{
{
snapshotIds: map[string]string{},
amiDevices: []*ec2.BlockDeviceMapping{},
launchDevices: []*ec2.BlockDeviceMapping{},
allDevices: []*ec2.BlockDeviceMapping{},
},
{
snapshotIds: map[string]string{},
amiDevices: []*ec2.BlockDeviceMapping{},
launchDevices: []*ec2.BlockDeviceMapping{
&ec2.BlockDeviceMapping{
Ebs: &ec2.EbsBlockDevice{},
DeviceName: aws.String(sourceDeviceName),
},
},
allDevices: []*ec2.BlockDeviceMapping{
&ec2.BlockDeviceMapping{
Ebs: &ec2.EbsBlockDevice{},
DeviceName: aws.String(rootDeviceName),
},
},
},
{
// Minimal single device
snapshotIds: map[string]string{
sourceDeviceName: "snap-0123456789abcdef1",
},
amiDevices: []*ec2.BlockDeviceMapping{},
launchDevices: []*ec2.BlockDeviceMapping{
&ec2.BlockDeviceMapping{
Ebs: &ec2.EbsBlockDevice{},
DeviceName: aws.String(sourceDeviceName),
},
},
allDevices: []*ec2.BlockDeviceMapping{
&ec2.BlockDeviceMapping{
Ebs: &ec2.EbsBlockDevice{
SnapshotId: aws.String("snap-0123456789abcdef1"),
},
DeviceName: aws.String(rootDeviceName),
},
},
},
{
// Single launch device with AMI device
snapshotIds: map[string]string{
sourceDeviceName: "snap-0123456789abcdef1",
},
amiDevices: []*ec2.BlockDeviceMapping{
&ec2.BlockDeviceMapping{
Ebs: &ec2.EbsBlockDevice{},
DeviceName: aws.String("/dev/xvdg"),
},
},
launchDevices: []*ec2.BlockDeviceMapping{
&ec2.BlockDeviceMapping{
Ebs: &ec2.EbsBlockDevice{},
DeviceName: aws.String(sourceDeviceName),
},
},
allDevices: []*ec2.BlockDeviceMapping{
&ec2.BlockDeviceMapping{
Ebs: &ec2.EbsBlockDevice{
SnapshotId: aws.String("snap-0123456789abcdef1"),
},
DeviceName: aws.String(rootDeviceName),
},
&ec2.BlockDeviceMapping{
Ebs: &ec2.EbsBlockDevice{},
DeviceName: aws.String("/dev/xvdg"),
},
},
},
{
// Multiple launch devices
snapshotIds: map[string]string{
sourceDeviceName: "snap-0123456789abcdef1",
"/dev/xvdg": "snap-0123456789abcdef2",
},
amiDevices: []*ec2.BlockDeviceMapping{},
launchDevices: []*ec2.BlockDeviceMapping{
&ec2.BlockDeviceMapping{
Ebs: &ec2.EbsBlockDevice{},
DeviceName: aws.String(sourceDeviceName),
},
&ec2.BlockDeviceMapping{
Ebs: &ec2.EbsBlockDevice{},
DeviceName: aws.String("/dev/xvdg"),
},
},
allDevices: []*ec2.BlockDeviceMapping{
&ec2.BlockDeviceMapping{
Ebs: &ec2.EbsBlockDevice{
SnapshotId: aws.String("snap-0123456789abcdef1"),
},
DeviceName: aws.String(rootDeviceName),
},
&ec2.BlockDeviceMapping{
Ebs: &ec2.EbsBlockDevice{
SnapshotId: aws.String("snap-0123456789abcdef2"),
},
DeviceName: aws.String("/dev/xvdg"),
},
},
},
{
// Multiple launch devices with encryption
snapshotIds: map[string]string{
sourceDeviceName: "snap-0123456789abcdef1",
"/dev/xvdg": "snap-0123456789abcdef2",
},
amiDevices: []*ec2.BlockDeviceMapping{},
launchDevices: []*ec2.BlockDeviceMapping{
&ec2.BlockDeviceMapping{
Ebs: &ec2.EbsBlockDevice{
Encrypted: aws.Bool(true),
},
DeviceName: aws.String(sourceDeviceName),
},
&ec2.BlockDeviceMapping{
Ebs: &ec2.EbsBlockDevice{
Encrypted: aws.Bool(true),
},
DeviceName: aws.String("/dev/xvdg"),
},
},
allDevices: []*ec2.BlockDeviceMapping{
&ec2.BlockDeviceMapping{
Ebs: &ec2.EbsBlockDevice{
SnapshotId: aws.String("snap-0123456789abcdef1"),
// Encrypted: true stripped from snapshotted devices
},
DeviceName: aws.String(rootDeviceName),
},
&ec2.BlockDeviceMapping{
Ebs: &ec2.EbsBlockDevice{
SnapshotId: aws.String("snap-0123456789abcdef2"),
},
DeviceName: aws.String("/dev/xvdg"),
},
},
},
{
// Multiple launch devices and AMI devices with encryption
snapshotIds: map[string]string{
sourceDeviceName: "snap-0123456789abcdef1",
"/dev/xvdg": "snap-0123456789abcdef2",
},
amiDevices: []*ec2.BlockDeviceMapping{
&ec2.BlockDeviceMapping{
Ebs: &ec2.EbsBlockDevice{
Encrypted: aws.Bool(true),
KmsKeyId: aws.String("keyId"),
},
// Source device name can be used in AMI devices
// since launch device of same name gets renamed
// to root device name
DeviceName: aws.String(sourceDeviceName),
},
},
launchDevices: []*ec2.BlockDeviceMapping{
&ec2.BlockDeviceMapping{
Ebs: &ec2.EbsBlockDevice{
Encrypted: aws.Bool(true),
},
DeviceName: aws.String(sourceDeviceName),
},
&ec2.BlockDeviceMapping{
Ebs: &ec2.EbsBlockDevice{
Encrypted: aws.Bool(true),
},
DeviceName: aws.String("/dev/xvdg"),
},
},
allDevices: []*ec2.BlockDeviceMapping{
&ec2.BlockDeviceMapping{
Ebs: &ec2.EbsBlockDevice{
Encrypted: aws.Bool(true),
KmsKeyId: aws.String("keyId"),
},
DeviceName: aws.String(sourceDeviceName),
},
&ec2.BlockDeviceMapping{
Ebs: &ec2.EbsBlockDevice{
SnapshotId: aws.String("snap-0123456789abcdef1"),
},
DeviceName: aws.String(rootDeviceName),
},
&ec2.BlockDeviceMapping{
Ebs: &ec2.EbsBlockDevice{
SnapshotId: aws.String("snap-0123456789abcdef2"),
},
DeviceName: aws.String("/dev/xvdg"),
},
},
},
}
for _, tc := range cases {
stepRegisterAmi := newStepRegisterAMI(tc.amiDevices, tc.launchDevices)
allDevices := stepRegisterAmi.combineDevices(tc.snapshotIds)
if !reflect.DeepEqual(sorted(allDevices), sorted(tc.allDevices)) {
t.Fatalf("Unexpected output from combineDevices")
}
}
}

View File

@ -1,103 +0,0 @@
package ebssurrogate
import (
"context"
"errors"
"fmt"
"time"
"github.com/aws/aws-sdk-go/service/ec2"
awscommon "github.com/hashicorp/packer/builder/amazon/common"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
// StepSnapshotNewRootVolume creates a snapshot of the created volume.
//
// Produces:
// snapshot_id string - ID of the created snapshot
type StepSnapshotNewRootVolume struct {
NewRootMountPoint string
snapshotId string
}
func (s *StepSnapshotNewRootVolume) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui)
instance := state.Get("instance").(*ec2.Instance)
var newRootVolume string
for _, volume := range instance.BlockDeviceMappings {
if *volume.DeviceName == s.NewRootMountPoint {
newRootVolume = *volume.Ebs.VolumeId
}
}
ui.Say(fmt.Sprintf("Creating snapshot of EBS Volume %s...", newRootVolume))
description := fmt.Sprintf("Packer: %s", time.Now().String())
createSnapResp, err := ec2conn.CreateSnapshot(&ec2.CreateSnapshotInput{
VolumeId: &newRootVolume,
Description: &description,
})
if err != nil {
err := fmt.Errorf("Error creating snapshot: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Set the snapshot ID so we can delete it later
s.snapshotId = *createSnapResp.SnapshotId
ui.Message(fmt.Sprintf("Snapshot ID: %s", s.snapshotId))
// Wait for the snapshot to be ready
stateChange := awscommon.StateChangeConf{
Pending: []string{"pending"},
StepState: state,
Target: "completed",
Refresh: func() (interface{}, string, error) {
resp, err := ec2conn.DescribeSnapshots(&ec2.DescribeSnapshotsInput{SnapshotIds: []*string{&s.snapshotId}})
if err != nil {
return nil, "", err
}
if len(resp.Snapshots) == 0 {
return nil, "", errors.New("No snapshots found.")
}
s := resp.Snapshots[0]
return s, *s.State, nil
},
}
_, err = awscommon.WaitForState(&stateChange)
if err != nil {
err := fmt.Errorf("Error waiting for snapshot: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
state.Put("snapshot_id", s.snapshotId)
return multistep.ActionContinue
}
func (s *StepSnapshotNewRootVolume) Cleanup(state multistep.StateBag) {
if s.snapshotId == "" {
return
}
_, cancelled := state.GetOk(multistep.StateCancelled)
_, halted := state.GetOk(multistep.StateHalted)
if cancelled || halted {
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui)
ui.Say("Removing snapshot since we cancelled or halted...")
_, err := ec2conn.DeleteSnapshot(&ec2.DeleteSnapshotInput{SnapshotId: &s.snapshotId})
if err != nil {
ui.Error(fmt.Sprintf("Error: %s", err))
}
}
}

View File

@ -0,0 +1,129 @@
package ebssurrogate
import (
"context"
"errors"
"fmt"
"sync"
"time"
"github.com/aws/aws-sdk-go/service/ec2"
multierror "github.com/hashicorp/go-multierror"
awscommon "github.com/hashicorp/packer/builder/amazon/common"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
// StepSnapshotVolumes creates snapshots of the created volumes.
//
// Produces:
// snapshot_ids map[string]string - IDs of the created snapshots
type StepSnapshotVolumes struct {
LaunchDevices []*ec2.BlockDeviceMapping
snapshotIds map[string]string
}
func (s *StepSnapshotVolumes) snapshotVolume(deviceName string, state multistep.StateBag) error {
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui)
instance := state.Get("instance").(*ec2.Instance)
var volumeId string
for _, volume := range instance.BlockDeviceMappings {
if *volume.DeviceName == deviceName {
volumeId = *volume.Ebs.VolumeId
}
}
if volumeId == "" {
return fmt.Errorf("Volume ID for device %s not found", deviceName)
}
ui.Say(fmt.Sprintf("Creating snapshot of EBS Volume %s...", volumeId))
description := fmt.Sprintf("Packer: %s", time.Now().String())
createSnapResp, err := ec2conn.CreateSnapshot(&ec2.CreateSnapshotInput{
VolumeId: &volumeId,
Description: &description,
})
if err != nil {
return err
}
// Set the snapshot ID so we can delete it later
s.snapshotIds[deviceName] = *createSnapResp.SnapshotId
// Wait for the snapshot to be ready
stateChange := awscommon.StateChangeConf{
Pending: []string{"pending"},
StepState: state,
Target: "completed",
Refresh: func() (interface{}, string, error) {
resp, err := ec2conn.DescribeSnapshots(&ec2.DescribeSnapshotsInput{
SnapshotIds: []*string{createSnapResp.SnapshotId},
})
if err != nil {
return nil, "", err
}
if len(resp.Snapshots) == 0 {
return nil, "", errors.New("No snapshots found.")
}
s := resp.Snapshots[0]
return s, *s.State, nil
},
}
_, err = awscommon.WaitForState(&stateChange)
return err
}
func (s *StepSnapshotVolumes) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
s.snapshotIds = map[string]string{}
var wg sync.WaitGroup
var errs *multierror.Error
for _, device := range s.LaunchDevices {
wg.Add(1)
go func(device *ec2.BlockDeviceMapping) {
defer wg.Done()
if err := s.snapshotVolume(*device.DeviceName, state); err != nil {
errs = multierror.Append(errs, err)
}
}(device)
}
wg.Wait()
if errs != nil {
state.Put("error", errs)
ui.Error(errs.Error())
return multistep.ActionHalt
}
state.Put("snapshot_ids", s.snapshotIds)
return multistep.ActionContinue
}
func (s *StepSnapshotVolumes) Cleanup(state multistep.StateBag) {
if len(s.snapshotIds) == 0 {
return
}
_, cancelled := state.GetOk(multistep.StateCancelled)
_, halted := state.GetOk(multistep.StateHalted)
if cancelled || halted {
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui)
ui.Say("Removing snapshots since we cancelled or halted...")
for _, snapshotId := range s.snapshotIds {
_, err := ec2conn.DeleteSnapshot(&ec2.DeleteSnapshotInput{SnapshotId: &snapshotId})
if err != nil {
ui.Error(fmt.Sprintf("Error: %s", err))
}
}
}
}