Skip to content

Commit

Permalink
Add example for using CloudFormation Custom Resource Emulator
Browse files Browse the repository at this point in the history
  • Loading branch information
flostadler committed Nov 7, 2024
1 parent 877d71b commit 8a8c517
Show file tree
Hide file tree
Showing 9 changed files with 3,458 additions and 0 deletions.
3 changes: 3 additions & 0 deletions examples/cfn-custom-resource/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name: aws-cidr-ts
runtime: nodejs
description: A TypeScript Pulumi program with AWS Cloud Control provider
107 changes: 107 additions & 0 deletions examples/cfn-custom-resource/ami-lookup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* A sample Lambda function that looks up the latest AMI ID for a given region and architecture.
**/

// Map instance architectures to an AMI name pattern
var archToAMINamePattern = {
"PV64": "amzn-ami-pv*x86_64-ebs",
"HVM64": "al2023-ami-2023.*-kernel-*-x86_64",
"HVMG2": "amzn-ami-graphics-hvm*x86_64-ebs*"
};
var aws = require("aws-sdk");

exports.handler = function(event, context) {

console.log("REQUEST RECEIVED:\n" + JSON.stringify(event));

// For Delete requests, immediately send a SUCCESS response.
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: [archToAMINamePattern[event.ResourceProperties.Architecture]]}],
Owners: [event.ResourceProperties.Architecture == "HVMG2" ? "679593333241" : "amazon"]
};

// Get AMI IDs with the specified name pattern and owner
ec2.describeImages(describeImagesParams, function(err, describeImagesResult) {
if (err) {
responseData = {Error: "DescribeImages call failed"};
console.log(responseData.Error + ":\n", err);
}
else {
var images = describeImagesResult.Images;
// Sort images by name in decscending 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 j = 0; j < images.length; j++) {
if (isBeta(images[j].Name)) continue;
responseStatus = "SUCCESS";
responseData["Id"] = images[j].ImageId;
break;
}
}
sendResponse(event, context, responseStatus, responseData);
});
};

// Check if the image is a beta or rc image. The Lambda function won't return any of those images.
function isBeta(imageName) {
return imageName.toLowerCase().indexOf("beta") > -1 || imageName.toLowerCase().indexOf(".rc") > -1;
}


// Send response to the pre-signed S3 URL
function sendResponse(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 https = require("https");
var url = require("url");

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
}
};

console.log("SENDING RESPONSE...\n");

var request = https.request(options, function(response) {
console.log("STATUS: " + response.statusCode);
console.log("HEADERS: " + JSON.stringify(response.headers));
// Tell AWS Lambda that the function execution is done
context.done();
});

request.on("error", function(error) {
console.log("sendResponse Error:" + error);
// Tell AWS Lambda that the function execution is done
context.done();
});

// write data to request body
request.write(responseBody);
request.end();
}
98 changes: 98 additions & 0 deletions examples/cfn-custom-resource/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright 2016-2024, Pulumi Corporation.

import * as pulumi from '@pulumi/pulumi';
import * as aws from "@pulumi/aws-native";
import * as awsClassic from "@pulumi/aws";

const amiRegion = new pulumi.Config().require("amiRegion");

// Create an IAM role for the Lambda function
const lambdaRole = new awsClassic.iam.Role("lambdaRole", {
assumeRolePolicy: awsClassic.iam.assumeRolePolicyForPrincipal({ Service: "lambda.amazonaws.com" }),
});

const policy = new awsClassic.iam.Policy("lambdaPolicy", {
policy: {
Version: "2012-10-17",
Statement: [{
Action: "ec2:DescribeImages",
Effect: "Allow",
Resource: "*",
}],
},
});

const rpa1 = new awsClassic.iam.RolePolicyAttachment("lambdaRolePolicyAttachment1", {
role: lambdaRole.name,
policyArn: policy.arn,
});

const rpa2 = new awsClassic.iam.RolePolicyAttachment("lambdaRolePolicyAttachment2", {
role: lambdaRole.name,
policyArn: awsClassic.iam.ManagedPolicies.AWSLambdaBasicExecutionRole,
});

const bucket = new awsClassic.s3.Bucket('custom-resource-emulator', {
forceDestroy: true,
});

const handlerCode = new awsClassic.s3.BucketObjectv2("handler-code", {
bucket: bucket.bucket,
key: "handlerCode",
source: new pulumi.asset.AssetArchive({
"index.js": new pulumi.asset.FileAsset("ami-lookup.js"),
})
})

// Create the Lambda function for the custom resource
const lambdaFunction = new awsClassic.lambda.Function("ami-lookup-custom-resource", {
runtime: awsClassic.types.enums.lambda.Runtime.NodeJS16dX,
s3Bucket: bucket.bucket,
s3Key: handlerCode.key,
handler: "index.handler",
role: lambdaRole.arn,
memorySize: 128,
timeout: 30,
}, { dependsOn: [rpa1, rpa2] });

const cfnCustomResource = new aws.cloudformation.CustomResourceEmulator('emulator', {
bucketName: bucket.id,
bucketKeyPrefix: 'custom-resource-emulator',
customResourceProperties: {
Region: amiRegion,
Architecture: 'HVM64',
},
serviceToken: lambdaFunction.arn,
resourceType: 'Custom::MyResource',
}, { customTimeouts: { create: '5m', update: '5m', delete: '5m' } });

const cloudformationStack = new awsClassic.cloudformation.Stack('stack', {
templateBody: pulumi.interpolate`{
"AWSTemplateFormatVersion" : "2010-09-09",
"Description" : "AWS CloudFormation AMI Look Up Sample Template: Demonstrates how to dynamically specify an AMI ID. This template provisions an EC2 instance with an AMI ID that is based on the instance's type and region. **WARNING** This template creates an Amazon EC2 instance. You will be billed for the AWS resources used if you create a stack from this template.",
"Resources" : {
"AMIInfo": {
"Type": "Custom::AMIInfo",
"Properties": {
"ServiceToken": "${lambdaFunction.arn}",
"ServiceTimeout": 300,
"Region": "${amiRegion}",
"Architecture": "HVM64"
}
}
},
"Outputs" : {
"AMIID" : {
"Description": "The Amazon EC2 instance AMI ID.",
"Value" : { "Fn::GetAtt": [ "AMIInfo", "Id" ] }
}
}
}
`
});

export const cloudformationAmiId = cloudformationStack.outputs['AMIID'];
export const emulatorAmiId = cfnCustomResource.data['Id'];
Loading

0 comments on commit 8a8c517

Please sign in to comment.