Infrastructure & Automation

Use AWS CDK to initialize Amazon RDS instances

In this blog post, we provide you with infrastructure as code (IaC) resources using the Amazon Web Services Cloud Development Kit (AWS CDK) framework. We describe how to initialize Amazon Relational Database Service (Amazon RDS) instances using AWS Lambda functions and AWS CloudFormation custom resources. Although we focus on MySQL, the concepts in this post can be applied to other Amazon RDS–supported environments.

After provisioning Amazon RDS instances, it’s common for infrastructure engineers to require initialization or management processes, usually through SQL scripts. The goal is to bootstrap or maintain the database server with a structure that matches the requirements of dependent applications or services.

Within the initialization process of an Amazon RDS instance, you can optionally address the following aspects:

  • Initialize databases with corresponding schema or table structures.
  • Initialize and maintain users and permissions.
  • Initialize and maintain stored procedures, views, or other database resources.
  • Run custom code.

When you provision your infrastructure on the AWS Cloud, custom initialization strategies require you to run code on a compute layer. To provision your infrastructure, we recommend using AWS Lambda or Amazon Elastic Container Service (Amazon ECS) combined with AWS Fargate because of the serverless lifecycle.

About this blog post
Time to read ~10 min.
Time to complete ~15 min.
Cost to complete $0
Learning level Advanced (300)
AWS services AWS CDK
AWS CloudFormation
Amazon RDS
AWS Lambda

The following architecture diagram shows a generic Amazon RDS–instances initialization process that is based in AWS Lambda, which is managed by AWS CDK and AWS CloudFormation. The architecture uses the AWS CloudFormation custom resources framework to run custom code during the provisioning process.

Figure 1. Amazon RDS architecture diagram

This deployment’s architecture sets up the following services and resources, as shown in figure 1:

  • AWS CloudFormation, which invokes the creation of custom resources through a Lambda function (custom resource proxy).
  • A highly available architecture that spans two Availability Zones.
  • A virtual private cloud (VPC) configured with private subnets.
  • In the private subnets:
    • A Lambda function (initialization logic).
    • An Amazon RDS instance where the initialization logic runs.
  • AWS Secrets Manager for storing credentials.

Prerequisites

To complete this walkthrough, you must have the following:

  • AWS CDK version 1.122 or later installed and configured on your local machine. The approach we document here is not yet compatible with CDK v2.x.
  • Node.js version 14 or later installed on your local machine.
  • Docker installed on your local machine.
  • AWS CDK version 1.122 or later installed and configured on your local machine.
  • A basic understanding of AWS CloudFormation.
  • A basic understanding of AWS CDK constructs and stacks.
  • Software development experience with TypeScript and JavaScript.

Walkthrough

The following sections describe how to initialize an Amazon RDS for MySQL instance. If you want to download and evaluate the code, see the associated GitHub repository.

Use TypeScript to create an empty CDK project

TypeScript is a fully supported client language for AWS CDK and is considered stable. Let’s proceed with creating an empty CDK project where we can develop our solution.

To create a new CDK project using TypeScript, follow these steps:

  1. From the AWS Command Line Interface (AWS CLI), navigate to your working folder.
  2. Install or update TypeScript:
    npm install -g typescript
  3. Create the project folder:
    mkdir rds-init-example
  4. Navigate to the project folder:
    cd rds-init-example
  5. Initialize the AWS CDK project for TypeScript:
    cdk init app --language typescript

Install software dependencies for your AWS CDK project

You must install aws-cdk–related dependencies that provide the base constructs. To install all of the required source-code dependencies, run the following command:

npm install @aws-cdk/aws-ec2 @aws-cdk/aws-iam @aws-cdk/aws-lambda @aws-cdk/aws-logs @aws-cdk/aws-rds @aws-cdk/aws-s3 @aws-cdk/aws-secretsmanager @aws-cdk/aws-ssm @aws-cdk/core @aws-cdk/custom-resources

Note: Depending on your configuration, you may need to restart your IDE.

Create the CdkResourceInitializer construct

CDKResourceInitializer is the AWS CDK construct that implements the initialization of AWS resources, such as Amazon RDS instances. To create the CDK construct, follow these steps:

  1. Create an empty lib/ folder in your project’s root folder.
  2. Create the resource-initializer.ts file inside the /lib folder. Copy and then paste the following content inside the file:
import * as ec2 from '@aws-cdk/aws-ec2'
import * as lambda from '@aws-cdk/aws-lambda'
import { Construct, Duration, Stack, Tags } from '@aws-cdk/core'
import { createHash } from 'crypto'
import { calculateFunctionHash } from '@aws-cdk/aws-lambda/lib/function-hash'
import { AwsCustomResource, AwsCustomResourcePolicy, AwsSdkCall, PhysicalResourceId } from '@aws-cdk/custom-resources'
import { RetentionDays } from '@aws-cdk/aws-logs'
import { PolicyStatement, Role, ServicePrincipal } from '@aws-cdk/aws-iam'

export interface CdkResourceInitializerProps {
  vpc: ec2.IVpc
  subnetsSelection: ec2.SubnetSelection
  fnSecurityGroups: ec2.ISecurityGroup[]
  fnTimeout: Duration
  fnCode: lambda.DockerImageCode
  fnLogRetention: RetentionDays
  fnMemorySize?: number
  config: any
}

export class CdkResourceInitializer extends Construct {
  public readonly response: string
  public readonly customResource: AwsCustomResource
  public readonly function: lambda.Function

  constructor (scope: Construct, id: string, props: CdkResourceInitializerProps) {
    super(scope, id)

    const stack = Stack.of(this)

    const fnSg = new ec2.SecurityGroup(this, 'ResourceInitializerFnSg', {
      securityGroupName: `${id}ResourceInitializerFnSg`,
      vpc: props.vpc,
      allowAllOutbound: true
    })

    const fn = new lambda.DockerImageFunction(this, 'ResourceInitializerFn', {
      memorySize: props.fnMemorySize || 128,
      functionName: `${id}-ResInit${stack.stackName}`,
      code: props.fnCode,
      vpcSubnets: props.vpc.selectSubnets(props.subnetsSelection),
      vpc: props.vpc,
      securityGroups: [fnSg, ...props.fnSecurityGroups],
      timeout: props.fnTimeout,
      logRetention: props.fnLogRetention,
      allowAllOutbound: true
    })

    const payload: string = JSON.stringify({
      params: {
        config: props.config
      }
    })

    const physicalResIdHash = createHash('md5')
      .update(calculateFunctionHash(fn) + payload)
      .digest('hex')

    const sdkCall: AwsSdkCall = {
      service: 'Lambda',
      action: 'invoke',
      parameters: {
        FunctionName: fn.functionName,
        Payload: payload
      },
      physicalResourceId: PhysicalResourceId.of(`${id}-AwsSdkCall-${physicalResIdHash}`)
    }
  
    const customResourceFnRole = new Role(this, 'AwsCustomResourceRole', {
      assumedBy: new ServicePrincipal('lambda.amazonaws.com')
    })
    customResourceFnRole.addToPolicy(
      new PolicyStatement({
        resources: [`arn:aws:lambda:${stack.region}:${stack.account}:function:*-ResInit${stack.stackName}`],
        actions: ['lambda:InvokeFunction']
      })
    )
    this.customResource = new AwsCustomResource(this, 'AwsCustomResource', {
      policy: AwsCustomResourcePolicy.fromSdkCalls({ resources: AwsCustomResourcePolicy.ANY_RESOURCE }),
      onUpdate: sdkCall,
      timeout: Duration.minutes(10),
      role: customResourceFnRole
    })

    this.response = this.customResource.getResponseField('Payload')

    this.function = fn
  }
}

Create the RDS initialization function code (Docker image)

To avoid unnecessary software dependencies, we recommend using Docker container images to package the Amazon RDS initialization function code. In this context, initialization function code is the RDS initialization process itself. For simplicity, we run a basic SQL script. The function implementation is based in Node.js and JavaScript.

To create the AWS Lambda function code:

  1. Create an empty demos/ folder in your project’s root folder.
  2. Create an empty demos/rds-init-fn-code/ folder.
  3. Create the Docker file inside the rds-init-fn-code folder. Copy and then paste the following content inside the file:
FROM amazon/aws-lambda-nodejs:14
WORKDIR ${LAMBDA_TASK_ROOT}

COPY package.json ./
RUN npm install --only=production
COPY index.js ./
COPY script.sql ./

CMD [ "index.handler" ]
  1. Create the index.js file inside the rds-init-fn-code folder, and paste the following content inside the file:
const mysql = require('mysql')
const AWS = require('aws-sdk')
const fs = require('fs')
const path = require('path')

const secrets = new AWS.SecretsManager({})

exports.handler = async (e) => {
  try {
    const { config } = e.params
    const { password, username, host } = await getSecretValue(config.credsSecretName)
    const connection = mysql.createConnection({
      host,
      user: username,
      password,
      multipleStatements: true
    })

    connection.connect()

    const sqlScript = fs.readFileSync(path.join(__dirname, 'script.sql')).toString()
    const res = await query(connection, sqlScript)

    return {
      status: 'OK',
      results: res
    }
  } catch (err) {
    return {
      status: 'ERROR',
      err,
      message: err.message
    }
  }
}

function query (connection, sql) {
  return new Promise((resolve, reject) => {
    connection.query(sql, (error, res) => {
      if (error) return reject(error)

      return resolve(res)
    })
  })
}

function getSecretValue (secretId) {
  return new Promise((resolve, reject) => {
    secrets.getSecretValue({ SecretId: secretId }, (err, data) => {
      if (err) return reject(err)

      return resolve(JSON.parse(data.SecretString))
    })
  })
}
  1. Create the package.json file inside the rds-init-fn-code folder, and paste the following content inside the file:
{
    "name": "rds-init-script",
    "version": "1.0.0",
    "description": "RDS initialization implementation in Node.js",
    "main": "index.js",
    "scripts": {
    },
    "keywords": [],
    "author": "",
    "license": "MIT",
    "dependencies": {
      "mysql": "^2.18.1"
    }
  }
  1. Create the script.sql file inside the rds-init-fn-code folder, and paste the following content inside:
-- Your SQL scripts for initialization goes here...

SELECT 'Hello World!' as message;

Example Amazon RDS stack with initialization support

Now create an AWS CDK stack to deploy the entire solution composed of a custom VPC, Amazon RDS instance, and a function-based initialization script. For simplicity, we use a hard-coded configuration within the RdsInitStackExample CDK stack.

To create and provision an example RDS Stack with initialization support:

  1. Create the rds-init-example.ts file inside the demos/ folder, and paste the following content inside:
import * as cdk from '@aws-cdk/core'
import { CfnOutput, Duration, Stack, Token } from '@aws-cdk/core'
import { CdkResourceInitializer } from '../lib/resource-initializer'
import { DockerImageCode } from '@aws-cdk/aws-lambda'
import { InstanceClass, InstanceSize, InstanceType, Port, SubnetType, Vpc } from '@aws-cdk/aws-ec2'
import { RetentionDays } from '@aws-cdk/aws-logs'
import { Credentials, DatabaseInstance, DatabaseInstanceEngine, DatabaseSecret, MysqlEngineVersion } from '@aws-cdk/aws-rds'

export class RdsInitStackExample extends Stack {
  constructor (scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props)

    const instanceIdentifier = 'mysql-01'
    const credsSecretName = `/${id}/rds/creds/${instanceIdentifier}`.toLowerCase()
    const creds = new DatabaseSecret(this, 'MysqlRdsCredentials', {
      secretName: credsSecretName,
      username: 'admin'
    })

    const vpc = new Vpc(this, 'MyVPC', {
      subnetConfiguration: [{
        cidrMask: 24,
        name: 'ingress',
        subnetType: SubnetType.PUBLIC,
      },{
        cidrMask: 24,
        name: 'compute',
        subnetType: SubnetType.PRIVATE_WITH_NAT,
      },{
        cidrMask: 28,
        name: 'rds',
        subnetType: SubnetType.PRIVATE_ISOLATED,
      }]
    })

    const dbServer = new DatabaseInstance(this, 'MysqlRdsInstance', {
      vpcSubnets: {
        onePerAz: true,
        subnetType: SubnetType.PRIVATE_ISOLATED
      },
      credentials: Credentials.fromSecret(creds),
      vpc: vpc,
      port: 3306,
      databaseName: 'main',
      allocatedStorage: 20,
      instanceIdentifier,
      engine: DatabaseInstanceEngine.mysql({
        version: MysqlEngineVersion.VER_8_0
      }),
      instanceType: InstanceType.of(InstanceClass.T2, InstanceSize.LARGE)
    })
    // potentially allow connections to the RDS instance...
    // dbServer.connections.allowFrom ...

    const initializer = new CdkResourceInitializer(this, 'MyRdsInit', {
      config: {
        credsSecretName
      },
      fnLogRetention: RetentionDays.FIVE_MONTHS,
      fnCode: DockerImageCode.fromImageAsset(`${__dirname}/rds-init-fn-code`, {}),
      fnTimeout: Duration.minutes(2),
      fnSecurityGroups: [],
      vpc,
      subnetsSelection: vpc.selectSubnets({
        subnetType: SubnetType.PRIVATE_WITH_NAT
      })
    })
    // manage resources dependency
    initializer.customResource.node.addDependency(dbServer)

    // allow the initializer function to connect to the RDS instance
    dbServer.connections.allowFrom(initializer.function, Port.tcp(3306))

    // allow initializer function to read RDS instance creds secret
    creds.grantRead(initializer.function)

    /* eslint no-new: 0 */
    new CfnOutput(this, 'RdsInitFnResponse', {
      value: Token.asString(initializer.response)
    })
  }
}
  1. Update the target bin.ts file defined in cdk.json with the following content:
#!/usr/bin/env node

import * as cdk from '@aws-cdk/core'
import { RdsInitStackExample } from '../demos/rds-init-example'

const app = new cdk.App()

/* eslint no-new: 0 */
new RdsInitStackExample(app, 'RdsInitExample')
  1. Provision the example RDS stack in your default AWS account by running the following command and its subsequent steps:
cdk deploy

Figure 2 shows the expected output. Note that the first time you run this, it may take a few minutes.

Figure 2. RDS stack output

Cleanup

To avoid incurring future charges, delete the provisioned RdsInitStackExample CDK Stack and related resources by running the following command:

cdk destroy

Conclusion

In this blog post, we guided you through an Amazon RDS initialization approach using AWS CDK and AWS CloudFormation custom resources. We also presented the CdkResourceInitializer construct implementation in TypeScript to support AWS resource initialization, such as RDS instances. In the same way, we presented a complete CDK stack and configuration, which contains all the necessary technical steps to provision the described solution.

For simplicity, we limited this demonstration to an RDS initialization using a basic script.sql file. While this approach may be sufficient for most use cases, you can extend this behavior to support more complex initialization processes that satisfy your requirements. You can use the CdkResourceInitializer construct to initialize or integrate logic for other resources and processes, such as the following:

  • Populating initial Amazon Simple Storage Service (Amazon S3) bucket structure and files.
  • Managing users and permissions for a new ActiveMQ broker.
  • Restoring backups on self-managed database instances.

Use the comments section to let us know if you have any questions.

About the authors

Rolando Santamaria Maso

Rolando is a senior cloud application development consultant at AWS Professional Services, based in Germany. He helps customers migrate and modernize workloads in the AWS Cloud, with a special focus on modern application architectures and development best practices, but he also creates IaC using AWS CDK. Outside work, he maintains open-source projects and enjoys spending time with family and friends.

Ramy Nasreldin

Ramy Nasreldin is a DevOps Architect at AWS, based in Sweden. Ramy helps customers design and implement their systems to run on the AWS Cloud. He also preaches best practices by automating everything, from infrastructures to application delivery, to achieve the most resilient and scalable solutions that best serve end users in a sustainable way. In his spare time, he enjoys swimming, playing football, and spending time with his family.

Prasanna Tuladhar Gitelman

Prasanna is a cloud infrastructure architect at AWS Professional Services, based in Germany. He likes to explore new challenges, be it databases, containers, or cloud infrastructures. Outside work, he likes jogging, hiking, and spending time with his family.