Closed eladb closed 5 years ago
I think I also want Constructs to be able to spread over multiple stacks. And i don't necessarily mean nested stacks.
Okay, this is starting to fall into place. Seems like we can generalize this even more and finally define the app model.
No need to store a revision in comments, GitHub has revisions on the issue description...
Introduced the concept of an app model which is essentially a desired state description of a full app.
What about our "toolkit stack" (the stack that contains the bucket which we upload artifacts to)? Can we extend the app model to include it as well?
Let's try:
Using yaml and ${} substitutions for brevity
Resources:
AssetsStack:
Type: AWS::CloudFormation::Stack
Properties:
# inline version of the "toolkit stack" template, now it's not "special" anymore
TemplateFile: "./assets-template.json"
MyLambdaCodePackage:
Type: AWS::S3::Object
Properties:
File: "./my-handler.zip"
Bucket: ${AssetsStack.AssetsBucket}
MyTemplate:
Type: AWS::S3::Object
Properties:
File: "./my-template.json"
Bucket: ${AssetsStack.AssetsBucket}
MyStack:
Type: AWS::CloudFormation::Stack
Proeprties:
TemplateURL: ${MyTemplate.URL}
Parameters:
MyLambdaCodeS3Bucket: ${MyLambdaCodePackage.Bucket}
MyLambdaCodeS3Key: ${MyLambdaCodePackage.Key}
This requires that AWS::CloudFormation::Stack
can also accept an template from a local file (instead of a URL), this limits it to 50KB, which is perfectly suited for this purpose.
A few humble comments (James Joyce's Stream-Of-Conciseness style):
ó Éirinn le craic
Keep an eye on re-usability with different combination of clouds/artifacts. I do understand that the primary focus is CFN now, but it wouldn't hurt. "Toolkit stack" is very CFN-specific.
That's a good point. I think we will need to use higher level abstractions for the app model also to allow tools to reach the desired state in different ways. For example, the assets bucket (e.g. where runtime code or CloudFormation templates or deployment artifacts are consumed) can either be a bucket created by the toolkit when used for development or the pipeline artifacts bucket in CI/CD. Either way, the desired state of an "asset store" with a bunch of specific artifacts is reached.
So let's see what types of "app resources" we need for the app model:
Would it be useful to model the "Asset Store"? Probably not really needed. An asset by definition is stored somewhere. The details of how/where/who defines the asset store can be left undefined in the model, and implemented differently by different tools.
So now, the stack + runtime code example will look like this:
Resources:
MyLambdaCodePackage:
Type: AWS::App::Asset
Properties:
File: "my-handler.zip"
MyTemplate:
Type: AWS::App::Asset
Properties:
File: "my-template.json"
MyStack:
Type: AWS::App::Stack
Properties:
TemplateURL: ${MyTemplate.URL}
Parameters:
MyLambdaCodeS3Bucket: ${MyLambdaCodePackage.Bucket}
MyLambdaCodeS3Key: ${MyLambdaCodePackage.Key}
AWS::S3::Object
In order to not confuse people (and ourselves) too much, how about we change the namespace of our app-level resources? CDK::S3::Object
?
The toolkit will expect to always find an
index.json
file in the working directory.
Don't understand why we have to go through the filesystem all of a sudden. Can we not do the same thing as before with stdout
?
It will read the file and form a deployment plan (calculate dependency graph based on references)
We will still need to specify a target
in some way. The whole "app model" is going to contain the entire application in all stages.
I think I would like to introduce the concept of an App
in there (distinct from our current App
class which doesn't really model anything but is there as an implementation detail), which is a thing that has a name that makes sense to deploy in one go. It might consist of one or more Stack
s which each depend on one or more resources, etc.
That is not to say you couldn't just address the S3::Object
operation as a target if you wanted to, but that's not going to be the common use case. Nor is it going to be the common use case to just deploy everything in a CDK app.
Cross stack refs: how do we implement cross stack/cross env references in this model?
They will have to be Parameter
s, just like file uploads.
There's a limit of 50 or 60 parameters. We might run into that for complicated apps.
Docker images
Local docker build
or whatever the command is again, push the image to ECR, return the ARN.
Apropos of nothing: I still wonder whether it makes sense for customers to define Stack
s on their own. Shouldn't they be defining App
s instead (new-style App
mentioned above, not our current one), which is then sliced into Stack
s by our runtime system in a way that makes most sense?
The rules are not that complicated:
(account, region)
per Stack
.Stack
(hand-waving on how we detect which object that is, but probably doable).I like it! It generalizes nicely!
We'll have to build a copy of CloudFormation's evaluation engine, but I guess it won't be too complicated?
Critical questions:
{ Ref }
, only { Fn::GetAtt }
please! :){ Fn::Whatever }
functions as well? Or only the ones we need?The more I think about Stack
s the more I think they're an implementation detail we shouldn't care about: the system should just take care of them.
The biggest downside to this is that people migrating over from CloudFormation, who want to recreate their existing templates in CDK, are going to want strict control over them.
I think we should give them that control without putting the concept of a Stack
front and center.
Nice!
@rix0rrr
CDK::S3::Object
+1 - this makes it much clearer that these aren't specific AWS resources!
@eladb et al:
How does the packaging story play in here? As part of my normal development workflow I usually want to 'package' my app and deploy it in quick succession. Then again as part of my CI/CD pipeline I need to be able to build my application before I deploy it. Is the idea that packaging (e.g. compilation, docker build
etc) is still done as a separate step or can I define that behaviour in the app-model also?
Could I define a construct
that does the packaging for my application (with common implementations)? Then the synthesize
method is simply the invocation of that packaging system (if that's the case synthesize
feels a little wrong as the verb). In this world the CDK toolkit is the entry-point to the process and owns the orchestration.
The other option is to reverse this, I suspect that many customers are going to want to be able to invoke CDK as part of their 'regular' build-tool. As a JVM developer I just want to be able to do gradle deploy
which could package and deploy my application - in this model the build tool owns the orchestration and handles the packaging before CDK/app-model gets involved.
Thoughts?
@rix0rrr wrote: The more I think about Stacks the more I think they're an implementation detail we shouldn't care about: the system should just take care of them.
You are right. Stacks as an abstraction are not really important, and perhaps at some point we can get rid of them when you define apps at a high level. Still, they are a mechanism that apps can use to isolate regions/account and updates, and therefore they make sense at the app model layer I believe.
@kiiadi, agreed about AWS::S3::Object
. We'll use AWS::App::Asset
and AWS::App::Stack
.
Regarding packaging - that's a good point. At the moment, the model is that packaging is done by idiomatic tools and consumed as assets by the app model, but I am not sure that's good enough. We need to think about it further. I think the idea of a construct performing a build is very interesting (and I am not sure that the "synthesis" terminology breaks, i.e. "this construct represents your Lambda's handler, and in order to synthesize it you need to build the code"). But I agree about IDEs and native tools. I am not sure it's a good idea for us to go to the software build business if we can avoid it and let people use their normal tools and IDEs.
@rix0rrr, instead of AWS::S3::Object
we will use AWS::App::Asset
. The distinction between an asset and an S3 object is that an asset implies an "asset store", which can have different implementations when running locally or via CI/CD.
Don't understand why we have to go through the filesystem all of a sudden. Can we not do the same thing as before with stdout?
Since we are going to want to emit multiple artifacts, which can potentially include GiBs of runtime code, I believe we should emit those directly into the filesystem. By the way, in many cases (e.g. those GiBs of runtime code), the construct can just emit a symlink to the actual code instead. If we go down the STDOUT path design, we will eventually define a filesystem protocol, which is not our business.
We will still need to specify a target in some way. The whole "app model" is going to contain the entire application in all stages.
You mean in the case where you only want to deploy a single resource from the app model (i.e. a dev stack). Yes, that should be possible. In this case, the toolkit should be able to deploy all it's dependencies as well. Added to the design.
I think I would like to introduce the concept of an App in there (distinct from our current App class which doesn't really model anything but is there as an implementation detail), which is a thing that has a name that makes sense to deploy in one go. It might consist of one or more Stacks which each depend on one or more resources, etc.
That's a good idea. Maybe just AWS::App::ResourceGroup
?
Cross stack refs: how do we implement cross stack/cross env references in this model? They will have to be Parameters, just like file uploads. There's a limit of 50 or 60 parameters. We might run into that for complicated apps.
I will add to the doc. It's in the list. It will be awesome (🤞)
Docker images Local docker build or whatever the command is again, push the image to ECR, return the ARN.
Also in the TODO list.
Apropos of nothing: I still wonder whether it makes sense for customers to define Stacks on their own.
As mentioned above, I think that's a good idea, but not at the app model level. These are abstractions we should easily implement in the CDK itself. Makes sense?
Hey guys,
Can we try to tackle not-just-AWS case for the format? I just want to avoid ECS-EKS situation. And we can be pioneers for this initiative in the industry.
Addendum: Do you guys think it would make sense to factor-out deployment system from templating one? Going to the extreme that two can be written in different languages. For instance, cloud assembly deployment toolkit can be written in Go/Rust (because it's cool and new and compiled and bundled and self-sufficient ...) and templating system can be CDK, Cloud-templates, Troposphere, GoFormation, or something internal to a company who wants to use the format.
That would be really cool.
The more I think about
Stack
s the more I think they're an implementation detail we shouldn't care about: the system should just take care of them.The biggest downside to this is that people migrating over from CloudFormation, who want to recreate their existing templates in CDK, are going to want strict control over them.
I think we should give them that control without putting the concept of a
Stack
front and center.
This is partly true. I believe structuring the code per "stacks" and naming them accordingly is actually a good practice.
something like the following:
stacks/
s3_stack/
bucket.ts
iam_stack/
user.ts
policy.ts
As we progress in our design for an end-to-end developer experience for cloud applications, and start to venture into more complex use cases, there's a common pattern that keeps emerging, where more than a single artifact is needed in order to deploy cloud applications. Moreover, these artifacts interact with each other.
Some examples:
In all of these cases, we still want the developer and CI/CD workflows to operate at the app level. What does it mean? It means that if a construct defines a Lambda function that uses some runtime code, when a developer invokes
cdk deploy
, the runtime code should be uploaded to an S3 bucket and the CloudFormation template should reference the correct bucket/key (and also make sure Lambda has permissions to read from the bucket). This should also seamlessly work within a CI/CD pipeline.Since the CDK's unit of reuse is a construct library, any solution must not look at the problem from the app's perspective, but from a construct perspective. It should be possible for a construct library to encapsulate runtime code, nested stacks, etc. Then, when this library is consumed, the workflow above should continue to work exactly in the same way.
Design approach
At the moment, synthesis is actually performed at the
App
level and is tailored to produce a single artifact (CloudFormation template). The proposed design will allow any construct in the tree to participate in the synthesis process and emit arbitrary artifacts.But it is not sufficient to just emit multiple artifacts. We need to model their interaction somehow (dependencies, data-flow, how do these artifact interact with cloud resources, etc).
Generalizing this, we effective need to have some way to describe the model for our app. If a CloudFormation template define the model for a single stack, we need a way to describe an entire cloud application.
Naturally, we should prefer a desired state configuration approach where the app model doesn't describe steps but rather the desired state, and then tools (like the CDK toolkit or CI/CD systems) can help achieve this desired state.
Let's say that the toolkit only knows how to work with app model files, which describe the desired state of an app in a format similar to CloudFormation templates:
This is not a CloudFormation template! It's an App Model file. It uses the same structure to define the desired state of an entire application. This file, together with all the artifacts synthesized from the app form a self-contained cloud app package ("cloud executable"?).
When tools read this file, they can produce a deployment plan for this app:
./my-handler.zip
and./my-template.json
to an S3 bucket.CreateStack
API, use the S3 URL to specify the template URL and pass in parameters that resolve to the location of the S3 bucket and key of the Lambda runtime archive.The power of this approach is that it is highly extensible. Anyone can implement App Resources which will participate in this process. The desired state approach deems that each resource needs to be able to be CREATED, UPDATED or DELETED, and also DIFFed against the desired state.
Implementation
Synthesis
Each construct in the tree may implement a method
synthesize(workdir)
which will be called during synthesis. Constructs can emit files (or symlinks) into a working directory at this time.App Model Resources
Similarly to CloudFormation Resources, we can defined constructs that represent app model resources (
AppStack
,AppAsset
). Similarly to how CloudFormation resources are implemented, these constructs will implement a methodtoAppModel()
which will return an app model JSON chunk to be merged into the complete app model.The
App
construct is now a simple construct, in it'ssynthesis()
method, it will collect all app resources from the tree, merge them together and emit anapp-model.json
(orindex.json
) into the working directory.The toolkit will expect to always find an
index.json
file in the working directory. It will read the file and form a deployment plan (calculate dependency graph based on references). Then, it will execute the deployment plan.The toolkit can either deploy the entire app or only a subset of the resources, in which case it can also deploy any dependencies of this set.
Each app resource will have a "provider" implemented in the toolkit via an extensible framework. Providers will implement the following operations:
In the normal flow, the toolkit will simply invoke the create/update operation in topological order (and concurrently if possible). It will supply the contents of the
Properties
object which represents the desired state. If a property includes a reference to another resource (viaFn::GetAtt
), it will replace the token with the value from the other resource.TODO
List of use cases we should tackle for this new design: