Walkthrough: Look up AMI IDs with a Lambda-backed custom resource - AWS CloudFormation

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.

Overview

The following steps provide an overview of this implementation.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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 as nodejs18.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 to 30 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 the Fn::GetAtt function to get the ARN of the LambdaExecutionRole 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
  1. Download the sample package from Amazon S3. When you save the file, use the same file name as the sample, amilookup.zip or amilookup-win.zip.

  2. Open the Amazon S3 console at https://console.aws.amazon.com/s3/home.

  3. On the navigation bar at the top of the screen, choose the AWS Region that you created your Amazon S3 bucket in.

  4. 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.

  5. Choose Upload.

  6. 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
  1. Open the CloudFormation console at https://console.aws.amazon.com/cloudformation/.

  2. From the Stacks page, choose Create stack at top right, and then choose With new resources (standard).

  3. For Prerequisite - Prepare template, choose Choose an existing template.

  4. 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

  5. Choose Next.

  6. For Stack name, type SampleEC2Instance.

  7. 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.

  8. For this walkthrough, you don't need to add tags or specify advanced settings, so choose Next.

  9. 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. The name of the log stream is the physical ID of the custom resource, which you can find by viewing the stack's resources. For more information, see View log data in the Amazon CloudWatch User Guide.

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
  1. From the CloudFormation console, choose the SampleEC2Instance stack.

  2. Choose Actions and then Delete Stack.

  3. 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.

Related information