|
1 | | -## My Project |
| 1 | +# Supporting Amazon RDS initialization using CDK |
2 | 2 |
|
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). |
4 | 5 |
|
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. |
6 | 7 |
|
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 | + |
| 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 |
9 | 189 |
|
10 | 190 | ## Security |
11 | 191 |
|
|
0 commit comments