Skip to content

Commit 31de51c

Browse files
authored
Merge pull request #27561 from hashicorp/f-ec2-api-idempotency
EC2: API idempotency
2 parents 9589ac5 + 8b85273 commit 31de51c

15 files changed

+152
-161
lines changed

.changelog/27561.txt

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
```release-note:bug
2+
resource/aws_instance: Use EC2 API idempotency to ensure that only a single Instance is created
3+
```
4+
5+
```release-note:enhancement
6+
resource/aws_ami_copy: Add `imds_support` attribute
7+
```
8+
9+
```release-note:enhancement
10+
resource/aws_ami_from_instance: Add `imds_support` attribute
11+
```

internal/service/ec2/ebs_volume.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/aws/aws-sdk-go/service/ec2"
1212
"github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr"
1313
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
14+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
1415
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1516
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
1617
"github.com/hashicorp/terraform-provider-aws/internal/conns"
@@ -122,6 +123,7 @@ func resourceEBSVolumeCreate(d *schema.ResourceData, meta interface{}) error {
122123

123124
input := &ec2.CreateVolumeInput{
124125
AvailabilityZone: aws.String(d.Get("availability_zone").(string)),
126+
ClientToken: aws.String(resource.UniqueId()),
125127
TagSpecifications: tagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeVolume),
126128
}
127129

@@ -161,7 +163,6 @@ func resourceEBSVolumeCreate(d *schema.ResourceData, meta interface{}) error {
161163
input.VolumeType = aws.String(value.(string))
162164
}
163165

164-
log.Printf("[DEBUG] Creating EBS Volume: %s", input)
165166
output, err := conn.CreateVolume(input)
166167

