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(core): democratize synthesis and introduce artifacts #1889

Merged
merged 28 commits into from
Mar 1, 2019

Conversation

eladb
Copy link
Contributor

@eladb eladb commented Feb 26, 2019

This change enables each construct in the tree to participate in synthesis
by overriding the synthesize and add "artifacts" and write files to a synthesis session.

At the moment, the only construct that implements synthesize is Stack which emits an aws:cloudformation:stack artifact which includes a reference to a template file stored into the synthesis session as well.

App.run() traverses the tree and invokes synthesize on all constructs.

When synthesis is finalized, a manifest is produced and stored in the root manifest.json file. This is accordance with the draft specification for cloud assemblies as authored by @RomainMuller and included in this change.

For backwards compatibility, and until we implement the required changes in the toolkit, the output also contains cdk.out which is 100% compatible with the current output.

Related #1716
Related #1893


Pull Request Checklist

  • Testing
    • Unit test added (prefer not to modify an existing test, otherwise, it's probably a breaking change)
    • CLI change?: coordinate update of integration tests with team
    • cdk-init template change?: coordinated update of integration tests with team
  • Docs
    • jsdocs: All public APIs documented
    • README: README and/or documentation topic updated
  • Title and Description
    • Change type: title prefixed with fix, feat will appear in changelog
    • Title: use lower-case and doesn't end with a period
    • Breaking?: last paragraph: "BREAKING CHANGE: <describe what changed + link for details>"
    • Issues: Indicate issues fixed via: "Fixes #xxx" or "Closes #xxx"
  • Sensitive Modules (requires 2 PR approvers)
    • IAM Policy Document (in @aws-cdk/aws-iam)
    • EC2 Security Groups and ACLs (in @aws-cdk/aws-ec2)
    • Grant APIs (only if not based on official documentation with a reference)

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license.

This change enables each construct in the tree to participate in synthesis
by overriding the "synthesize" method and emitting files to the session directory.

It is a small step towards generalizing how synthesis artifacts are being
produced, built and deployed by the CDK.

This change moves the code that synthesizes a CloudFormation template from a 
CDK stack to the `Stack` class and emits a `.template.json` file for each
stack.

The CLI hasn't been changed to work with these new files yet, so the output
of this file is ALSO included in the main manifest (defined `cxapi.OUTFILE_NAME`),
which is the only file currently being read by the CLI.
@eladb eladb requested review from sam-goodwin and a team February 26, 2019 10:16
packages/@aws-cdk/cdk/lib/app.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/cdk/lib/app.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/cdk/lib/app.ts Show resolved Hide resolved
packages/@aws-cdk/cdk/lib/app.ts Show resolved Hide resolved
packages/@aws-cdk/cdk/lib/app.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/cdk/lib/cloudformation/stack.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/cdk/lib/cloudformation/stack.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/cdk/lib/core/construct.ts Show resolved Hide resolved
packages/@aws-cdk/cdk/lib/core/construct.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/cdk/lib/synthesis.ts Outdated Show resolved Hide resolved
@eladb eladb requested a review from rix0rrr February 26, 2019 16:23
Elad Ben-Israel added 7 commits February 26, 2019 18:59
Start moving towards the cloud-assembly specification where an output
of a CDK program is a bunch of artifacts and those are processed by
the toolkit.

Related #956
Related #233
Related #1119
Instead of an empty protected method, use an interface to discover if
a construct is synthesizable.
@eladb eladb changed the title feat(core): democratize synthesis feat(core): democratize synthesis and introduce artifacts Feb 27, 2019
@eladb eladb requested a review from RomainMuller February 27, 2019 15:53
packages/@aws-cdk/cdk/lib/app.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/cdk/lib/runtime-info.ts Show resolved Hide resolved
}

// add an artifact that represents this stack
session.addArtifact(this.node.id, artifact);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't you be using node.uniqueId instead of node.id? Same question for dependencies. Are we leaving it up to the caller to ensure the uniqueness of the id?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am using node.id for backwards compatibility purposes. We are going to add support for composite apps (#1582) and will address there.

I decided that for now, I'll introduce the low-level API and the caller will be responsible. As you observed, not many constructs are going to participate in synthesis so I believe we can leave it to them to take responsibility and apply a best practice to use node.uniqueId for now. Happy to iterate after this PR lands to make more implicit/automatic.

properties: { template }
};

const deps = this.dependencies().map(s => s.node.id);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reasoning behind this? Does a construct dependency always imply a droplet dependency?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are stack dependencies, not construct dependencies.

// write the CloudFormation template as a JSON file
session.store.writeJson(template, this.toCloudFormation());

const artifact: cxapi.Artifact = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it looks like staging will use the same format as cloud assembly? How would I communicate something like 'build this folder'? If it's an Artifact, then how do we plan on distinguishing between synth-only artifacts and cloud-assembly artifacts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The output of App.run() is an intermediate step towards a cloud assembly. I've added a build: any to Artifact as a placeholder, with the idea that the toolkit will use this information to build the intermediate artifact and produce the final assembly.

