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.
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:
- From the AWS Command Line Interface (AWS CLI), navigate to your working folder.
- Install or update TypeScript:
npm install -g typescript
- Create the project folder:
- Navigate to the project folder:
- 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:
- Create an empty
lib/
folder in your project’s root folder.
- 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 } from '@aws-cdk/core'
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'
import { createHash } from 'crypto'
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 payloadHashPrefix = createHash('md5').update(payload).digest('hex').substring(0, 6)
const sdkCall: AwsSdkCall = {
service: 'Lambda',
action: 'invoke',
parameters: {
FunctionName: fn.functionName,
Payload: payload
},
physicalResourceId: PhysicalResourceId.of(`${id}-AwsSdkCall-${fn.currentVersion.version + payloadHashPrefix}`)
}
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:
- Create an empty
demos/
folder in your project’s root folder.
- Create an empty
demos/rds-init-fn-code/
folder.
- 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" ]
- 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))
})
})
}
- 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"
}
}
- 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:
- 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)
})
}
}
- 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')
- Provision the example RDS stack in your default AWS account by running the following command and its subsequent steps:
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:
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.