DEV Community

Marcos Henrique
Marcos Henrique

Posted on

Create Aurora MySQL on top of CDK with TS

This post has the proposal to propagate some information about how to create your own Aurora MySQL with CDK on top of typescript.

ralph-monkey-stink-land-of-the-lost

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!!!

diving on mud
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.
npm i aws-cdk aws-cdk-lib constructs
Enter fullscreen mode Exit fullscreen mode

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

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',
    })
  }
}

Enter fullscreen mode Exit fullscreen mode

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,
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
      },
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

A dude celebrating

Discussion (0)