aws-cdk-rfcs: Deployment Triggers

Description

Allow specifying arbitrary handlers which execute as part of the deployment process and trigger them before/after resources or stacks.

Published: https://github.com/awslabs/cdk-triggers

README

Hypothetical README for this feature

You can trigger the execution of arbitrary AWS Lambda functions before or after resources or groups of resources are provisioned using the Triggers API.

The library includes constructs that represent different triggers. The BeforeCreate and AfterCreate constructs can be used to trigger a handler before/after a set of resources have been created.

new triggers.AfterCreate(this, 'InvokeAfter', {
  resources: [resource1, resource2, stack, ...],
  handler: myLambdaFunction,
});

Similarly, triggers.BeforeCreate can be used to set up a “before” trigger.

Where resources is a list of construct scopes which determine when handler is invoked. Scopes can be either specific resources or composite constructs (in which case all the resources in the construct will be used as a group). The scope can also be a Stack, in which case the trigger will apply to all the resources within the stack (same as any composite construct). All scopes must roll up to the same stack.

Let’s look at an example. Say we want to publish a notification to an SNS topic that says “hello, topic!” after the topic is created.

// define a topic
const topic = new sns.Topic(this, 'MyTopic');

// define a lambda function which publishes a message to the topic
const publisher = new NodeJsFunction(this, 'PublishToTopic');
publisher.addEnvironment('TOPIC_ARN', topic.topicArn);
publisher.addEnvironment('MESSAGE', 'Hello, topic!');
topic.grantPublish(publisher);

// trigger the lambda function after the topic is created
new triggers.AfterCreate(this, 'SayHello', {
  scopes: [topic],
  handler: publisher
});

Requirements

  • One-off exec before/after resource/s are created (Trigger.AfterCreate).
  • Additional periodic execution after deployment (repeatOnSchedule).
  • Async checks (retryWithTImeout)
  • Execute on updates (bind logical ID to hash of CFN properties of triggered resource)
  • Execute shell command inside a Docker image

Use Cases

Here are some examples of use cases for triggers:

  • Intrinsic validations: execute a check to verify that a resource or set of resources have been deployed correctly
    • Test connections to external systems (e.g. security tokens are valid)
    • Verify integration between resources is working as expected
    • Execute as one-off and also periodically after deployment
    • Wait for data to start flowing (e.g. wait for a metric) before deployment is successful
  • Data priming: add data to resources after they are created
    • CodeCommit repo + initial commit
    • Database + test data for development
  • Check prerequisites before depoyment
    • Account limits
    • Availability of external services
  • Connect to other accounts

Implementation

At the base level, the trigger handler can be invoked through a custom resource and the timing (before/after) will be determined using CFN dependencies (“after” means the trigger CR depends on the scope, and “before” is the opposite).

This simple implementation will allow us to implement “one-off” triggers. This means that we wait for a CFN CREATE request on the custom resource and invoke the handler. Any updates to the stack will not include any changes to the properties of the custom resource and therefore the trigger won’t get invoked again (unless it’s removed).

We need to consider the following:

  • If the trigger handler itself changes, do we want it to be invoked again?
  • If the triggering resource is updated, do we want the trigger to be invoked again?
  • Do we want some kind of support for triggers that always gets invoked (for any update)?
  • Do we want triggers for “AfterDelete” or “AfterUpdate” does that make sense?

Lots to talk about!

Next Steps

  • Least-privilege IAM policy for trigger custom resource provider (currently it’s invokeFunction for * resources).
  • Invoke trigger if another resource is added to the stack (even if the trigger has already been created).
  • Consider adding support for “update” triggers (if the triggering resource has been updated).

Related Issues

See #75 for a discussion, then, use these for e.g. integration test assertions (#31)

Progress

  • Tracking Issue Created
  • RFC PR Created
  • Core Team Member Assigned
  • Initial Approval / Final Comment Period
  • Ready For Implementation
    • implementation issue 1
  • Resolved

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 57
  • Comments: 38 (18 by maintainers)

Most upvoted comments

Thanks for everyone who attended CDK Construction Zone. We started building this in the first episode. Code is here: https://github.com/eladb/cdk-triggers

Just ran into a situation where i need to run some integration tests in my ci pipeline, some event emitter/hooks would be awesome to bundle that within cdk.

This capability is now available as part of the AWS CDK: https://github.com/aws/aws-cdk/tree/master/packages/%40aws-cdk/triggers

CDK Triggers have been released: https://github.com/awslabs/cdk-triggers

Recording of the first episode is now available on the AWS Twitch channel: https://www.twitch.tv/videos/917691798

At this stage (personally for me), it would be sufficient to just have the “hooks” after the stacks are deployed.

Similar to aws/aws-cdk#1938 but on the client-side.

The use-cases:

  • use AWS API to amend the resources that CloudFormation doesn’t support (#1938 is probably better for that, but local update often times is enough)
  • pushing notifications, outside of the account where the deployment happens
  • confirming a success/failure and the details for the automation scripts

Possible API (not well thought through) could look like:

// App-level - constructor parameters
const app = new cdk.App(hooks: { deployed: (s: Stack, outcome: cdk.OutcomeDetails)=>{...} });
// ...
app.run()



// App-level - promise-like
const app = new cdk.App();
// ...
app.run().whenDeployed().then((stack, outcome) => { ... });



// Stack override
export class Stack1 extends cdk.Stack {
    constructor(scope: cdk.App) { ... }

    whenDeployed(outcome: cdk.OutcomeDetails) {
      // default implementaion - noop
    }
}

const app = new cdk.App();
const stack1 = new Stack1(app);

Out of the above, I think my current preference is Stack override as it would make sense in my use-case - amend Cognito client OAuth details. This option keeps the stack-related details closer together.

But the application-level options are fine too and probably are better for other use-cases.

A few use cases. If we can access the stack vars after deployment such as names, arns and parameters of the deployed services that are not known pre-deployment, and we can trigger events after a service and/or stack deployment, and then run code locally with that information, it opens a whole bunch of automation opportunities.

  • configure local git as mentioned, even do a first commit & push
  • deploy templates, config files, bootstraps, etc into the local directory from a shared location
  • useful feedback to the Dev such as API and ui URLs of what was deployed
  • automatically commit/push after a successful deployment (potentially to trigger any codepipelines for deploying ui to s3 or similar)

I’m sure I can think of more things that would be useful to me/my clients but it’s getting late here 😃 perhaps others could chime in?

We will continue the implementation in the next episode of “CDK Construction Zone”, happening on Feb 23rd 9AM PDT. Check out the AWS twitch channel schedule for more details.

Another good example would be setting up a client or site-to-site VPN in the VPC and then from the deployment machine, configuring the VPN software to connect to that VPC.