diff --git a/.changelog/10807.txt b/.changelog/10807.txt new file mode 100644 index 000000000000..7ebf0abaebd1 --- /dev/null +++ b/.changelog/10807.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_instance: Add support for configuration with Launch Template +``` diff --git a/aws/resource_aws_autoscaling_group.go b/aws/resource_aws_autoscaling_group.go index dcbb4805b10b..9587730ed827 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 } @@ -1891,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 } @@ -1899,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 { @@ -1934,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) != "" { @@ -1944,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 { @@ -1979,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 { @@ -2294,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 abcdedb08f52..6e50473399f4 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" @@ -50,9 +52,11 @@ func resourceAwsInstance() *schema.Resource { Schema: map[string]*schema.Schema{ "ami": { - Type: schema.TypeString, - Required: true, - ForceNew: true, + Type: schema.TypeString, + ForceNew: true, + Computed: true, + Optional: true, + AtLeastOneOf: []string{"ami", "launch_template"}, }, "arn": { Type: schema.TypeString, @@ -119,6 +123,7 @@ func resourceAwsInstance() *schema.Resource { "disable_api_termination": { Type: schema.TypeBool, Optional: true, + Computed: true, }, "ebs_block_device": { Type: schema.TypeSet, @@ -200,6 +205,7 @@ func resourceAwsInstance() *schema.Resource { "ebs_optimized": { Type: schema.TypeBool, Optional: true, + Computed: true, ForceNew: true, }, "enclave_options": { @@ -280,8 +286,10 @@ func resourceAwsInstance() *schema.Resource { Computed: true, }, "instance_type": { - Type: schema.TypeString, - Required: true, + Type: schema.TypeString, + Computed: true, + Optional: true, + AtLeastOneOf: []string{"instance_type", "launch_template"}, }, "ipv6_address_count": { Type: schema.TypeInt, @@ -305,6 +313,39 @@ func resourceAwsInstance() *schema.Resource { ForceNew: true, Computed: true, }, + "launch_template": { + 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, + ExactlyOneOf: []string{"launch_template.0.name", "launch_template.0.id"}, + ValidateFunc: validateLaunchTemplateId, + }, + "name": { + 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, + Optional: true, + ValidateFunc: validation.StringLenBetween(1, 255), + Default: "$Default", + }, + }, + }, + }, "metadata_options": { Type: schema.TypeList, Optional: true, @@ -336,6 +377,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 +558,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 +583,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 +636,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 +715,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 +735,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 +889,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 +966,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 +2046,62 @@ 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"] + + 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,11 +2355,29 @@ func readBlockDeviceMappingsFromConfig(d *schema.ResourceData, conn *ec2.EC2) ([ } } - if dn, err := fetchRootDeviceName(d.Get("ami").(string), conn); err == nil { + 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(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{ @@ -2359,6 +2542,7 @@ type awsInstanceOpts struct { Ipv6AddressCount *int64 Ipv6Addresses []*ec2.InstanceIpv6Address KeyName *string + LaunchTemplate *ec2.LaunchTemplateSpecification NetworkInterfaces []*ec2.InstanceNetworkInterfaceSpecification Placement *ec2.Placement PrivateIPAddress *string @@ -2377,16 +2561,27 @@ type awsInstanceOpts struct { func buildAwsInstanceOpts(d *schema.ResourceData, meta interface{}) (*awsInstanceOpts, error) { conn := meta.(*AWSClient).ec2conn - 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 v, ok := d.GetOk("ami"); ok { + opts.ImageID = aws.String(v.(string)) + } + + if v, ok := d.GetOk("instance_type"); ok { + opts.InstanceType = aws.String(v.(string)) + } + + 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{ @@ -2884,3 +3079,143 @@ 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 +} + +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/resource_aws_instance_test.go b/aws/resource_aws_instance_test.go index 1a88c300f9d8..fdaefe286f48 100644 --- a/aws/resource_aws_instance_test.go +++ b/aws/resource_aws_instance_test.go @@ -2609,6 +2609,200 @@ func TestAccAWSInstance_associatePublic_overridePrivate(t *testing.T) { }) } +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) }, + ErrorCheck: testAccErrorCheck(t, ec2.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckInstanceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccInstanceConfig_WithTemplate_Basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + 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: testAccInstanceConfig_WithTemplate_OverrideTemplate(rName), + Check: resource.ComposeAggregateTestCheckFunc( + 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: testAccInstanceConfig_WithTemplate_UpdateVersion(rName), + Check: resource.ComposeAggregateTestCheckFunc( + 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: testAccInstanceConfig_WithTemplate_Basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckInstanceExists(resourceName, &v1), + resource.TestCheckResourceAttrPair(resourceName, "launch_template.0.id", launchTemplateResourceName, "id"), + resource.TestCheckResourceAttrPair(resourceName, "launch_template.0.name", launchTemplateResourceName, "name"), + ), + }, + { + Config: testAccInstanceConfig_WithTemplate_WithName(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.name", launchTemplateResourceName, "name"), + ), + }, + }, + }) +} + func TestAccAWSInstance_getPasswordData_falseToTrue(t *testing.T) { var before, after ec2.Instance resourceName := "aws_instance.test" @@ -6014,7 +6208,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" @@ -6145,16 +6341,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 @@ -6264,3 +6467,124 @@ resource "aws_ec2_capacity_reservation" "test" { } `, rName, ec2.CapacityReservationInstancePlatformLinuxUnix)) } + +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" "test" { + launch_template { + id = aws_launch_template.test.id + } +} +`, rName)) +} + +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" "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.test.id + } +} +`, rName)) +} + +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" "test" { + launch_template { + id = aws_launch_template.test.id + version = aws_launch_template.test.default_version + } +} +`, rName)) +} + +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" "test" { + launch_template { + id = aws_launch_template.test.id + } +} +`, rName)) +} + +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 + + update_default_version = true +} + +resource "aws_instance" "test" { + launch_template { + id = aws_launch_template.test.id + version = aws_launch_template.test.default_version + } +} +`, rName)) +} + +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 { + name = aws_launch_template.test.name + } +} +`, rName)) +} diff --git a/aws/structure.go b/aws/structure.go index 3acada8709ea..e852d87cdcb0 100644 --- a/aws/structure.go +++ b/aws/structure.go @@ -2690,61 +2690,6 @@ func flattenIotThingTypeProperties(s *iot.ThingTypeProperties) []map[string]inte return []map[string]interface{}{m} } -func expandLaunchTemplateSpecification(specs []interface{}) (*autoscaling.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 := &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, nil -} - -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 previosly 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 diff --git a/aws/tags.go b/aws/tags.go index 125b976bd18d..cc65edf57afe 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/docs/contributing/running-and-writing-acceptance-tests.md b/docs/contributing/running-and-writing-acceptance-tests.md index 4611a6352f1d..7f5424a40db4 100644 --- a/docs/contributing/running-and-writing-acceptance-tests.md +++ b/docs/contributing/running-and-writing-acceptance-tests.md @@ -399,7 +399,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 diff --git a/website/docs/r/instance.html.markdown b/website/docs/r/instance.html.markdown index 6a566dcb855b..3971afe985de 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: