-
Notifications
You must be signed in to change notification settings - Fork 83
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
RFC 322: new CDK Pipelines API #323
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,324 @@ | ||
--- | ||
rfc pr: [#xxx](https://github.com/aws/aws-cdk-rfcs/pull/xxx) <-- fill this after you've already created the PR | ||
tracking issue: https://github.com/aws/aws-cdk-rfcs/issues/322 | ||
--- | ||
|
||
# CDK Pipelines Updated API | ||
|
||
> We are reworking the internals of CDK Pipelines to separate out the concerns of CDK Deployments and AWS CodePipeline | ||
> better, and provide a more streamlined user-facing API, | ||
> both while staying backwards compatible. | ||
|
||
## Working Backwards | ||
|
||
The final API for CDK Pipelines is finally here. Compared to the previous API: | ||
|
||
- CDK applications could not be deployed in waves. They can now, as we automatically establish runorders between | ||
actions. | ||
- You could not build pipelines that used multiple sources to feed into the synth, or that had a custom build step. You can now. | ||
- Large CDK applications (> 25 stacks) could not previously be deployed because they would exceed the 50 actions per | ||
stage limit that CodePipeline has. With the new implementation, the actions will automatically spread over multiple | ||
stages, even if you deploy multiple applications in parallel. | ||
- For simple use cases, you no longer need to manage CodePipeline Artifacts: artifact management is implicit | ||
as objects that produce artifacts can be used to reference them. | ||
|
||
The following: | ||
|
||
```ts | ||
import { SecretValue } from '@aws-cdk/core'; | ||
import * as codepipeline from '@aws-cdk/aws-codepipeline'; | ||
import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions'; | ||
import * as cdkp from '@aws-cdk/pipelines'; | ||
|
||
const sourceArtifact = new codepipeline.Artifact(); | ||
const cloudAssemblyArtifact = new codepipeline.Artifact('CloudAsm'); | ||
const integTestArtifact = new codepipeline.Artifact('IntegTests'); | ||
|
||
const pipeline = new cdkp.CdkPipeline(this, 'Pipeline', { | ||
cloudAssemblyArtifact, | ||
|
||
// Where the source can be found | ||
sourceAction: new codepipeline_actions.GitHubSourceAction({ | ||
actionName: 'GitHub', | ||
output: sourceArtifact, | ||
oauthToken: SecretValue.secretsManager('github-token'), | ||
owner: 'OWNER' | ||
repo: 'REPO', | ||
trigger: codepipeline_actions.GitHubTrigger.POLL, | ||
}), | ||
|
||
// How it will be built | ||
synthAction: cdkp.SimpleSynthAction.standardNpmSynth({ | ||
sourceArtifact, | ||
cloudAssemblyArtifact, | ||
projectName: 'MyServicePipeline-synth', | ||
additionalArtifacts: [ | ||
{ | ||
directory: 'test', | ||
artifact: integTestArtifact, | ||
}, | ||
], | ||
}), | ||
}); | ||
|
||
const stage = new MyStage(this, 'PreProd', { | ||
env: { account: '12345', region: 'us-east-1' }, | ||
}); | ||
stage.addActions( | ||
new cdkp.ShellScriptAction({ | ||
commands: ['node ./integ-tests'], | ||
additionalArtifacts: [integTestArtifact], | ||
}), | ||
); | ||
``` | ||
|
||
Becomes: | ||
|
||
```ts | ||
import * as rollout from '@aws-cdk/rollout'; | ||
|
||
const pipeline = new rollout.Rollout(this, 'Pipeline', { | ||
build: rollout.Build.shellScript({ | ||
input: rollout.CodePipelineSource.gitHub('OWNER/REPO'), | ||
commands: ['npm ci', 'npm run build'], | ||
additionalOutputs: { | ||
tests: rollout.AdditionalOutput.fromDirectory('test'), | ||
} | ||
}), | ||
backend: new rollout.AwsCodePipelineBackend(), | ||
}); | ||
|
||
const stage = new MyStage(this, 'PreProd', { | ||
env: { account: '12345', region: 'us-east-1' }, | ||
}); | ||
pipeline.addApplicationStage(stage, { | ||
approvals: [ | ||
rollout.Approval.shellScript({ | ||
input: pipeline.build.additionalOutput('tests'), | ||
commands: ['node ./integ-tests'], | ||
}), | ||
], | ||
}); | ||
``` | ||
|
||
### How we customize CodeBuild projects, and other things that are AWS-specific? | ||
|
||
AWS-specific customizations are passed as parameters to the Backend class: | ||
|
||
``` | ||
const pipeline = new rollout.Rollout(this, 'Pipeline', { | ||
build: rollout.Build.shellScript({ | ||
input: rollout.CodePipelineSource.gitHub('OWNER/REPO'), | ||
commands: ['npm ci', 'npm run build', 'npx cdk synth'], | ||
environment: { | ||
NPM_CONFIG_UNSAFE_PERM: 'true', | ||
}, | ||
}), | ||
backend: new rollout.AwsCodePipelineBackend({ | ||
pipeline, | ||
pipelineName: 'MyPipeline', | ||
vpc, | ||
subnetSelection: { subnetType: ec2.SubnetType.PRIVATE }, | ||
crossAccountKeys: true, | ||
buildEnvironment: { | ||
image: codebuild.CodeBuildImage.AWS_STANDARD_6, | ||
privilegedMode: true, | ||
}, | ||
buildCaching: true, | ||
cdkCliVersion: '1.2.3', | ||
selfMutating: false, | ||
pipelineUsesDockerAssets: true, | ||
dockerAssetBuildPrefetch: true, | ||
dockerCredentials: { | ||
'': rollout.DockerCredentials.fromSecretsManager('my-dockerhub-login'), | ||
'111111.dkr.ecr.us-east-2.amazonaws.com': rollout.DockerCredentials.standardEcrCredentials(), | ||
}, | ||
buildTestReports: { | ||
SurefireReports: { | ||
baseDirectory: 'target/surefire-reports', | ||
files: ['**/*'], | ||
} | ||
}, | ||
}), | ||
}); | ||
``` | ||
|
||
## Implementation FAQ | ||
|
||
### How will this work? | ||
|
||
The library will be organized into 3 layers: | ||
|
||
``` | ||
┌──────────────────┬────────────────┬───────────────────────┐ | ||
│ │ │ Generic actions │ | ||
│ Backend, │ Rollout │ │ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we really need to separate rollout from the workflow core now? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Depends on how you want to look at it. In my head, those are data structures that the user doesn't necessarily directly interact with (the front-level API will be mostly in terms of CDK apps, which get translated into middle-level Actions). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I understand. So basically the middle layer is the model from which the backends render their concrete implementation. Did I get that right? |
||
│ backend-specific │ │ Build.shellScript │ | ||
│sources & actions │ │ │ | ||
│ ┌──────────────┴────────────────┴───────────────────────┤ | ||
│ │ │ | ||
│ │ Workflow core + CDK app knowledge │ | ||
│ │ (steps, dependencies, translate CDK app into steps) │ | ||
│ │ │ | ||
│ └───────────────────────────────────────────────────────┤ | ||
│ │ | ||
│ Render to CodePipeline/CodeBuild │ | ||
│ │ | ||
└───────────────────────────────────────────────────────────┘ | ||
``` | ||
|
||
The **middle** layer has facilities to build and manipulate an abstract workflow, which features concepts like *steps*, | ||
*dependencies*, *artifacts*, and knows how to translate a CDK app into a sequence of backend-agnostic steps. | ||
|
||
The **bottom** layer renders the generic workflow to a specific CI/CD runner. In the CDK Pipelines case, a CodePipeline | ||
with a set of CodeBuild projects and CloudFormation Actions. | ||
|
||
The **top** layer is the one the user interacts with. It is has generic classes (that apply for any backend), as well as | ||
backend-specific classes, so can deal in concepts that are familiar to the user and offer all the backend-specific | ||
customizations users might want (Most commonly: source customizations such as credentials coming from AWS | ||
SecretsManager, and backend-specific customizations such as CodeBuild VPC bindings, specific additional IAM permissions, | ||
enabling CodeBuild test reporting, customizing Log Groups, etc). The user may inject backend-specific actions for even | ||
more control. | ||
|
||
### How will we port to another backend? | ||
|
||
What is the work to implement different backends for CDK deployments (like, for example, GitHub Actions)? | ||
|
||
* Obviously, we need to replace layer 3 | ||
* We may need to add some new backend-specific components to layer 1 (such as new sources, or other new kinds of | ||
backend-specific actions). | ||
|
||
We will gain efficiency from the fact that the reimplementation will not contain business logic: it will | ||
mostly be mapping types and properties between the different levels. All the knowledge about the structure | ||
and order of deploying CDK apps will be located in layer 2, and is reused. | ||
|
||
### Why will we stay backwards compatible? | ||
|
||
Many people have already invested a lot of effort into writing code against the CDK Pipelines | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As mentioned above. This is nice-to-have but not a goal in itself in my mind, and we can always provide a shim layer for backwards compatibility to allow dev-previewers to migrate. Let's make sure this is not influencing our design. |
||
library. The changes we are planning mostly affect the internals of the library, and we want to add | ||
some slight enhanced expressiveness and customizability to the API. | ||
|
||
With a little additional effort, we can make the new API additive and have the old one continue to work, | ||
giving people the opportunity to switch over to the new API at their own pace. | ||
|
||
### How do we render deployments out across backends? | ||
|
||
In the middle layer, a stack deployments gets represented as nested state machines, | ||
like this: | ||
|
||
``` | ||
┌───────────────────────────────────────────────────────────────┐ | ||
│ DeployStack │ | ||
│ │ | ||
│ ┌────────────────────┐ ┌────────────────────┐ │ | ||
│ │ │ │ │ │ | ||
│ │ CreateChangeSet │────────▶│ ExecuteChangeSet │ │ | ||
│ │ │ │ │ │ | ||
│ └────────────────────┘ └────────────────────┘ │ | ||
│ │ | ||
└───────────────────────────────────────────────────────────────┘ | ||
``` | ||
|
||
A backend rendered has the choice to either: | ||
|
||
* Recognize a `DeployStack` state and render that out to a `cdk deploy` command (or something | ||
else appropriate); or | ||
* Recognize the individual `CreateChangeSet` and `ExecuteChangeSet` states and render those | ||
out either to CodePipeline actions or `aws cloudformation change-change-set` CLI commands, | ||
as may be appropriate. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Feels a little fragile. The backend should either respect the full model or not. Otherwise we may end up with features that work only in specific backends... I am wondering if "DeployStack" is a sufficient level of detail for the middle layer. What value do we get by modeling "CreateChangeSet" and "ExecuteChangeSet" in the middle tier? Feels like this is an implementation detail that each backend should determine. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
And that is fine! The big value is in shared types and a shared framework--not that every backend needs to support every type of workflow!
If you don't there is no way to add ChangeSet approvers. |
||
|
||
### How do approval workflows work? | ||
|
||
Every `Approval` step will be handed a reference to the workflow of the application | ||
being deployed, as well as to an empty "approval" workflow. It can then choose to | ||
add new actions and dependencies to any of those workflows. | ||
|
||
In the most common case, it will add things like "shell script" actions to the approval workflow; but it might also | ||
choose to add actions before "Deploy" (`Approval.securityChanges()`), or it might choose to add actions *in between* | ||
`CreateChangeSet` and `DeployChangeSet` pairs. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. :-) see my comment above... If a backend ignores CreateChangeSet and DeployChangeSet how will an approval workflow that is executed between them work? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In that case, it will throw an error saying "this kind of validation can not be used with this type of backend". Which again is fine, right. Other types of validations WILL work, just not this particular one. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Who will throw this error? The backend? How will it even know that this validation is defined if it ignores the "Create/DeployChangeSet" actions? |
||
|
||
The actions it adds may be generic (from level 2), translatable into any backend and so | ||
the approval workflow is reusable, or they may be backend-specific and only apply to | ||
one specific backend. | ||
|
||
``` | ||
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ | ||
App | ||
│┌────────────────────────────────────────────────────┐ ┌───────────────────────────────────┐ │ | ||
│ Deploy │ │ Approve │ | ||
││ │ │ │ │ | ||
│ ┌────────────────────┐ ┌────────────────────┐ │ │ │ | ||
││ │ Stack1 │ │ Stack2 │ │ │ │ │ | ||
│ │ │ │ │ │ │ │ | ||
││ │ ┌────┐ ┌────┐ │ │ ┌────┐ ┌────┐ │ │─────────▶│ │ │ | ||
│ │ │ C │───▶│ E │ │───▶│ │ C │───▶│ E │ │ │ │ │ | ||
││ │ └────┘ └────┘ │ │ └────┘ └────┘ │ │ │ │ │ | ||
│ │ │ │ │ │ │ │ | ||
││ └────────────────────┘ └────────────────────┘ │ │ │ │ | ||
│ │ │ │ | ||
│└────────────────────────────────────────────────────┘ └───────────────────────────────────┘ │ | ||
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ | ||
``` | ||
|
||
### How will asset publishing work? | ||
|
||
Similar to how approval workflows work, the asset publishing strategy can be configured | ||
with a callback which can manipulate the workflow as desired. This allows for easy switching | ||
between: | ||
|
||
* Prepublish all assets in individual CodeBuild projects | ||
* Prepublish all assets in one CodeBuild project | ||
* Publish assets just before each app deployment | ||
* Publish initial assets as usual but afterwards wait for ECR replication to finish | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Generally speaking, let's try to avoid "callback APIs" - that's a very fragile contract, and very hard to undo if we get wrong because we can never tell what people are doing with it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've also heard "trigger a StepFunctions workflow to do the asset publishing". I definitely don't want to add support for everything on our end. There is lots of value in extensibility. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Supporting random customizations is not a requirement and usually not a good API design practice as it oftentimes leads to leaky APIs that are very hard to evolve. If there is a concrete use case with merit, we should consider how to support it and expose the correct API for it. As for triggering step functions, what's the use case? What's the rationale? Or is it just one user's preference. I rather we design our API with the minimal surface area and expand it as new requirements emerge then offer an extensible API that we won't be able to back away from. |
||
|
||
### What are the generic primitives? | ||
|
||
In the middle layer, the following types of actions/states exist: | ||
|
||
* ShellScript | ||
- May have concepts like shell commands, Docker image, IAM permissions it should | ||
be possible to assume | ||
- Renders to CodeBuild for CodePipeline, a Step in GitHub Actions | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A step in GitHub workflows cannot have a specific docker image. Maybe a "job"? |
||
* Manual Approval | ||
- They GitHub Actions renderer may reject this type of action, and that is okay. | ||
* Create ChangeSet | ||
* Execute ChangeSet | ||
- With or without capturing outputs (backend may reject capturing outputs if it cannot implement that) | ||
|
||
In addition to these, a specific implementation may add backend-specific actions. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is what I was expecting as the pattern to support backend specific things in the top-layer. |
||
So for example, a `CodePipelineAction` can hold any `codepipeline.IAction`, which | ||
can be added into the graph to do whatever. The CodePipeline renderer would render | ||
those out, while any other backend would reject them. This is our "escape hatch" from | ||
level 1 down to level 3. | ||
|
||
### Why are we doing this? | ||
|
||
We are trying to stabilize the current iteration of CDK Pipelines, so that it can | ||
be declared ready for use by all CDK users. At the same time, we are trying to make | ||
sure our work isn't completely targeted at AWS CodePipeline only, and can be made | ||
to work with other deployment backends like GitHub Actions. | ||
|
||
### Why should we _not_ do this? | ||
|
||
Maybe generalizing to multiple deployment backends is too speculative, and we shouldn't be spending effort on it. | ||
|
||
Maybe leaving a specialized API at level 1 is not generic enough, and we'll end up reimplementing substantial amounts of | ||
code anyway, wasting the effort generalizing. | ||
|
||
In any case, our current front-end API is a bit awkward and can do with some optimizing (it's not as minimal and | ||
humanist as we prefer our CDK APIs to be), and the internals need some reworking to address bits of | ||
flow customization users currently aren't able to do, which requires mutation of an in-memory model | ||
anyway. While doing that, it's not a *lot* of additional effort to separate out responsibilities | ||
enough that it becomes feasible to port the concepts. | ||
|
||
### What are the drawbacks of this solution? | ||
|
||
|
||
### What alternative solutions did you consider? | ||
|
||
|
||
### What is the high level implementation plan? | ||
|
||
### Are there any open issues that need to be addressed later? | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it will be better to just describe the final API here (basically, the README for the pipelines module) and maybe at the end add a section "Migrating from Dev Preview?".
Can we take the current README and rewrite it with the new API? It will be a good way to ensure we did not miss anything