Skip to content

Commit 27c3fa0

Browse files
committed
supporting deleting underlying imagebuilder AMIs/Containers on Image deletion
1 parent 35c7b37 commit 27c3fa0

File tree

4 files changed

+191
-8
lines changed

4 files changed

+191
-8
lines changed

.changelog/45486.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
resource/aws_imagebuilder_image: Add `deletion_settings` configuration block to enable managed deletion of images and associated AWS resources
3+
```

internal/service/imagebuilder/image.go

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,20 @@ func resourceImage() *schema.Resource {
221221
Type: schema.TypeString,
222222
Computed: true,
223223
},
224+
"deletion_settings": {
225+
Type: schema.TypeList,
226+
Optional: true,
227+
MaxItems: 1,
228+
Elem: &schema.Resource{
229+
Schema: map[string]*schema.Schema{
230+
"execution_role": {
231+
Type: schema.TypeString,
232+
Required: true,
233+
ValidateFunc: verify.ValidARN,
234+
},
235+
},
236+
},
237+
},
224238
"workflow": {
225239
Type: schema.TypeSet,
226240
Computed: true,
@@ -387,6 +401,9 @@ func resourceImageRead(ctx context.Context, d *schema.ResourceData, meta any) di
387401
}
388402
d.Set("platform", image.Platform)
389403
d.Set(names.AttrVersion, image.Version)
404+
if v, ok := d.GetOk("deletion_settings"); ok {
405+
d.Set("deletion_settings", v)
406+
}
390407
if image.Workflows != nil {
391408
if err := d.Set("workflow", flattenWorkflowConfigurations(image.Workflows)); err != nil {
392409
return sdkdiag.AppendErrorf(diags, "setting workflow: %s", err)
@@ -413,21 +430,86 @@ func resourceImageDelete(ctx context.Context, d *schema.ResourceData, meta any)
413430
conn := meta.(*conns.AWSClient).ImageBuilderClient(ctx)
414431

415432
log.Printf("[DEBUG] Deleting Image Builder Image: %s", d.Id())
416-
_, err := conn.DeleteImage(ctx, &imagebuilder.DeleteImageInput{
417-
ImageBuildVersionArn: aws.String(d.Id()),
418-
})
419433

420-
if tfawserr.ErrCodeEquals(err, errCodeResourceNotFoundException) {
421-
return diags
422-
}
434+
if v, ok := d.GetOk("deletion_settings"); ok && len(v.([]any)) > 0 && v.([]any)[0] != nil {
435+
deletionSettings := v.([]any)[0].(map[string]any)
423436

424-
if err != nil {
425-
return sdkdiag.AppendErrorf(diags, "deleting Image Builder Image (%s): %s", d.Id(), err)
437+
input := &imagebuilder.StartResourceStateUpdateInput{
438+
ResourceArn: aws.String(d.Id()),
439+
State: &awstypes.ResourceState{
440+
Status: awstypes.ResourceStatusDeleted,
441+
},
442+
}
443+
444+
if executionRole, ok := deletionSettings["execution_role"].(string); ok && executionRole != "" {
445+
input.ExecutionRole = aws.String(executionRole)
446+
}
447+
448+
image, err := findImageByARN(ctx, conn, d.Id())
449+
if err != nil {
450+
return sdkdiag.AppendErrorf(diags, "reading Image Builder Image (%s) for deletion: %s", d.Id(), err)
451+
}
452+
453+
if image.Type == awstypes.ImageTypeDocker {
454+
input.IncludeResources = &awstypes.ResourceStateUpdateIncludeResources{
455+
Containers: true,
456+
}
457+
} else {
458+
input.IncludeResources = &awstypes.ResourceStateUpdateIncludeResources{
459+
Amis: true,
460+
Snapshots: true,
461+
}
462+
}
463+
464+
output, err := conn.StartResourceStateUpdate(ctx, input)
465+
if err != nil {
466+
return sdkdiag.AppendErrorf(diags, "starting resource state update for Image Builder Image (%s): %s", d.Id(), err)
467+
}
468+
469+
if err := waitLifecycleExecution(ctx, conn, aws.ToString(output.LifecycleExecutionId)); err != nil {
470+
return sdkdiag.AppendErrorf(diags, "waiting for Image Builder Image (%s) deletion: %s", d.Id(), err)
471+
}
472+
} else {
473+
_, err := conn.DeleteImage(ctx, &imagebuilder.DeleteImageInput{
474+
ImageBuildVersionArn: aws.String(d.Id()),
475+
})
476+
477+
if tfawserr.ErrCodeEquals(err, errCodeResourceNotFoundException) {
478+
return diags
479+
}
480+
481+
if err != nil {
482+
return sdkdiag.AppendErrorf(diags, "deleting Image Builder Image (%s): %s", d.Id(), err)
483+
}
426484
}
427485

428486
return diags
429487
}
430488

489+
func waitLifecycleExecution(ctx context.Context, conn *imagebuilder.Client, id string) error {
490+
for {
491+
output, err := conn.GetLifecycleExecution(ctx, &imagebuilder.GetLifecycleExecutionInput{
492+
LifecycleExecutionId: aws.String(id),
493+
})
494+
if err != nil {
495+
return err
496+
}
497+
498+
status := string(output.LifecycleExecution.State.Status)
499+
if status == "SUCCESS" {
500+
return nil
501+
}
502+
if status == "FAILED" {
503+
return errors.New(aws.ToString(output.LifecycleExecution.State.Reason))
504+
}
505+
if status == "CANCELLED" {
506+
return errors.New("lifecycle execution was cancelled")
507+
}
508+
509+
time.Sleep(10 * time.Second)
510+
}
511+
}
512+
431513
func findImageByARN(ctx context.Context, conn *imagebuilder.Client, arn string) (*awstypes.Image, error) {
432514
input := &imagebuilder.GetImageInput{
433515
ImageBuildVersionArn: aws.String(arn),

internal/service/imagebuilder/image_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,28 @@ func TestAccImageBuilderImage_workflows(t *testing.T) {
347347
})
348348
}
349349

350+
func TestAccImageBuilderImage_deletionSettings(t *testing.T) {
351+
ctx := acctest.Context(t)
352+
rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix)
353+
resourceName := "aws_imagebuilder_image.test"
354+
355+
resource.ParallelTest(t, resource.TestCase{
356+
PreCheck: func() { acctest.PreCheck(ctx, t) },
357+
ErrorCheck: acctest.ErrorCheck(t, names.ImageBuilderServiceID),
358+
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories,
359+
CheckDestroy: testAccCheckImageDestroy(ctx),
360+
Steps: []resource.TestStep{
361+
{
362+
Config: testAccImageConfig_deletionSettings(rName),
363+
Check: resource.ComposeTestCheckFunc(
364+
testAccCheckImageExists(ctx, resourceName),
365+
resource.TestCheckResourceAttr(resourceName, "deletion_settings.#", "1"),
366+
),
367+
},
368+
},
369+
})
370+
}
371+
350372
func testAccCheckImageDestroy(ctx context.Context) resource.TestCheckFunc {
351373
return func(s *terraform.State) error {
352374
conn := acctest.Provider.Meta().(*conns.AWSClient).ImageBuilderClient(ctx)
@@ -1069,3 +1091,63 @@ resource "aws_imagebuilder_image" "test" {
10691091
}
10701092
`)
10711093
}
1094+
1095+
func testAccImageConfig_deletionSettings(rName string) string {
1096+
return acctest.ConfigCompose(
1097+
testAccImageBaseConfig(rName),
1098+
fmt.Sprintf(`
1099+
resource "aws_iam_role" "deletion" {
1100+
name = "%[1]s-deletion"
1101+
assume_role_policy = jsonencode({
1102+
Version = "2012-10-17"
1103+
Statement = [{
1104+
Action = "sts:AssumeRole"
1105+
Effect = "Allow"
1106+
Principal = {
1107+
Service = "imagebuilder.${data.aws_partition.current.dns_suffix}"
1108+
}
1109+
}]
1110+
})
1111+
}
1112+
1113+
resource "aws_iam_role_policy" "deletion" {
1114+
name = "%[1]s-deletion"
1115+
role = aws_iam_role.deletion.id
1116+
policy = jsonencode({
1117+
Version = "2012-10-17"
1118+
Statement = [{
1119+
Effect = "Allow"
1120+
Action = [
1121+
"imagebuilder:GetImage",
1122+
"imagebuilder:GetLifecycleExecution",
1123+
"imagebuilder:DeleteImage",
1124+
"ec2:DescribeImages",
1125+
"ec2:DescribeImageAttribute",
1126+
"ec2:DescribeSnapshots",
1127+
"ec2:DeregisterImage",
1128+
"ec2:DeleteSnapshot",
1129+
"ecr:BatchDeleteImage",
1130+
"ecr:DescribeImages",
1131+
"ecr:DescribeRepositories"
1132+
]
1133+
Resource = "*"
1134+
}]
1135+
})
1136+
}
1137+
1138+
resource "aws_imagebuilder_image" "test" {
1139+
image_recipe_arn = aws_imagebuilder_image_recipe.test.arn
1140+
infrastructure_configuration_arn = aws_imagebuilder_infrastructure_configuration.test.arn
1141+
1142+
deletion_settings {
1143+
execution_role = aws_iam_role.deletion.arn
1144+
}
1145+
1146+
depends_on = [aws_iam_role_policy.deletion]
1147+
1148+
tags = {
1149+
Name = %[1]q
1150+
}
1151+
}
1152+
`, rName))
1153+
}

website/docs/r/imagebuilder_image.html.markdown

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ The following arguments are optional:
3030

3131
* `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).
3232
* `container_recipe_arn` - (Optional) - Amazon Resource Name (ARN) of the container recipe.
33+
* `deletion_settings` - (Optional) Configuration block with deletion settings. Detailed below.
3334
* `distribution_configuration_arn` - (Optional) Amazon Resource Name (ARN) of the Image Builder Distribution Configuration.
3435
* `enhanced_image_metadata_enabled` - (Optional) Whether additional information about the image being created is collected. Defaults to `true`.
3536
* `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:
8384
* `name` - (Required) The name of the Workflow parameter.
8485
* `value` - (Required) The value of the Workflow parameter.
8586

87+
### deletion_settings
88+
89+
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.
90+
91+
For AMI-based images, managed deletion removes:
92+
* AMIs
93+
* EBS snapshots
94+
95+
For container images, managed deletion removes:
96+
* Container images from ECR repositories
97+
98+
The following arguments are required:
99+
100+
* `execution_role` - (Required) Amazon Resource Name (ARN) of the IAM role used to delete the image and associated resources.
101+
86102
## Attribute Reference
87103

88104
This resource exports the following attributes in addition to the arguments above:

0 commit comments

Comments
 (0)