AWS APIGateway × OpenAPI (3. Output)
2022-08-02
I have been working on a library that integrates an OpenAPI definition with a REST API definition on the CDK. This is the third blog post of the series that will walk you through the development of the library.
Background
In the first blog post of this series, we left the following challenge,
When should the library actually output the OpenAPI definition file?
- Should a user explicitly call a function to save?
- Or, can we magically save the OpenAPI definition file like the CDK does to the CloudFormation template?
In this blog post, we tackle the latter case; i.e., make the library output an OpenAPI definition without an explicit call by a user.
When does the CDK output a CloudFormation template?
If we can trap the timing when the AWS Cloud Development Kit (CDK) outputs a CloudFormation template, we may output an OpenAPI definition without an explicit call by a user.
So we are going to walk through the CDK source code* to locate hooks we can utilize to write an OpenAPI definition.
Let's look into the cdk
command first.
If you want to skip the extensive code review, you can jump to the "Hooks" section.
* I have analyzed the then latest version 2.34.2
of the CDK.
Things may be different in other CDK versions.
Where is the cdk command defined?
The cdk
command is included in the packages/aws-cdk
folder in the CDK repository.
Since the repository of the CDK is huge, it was not that obvious to me where the cdk
command was defined.
In this blog post, I abbreviate the packages/aws-cdk
folder to aws-cdk
.
What do the synth and deploy subcommands do?
In terms of the CloudFormation template generation, the following two cdk
subcommands are our concern,
The command line interface of the cdk
command is defined in aws-cdk/lib/cli.ts
.
In the long lines of yargs
settings, you can find definitions of synth
and deploy
subcommands.
synth
- options: aws-cdk/lib/cli.ts#L86-L89
- handler: aws-cdk/lib/cli.ts#L528-L534
- Ends up with a call to
CdkToolkit#synth
.
- Ends up with a call to
deploy
- options: aws-cdk/lib/cli.ts#L107-L150
- handler: aws-cdk/lib/cli.ts#L454-L483
- Ends up with a call to
CdkToolkit#deploy
.
- Ends up with a call to
The cdk
subcommands eventually call corresponding methods of CdkToolkit
defined in aws-cdk/lib/cdk-toolkit.ts
.
CdkToolkit#synth
Definition: aws-cdk/lib/cdk-toolkit.ts#L517-L545
It calls CdkToolkit#selectStacksForDiff
at aws-cdk/lib/cdk-toolkit.ts#L518:
;
Although it is not clear from the method name, CdkToolkit#selectionStacksForDiff
does essential work for the CloudFormation template generation.
It eventually calls CdkToolkit#assembly
at aws-cdk/lib/cdk-toolkit.ts#L621:
;
CdkToolkit#deploy
Definition: aws-cdk/lib/cdk-toolkit.ts#L126-L282
It calls CdkToolkit#selectStacksForDeploy
at aws-cdk/lib/cdk-toolkit.ts#L140:
;
Like CdkToolkit#synth
, CdkToolkit#selectStacksForDeploy
does essential work for the CloudFormation template generation.
It also ends up with a call to CdkToolkit#assembly
at aws-cdk/lib/cdk-toolkit.ts#L608:
;
CdkToolkit#assembly
Definition: aws-cdk/lib/cdk-toolkit.ts#L690-L692
private cacheCloudAssembly?: boolean : Promise<CloudAssembly>
It is equivalent to a call to CloudExecutable#synthesize
.
CloudExecutable
is defined in aws-cdk/lib/api/cxapp/cloud-executable
.
CloudExecutable#synthesize
Definition: aws-cdk/lib/api/cxapp/cloud-executable.ts#L63-L68
It calls CloudExecutable#doSynthesize
at aws-cdk/lib/api/cxapp/cloud-executable.ts#L65:
this._cloudAssembly = await ;
CloudExecutable#doSynthesize
Definition: aws-cdk/lib/api/cxapp/cloud-executable.ts#L70-L124
The actual synthesis is done at aws-cdk/lib/api/cxapp/cloud-executable.ts#L79:
;
this.props.synthesizer
is a Synthesizer
defined at aws-cdk/lib/api/cxapp/cloud-executable.ts#L14:
;
In the context of aws-cdk/lib/cli.ts
, this.props.synthesizer
is always execProgram
as it is initialized in aws-cdk/lib/cli.ts#L305-L309.
;
execProgram
is defined in aws-cdk/lib/api/cxapp/exec.ts#L12-L136.
According to the following lines, it runs the command specified to the app
option of the cdk
command (aws-cdk/lib/api/cxapp/exec.ts#L54, aws-cdk/lib/api/cxapp/exec.ts#L65, and aws-cdk/lib/api/cxapp/exec.ts#L86 respectively):
;
;
await ' ';
You may think you have never specified the app
option to the cdk
command.
If you initialize your CDK project with the cdk init
command, the command creates the cdk.json
file and saves the default app
option value in it.
If you look into your cdk.json
file, you will find a line similar to the following:
"app": "npx ts-node --prefer-ts-exts bin/cdk.ts",
This command (npx ts-node --prefer-ts-exts bin/cdk.ts
) is what execProgram
executes.
After running the command given to the app
option, execProgram
loads the artifacts from the output folder specified to the output
option of the cdk
command (aws-cdk/lib/api/cxapp/exec.ts#L67, aws-cdk/lib/api/cxapp/exec.ts#L78, and aws-cdk/lib/api/cxapp/exec.ts#L88 respectively):
;
env = outdir;
return outdir;
The output
option is "cdk.out"
by default (aws-cdk/lib/settings.ts#L75-L79):
public readonly defaultConfig = new Settings;
Thus, our next focus is what our bin/cdk.ts
does.
When does App output a CloudFormation template?
In our bin/cdk.ts
, we usually create an instance of App
.
So when does App
output a CloudFormation template?
Let's look into the source code of App
.
App
is defined in core/lib/app.ts#L94-L155.
In this blog post, I abbreviate the packages/@aws-cdk/core
folder to core
.
In the constructor of App
, there is an interesting line at core/lib/app.ts#L131:
'beforeExit',;
It makes the process running App
call App#synth
at exit.
So we can anticipate the answer would be in App#synth
.
Since App
extends Stage
, App#synth
is equivalent to Stage#synth
.
Stage#synth
Definition: core/lib/stage.ts#L174-L183
public options: StageSynthesisOptions = : cxapi.CloudAssembly
It calls synthesize
defined at core/lib/private/synthesis.ts#L23-L50.
I have found that the following two lines in synthesize
lead to two possible hooks we can count on,
-
core/lib/private/synthesis.ts#L36
root;
validateTree
leads to a "validator hook". -
core/lib/private/synthesis.ts#L47
root, builder, options.validateOnSynthesis;
synthesizeTree
leads to a "_toCloudFormation hook".
validateTree
Definition: core/lib/private/synthesis.ts#L201-L214
validateTree
traverses (visits) all the nodes in the constructs tree starting from root
and applies Node#validate
to each of them.
Node#validate
calls IValidation#validate
of IValidation
s attached to the node with Node#addValidation
.
So IValidation
can be a hook to output an OpenAPI definition.
Please refer to "Validator hook" for how to use it.
synthesizeTree
Definition: core/lib/private/synthesis.ts#L174-L191
synthesizeTree
traverses (visits) all the nodes in the constructs tree starting from root
and processes each construct.
The important line is at core/lib/private/synthesis.ts#L183:
session;
Here construct
is an instance of Stack
, and Stack#synthesizer
is DefaultStackSynthesizer
by default.
So the above line usually becomes a call to DefaultStackSynthesizer#synthesize.
DefaultStackSynthesizer#synthesize
Definition: core/lib/stack-synthesizers/default-synthesizer.ts#L387-L424
DefaultStackSynthesizer#synthesize
calls DefaultStackSynthesizer#synthesizeStackTemplate
at core/lib/stack-synthesizers/default-synthesizer.ts#L400:
this.stack, session;
DefaultStackSynthesizer#synthesizeStackTemplate
is equivalent to a call to Stack#_synthesizeTemplate
(core/lib/stack-synthesizers/default-synthesizer.ts#L380-L382):
protected stack: Stack, session: ISynthesisSession
Stack#_synthesizeTemplate
Definition: core/lib/stack.ts#L770-L804
This method calls Stack#_toCloudFormation
at core/lib/stack.ts#L779:
;
Stack#_toCloudFormation
Definition: core/lib/stack.ts#L1007-L1045
Please pay attention to the following two lines (core/lib/stack.ts#L1031-L1032):
;
;
This method collects all the child CfnElement
s with cfnElements
, and applies CfnElement#_toCloudFormation
to each of them.
CfnElement
is the base class of every L1 construct, and CfnElement#_toCloudFormation
is an internal method defined at core/lib/cfn-element.ts#L161.
So the CfnElement#_toCloudFormation
method can be another hook to output an OpenAPI definition.
Please refer to "_toCloudFormation hook" for how to use it.
Hooks
According to the above analysis, there may be two hooks we can utilize to output an OpenAPI definition.
Validator hook
A "validator hook" utilizes IValidation
that we can attach to Node
s with Node#addValidation
.
Please refer to the section "validateTree" for more detailed analysis.
I have chosen this hook to implement my library so far because of its simplicity.
The following code is an excerpt from the constructor of RestApiWithSpec
.
You can find the full definition on my GitHub repository.
scope: Construct, id: string, readonly props: RestApiWithSpecProps
A drawback is that it does not work if validation is disabled.
_toCloudFormation hook
A "_toCloudFormation hook" utilizes an internal method CfnElement#_toCloudFormation
that we can override.
Please refer to the section "synthesizeTree" for more detailed analysis.
If I used this hook in my library, the constructor of RestApiWithSpec
could become something similar to the following:
scope: Construct, id: string, readonly props: RestApiWithSpecProps
There are some disadvantages to this hook.
CfnElement#_toCloudFormation
is an internal method that may be subject to change.- In my experiment,
ToCloudFormationHook#_toCloudFormation
was called twice (I have not figured out why).
Conclusion
In this blog post, we have walked through the source code of the CDK. We have located the following two hooks we can use to output an OpenAPI definition,
I have chosen "Validator hook" for my library because of its simplicity.
Appendix
This section introduces some utility functions used in the CDK.
visit
Definition: core/lib/private/synthesis.ts#L219-L232
This function traverses all the nodes in the constructs tree starting from root
, and applies a function specified to cb
to each of them.
cfnElements
Definition: core/lib/stack.ts#L1279-L1292
This function recursively collects every CfnElement
in the constructs tree starting from node
.