Short description
You can use AWS Software Development Kit (AWS SDK) calls for the following stack deployment scenarios:
- To retrieve configurations dynamically when you create and update a stack.
- To retrieve attributes of resources that AWS CloudFormation doesn't support in return values.
- To make small patches or configuration changes that AWS CloudFormation doesn't support.
You can trigger AWS SDK calls on different CloudFormation stack operation events such as Create, Delete, or Update. You don't need to set up access and tools for your deployment pipeline to make AWS SDK calls. The AWS SDK runtime is set up by AWS CDK. You can customize and limit the access for the AWS Lambda function behind the custom resource.
Resolution
Note: The following examples use TypeScript. Make sure to use syntax specific to the programming language that you're using.
To make an AWS SDK call using AwsSdkCall, use the following parameters:
- For Service, use the AWS service that you intend to call. Values are case-sensitive.
- For Action, use the API or action call that you want to make. This usually follows a camel-case pattern, and values are case-sensitive.
- For Parameters , use optional parameters that you pass when you make the API or action call. You can declare parameters as a variable and then pass them to the interface resembling a JSON object for API payload. For more information, see AWS SDK for JavaScript.
Example 1: Retrieve configurations dynamically
The following example retrieves an Amazon Machine Image ID from AWS Systems Manager Parameter Store during stack creation. It shows how to reference the response of the API call with the getResponseField method using dot notation to navigate the SDK response structure:
import * as cdk from "aws-cdk-lib/core";
import { Construct } from "constructs";
import * as cr from "aws-cdk-lib/custom-resources";
export class Example1Stack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const getAmiId = new cr.AwsCustomResource(this, "GetAMIId", {
onCreate: {
service: "SSM",
action: "getParameter",
parameters: {
Name: "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64",
},
physicalResourceId: cr.PhysicalResourceId.of("get-ami-id"),
},
policy: cr.AwsCustomResourcePolicy.fromSdkCalls({
resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE,
}),
});
new cdk.CfnOutput(this, "ImageId", {
value: getAmiId.getResponseField("Parameter.Value"),
});
}
}
Example 2: Retrieve resource attributes not available in CloudFormation
CloudFormation doesn't expose all resource attributes through Fn::GetAtt. If CloudFormation doesn't return the value you need, then use an SDK call to retrieve it directly from the service API. This method also lets you retrieve resources that exist outside of CloudFormation and their attributes that you haven't stored in stack exports, SSM parameters, or Secrets Manager.
For example, AWS::Organizations::Organization exposes Id and ManagementAccountId as return values, but CloudFormation doesn’t create most organizations. An SDK call to describeOrganization lets you retrieve these values from a member account without requiring you to include the organization as part of your stack.
import * as cdk from "aws-cdk-lib/core";
import { Construct } from "constructs";
import * as cr from "aws-cdk-lib/custom-resources";
export class Example2Stack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const orgInfo = new cr.AwsCustomResource(this, "DescribeOrg", {
onCreate: {
service: "Organizations",
action: "describeOrganization",
physicalResourceId: cr.PhysicalResourceId.of("describe-org"),
outputPaths: ["Organization.Id", "Organization.MasterAccountId"],
},
policy: cr.AwsCustomResourcePolicy.fromSdkCalls({
resources: cr.AwsCustomResourcePolicy.ANY_RESOURCE,
}),
});
new cdk.CfnOutput(this, "OrgId", {
value: orgInfo.getResponseField("Organization.Id"),
});
new cdk.CfnOutput(this, "ManagementAccountId", {
value: orgInfo.getResponseField("Organization.MasterAccountId"),
});
}
}
Note: The example uses outputPaths to restrict the response to only the fields needed. This avoids hitting the 4096-byte CloudFormation response limit.
Example 3: make configuration changes not supported by CloudFormation
Some operations have no corresponding CloudFormation resource or property. For example, CloudFormation can create a DynamoDB table but cannot insert items into it. To resolve this, use AwsSdkCall to seed initial data at deploy time.
The following example uses onCreate so it only inserts the item when you first create the stack. To also update the item on stack updates, add the same call to onUpdate.
import * as cdk from "aws-cdk-lib/core";
import { Construct } from "constructs";
import * as cr from "aws-cdk-lib/custom-resources";
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
export class Example3Stack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const table = new dynamodb.Table(this, "ConfigTable", {
partitionKey: { name: "PK", type: dynamodb.AttributeType.STRING },
});
new cr.AwsCustomResource(this, "SeedData", {
onCreate: {
service: "DynamoDB",
action: "putItem",
parameters: {
TableName: table.tableName,
Item: {
PK: { S: "CONFIG" },
version: { S: "1.0" },
enabled: { BOOL: true },
},
},
physicalResourceId: cr.PhysicalResourceId.of("seed-data"),
},
policy: cr.AwsCustomResourcePolicy.fromSdkCalls({
resources: [table.tableArn],
}),
});
}
}
Handling complex permission scenarios
The policy property grants permissions to the Lambda function behind the custom resource. Using fromSdkCalls() translates each API call into the corresponding IAM permissions automatically. When possible, scope permissions to specific resource ARNs instead of using ANY_RESOURCE (example 3).
In some cases, fromSdkCalls() may not generate all the permissions needed. For example, if the DynamoDB table in Example 3 uses a customer-managed KMS key, fromSdkCalls() only generates dynamodb:PutItem and adds no KMS permissions at all. The deployment will fail because DynamoDB requires kms:Encrypt, kms:Decrypt, and kms:GenerateDataKey to write items to an encrypted table. You can use fromStatements() to specify the exact permissions.
For example:
import * as cdk from "aws-cdk-lib/core";
import { Construct } from "constructs";
import * as cr from "aws-cdk-lib/custom-resources";
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
import * as kms from "aws-cdk-lib/aws-kms";
import * as iam from "aws-cdk-lib/aws-iam";
export class Example4Stack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const encryptionKey = new kms.Key(this, "TableKey");
const table = new dynamodb.Table(this, "ConfigTable", {
partitionKey: { name: "PK", type: dynamodb.AttributeType.STRING },
encryption: dynamodb.TableEncryption.CUSTOMER_MANAGED,
encryptionKey,
});
new cr.AwsCustomResource(this, "SeedData", {
onCreate: {
service: "DynamoDB",
action: "putItem",
parameters: {
TableName: table.tableName,
Item: {
PK: { S: "CONFIG" },
version: { S: "1.0" },
enabled: { BOOL: true },
},
},
physicalResourceId: cr.PhysicalResourceId.of("seed-data"),
},
policy: cr.AwsCustomResourcePolicy.fromStatements([
new iam.PolicyStatement({
actions: ["dynamodb:PutItem"],
resources: [table.tableArn],
}),
new iam.PolicyStatement({
actions: ["kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey"],
resources: [encryptionKey.keyArn],
}),
]),
});
}
}