Motivation
- For my new frontend project, it will work on CloudFront + S3. I'd like to know how it works.
- I'd like to know if CDK for Terraform is beneficial or not.
- I just want to define AWS resources by Terraform + TypeScript.
What is AWS CDK for Terraform?
AWS CDK stands for AWS Cloud Development Kit.
It's an OSS framework to define cloud application resources by using your familiar programming languages including TypeScript, Python, Java, etc.
When we define our cloud resources, CDK will generate a CloudFormation template.
By using it, the actual resources are created.
From 2020, CDK has started supporting Terraform. So we can choose Terraform instead of CloudFormation when we use CDK.
It means we can define other cloud resources like GCP or Azure as well.
How Route53, CloudFront, and S3 work together?
Before writing the code, let me describe how SPA work with Route53, CloudFront, and S3.
Let's say I have registered example.com
domain in Route53.
And i'd like to host a SPA with blog.example.com
.
To achieve it, the following settings are necessary.
- Creating an SSL certification by ACM
- Creating a bucket on S3 that will have the SPA related files like HTML, JavaScript, CSS, etc
- Creating a CloudFront distribution
- Associating the distribution with the certification and the bucket
- Associating
blog.example.com
with the distribution
Let's configure them with CDK for Terraform + TypeScript.
Creating a project
First of all, cdktf-cli
is necessary.
It can be installed via npm.
npm i -g cdktf-cli@latest
After the installation, you can create a project of CDK for Terraform.
This time I'd like to use TypeScript. So the command to create a project is like this
mkdir my-cdktf-project
cd my-cdktf-project
cdktf init --template=typescript
After executing the above command, you are asked to determine the following settings.
- Whether you use Terraform Cloud as a remote state or not
- Project name
- Project description
Do you want to continue with Terraform Cloud remote state management? (Y/n) → n Since I use S3 as a remote state this time
Project Name (my-cdktf-project) → empty
Project Description (A simple getting started project for cdktf.) → empty
After that, the project is ready with the following structure.
my-cdktf-project/
├ .gitignore
├ .npmrc
├ __tests__/
├ cdktf.json
├ help/
├ jest.config.js
├ main.ts
├ node_modules/
├ package-lock.json
├ package.json
├ setup.js
└ tsconfig.json
main.ts
is the place to define the resources. (I’ll define all the resources in the place this time)
The initial code in the file is the following.
import { Construct } from "constructs";
import { App, TerraformStack } from "cdktf";
class MyStack extends TerraformStack {
constructor(scope: Construct, name: string) {
super(scope, name);
// define resources here
}
}
const app = new App();
new MyStack(app, "my-cdktf-project");
app.synth();
Configure Provider and remote state
Provider
The provider is AWS this time.
In order to configure that, @cdktf/provider-aws
is necessary to be installed.
npm i @cdktf/provider-aws
Then you can use the provider.
import { AwsProvider } from "@cdktf/provider-aws";
class MyStack extends TerraformStack {
constructor(scope: Construct, name: string) {
super(scope, name);
// provider
new AwsProvider(this, "default", {
region: "ap-northeast-1",
});
It's the same as using HCL
provider "default" {
region = "ap-northeast-1"
}
To create SSL certification via ACM that is defined later, it's necessary to specify us-east-1 region.
So let me define another provider for that.
const awsProviderUsEast = new AwsProvider(this, "use_east", {
region: "us-east-1",
alias: "us-east-1"
});
Remote state
I'd like to use S3 for the remote state.
It's defined like this
import { App, S3Backend, TerraformStack } from "cdktf";
...
new S3Backend(this, {
bucket: "hogehoge-tfstate",
key: "website",
region: "ap-northeast-1"
});
Creating an SSL certification
With the following code, the certification for blog.example.com
is created with DNS validation.
import { AwsProvider, route53, acm } from "@cdktf/provider-aws";
...
const dataAwsRoute53Zone = new route53.DataAwsRoute53Zone(this, "zone", {
name: "example.com"
});
const acmCertificate = new acm.AcmCertificate(this, "cert", {
domainName: "blog.example.com",
validationMethod: "DNS",
provider: awsProviderUsEast,
lifecycle: {
createBeforeDestroy: true
}
});
const record = new route53.Route53Record(this, "validation_record", {
name: acmCertificate.domainValidationOptions("0").resourceRecordName,
type: acmCertificate.domainValidationOptions("0").resourceRecordType,
records: [acmCertificate.domainValidationOptions("0").resourceRecordValue],
zoneId: dataAwsRoute53Zone.zoneId,
ttl: 60
});
new acm.AcmCertificateValidation(this, "certification_validation", {
certificateArn: acmCertificate.arn,
validationRecordFqdns: [record.fqdn],
provider: awsProviderUsEast
});
Creating a bucket on S3
HTML, JavaScript, CSS files will be in this bucket.
import { AwsProvider, s3, route53, acm } from "@cdktf/provider-aws";
...
const s3Bucket = new s3.S3Bucket(this, "blog_example_com", {
bucket: "blog.example.com"
});
Creating a CloudFront distribution
Next, will create OAI first.
OAI(origin access identity) is a special CloudFront user.
By associating it with a Cloudfront distribution, CloudFront can access an S3 bucket that doesn't allow public access.
import { AwsProvider, s3, route53, acm, cloudfront } from "@cdktf/provider-aws";
...
const oai = new cloudfront.CloudfrontOriginAccessIdentity(this, "oai", {
comment: "blog.example.com"
});
And the S3 bucket is supposed to be accessed by the OAI.
So I defined the following bucket policy.
import { AwsProvider, s3, route53, acm, cloudfront, iam } from "@cdktf/provider-aws";
...
const policyDocument = new iam.DataAwsIamPolicyDocument(this, "policy_document", {
statement: [{
actions: ["s3:GetObject"],
resources: [`${s3Bucket.arn}/*`],
principals: [{
type: "AWS",
identifiers: [oai.iamArn]
}]
}]
});
new s3.S3BucketPolicy(this, "connect_bucket_and_policy", {
bucket: s3Bucket.id,
policy: policyDocument.json
});
And the following code is the distribution definition.
In the code, the S3 bucket is defined as the origin and the certification is attached.
const distribution = new cloudfront.CloudfrontDistribution(this, "distribution", {
enabled: true,
defaultRootObject: "index.html",
aliases: ["blog.example.com"],
customErrorResponse: [
{
errorCode: 403,
responseCode: 200,
responsePagePath: "/"
}
],
origin: [
{
originId: s3Bucket.id,
domainName: s3Bucket.bucketRegionalDomainName,
s3OriginConfig: {
originAccessIdentity: oai.cloudfrontAccessIdentityPath
}
}
],
defaultCacheBehavior: {
allowedMethods: ["GET", "HEAD"],
cachedMethods: ["GET", "HEAD"],
targetOriginId: s3Bucket.id,
forwardedValues: {
queryString: false,
cookies: {
forward: "none"
}
},
viewerProtocolPolicy: "redirect-to-https",
minTtl: 0,
defaultTtl: 0,
maxTtl: 0
},
restrictions: {
geoRestriction: {
restrictionType: "none"
}
},
viewerCertificate: {
acmCertificateArn: acmCertificate.arn,
sslSupportMethod: "sni-only"
}
});
You might already know, SPA doesn’t have an actual HTML file for all the paths.
So with the following configuration, the root index.html
is returned in that case.
customErrorResponse: [
{
errorCode: 403,
responseCode: 200,
responsePagePath: "/"
}
],
Associating blog.example.com
with the distribution
Alias record can use for some AWS endpoints like CloudFront distribution, S3 bucket, ELB, and the endpoint returns its IP address.
I'd like to associate blog.example.com
with the IP address of the distribution.
So the code is the following
new route53.Route53Record(this, "website", {
zoneId: dataAwsRoute53Zone.id,
name: "blog.example.com",
type: "A",
alias: [{
name: distribution.domainName,
zoneId: distribution.hostedZoneId,
evaluateTargetHealth: false
}]
});
Create resources by cdktf
The code is ready to be applied, so to confirm the change by above code, you can run the following command
cdktf plan
To apply them
cdktf apply
After a while, all the resources defined with the above code will be created. Then you can see your SPA. Of course you need to upload your HTML, JS, CSS, etc files to the S3 bucket.
Is CDK for Terraform beneficial?
Type checking
Type checking is a benefit.
TypeScript informs us when the type of a value doesn't have the expected type.
Needs to translate the Terraform syntax to CDK's
When you define a data source, you can write it by HCL like
data "aws_iam_policy_document" "example" {
But with CDK, it becomes
new iam.DataAwsIamPolicyDocument(this, "example", {
You need to translate the Terraform syntax to CDK's every time. it's a bother a bit.
Other
- With CDK, need to instantiate a class every time we define a resource or a data source. HCL is simpler and more concise.
- CDK for Terraform is still beta. It's not recommended to use it in production.
Conclusion
If you are already familiar with Terraform, you can't find reasone to use CDK for Terraform i think.
But if you are newbie of IaC and want to start with your familiar programing language, it might be comfortable.
Top comments (0)