Skip to content

Commit 8713ac6

Browse files
rix0rrrElad Ben-Israel
authored and
Elad Ben-Israel
committed
feat(decdk) prototype for declarative CDK (decdk) (#1618)
A prototype for a tool that reads CloudFormation-like JSON/YAML templates which can contain both normal CloudFormation resources (AWS::S3::Bucket) and also reference AWS CDK resources (@aws-cdk/aws-s3.Bucket). See README for details.
1 parent 2480f0f commit 8713ac6

31 files changed

+48314
-1
lines changed

Diff for: scripts/foreach.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@ fi
2020
echo "#!/bin/bash"
2121
echo "set -euo pipefail"
2222

23-
lerna ls | xargs -n1 -I{} echo "lerna exec --stream --scope {} \"$@\""
23+
lerna ls --toposort | xargs -n1 -I{} echo "lerna exec --stream --scope {} \"$@\""

Diff for: tools/decdk/.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
*.js
2+
*.d.ts
3+
!deps.js
4+
test/fixture/.jsii
5+
cdk.schema.json

Diff for: tools/decdk/README.md

+276
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
# deCDK - Declarative CDK
2+
3+
[![experimental](http://badges.github.io/stability-badges/dist/experimental.svg)](http://github.com/badges/stability-badges)
4+
5+
Define AWS CDK applications declaratively.
6+
7+
This tool reads CloudFormation-like JSON/YAML templates which can contain both normal CloudFormation resources (`AWS::S3::Bucket`) and also reference AWS CDK resources (`@aws-cdk/aws-s3.Bucket`).
8+
9+
## Getting Started
10+
11+
Install the AWS CDK CLI and the `decdk` tool:
12+
13+
```console
14+
$ npm i -g aws-cdk decdk
15+
```
16+
17+
This is optional (but highly recommended): You can use `decdk-schema` to generate a JSON schema and use it for IDE completion and validation:
18+
19+
```console
20+
$ decdk-schema > cdk.schema.json
21+
```
22+
23+
Okay, we are ready to begin with a simple example. Create a file called `hello.json`:
24+
25+
```json
26+
{
27+
"$schema": "./cdk.schema.json",
28+
"Resources": {
29+
"MyQueue": {
30+
"Type": "@aws-cdk/aws-sqs.Queue",
31+
"Properties": {
32+
"fifo": true
33+
}
34+
}
35+
}
36+
}
37+
```
38+
39+
Now, you can use it as a CDK app (you'll need to `npm install -g aws-cdk`):
40+
41+
```console
42+
$ cdk -a "decdk hello.json" synth
43+
Resources:
44+
MyQueueE6CA6235:
45+
Type: AWS::SQS::Queue
46+
Properties:
47+
FifoQueue: true
48+
Metadata:
49+
aws:cdk:path: hello2/MyQueue/Resource
50+
```
51+
52+
As you can see, the deCDK has the same semantics as a CloudFormation template. It contains a section for “Resources”, where each resource is defined by a *type* and a set of *properties*. deCDK allows using constructs from AWS Construct Library in templates by identifying the class name (in this case `@aws-cdk/aws-sqs.Queue`).
53+
54+
When deCDK processes a template, it identifies these special resources and under-the-hood, it instantiates an object of that type, passing in the properties to the object's constructor. All CDK constructs have a uniform signature, so this is actually straightforward.
55+
56+
## Development
57+
58+
### Examples/Tests
59+
60+
When you build this module, it will produce a `cdk.schema.json` file at the root, which is referenced by the examples in the [`examples`](./examples) directory. This directory includes working examples of deCDK templates for various areas. We also snapshot-test those to ensure there are no unwanted regressions.
61+
62+
## Design
63+
64+
"Deconstruction" is the process of reflecting on the AWS Construct Library's type system and determining what would be the declarative interface for each API. This section describes how various elements in the library's type system are represented through the template format.
65+
66+
### Constructs
67+
68+
Constructs can be defined in the `Resources` section of the template. The `Type` of the resource is the fully-qualified class name (e.g. `@aws-cdk/aws-s3.Bucket`) and `Properties` are mapped to the deconstructed type of the construct's "Props" interface (e.g. `BucketProps`).
69+
70+
### Data Interfaces ("Props")
71+
72+
jsii has a concept of "data interfaces", which are basically interfaces that do not have methods. For example, all construct "props" are data interfaces.
73+
74+
> In some languages (Python, Ruby), if a method accepts a data interface as the last argument, interface properties can be used as keyword arguments in the method call. Other languages have a different idiomatic representation of data such as Java PoJos and Builders.
75+
76+
deCDK maps data interfaces to closed JSON objects (no additional properties), and will recursively deconstruct all property types.
77+
78+
### Primitives
79+
80+
Strings, numbers, booleans, dates, lists and maps are all deconstructed 1:1 to their JSON representation.
81+
82+
### Enums
83+
84+
Enums are mapped to JSON schema enums.
85+
86+
### References
87+
88+
If deCDK encounters a reference to another __construct__ (a type that extends `cdk.Construct` or an interface that extends `cdk.IConstruct`), it will allow referencing it via a “Ref” intrinsic. For example, here's a definition of an ECS cluster that references a VPC:
89+
90+
```yaml
91+
Resources:
92+
VPC:
93+
Type: "@aws-cdk/aws-ec2.VpcNetwork"
94+
Properties:
95+
maxAZs: 1
96+
Cluster:
97+
Type: "@aws-cdk/aws-ecs.Cluster"
98+
Properties:
99+
vpc:
100+
Ref: VPC
101+
```
102+
103+
### Enum-like Classes
104+
105+
Based on the AWS Construct Library's consistent guidelines and conventions, which are also enforced by a tool we use called “awslint”, deCDK is also capable of expressive more complex idioms. For example, enum-like classes, which are classes that expose a set of static properties or methods can be mapped to JSON enums or method invocations. For example, this is how you define an AWS Lambda function in the CDK (TypeScript):
106+
107+
```ts
108+
new lambda.Function(this, 'MyHandler', {
109+
handler: 'index.handler',
110+
runtime: lambda.Runtime.NodeJS810,
111+
code: lambda.Code.asset('./src')
112+
});
113+
```
114+
115+
And here's the deCDK version:
116+
117+
```json
118+
{
119+
"MyHandler": {
120+
"Type": "@aws-cdk/aws-lambda.Function",
121+
"Properties": {
122+
"handler": "index.handler",
123+
"runtime": "NodeJS810",
124+
"code": { "asset": { "path": "./src" } }
125+
}
126+
}
127+
}
128+
```
129+
130+
### Polymorphism
131+
132+
Due to the decoupled nature of AWS, The AWS Construct Library highly utilizes polymorphism to expose rich APIs to users. In many cases, APIs would accept an interface of some kind, and various AWS services provide an implementation for that interface. deCDK is able to find all concrete implementation of an interface or an abstract class and offer the user an enum-like experience. The following example shows how this approach can be used to define AWS Lambda events:
133+
134+
```json
135+
{
136+
"Resources": {
137+
"MyTopic": {
138+
"Type": "@aws-cdk/aws-sns.Topic"
139+
},
140+
"Table": {
141+
"Type": "@aws-cdk/aws-dynamodb.Table",
142+
"Properties": {
143+
"partitionKey": {
144+
"name": "ID",
145+
"type": "String"
146+
},
147+
"streamSpecification": "NewAndOldImages"
148+
}
149+
},
150+
"HelloWorldFunction": {
151+
"Type": "@aws-cdk/aws-lambda.Function",
152+
"Properties": {
153+
"handler": "app.hello_handler",
154+
"runtime": "Python36",
155+
"code": {
156+
"asset": { "path": "." }
157+
},
158+
"environment": {
159+
"Param": "f"
160+
},
161+
"events": [
162+
{ "@aws-cdk/aws-lambda-event-sources.DynamoEventSource": { "table": { "Ref": "Table" }, "startingPosition": "TrimHorizon" } },
163+
{ "@aws-cdk/aws-lambda-event-sources.ApiEventSource": { "method": "GET", "path": "/hello" } },
164+
{ "@aws-cdk/aws-lambda-event-sources.ApiEventSource": { "method": "POST", "path": "/hello" } },
165+
{ "@aws-cdk/aws-lambda-event-sources.SnsEventSource": { "topic": { "Ref": "MyTopic" } } }
166+
]
167+
}
168+
}
169+
}
170+
}
171+
```
172+
173+
The keys in the “events” array are all fully qualified names of classes in the AWS Construct Library. The declaration is “Array<IEventSource>”. When deCDK deconstructs the objects in this array, it will create objects of these types and pass them in as IEventSource objects.
174+
175+
### `Fn::GetAtt`
176+
177+
deCDK also supports referencing specific attributes of CDK resources by the intrinsic `Fn::GetAtt`. When processing the template, if an `Fn::GetAtt` is found, and references a CDK construct, the attribute name is treated as a property name of the construct and its value is used.
178+
179+
The following example shows how to output the “url” property of a `@aws-cdk/aws-lambda.Function` from above:
180+
181+
```yaml
182+
Outputs:
183+
HelloWorldApi:
184+
Description: API Gateway endpoint URL for Prod stage for Hello World function
185+
Value:
186+
Fn::GetAtt:
187+
- MyHandler
188+
- url
189+
```
190+
191+
### Raw CloudFormation
192+
193+
If deCDK doesn't identify a resource type as a CDK resource, it will just pass it through to the resulting output. This means that any existing CloudFormation/SAM resources (such as `AWS::SQS::Queue`) can be used as-is.
194+
195+
## Roadmap
196+
197+
There is much more we can do here. This section lists API surfaces with ideas on how to deconstruct them.
198+
199+
### Imports
200+
201+
When decdk encounters a reference to an AWS construct, it currently requires a `Ref` to another resource in the template. We should also support importing external resources by reflecting on the various static `fromXxx`, `importXxx` and deconstructing those methods.
202+
203+
For example if we have a property `Bucket` that's modeled as an `s3.IBucket`, at the moment it will only accept:
204+
205+
```json
206+
"Bucket": { "Ref": "MyBucket" }
207+
```
208+
209+
But this requires that `MyBucket` is defined within the same template. If we want to reference a bucket by ARN, we should be able to do this:
210+
211+
```json
212+
"Bucket": { "arn": "arn-of-bucket" }
213+
```
214+
215+
Which should be translated to a call:
216+
217+
```ts
218+
bucket: Bucket.fromBucketArn(this, 'arn-of-bucket')
219+
```
220+
221+
### Grants
222+
223+
AWS constructs expose a set of "grant" methods that can be used to grant IAM principals permissions to perform certain actions on a resource (e.g. `table.grantRead` or `lambda.grantInvoke`).
224+
225+
deCDK should be able to provide a declarative-style for expressing those grants:
226+
227+
```json
228+
"MyFunction": {
229+
"Type": "@aws-cdk/aws-lambda.Function",
230+
"Properties": {
231+
"grants": {
232+
"invoke": [ { "Ref": "MyRole" }, { "Ref": "AnotherRole" } ]
233+
}
234+
}
235+
}
236+
```
237+
238+
### Events
239+
240+
The CDK employs a loose pattern for event-driven programming by exposing a set of `onXxx` methods from AWS constructs. This pattern is used for various types of event systems such as CloudWatch events, bucket notifications, etc.
241+
242+
It might be possible to add a bit more rigor to these patterns and expose them also via a declarative API:
243+
244+
```json
245+
"MyBucket": {
246+
"Type": "@aws-cdk/aws-s3.Bucket",
247+
"Properties": {
248+
"on": {
249+
"objectCreated": [
250+
{
251+
"target": { "Ref": "MyFunction" },
252+
"prefix": "foo/"
253+
}
254+
]
255+
}
256+
}
257+
}
258+
```
259+
260+
### addXxxx
261+
262+
We should enforce in our APIs that anything that can be "added" to a construct can also be defined in props as an array. `awslint` can enforce this and ensure that `addXxx` methods always return `void` and have a corresponding prop.
263+
264+
### Supporting user-defined constructs
265+
266+
deCDK can deconstruct APIs that adhere to the standards defined by __awslint__ and exposed through jsii (it reflects on the jsii type system). Technically, nothing prevents us from allowing users to "bring their own constructs" to decdk, but those requirements must be met.
267+
268+
### Fully qualified type names
269+
270+
As you might have observed, whenever users need to reference a type in deCDK templates they are required to reference the fully qualified name (e.g. `@aws-cdk/aws-s3.Bucket`). We can obvsiouly come up with a more concise way to reference these types, as long as it will be possible to deterministically translate back and forth.
271+
272+
### Misc
273+
274+
- `iam.PolicyDocument` is be tricky since it utilizes a fluent API. We need to think whether we want to revise the PolicyDocument API to be more compatible or add a utility class that can help.
275+
- We should enable shorthand tags for intrinsics in YAML
276+

Diff for: tools/decdk/bin/decdk

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env node
2+
// tslint:disable-next-line:no-var-requires
3+
require('./decdk.js');

Diff for: tools/decdk/bin/decdk-schema

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env node
2+
// tslint:disable-next-line:no-var-requires
3+
require('./decdk-schema.js');

Diff for: tools/decdk/bin/decdk-schema.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { loadTypeSystem } from '../lib';
2+
import { renderFullSchema } from '../lib/cdk-schema';
3+
4+
// tslint:disable:no-console
5+
6+
async function main() {
7+
const typeSystem = await loadTypeSystem();
8+
const schema = await renderFullSchema(typeSystem, { colors: true, warnings: true });
9+
console.log(JSON.stringify(schema, undefined, 2));
10+
}
11+
12+
main().catch(e => {
13+
console.error(e);
14+
process.exit(1);
15+
});

Diff for: tools/decdk/bin/decdk.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import cdk = require('@aws-cdk/cdk');
2+
import colors = require('colors/safe');
3+
import { DeclarativeStack, loadTypeSystem, readTemplate, stackNameFromFileName } from '../lib';
4+
5+
async function main() {
6+
const args = require('yargs')
7+
.usage('$0 <filename>', 'Hydrate a deconstruct file', (yargs: any) => {
8+
yargs.positional('filename', { type: 'string', required: true });
9+
})
10+
.parse();
11+
12+
const templateFile = args.filename;
13+
const template = await readTemplate(templateFile);
14+
const stackName = stackNameFromFileName(templateFile);
15+
const typeSystem = await loadTypeSystem();
16+
17+
const app = new cdk.App();
18+
new DeclarativeStack(app, stackName, { template, typeSystem });
19+
app.run();
20+
}
21+
22+
main().catch(e => {
23+
// tslint:disable-next-line:no-console
24+
console.error(colors.red(e));
25+
process.exit(1);
26+
});

0 commit comments

Comments
 (0)