Every EC2 instance starts from a long list of choices: which AMI, which instance type, which subnet and security groups, which IAM role, how big the root volume is, whether the metadata service requires session tokens, and what script runs on first boot. Typing those choices into every launch call does not scale, so AWS lets you save them. There have been two ways to do that. The older one, the launch configuration, belonged to EC2 Auto Scaling and could never be changed after creation. The newer one, the launch template, belongs to EC2 itself, keeps numbered versions, and is the only one that receives new features.
This article explains launch templates from first principles: what a version is, how $Default and $Latest are resolved, how an Auto Scaling group combines a template with per-instance-type overrides, how to migrate a group off a launch configuration, and how to roll out a new version without an outage. It ends with the failure modes that bite most teams and a checklist you can apply to your own account today.
What a launch template is
A launch template is a named set of optional launch parameters stored in one Region. Almost every field you can pass to RunInstances can live in it: image ID, instance type, key pair, network interfaces or security groups, IAM instance profile, block device mappings, user data, tags to apply to instances and volumes, metadata options, placement, capacity reservation preferences and market options for Spot. Three properties shape everything else.
- Parameters are optional. A template may leave out the AMI or the instance type; whoever launches from it must then supply them. This is what lets an Auto Scaling group override the instance type per launch.
- Parameters are not fully validated. EC2 stores what you give it. An unsupported combination, such as an instance type that a placement group cannot hold, is discovered only when a launch fails.
- Versions are immutable. You never edit a template; you create a new version, and EC2 numbers it for you in creation order. You can tag the template but not an individual version.
The default quotas are generous: 5,000 launch templates per Region and 10,000 versions per template, though AWS notes that your account quotas can differ, so check Service Quotas before you build a pipeline that creates a version on every commit.
Launch configurations are frozen
Launch configurations still exist in older accounts, but they are frozen. The EC2 Auto Scaling User Guide states three cut-offs. From 1 January 2023, new instance types are not supported in launch configurations, including types added to a Region after its launch. Accounts created on or after 1 June 2023 cannot create launch configurations in the console. Accounts created on or after 1 October 2024 cannot create them by any method: console, API, CLI or CloudFormation.
The practical consequence is that a group still on a launch configuration cannot adopt a newer instance family, Graviton generation or EC2 feature, and an infrastructure-as-code module that creates launch configurations will fail outright in a new account. The comparison below is the short version of why migration is worth a day of work.
| Property | Launch configuration | Launch template |
|---|---|---|
| Owner | EC2 Auto Scaling | EC2 (usable by RunInstances, Auto Scaling, Fleet, Spot) |
| Change model | Immutable; create a new one and repoint the group | Immutable versions under one template ID |
| New instance types | None since 1 January 2023 | All |
| Multiple instance types per group | No | Yes, through a mixed instances policy |
| Spot and On-Demand mix in one group | No | Yes |
| AMI from an SSM parameter | No | Yes |
| Creation in new accounts | Blocked from 1 October 2024 | Supported |
Versions, $Default and $Latest
When anything launches from a template it names a version. There are three ways to name one.
- A number, such as
7. Deterministic and auditable: the group launches exactly that version until someone changes the group. $Default, a pointer you move explicitly withmodify-launch-template --default-version. New templates start with version 1 as the default.$Latest, which always means the highest version number. Creating a version is enough to change what the next launch uses.
The aliases are resolved at launch time, not when you attach the template to a group. That one fact explains most surprises. If a group follows $Latest and a developer creates a version with a broken user data script, nothing happens to running instances, but the next scale-out or health replacement launches the broken version. Teams that want a deliberate release step either pin a number in the group and change it during deployment, or follow $Default and treat moving the default as the release.
Creating and versioning from the CLI
The CLI flow is short. Create the template with version 1, then create later versions from a source version so you only state what changes. The AMI can be resolved from a Systems Manager parameter at launch time with the resolve:ssm: prefix, which keeps the template stable while the AMI behind a parameter is updated by your image pipeline.
# Version 1: the full definition
aws ec2 create-launch-template \
--launch-template-name web-app \
--launch-template-data '{
"ImageId": "resolve:ssm:/golden/web-app/x86_64",
"InstanceType": "m6i.large",
"IamInstanceProfile": {"Name": "web-app-instance"},
"SecurityGroupIds": ["sg-0a1b2c3d4e5f60718"],
"MetadataOptions": {"HttpTokens": "required", "HttpPutResponseHopLimit": 1},
"BlockDeviceMappings": [{"DeviceName": "/dev/xvda",
"Ebs": {"VolumeSize": 30, "VolumeType": "gp3", "Encrypted": true}}],
"TagSpecifications": [{"ResourceType": "instance",
"Tags": [{"Key": "service", "Value": "web-app"}]}]
}'
# Version 2: copy version 1, change only the user data
aws ec2 create-launch-template-version \
--launch-template-name web-app \
--source-version 1 \
--version-description "add log shipper" \
--launch-template-data "{\"UserData\": \"$(base64 -w0 user-data.sh)\"}"
# Make version 2 the release
aws ec2 modify-launch-template --launch-template-name web-app --default-version 2
# Inspect what a version actually contains
aws ec2 describe-launch-template-versions --launch-template-name web-app --versions '$Default'Note that --source-version copies the whole source version and then merges your data on top, so fields you do not mention are inherited. Single quotes around $Default stop the shell from expanding it as a variable, a small mistake that produces a confusing error. User data in launch template data must be base64 encoded.
How an Auto Scaling group uses a template
An Auto Scaling group references a template in one of two ways. The simple form, LaunchTemplate, names the template and version and launches one instance type. The richer form, MixedInstancesPolicy, wraps the same reference with a list of overrides and an instances distribution that sets the On-Demand base, the On-Demand percentage above it, and the allocation strategies for each purchase option.
Each override can replace the instance type, attach a WeightedCapacity so a larger type counts as several units of desired capacity, and, crucially, name its own launch template. That last field is how one group runs both x86 and Arm instances: the Arm types point at a template whose AMI is arm64, while the x86 types use the base template. The group, its scaling policies and its load balancer registration stay the same.
{
"AutoScalingGroupName": "web-app",
"MinSize": 2, "MaxSize": 20, "DesiredCapacity": 4,
"VPCZoneIdentifier": "subnet-0aa1,subnet-0bb2,subnet-0cc3",
"MixedInstancesPolicy": {
"LaunchTemplate": {
"LaunchTemplateSpecification": {"LaunchTemplateName": "web-app", "Version": "$Default"},
"Overrides": [
{"InstanceType": "m7g.large",
"LaunchTemplateSpecification": {"LaunchTemplateName": "web-app-arm64", "Version": "$Default"}},
{"InstanceType": "m6i.large"},
{"InstanceType": "m6i.xlarge", "WeightedCapacity": "2"}
]
},
"InstancesDistribution": {
"OnDemandBaseCapacity": 2,
"OnDemandPercentageAboveBaseCapacity": 25,
"SpotAllocationStrategy": "price-capacity-optimized"
}
}
}Instead of listing types, an override can describe requirements with InstanceRequirements, such as a vCPU and memory range, and let the group choose matching types, including ones released after you wrote the policy. That is convenient for Spot diversity, but it hands the choice of CPU vendor and architecture to the selector, so constrain the architecture explicitly if your AMI only supports one. Networking is the other trap: the group places instances into subnets from VPCZoneIdentifier, so a template used by a group should carry security groups but not a subnet-bound network interface.
Worked example: migrating a group
Here is a worked migration for a group called api that still uses a launch configuration. The console offers a copy-to-launch-template action for launch configurations; the steps below show the same thing explicitly so you can review each field.
- Export the existing definition with
aws autoscaling describe-launch-configurations --launch-configuration-names api-lc-17and note the AMI, type, security groups, profile, block devices, user data and metadata options. - Create a template with those values. Fix what the old configuration got wrong while you are there: require IMDSv2 tokens, encrypt volumes, switch gp2 roots to gp3. Each change is a behaviour change, so record it.
- Point the group at the template with
aws autoscaling update-auto-scaling-group --auto-scaling-group-name api --launch-template LaunchTemplateName=api,Version='$Default'. Running instances are untouched; only new launches use the template. - Replace the old instances with an instance refresh, described next, so the fleet matches the template.
- Once the group has run a full deploy cycle on the template, delete the launch configuration so nobody repoints the group at it.
If the group is managed by CloudFormation or Terraform, make the change there instead, or the next apply will revert it. In CloudFormation that means replacing LaunchConfigurationName with a LaunchTemplate property that references an AWS::EC2::LaunchTemplate resource and its LatestVersionNumber attribute.
Rolling out a new version
Changing the version a group uses affects future launches only. To bring running instances in line, start an instance refresh. The group replaces instances in batches while keeping a minimum healthy percentage in service, waits an instance warm-up period before counting a replacement as healthy, and can pause at checkpoints so you can watch metrics between stages.
aws autoscaling start-instance-refresh \
--auto-scaling-group-name web-app \
--desired-configuration '{"LaunchTemplate":
{"LaunchTemplateName": "web-app", "Version": "3"}}' \
--preferences '{
"MinHealthyPercentage": 90,
"InstanceWarmup": 120,
"SkipMatching": true,
"AutoRollback": true,
"CheckpointPercentages": [10, 50, 100],
"CheckpointDelay": 600
}'Passing the target version in DesiredConfiguration ties the group update and the rollout together: if the refresh fails or is cancelled with rollback, the group returns to its previous configuration rather than being left half-updated. SkipMatching skips instances that already match the desired configuration, which makes reruns cheap. Automatic rollback reacts to a failed refresh, and it can also be triggered by CloudWatch alarms that you attach to the refresh, which is the safest setup for production: a rising error rate halts the rollout before it reaches the whole fleet. The Auto Scaling groups guide covers warm-up, health checks and termination order in more detail.
Failure modes
These are the failures that show up in real accounts, with the signal that reveals each.
| Failure | What you see | Fix |
|---|---|---|
Group follows $Latest and someone creates a bad version | Scale-out instances fail health checks while old ones are fine | Pin a number or follow $Default; restrict ec2:CreateLaunchTemplateVersion in IAM |
| Unsupported parameter combination | Group activity history shows launch failures; desired capacity never reached | Launch one instance from the version manually before releasing it |
| AMI deregistered or not shared | InvalidAMIID errors on every launch | Use SSM parameters for AMIs; never delete an AMI a live version references |
| Encrypted volumes with a customer managed KMS key | Instances terminate right after launch | Grant the Auto Scaling service-linked role use of the key |
| Template carries a subnet-bound network interface | Group cannot spread across its subnets | Keep subnets in the group; put only security groups in the template |
| Override template has the wrong architecture | Launches fail for the Arm types | Give every override a template whose AMI matches the type |
One more subtle failure: the version quota. A pipeline that creates a version on every build can reach the per-template limit after a few years. Delete old versions you no longer reference, but never delete the default version or a version a group still names.
Trade-offs
The main choice is how a group names its version. A pinned number gives you an audit trail in the group itself and makes every change an explicit deployment, at the cost of one extra API call per release. $Default moves the release decision to the template, which is useful when several groups share one template and should move together, but it also means one default change affects all of them. $Latest is convenient in development and dangerous in production because creating and releasing are the same action.
The second choice is one template with many overrides versus several templates. One template is simpler to reason about; per-override templates are required as soon as instance types need different AMIs, which is always the case when you mix x86 and Arm. Keep the shared settings identical across those templates and generate them from one source so they cannot drift. For the underlying instance and hypervisor model, see the EC2 overview and the Nitro System article; for defining templates as code, CloudFormation in depth.
What to do next
- List groups still on launch configurations:
aws autoscaling describe-auto-scaling-groups --query "AutoScalingGroups[?LaunchConfigurationName].AutoScalingGroupName". - Migrate each one with the five-step flow above, making the change in your infrastructure code.
- Decide per group whether it pins a number or follows
$Default, and remove$Latestfrom production groups. - Require IMDSv2 and encrypted gp3 volumes in every template version you create from now on.
- Move AMI IDs into SSM parameters and reference them with
resolve:ssm:. - Roll out every new version with an instance refresh that uses checkpoints and alarm-based rollback.
- Add a scheduled job that deletes unreferenced versions well before the quota is reached.