Skip to content

Commit 48a88fc

Browse files
rsmaso-awsRolando Santamaria Maso
andauthored
initial commit (#1)
* initial implementation Co-authored-by: Rolando Santamaria Maso <rsmaso@amazon.de>
1 parent 8bce62e commit 48a88fc

File tree

16 files changed

+567
-5
lines changed

16 files changed

+567
-5
lines changed

.gitignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
*.js
2+
!jest.config.js
3+
*.d.ts
4+
node_modules
5+
6+
!demos/*/*.js
7+
8+
.cdk.staging
9+
cdk.out
10+
11+
.npmrc
12+
13+
package-lock.json

.npmignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
*.ts
2+
!*.d.ts
3+
4+
# CDK asset staging directory
5+
.cdk.staging
6+
cdk.out

README.md

Lines changed: 185 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,191 @@
1-
## My Project
1+
# Supporting Amazon RDS initialization using CDK
22

3-
TODO: Fill this README out!
3+
## Introduction
4+
The source code and documentation in this repository describes how to support Amazon RDS instances initialization using CDK and CloudFormation Custom Resources. For the compute layer, it uses a Lambda function implemented in Node.js which is able to run custom SQL scripts with the purpose of initializing the Amazon RDS instance, but also to execute custom commands supported by the [Node.js client for MySQL](https://www.npmjs.com/package/mysql).
45

5-
Be sure to:
6+
> Through this documentation and examples we focus on Amazon RDS for MySQL, but the concept being described can be applied to any other supported RDS engine.
67
7-
* Change the title in this README
8-
* Edit your repository description on GitHub
8+
### Potential use cases
9+
- Initialize databases with their corresponding schema or table structures.
10+
- Initialize/maintain users and their permissions.
11+
- Initialize/maintain stored procedures, views or other database resources.
12+
- Execute other custom logic as part of a resource initialization process.
13+
14+
### Pre-Requisites:
15+
- Node.js v14+ installed on your local machine: https://nodejs.org/en/download/
16+
- Docker installed on your local machine: https://docs.docker.com/get-docker/
17+
- CDK v1.122+ installed and configured on your local machine: https://docs.aws.amazon.com/cdk/latest/guide/getting_started.html
18+
19+
## Technical implementation
20+
In order to achieve custom logic execution during the deployment flow of a CDK stack, we make use of CloudFormation Custom Resources. In the context of CDK, we use the `AwsCustomResource` construct to invoke a deployed lambda containing the RDS initialization logic (execute SQL scripts).
21+
22+
> Optionally you can read more about making custom AWS API calls using the AwsCustomResource construct: https://docs.aws.amazon.com/cdk/api/latest/docs/custom-resources-readme.html#custom-resources-for-aws-apis
23+
24+
### Client implementation based on Node.js
25+
To execute SQL scripts on the provisioned Amazon RDS instance we make use of the `mysql` NPM module, it allow us to easily execute custom SQL scripts or any other support client -> server command:
26+
```js
27+
const mysql = require('mysql')
28+
const connection = mysql.createConnection({
29+
host,
30+
user,
31+
password,
32+
multipleStatements: true
33+
})
34+
connection.connect()
35+
36+
connection.query("SELECT 'Hello World!';", (err, res) => {
37+
// ...
38+
})
39+
```
40+
> Full Node.js implementation example for MySQL is available at `./demos/rds-init-fn-code/index.js`
41+
42+
### Docker container images for Lambda functions
43+
To avoid unnecessary overhead dealing with software dependencies, we promote the usage of Docker container images to package the RDS initialization Lambda function code.
44+
45+
Docker container images are automatically managed by CDK and there is no need to interact with ECR repositories, simply use:
46+
```js
47+
const fnCode = DockerImageCode.fromImageAsset(`${__dirname}/your-fn-code-directory`, {})
48+
```
49+
> You can see a Lambda function code example inside the `./demos/rds-init-fn-code` directory.
50+
51+
### High level solution overview
52+
![Solution Overview](solution-overview.png "Solution Overview")
53+
54+
> NOTE: For simplicity, Amazon S3, Amazon ECR and Amazon CloudWatch configurations are ommitted from the diagram.
55+
56+
### The CdkResourceInitializer construct
57+
58+
TThe `CDKResourceInitializer` CDK construct generalizes the proposed solution, it encapsulates the integration requirements behind `CloudFormation Custom Resources` and `CDK`, to support the execution of AWS Lambda functions with custom initialization logic.
59+
60+
#### Usage (full example)
61+
```ts
62+
import * as cdk from '@aws-cdk/core'
63+
import { CfnOutput, Duration, Stack, Token } from '@aws-cdk/core'
64+
import { CdkResourceInitializer } from '../lib/resource-initializer'
65+
import { DockerImageCode } from '@aws-cdk/aws-lambda'
66+
import { InstanceClass, InstanceSize, InstanceType, Port, SubnetType, Vpc } from '@aws-cdk/aws-ec2'
67+
import { RetentionDays } from '@aws-cdk/aws-logs'
68+
import { Credentials, DatabaseInstance, DatabaseInstanceEngine, DatabaseSecret, MysqlEngineVersion } from '@aws-cdk/aws-rds'
69+
70+
export class RdsInitStackExample extends Stack {
71+
constructor (scope: cdk.App, id: string, props?: cdk.StackProps) {
72+
super(scope, id, props)
73+
74+
const instanceIdentifier = 'mysql-01'
75+
const credsSecretName = `/${id}/rds/creds/${instanceIdentifier}`.toLowerCase()
76+
const creds = new DatabaseSecret(this, 'MysqlRdsCredentials', {
77+
secretName: credsSecretName,
78+
username: 'admin'
79+
})
80+
81+
const vpc = new Vpc(this, 'MyVPC', {
82+
subnetConfiguration: [{
83+
cidrMask: 24,
84+
name: 'ingress',
85+
subnetType: SubnetType.PUBLIC,
86+
},{
87+
cidrMask: 24,
88+
name: 'compute',
89+
subnetType: SubnetType.PRIVATE_WITH_NAT,
90+
},{
91+
cidrMask: 28,
92+
name: 'rds',
93+
subnetType: SubnetType.PRIVATE_ISOLATED,
94+
}]
95+
})
96+
97+
const dbServer = new DatabaseInstance(this, 'MysqlRdsInstance', {
98+
vpcSubnets: {
99+
onePerAz: true,
100+
subnetType: SubnetType.PRIVATE_ISOLATED
101+
},
102+
credentials: Credentials.fromSecret(creds),
103+
vpc: vpc,
104+
port: 3306,
105+
databaseName: 'main',
106+
allocatedStorage: 20,
107+
instanceIdentifier,
108+
engine: DatabaseInstanceEngine.mysql({
109+
version: MysqlEngineVersion.VER_8_0
110+
}),
111+
instanceType: InstanceType.of(InstanceClass.T2, InstanceSize.LARGE)
112+
})
113+
// potentially allow connections to the RDS instance...
114+
// dbServer.connections.allowFrom ...
115+
116+
const initializer = new CdkResourceInitializer(this, 'MyRdsInit', {
117+
config: {
118+
credsSecretName
119+
},
120+
fnLogRetention: RetentionDays.FIVE_MONTHS,
121+
fnCode: DockerImageCode.fromImageAsset(`${__dirname}/rds-init-fn-code`, {}),
122+
fnTimeout: Duration.minutes(2),
123+
fnSecurityGroups: [],
124+
vpc,
125+
subnetsSelection: vpc.selectSubnets({
126+
subnetType: SubnetType.PRIVATE_WITH_NAT
127+
})
128+
})
129+
// manage resources dependency
130+
initializer.customResource.node.addDependency(dbServer)
131+
132+
// allow the initializer function to connect to the RDS instance
133+
dbServer.connections.allowFrom(initializer.function, Port.tcp(3306))
134+
135+
// allow initializer function to read RDS instance creds secret
136+
creds.grantRead(initializer.function)
137+
138+
new CfnOutput(this, 'RdsInitFnResponse', {
139+
value: Token.asString(initializer.response)
140+
})
141+
}
142+
}
143+
144+
```
145+
146+
#### Configuration options
147+
```ts
148+
export interface CdkResourceInitializerProps {
149+
vpc: ec2.IVpc
150+
subnetsSelection: ec2.SubnetSelection
151+
fnSecurityGroups: ec2.ISecurityGroup[]
152+
fnTimeout: Duration
153+
fnCode: lambda.DockerImageCode
154+
fnLogRetention: RetentionDays
155+
fnMemorySize?: number // defaults to 128
156+
config: any
157+
}
158+
```
159+
160+
#### Instance properties
161+
The `CdkResourceInitializer` class exposes the following readonly properties:
162+
```ts
163+
// response from initializer function once executed (JSON string)
164+
public readonly response: string
165+
// reference to the internal AwsCustomResource resource instance
166+
public readonly customResource: AwsCustomResource
167+
// reference to the internal Function resource instance
168+
public readonly function: lambda.Function
169+
```
170+
171+
### Considerations networking configuration
172+
173+
The `CdkResourceInitializer` construct interface requires networking parameters such as VPC and Subnets, the intention here is to allow the initializer function to communicate with RDS instances which are usually provisioned on Private or Isolated subnets within customer managed VPCs.
174+
175+
**IMPORTANT**: Because the initializer function also requires to fetch AWS Secrets (RDS credentials), we require to provision it inside a Subnet with internet access or at least with an existing [VPC endpoint for AWS Secrets Manager](https://docs.aws.amazon.com/secretsmanager/latest/userguide/vpc-endpoint-overview.html) attached.
176+
177+
### Initializer function execution lifecycle
178+
179+
The initializer function will be executed under one the following circumstances:
180+
- The `CdkResourceInitializer` construct is provisioned the first time.
181+
- The function configuration (networking, code, etc...) changes.
182+
- The `config` parameter changes.
183+
184+
## Useful CDK commands for this repository
185+
186+
- `cdk deploy` Deploy the example `rds-init-example` Stack to your default AWS account/region
187+
- `cdk diff` Compare deployed stack with current local state
188+
- `cdk synth` Generates a synthesized CloudFormation template
9189

10190
## Security
11191

bin/rds-init-example.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
#!/usr/bin/env node
2+
3+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4+
// SPDX-License-Identifier: MIT-0
5+
6+
import * as cdk from '@aws-cdk/core'
7+
import { RdsInitStackExample } from '../demos/rds-init-example'
8+
9+
const app = new cdk.App()
10+
11+
/* eslint no-new: 0 */
12+
new RdsInitStackExample(app, 'RdsInitExample')

cdk.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"app": "npx ts-node --prefer-ts-exts bin/rds-init-example.ts",
3+
"context": {
4+
"@aws-cdk/core:enableStackNameDuplicates": "true",
5+
"aws-cdk:enableDiffNoFail": "true",
6+
"@aws-cdk/core:stackRelativeExports": "true",
7+
"@aws-cdk/aws-ecr-assets:dockerIgnoreSupport": true,
8+
"@aws-cdk/aws-secretsmanager:parseOwnedSecretName": true,
9+
"@aws-cdk/aws-kms:defaultKeyPolicies": true,
10+
"@aws-cdk/aws-s3:grantWriteWithoutAcl": true,
11+
"@aws-cdk/aws-ecs-patterns:removeDefaultDesiredCount": true
12+
}
13+
}

