From 27c3fa03e3802c5ef1dba31b74a4c6cce9ac425e Mon Sep 17 00:00:00 2001 From: Vivian Zhang Date: Sun, 7 Dec 2025 20:39:28 +0000 Subject: [PATCH] supporting deleting underlying imagebuilder AMIs/Containers on Image deletion --- .changelog/45486.txt | 3 + internal/service/imagebuilder/image.go | 98 +++++++++++++++++-- internal/service/imagebuilder/image_test.go | 82 ++++++++++++++++ .../docs/r/imagebuilder_image.html.markdown | 16 +++ 4 files changed, 191 insertions(+), 8 deletions(-) create mode 100644 .changelog/45486.txt diff --git a/.changelog/45486.txt b/.changelog/45486.txt new file mode 100644 index 000000000000..211c196a791e --- /dev/null +++ b/.changelog/45486.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_imagebuilder_image: Add `deletion_settings` configuration block to enable managed deletion of images and associated AWS resources +``` diff --git a/internal/service/imagebuilder/image.go b/internal/service/imagebuilder/image.go index 2effd9f080dd..5e3d8e0a0861 100644 --- a/internal/service/imagebuilder/image.go +++ b/internal/service/imagebuilder/image.go @@ -221,6 +221,20 @@ func resourceImage() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "deletion_settings": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "execution_role": { + Type: schema.TypeString, + Required: true, + ValidateFunc: verify.ValidARN, + }, + }, + }, + }, "workflow": { Type: schema.TypeSet, Computed: true, @@ -387,6 +401,9 @@ func resourceImageRead(ctx context.Context, d *schema.ResourceData, meta any) di } d.Set("platform", image.Platform) d.Set(names.AttrVersion, image.Version) + if v, ok := d.GetOk("deletion_settings"); ok { + d.Set("deletion_settings", v) + } if image.Workflows != nil { if err := d.Set("workflow", flattenWorkflowConfigurations(image.Workflows)); err != nil { return sdkdiag.AppendErrorf(diags, "setting workflow: %s", err) @@ -413,21 +430,86 @@ func resourceImageDelete(ctx context.Context, d *schema.ResourceData, meta any) conn := meta.(*conns.AWSClient).ImageBuilderClient(ctx) log.Printf("[DEBUG] Deleting Image Builder Image: %s", d.Id()) - _, err := conn.DeleteImage(ctx, &imagebuilder.DeleteImageInput{ - ImageBuildVersionArn: aws.String(d.Id()), - }) - if tfawserr.ErrCodeEquals(err, errCodeResourceNotFoundException) { - return diags - } + if v, ok := d.GetOk("deletion_settings"); ok && len(v.([]any)) > 0 && v.([]any)[0] != nil { + deletionSettings := v.([]any)[0].(map[string]any) - if err != nil { - return sdkdiag.AppendErrorf(diags, "deleting Image Builder Image (%s): %s", d.Id(), err) + input := &imagebuilder.StartResourceStateUpdateInput{ + ResourceArn: aws.String(d.Id()), + State: &awstypes.ResourceState{ + Status: awstypes.ResourceStatusDeleted, + }, + } + + if executionRole, ok := deletionSettings["execution_role"].(string); ok && executionRole != "" { + input.ExecutionRole = aws.String(executionRole) + } + + image, err := findImageByARN(ctx, conn, d.Id()) + if err != nil { + return sdkdiag.AppendErrorf(diags, "reading Image Builder Image (%s) for deletion: %s", d.Id(), err) + } + + if image.Type == awstypes.ImageTypeDocker { + input.IncludeResources = &awstypes.ResourceStateUpdateIncludeResources{ + Containers: true, + } + } else { + input.IncludeResources = &awstypes.ResourceStateUpdateIncludeResources{ + Amis: true, + Snapshots: true, + } + } + + output, err := conn.StartResourceStateUpdate(ctx, input) + if err != nil { + return sdkdiag.AppendErrorf(diags, "starting resource state update for Image Builder Image (%s): %s", d.Id(), err) + } + + if err := waitLifecycleExecution(ctx, conn, aws.ToString(output.LifecycleExecutionId)); err != nil { + return sdkdiag.AppendErrorf(diags, "waiting for Image Builder Image (%s) deletion: %s", d.Id(), err) + } + } else { + _, err := conn.DeleteImage(ctx, &imagebuilder.DeleteImageInput{ + ImageBuildVersionArn: aws.String(d.Id()), + }) + + if tfawserr.ErrCodeEquals(err, errCodeResourceNotFoundException) { + return diags + } + + if err != nil { + return sdkdiag.AppendErrorf(diags, "deleting Image Builder Image (%s): %s", d.Id(), err) + } } return diags } +func waitLifecycleExecution(ctx context.Context, conn *imagebuilder.Client, id string) error { + for { + output, err := conn.GetLifecycleExecution(ctx, &imagebuilder.GetLifecycleExecutionInput{ + LifecycleExecutionId: aws.String(id), + }) + if err != nil { + return err + } + + status := string(output.LifecycleExecution.State.Status) + if status == "SUCCESS" { + return nil + } + if status == "FAILED" { + return errors.New(aws.ToString(output.LifecycleExecution.State.Reason)) + } + if status == "CANCELLED" { + return errors.New("lifecycle execution was cancelled") + } + + time.Sleep(10 * time.Second) + } +} + func findImageByARN(ctx context.Context, conn *imagebuilder.Client, arn string) (*awstypes.Image, error) { input := &imagebuilder.GetImageInput{ ImageBuildVersionArn: aws.String(arn), diff --git a/internal/service/imagebuilder/image_test.go b/internal/service/imagebuilder/image_test.go index 6a3430b2f12a..3cc36340ba76 100644 --- a/internal/service/imagebuilder/image_test.go +++ b/internal/service/imagebuilder/image_test.go @@ -347,6 +347,28 @@ func TestAccImageBuilderImage_workflows(t *testing.T) { }) } +func TestAccImageBuilderImage_deletionSettings(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_imagebuilder_image.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ImageBuilderServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckImageDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccImageConfig_deletionSettings(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckImageExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, "deletion_settings.#", "1"), + ), + }, + }, + }) +} + func testAccCheckImageDestroy(ctx context.Context) resource.TestCheckFunc { return func(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).ImageBuilderClient(ctx) @@ -1069,3 +1091,63 @@ resource "aws_imagebuilder_image" "test" { } `) } + +func testAccImageConfig_deletionSettings(rName string) string { + return acctest.ConfigCompose( + testAccImageBaseConfig(rName), + fmt.Sprintf(` +resource "aws_iam_role" "deletion" { + name = "%[1]s-deletion" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "imagebuilder.${data.aws_partition.current.dns_suffix}" + } + }] + }) +} + +resource "aws_iam_role_policy" "deletion" { + name = "%[1]s-deletion" + role = aws_iam_role.deletion.id + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Action = [ + "imagebuilder:GetImage", + "imagebuilder:GetLifecycleExecution", + "imagebuilder:DeleteImage", + "ec2:DescribeImages", + "ec2:DescribeImageAttribute", + "ec2:DescribeSnapshots", + "ec2:DeregisterImage", + "ec2:DeleteSnapshot", + "ecr:BatchDeleteImage", + "ecr:DescribeImages", + "ecr:DescribeRepositories" + ] + Resource = "*" + }] + }) +} + +resource "aws_imagebuilder_image" "test" { + image_recipe_arn = aws_imagebuilder_image_recipe.test.arn + infrastructure_configuration_arn = aws_imagebuilder_infrastructure_configuration.test.arn + + deletion_settings { + execution_role = aws_iam_role.deletion.arn + } + + depends_on = [aws_iam_role_policy.deletion] + + tags = { + Name = %[1]q + } +} +`, rName)) +} diff --git a/website/docs/r/imagebuilder_image.html.markdown b/website/docs/r/imagebuilder_image.html.markdown index 439047b96430..f65bf965b4f0 100644 --- a/website/docs/r/imagebuilder_image.html.markdown +++ b/website/docs/r/imagebuilder_image.html.markdown @@ -30,6 +30,7 @@ The following arguments are optional: * `region` - (Optional) Region where this resource will be [managed](https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints). Defaults to the Region set in the [provider configuration](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#aws-configuration-reference). * `container_recipe_arn` - (Optional) - Amazon Resource Name (ARN) of the container recipe. +* `deletion_settings` - (Optional) Configuration block with deletion settings. Detailed below. * `distribution_configuration_arn` - (Optional) Amazon Resource Name (ARN) of the Image Builder Distribution Configuration. * `enhanced_image_metadata_enabled` - (Optional) Whether additional information about the image being created is collected. Defaults to `true`. * `execution_role` - (Optional) Amazon Resource Name (ARN) of the service-linked role to be used by Image Builder to [execute workflows](https://docs.aws.amazon.com/imagebuilder/latest/userguide/manage-image-workflows.html). @@ -83,6 +84,21 @@ The following arguments are required: * `name` - (Required) The name of the Workflow parameter. * `value` - (Required) The value of the Workflow parameter. +### deletion_settings + +When configured, enables managed deletion of the image and all associated resources using Image Builder's lifecycle management. When not configured, uses standard deletion which only removes the image record. + +For AMI-based images, managed deletion removes: +* AMIs +* EBS snapshots + +For container images, managed deletion removes: +* Container images from ECR repositories + +The following arguments are required: + +* `execution_role` - (Required) Amazon Resource Name (ARN) of the IAM role used to delete the image and associated resources. + ## Attribute Reference This resource exports the following attributes in addition to the arguments above: