Walkthrough: Look up AMI IDs with a Lambda-backed custom resource
This walkthrough shows you how to use Lambda and a custom resource to dynamically look up Amazon Machine Image (AMI) IDs in your CloudFormation template. This can help you streamline how you update the AMI IDs in your CloudFormation templates.
When you create a CloudFormation template to launch an Amazon EC2 instance, you must specify an AMI ID, which is like a template for the operating system and software that will be installed on the instance. The correct AMI ID depends on the instance type and AWS Region you're launching your instance in. These IDs can change regularly, such as when an AMI is updated with software updates.
You typically specify the AMI ID in a Mappings
section that maps AMI IDs to
specific instance types and Regions. This means that to update the IDs, you must manually change
them in each of your templates. However, by using custom resources and Lambda, you can create a
function that gets the IDs of the latest AMIs for the instance type and Region that you're
using. This way, you don't have to manually maintain the mappings between AMI IDs, instance
types, and Regions in your stack templates.
This walkthrough shows you how to create a custom resource and associate a Lambda function with it to look up AMI IDs. It assumes that you are familiar with custom resources and Lambda functions. For an introduction to custom resources and how they work, see Create custom provisioning logic with custom resources. For information about Lambda, see the AWS Lambda Developer Guide.
Note
CloudFormation is a free service; however, you are charged for the AWS resources, such as
the Lambda function and EC2 instance, that you include in your stacks at the current rate for
each. For more information about AWS pricing, see the detail page for each product at http://aws.amazon.com
As an alternative to creating a custom resource and Lambda function, you can use AWS Systems Manager parameters in your template to retrieve the latest AMI ID value stored in a Systems Manager parameter. This makes your templates more reusable and easier to maintain. For more information, see Specify existing resources at runtime with CloudFormation-supplied parameter types.
Topics
Overview
The following steps provide an overview of this implementation.
-
Save a sample package containing the Lambda function code to an Amazon S3 bucket in the same AWS Region where you want to create your EC2 instance.
-
Use the sample template to create your stack with a custom resource, a Lambda function, an EC2 instance, and an IAM role that Lambda uses to make calls to Amazon EC2.
-
The stack associates the Lambda function with the custom resource. When the stack is created, CloudFormation invokes the function and sends it information, such as the request type, input data, and a pre-signed Amazon S3 URL.
-
The Lambda function uses the input data to look up the latest AMI ID and sends the AMI ID as a response to the pre-signed URL.
-
CloudFormation gets a response in the pre-signed URL location and proceeds with creating the stack. When CloudFormation creates the instance, it uses the AMI ID provided by the Lambda function to create the EC2 instance with the latest AMI.
Template walkthrough
To view the entire sample template, see:
The following snippets explain relevant parts of the sample template to help you understand how to associate a Lambda function with a custom resource and how to use the function's response.
AWS::Lambda::Function resource AMIInfoFunction
The AWS::Lambda::Function
resource specifies the function's source code,
handler name, runtime environment, and execution role Amazon Resource Name (ARN).
-
The
Code
property specifies the Amazon S3 location (bucket name and file name) where you uploaded the sample package. The sample template uses input parameters ("Ref": "S3Bucket"
and"Ref": "S3Key"
) to set the bucket and file names so that you are able to specify the names when you create the stack. Similarly, the handler name, which corresponds to the name of the source file (the JavaScript file) in the.zip
package, also uses an input parameter ("Ref": "ModuleName"
). Because the source file is JavaScript code, the runtime is specified asnodejs18.x
. -
For the code that's used in this walkthrough, the execution time for the function exceeds the default value of
3
seconds, so the timeout is set to30
seconds. If you don't specify a sufficiently long timeout, Lambda might cause a timeout before the function can complete, causing stack creation to fail. -
The
Role
property uses theFn::GetAtt
function to get the ARN of theLambdaExecutionRole
execution role that's declared elsewhere in the template.
JSON
"AMIInfoFunction": { "Type": "AWS::Lambda::Function", "Properties": { "Code": { "S3Bucket": { "Ref": "S3Bucket" }, "S3Key": { "Ref": "S3Key" } }, "Handler": { "Fn::Join" : [ "", [{ "Ref": "ModuleName" },".handler"] ] }, "Runtime": "nodejs18.x", "Timeout": "30", "Role": { "Fn::GetAtt" : ["LambdaExecutionRole", "Arn"] } } }
YAML
AMIInfoFunction: Type: AWS::Lambda::Function Properties: Code: S3Bucket: !Ref S3Bucket S3Key: !Ref S3Key Handler: !Sub "${ModuleName}.handler" Runtime: nodejs18.x Timeout: 30 Role: !GetAtt LambdaExecutionRole.Arn
AWS::IAM::Role
resource LambdaExecutionRole
The execution role grants the Lambda function permission to send logs to AWS and to call
the EC2 DescribeImages
API.
JSON
"LambdaExecutionRole": { "Type": "AWS::IAM::Role", "Properties": { "AssumeRolePolicyDocument": { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"Service": ["lambda.amazonaws.com"]}, "Action": ["sts:AssumeRole"] }] }, "Path": "/", "Policies": [{ "PolicyName": "root", "PolicyDocument": { "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["logs:CreateLogGroup","logs:CreateLogStream","logs:PutLogEvents"], "Resource": "arn:aws:logs:*:*:*" }, { "Effect": "Allow", "Action": ["ec2:DescribeImages"], "Resource": "*" }] } }] } }
YAML
LambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - sts:AssumeRole Path: "/" Policies: - PolicyName: root PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: arn:aws:logs:*:*:* - Effect: Allow Action: - ec2:DescribeImages Resource: "*"
Custom::AMIInfo resource AMIInfo
For both the Linux and Windows templates, the custom resource invokes the Lambda function
that's associated with it. To associate a function with a custom resource, you specify the ARN
of the function for the ServiceToken
property, using the Fn::GetAtt
intrinsic function. CloudFormation sends the additional properties that are included in the custom
resource declaration, such as Region
and Architecture
, to the Lambda
function as inputs. The Lambda function determines the correct names and values for these input
properties.
JSON
"AMIInfo": { "Type": "Custom::AMIInfo", "Properties": { "ServiceToken": { "Fn::GetAtt" : ["AMIInfoFunction", "Arn"] }, "Region": { "Ref": "AWS::Region" }, "Architecture": { "Fn::FindInMap" : [ "AWSInstanceType2Arch", { "Ref" : "InstanceType" }, "Arch" ] } } }
YAML
AMIInfo: Type: Custom::AMIInfo Properties: ServiceToken: !GetAtt AMIInfoFunction.Arn Region: !Ref "AWS::Region" Architecture: Fn::FindInMap: - AWSInstanceType2Arch - !Ref InstanceType - Arch
For Windows, the custom resource provides the Windows version to the Lambda function instead of the instance's architecture.
JSON
"AMIInfo": { "Type": "Custom::AMIInfo", "Properties": { "ServiceToken": { "Fn::GetAtt": ["AMIInfoFunction", "Arn"] }, "Region": { "Ref": "AWS::Region" }, "OSName": { "Ref": "WindowsVersion" } } }
YAML
AMIInfo: Type: Custom::AMIInfo Properties: ServiceToken: !GetAtt AMIInfoFunction.Arn Region: !Ref "AWS::Region" OSName: !Ref "WindowsVersion"
When CloudFormation invokes the Lambda function, the function calls the EC2
DescribeImages
API, using the AWS Region and instance architecture or the OS
name to filter the list of images. Then the function sorts the list of images by date and
returns the ID of the latest AMI.
When returning the ID of the latest AMI, the function sends the ID to a pre-signed URL in
the Data
property of the response
object. The data is structured as a name-value pair, as shown in the following
example:
"Data": { "Id": "ami-02354e95b3example" }
AWS::EC2::Instance resource SampleInstance
The following snippet shows how to get the data from a Lambda function. It uses the
Fn::GetAtt
intrinsic function, providing the name of the custom resource and
the attribute name of the value that you want to get. In this walkthrough, the custom resource
name is AMIInfo
and the attribute name is Id
.
JSON
"SampleInstance": { "Type": "AWS::EC2::Instance", "Properties": { "InstanceType" : { "Ref": "InstanceType" }, "ImageId": { "Fn::GetAtt": [ "AMIInfo", "Id" ] } } }
YAML
SampleInstance: Type: AWS::EC2::Instance Properties: InstanceType: !Ref InstanceType ImageId: !GetAtt AMIInfo.Id
Prerequisites
Before you can complete the steps to create the stack, you must already have an Amazon S3 bucket. When you create a stack with a Lambda function, you must specify the location of the Amazon S3 bucket that contains the function's source code. The bucket must be in the same AWS Region you create your stack in. For more information about creating a bucket, see Creating a bucket in the Amazon Simple Storage Service User Guide.
You must also have IAM permissions to use all the corresponding services, such as Lambda, Amazon EC2, and CloudFormation.
Step 1: Save the sample Lambda package to Amazon S3
In this step, you upload the sample Lambda package (a .zip
file) to
your Amazon S3 bucket.
This package contains the source code for the Lambda function and required libraries. For this walkthrough, the function doesn't require additional libraries.
To save the sample package in Amazon S3
-
Download the sample package from Amazon S3. When you save the file, use the same file name as the sample,
amilookup.zip
oramilookup-win.zip
.- Look up Linux AMI IDs
-
https://s3.amazonaws.com/cloudformation-examples/lambda/amilookup.zip
- Look up Windows AMI IDs
-
https://s3.amazonaws.com/cloudformation-examples/lambda/amilookup-win.zip
-
Open the Amazon S3 console at https://console.aws.amazon.com/s3/home
. -
On the navigation bar at the top of the screen, choose the AWS Region that you created your Amazon S3 bucket in.
-
In the Buckets list, choose the name of your bucket. Make a note of the bucket name because you use it when you create the stack.
-
Choose Upload.
-
Under Upload, for Files and folders , upload the sample package to the bucket. For more information, see Uploading objects in the Amazon Simple Storage Service User Guide.
Step 2: Launch the stack
In this step, you launch a stack from a sample template. The stack includes a Lambda function, an IAM role (execution role), a custom resource that invokes the function, and an EC2 instance that uses the results from the function.
During stack creation, the custom resource invokes the Lambda function and waits until the function sends a response to the pre-signed Amazon S3 URL. In the response, the function returns the ID of the latest AMI that corresponds to the EC2 instance type and AWS Region you are creating the instance in. The data from the function's response is stored as an attribute of the custom resource, which is used to specify the AMI ID of the EC2 instance.
To create the stack
-
Open the CloudFormation console at https://console.aws.amazon.com/cloudformation/
. -
From the Stacks page, choose Create stack at top right, and then choose With new resources (standard).
For Prerequisite - Prepare template, choose Choose an existing template.
-
For Specify template, choose Amazon S3 URL, and then copy and paste the following URL in the Amazon S3 URL field:
- Linux template
-
https://s3.amazonaws.com/cloudformation-examples/lambda/LambdaAMILookupSample.template
- Windows template
-
https://s3.amazonaws.com/cloudformation-examples/lambda/LambdaAMILookupSample-win.template
-
Choose Next.
-
For Stack name, type
SampleEC2Instance
. -
For Parameters, specify the name of the Amazon S3 bucket that you created, and then choose Next.
The default values for the other parameters are the same names that are used in the sample
.zip
package. -
For this walkthrough, you don't need to add tags or specify advanced settings, so choose Next.
-
Ensure that the stack name and template URL are correct, and then choose Create.
It might take several minutes for CloudFormation to create your stack. To monitor progress, view the stack events. For more information, see View stack information from the CloudFormation console.
If stack creation succeeds, all resources in the stack, such as the Lambda function, custom resource, and EC2 instance, were created. You successfully used a Lambda function and custom resource to specify the AMI ID of an EC2 instance. You don't need to create and maintain a mapping of AMI IDs in this template.
To see which AMI ID CloudFormation used to create the EC2 instance, view the stack outputs.
If the Lambda function returns an error, view the function's logs in the CloudWatch Logs console
Step 3: Clean up resources
Delete the stack to clean up all the stack resources that you created so that you aren't charged for unnecessary resources.
To delete the stack
-
From the CloudFormation console, choose the SampleEC2Instance stack.
-
Choose Actions and then Delete Stack.
-
In the confirmation message, choose Yes, Delete.
All the resources that you created are deleted.
Now that you understand how to create and use Lambda functions with CloudFormation, you can use the sample template and code from this walkthrough to build other stacks and functions.