From fc6ffe68c6aac2f0e35541e74825ed45531ec0a0 Mon Sep 17 00:00:00 2001 From: Artem Iarmoliuk Date: Tue, 15 May 2018 16:29:42 +0300 Subject: [PATCH 1/5] r/aws_instance: Add support for launch template --- aws/resource_aws_instance.go | 339 +++++++++++++++++++++++++- aws/resource_aws_instance_test.go | 324 +++++++++++++++++++++++- aws/structure.go | 29 +++ aws/tags.go | 25 ++ website/docs/r/instance.html.markdown | 19 +- 5 files changed, 726 insertions(+), 10 deletions(-) diff --git a/aws/resource_aws_instance.go b/aws/resource_aws_instance.go index abcdedb08f5..2b488bedd62 100644 --- a/aws/resource_aws_instance.go +++ b/aws/resource_aws_instance.go @@ -2,6 +2,7 @@ package aws import ( "bytes" + "context" "crypto/sha1" "encoding/base64" "encoding/hex" @@ -16,6 +17,7 @@ import ( "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -51,8 +53,9 @@ func resourceAwsInstance() *schema.Resource { Schema: map[string]*schema.Schema{ "ami": { Type: schema.TypeString, - Required: true, ForceNew: true, + Computed: true, + Optional: true, }, "arn": { Type: schema.TypeString, @@ -119,6 +122,7 @@ func resourceAwsInstance() *schema.Resource { "disable_api_termination": { Type: schema.TypeBool, Optional: true, + Computed: true, }, "ebs_block_device": { Type: schema.TypeSet, @@ -200,6 +204,7 @@ func resourceAwsInstance() *schema.Resource { "ebs_optimized": { Type: schema.TypeBool, Optional: true, + Computed: true, ForceNew: true, }, "enclave_options": { @@ -281,7 +286,8 @@ func resourceAwsInstance() *schema.Resource { }, "instance_type": { Type: schema.TypeString, - Required: true, + Computed: true, + Optional: true, }, "ipv6_address_count": { Type: schema.TypeInt, @@ -305,6 +311,38 @@ func resourceAwsInstance() *schema.Resource { ForceNew: true, Computed: true, }, + "launch_template": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ConflictsWith: []string{"launch_template.0.name"}, + ValidateFunc: validateLaunchTemplateId, + }, + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ConflictsWith: []string{"launch_template.0.id"}, + ValidateFunc: validateLaunchTemplateName, + }, + "version": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 255), + Default: "$Default", + }, + }, + }, + }, "metadata_options": { Type: schema.TypeList, Optional: true, @@ -336,6 +374,7 @@ func resourceAwsInstance() *schema.Resource { "monitoring": { Type: schema.TypeBool, Optional: true, + Computed: true, }, "network_interface": { ConflictsWith: []string{"associate_public_ip_address", "subnet_id", "private_ip", "secondary_private_ips", "vpc_security_group_ids", "security_groups", "ipv6_addresses", "ipv6_address_count", "source_dest_check"}, @@ -516,6 +555,7 @@ func resourceAwsInstance() *schema.Resource { Type: schema.TypeString, Optional: true, ForceNew: true, + Computed: true, ConflictsWith: []string{"user_data_base64"}, DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { // Sometimes the EC2 API responds with the equivalent, empty SHA1 sum @@ -540,6 +580,7 @@ func resourceAwsInstance() *schema.Resource { Type: schema.TypeString, Optional: true, ForceNew: true, + Computed: true, ConflictsWith: []string{"user_data"}, ValidateFunc: func(v interface{}, name string) (warns []string, errs []error) { s := v.(string) @@ -592,7 +633,59 @@ func resourceAwsInstance() *schema.Resource { }, }, - CustomizeDiff: SetTagsDiff, + CustomizeDiff: customdiff.All( + SetTagsDiff, + func(_ context.Context, diff *schema.ResourceDiff, meta interface{}) error { + if diff.HasChange("launch_template.0.version") { + conn := meta.(*AWSClient).ec2conn + + stateVersion := diff.Get("launch_template.0.version") + + var err error + var templateId, instanceVersion, defaultVersion, latestVersion string + + templateId, err = getAwsInstanceLaunchTemplateId(conn, diff.Id()) + if err != nil { + return err + } + + if templateId != "" { + instanceVersion, err = getAwsInstanceLaunchTemplateVersion(conn, diff.Id()) + if err != nil { + return err + } + + _, defaultVersion, latestVersion, err = getAwsLaunchtemplateSpecification(conn, templateId) + if err != nil { + return err + } + } + + switch stateVersion { + case "$Default": + if instanceVersion != defaultVersion { + diff.ForceNew("launch_template.0.version") + } + case "$Latest": + if instanceVersion != latestVersion { + diff.ForceNew("launch_template.0.version") + } + default: + if stateVersion != instanceVersion { + diff.ForceNew("launch_template.0.version") + } + } + } + + return nil + }, + customdiff.ComputedIf("launch_template.0.id", func(_ context.Context, diff *schema.ResourceDiff, meta interface{}) bool { + return diff.HasChange("launch_template.0.name") + }), + customdiff.ComputedIf("launch_template.0.name", func(_ context.Context, diff *schema.ResourceDiff, meta interface{}) bool { + return diff.HasChange("launch_template.0.id") + }), + ), } } @@ -619,7 +712,7 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error { instanceOpts, err := buildAwsInstanceOpts(d, meta) if err != nil { - return err + return fmt.Errorf("error collecting instance settings: %w", err) } tagSpecifications := ec2TagSpecificationsFromKeyValueTags(tags, ec2.ResourceTypeInstance) @@ -639,6 +732,7 @@ func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error { Ipv6AddressCount: instanceOpts.Ipv6AddressCount, Ipv6Addresses: instanceOpts.Ipv6Addresses, KeyName: instanceOpts.KeyName, + LaunchTemplate: instanceOpts.LaunchTemplate, MaxCount: aws.Int64(int64(1)), MinCount: aws.Int64(int64(1)), NetworkInterfaces: instanceOpts.NetworkInterfaces, @@ -792,16 +886,18 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error { // If the instance was not found, return nil so that we can show // that the instance is gone. if isAWSErr(err, "InvalidInstanceID.NotFound", "") { + log.Printf("[WARN] EC2 Instance (%s) not found, removing from state", d.Id()) d.SetId("") return nil } // Some other error, report it - return err + return fmt.Errorf("error retrieving instance (%s): %w", d.Id(), err) } // If nothing was found, then return no state if instance == nil { + log.Printf("[WARN] EC2 Instance (%s) not found, removing from state", d.Id()) d.SetId("") return nil } @@ -867,6 +963,16 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error { d.Set("iam_instance_profile", nil) } + { + launchTemplate, err := getAwsInstanceLaunchTemplate(conn, d) + if err != nil { + return fmt.Errorf("error reading Instance (%s) Launch Template: %w", d.Id(), err) + } + if err := d.Set("launch_template", launchTemplate); err != nil { + return fmt.Errorf("error setting launch_template: %w", err) + } + } + // Set configured Network Interface Device Index Slice // We only want to read, and populate state for the configured network_interface attachments. Otherwise, other // resources have the potential to attach network interfaces to the instance, and cause a perpetual create/destroy @@ -1937,6 +2043,66 @@ func blockDeviceIsRoot(bd *ec2.InstanceBlockDeviceMapping, instance *ec2.Instanc aws.StringValue(bd.DeviceName) == aws.StringValue(instance.RootDeviceName) } +func fetchLaunchTemplateAmi(specs []interface{}, conn *ec2.EC2) (string, error) { + if len(specs) < 1 { + return "", errors.New("Cannot fetch AMI for blank launch template.") + } + + spec := specs[0].(map[string]interface{}) + + idValue, idOk := spec["id"] + nameValue, nameOk := spec["name"] + + if idValue == "" && nameValue == "" { + return "", fmt.Errorf("One of `id` or `name` must be set for `launch_template`") + } + + request := &ec2.DescribeLaunchTemplateVersionsInput{} + + if idOk && idValue != "" { + request.LaunchTemplateId = aws.String(idValue.(string)) + } else if nameOk && nameValue != "" { + request.LaunchTemplateName = aws.String(nameValue.(string)) + } + + var isLatest bool + defaultFilter := []*ec2.Filter{ + { + Name: aws.String("is-default-version"), + Values: aws.StringSlice([]string{"true"}), + }, + } + if v, ok := spec["version"]; ok && v != "" { + switch v { + case "$Default": + request.Filters = defaultFilter + case "$Latest": + isLatest = true + default: + request.Versions = []*string{aws.String(v.(string))} + } + } + + dltv, err := conn.DescribeLaunchTemplateVersions(request) + if err != nil { + return "", err + } + + var ltData *ec2.ResponseLaunchTemplateData + if isLatest { + index := len(dltv.LaunchTemplateVersions) - 1 + ltData = dltv.LaunchTemplateVersions[index].LaunchTemplateData + } else { + ltData = dltv.LaunchTemplateVersions[0].LaunchTemplateData + } + + if ltData.ImageId != nil { + return *ltData.ImageId, nil + } + + return "", nil +} + func fetchRootDeviceName(ami string, conn *ec2.EC2) (*string, error) { if ami == "" { return nil, errors.New("Cannot fetch root device name for blank AMI ID.") @@ -2190,6 +2356,24 @@ func readBlockDeviceMappingsFromConfig(d *schema.ResourceData, conn *ec2.EC2) ([ } } + var ami string + if v, ok := d.GetOk("launch_template"); ok { + var err error + ami, err = fetchLaunchTemplateAmi(v.([]interface{}), conn) + if err != nil { + return nil, err + } + } + + // AMI id from attributes overrides ami from launch template + if v, ok := d.GetOk("ami"); ok { + ami = v.(string) + } + + if ami == "" { + return nil, errors.New("`ami` must be set or provided via launch template") + } + if dn, err := fetchRootDeviceName(d.Get("ami").(string), conn); err == nil { if dn == nil { return nil, fmt.Errorf( @@ -2359,6 +2543,7 @@ type awsInstanceOpts struct { Ipv6AddressCount *int64 Ipv6Addresses []*ec2.InstanceIpv6Address KeyName *string + LaunchTemplate *ec2.LaunchTemplateSpecification NetworkInterfaces []*ec2.InstanceNetworkInterfaceSpecification Placement *ec2.Placement PrivateIPAddress *string @@ -2377,15 +2562,40 @@ type awsInstanceOpts struct { func buildAwsInstanceOpts(d *schema.ResourceData, meta interface{}) (*awsInstanceOpts, error) { conn := meta.(*AWSClient).ec2conn + amiValue, amiOk := d.GetOk("ami") + typeValue, typeOk := d.GetOk("instance_type") + templateValue, templateOk := d.GetOk("launch_template") + + if !amiOk && !templateOk { + return nil, fmt.Errorf("Either `ami` must be set or `launch_template` that defines `image_id` must be provided") + } + + if !typeOk && !templateOk { + return nil, fmt.Errorf("Either `instance_type` must be set or `launch_template` must be provided") + } + instanceType := d.Get("instance_type").(string) opts := &awsInstanceOpts{ DisableAPITermination: aws.Bool(d.Get("disable_api_termination").(bool)), EBSOptimized: aws.Bool(d.Get("ebs_optimized").(bool)), - ImageID: aws.String(d.Get("ami").(string)), - InstanceType: aws.String(instanceType), MetadataOptions: expandEc2InstanceMetadataOptions(d.Get("metadata_options").([]interface{})), EnclaveOptions: expandEc2EnclaveOptions(d.Get("enclave_options").([]interface{})), } + if amiOk { + opts.ImageID = aws.String(amiValue.(string)) + } + + if typeOk { + opts.InstanceType = aws.String(typeValue.(string)) + } + + if templateOk { + var err error + opts.LaunchTemplate, err = expandEc2LaunchTemplateSpecification(templateValue.([]interface{})) + if err != nil { + return nil, err + } + } // Set default cpu_credits as Unlimited for T3 instance type if strings.HasPrefix(instanceType, "t3") { @@ -2884,3 +3094,118 @@ func resourceAwsInstanceFind(conn *ec2.EC2, params *ec2.DescribeInstancesInput) return resp.Reservations[0].Instances, nil } + +func getAwsInstanceLaunchTemplate(conn *ec2.EC2, d *schema.ResourceData) ([]map[string]interface{}, error) { + attrs := map[string]interface{}{} + result := make([]map[string]interface{}, 0) + + id, err := getAwsInstanceLaunchTemplateId(conn, d.Id()) + if err != nil { + return nil, err + } + if id == "" { + return nil, nil + } + + name, defaultVersion, latestVersion, err := getAwsLaunchtemplateSpecification(conn, id) + + if err != nil { + if isAWSErr(err, "InvalidLaunchTemplateId.Malformed", "") { + // Instance is tagged with non existent template just set it to nil + log.Printf("[WARN] Launch template %s not found, removing from state", id) + return nil, nil + } + return nil, fmt.Errorf("error reading Launch Template: %s", err) + } + + attrs["id"] = id + attrs["name"] = name + + version, err := getAwsInstanceLaunchTemplateVersion(conn, d.Id()) + if err != nil { + return nil, err + } + + dltvi := &ec2.DescribeLaunchTemplateVersionsInput{ + LaunchTemplateId: aws.String(id), + Versions: []*string{aws.String(version)}, + } + + if _, err := conn.DescribeLaunchTemplateVersions(dltvi); err != nil { + if isAWSErr(err, "InvalidLaunchTemplateId.VersionNotFound", "") { + // Instance is tagged with non existent template version, just don't set it + log.Printf("[WARN] Launch template %s version %s not found, removing from state", id, version) + result = append(result, attrs) + return result, nil + } + return nil, fmt.Errorf("error reading Launch Template Version: %s", err) + } + + if v, ok := d.GetOk("launch_template.0.version"); ok { + switch v { + case "$Default": + if version == defaultVersion { + attrs["version"] = "$Default" + } else { + attrs["version"] = version + } + case "$Latest": + if version == latestVersion { + attrs["version"] = "$Latest" + } else { + attrs["version"] = version + } + default: + attrs["version"] = version + } + } + + result = append(result, attrs) + + return result, nil +} + +func getAwsInstanceLaunchTemplateId(conn *ec2.EC2, instanceId string) (string, error) { + idTag := "aws:ec2launchtemplate:id" + + launchTemplateId, err := getInstanceTagValue(conn, instanceId, idTag) + if err != nil { + return "", fmt.Errorf("error reading Instance Launch Template Id Tag: %s", err) + } + if launchTemplateId == nil { + return "", nil + } + + return *launchTemplateId, nil +} + +func getAwsInstanceLaunchTemplateVersion(conn *ec2.EC2, instanceId string) (string, error) { + versionTag := "aws:ec2launchtemplate:version" + + launchTemplateVersion, err := getInstanceTagValue(conn, instanceId, versionTag) + if err != nil { + return "", fmt.Errorf("error reading Instance Launch Template Version Tag: %s", err) + } + if launchTemplateVersion == nil { + return "", nil + } + + return *launchTemplateVersion, nil +} + +// getAwsLaunchtemplateSpecification takes conn and template id +// returns name, default version, latest version +func getAwsLaunchtemplateSpecification(conn *ec2.EC2, id string) (string, string, string, error) { + dlt, err := conn.DescribeLaunchTemplates(&ec2.DescribeLaunchTemplatesInput{ + LaunchTemplateIds: []*string{aws.String(id)}, + }) + if err != nil { + return "", "", "", err + } + + name := *dlt.LaunchTemplates[0].LaunchTemplateName + defaultVersion := strconv.FormatInt(*dlt.LaunchTemplates[0].DefaultVersionNumber, 10) + latestVersion := strconv.FormatInt(*dlt.LaunchTemplates[0].LatestVersionNumber, 10) + + return name, defaultVersion, latestVersion, nil +} diff --git a/aws/resource_aws_instance_test.go b/aws/resource_aws_instance_test.go index f5cc1067d4e..bb77111dbc4 100644 --- a/aws/resource_aws_instance_test.go +++ b/aws/resource_aws_instance_test.go @@ -2616,6 +2616,70 @@ func TestAccAWSInstance_associatePublic_overridePrivate(t *testing.T) { }) } +// Test suite for instance launch_template feature +// 1. Create instance without launch template +// 2. Update with launch_template - this should recreate instance +// 3. Root volume size should be overridden from instance attributes +// 4. Creating new template version should not trigger instance recreation when default is used +// 5. Updating from $Default version to exact version should not trigger recreation +// 6. Updating to latest template version should recreate instance +func TestAccAWSInstance_LaunchTemplate(t *testing.T) { + var v1, v2, v3, v4, v5 ec2.Instance + + resName := "aws_instance.foo" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckInstanceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccInstanceConfigCreateWithoutTemplate, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInstanceExists(resName, &v1), + resource.TestCheckResourceAttr(resName, "instance_type", "t2.medium"), + resource.TestCheckResourceAttr(resName, "root_block_device.0.volume_size", "10"), + ), + }, + { + Config: testAccInstanceConfigUpdateWithTemplate, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInstanceExists(resName, &v2), + testAccCheckInstanceRecreated(&v1, &v2), + resource.TestCheckResourceAttr(resName, "instance_type", "t2.micro"), + resource.TestCheckResourceAttr(resName, "root_block_device.0.volume_size", "11"), + ), + }, + { + Config: testAccInstanceConfigUpdateTemplateSettings, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInstanceExists(resName, &v3), + testAccCheckInstanceNotRecreated(&v2, &v3), + resource.TestCheckResourceAttr(resName, "instance_type", "t2.micro"), + ), + }, + { + Config: testAccInstanceConfigUpdateInstanceTemplateVersion, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInstanceExists(resName, &v4), + testAccCheckInstanceNotRecreated(&v3, &v4), + resource.TestCheckResourceAttr(resName, "instance_type", "t2.micro"), + ), + }, + { + Config: testAccInstanceConfigUpdateInstanceTemplateLatest, + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInstanceExists(resName, &v5), + testAccCheckInstanceRecreated(&v4, &v5), + resource.TestCheckResourceAttr(resName, "instance_type", "t2.nano"), + resource.TestCheckResourceAttr(resName, "root_block_device.0.volume_size", "10"), + ), + }, + }, + }) +} + func TestAccAWSInstance_getPasswordData_falseToTrue(t *testing.T) { var before, after ec2.Instance resourceName := "aws_instance.test" @@ -6008,7 +6072,9 @@ resource "aws_subnet" "test" { } func testAccInstanceConfigHibernation(hibernation bool) string { - return composeConfig(testAccLatestAmazonLinuxHvmEbsAmiConfig(), fmt.Sprintf(` + return composeConfig( + testAccLatestAmazonLinuxHvmEbsAmiConfig(), + fmt.Sprintf(` resource "aws_vpc" "test" { cidr_block = "10.1.0.0/16" @@ -6258,3 +6324,259 @@ resource "aws_ec2_capacity_reservation" "test" { } `, rName, ec2.CapacityReservationInstancePlatformLinuxUnix)) } + +const testAccInstanceConfigCreateWithoutTemplate = ` +data "aws_ami" "debian_jessie_latest" { + most_recent = true + + filter { + name = "name" + values = ["debian-jessie-*"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } + + filter { + name = "architecture" + values = ["x86_64"] + } + + filter { + name = "root-device-type" + values = ["ebs"] + } + + owners = ["379101102735"] # Debian +} + +resource "aws_instance" "foo" { + ami = "${data.aws_ami.debian_jessie_latest.id}" + associate_public_ip_address = true + instance_type = "t2.medium" + + root_block_device { + volume_size = "10" + volume_type = "standard" + delete_on_termination = true + } + + tags = { + Name = "test-terraform" + } +} +` + +const testAccInstanceConfigUpdateWithTemplate = ` +data "aws_ami" "debian_jessie_latest" { + most_recent = true + + filter { + name = "name" + values = ["debian-jessie-*"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } + + filter { + name = "architecture" + values = ["x86_64"] + } + + filter { + name = "root-device-type" + values = ["ebs"] + } + + owners = ["379101102735"] # Debian +} + +resource "aws_launch_template" "foobar" { + name_prefix = "foobar" + image_id = "${data.aws_ami.debian_jessie_latest.id}" + instance_type = "t2.micro" +} + +resource "aws_instance" "foo" { + ami = "${data.aws_ami.debian_jessie_latest.id}" + associate_public_ip_address = true + + launch_template { + id = "${aws_launch_template.foobar.id}" + } + + root_block_device { + volume_size = "11" + volume_type = "standard" + delete_on_termination = true + } + + tags = { + Name = "test-terraform" + } +} +` + +const testAccInstanceConfigUpdateTemplateSettings = ` +data "aws_ami" "debian_jessie_latest" { + most_recent = true + + filter { + name = "name" + values = ["debian-jessie-*"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } + + filter { + name = "architecture" + values = ["x86_64"] + } + + filter { + name = "root-device-type" + values = ["ebs"] + } + + owners = ["379101102735"] # Debian +} + +resource "aws_launch_template" "foobar" { + name_prefix = "foobar" + image_id = "${data.aws_ami.debian_jessie_latest.id}" + instance_type = "t2.nano" +} + +resource "aws_instance" "foo" { + associate_public_ip_address = true + + launch_template { + id = "${aws_launch_template.foobar.id}" + } + + root_block_device { + volume_size = "11" + volume_type = "standard" + delete_on_termination = true + } + + tags = { + Name = "test-terraform" + } +} +` + +const testAccInstanceConfigUpdateInstanceTemplateVersion = ` +data "aws_ami" "debian_jessie_latest" { + most_recent = true + + filter { + name = "name" + values = ["debian-jessie-*"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } + + filter { + name = "architecture" + values = ["x86_64"] + } + + filter { + name = "root-device-type" + values = ["ebs"] + } + + owners = ["379101102735"] # Debian +} + +resource "aws_launch_template" "foobar" { + name_prefix = "foobar" + image_id = "${data.aws_ami.debian_jessie_latest.id}" + instance_type = "t2.nano" +} + +resource "aws_instance" "foo" { + associate_public_ip_address = true + + launch_template { + id = "${aws_launch_template.foobar.id}" + version = "1" + } + + root_block_device { + volume_size = "11" + volume_type = "standard" + delete_on_termination = true + } + + tags = { + Name = "test-terraform" + } +} +` + +const testAccInstanceConfigUpdateInstanceTemplateLatest = ` +data "aws_ami" "debian_jessie_latest" { + most_recent = true + + filter { + name = "name" + values = ["debian-jessie-*"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } + + filter { + name = "architecture" + values = ["x86_64"] + } + + filter { + name = "root-device-type" + values = ["ebs"] + } + + owners = ["379101102735"] # Debian +} + +resource "aws_launch_template" "foobar" { + name_prefix = "foobar" + image_id = "${data.aws_ami.debian_jessie_latest.id}" + instance_type = "t2.nano" +} + +resource "aws_instance" "foo" { + ami = "${data.aws_ami.debian_jessie_latest.id}" + associate_public_ip_address = true + + launch_template { + id = "${aws_launch_template.foobar.id}" + version = "${aws_launch_template.foobar.latest_version}" + } + + root_block_device { + volume_size = "10" + volume_type = "standard" + delete_on_termination = true + } + + tags = { + Name = "test-terraform" + } +} +` diff --git a/aws/structure.go b/aws/structure.go index 3acada8709e..658f68d9370 100644 --- a/aws/structure.go +++ b/aws/structure.go @@ -2721,6 +2721,35 @@ func expandLaunchTemplateSpecification(specs []interface{}) (*autoscaling.Launch return result, nil } +func expandEc2LaunchTemplateSpecification(specs []interface{}) (*ec2.LaunchTemplateSpecification, error) { + if len(specs) < 1 { + return nil, nil + } + + spec := specs[0].(map[string]interface{}) + + idValue, idOk := spec["id"] + nameValue, nameOk := spec["name"] + + if idValue == "" && nameValue == "" { + return nil, fmt.Errorf("One of `id` or `name` must be set for `launch_template`") + } + + result := &ec2.LaunchTemplateSpecification{} + + if idOk && idValue != "" { + result.LaunchTemplateId = aws.String(idValue.(string)) + } else if nameOk && nameValue != "" { + result.LaunchTemplateName = aws.String(nameValue.(string)) + } + + if v, ok := spec["version"]; ok && v != "" { + result.Version = aws.String(v.(string)) + } + + return result, nil +} + func flattenLaunchTemplateSpecification(lt *autoscaling.LaunchTemplateSpecification) []map[string]interface{} { if lt == nil { return []map[string]interface{}{} diff --git a/aws/tags.go b/aws/tags.go index 125b976bd18..cc65edf57af 100644 --- a/aws/tags.go +++ b/aws/tags.go @@ -133,3 +133,28 @@ func SetTagsDiff(_ context.Context, diff *schema.ResourceDiff, meta interface{}) return nil } + +// getInstanceTagValue returns instance tag value by name +func getInstanceTagValue(conn *ec2.EC2, instanceId string, tagKey string) (*string, error) { + tagsResp, err := conn.DescribeTags(&ec2.DescribeTagsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("resource-id"), + Values: []*string{aws.String(instanceId)}, + }, + { + Name: aws.String("key"), + Values: []*string{aws.String(tagKey)}, + }, + }, + }) + if err != nil { + return nil, err + } + + if len(tagsResp.Tags) != 1 { + return nil, nil + } + + return tagsResp.Tags[0].Value, nil +} diff --git a/website/docs/r/instance.html.markdown b/website/docs/r/instance.html.markdown index 6a566dcb855..3971afe985d 100644 --- a/website/docs/r/instance.html.markdown +++ b/website/docs/r/instance.html.markdown @@ -90,7 +90,7 @@ resource "aws_instance" "foo" { The following arguments are supported: -* `ami` - (Required) AMI to use for the instance. +* `ami` - (Optional) AMI to use for the instance. Required unless `launch_template` is specified and the Launch Template specifes an AMI. If an AMI is specified in the Launch Template, setting `ami` will override the AMI specified in the Launch Template. * `associate_public_ip_address` - (Optional) Whether to associate a public IP address with an instance in a VPC. * `availability_zone` - (Optional) AZ to start the instance in. * `capacity_reservation_specification` - (Optional) Describes an instance's Capacity Reservation targeting option. See [Capacity Reservation Specification](#capacity-reservation-specification) below for more details. @@ -110,10 +110,12 @@ The following arguments are supported: * `host_id` - (Optional) ID of a dedicated host that the instance will be assigned to. Use when an instance is to be launched on a specific dedicated host. * `iam_instance_profile` - (Optional) IAM Instance Profile to launch the instance with. Specified as the name of the Instance Profile. Ensure your credentials have the correct permission to assign the instance profile according to the [EC2 documentation](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2.html#roles-usingrole-ec2instance-permissions), notably `iam:PassRole`. * `instance_initiated_shutdown_behavior` - (Optional) Shutdown behavior for the instance. Amazon defaults this to `stop` for EBS-backed instances and `terminate` for instance-store instances. Cannot be set on instance-store instances. See [Shutdown Behavior](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/terminating-instances.html#Using_ChangingInstanceInitiatedShutdownBehavior) for more information. -* `instance_type` - (Required) Type of instance to start. Updates to this field will trigger a stop/start of the EC2 instance. +* `instance_type` - (Optional) The instance type to use for the instance. Updates to this field will trigger a stop/start of the EC2 instance. * `ipv6_address_count`- (Optional) A number of IPv6 addresses to associate with the primary network interface. Amazon EC2 chooses the IPv6 addresses from the range of your subnet. * `ipv6_addresses` - (Optional) Specify one or more IPv6 addresses from the range of the subnet to associate with the primary network interface * `key_name` - (Optional) Key name of the Key Pair to use for the instance; which can be managed using [the `aws_key_pair` resource](key_pair.html). +* `launch_template` - (Optional) Specifies a Launch Template to configure the instance. Parameters configured on this resource will override the corresponding parameters in the Launch Template. + See [Launch Template Specification](#launch-template-specification) below for more details. * `metadata_options` - (Optional) Customize the metadata options of the instance. See [Metadata Options](#metadata-options) below for more details. * `monitoring` - (Optional) If true, the launched EC2 instance will have detailed monitoring enabled. (Available since v0.6.0) * `network_interface` - (Optional) Customize network interfaces to be attached at instance boot time. See [Network Interfaces](#network-interfaces) below for more details. @@ -250,6 +252,19 @@ Each `network_interface` block supports the following: * `device_index` - (Required) Integer index of the network interface attachment. Limited by instance type. * `network_interface_id` - (Required) ID of the network interface to attach. +### Launch Template Specification + +-> **Note:** Launch Template parameters will be used only once during instance creation. If you want to update existing instance you need to change parameters +directly. Updating Launch Template specification will force a new instance. + +Any other instance parameters that you specify will override the same parameters in the launch template. + +The `launch_template` block supports the following: + +* `id` - The ID of the launch template. Conflicts with `name`. +* `name` - The name of the launch template. Conflicts with `id`. +* `version` - Template version. Can be a specific version number, `$Latest` or `$Default`. The default value is `$Default`. + ## Attributes Reference In addition to all arguments above, the following attributes are exported: From 318652c6c75522792cf66ad55180b097697a270a Mon Sep 17 00:00:00 2001 From: Artem Yarmoluk Date: Wed, 5 Feb 2020 23:46:05 +0800 Subject: [PATCH 2/5] r/aws_instance: improve attribute validation with builtin schema helpers --- aws/resource_aws_autoscaling_group.go | 8 +-- aws/resource_aws_instance.go | 89 +++++++++++---------------- aws/resource_aws_instance_test.go | 24 ++++---- aws/structure.go | 22 +++---- 4 files changed, 58 insertions(+), 85 deletions(-) diff --git a/aws/resource_aws_autoscaling_group.go b/aws/resource_aws_autoscaling_group.go index dcbb4805b10..438d5eca616 100644 --- a/aws/resource_aws_autoscaling_group.go +++ b/aws/resource_aws_autoscaling_group.go @@ -687,11 +687,7 @@ func resourceAwsAutoscalingGroupCreate(d *schema.ResourceData, meta interface{}) } if v, ok := d.GetOk("launch_template"); ok { - var err error - createOpts.LaunchTemplate, err = expandLaunchTemplateSpecification(v.([]interface{})) - if err != nil { - return err - } + createOpts.LaunchTemplate = expandLaunchTemplateSpecification(v.([]interface{})) } // Availability Zones are optional if VPC Zone Identifier(s) are specified @@ -1055,7 +1051,7 @@ func resourceAwsAutoscalingGroupUpdate(d *schema.ResourceData, meta interface{}) if d.HasChange("launch_template") { if v, ok := d.GetOk("launch_template"); ok && len(v.([]interface{})) > 0 { - opts.LaunchTemplate, _ = expandLaunchTemplateSpecification(v.([]interface{})) + opts.LaunchTemplate = expandLaunchTemplateSpecification(v.([]interface{})) } shouldRefreshInstances = true } diff --git a/aws/resource_aws_instance.go b/aws/resource_aws_instance.go index 2b488bedd62..f23803a8dcc 100644 --- a/aws/resource_aws_instance.go +++ b/aws/resource_aws_instance.go @@ -52,10 +52,11 @@ func resourceAwsInstance() *schema.Resource { Schema: map[string]*schema.Schema{ "ami": { - Type: schema.TypeString, - ForceNew: true, - Computed: true, - Optional: true, + Type: schema.TypeString, + ForceNew: true, + Computed: true, + Optional: true, + AtLeastOneOf: []string{"ami", "launch_template"}, }, "arn": { Type: schema.TypeString, @@ -285,9 +286,10 @@ func resourceAwsInstance() *schema.Resource { Computed: true, }, "instance_type": { - Type: schema.TypeString, - Computed: true, - Optional: true, + Type: schema.TypeString, + Computed: true, + Optional: true, + AtLeastOneOf: []string{"instance_type", "launch_template"}, }, "ipv6_address_count": { Type: schema.TypeInt, @@ -312,27 +314,28 @@ func resourceAwsInstance() *schema.Resource { Computed: true, }, "launch_template": { - Type: schema.TypeList, - MaxItems: 1, - Optional: true, - ForceNew: true, + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + ForceNew: true, + AtLeastOneOf: []string{"ami", "instance_type", "launch_template"}, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - ConflictsWith: []string{"launch_template.0.name"}, - ValidateFunc: validateLaunchTemplateId, + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ExactlyOneOf: []string{"launch_template.0.name", "launch_template.0.id"}, + ValidateFunc: validateLaunchTemplateId, }, "name": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - ConflictsWith: []string{"launch_template.0.id"}, - ValidateFunc: validateLaunchTemplateName, + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ExactlyOneOf: []string{"launch_template.0.name", "launch_template.0.id"}, + ValidateFunc: validateLaunchTemplateName, }, "version": { Type: schema.TypeString, @@ -2053,10 +2056,6 @@ func fetchLaunchTemplateAmi(specs []interface{}, conn *ec2.EC2) (string, error) idValue, idOk := spec["id"] nameValue, nameOk := spec["name"] - if idValue == "" && nameValue == "" { - return "", fmt.Errorf("One of `id` or `name` must be set for `launch_template`") - } - request := &ec2.DescribeLaunchTemplateVersionsInput{} if idOk && idValue != "" { @@ -2374,11 +2373,11 @@ func readBlockDeviceMappingsFromConfig(d *schema.ResourceData, conn *ec2.EC2) ([ return nil, errors.New("`ami` must be set or provided via launch template") } - if dn, err := fetchRootDeviceName(d.Get("ami").(string), conn); err == nil { + if dn, err := fetchRootDeviceName(ami, conn); err == nil { if dn == nil { return nil, fmt.Errorf( "Expected 1 AMI for ID: %s, got none", - d.Get("ami").(string)) + ami) } blockDevices = append(blockDevices, &ec2.BlockDeviceMapping{ @@ -2562,41 +2561,27 @@ type awsInstanceOpts struct { func buildAwsInstanceOpts(d *schema.ResourceData, meta interface{}) (*awsInstanceOpts, error) { conn := meta.(*AWSClient).ec2conn - amiValue, amiOk := d.GetOk("ami") - typeValue, typeOk := d.GetOk("instance_type") - templateValue, templateOk := d.GetOk("launch_template") - - if !amiOk && !templateOk { - return nil, fmt.Errorf("Either `ami` must be set or `launch_template` that defines `image_id` must be provided") - } - - if !typeOk && !templateOk { - return nil, fmt.Errorf("Either `instance_type` must be set or `launch_template` must be provided") - } - - instanceType := d.Get("instance_type").(string) opts := &awsInstanceOpts{ DisableAPITermination: aws.Bool(d.Get("disable_api_termination").(bool)), EBSOptimized: aws.Bool(d.Get("ebs_optimized").(bool)), MetadataOptions: expandEc2InstanceMetadataOptions(d.Get("metadata_options").([]interface{})), EnclaveOptions: expandEc2EnclaveOptions(d.Get("enclave_options").([]interface{})), } - if amiOk { - opts.ImageID = aws.String(amiValue.(string)) + + if v, ok := d.GetOk("ami"); ok { + opts.ImageID = aws.String(v.(string)) } - if typeOk { - opts.InstanceType = aws.String(typeValue.(string)) + if v, ok := d.GetOk("instance_type"); ok { + opts.InstanceType = aws.String(v.(string)) } - if templateOk { - var err error - opts.LaunchTemplate, err = expandEc2LaunchTemplateSpecification(templateValue.([]interface{})) - if err != nil { - return nil, err - } + if v, ok := d.GetOk("launch_template"); ok { + opts.LaunchTemplate = expandEc2LaunchTemplateSpecification(v.([]interface{})) } + instanceType := d.Get("instance_type").(string) + // Set default cpu_credits as Unlimited for T3 instance type if strings.HasPrefix(instanceType, "t3") { opts.CreditSpecification = &ec2.CreditSpecificationRequest{ diff --git a/aws/resource_aws_instance_test.go b/aws/resource_aws_instance_test.go index bb77111dbc4..424930c0c17 100644 --- a/aws/resource_aws_instance_test.go +++ b/aws/resource_aws_instance_test.go @@ -6353,7 +6353,7 @@ data "aws_ami" "debian_jessie_latest" { } resource "aws_instance" "foo" { - ami = "${data.aws_ami.debian_jessie_latest.id}" + ami = data.aws_ami.debian_jessie_latest.id associate_public_ip_address = true instance_type = "t2.medium" @@ -6398,16 +6398,16 @@ data "aws_ami" "debian_jessie_latest" { resource "aws_launch_template" "foobar" { name_prefix = "foobar" - image_id = "${data.aws_ami.debian_jessie_latest.id}" + image_id = data.aws_ami.debian_jessie_latest.id instance_type = "t2.micro" } resource "aws_instance" "foo" { - ami = "${data.aws_ami.debian_jessie_latest.id}" + ami = data.aws_ami.debian_jessie_latest.id associate_public_ip_address = true launch_template { - id = "${aws_launch_template.foobar.id}" + id = aws_launch_template.foobar.id } root_block_device { @@ -6451,7 +6451,7 @@ data "aws_ami" "debian_jessie_latest" { resource "aws_launch_template" "foobar" { name_prefix = "foobar" - image_id = "${data.aws_ami.debian_jessie_latest.id}" + image_id = data.aws_ami.debian_jessie_latest.id instance_type = "t2.nano" } @@ -6459,7 +6459,7 @@ resource "aws_instance" "foo" { associate_public_ip_address = true launch_template { - id = "${aws_launch_template.foobar.id}" + id = aws_launch_template.foobar.id } root_block_device { @@ -6503,7 +6503,7 @@ data "aws_ami" "debian_jessie_latest" { resource "aws_launch_template" "foobar" { name_prefix = "foobar" - image_id = "${data.aws_ami.debian_jessie_latest.id}" + image_id = data.aws_ami.debian_jessie_latest.id instance_type = "t2.nano" } @@ -6511,7 +6511,7 @@ resource "aws_instance" "foo" { associate_public_ip_address = true launch_template { - id = "${aws_launch_template.foobar.id}" + id = aws_launch_template.foobar.id version = "1" } @@ -6556,17 +6556,17 @@ data "aws_ami" "debian_jessie_latest" { resource "aws_launch_template" "foobar" { name_prefix = "foobar" - image_id = "${data.aws_ami.debian_jessie_latest.id}" + image_id = data.aws_ami.debian_jessie_latest.id instance_type = "t2.nano" } resource "aws_instance" "foo" { - ami = "${data.aws_ami.debian_jessie_latest.id}" + ami = data.aws_ami.debian_jessie_latest.id associate_public_ip_address = true launch_template { - id = "${aws_launch_template.foobar.id}" - version = "${aws_launch_template.foobar.latest_version}" + id = aws_launch_template.foobar.id + version = aws_launch_template.foobar.latest_version } root_block_device { diff --git a/aws/structure.go b/aws/structure.go index 658f68d9370..ee10ae77e2e 100644 --- a/aws/structure.go +++ b/aws/structure.go @@ -2690,9 +2690,9 @@ func flattenIotThingTypeProperties(s *iot.ThingTypeProperties) []map[string]inte return []map[string]interface{}{m} } -func expandLaunchTemplateSpecification(specs []interface{}) (*autoscaling.LaunchTemplateSpecification, error) { +func expandLaunchTemplateSpecification(specs []interface{}) *autoscaling.LaunchTemplateSpecification { if len(specs) < 1 { - return nil, nil + return nil } spec := specs[0].(map[string]interface{}) @@ -2700,10 +2700,6 @@ func expandLaunchTemplateSpecification(specs []interface{}) (*autoscaling.Launch idValue, idOk := spec["id"] nameValue, nameOk := spec["name"] - if idValue == "" && nameValue == "" { - return nil, fmt.Errorf("One of `id` or `name` must be set for `launch_template`") - } - result := &autoscaling.LaunchTemplateSpecification{} // DescribeAutoScalingGroups returns both name and id but LaunchTemplateSpecification @@ -2718,12 +2714,12 @@ func expandLaunchTemplateSpecification(specs []interface{}) (*autoscaling.Launch result.Version = aws.String(v.(string)) } - return result, nil + return result } -func expandEc2LaunchTemplateSpecification(specs []interface{}) (*ec2.LaunchTemplateSpecification, error) { +func expandEc2LaunchTemplateSpecification(specs []interface{}) *ec2.LaunchTemplateSpecification { if len(specs) < 1 { - return nil, nil + return nil } spec := specs[0].(map[string]interface{}) @@ -2731,10 +2727,6 @@ func expandEc2LaunchTemplateSpecification(specs []interface{}) (*ec2.LaunchTempl idValue, idOk := spec["id"] nameValue, nameOk := spec["name"] - if idValue == "" && nameValue == "" { - return nil, fmt.Errorf("One of `id` or `name` must be set for `launch_template`") - } - result := &ec2.LaunchTemplateSpecification{} if idOk && idValue != "" { @@ -2747,7 +2739,7 @@ func expandEc2LaunchTemplateSpecification(specs []interface{}) (*ec2.LaunchTempl result.Version = aws.String(v.(string)) } - return result, nil + return result } func flattenLaunchTemplateSpecification(lt *autoscaling.LaunchTemplateSpecification) []map[string]interface{} { @@ -2762,7 +2754,7 @@ func flattenLaunchTemplateSpecification(lt *autoscaling.LaunchTemplateSpecificat attrs["id"] = *lt.LaunchTemplateId attrs["name"] = *lt.LaunchTemplateName - // version is returned only if it was previosly set + // version is returned only if it was previously set if lt.Version != nil { attrs["version"] = *lt.Version } else { From c5ddb64005f8d1be902bd6bf2b21471828609f65 Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Mon, 19 Jul 2021 21:40:27 -0700 Subject: [PATCH 3/5] Removes functions from `structure.go` --- aws/resource_aws_autoscaling_group.go | 61 +++++++++++++++++++-- aws/resource_aws_instance.go | 33 ++++++++++-- aws/structure.go | 76 --------------------------- 3 files changed, 85 insertions(+), 85 deletions(-) diff --git a/aws/resource_aws_autoscaling_group.go b/aws/resource_aws_autoscaling_group.go index 438d5eca616..9587730ed82 100644 --- a/aws/resource_aws_autoscaling_group.go +++ b/aws/resource_aws_autoscaling_group.go @@ -1887,7 +1887,7 @@ func expandAutoScalingInstancesDistribution(l []interface{}) *autoscaling.Instan return instancesDistribution } -func expandAutoScalingLaunchTemplate(l []interface{}) *autoscaling.LaunchTemplate { +func expandMixedInstancesLaunchTemplate(l []interface{}) *autoscaling.LaunchTemplate { if len(l) == 0 || l[0] == nil { return nil } @@ -1895,7 +1895,7 @@ func expandAutoScalingLaunchTemplate(l []interface{}) *autoscaling.LaunchTemplat m := l[0].(map[string]interface{}) launchTemplate := &autoscaling.LaunchTemplate{ - LaunchTemplateSpecification: expandAutoScalingLaunchTemplateSpecification(m["launch_template_specification"].([]interface{})), + LaunchTemplateSpecification: expandMixedInstancesLaunchTemplateSpecification(m["launch_template_specification"].([]interface{})), } if v, ok := m["override"]; ok { @@ -1930,7 +1930,7 @@ func expandAutoScalingLaunchTemplateOverride(m map[string]interface{}) *autoscal } if v, ok := m["launch_template_specification"]; ok && v.([]interface{}) != nil { - launchTemplateOverrides.LaunchTemplateSpecification = expandAutoScalingLaunchTemplateSpecification(m["launch_template_specification"].([]interface{})) + launchTemplateOverrides.LaunchTemplateSpecification = expandMixedInstancesLaunchTemplateSpecification(m["launch_template_specification"].([]interface{})) } if v, ok := m["weighted_capacity"]; ok && v.(string) != "" { @@ -1940,7 +1940,7 @@ func expandAutoScalingLaunchTemplateOverride(m map[string]interface{}) *autoscal return launchTemplateOverrides } -func expandAutoScalingLaunchTemplateSpecification(l []interface{}) *autoscaling.LaunchTemplateSpecification { +func expandMixedInstancesLaunchTemplateSpecification(l []interface{}) *autoscaling.LaunchTemplateSpecification { launchTemplateSpecification := &autoscaling.LaunchTemplateSpecification{} if len(l) == 0 || l[0] == nil { @@ -1975,7 +1975,7 @@ func expandAutoScalingMixedInstancesPolicy(l []interface{}) *autoscaling.MixedIn m := l[0].(map[string]interface{}) mixedInstancesPolicy := &autoscaling.MixedInstancesPolicy{ - LaunchTemplate: expandAutoScalingLaunchTemplate(m["launch_template"].([]interface{})), + LaunchTemplate: expandMixedInstancesLaunchTemplate(m["launch_template"].([]interface{})), } if v, ok := m["instances_distribution"]; ok { @@ -2290,3 +2290,54 @@ func validateAutoScalingGroupInstanceRefreshTriggerFields(i interface{}, path ct return diag.Errorf("'%s' is not a recognized parameter name for aws_autoscaling_group", v) } + +func expandLaunchTemplateSpecification(specs []interface{}) *autoscaling.LaunchTemplateSpecification { + if len(specs) < 1 { + return nil + } + + spec := specs[0].(map[string]interface{}) + + idValue, idOk := spec["id"] + nameValue, nameOk := spec["name"] + + result := &autoscaling.LaunchTemplateSpecification{} + + // DescribeAutoScalingGroups returns both name and id but LaunchTemplateSpecification + // allows only one of them to be set + if idOk && idValue != "" { + result.LaunchTemplateId = aws.String(idValue.(string)) + } else if nameOk && nameValue != "" { + result.LaunchTemplateName = aws.String(nameValue.(string)) + } + + if v, ok := spec["version"]; ok && v != "" { + result.Version = aws.String(v.(string)) + } + + return result +} + +func flattenLaunchTemplateSpecification(lt *autoscaling.LaunchTemplateSpecification) []map[string]interface{} { + if lt == nil { + return []map[string]interface{}{} + } + + attrs := map[string]interface{}{} + result := make([]map[string]interface{}, 0) + + // id and name are always returned by DescribeAutoscalingGroups + attrs["id"] = aws.StringValue(lt.LaunchTemplateId) + attrs["name"] = aws.StringValue(lt.LaunchTemplateName) + + // version is returned only if it was previously set + if lt.Version != nil { + attrs["version"] = aws.StringValue(lt.Version) + } else { + attrs["version"] = nil + } + + result = append(result, attrs) + + return result +} diff --git a/aws/resource_aws_instance.go b/aws/resource_aws_instance.go index f23803a8dcc..6e50473399f 100644 --- a/aws/resource_aws_instance.go +++ b/aws/resource_aws_instance.go @@ -658,7 +658,7 @@ func resourceAwsInstance() *schema.Resource { return err } - _, defaultVersion, latestVersion, err = getAwsLaunchtemplateSpecification(conn, templateId) + _, defaultVersion, latestVersion, err = getAwsLaunchTemplateSpecification(conn, templateId) if err != nil { return err } @@ -3092,7 +3092,7 @@ func getAwsInstanceLaunchTemplate(conn *ec2.EC2, d *schema.ResourceData) ([]map[ return nil, nil } - name, defaultVersion, latestVersion, err := getAwsLaunchtemplateSpecification(conn, id) + name, defaultVersion, latestVersion, err := getAwsLaunchTemplateSpecification(conn, id) if err != nil { if isAWSErr(err, "InvalidLaunchTemplateId.Malformed", "") { @@ -3178,9 +3178,9 @@ func getAwsInstanceLaunchTemplateVersion(conn *ec2.EC2, instanceId string) (stri return *launchTemplateVersion, nil } -// getAwsLaunchtemplateSpecification takes conn and template id +// getAwsLaunchTemplateSpecification takes conn and template id // returns name, default version, latest version -func getAwsLaunchtemplateSpecification(conn *ec2.EC2, id string) (string, string, string, error) { +func getAwsLaunchTemplateSpecification(conn *ec2.EC2, id string) (string, string, string, error) { dlt, err := conn.DescribeLaunchTemplates(&ec2.DescribeLaunchTemplatesInput{ LaunchTemplateIds: []*string{aws.String(id)}, }) @@ -3194,3 +3194,28 @@ func getAwsLaunchtemplateSpecification(conn *ec2.EC2, id string) (string, string return name, defaultVersion, latestVersion, nil } + +func expandEc2LaunchTemplateSpecification(specs []interface{}) *ec2.LaunchTemplateSpecification { + if len(specs) < 1 { + return nil + } + + spec := specs[0].(map[string]interface{}) + + idValue, idOk := spec["id"] + nameValue, nameOk := spec["name"] + + result := &ec2.LaunchTemplateSpecification{} + + if idOk && idValue != "" { + result.LaunchTemplateId = aws.String(idValue.(string)) + } else if nameOk && nameValue != "" { + result.LaunchTemplateName = aws.String(nameValue.(string)) + } + + if v, ok := spec["version"]; ok && v != "" { + result.Version = aws.String(v.(string)) + } + + return result +} diff --git a/aws/structure.go b/aws/structure.go index ee10ae77e2e..e852d87cdcb 100644 --- a/aws/structure.go +++ b/aws/structure.go @@ -2690,82 +2690,6 @@ func flattenIotThingTypeProperties(s *iot.ThingTypeProperties) []map[string]inte return []map[string]interface{}{m} } -func expandLaunchTemplateSpecification(specs []interface{}) *autoscaling.LaunchTemplateSpecification { - if len(specs) < 1 { - return nil - } - - spec := specs[0].(map[string]interface{}) - - idValue, idOk := spec["id"] - nameValue, nameOk := spec["name"] - - result := &autoscaling.LaunchTemplateSpecification{} - - // DescribeAutoScalingGroups returns both name and id but LaunchTemplateSpecification - // allows only one of them to be set - if idOk && idValue != "" { - result.LaunchTemplateId = aws.String(idValue.(string)) - } else if nameOk && nameValue != "" { - result.LaunchTemplateName = aws.String(nameValue.(string)) - } - - if v, ok := spec["version"]; ok && v != "" { - result.Version = aws.String(v.(string)) - } - - return result -} - -func expandEc2LaunchTemplateSpecification(specs []interface{}) *ec2.LaunchTemplateSpecification { - if len(specs) < 1 { - return nil - } - - spec := specs[0].(map[string]interface{}) - - idValue, idOk := spec["id"] - nameValue, nameOk := spec["name"] - - result := &ec2.LaunchTemplateSpecification{} - - if idOk && idValue != "" { - result.LaunchTemplateId = aws.String(idValue.(string)) - } else if nameOk && nameValue != "" { - result.LaunchTemplateName = aws.String(nameValue.(string)) - } - - if v, ok := spec["version"]; ok && v != "" { - result.Version = aws.String(v.(string)) - } - - return result -} - -func flattenLaunchTemplateSpecification(lt *autoscaling.LaunchTemplateSpecification) []map[string]interface{} { - if lt == nil { - return []map[string]interface{}{} - } - - attrs := map[string]interface{}{} - result := make([]map[string]interface{}, 0) - - // id and name are always returned by DescribeAutoscalingGroups - attrs["id"] = *lt.LaunchTemplateId - attrs["name"] = *lt.LaunchTemplateName - - // version is returned only if it was previously set - if lt.Version != nil { - attrs["version"] = *lt.Version - } else { - attrs["version"] = nil - } - - result = append(result, attrs) - - return result -} - func flattenVpcPeeringConnectionOptions(options *ec2.VpcPeeringConnectionOptionsDescription) []interface{} { // When the VPC Peering Connection is pending acceptance, // the details about accepter and/or requester peering From c2ed8f7e128b07c729dc8b7a8aebbd2195e8ce3c Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Tue, 20 Jul 2021 16:51:43 -0700 Subject: [PATCH 4/5] Breaks out individual test cases --- aws/resource_aws_instance_test.go | 512 +++++++++--------- .../running-and-writing-acceptance-tests.md | 2 +- 2 files changed, 258 insertions(+), 256 deletions(-) diff --git a/aws/resource_aws_instance_test.go b/aws/resource_aws_instance_test.go index 424930c0c17..19d3d6ae188 100644 --- a/aws/resource_aws_instance_test.go +++ b/aws/resource_aws_instance_test.go @@ -2616,17 +2616,14 @@ func TestAccAWSInstance_associatePublic_overridePrivate(t *testing.T) { }) } -// Test suite for instance launch_template feature -// 1. Create instance without launch template -// 2. Update with launch_template - this should recreate instance -// 3. Root volume size should be overridden from instance attributes -// 4. Creating new template version should not trigger instance recreation when default is used -// 5. Updating from $Default version to exact version should not trigger recreation -// 6. Updating to latest template version should recreate instance -func TestAccAWSInstance_LaunchTemplate(t *testing.T) { - var v1, v2, v3, v4, v5 ec2.Instance - - resName := "aws_instance.foo" +func TestAccAWSInstance_LaunchTemplate_basic(t *testing.T) { + var v ec2.Instance + resourceName := "aws_instance.test" + launchTemplateResourceName := "aws_launch_template.test" + amiDataSourceName := "data.aws_ami.amzn-ami-minimal-hvm-ebs" + instanceTypeDataSourceName := "data.aws_ec2_instance_type_offering.available" + + rName := acctest.RandomWithPrefix("tf-acc-test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -2635,45 +2632,178 @@ func TestAccAWSInstance_LaunchTemplate(t *testing.T) { CheckDestroy: testAccCheckInstanceDestroy, Steps: []resource.TestStep{ { - Config: testAccInstanceConfigCreateWithoutTemplate, + Config: testAccInstanceConfig_WithTemplate_Basic(rName), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckInstanceExists(resName, &v1), - resource.TestCheckResourceAttr(resName, "instance_type", "t2.medium"), - resource.TestCheckResourceAttr(resName, "root_block_device.0.volume_size", "10"), + testAccCheckInstanceExists(resourceName, &v), + resource.TestCheckResourceAttrPair(resourceName, "launch_template.0.id", launchTemplateResourceName, "id"), + resource.TestCheckResourceAttrPair(resourceName, "launch_template.0.name", launchTemplateResourceName, "name"), + resource.TestCheckResourceAttr(resourceName, "launch_template.0.version", "$Default"), + resource.TestCheckResourceAttrPair(resourceName, "ami", amiDataSourceName, "id"), + resource.TestCheckResourceAttrPair(resourceName, "instance_type", instanceTypeDataSourceName, "instance_type"), ), }, + }, + }) +} + +func TestAccAWSInstance_LaunchTemplate_OverrideTemplate(t *testing.T) { + var v ec2.Instance + resourceName := "aws_instance.test" + launchTemplateResourceName := "aws_launch_template.test" + amiDataSourceName := "data.aws_ami.amzn-ami-minimal-hvm-ebs" + instanceTypeDataSourceName := "data.aws_ec2_instance_type_offering.small" + + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckInstanceDestroy, + Steps: []resource.TestStep{ { - Config: testAccInstanceConfigUpdateWithTemplate, + Config: testAccInstanceConfig_WithTemplate_OverrideTemplate(rName), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckInstanceExists(resName, &v2), - testAccCheckInstanceRecreated(&v1, &v2), - resource.TestCheckResourceAttr(resName, "instance_type", "t2.micro"), - resource.TestCheckResourceAttr(resName, "root_block_device.0.volume_size", "11"), + testAccCheckInstanceExists(resourceName, &v), + resource.TestCheckResourceAttrPair(resourceName, "launch_template.0.id", launchTemplateResourceName, "id"), + resource.TestCheckResourceAttrPair(resourceName, "ami", amiDataSourceName, "id"), + resource.TestCheckResourceAttrPair(resourceName, "instance_type", instanceTypeDataSourceName, "instance_type"), + ), + }, + }, + }) +} + +func TestAccAWSInstance_LaunchTemplate_SetSpecificVersion(t *testing.T) { + var v1, v2 ec2.Instance + resourceName := "aws_instance.test" + launchTemplateResourceName := "aws_launch_template.test" + + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckInstanceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccInstanceConfig_WithTemplate_Basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInstanceExists(resourceName, &v1), + resource.TestCheckResourceAttrPair(resourceName, "launch_template.0.id", launchTemplateResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "launch_template.0.version", "$Default"), + ), + }, + { + Config: testAccInstanceConfig_WithTemplate_SpecificVersion(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInstanceExists(resourceName, &v2), + testAccCheckInstanceNotRecreated(&v1, &v2), + resource.TestCheckResourceAttrPair(resourceName, "launch_template.0.id", launchTemplateResourceName, "id"), + resource.TestCheckResourceAttrPair(resourceName, "launch_template.0.version", launchTemplateResourceName, "default_version"), + ), + }, + }, + }) +} + +func TestAccAWSInstance_LaunchTemplate_ModifyTemplate_DefaultVersion(t *testing.T) { + var v1, v2 ec2.Instance + resourceName := "aws_instance.test" + launchTemplateResourceName := "aws_launch_template.test" + + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckInstanceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccInstanceConfig_WithTemplate_Basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInstanceExists(resourceName, &v1), + resource.TestCheckResourceAttrPair(resourceName, "launch_template.0.id", launchTemplateResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "launch_template.0.version", "$Default"), + ), + }, + { + Config: testAccInstanceConfig_WithTemplate_ModifyTemplate(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInstanceExists(resourceName, &v2), + testAccCheckInstanceNotRecreated(&v1, &v2), + resource.TestCheckResourceAttrPair(resourceName, "launch_template.0.id", launchTemplateResourceName, "id"), + resource.TestCheckResourceAttr(resourceName, "launch_template.0.version", "$Default"), + ), + }, + }, + }) +} + +func TestAccAWSInstance_LaunchTemplate_UpdateTemplateVersion(t *testing.T) { + var v1, v2 ec2.Instance + resourceName := "aws_instance.test" + launchTemplateResourceName := "aws_launch_template.test" + + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckInstanceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccInstanceConfig_WithTemplate_SpecificVersion(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInstanceExists(resourceName, &v1), + resource.TestCheckResourceAttrPair(resourceName, "launch_template.0.id", launchTemplateResourceName, "id"), + resource.TestCheckResourceAttrPair(resourceName, "launch_template.0.version", launchTemplateResourceName, "default_version"), ), }, { - Config: testAccInstanceConfigUpdateTemplateSettings, + Config: testAccInstanceConfig_WithTemplate_UpdateVersion(rName), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckInstanceExists(resName, &v3), - testAccCheckInstanceNotRecreated(&v2, &v3), - resource.TestCheckResourceAttr(resName, "instance_type", "t2.micro"), + testAccCheckInstanceExists(resourceName, &v2), + testAccCheckInstanceRecreated(&v1, &v2), + resource.TestCheckResourceAttrPair(resourceName, "launch_template.0.id", launchTemplateResourceName, "id"), + resource.TestCheckResourceAttrPair(resourceName, "launch_template.0.version", launchTemplateResourceName, "default_version"), ), }, + }, + }) +} + +func TestAccAWSInstance_LaunchTemplate_SwapIDAndName(t *testing.T) { + var v1, v2 ec2.Instance + resourceName := "aws_instance.test" + launchTemplateResourceName := "aws_launch_template.test" + + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckInstanceDestroy, + Steps: []resource.TestStep{ { - Config: testAccInstanceConfigUpdateInstanceTemplateVersion, + Config: testAccInstanceConfig_WithTemplate_Basic(rName), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckInstanceExists(resName, &v4), - testAccCheckInstanceNotRecreated(&v3, &v4), - resource.TestCheckResourceAttr(resName, "instance_type", "t2.micro"), + testAccCheckInstanceExists(resourceName, &v1), + resource.TestCheckResourceAttrPair(resourceName, "launch_template.0.id", launchTemplateResourceName, "id"), + resource.TestCheckResourceAttrPair(resourceName, "launch_template.0.name", launchTemplateResourceName, "name"), ), }, { - Config: testAccInstanceConfigUpdateInstanceTemplateLatest, + Config: testAccInstanceConfig_WithTemplate_WithName(rName), Check: resource.ComposeAggregateTestCheckFunc( - testAccCheckInstanceExists(resName, &v5), - testAccCheckInstanceRecreated(&v4, &v5), - resource.TestCheckResourceAttr(resName, "instance_type", "t2.nano"), - resource.TestCheckResourceAttr(resName, "root_block_device.0.volume_size", "10"), + testAccCheckInstanceExists(resourceName, &v2), + testAccCheckInstanceNotRecreated(&v1, &v2), + resource.TestCheckResourceAttrPair(resourceName, "launch_template.0.id", launchTemplateResourceName, "id"), + resource.TestCheckResourceAttrPair(resourceName, "launch_template.0.name", launchTemplateResourceName, "name"), ), }, }, @@ -6205,16 +6335,23 @@ resource "aws_instance" "test" { // the first available EC2 instance type offering in the current region from a list of preferred instance types. // The data source is named 'available'. func testAccAvailableEc2InstanceTypeForRegion(preferredInstanceTypes ...string) string { + return testAccAvailableEc2InstanceTypeForRegionNamed("available", preferredInstanceTypes...) +} + +// testAccAvailableEc2InstanceTypeForRegionNamed returns the configuration for a data source that describes +// the first available EC2 instance type offering in the current region from a list of preferred instance types. +// The data source name is configurable. +func testAccAvailableEc2InstanceTypeForRegionNamed(name string, preferredInstanceTypes ...string) string { return fmt.Sprintf(` -data "aws_ec2_instance_type_offering" "available" { +data "aws_ec2_instance_type_offering" "%[1]s" { filter { name = "instance-type" - values = ["%[1]s"] + values = ["%[2]s"] } - preferred_instance_types = ["%[1]s"] + preferred_instance_types = ["%[2]s"] } -`, strings.Join(preferredInstanceTypes, "\", \"")) +`, name, strings.Join(preferredInstanceTypes, "\", \"")) } // testAccAvailableEc2InstanceTypeForAvailabilityZone returns the configuration for a data source that describes @@ -6325,258 +6462,123 @@ resource "aws_ec2_capacity_reservation" "test" { `, rName, ec2.CapacityReservationInstancePlatformLinuxUnix)) } -const testAccInstanceConfigCreateWithoutTemplate = ` -data "aws_ami" "debian_jessie_latest" { - most_recent = true - - filter { - name = "name" - values = ["debian-jessie-*"] - } - - filter { - name = "virtualization-type" - values = ["hvm"] - } - - filter { - name = "architecture" - values = ["x86_64"] - } - - filter { - name = "root-device-type" - values = ["ebs"] - } - - owners = ["379101102735"] # Debian +func testAccInstanceConfig_WithTemplate_Basic(rName string) string { + return composeConfig( + testAccLatestAmazonLinuxHvmEbsAmiConfig(), + testAccAvailableEc2InstanceTypeForRegion("t3.micro", "t2.micro", "t1.micro", "m1.small"), + fmt.Sprintf(` +resource "aws_launch_template" "test" { + name = %[1]q + image_id = data.aws_ami.amzn-ami-minimal-hvm-ebs.id + instance_type = data.aws_ec2_instance_type_offering.available.instance_type } -resource "aws_instance" "foo" { - ami = data.aws_ami.debian_jessie_latest.id - associate_public_ip_address = true - instance_type = "t2.medium" - - root_block_device { - volume_size = "10" - volume_type = "standard" - delete_on_termination = true - } - - tags = { - Name = "test-terraform" +resource "aws_instance" "test" { + launch_template { + id = aws_launch_template.test.id } } -` - -const testAccInstanceConfigUpdateWithTemplate = ` -data "aws_ami" "debian_jessie_latest" { - most_recent = true - - filter { - name = "name" - values = ["debian-jessie-*"] - } - - filter { - name = "virtualization-type" - values = ["hvm"] - } - - filter { - name = "architecture" - values = ["x86_64"] - } - - filter { - name = "root-device-type" - values = ["ebs"] - } - - owners = ["379101102735"] # Debian +`, rName)) } -resource "aws_launch_template" "foobar" { - name_prefix = "foobar" - image_id = data.aws_ami.debian_jessie_latest.id - instance_type = "t2.micro" +func testAccInstanceConfig_WithTemplate_OverrideTemplate(rName string) string { + return composeConfig( + testAccLatestAmazonLinuxHvmEbsAmiConfig(), + testAccAvailableEc2InstanceTypeForRegionNamed("micro", "t3.micro", "t2.micro", "t1.micro", "m1.small"), + testAccAvailableEc2InstanceTypeForRegionNamed("small", "t3.small", "t2.small", "t1.small", "m1.medium"), + fmt.Sprintf(` +resource "aws_launch_template" "test" { + name = %[1]q + instance_type = data.aws_ec2_instance_type_offering.micro.instance_type } -resource "aws_instance" "foo" { - ami = data.aws_ami.debian_jessie_latest.id - associate_public_ip_address = true +resource "aws_instance" "test" { + ami = data.aws_ami.amzn-ami-minimal-hvm-ebs.id + instance_type = data.aws_ec2_instance_type_offering.small.instance_type launch_template { - id = aws_launch_template.foobar.id - } - - root_block_device { - volume_size = "11" - volume_type = "standard" - delete_on_termination = true - } - - tags = { - Name = "test-terraform" + id = aws_launch_template.test.id } } -` - -const testAccInstanceConfigUpdateTemplateSettings = ` -data "aws_ami" "debian_jessie_latest" { - most_recent = true - - filter { - name = "name" - values = ["debian-jessie-*"] - } - - filter { - name = "virtualization-type" - values = ["hvm"] - } - - filter { - name = "architecture" - values = ["x86_64"] - } - - filter { - name = "root-device-type" - values = ["ebs"] - } - - owners = ["379101102735"] # Debian +`, rName)) } -resource "aws_launch_template" "foobar" { - name_prefix = "foobar" - image_id = data.aws_ami.debian_jessie_latest.id - instance_type = "t2.nano" +func testAccInstanceConfig_WithTemplate_SpecificVersion(rName string) string { + return composeConfig( + testAccLatestAmazonLinuxHvmEbsAmiConfig(), + testAccAvailableEc2InstanceTypeForRegion("t3.micro", "t2.micro", "t1.micro", "m1.small"), + fmt.Sprintf(` +resource "aws_launch_template" "test" { + name = %[1]q + image_id = data.aws_ami.amzn-ami-minimal-hvm-ebs.id + instance_type = data.aws_ec2_instance_type_offering.available.instance_type } -resource "aws_instance" "foo" { - associate_public_ip_address = true - +resource "aws_instance" "test" { launch_template { - id = aws_launch_template.foobar.id - } - - root_block_device { - volume_size = "11" - volume_type = "standard" - delete_on_termination = true - } - - tags = { - Name = "test-terraform" + id = aws_launch_template.test.id + version = aws_launch_template.test.default_version } } -` - -const testAccInstanceConfigUpdateInstanceTemplateVersion = ` -data "aws_ami" "debian_jessie_latest" { - most_recent = true - - filter { - name = "name" - values = ["debian-jessie-*"] - } - - filter { - name = "virtualization-type" - values = ["hvm"] - } - - filter { - name = "architecture" - values = ["x86_64"] - } - - filter { - name = "root-device-type" - values = ["ebs"] - } - - owners = ["379101102735"] # Debian +`, rName)) } -resource "aws_launch_template" "foobar" { - name_prefix = "foobar" - image_id = data.aws_ami.debian_jessie_latest.id - instance_type = "t2.nano" +func testAccInstanceConfig_WithTemplate_ModifyTemplate(rName string) string { + return composeConfig( + testAccLatestAmazonLinuxHvmEbsAmiConfig(), + testAccAvailableEc2InstanceTypeForRegion("t3.small", "t2.small", "t1.small", "m1.medium"), + fmt.Sprintf(` +resource "aws_launch_template" "test" { + name = %[1]q + image_id = data.aws_ami.amzn-ami-minimal-hvm-ebs.id + instance_type = data.aws_ec2_instance_type_offering.available.instance_type } -resource "aws_instance" "foo" { - associate_public_ip_address = true - +resource "aws_instance" "test" { launch_template { - id = aws_launch_template.foobar.id - version = "1" - } - - root_block_device { - volume_size = "11" - volume_type = "standard" - delete_on_termination = true - } - - tags = { - Name = "test-terraform" + id = aws_launch_template.test.id } } -` - -const testAccInstanceConfigUpdateInstanceTemplateLatest = ` -data "aws_ami" "debian_jessie_latest" { - most_recent = true - - filter { - name = "name" - values = ["debian-jessie-*"] - } +`, rName)) +} - filter { - name = "virtualization-type" - values = ["hvm"] - } +func testAccInstanceConfig_WithTemplate_UpdateVersion(rName string) string { + return composeConfig( + testAccLatestAmazonLinuxHvmEbsAmiConfig(), + testAccAvailableEc2InstanceTypeForRegion("t3.small", "t2.small", "t1.small", "m1.medium"), + fmt.Sprintf(` +resource "aws_launch_template" "test" { + name = %[1]q + image_id = data.aws_ami.amzn-ami-minimal-hvm-ebs.id + instance_type = data.aws_ec2_instance_type_offering.available.instance_type - filter { - name = "architecture" - values = ["x86_64"] - } + update_default_version = true +} - filter { - name = "root-device-type" - values = ["ebs"] +resource "aws_instance" "test" { + launch_template { + id = aws_launch_template.test.id + version = aws_launch_template.test.default_version } - - owners = ["379101102735"] # Debian } - -resource "aws_launch_template" "foobar" { - name_prefix = "foobar" - image_id = data.aws_ami.debian_jessie_latest.id - instance_type = "t2.nano" +`, rName)) } -resource "aws_instance" "foo" { - ami = data.aws_ami.debian_jessie_latest.id - associate_public_ip_address = true +func testAccInstanceConfig_WithTemplate_WithName(rName string) string { + return composeConfig( + testAccLatestAmazonLinuxHvmEbsAmiConfig(), + testAccAvailableEc2InstanceTypeForRegion("t3.micro", "t2.micro", "t1.micro", "m1.small"), + fmt.Sprintf(` +resource "aws_launch_template" "test" { + name = %[1]q + image_id = data.aws_ami.amzn-ami-minimal-hvm-ebs.id + instance_type = data.aws_ec2_instance_type_offering.available.instance_type +} +resource "aws_instance" "test" { launch_template { - id = aws_launch_template.foobar.id - version = aws_launch_template.foobar.latest_version - } - - root_block_device { - volume_size = "10" - volume_type = "standard" - delete_on_termination = true - } - - tags = { - Name = "test-terraform" + name = aws_launch_template.test.name } } -` +`, rName)) +} diff --git a/docs/contributing/running-and-writing-acceptance-tests.md b/docs/contributing/running-and-writing-acceptance-tests.md index 1a3a4043736..2c6d6574311 100644 --- a/docs/contributing/running-and-writing-acceptance-tests.md +++ b/docs/contributing/running-and-writing-acceptance-tests.md @@ -398,7 +398,7 @@ resource "aws_example_thing" "test" { These test configurations are typical implementations we have found or allow testing to implement best practices easier, since the Terraform AWS Provider testing is expected to run against various AWS Regions and Partitions. -- `testAccAvailableEc2InstanceTypeForRegion("type1", "type2", ...)`: Typically used to replace hardcoded EC2 Instance Types. Uses `aws_ec2_instance_type_offering` data source to return an available EC2 Instance Type in preferred ordering. Reference the instance type via: `data.aws_ec2_instance_type_offering.available.instance_type` +- `testAccAvailableEc2InstanceTypeForRegion("type1", "type2", ...)`: Typically used to replace hardcoded EC2 Instance Types. Uses `aws_ec2_instance_type_offering` data source to return an available EC2 Instance Type in preferred ordering. Reference the instance type via: `data.aws_ec2_instance_type_offering.available.instance_type`. Use `testAccAvailableEc2InstanceTypeForRegionNamed("name", "type1", "type2", ...)` to specify a name for the data source - `testAccLatestAmazonLinuxHvmEbsAmiConfig()`: Typically used to replace hardcoded EC2 Image IDs (`ami-12345678`). Uses `aws_ami` data source to find the latest Amazon Linux image. Reference the AMI ID via: `data.aws_ami.amzn-ami-minimal-hvm-ebs.id` #### Randomized Naming From af1c0822ab03b44c429974607ce9d953dcea6d63 Mon Sep 17 00:00:00 2001 From: Graham Davison Date: Wed, 21 Jul 2021 09:37:08 -0700 Subject: [PATCH 5/5] Adds CHANGELOG --- .changelog/10807.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/10807.txt diff --git a/.changelog/10807.txt b/.changelog/10807.txt new file mode 100644 index 00000000000..7ebf0abaebd --- /dev/null +++ b/.changelog/10807.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_instance: Add support for configuration with Launch Template +```