November 22, 2015

Obtain AMI Id from name to be used in CFN

In most cases we create custom AMI’s for various reasons like OS hardening, installing and configuring additional software, etc. If you use single AWS account to create the AMI’s and share it with other AWS accounts and use that as part of CloudFormation template, it is required to pass the new AMI Id every time.

AWS Lambda comes handy for easy and elegant solution to get the latest AMI Id based on the AMI name and owner.

Lambda function

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
/**
*
* Handler called by Lambda function.
* @param {object} event - event parameter gets the attributes from CFN trigger.
* @param {object} context - context parameter used to log details to CloudWatch log stream.
*
*/
exports.handler = function(event, context) {
  var aws = require('aws-sdk');
  var https = require('https');
  var url = require('url');
  if (event.RequestType === 'Delete') {
    sendResponse(event, context, 'SUCCESS');
    return;
  }

  var responseStatus = 'FAILED';
  var responseData = {};
  var ec2 = new aws.EC2({ region: event.ResourceProperties.Region });
  var describeImagesParams = {
    Filters: [
      {
                Name: 'name',
                Values: [event.ResourceProperties.AMIName]
      }
    ],
    Owners: [event.ResourceProperties.AMIOwner]
  };

  // Get AMI IDs with the specified name pattern and owner
  ec2.describeImages(describeImagesParams, function(err, data) {
    if (err) {
      responseData = { Error: 'DescribeImages call failed' };
      console.log(responseData.Error + ':n', err);
    }
    else {
      var images = data.Images;
      // Sort images by name in descending order -- the names contain the AMI version formatted as YYYY.MM.Ver.
      images.sort(function(x, y) { return y.Name.localeCompare(x.Name); });
      for (var i = 0; i < images.length; i++) {
        responseStatus = 'SUCCESS';
        responseData.Id = images[i].ImageId;
        break;
      }
      console.log('AMI ID is ', responseData.Id);
    }
    sendResponse(event, context, responseStatus, responseData);
  });
};

// Sends a response to the pre-signed S3 URL
var sendResponse = function(event, context, responseStatus, responseData) {
  var responseBody = JSON.stringify({
    Status: responseStatus,
    Reason: 'See the details in CloudWatch Log Stream: ' + context.logStreamName,
    PhysicalResourceId: context.logStreamName,
    StackId: event.StackId,
    RequestId: event.RequestId,
    LogicalResourceId: event.LogicalResourceId,
    Data: responseData
  });

  console.log('RESPONSE BODY:n', responseBody);

  var parsedUrl = url.parse(event.ResponseURL);
  var options = {
    hostname: parsedUrl.hostname,
    port: 443,
    path: parsedUrl.path,
    method: 'PUT',
    headers: {
      'Content-Type': '',
      'Content-Length': responseBody.length
    }
  };

  var req = https.request(options, function(res) {
    console.log('STATUS:', res.statusCode);
    console.log('HEADERS:', JSON.stringify(res.headers));
    context.succeed('Successfully sent stack response!');
  });

  req.on('error', function(err) {
    console.log('sendResponse Error:n', err);
    context.fail(err);
  });

  req.write(responseBody);
  req.end();
};

Using CFN custom resource, invoke the Lambda function with AMI Name & Owner as parameters.

► Invoking Lambda function through CFN
1
2
3
4
5
6
7
"GetAMIId":
  "Type": "Custom::GetAMIId"
  "Properties":
    "ServiceToken": "<ARN of the Lambda function that needs to be invoked.>"
    "AMIName": { "Ref": "AMIName" }
    "AMIOwner": { "Ref": "AMIOwner" }
    "Region": { "Ref": "AWS::Region" }

Output of the above mentioned CFN resources can be referenced using Intrinsic Function like ({ "Fn::GetAtt" : [ "GetAMIId", "Id" ] })where the AMI Id property needs to be passed in the CFN resource.

This works for all types of images (self, public & private) since the script filters the image using both AMI name & owner. To use specific version of an image pass the full name of the image (e.g. base_web_v1.9.0) for AMIName parameter. If you want to use the latest version of a specific image pass the name without the version string and instead use wildcard (e.g. base_web_v*).

This function works only if the images follow some pattern in its name. Hope you have an automated mechanism to create AMI’s, if not better do that immediately. Packer is a wonderful tool to create images automatically.

Further improvements:

The script can be enhanced to support filters that are based on root device type (ebs/instance-store) and virtualization type (hvm/paravirtual). It’s very easy to extend the script to enable that. As it’s not required for my use case I skipped it.

© Prakash P 2015 - 2019

Powered by Hugo & Kiss.