Skip to content

Commit

Permalink
Support DB clusters
Browse files Browse the repository at this point in the history
  • Loading branch information
mnapoli committed Feb 9, 2021
1 parent 75a7dbc commit 4c52a1e
Show file tree
Hide file tree
Showing 11 changed files with 993 additions and 49 deletions.
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,37 @@ The database will be securely placed in the private VPC subnet. Lambda functions

By default, the instance will be a `db.t3.micro` MySQL instance with no replication. It is a development instance.

*Note: deploying a RDS database can take a long time (more than 5 minutes).*

Here are all the options available:

```yaml
db:
# By default the DB name will be the same as the app name.
name: mydatabasename
# mysql, mariadb, postgres, aurora, aurora-mysql, aurora-postgresql
engine: aurora-mysql
# See https://aws.amazon.com/rds/instance-types/
# The default is `db.t3.micro` (the cheapest and smallest).
class: db.t3.micro
# Storage size in GB. The default is the minimum: 20GB.
storageSize: 20
```
*Note: deploying a RDS database can take a long time (more than 5 minutes).*
If the engine is Aurora-based (`aurora`, `aurora-mysql` or `aurora-postgresql`), then a DB cluster with only 1 instance will be created (no replica will be created).

To create an Aurora serverless database:

```yaml
db:
name: mydatabasename
serverless:
min: 1 # optional, default is 1
max: 4 # required
autoPause: false # optional, default is false
```

By default, a MySQL 5.7 serverless database is created. Set `engine: aurora` for MySQL 5.6, or `engine: aurora-postgresql` for PostgreSQL.

## S3 bucket

Expand Down
11 changes: 8 additions & 3 deletions lift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@ s3:
# public: true
# cors: true

queues:
jobs:
#queues:
# jobs:
# maxRetries: 5

#db:
db:
engine: aurora-mysql
serverless:
min: 1
max: 1
autoPause: true

#static-website:
3 changes: 3 additions & 0 deletions src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export class Config {
}

