diff --git a/.web-docs/components/builder/ebssurrogate/README.md b/.web-docs/components/builder/ebssurrogate/README.md index c8c131466..fbd29b5e9 100644 --- a/.web-docs/components/builder/ebssurrogate/README.md +++ b/.web-docs/components/builder/ebssurrogate/README.md @@ -86,6 +86,15 @@ necessary for this build to succeed and can be found further down the page. [NitroTPM Support](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/enable-nitrotpm-support-on-ami.html) for more information. Only enabled if a valid option is provided, otherwise ignored. +- `use_create_image` (bool) - Whether to use the CreateImage or RegisterImage API when creating the AMI. + When set to `true`, the CreateImage API is used and will create the image + from the instance itself, and inherit properties from the instance. + When set to `false`, the RegisterImage API is used and the image is created using + a snapshot of the specified EBS volume, and no properties are inherited from the instance. + Defaults to `false`. + Ref: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CreateImage.html + https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_RegisterImage.html + diff --git a/builder/ebssurrogate/builder.go b/builder/ebssurrogate/builder.go index dc7455c37..c35945def 100644 --- a/builder/ebssurrogate/builder.go +++ b/builder/ebssurrogate/builder.go @@ -85,6 +85,15 @@ type Config struct { // [NitroTPM Support](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/enable-nitrotpm-support-on-ami.html) for // more information. Only enabled if a valid option is provided, otherwise ignored. TpmSupport string `mapstructure:"tpm_support" required:"false"` + // Whether to use the CreateImage or RegisterImage API when creating the AMI. + // When set to `true`, the CreateImage API is used and will create the image + // from the instance itself, and inherit properties from the instance. + // When set to `false`, the RegisterImage API is used and the image is created using + // a snapshot of the specified EBS volume, and no properties are inherited from the instance. + // Defaults to `false`. + //Ref: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CreateImage.html + // https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_RegisterImage.html + UseCreateImage bool `mapstructure:"use_create_image" required:"false"` ctx interpolate.Context } @@ -318,6 +327,52 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) amiDevices := b.config.AMIMappings.BuildEC2BlockDeviceMappings() launchDevices := b.config.LaunchMappings.BuildEC2BlockDeviceMappings() + var buildAmiStep multistep.Step + var volumeStep multistep.Step + + if b.config.UseCreateImage { + volumeStep = &StepSwapVolumes{ + PollingConfig: b.config.PollingConfig, + RootDevice: b.config.RootDevice, + LaunchDevices: launchDevices, + LaunchOmitMap: b.config.LaunchMappings.GetOmissions(), + Ctx: b.config.ctx, + } + + buildAmiStep = &StepCreateAMI{ + AMISkipBuildRegion: b.config.AMISkipBuildRegion, + RootDevice: b.config.RootDevice, + AMIDevices: amiDevices, + LaunchDevices: launchDevices, + PollingConfig: b.config.PollingConfig, + IsRestricted: b.config.IsChinaCloud() || b.config.IsGovCloud(), + Tags: b.config.RunTags, + Ctx: b.config.ctx, + } + } else { + volumeStep = &StepSnapshotVolumes{ + PollingConfig: b.config.PollingConfig, + LaunchDevices: launchDevices, + SnapshotOmitMap: b.config.LaunchMappings.GetOmissions(), + SnapshotTags: b.config.SnapshotTags, + Ctx: b.config.ctx, + } + buildAmiStep = &StepRegisterAMI{ + RootDevice: b.config.RootDevice, + AMIDevices: amiDevices, + LaunchDevices: launchDevices, + EnableAMISriovNetSupport: b.config.AMISriovNetSupport, + EnableAMIENASupport: b.config.AMIENASupport, + Architecture: b.config.Architecture, + LaunchOmitMap: b.config.LaunchMappings.GetOmissions(), + AMISkipBuildRegion: b.config.AMISkipBuildRegion, + PollingConfig: b.config.PollingConfig, + BootMode: b.config.BootMode, + UefiData: b.config.UefiData, + TpmSupport: b.config.TpmSupport, + } + } + // Build the steps steps := []multistep.Step{ &awscommon.StepPreValidate{ @@ -420,13 +475,7 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) EnableAMISriovNetSupport: b.config.AMISriovNetSupport, EnableAMIENASupport: b.config.AMIENASupport, }, - &StepSnapshotVolumes{ - PollingConfig: b.config.PollingConfig, - LaunchDevices: launchDevices, - SnapshotOmitMap: b.config.LaunchMappings.GetOmissions(), - SnapshotTags: b.config.SnapshotTags, - Ctx: b.config.ctx, - }, + volumeStep, &awscommon.StepDeregisterAMI{ AccessConfig: &b.config.AccessConfig, ForceDeregister: b.config.AMIForceDeregister, @@ -434,20 +483,7 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) AMIName: b.config.AMIName, Regions: b.config.AMIRegions, }, - &StepRegisterAMI{ - RootDevice: b.config.RootDevice, - AMIDevices: amiDevices, - LaunchDevices: launchDevices, - EnableAMISriovNetSupport: b.config.AMISriovNetSupport, - EnableAMIENASupport: b.config.AMIENASupport, - Architecture: b.config.Architecture, - LaunchOmitMap: b.config.LaunchMappings.GetOmissions(), - AMISkipBuildRegion: b.config.AMISkipBuildRegion, - PollingConfig: b.config.PollingConfig, - BootMode: b.config.BootMode, - UefiData: b.config.UefiData, - TpmSupport: b.config.TpmSupport, - }, + buildAmiStep, &awscommon.StepAMIRegionCopy{ AccessConfig: &b.config.AccessConfig, Regions: b.config.AMIRegions, diff --git a/builder/ebssurrogate/builder.hcl2spec.go b/builder/ebssurrogate/builder.hcl2spec.go index 3cda9cd1a..3031fbb49 100644 --- a/builder/ebssurrogate/builder.hcl2spec.go +++ b/builder/ebssurrogate/builder.hcl2spec.go @@ -210,6 +210,7 @@ type FlatConfig struct { BootMode *string `mapstructure:"boot_mode" required:"false" cty:"boot_mode" hcl:"boot_mode"` UefiData *string `mapstructure:"uefi_data" required:"false" cty:"uefi_data" hcl:"uefi_data"` TpmSupport *string `mapstructure:"tpm_support" required:"false" cty:"tpm_support" hcl:"tpm_support"` + UseCreateImage *bool `mapstructure:"use_create_image" required:"false" cty:"use_create_image" hcl:"use_create_image"` } // FlatMapstructure returns a new FlatConfig. @@ -377,6 +378,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "boot_mode": &hcldec.AttrSpec{Name: "boot_mode", Type: cty.String, Required: false}, "uefi_data": &hcldec.AttrSpec{Name: "uefi_data", Type: cty.String, Required: false}, "tpm_support": &hcldec.AttrSpec{Name: "tpm_support", Type: cty.String, Required: false}, + "use_create_image": &hcldec.AttrSpec{Name: "use_create_image", Type: cty.Bool, Required: false}, } return s } diff --git a/builder/ebssurrogate/builder_acc_test.go b/builder/ebssurrogate/builder_acc_test.go index fa547a15d..e277726ce 100644 --- a/builder/ebssurrogate/builder_acc_test.go +++ b/builder/ebssurrogate/builder_acc_test.go @@ -109,6 +109,75 @@ func TestAccBuilder_Ebssurrogate_SSHPrivateKeyFile_SSM(t *testing.T) { acctest.TestPlugin(t, testcase) } +func TestAccBuilder_EbssurrogateUseCreateImageTrue(t *testing.T) { + ami := amazon_acc.AMIHelper{ + Region: "us-east-1", + Name: "ebs-image-method-create-acc-test", + } + testCase := &acctest.PluginTestCase{ + Name: "amazon-ebssurrogate_image_method_create_test", + Template: fmt.Sprintf(testBuilderAccUseCreateImageTrue, ami.Name), + Teardown: func() error { + return ami.CleanUpAmi() + }, + Check: func(buildCommand *exec.Cmd, logfile string) error { + if buildCommand.ProcessState != nil { + if buildCommand.ProcessState.ExitCode() != 0 { + return fmt.Errorf("Bad exit code. Logfile: %s", logfile) + } + } + return nil + }, + } + acctest.TestPlugin(t, testCase) +} + +func TestAccBuilder_EbssurrogateUseCreateImageFalse(t *testing.T) { + ami := amazon_acc.AMIHelper{ + Region: "us-east-1", + Name: "ebs-image-method-register-acc-test", + } + testCase := &acctest.PluginTestCase{ + Name: "amazon-ebssurrogate_image_method_register_test", + Template: fmt.Sprintf(testBuilderAccUseCreateImageFalse, ami.Name), + Teardown: func() error { + return ami.CleanUpAmi() + }, + Check: func(buildCommand *exec.Cmd, logfile string) error { + if buildCommand.ProcessState != nil { + if buildCommand.ProcessState.ExitCode() != 0 { + return fmt.Errorf("Bad exit code. Logfile: %s", logfile) + } + } + return nil + }, + } + acctest.TestPlugin(t, testCase) +} + +func TestAccBuilder_EbssurrogateUseCreateImageOptional(t *testing.T) { + ami := amazon_acc.AMIHelper{ + Region: "us-east-1", + Name: "ebs-image-method-empty-acc-test", + } + testCase := &acctest.PluginTestCase{ + Name: "amazon-ebssurrogate_image_method_empty_test", + Template: fmt.Sprintf(testBuilderAccUseCreateImageOptional, ami.Name), + Teardown: func() error { + return ami.CleanUpAmi() + }, + Check: func(buildCommand *exec.Cmd, logfile string) error { + if buildCommand.ProcessState != nil { + if buildCommand.ProcessState.ExitCode() != 0 { + return fmt.Errorf("Bad exit code. Logfile: %s", logfile) + } + } + return nil + }, + } + acctest.TestPlugin(t, testCase) +} + const testBuilderAccBasic = ` source "amazon-ebssurrogate" "test" { ami_name = "%s" @@ -197,3 +266,89 @@ build { sources = ["amazon-ebssurrogate.test"] } ` + +const testBuilderAccUseCreateImageTrue = ` +source "amazon-ebssurrogate" "test" { + ami_name = "%s" + region = "us-east-1" + instance_type = "m3.medium" + source_ami = "ami-76b2a71e" + ssh_username = "ubuntu" + use_create_image = true + launch_block_device_mappings { + device_name = "/dev/xvda" + delete_on_termination = true + volume_size = 8 + volume_type = "gp2" + } + ami_virtualization_type = "hvm" + ami_root_device { + source_device_name = "/dev/xvda" + device_name = "/dev/sda1" + delete_on_termination = true + volume_size = 8 + volume_type = "gp2" + } +} + +build { + sources = ["amazon-ebssurrogate.test"] +} +` + +const testBuilderAccUseCreateImageFalse = ` +source "amazon-ebssurrogate" "test" { + ami_name = "%s" + region = "us-east-1" + instance_type = "m3.medium" + source_ami = "ami-76b2a71e" + ssh_username = "ubuntu" + use_create_image = false + launch_block_device_mappings { + device_name = "/dev/xvda" + delete_on_termination = true + volume_size = 8 + volume_type = "gp2" + } + ami_virtualization_type = "hvm" + ami_root_device { + source_device_name = "/dev/xvda" + device_name = "/dev/sda1" + delete_on_termination = true + volume_size = 8 + volume_type = "gp2" + } +} + +build { + sources = ["amazon-ebssurrogate.test"] +} +` + +const testBuilderAccUseCreateImageOptional = ` +source "amazon-ebssurrogate" "test" { + ami_name = "%s" + region = "us-east-1" + instance_type = "m3.medium" + source_ami = "ami-76b2a71e" + ssh_username = "ubuntu" + launch_block_device_mappings { + device_name = "/dev/xvda" + delete_on_termination = true + volume_size = 8 + volume_type = "gp2" + } + ami_virtualization_type = "hvm" + ami_root_device { + source_device_name = "/dev/xvda" + device_name = "/dev/sda1" + delete_on_termination = true + volume_size = 8 + volume_type = "gp2" + } +} + +build { + sources = ["amazon-ebssurrogate.test"] +} +` diff --git a/builder/ebssurrogate/step_create_ami.go b/builder/ebssurrogate/step_create_ami.go new file mode 100644 index 000000000..36a1b37d9 --- /dev/null +++ b/builder/ebssurrogate/step_create_ami.go @@ -0,0 +1,250 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ebssurrogate + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + awscommon "github.com/hashicorp/packer-plugin-amazon/builder/common" + "github.com/hashicorp/packer-plugin-amazon/builder/common/awserrors" + "github.com/hashicorp/packer-plugin-sdk/multistep" + packersdk "github.com/hashicorp/packer-plugin-sdk/packer" + "github.com/hashicorp/packer-plugin-sdk/random" + "github.com/hashicorp/packer-plugin-sdk/retry" + "github.com/hashicorp/packer-plugin-sdk/template/interpolate" +) + +type StepCreateAMI struct { + PollingConfig *awscommon.AWSPollingConfig + RootDevice RootBlockDevice + AMIDevices []*ec2.BlockDeviceMapping + LaunchDevices []*ec2.BlockDeviceMapping + LaunchOmitMap map[string]bool + image *ec2.Image + AMISkipBuildRegion bool + IsRestricted bool + Ctx interpolate.Context + Tags map[string]string +} + +func (s *StepCreateAMI) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + config := state.Get("config").(*Config) + ec2conn := state.Get("ec2").(*ec2.EC2) + instance := state.Get("instance").(*ec2.Instance) + ui := state.Get("ui").(packersdk.Ui) + + blockDevices := s.combineDevices() + + // Create the image + amiName := config.AMIName + state.Put("intermediary_image", false) + if config.AMIEncryptBootVolume.True() || s.AMISkipBuildRegion { + state.Put("intermediary_image", true) + + // From AWS SDK docs: You can encrypt a copy of an unencrypted snapshot, + // but you cannot use it to create an unencrypted copy of an encrypted + // snapshot. Your default CMK for EBS is used unless you specify a + // non-default key using KmsKeyId. + + // If encrypt_boot is nil or true, we need to create a temporary image + // so that in step_region_copy, we can copy it with the correct + // encryption + amiName = random.AlphaNum(7) + } + + ui.Say(fmt.Sprintf("Creating AMI %s from instance %s", amiName, *instance.InstanceId)) + createOpts := &ec2.CreateImageInput{ + InstanceId: instance.InstanceId, + Name: &amiName, + BlockDeviceMappings: blockDevices, + } + + if !s.IsRestricted { + ec2Tags, err := awscommon.TagMap(s.Tags).EC2Tags(s.Ctx, *ec2conn.Config.Region, state) + if err != nil { + err := fmt.Errorf("Error tagging AMI: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + createOpts.TagSpecifications = ec2Tags.TagSpecifications(ec2.ResourceTypeImage, ec2.ResourceTypeSnapshot) + } + + var createResp *ec2.CreateImageOutput + + // Create a timeout for the CreateImage call. + timeoutCtx, cancel := context.WithTimeout(ctx, time.Minute*15) + defer cancel() + + err := retry.Config{ + Tries: 0, + ShouldRetry: func(err error) bool { + return awserrors.Matches(err, "InvalidParameterValue", "Instance is not in state") + }, + RetryDelay: (&retry.Backoff{InitialBackoff: 200 * time.Millisecond, MaxBackoff: 30 * time.Second, Multiplier: 2}).Linear, + }.Run(timeoutCtx, func(ctx context.Context) error { + var err error + createResp, err = ec2conn.CreateImage(createOpts) + return err + }) + if err != nil { + err := fmt.Errorf("Error creating AMI: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // Set the AMI ID in the state + ui.Message(fmt.Sprintf("AMI: %s", *createResp.ImageId)) + amis := make(map[string]string) + amis[*ec2conn.Config.Region] = *createResp.ImageId + state.Put("amis", amis) + + // Wait for the image to become ready + ui.Say("Waiting for AMI to become ready...") + if waitErr := s.PollingConfig.WaitUntilAMIAvailable(ctx, ec2conn, *createResp.ImageId); waitErr != nil { + // waitErr should get bubbled up if the issue is a wait timeout + err := fmt.Errorf("Error waiting for AMI: %s", waitErr) + imResp, imerr := ec2conn.DescribeImages(&ec2.DescribeImagesInput{ImageIds: []*string{createResp.ImageId}}) + if imerr != nil { + // If there's a failure describing images, bubble that error up too, but don't erase the waitErr. + log.Printf("DescribeImages call was unable to determine reason waiting for AMI failed: %s", imerr) + err = fmt.Errorf("Unknown error waiting for AMI; %s. DescribeImages returned an error: %s", waitErr, imerr) + } + if imResp != nil && len(imResp.Images) > 0 { + // Finally, if there's a stateReason, store that with the wait err + image := imResp.Images[0] + if image != nil { + stateReason := image.StateReason + if stateReason != nil { + err = fmt.Errorf("Error waiting for AMI: %s. DescribeImages returned the state reason: %s", waitErr, stateReason) + } + } + } + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + imagesResp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{ImageIds: []*string{createResp.ImageId}}) + if err != nil { + err := fmt.Errorf("Error searching for AMI: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + s.image = imagesResp.Images[0] + + snapshots := make(map[string][]string) + for _, blockDeviceMapping := range imagesResp.Images[0].BlockDeviceMappings { + if blockDeviceMapping.Ebs != nil && blockDeviceMapping.Ebs.SnapshotId != nil { + + snapshots[*ec2conn.Config.Region] = append(snapshots[*ec2conn.Config.Region], *blockDeviceMapping.Ebs.SnapshotId) + } + } + state.Put("snapshots", snapshots) + + return multistep.ActionContinue +} + +func (s *StepCreateAMI) combineDevices() []*ec2.BlockDeviceMapping { + devices := map[string]*ec2.BlockDeviceMapping{} + + for _, device := range s.AMIDevices { + devices[*device.DeviceName] = device + } + + // Devices in launch_block_device_mappings override any with + // the same name in ami_block_device_mappings + // If launch device name is equal to Root SourceDeviceName + // then set the device's deviceName to RootDevice.DeviceName from root ami_root_device + // and overwrite / add the device in the devices map + for _, device := range s.LaunchDevices { + // Skip devices we've flagged for omission + omit, ok := s.LaunchOmitMap[*device.DeviceName] + if ok && omit { + continue + } + + // Use root device name for the new source root device + 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 +} + +func (s *StepCreateAMI) Cleanup(state multistep.StateBag) { + if s.image == nil { + return + } + + _, cancelled := state.GetOk(multistep.StateCancelled) + _, halted := state.GetOk(multistep.StateHalted) + if !cancelled && !halted { + return + } + + ec2conn := state.Get("ec2").(*ec2.EC2) + ui := state.Get("ui").(packersdk.Ui) + + ui.Say("Deregistering the AMI and deleting associated snapshots because " + + "of cancellation, or error...") + + resp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{ + ImageIds: []*string{s.image.ImageId}, + }) + + if err != nil { + err := fmt.Errorf("Error describing AMI: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return + } + + // Deregister image by name. + for _, i := range resp.Images { + _, err := ec2conn.DeregisterImage(&ec2.DeregisterImageInput{ + ImageId: i.ImageId, + }) + + if err != nil { + err := fmt.Errorf("Error deregistering existing AMI: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return + } + ui.Say(fmt.Sprintf("Deregistered AMI id: %s", *i.ImageId)) + + // Delete snapshot(s) by image + for _, b := range i.BlockDeviceMappings { + if b.Ebs != nil && aws.StringValue(b.Ebs.SnapshotId) != "" { + _, err := ec2conn.DeleteSnapshot(&ec2.DeleteSnapshotInput{ + SnapshotId: b.Ebs.SnapshotId, + }) + + if err != nil { + err := fmt.Errorf("Error deleting existing snapshot: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return + } + ui.Say(fmt.Sprintf("Deleted snapshot: %s", *b.Ebs.SnapshotId)) + } + } + } +} diff --git a/builder/ebssurrogate/step_swap_volumes.go b/builder/ebssurrogate/step_swap_volumes.go new file mode 100644 index 000000000..7a19c02b2 --- /dev/null +++ b/builder/ebssurrogate/step_swap_volumes.go @@ -0,0 +1,117 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ebssurrogate + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + awscommon "github.com/hashicorp/packer-plugin-amazon/builder/common" + "github.com/hashicorp/packer-plugin-sdk/multistep" + packersdk "github.com/hashicorp/packer-plugin-sdk/packer" + "github.com/hashicorp/packer-plugin-sdk/template/interpolate" +) + +// StepSwapVolumes detaches omitted volumes and original root volume and reattaches +// the new root volume specified by ami_root_device.source_device_name. +type StepSwapVolumes struct { + PollingConfig *awscommon.AWSPollingConfig + RootDevice RootBlockDevice + LaunchDevices []*ec2.BlockDeviceMapping + LaunchOmitMap map[string]bool + Ctx interpolate.Context +} + +func (s *StepSwapVolumes) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + ec2conn := state.Get("ec2").(*ec2.EC2) + ui := state.Get("ui").(packersdk.Ui) + instance := state.Get("instance").(*ec2.Instance) + + // Describe the instance + input := &ec2.DescribeInstancesInput{ + InstanceIds: []*string{ + aws.String(*instance.InstanceId), + }, + } + + result, err := ec2conn.DescribeInstances(input) + if err != nil { + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + deviceToVolumeMap := make(map[string]string) + + // Iterate through block device mappings and populate the map + for _, reservation := range result.Reservations { + for _, instance := range reservation.Instances { + for _, blockDevice := range instance.BlockDeviceMappings { + deviceToVolumeMap[*blockDevice.DeviceName] = *blockDevice.Ebs.VolumeId + } + } + } + + for deviceName, volumeID := range deviceToVolumeMap { + omit, ok := s.LaunchOmitMap[deviceName] + if ok && omit { + ui.Say(fmt.Sprintf("Detaching Ommitted EBS Device Name: %s, Volume ID: %s\n", deviceName, volumeID)) + err = s.detachVolume(ctx, ec2conn, deviceName, volumeID) + } else if deviceName == s.RootDevice.DeviceName || deviceName == s.RootDevice.SourceDeviceName || deviceName == "/dev/sda1" { + ui.Say(fmt.Sprintf("Detaching Root EBS Device Name: %s, Volume ID: %s\n", deviceName, volumeID)) + err = s.detachVolume(ctx, ec2conn, deviceName, volumeID) + } else { + ui.Say(fmt.Sprintf("Skip Detach of EBS Device Name: %s, Volume ID: %s\n", deviceName, volumeID)) + } + + if err != nil { + err := fmt.Errorf("error detaching volume: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + } + + rootVolumeId := aws.String(deviceToVolumeMap[s.RootDevice.SourceDeviceName]) + rootDeviceName := aws.String(s.RootDevice.DeviceName) + ui.Say(fmt.Sprintf("Attaching Root EBS Device Name %s, Volume ID: %s", *rootDeviceName, *rootVolumeId)) + + _, err = ec2conn.AttachVolume(&ec2.AttachVolumeInput{ + InstanceId: instance.InstanceId, + VolumeId: rootVolumeId, + Device: rootDeviceName, + }) + + if err != nil { + err := fmt.Errorf("error attaching volume: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + // Wait for the volume to become attached + err = s.PollingConfig.WaitUntilVolumeAttached(ctx, ec2conn, *rootVolumeId) + if err != nil { + err := fmt.Errorf("error waiting for volume: %s", err) + state.Put("error", err) + ui.Error(err.Error()) + return multistep.ActionHalt + } + + return multistep.ActionContinue +} + +func (s *StepSwapVolumes) detachVolume(ctx context.Context, ec2conn *ec2.EC2, deviceName string, volumeId string) error { + _, err := ec2conn.DetachVolume(&ec2.DetachVolumeInput{VolumeId: &volumeId}) + if err == nil { + return s.PollingConfig.WaitUntilVolumeDetached(ctx, ec2conn, volumeId) + } + + return err +} + +func (s *StepSwapVolumes) Cleanup(state multistep.StateBag) {} diff --git a/docs-partials/builder/ebssurrogate/Config-not-required.mdx b/docs-partials/builder/ebssurrogate/Config-not-required.mdx index c0c8187b2..77358a1d6 100644 --- a/docs-partials/builder/ebssurrogate/Config-not-required.mdx +++ b/docs-partials/builder/ebssurrogate/Config-not-required.mdx @@ -42,4 +42,13 @@ [NitroTPM Support](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/enable-nitrotpm-support-on-ami.html) for more information. Only enabled if a valid option is provided, otherwise ignored. +- `use_create_image` (bool) - Whether to use the CreateImage or RegisterImage API when creating the AMI. + When set to `true`, the CreateImage API is used and will create the image + from the instance itself, and inherit properties from the instance. + When set to `false`, the RegisterImage API is used and the image is created using + a snapshot of the specified EBS volume, and no properties are inherited from the instance. + Defaults to `false`. + Ref: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_CreateImage.html + https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_RegisterImage.html +