-
Notifications
You must be signed in to change notification settings - Fork 4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cloudformation): aws-api custom resource (#1850)
This PR adds a CF custom resource to make calls on the AWS API using AWS SDK JS v2. There are lots of use cases when the CF coverage is not sufficient and adding a simple API call can solve the problem. It could be also used internally to create better L2 constructs. Does this fit in the scope of the cdk? If accepted, I think that ideally it should live in its own lerna package. API: ```ts new AwsSdkJsCustomResource(this, 'AwsSdk', { onCreate: { // AWS SDK call when resource is created (defaults to onUpdate) service: '...', action: '...', parameters: { ... } }. onUpdate: { ... }. // AWS SDK call when resource is updated (defaults to onCreate) onDelete: { ... }, // AWS SDK call when resource is deleted policyStatements: [...] // Automatically derived from the calls if not specified }); ``` Fargate scheduled task example (could be used in `@aws-cdk/aws-ecs` to implement the missing `FargateEventRuleTarget`): ```ts const vpc = ...; const cluster = new ecs.Cluster(...); const taskDefinition = new ecs.FargateTaskDefinition(...); const rule = new events.EventRule(this, 'Rule', { scheduleExpression: 'rate(1 hour)', }); const ruleRole = new iam.Role(...); new AwsSdkJsCustomResource(this, 'PutTargets', { onCreate: { service: 'CloudWatchEvents', action: 'putTargets', parameters: { Rule: rule.ruleName, Targets: [ Arn: cluster.clusterArn, Id: ..., EcsParameters: { taskDefinitionArn: taskDefinition.taskDefinitionArn, LaunchType: 'FARGATE', NetworkConfiguration: { awsvpcConfiguration: { AssignPublicIp: 'DISABLED', SecurityGroups: [...], Subnets: vpc.privateSubnets.map(subnet => subnet.subnetId), }, }, RoleArn: ruleRole.roleArn } ] } } }) ```
- Loading branch information
Showing
10 changed files
with
1,539 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
103 changes: 103 additions & 0 deletions
103
packages/@aws-cdk/aws-cloudformation/lib/aws-custom-resource-provider/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
}); | ||
} | ||
} |
Oops, something went wrong.