static fromFile(file: string = 'lift.yml'): Config {
if (! fs.existsSync(file)) {
throw new Error('No `lift.yml` file found in the current directory.');
}
const yamlString = fs.readFileSync(file, 'utf8');
const config = yaml.safeLoad(yamlString) as Record<string, any>;
if (!config || typeof config !== 'object' || !config.hasOwnProperty('name')) {
Expand Down
113 changes: 90 additions & 23 deletions src/components/Database.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import {Component} from "./Component";
import {Stack} from '../Stack';
import {DBCluster, DBSubnetGroup, ScalingConfiguration} from 'aws-sdk/clients/rds';

type Engine = 'mysql' | 'mariadb' | 'postgres' | 'aurora' | 'aurora-mysql' | 'aurora-postgresql';

export class Database extends Component {
private readonly props: Record<string, any>;
Expand All @@ -17,27 +20,9 @@ export class Database extends Component {

compile(): Record<string, any> {
const availabilityZones = this.stack.availabilityZones();

const subnetGroupResourceId = this.formatCloudFormationId('DbSubnetGroup');

return {
[this.dbResourceName]: {
Type: 'AWS::RDS::DBInstance',
Properties: {
DBName: this.getDbName(),
Engine: this.getEngine(),
MasterUsername: 'admin',
MasterUserPassword: 'password',
DBInstanceIdentifier: this.getDbName(),
DBInstanceClass: 'db.t3.micro',
StorageType: 'gp2',
AllocatedStorage: '20', // minimum is 20 GB
DBSubnetGroupName: this.fnRef(subnetGroupResourceId),
VPCSecurityGroups: [
this.fnRef(this.formatCloudFormationId('DBSecurityGroup')),
],
},
},
const resources: Record<string, any> = {
[subnetGroupResourceId]: {
Type: 'AWS::RDS::DBSubnetGroup',
Properties: {
Expand All @@ -46,9 +31,75 @@ export class Database extends Component {
SubnetIds: availabilityZones.map(zone => {
return this.fnRef(this.formatCloudFormationId(`SubnetPrivate-${zone}`));
}),
}
} as DBSubnetGroup,
},
};

const instance = {
Type: 'AWS::RDS::DBInstance',
Properties: {
DBInstanceIdentifier: this.getDbName(),
DBName: this.getDbName(),
Engine: this.getEngine(),
MasterUsername: 'admin',
MasterUserPassword: 'password',
DBInstanceClass: this.props.class ? this.props.class : 'db.t3.micro',
StorageType: 'gp2',
AllocatedStorage: this.props.storageSize ? this.props.storageSize : '20', // minimum is 20 GB
DBSubnetGroupName: this.fnRef(subnetGroupResourceId),
VPCSecurityGroups: [
this.fnRef(this.formatCloudFormationId('DBSecurityGroup')),
],
},
};

let cluster = null;
if (this.isCluster()) {
Object.assign(instance.Properties, {
DBClusterIdentifier: this.fnRef(this.dbResourceName),
});

cluster = {
Type: 'AWS::RDS::DBCluster',
Properties: {
DBClusterIdentifier: this.getDbName(),
DatabaseName: this.getDbName(),
Engine: this.getEngine(),
// DBClusterParameterGroupName ?
MasterUsername: 'admin',
MasterUserPassword: 'password',
DBSubnetGroupName: this.fnRef(subnetGroupResourceId),
VpcSecurityGroupIds: [
this.fnRef(this.formatCloudFormationId('DBSecurityGroup')),
],
} as DBCluster,
}
resources[this.dbResourceName] = cluster;
resources[this.dbResourceName + 'Instance'] = instance;
} else {
resources[this.dbResourceName] = instance;
}

if (this.props.serverless) {
if (! cluster) throw new Error('RDS serverless can only be used with RDS engines of type `aurora`, `aurora-mysql` or `aurora-postgresql`')
if (! ('max' in this.props.serverless)) throw new Error('The `max` key is required in the `db.serverless` config.');

Object.assign(cluster.Properties, {
EngineMode: 'serverless',
EnableHttpEndpoint: true,
ScalingConfiguration: {
MinCapacity: ('min' in this.props.serverless) ? this.props.serverless.min : '1',
MaxCapacity: this.props.serverless.max,
AutoPause: ('autoPause' in this.props.serverless) ? this.props.serverless.autoPause : false,
SecondsUntilAutoPause: 60 * 10, // 10 minutes
} as ScalingConfiguration,
});

// Remove the DB instance from the template
delete resources[this.dbResourceName + 'Instance'];
}

return resources;
}

outputs() {
Expand Down Expand Up @@ -99,13 +150,13 @@ export class Database extends Component {
};
}

private getEngine(): string {
private getEngine(): Engine {
const availableEngines = [
'mysql',
'mariadb',
'postgres',
'aurora',
'aurora-mysql',
'aurora', // MySQL 5.6
'aurora-mysql', // MySQL 5.7
'aurora-postgresql',
];
if (this.props.engine) {
Expand All @@ -114,9 +165,25 @@ export class Database extends Component {
}
return this.props.engine;
}
if (this.props.serverless) {
return 'aurora-mysql';
}
return 'mysql';
}

private isCluster(): boolean {
const engine = this.getEngine();
const isCluster = {
'mysql': false,
'mariadb': false,
'postgres': false,
'aurora': true,
'aurora-mysql': true,
'aurora-postgresql': true,
};
return isCluster[engine];
}

private getDbName(): string {
const name = this.props.name ? this.props.name : this.stackName;
if (! name.match(/^[\w\d]*$/g)) {
Expand Down
22 changes: 16 additions & 6 deletions test/functional/db.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,21 @@ afterEach(() => {
sinon.restore();
});

describe('lift deploy', () => {
describe('db', () => {

it('should deploy database', async function() {
const output = await runCommand(__dirname + '/db', 'export');
assertCloudFormation(output, 'db/expected.yaml');
it('should deploy database instances', async function() {
const output = await runCommand(__dirname + '/db/instance', 'export');
assertCloudFormation(output, 'db/instance/expected.yaml');
});

it('should deploy database clusters', async function() {
const output = await runCommand(__dirname + '/db/cluster', 'export');
assertCloudFormation(output, 'db/cluster/expected.yaml');
});

it('should deploy serverless databases', async function() {
const output = await runCommand(__dirname + '/db/serverless', 'export');
assertCloudFormation(output, 'db/serverless/expected.yaml');
});

it('should export database variables', async function() {
Expand All @@ -31,7 +41,7 @@ describe('lift deploy', () => {
DatabasePort: '3306',
});

const output = await runCommand(__dirname + '/db', 'variables');
const output = await runCommand(__dirname + '/db/instance', 'variables');
assert.deepStrictEqual(JSON.parse(output), {
DATABASE_HOST: 'dbname.e2sctvp0nqos.us-east-1.rds.amazonaws.com',
DATABASE_NAME: 'dbname',
Expand All @@ -40,7 +50,7 @@ describe('lift deploy', () => {
});

it('should export database permissions', async function() {
const output = await runCommand(__dirname + '/db', 'permissions');
const output = await runCommand(__dirname + '/db/instance', 'permissions');
assert.deepStrictEqual(JSON.parse(output), []);
});

Expand Down
Loading

0 comments on commit 4c52a1e

Please sign in to comment.