This guide walks you through a codebase that uses the CDK for Terraform to deploy a serverless application to AWS. The application features a small frontend written in React that connects to an API powered by AWS APIGateway and AWS Lambda.
The repository can be found here:
https://github.com/hashicorp/cdktf-integration-serverless-example
(includes screenhots)
There are also examples available using Docker containers on AWS ECS or GCP K8S.
This guide assumes basic familarity with the CDK for Terraform and that you have
it installed already. If you are new to the CDKTF it is recommended to first
have a look at the getting started guide
which explains the CDK for Terraform itself in more detail and the commands used
to deploy infrastructure. This guide will mainly explain the codebase of the
serverless example and what it does.
For a simpler serverless example there is also a step by step tutorial on
HashiCorp Learn.
aws
CLI
The deployment of the frontend requires the AWS CLI to be installed on your system.
The project was initialized using the following cdktf init
command:
cdktf init --local --template=typescript
You can either clone the whole repository or initialize a new one with the aforementioned command and copy the code from the example repo as we move along and discuss it.
The example uses environment variables to configure the access credentials for the AWS Terraform provider. You can refer to its docs to see which variables to set.
in the root directory of the example you can find the main.ts
file which
contains the configuration for the stacks that are going to be deployed. A stack
has its own Terraform state and is deployed separately from other stacks.
The example consists of three stacks:
- FrontendStack
- PostsStack
- PreviewStack
For the FrontendStack
and the PostsStack
there are two instances each, one for each environment (dev
and prod
).
The frontend stack manages infrastructure for a statically hosted web frontend
and deploys that frontend via a short script.
The frontend itself is set up with
Create React App inside the frontend/code
directory and its build output is by default written into the
frontend/code/dist
directory. This guide will not cover the React frontend
itself. See the "Frontend" section below for specifics.
The posts stack manages the required infrastructure making up the serverless
posts api. Its implementation is located inside the posts
directory and is
described in detail in the "Posts API" section below.
The preview stack is not yet used but is supposed to show that both the frontend and the posts API could be deployed at once while sharing a single Terraform state. This will make it simpler to deploy the whole infrastructure at once in CI for implementing a pull request preview feature. However the required CI configuration does not exist yet and will be part of a future iteration on this example.
The frontend is located inside the frontend
directory. It is hosted via an AWS
S3 Bucket and AWS CloudFront.
The infrastructure for the frontend is defined in frontend/index.ts
. That file
exports a Frontend
class which extends the Resource
construct from CDKTF. We
use this pattern to create new building blocks, that can be added to a
TerraformStack
(see root main.ts
that uses our new Frontend
resource).
The Frontend
constructor gets passed an options
object which passes down the
current environment
(e.g. dev
) and the apiEndpoint
for the posts api
(covered later) which is required in the build process of the frontend as it
needs to be injected into the static output.
Inside the constructor a few resources are defined that are required for the frontend to be deployed.
aws.S3Bucket
The AWS S3 Bucket is used to host the files for the frontend (i.e. HTML, JS and
some CSS files). We enable the
website hosting
capabilities of the S3 Bucket to serve our index.html
for any route that might
have been requested.
aws.S3BucketPolicy
The bucket policy makes it possible to make contents of the S3 bucket publicly
available. It is required as by default nobody would be allowed to access our
files.
aws.CloudfrontDistribution
AWS CloudFront is a content delivery network (CDN) which we use to deliver our
files faster to any location worldwide. We configure it to respond with data
from the S3 website endpoint url (bucket.websiteEndpoint
). Our hosted website
will be available via the domain name of this CloudFront Distribution for now.
In a future iteration of this guide, we might add a custom domain name to it.
File
We use Terraform to write the .env.production.local
file into our React
frontend directory (frontend/code
). This file is used by Create React App when
building our application to inject the REACT_APP_API_ENDPOINT
environment
variable into our frontend build. Create React App will only inject environment
variables with the prefix REACT_APP_
into static files to not accidentally
expose secrets from environment variables.
Furthermore we write the name of the S3 Bucket we use into this file as well to
be able to use that bucket as a target when we deploy the frontend (explained
later).
TerraformOutput
We define an output with the name frontend_domainname
which exposes the domain
name from our CloudFront Distribution and prints it in the shell after deploying
our infrastructure. This makes it easy for us to know where our frontend can be
reached after we deployed it as that domain is generated automatically.
As mentioned before we let Terraform write the .env.production.local
file. The
deployment of the frontend can be triggered via npm run deploy
inside our
frontend/code
directory. This will execute a build via the predeploy
step
defined in the frontend/code/package.json
file and afterwards execute a small
deployment script that can be found in frontend/code/scripts
. This script
requires the
AWS CLI
to be installed and will spawn the aws s3 sync
command to copy the build
output to our S3 bucket.
The infrastructure and code for the serverless function for the posts api is
defined inside the posts
directory.
The infrastructure code is split into multiple files. The main entrypoint is
posts/index.ts
which only combines the groups of resources the posts api
consists of into a single resource named Posts
.
The posts storage custom resource is located inside the posts/storage.ts
file
and currently only defines a single DynamodbTable
resource which is used for
storing the posts.
The post api custom resource can be found in the furtherly nested
posts/api/index.ts
file and contains resources required for the serverless
infrastructure and the code for the deployed AWS Lambda function (in
posts/api/lambda
).
In the constructor the following resources are defined for the infrastructure:
NodejsFunction
This resource is a custom one that is defined in lib/nodejs-function.ts
and
described in the section "Posts API deployment" below. It compiles and bundles
the TypeScript code for the AWS Lambda function and makes it available to
Terraform via the cdktf.Asset
construct.
aws.IamRole
Creates an IAM role for the AWS Lambda function which allows it to access the
DynamoDB Table used to store the posts.
aws.IamRolePolicyAttachment
Attaches the AWS managed default IAM policy AWSLambdaBasicExecutionRole
to the
aforementioned IAM role which e.g. allows the Lambda to write its logs to
CloudWatch.
aws.LambdaFunction
The heart of our API. It handles the following requests:
GET /posts
GET /posts/:id/detail
POST /posts
The code for the lambda function itself is compiled via the custom
NodejsFunction
resource and its source can be found inside posts/api/lambda
.
We pass the name of the DynamoDB Table (used for storing the posts) as the
environment variable DYNAMODB_TABLE_NAME
to the Lambda function.
aws.Apigatewayv2Api
The API Gateway is the publicly accessible endpoint for our posts API, which has
the lambda defined as a target
so it gets invoked for requests that hit the
api.
We also configure CORS to allow requests from any origin here as we don't have
dedicated domain names yet.
aws.LambdaPermission
This resource is required to allow the API Gateway to invoke the Lambda function
for incoming requests.
The generator is currently empty and has no effect on the infrastructure but will in the future be extended to create some dummy posts in a regular interval via some "cloud native" / serverless cron definition.
There is no additional script required as is for the frontend. The AWS Lambda
function which handles the api requests and connects to DynamoDB to store and
retrieve posts is deployed automatically via the CDK for Terraform
(cdktf deploy
).
For this to work, we have defined a utility in lib/nodejs-function.ts
that
uses esbuild
to compile the function
synchronously while the TypeScript code is synthesized (cdktf synth
) to the
JSON output (cdk.tf.json
) that is later used with Terraform.
The build output is uploaded and linked to the Lambda function by Terraform.
The posts API is located inside the posts
directory. Naming it after its
business domain was inspired by the style of the
AWS ecommerce platform example.
The CDK for Terraform currently does not manage cross stack references
automatically. So we have to connect our two stacks (FrontendStack
and
PostsStack
) manually.
As separate stacks have separate Terraform states we have to expose values
inside one stack via TerraformOutput
s to be able to refer to them (via a
terraform remote state resource) in the other stack.
You can see this in main.ts
in the PostsStack
:
const output = new TerraformOutput(this, "apiEndpoint", {
value: posts.apiEndpoint,
});
this.apiEndpointOutputId = output.friendlyUniqueId;
Where we create an output with the (stack local) id apiEndpoint
and expose its
global "friendlyUniqueId
" on the stack itself. This apiEndpointOutputId
in
turn can then be used to access that value in the FrontendStack
:
const apiState = options.createApiRemoteState(this, "api");
const apiEndpoint = apiState.getString(options.apiEndpointOutputId);
with createApiRemoteState
and apiEndpointOutputId
being passed like this:
...
apiEndpointOutputId: postsDev.apiEndpointOutputId,
createApiRemoteState: (scope, id) => new DataTerraformRemoteStateLocal(scope, id, {
path: path.resolve(__dirname, `terraform.${postsDev.name}.tfstate`),
})
...
While the DataTerraformRemoteStateLocal
resource could also have been
specified inside the constructor of the FrontendStack
this pattern allows us
colocate its creation with the instantiation of the Stack. If we would use a
RemoteStateResource that tracks the Terraform state via e.g. Terraform Cloud or
AWS S3 instead of in a local file, we could switch the
DataTerraformRemoteStateLocal
resource with the respective other resource to
refer to the right state.
You can upvote and subscribe to this issue which tracks support for cross stack references.
If you encounter any issues with the CDK for Terraform don't hesitate to get in touch with us:
- Ask a question on the HashiCorp Discuss using the terraform-cdk category.
- Report a bug or request a new feature.
- Browse all open issues.