I am not in love with this design, but maybe it's the pragmatic thing to do, especially given the simple case (CloudFormation template) is a no-op between staging=>assembly.

Happy to hear your thoughts. Maybe we can start with this and iterate and see where it takes us.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about this some more. I think you are right... Something is still not clicking. Crap.

NEED MORE THINKING.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, here's where I am at right now.

Let's say that app.run() produces two files: manifest.json, which is the cloud assembly manifest proper and build.json which build instructions for the toolkit.

Let's assume that manifest.json is not touched by the toolkit and that the build system (which is not modeled yet), will produce outputs that are pre-referenced by the emitted manifest.

As you can see in the example below, the artifact MyHandlerCode is of type aws:s3:object (because this is the resource that will eventually be created) and the filePath property points to file://3cdf5efc05e1254da7d751257c344e74.zip, which doesn't exist when app.run() ends. The file location is always relative to the assembly root.

{
  "artifacts": {
    "MyStack": {
      "type": "aws:cloudformation:stack",
      "environment": "aws://account/region",
      "properties": {
        "name": "my-stack-name",
        "parameters": {
          "MyHandlerLocation": "s3://${MyHandlerCode.bucket}/${MyHandlerCode.key}"
        },
        "template": "file://${assembly}/my-stack.template.json"
      }
    },
    "MyHandlerCode": {
      "type": "aws:s3:object",
      "properties": {
        "filePath": "file://3cdf5efc05e1254da7d751257c344e74.zip"
      }
    }
  }
}

Completely decoupled from that, we also emit build.json (which is not modeled yet) but basically tells the toolkit how to produce 3cdf5efc05e1254da7d751257c344e74.zip. This is where we can get wild with your declarative build system.

So, for the above example, let's say the handler is a Java project and we use lambda builders, then build.json will model the following process:

  1. (future) Generate some magical code into the project directory (only referenced in build.json, not needed by manifest.json).
  2. Run lambda-builders-java for the project directory inside the docker image
  3. Zip up the resulting output (say it's not zipped by lambda-builders, I don't know)
  4. Output to 3cdf5efc05e1254da7d751257c344e74.zip

It is important that artifacts and build commands are completely decoupled, and only connect when a build output is used as an artifact's input. For example, the build output name is based on the content hash of the project directory, so if multiple assets use the same project directory, it's the same build output.

}
}

export interface ISessionStore {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would I make a project directory available for a build? Would I symlink or manually copy each file into the store?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am thinking that project directories will be referenced directly in the Artifact's build field without copying them to the staging area.

I think that code generation and build will need to happen in-place in order to allow IDE integration to be sane. Otherwise, I can't see how users will be able to sanely consume any generated code.

You mentioned something yesterday about generating complete libraries and not code, and I wanted to loop back on that and tell you that this was not my intention when we talked about code generation last week, and I actually don't think it's very pragmatic to generate full libraries. All I was saying is that you should assume that users never edit or check in any generated code, so if you want users to consume something that you generated, they would do something like:

import { GeneratedClass } from '../cdk.generated';

// use GeneratedClass

Now, the CDK can generate it's code into cdk.generated.

@eladb
Copy link
Contributor Author

eladb commented Feb 28, 2019

@sam-goodwin I've updated the PR with support for session.addBuildStep which is a first approximation at this idea of a separate set of build steps emitted alongside the manifest.json. Let me know what you think.

I think I will slightly change the structure of the output directory to look like this:

.
├── assembly
│   ├── manifest.json
│   ├── stack1.template.json
│   ├── ... artifacts produced directly from app.run()
└── build.json

This way, the assembly directory is basically the staging area for the cloud assembly and the toolkit only adds stuff to it (results from executing the build steps defined under build.json).

@sam-goodwin sam-goodwin merged commit 4ab1cd3 into master Mar 1, 2019
@rix0rrr rix0rrr deleted the benisrae/construct-synthesis branch April 23, 2019 07:13
@NGL321 NGL321 added the contribution/core This is a PR that came from AWS. label Sep 23, 2019
@mergify
Copy link
Contributor

mergify bot commented Sep 23, 2019

Thanks so much for taking the time to contribute to the AWS CDK ❤️

We will shortly assign someone to review this pull request and help get it
merged. In the meantime, please take a minute to make sure you follow this
checklist
:

  • PR title type(scope): text
    • type: fix, feat, refactor go into CHANGELOG, chore is hidden
    • scope: name of module without aws- or cdk- prefix or postfix (e.g. s3 instead of aws-s3-deployment)
    • text: use all lower-case, do not end with a period, do not include issue refs
  • PR Description
    • Rationale: describe rationale of change and approach taken
    • Issues: indicate issues fixed via: fixes #xxx or closes #xxx
    • Breaking?: last paragraph: BREAKING CHANGE: <describe what changed + link for details>
  • Testing
    • Unit test added. Prefer to add a new test rather than modify existing tests
    • CLI or init templates change? Re-run/add CLI integration tests
  • Documentation
    • README: update module README to describe new features
    • API docs: public APIs must be documented. Copy from official AWS docs when possible
    • Design: for significant features, follow design process

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
contribution/core This is a PR that came from AWS.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants