Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cloudformation): aws custom resource #1850

Merged
merged 31 commits into from
May 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4a8e4b6
feat(cloudformation): aws sdk js custom resource
jogold Feb 23, 2019
5ab6588
Log AWS SDK version
jogold Feb 25, 2019
15a6ef8
Add tests
jogold Feb 25, 2019
34a3ebc
Merge branch 'master' into aws-sdk-js-resource
jogold Mar 1, 2019
ad86457
Return API response as output attributes
jogold Mar 4, 2019
84c73ab
Remove default on onUpdate
jogold Mar 4, 2019
5ba7765
Add options to specify physical resource id
jogold Mar 4, 2019
6f1c0c6
Merge branch 'master' into aws-sdk-js-resource
jogold Mar 26, 2019
5811c94
Rename to AwsCustomResource
jogold Mar 26, 2019
941913b
Add readonly
jogold Mar 28, 2019
552a5e7
Merge branch 'master' into aws-sdk-js-resource
jogold Mar 28, 2019
ae446b9
Write provider in ts
jogold Mar 28, 2019
2fb5163
Support path for physical resource id
jogold Mar 28, 2019
04c8df5
Typo
jogold Mar 28, 2019
8ddf39f
Merge branch 'master' into aws-sdk-js-resource
jogold Mar 28, 2019
82d1ea1
Fix bad pkglint version after merge
jogold Mar 28, 2019
30d3226
Merge branch 'master' into aws-sdk-js-resource
jogold Apr 2, 2019
a26be81
Add option to catch API errors
jogold Apr 2, 2019
7e9b21c
Merge branch 'aws-sdk-js-resource' of github.com:jogold/aws-cdk into …
jogold Apr 2, 2019
b5fc424
Restore package-lock.json in aws-codepipeline-actions
jogold Apr 2, 2019
25691a0
Merge branch 'master' into aws-sdk-js-resource
jogold May 13, 2019
1b7f392
CustomResourceProvider
jogold May 13, 2019
598f605
exclude construct-ctor-props-optional
jogold May 13, 2019
290d77c
remove duplicate statements check
jogold May 13, 2019
8981600
add option to lock api version
jogold May 13, 2019
c423f35
JSDoc
jogold May 13, 2019
d7b66cc
fix booleans in parameters
jogold May 14, 2019
e8813f5
update integration test
jogold May 14, 2019
1b204d8
update README
jogold May 14, 2019
700fa4d
Merge branch 'master' into aws-sdk-js-resource
jogold May 27, 2019
e02480f
update integ test
jogold May 27, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 106 additions & 21 deletions packages/@aws-cdk/aws-cloudformation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,30 +30,30 @@ Sample of a Custom Resource that copies files into an S3 bucket during deploymen

```ts
interface CopyOperationProps {
sourceBucket: IBucket;
targetBucket: IBucket;
sourceBucket: IBucket;
targetBucket: IBucket;
}

class CopyOperation extends Construct {
constructor(parent: Construct, name: string, props: DemoResourceProps) {
super(parent, name);

const lambdaProvider = new SingletonLambda(this, 'Provider', {
uuid: 'f7d4f730-4ee1-11e8-9c2d-fa7ae01bbebc',
code: new LambdaInlineCode(resources['copy.py']),
handler: 'index.handler',
timeout: 60,
runtime: LambdaRuntime.Python3,
});

new CustomResource(this, 'Resource', {
lambdaProvider,
properties: {
sourceBucketArn: props.sourceBucket.bucketArn,
targetBucketArn: props.targetBucket.bucketArn,
}
});
}
constructor(parent: Construct, name: string, props: DemoResourceProps) {
super(parent, name);

const lambdaProvider = new SingletonLambda(this, 'Provider', {
uuid: 'f7d4f730-4ee1-11e8-9c2d-fa7ae01bbebc',
code: new LambdaInlineCode(resources['copy.py']),
handler: 'index.handler',
timeout: 60,
runtime: LambdaRuntime.Python3,
});

new CustomResource(this, 'Resource', {
provider: CustomResourceProvider.lambda(provider),
properties: {
sourceBucketArn: props.sourceBucket.bucketArn,
targetBucketArn: props.targetBucket.bucketArn,
}
});
}
}
```

Expand All @@ -67,3 +67,88 @@ See the following section of the docs on details to write Custom Resources:
* [Introduction](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html)
* [Reference](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/crpg-ref.html)
* [Code Reference](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html)

#### AWS Custom Resource
Sometimes a single API call can fill the gap in the CloudFormation coverage. In
this case you can use the `AwsCustomResource` construct. This construct creates
a custom resource that can be customized to make specific API calls for the
`CREATE`, `UPDATE` and `DELETE` events. Additionally, data returned by the API
call can be extracted and used in other constructs/resources (creating a real
CloudFormation dependency using `Fn::GetAtt` under the hood).

The physical id of the custom resource can be specified or derived from the data
return by the API call.

The `AwsCustomResource` uses the AWS SDK for JavaScript. Services, actions and
parameters can be found in the [API documentation](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/index.html).

Path to data must be specified using a dot notation, e.g. to get the string value
of the `Title` attribute for the first item returned by `dynamodb.query` it should
be `Items.0.Title.S`.

##### Examples
Verify a domain with SES:

```ts
const verifyDomainIdentity = new AwsCustomResource(this, 'VerifyDomainIdentity', {
onCreate: {
service: 'SES',
action: 'verifyDomainIdentity',
parameters: {
Domain: 'example.com'
},
physicalResourceIdPath: 'VerificationToken' // Use the token returned by the call as physical id
}
});

new route53.TxtRecord(zone, 'SESVerificationRecord', {
recordName: `_amazonses.example.com`,
recordValue: verifyDomainIdentity.getData('VerificationToken')
});
```

Get the latest version of a secure SSM parameter:

```ts
const getParameter = new AwsCustomResource(this, 'GetParameter', {
onUpdate: { // will also be called for a CREATE event
service: 'SSM',
action: 'getParameter',
parameters: {
Name: 'my-parameter',
WithDecryption: true
},
physicalResourceId: Date.now().toString() // Update physical id to always fetch the latest version
}
});

// Use the value in another construct with
getParameter.getData('Parameter.Value')
```

IAM policy statements required to make the API calls are derived from the calls
and allow by default the actions to be made on all resources (`*`). You can
restrict the permissions by specifying your own list of statements with the
`policyStatements` prop.

Chained API calls can be achieved by creating dependencies:
```ts
const awsCustom1 = new AwsCustomResource(this, 'API1', {
onCreate: {
service: '...',
action: '...',
physicalResourceId: '...'
}
});

const awsCustom2 = new AwsCustomResource(this, 'API2', {
onCreate: {
service: '...',
action: '...'
parameters: {
text: awsCustom1.getData('Items.0.text')
},
physicalResourceId: '...'
}
})
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// tslint:disable:no-console
import AWS = require('aws-sdk');
import { AwsSdkCall } from '../aws-custom-resource';

/**
* Flattens a nested object
*
* @param object the object to be flattened
* @returns a flat object with path as keys
*/
function flatten(object: object): { [key: string]: string } {
return Object.assign(
{},
...function _flatten(child: any, path: string[] = []): any {
return [].concat(...Object.keys(child)
.map(key =>
typeof child[key] === 'object'
? _flatten(child[key], path.concat([key]))
: ({ [path.concat([key]).join('.')]: child[key] })
));
}(object)
);
}

/**
* Converts true/false strings to booleans in an object
*/
function fixBooleans(object: object) {
return JSON.parse(JSON.stringify(object), (_k, v) => v === 'true'
? true
: v === 'false'
? false
: v);
}

export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent, context: AWSLambda.Context) {
try {
console.log(JSON.stringify(event));
console.log('AWS SDK VERSION: ' + (AWS as any).VERSION);

let physicalResourceId = (event as any).PhysicalResourceId;
let data: { [key: string]: string } = {};
const call: AwsSdkCall | undefined = event.ResourceProperties[event.RequestType];

if (call) {
const awsService = new (AWS as any)[call.service](call.apiVersion && { apiVersion: call.apiVersion });

try {
const response = await awsService[call.action](call.parameters && fixBooleans(call.parameters)).promise();
data = flatten(response);
} catch (e) {
if (!call.catchErrorPattern || !new RegExp(call.catchErrorPattern).test(e.code)) {
throw e;
}
}

if (call.physicalResourceIdPath) {
physicalResourceId = data[call.physicalResourceIdPath];
} else {
physicalResourceId = call.physicalResourceId!;
}
}

await respond('SUCCESS', 'OK', physicalResourceId, data);
} catch (e) {
console.log(e);
await respond('FAILED', e.message, context.logStreamName, {});
}

function respond(responseStatus: string, reason: string, physicalResourceId: string, data: any) {
const responseBody = JSON.stringify({
Status: responseStatus,
Reason: reason,
PhysicalResourceId: physicalResourceId,
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
NoEcho: false,
Data: data
});

console.log('Responding', responseBody);

const parsedUrl = require('url').parse(event.ResponseURL);
const requestOptions = {
hostname: parsedUrl.hostname,
path: parsedUrl.path,
method: 'PUT',
headers: { 'content-type': '', 'content-length': responseBody.length }
};

return new Promise((resolve, reject) => {
try {
const request = require('https').request(requestOptions, resolve);
request.on('error', reject);
request.write(responseBody);
request.end();
} catch (e) {
reject(e);
}
});
}
}
Loading