167168
if err != nil {

internal/service/ec2/ec2_ami.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,8 @@ const (
3131
func ResourceAMI() *schema.Resource {
3232
return &schema.Resource{
3333
Create: resourceAMICreate,
34-
// The Read, Update and Delete operations are shared with aws_ami_copy
35-
// and aws_ami_from_instance, since they differ only in how the image
36-
// is created.
34+
// The Read, Update and Delete operations are shared with aws_ami_copy and aws_ami_from_instance,
35+
// since they differ only in how the image is created.
3736
Read: resourceAMIRead,
3837
Update: resourceAMIUpdate,
3938
Delete: resourceAMIDelete,
@@ -48,6 +47,7 @@ func ResourceAMI() *schema.Resource {
4847
Delete: schema.DefaultTimeout(amiDeleteTimeout),
4948
},
5049

50+
// Keep in sync with aws_ami_copy's and aws_ami_from_instance's schemas.
5151
Schema: map[string]*schema.Schema{
5252
"architecture": {
5353
Type: schema.TypeString,

internal/service/ec2/ec2_ami_copy.go

+10-3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/aws/aws-sdk-go/aws"
99
"github.com/aws/aws-sdk-go/service/ec2"
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
1011
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1112
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
1213
"github.com/hashicorp/terraform-provider-aws/internal/conns"
@@ -30,6 +31,7 @@ func ResourceAMICopy() *schema.Resource {
3031
Delete: schema.DefaultTimeout(amiDeleteTimeout),
3132
},
3233

34+
// Keep in sync with aws_ami's schema.
3335
Schema: map[string]*schema.Schema{
3436
"architecture": {
3537
Type: schema.TypeString,
@@ -168,6 +170,10 @@ func ResourceAMICopy() *schema.Resource {
168170
Type: schema.TypeString,
169171
Computed: true,
170172
},
173+
"imds_support": {
174+
Type: schema.TypeString,
175+
Computed: true,
176+
},
171177
"kernel_id": {
172178
Type: schema.TypeString,
173179
Computed: true,
@@ -262,6 +268,7 @@ func resourceAMICopyCreate(d *schema.ResourceData, meta interface{}) error {
262268
name := d.Get("name").(string)
263269
sourceImageID := d.Get("source_ami_id").(string)
264270
input := &ec2.CopyImageInput{
271+
ClientToken: aws.String(resource.UniqueId()),
265272
Description: aws.String(d.Get("description").(string)),
266273
Encrypted: aws.Bool(d.Get("encrypted").(bool)),
267274
Name: aws.String(name),
@@ -280,20 +287,20 @@ func resourceAMICopyCreate(d *schema.ResourceData, meta interface{}) error {
280287
output, err := conn.CopyImage(input)
281288

282289
if err != nil {
283-
return fmt.Errorf("error creating EC2 AMI (%s) from source EC2 AMI (%s): %w", name, sourceImageID, err)
290+
return fmt.Errorf("creating EC2 AMI (%s) from source EC2 AMI (%s): %w", name, sourceImageID, err)
284291
}
285292

286293
d.SetId(aws.StringValue(output.ImageId))
287294
d.Set("manage_ebs_snapshots", true)
288295

289296
if len(tags) > 0 {
290297
if err := CreateTags(conn, d.Id(), tags); err != nil {
291-
return fmt.Errorf("error adding tags: %s", err)
298+
return fmt.Errorf("adding tags: %w", err)
292299
}
293300
}
294301

295302
if _, err := WaitImageAvailable(conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil {
296-
return fmt.Errorf("error waiting for EC2 AMI (%s) create: %w", d.Id(), err)
303+
return fmt.Errorf("waiting for EC2 AMI (%s) create: %w", d.Id(), err)
297304
}
298305

299306
if v, ok := d.GetOk("deprecation_time"); ok {

internal/service/ec2/ec2_ami_from_instance.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ func ResourceAMIFromInstance() *schema.Resource {
3030
Delete: schema.DefaultTimeout(amiDeleteTimeout),
3131
},
3232

33+
// Keep in sync with aws_ami's schema.
3334
Schema: map[string]*schema.Schema{
3435
"architecture": {
3536
Type: schema.TypeString,
@@ -157,6 +158,10 @@ func ResourceAMIFromInstance() *schema.Resource {
157158
Type: schema.TypeString,
158159
Computed: true,
159160
},
161+
"imds_support": {
162+
Type: schema.TypeString,
163+
Computed: true,
164+
},
160165
"kernel_id": {
161166
Type: schema.TypeString,
162167
Computed: true,
@@ -254,14 +259,14 @@ func resourceAMIFromInstanceCreate(d *schema.ResourceData, meta interface{}) err
254259
output, err := conn.CreateImage(input)
255260

256261
if err != nil {
257-
return fmt.Errorf("error creating EC2 AMI (%s) from EC2 Instance (%s): %w", name, instanceID, err)
262+
return fmt.Errorf("creating EC2 AMI (%s) from EC2 Instance (%s): %w", name, instanceID, err)
258263
}
259264

260265
d.SetId(aws.StringValue(output.ImageId))
261266
d.Set("manage_ebs_snapshots", true)
262267

263268
if _, err := WaitImageAvailable(conn, d.Id(), d.Timeout(schema.TimeoutCreate)); err != nil {
264-
return fmt.Errorf("error waiting for EC2 AMI (%s) create: %w", d.Id(), err)
269+
return fmt.Errorf("waiting for EC2 AMI (%s) create: %w", d.Id(), err)
265270
}
266271

267272
if v, ok := d.GetOk("deprecation_time"); ok {

internal/service/ec2/ec2_host.go

+12-11
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/aws/aws-sdk-go/aws/arn"
99
"github.com/aws/aws-sdk-go/service/ec2"
1010
"github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr"
11+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
1112
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1213
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
1314
"github.com/hashicorp/terraform-provider-aws/internal/conns"
@@ -84,6 +85,7 @@ func resourceHostCreate(d *schema.ResourceData, meta interface{}) error {
8485
input := &ec2.AllocateHostsInput{
8586
AutoPlacement: aws.String(d.Get("auto_placement").(string)),
8687
AvailabilityZone: aws.String(d.Get("availability_zone").(string)),
88+
ClientToken: aws.String(resource.UniqueId()),
8789
HostRecovery: aws.String(d.Get("host_recovery").(string)),
8890
Quantity: aws.Int64(1),
8991
}
@@ -104,17 +106,16 @@ func resourceHostCreate(d *schema.ResourceData, meta interface{}) error {
104106
input.TagSpecifications = tagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeDedicatedHost)
105107
}
106108

107-
log.Printf("[DEBUG] Creating EC2 Host: %s", input)
108109
output, err := conn.AllocateHosts(input)
109110

110111
if err != nil {
111-
return fmt.Errorf("error allocating EC2 Host: %w", err)
112+
return fmt.Errorf("allocating EC2 Host: %w", err)
112113
}
113114

114115
d.SetId(aws.StringValue(output.HostIds[0]))
115116

116117
if _, err := WaitHostCreated(conn, d.Id()); err != nil {
117-
return fmt.Errorf("error waiting for EC2 Host (%s) create: %w", d.Id(), err)
118+
return fmt.Errorf("waiting for EC2 Host (%s) create: %w", d.Id(), err)
118119
}
119120

120121
return resourceHostRead(d, meta)
@@ -134,7 +135,7 @@ func resourceHostRead(d *schema.ResourceData, meta interface{}) error {
134135
}
135136

136137
if err != nil {
137-
return fmt.Errorf("error reading EC2 Host (%s): %w", d.Id(), err)
138+
return fmt.Errorf("reading EC2 Host (%s): %w", d.Id(), err)
138139
}
139140

140141
arn := arn.ARN{
@@ -157,11 +158,11 @@ func resourceHostRead(d *schema.ResourceData, meta interface{}) error {
157158

158159
//lintignore:AWSR002
159160
if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil {
160-
return fmt.Errorf("error setting tags: %w", err)
161+
return fmt.Errorf("setting tags: %w", err)
161162
}
162163

163164
if err := d.Set("tags_all", tags.Map()); err != nil {
164-
return fmt.Errorf("error setting tags_all: %w", err)
165+
return fmt.Errorf("setting tags_all: %w", err)
165166
}
166167

167168
return nil
@@ -198,18 +199,18 @@ func resourceHostUpdate(d *schema.ResourceData, meta interface{}) error {
198199
}
199200

200201
if err != nil {
201-
return fmt.Errorf("error modifying EC2 Host (%s): %w", d.Id(), err)
202+
return fmt.Errorf("modifying EC2 Host (%s): %w", d.Id(), err)
202203
}
203204

204205
if _, err := WaitHostUpdated(conn, d.Id()); err != nil {
205-
return fmt.Errorf("error waiting for EC2 Host (%s) update: %w", d.Id(), err)
206+
return fmt.Errorf("waiting for EC2 Host (%s) update: %w", d.Id(), err)
206207
}
207208
}
208209

209210
if d.HasChange("tags_all") {
210211
o, n := d.GetChange("tags_all")
211212
if err := UpdateTags(conn, d.Id(), o, n); err != nil {
212-
return fmt.Errorf("error updating EC2 Host (%s) tags: %w", d.Id(), err)
213+
return fmt.Errorf("updating EC2 Host (%s) tags: %w", d.Id(), err)
213214
}
214215
}
215216

@@ -233,11 +234,11 @@ func resourceHostDelete(d *schema.ResourceData, meta interface{}) error {
233234
}
234235

235236
if err != nil {
236-
return fmt.Errorf("error releasing EC2 Host (%s): %w", d.Id(), err)
237+
return fmt.Errorf("releasing EC2 Host (%s): %w", d.Id(), err)
237238
}
238239

239240
if _, err := WaitHostDeleted(conn, d.Id()); err != nil {
240-
return fmt.Errorf("error waiting for EC2 Host (%s) delete: %w", d.Id(), err)
241+
return fmt.Errorf("waiting for EC2 Host (%s) delete: %w", d.Id(), err)
241242
}
242243

243244
return nil

internal/service/ec2/ec2_host_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ func testAccCheckHostDestroy(s *terraform.State) error {
242242
func testAccHostConfig_basic() string {
243243
return acctest.ConfigCompose(acctest.ConfigAvailableAZsNoOptIn(), `
244244
resource "aws_ec2_host" "test" {
245-
availability_zone = data.aws_availability_zones.available.names[0]
245+
availability_zone = data.aws_availability_zones.available.names[1]
246246
instance_type = "a1.large"
247247
}
248248
`)

internal/service/ec2/ec2_instance.go

+1
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,7 @@ func resourceInstanceCreate(d *schema.ResourceData, meta interface{}) error {
816816
input := &ec2.RunInstancesInput{
817817
BlockDeviceMappings: instanceOpts.BlockDeviceMappings,
818818
CapacityReservationSpecification: instanceOpts.CapacityReservationSpecification,
819+
ClientToken: aws.String(resource.UniqueId()),
819820
CpuOptions: instanceOpts.CpuOptions,
820821
CreditSpecification: instanceOpts.CreditSpecification,
821822
DisableApiTermination: instanceOpts.DisableAPITermination,

internal/service/ec2/ec2_spot_fleet_request.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -836,14 +836,14 @@ func resourceSpotFleetRequestCreate(d *schema.ResourceData, meta interface{}) er
836836

837837
// http://docs.aws.amazon.com/sdk-for-go/api/service/ec2.html#type-SpotFleetRequestConfigData
838838
spotFleetConfig := &ec2.SpotFleetRequestConfigData{
839+
ClientToken: aws.String(resource.UniqueId()),
839840
IamFleetRole: aws.String(d.Get("iam_fleet_role").(string)),
841+
InstanceInterruptionBehavior: aws.String(d.Get("instance_interruption_behaviour").(string)),
842+
ReplaceUnhealthyInstances: aws.Bool(d.Get("replace_unhealthy_instances").(bool)),
843+
TagSpecifications: tagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeSpotFleetRequest),
840844
TargetCapacity: aws.Int64(int64(d.Get("target_capacity").(int))),
841-
ClientToken: aws.String(resource.UniqueId()),
842845
TerminateInstancesWithExpiration: aws.Bool(d.Get("terminate_instances_with_expiration").(bool)),
843-
ReplaceUnhealthyInstances: aws.Bool(d.Get("replace_unhealthy_instances").(bool)),
844-
InstanceInterruptionBehavior: aws.String(d.Get("instance_interruption_behaviour").(string)),
845846
Type: aws.String(d.Get("fleet_type").(string)),
846-
TagSpecifications: tagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeSpotFleetRequest),
847847
}
848848

849849
if launchSpecificationOk {

internal/service/ec2/ec2_spot_instance_request.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ func ResourceSpotInstanceRequest() *schema.Resource {
2626
Read: resourceSpotInstanceRequestRead,
2727
Delete: resourceSpotInstanceRequestDelete,
2828
Update: resourceSpotInstanceRequestUpdate,
29+
2930
Importer: &schema.ResourceImporter{
3031
State: schema.ImportStatePassthrough,
3132
},
@@ -140,16 +141,12 @@ func resourceSpotInstanceRequestCreate(d *schema.ResourceData, meta interface{})
140141
}
141142

142143
spotOpts := &ec2.RequestSpotInstancesInput{
143-
SpotPrice: aws.String(d.Get("spot_price").(string)),
144-
Type: aws.String(d.Get("spot_type").(string)),
145-
InstanceInterruptionBehavior: aws.String(d.Get("instance_interruption_behavior").(string)),
146-
TagSpecifications: tagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeSpotInstancesRequest),
147-
144+
ClientToken: aws.String(resource.UniqueId()),
148145
// Though the AWS API supports creating spot instance requests for multiple
149146
// instances, for TF purposes we fix this to one instance per request.
150147
// Users can get equivalent behavior out of TF's "count" meta-parameter.
151-
InstanceCount: aws.Int64(1),
152-
148+
InstanceCount: aws.Int64(1),
149+
InstanceInterruptionBehavior: aws.String(d.Get("instance_interruption_behavior").(string)),
153150
LaunchSpecification: &ec2.RequestSpotLaunchSpecification{
154151
BlockDeviceMappings: instanceOpts.BlockDeviceMappings,
155152
EbsOptimized: instanceOpts.EBSOptimized,
@@ -164,6 +161,9 @@ func resourceSpotInstanceRequestCreate(d *schema.ResourceData, meta interface{})
164161
UserData: instanceOpts.UserData64,
165162
NetworkInterfaces: instanceOpts.NetworkInterfaces,
166163
},
164+
SpotPrice: aws.String(d.Get("spot_price").(string)),
165+
TagSpecifications: tagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeSpotInstancesRequest),
166+
Type: aws.String(d.Get("spot_type").(string)),
167167
}
168168

169169
if v, ok := d.GetOk("block_duration_minutes"); ok {

internal/service/ec2/vpc_egress_only_internet_gateway.go

+8-7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"github.com/aws/aws-sdk-go/aws"
88
"github.com/aws/aws-sdk-go/service/ec2"
99
"github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr"
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
1011
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1112
"github.com/hashicorp/terraform-provider-aws/internal/conns"
1213
tftags "github.com/hashicorp/terraform-provider-aws/internal/tags"
@@ -45,15 +46,15 @@ func resourceEgressOnlyInternetGatewayCreate(d *schema.ResourceData, meta interf
4546
tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{})))
4647

4748
input := &ec2.CreateEgressOnlyInternetGatewayInput{
49+
ClientToken: aws.String(resource.UniqueId()),
4850
TagSpecifications: tagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeEgressOnlyInternetGateway),
4951
VpcId: aws.String(d.Get("vpc_id").(string)),
5052
}
5153

52-
log.Printf("[DEBUG] Creating EC2 Egress-only Internet Gateway: %s", input)
5354
output, err := conn.CreateEgressOnlyInternetGateway(input)
5455

5556
if err != nil {
56-
return fmt.Errorf("error creating EC2 Egress-only Internet Gateway: %w", err)
57+
return fmt.Errorf("creating EC2 Egress-only Internet Gateway: %w", err)
5758
}
5859

5960
d.SetId(aws.StringValue(output.EgressOnlyInternetGateway.EgressOnlyInternetGatewayId))
@@ -77,7 +78,7 @@ func resourceEgressOnlyInternetGatewayRead(d *schema.ResourceData, meta interfac
7778
}
7879

7980
if err != nil {
80-
return fmt.Errorf("error reading EC2 Egress-only Internet Gateway (%s): %w", d.Id(), err)
81+
return fmt.Errorf("reading EC2 Egress-only Internet Gateway (%s): %w", d.Id(), err)
8182
}
8283

8384
ig := outputRaw.(*ec2.EgressOnlyInternetGateway)
@@ -92,11 +93,11 @@ func resourceEgressOnlyInternetGatewayRead(d *schema.ResourceData, meta interfac
9293

9394
//lintignore:AWSR002
9495
if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil {
95-
return fmt.Errorf("error setting tags: %w", err)
96+
return fmt.Errorf("setting tags: %w", err)
9697
}
9798

9899
if err := d.Set("tags_all", tags.Map()); err != nil {
99-
return fmt.Errorf("error setting tags_all: %w", err)
100+
return fmt.Errorf("setting tags_all: %w", err)
100101
}
101102

102103
return nil
@@ -109,7 +110,7 @@ func resourceEgressOnlyInternetGatewayUpdate(d *schema.ResourceData, meta interf
109110
o, n := d.GetChange("tags_all")
110111

111112
if err := UpdateTags(conn, d.Id(), o, n); err != nil {
112-
return fmt.Errorf("error updating EC2 Egress-only Internet Gateway (%s) tags: %w", d.Id(), err)
113+
return fmt.Errorf("updating EC2 Egress-only Internet Gateway (%s) tags: %w", d.Id(), err)
113114
}
114115
}
115116

@@ -129,7 +130,7 @@ func resourceEgressOnlyInternetGatewayDelete(d *schema.ResourceData, meta interf
129130
}
130131

131132
if err != nil {
132-
return fmt.Errorf("error deleting EC2 Egress-only Internet Gateway (%s): %w", d.Id(), err)
133+
return fmt.Errorf("deleting EC2 Egress-only Internet Gateway (%s): %w", d.Id(), err)
133134
}
134135

135136
return nil

0 commit comments

Comments
 (0)