demos/rds-init-example.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: MIT-0
3+
4+
import * as cdk from '@aws-cdk/core'
5+
import { CfnOutput, Duration, Stack, Token } from '@aws-cdk/core'
6+
import { CdkResourceInitializer } from '../lib/resource-initializer'
7+
import { DockerImageCode } from '@aws-cdk/aws-lambda'
8+
import { InstanceClass, InstanceSize, InstanceType, Port, SubnetType, Vpc } from '@aws-cdk/aws-ec2'
9+
import { RetentionDays } from '@aws-cdk/aws-logs'
10+
import { Credentials, DatabaseInstance, DatabaseInstanceEngine, DatabaseSecret, MysqlEngineVersion } from '@aws-cdk/aws-rds'
11+
12+
export class RdsInitStackExample extends Stack {
13+
constructor (scope: cdk.App, id: string, props?: cdk.StackProps) {
14+
super(scope, id, props)
15+
16+
const instanceIdentifier = 'mysql-01'
17+
const credsSecretName = `/${id}/rds/creds/${instanceIdentifier}`.toLowerCase()
18+
const creds = new DatabaseSecret(this, 'MysqlRdsCredentials', {
19+
secretName: credsSecretName,
20+
username: 'admin'
21+
})
22+
23+
const vpc = new Vpc(this, 'MyVPC', {
24+
subnetConfiguration: [{
25+
cidrMask: 24,
26+
name: 'ingress',
27+
subnetType: SubnetType.PUBLIC,
28+
},{
29+
cidrMask: 24,
30+
name: 'compute',
31+
subnetType: SubnetType.PRIVATE_WITH_NAT,
32+
},{
33+
cidrMask: 28,
34+
name: 'rds',
35+
subnetType: SubnetType.PRIVATE_ISOLATED,
36+
}]
37+
})
38+
39+
const dbServer = new DatabaseInstance(this, 'MysqlRdsInstance', {
40+
vpcSubnets: {
41+
onePerAz: true,
42+
subnetType: SubnetType.PRIVATE_ISOLATED
43+
},
44+
credentials: Credentials.fromSecret(creds),
45+
vpc: vpc,
46+
port: 3306,
47+
databaseName: 'main',
48+
allocatedStorage: 20,
49+
instanceIdentifier,
50+
engine: DatabaseInstanceEngine.mysql({
51+
version: MysqlEngineVersion.VER_8_0
52+
}),
53+
instanceType: InstanceType.of(InstanceClass.T2, InstanceSize.LARGE)
54+
})
55+
// potentially allow connections to the RDS instance...
56+
// dbServer.connections.allowFrom ...
57+
58+
const initializer = new CdkResourceInitializer(this, 'MyRdsInit', {
59+
config: {
60+
credsSecretName
61+
},
62+
fnLogRetention: RetentionDays.FIVE_MONTHS,
63+
fnCode: DockerImageCode.fromImageAsset(`${__dirname}/rds-init-fn-code`, {}),
64+
fnTimeout: Duration.minutes(2),
65+
fnSecurityGroups: [],
66+
vpc,
67+
subnetsSelection: vpc.selectSubnets({
68+
subnetType: SubnetType.PRIVATE_WITH_NAT
69+
})
70+
})
71+
// manage resources dependency
72+
initializer.customResource.node.addDependency(dbServer)
73+
74+
// allow the initializer function to connect to the RDS instance
75+
dbServer.connections.allowFrom(initializer.function, Port.tcp(3306))
76+
77+
// allow initializer function to read RDS instance creds secret
78+
creds.grantRead(initializer.function)
79+
80+
/* eslint no-new: 0 */
81+
new CfnOutput(this, 'RdsInitFnResponse', {
82+
value: Token.asString(initializer.response)
83+
})
84+
}
85+
}

demos/rds-init-fn-code/Dockerfile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
# SPDX-License-Identifier: MIT-0
3+
4+
FROM amazon/aws-lambda-nodejs:14
5+
WORKDIR ${LAMBDA_TASK_ROOT}
6+
7+
COPY package.json ./
8+
RUN npm install --only=production
9+
COPY index.js ./
10+
COPY script.sql ./
11+
12+
CMD [ "index.handler" ]

0 commit comments

Comments
 (0)