Create Aurora MySQL on top of CDK with TS
Marcos Henrique
Posted on May 18, 2022
This post has the proposal to propagate some information about how to create your own Aurora MySQL with CDK on top of typescript.
What is Aurora?
Amazon Aurora MySQL is a fully managed, MySQL-compatible, relational database engine that combines the speed and reliability of high-end commercial databases with the simplicity and cost-effectiveness of open-source databases.
Aurora MySQL is a drop-in replacement for MySQL. It makes it simple and cost-effective to set up, operate, and scale your new and existing MySQL deployments, thus freeing you to focus on your business and applications. Amazon RDS provides administration for Aurora by handling routine database tasks such as provisioning, patching, backup, recovery, failure detection, and repair. Amazon RDS also provides push-button migration tools to convert your existing Amazon RDS for MySQL applications to Aurora MySQL, if you want to go deep in Aurora you can read about it in the docs.
What is CDK?
It's an IaC-like terraform or pulumi, AWS CDK is a framework for defining cloud infrastructure in code and provisioning it through AWS CloudFormation.
The AWS CDK lets you build reliable, scalable, cost-effective applications in the cloud with the considerable expressive power of a programming language you can see more about CDK here.
It's time to get your hand dirty!!!
In this brief tutorial, we are going to use CDK v2, so you need to create a project using Typescript, and I'll suppose you are already familiar with Typescript and skip the baby steps on configuration for TS projects.
Do we need to install the following packages:
- aws-cdk The Toolkit provides the cdk command-line interface that can be used to work with AWS CDK applications.
- constructs are classes that define a "piece of system state"
- aws-cdk-lib library provides APIs to define your CDK application and add CDK constructs to the application. ```bash
npm i aws-cdk aws-cdk-lib constructs
Now you will create a file called `app.ts` or anything that represents for you our infrastructure and in this file, you will create a class that extends from `constructs` and Represents the building block of the constructed graph.
### app.ts
```typescript
import { Environment } from 'aws-cdk-lib'
import { Construct } from 'constructs'
import { AuroraMySqlStorage } from '../stacks/aurora-mysql-storage.stack'
export interface DatabaseProps {
env: Environment
variables?: any
}
export class DatabaseAuroraApp extends Construct {
constructor(scope: Construct, id: string, props: DatabaseProps) {
super(scope, id)
new AuroraMySqlStorage(this, 'my-first-database', {
stackName: 'my-first-database-stack',
env: props.env,
variables: props.variables,
databaseName: 'my-first-database',
})
}
}
In Cloudformation a stack is a collection of AWS resources that you can manage as a single unit.
In other words, you can create, update, or delete a collection of resources by creating, updating, or deleting stacks. All the resources in a stack are defined by the stack's AWS CloudFormation template.
A stack, for instance, can include all the resources required to run a web application, such as a web server, a database, and networking rules. If you no longer require that web application, you can simply delete the stack, and all of its related resources are deleted.
So in the code above, we are just creating our Aurora MySQL Stack, let's create a folder called stacks
and a file called aurora-mysql-storage.stack.ts
inside this folder that represents our Aurora MySQL Cluster itself and your resources.
stacks/aurora-mysql-storage.stack.ts
import { StackProps, Environment, Stack } from 'aws-cdk-lib'
import { IVpc, Vpc } from 'aws-cdk-lib/aws-ec2'
import { Construct } from 'constructs'
import { DatabaseAuroraMySQLStorage } from '../lib/database-aurora-mysql-storage'
export interface IStorageStackProps extends StackProps {
variables?: any
env: Environment
databaseName: string
}
export class AuroraMySqlStorage extends Stack {
constructor(scope: Construct, id: string, props: IStorageStackProps) {
super(scope, id, props)
const vpc: IVpc = Vpc.fromLookup(this, 'vpc', {
tags: { vpcIdentity: 'my-vpc' },
})
new DatabaseAuroraMySQLStorage(this, 'my-first-database-storage', {
variables: props.variables,
env: props.env,
databaseName: props.databaseName,
vpc,
})
}
}
In a lib folder create a file that represents the aurora creation itself
lib/database-aurora-mysql-storage.ts
import { Environment, Duration, RemovalPolicy } from 'aws-cdk-lib'
import { InstanceType, IVpc, SecurityGroup, SubnetType } from 'aws-cdk-lib/aws-ec2'
import { RetentionDays } from 'aws-cdk-lib/aws-logs'
import {
DatabaseClusterEngine,
SubnetGroup,
DatabaseCluster,
ParameterGroup,
AuroraMysqlEngineVersion,
} from 'aws-cdk-lib/aws-rds'
import { Secret } from 'aws-cdk-lib/aws-secretsmanager'
import { Construct } from 'constructs'
export interface DatabaseAuroraAppProps {
env: Environment
variables?: any
databaseName: string
vpc: IVpc
}
export class DatabaseAuroraMySQLStorage extends Construct {
variables: any
instanceName: string
constructor(scope: Construct, id: string, props: DatabaseAuroraAppProps) {
super(scope, id)
this.variables = props.variables
this.instanceName = `${props.databaseName}-aurora-mysql`
const databaseUsername = 'admin'
const clusterName = `${this.instanceName}-cluster-aurora-mysql`
const securityGroup = new SecurityGroup(this, id.concat(`${this.instanceName}-sg`), {
vpc: props.vpc,
description: `${this.instanceName}-instance-sg`,
securityGroupName: `${this.instanceName}-instance-sg`,
allowAllOutbound: true,
})
const databaseCredentialsSecret = new Secret(this, 'DBCredentialsSecret', {
secretName: `${this.instanceName}-credentials`,
generateSecretString: {
secretStringTemplate: JSON.stringify({
username: databaseUsername,
}),
excludePunctuation: true,
includeSpace: false,
generateStringKey: 'password',
},
})
/** Version "8.0.mysql_aurora.3.01.0". */
const dbEngine = DatabaseClusterEngine.auroraMysql({ version: AuroraMysqlEngineVersion.VER_3_01_0 })
/**
let's suppose you need to create a trigger on your database,
this custom parameter group it's responsible to perform this with the following parameter log_bin_trust_function_creators,
because the default parameter group is not editable
*/
const parameterGroupForInstance = new ParameterGroup(
this,
`${this.instanceName}-${dbEngine.engineVersion?.fullVersion}`,
{
engine: dbEngine,
description: `Aurora RDS Instance Parameter Group for database ${this.instanceName}`,
parameters: {
log_bin_trust_function_creators: '1',
},
},
)
new DatabaseCluster(this, clusterName, {
engine: dbEngine,
instanceProps: {
instanceType: new InstanceType('t3.small'),
vpc: props.vpc,
vpcSubnets: {
subnetType: SubnetType.PRIVATE_ISOLATED,
},
securityGroups: [securityGroup],
parameterGroup: parameterGroupForInstance,
},
backup: {
retention: Duration.days(RetentionDays.ONE_WEEK),
preferredWindow: '03:00-04:00',
},
credentials: {
username: databaseUsername,
password: databaseCredentialsSecret.secretValueFromJson('password'),
},
instances: 1,
cloudwatchLogsRetention: RetentionDays.ONE_WEEK,
defaultDatabaseName: props.databaseName,
iamAuthentication: false,
clusterIdentifier: 'my-first-aurora-cluster',
subnetGroup: this.createSubnetGroup(props.vpc),
})
}
// you need to create a subnet group for your database
private createSubnetGroup(vpc: IVpc) {
return new SubnetGroup(this, 'aurora-rds-subnet-group', {
description: `Aurora RDS Subnet Group for database ${this.instanceName}`,
subnetGroupName: 'aurora-rds-subnet-group',
vpc,
removalPolicy: RemovalPolicy.DESTROY,
vpcSubnets: {
subnets: vpc.isolatedSubnets,
},
})
}
}
Now you need to create on your root file a entry point, I don't have so much creativity so I called this as startup.ts
startup.ts
#!/usr/bin/env node
import { App } from 'aws-cdk-lib'
/**
*This module provides source map support for stack traces in node via the V8 stack trace API.
It uses the source-map module to replace the paths and line numbers of source-mapped files
with their original paths and line numbers. The output mimics the node's stack trace format
with the goal of making every compile-to-JS language more of a first-class citizen.
Source maps are completely general (not specific to any one language)
so you can use source maps with multiple compile-to-JS languages in the same node process.
*/
import 'source-map-support/register'
import { DatabaseAuroraApp } from './lib/app.ts'
const app = new App()
new DatabaseAuroraApp(app, 'my-database', {
env: someEnvConfigGoesHere,
variables: someVariablesGoesHere,
})
/**
Synthesize this stage into a cloud assembly.
*/
app.synth()
Now you can add on your package.json this scripts:
"build:cdk": "npx cdk synth", //this can validate your cdk
"diff:cdk": "npx cdk diff", //this can see all diffs
"deploy:cdk": "npx cdk deploy --all --ci --no-previous-parameters", //this can deploy
And it's all folks, I hope you enjoy and get deep on this because CDK is a great tool, yes has a vendor lock, but AWS is awesome :D
Posted on May 18, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.