@myhelix-cdk/core v0.36.4
core
These are constructs which every Helix project should be using.
Stack
Helix super-classes the Stack
and StackProps
classes.
The @myhelix-ckd/core.Stack
class provides a number of getter methods.
For example, vpc
and rdsSharedSecurityGroup
both return appropriate objects.
It also ensures that everything we deploy is tagged with accounting metadata tags.
Because the accounting tags come from an enum, it ensures compliance with our accounting requirements.
Finally, it enforces the use of NamedEnv
s.
Environment and NamedEnvs
CDK has the concept of an env
,
which is the combination of an account and a region.
CDK deploys stacks into envs.
myStack(app, 'myStackName', {
env: {
account: '123412341234',
region: 'us-east-1',
},
});
Our devs don't want to keep track of all the different account ids and regions.
Having what amounts to a constant spread of magic numbers
across dozens of CDK applications in dozens of repos
sounded like a bad idea.
Instead, we centralized this information.
At Helix, we use a NamedEnv
, which includes the required
account
and region
.
We realized that this would be a great place to put other environment specific data, too.
export interface NamedEnv extends cdk.Environment {
/**
* The kebab-name of the environment.
*/
readonly name: string;
/**
* What kind of an account is this?
*/
readonly organizationalUnit: OrganizationalUnit;
/**
* Is it a production or non-production environment?
* @deprecated use organizationalUnit instead
*/
readonly isProduction: boolean;
/**
* The securityGroupId for the shared RDS SG.
*/
readonly rdsSharedSecurityGroupId: string;
/**
* The vpc cidr for the environmet.
*/
readonly cidr: string;
/**
* The DNS zone into which services should be deployed.
*/
readonly zoneName: string;
}
There's a lot of information there. Much of it can be derived from, of all things, the name. Strong naming conventions are a wonderful thing. So, we built a factory function.
export interface NamedEnvironmentProps {
/**
* The numeric account id as used by cdk.Environment.account
*/
readonly account: string;
/**
* The region as used by cdk.Environment.region
*
* @default 'us-east-1'
*/
readonly region?: string;
/**
* The proper name of the environment in kebab-format.
*/
readonly name: string;
/**
* What kind of an account is this?
* @default 'pulled from the name'
*/
readonly organizationalUnit?: OrganizationalUnit;
/**
* The SgId of the RDS security group.
*/
readonly rdsSharedSecurityGroupId: string;
/**
* The cidr in '0.0.0.0/32' format.
*/
readonly cidr: string;
}
function newNamedEnv(props: NamedEnvironmentProps): NamedEnv {
/* eslint-disable prettier/prettier */
const organizationalUnit = props.organizationalUnit
? props.organizationalUnit
: props.name.includes('production')
? OrganizationalUnit.PRODUCTION
: props.name.includes('staging')
? OrganizationalUnit.STAGING
: props.name.includes('development')
? OrganizationalUnit.DEVELOPMENT
: props.name.includes('test')
? OrganizationalUnit.DEVELOPMENT
: null;
/* eslint-enable prettier/prettier */
if (organizationalUnit == null) {
throw new Error('Can not figure out OrganizationalUnit, please provide one');
}
return {
account: props.account,
name: props.name,
rdsSharedSecurityGroupId: props.rdsSharedSecurityGroupId,
cidr: props.cidr,
region: props.region ? props.region : 'us-east-1',
/* We derive a LOT of things based on the name */
isProduction: props.name.includes('production'),
organizationalUnit: organizationalUnit as OrganizationalUnit,
/* eslint-disable prettier/prettier */
zoneName: props.name.includes('production') ? 'helix.com'
: props.name.includes('staging') ? 'staging.helix.com'
: props.name.includes('development') ? 'development.helix.com'
: props.name + '.helix.com',
/* eslint-enable prettier/prettier */
};
}
Now it is simply a matter of stamping out environments
as statics in the Environment
class.
We are using statics rather than declaring consts directly
because we export this module using JSII to both typescript and python.
export class Environment {
public static readonly Ci: NamedEnv = newNamedEnv({
name: 'ci',
account: '123412341234',
rdsSharedSecurityGroupId: '',
organizationalUnit: OrganizationalUnit.DEVELOPMENT,
cidr: '0.0.0.0/32',
});
public static readonly HipaaStaging: NamedEnv = newNamedEnv({
name: 'hipaa-staging',
account: '123412341234',
rdsSharedSecurityGroupId: 'sg-12341234123412341',
cidr: '0.0.0.0/32', .
});
public static readonly HipaaProduction: NamedEnv = newNamedEnv({
name: 'hipaa-production',
account: '123412341234',
rdsSharedSecurityGroupId: 'sg-12341234123412341',
cidr: '0.0.0.0/32',
});
public static readonly PlatformDevelopment: NamedEnv = newNamedEnv({
name: 'platform-development',
account: '123412341234',
rdsSharedSecurityGroupId: 'sg-12341234123412341',
cidr: '0.0.0.0/32',
});
// ... and many more
}
Deploying a stack using NamedEnv
s makes it clear where the stack is going:
import cdk = require('@aws-cdk/core');
import core = require('@myhelix-cdk/core');
const app = cdk.App();
myStack(app, 'stackName', {
namedEnv: core.Environment.HipaaStaging,
});
Stacks can then leverage the additional metadata. For example, our Aurora stack cares a lot about prod vs non-prod.
const keyRemovalPolicy =
props.namedEnv.organizationalUnit == core.OrganizationalUnit.PRODUCTION
? cdk.RemovalPolicy.RETAIN
: cdk.RemovalPolicy.DESTROY
const instances = props.instances
? props.instances
: props.namedEnv.organizationalUnit == core.OrganizationalUnit.PRODUCTION
? 2
: 1;
We don't just publish Construct
s and Stack
s which leverage NamedEnv
,
we also have entire App
s which will deploy sets of stacks to sets of environments.
export enum DeployTarget {
RED = 'red', // AKA hipaa
ORANGE = 'orange',
}
export interface DeployEnv {
readonly env: core.NamedEnv;
// ... other properties
}
export interface DeployEnvs {
readonly development: DeployEnv;
readonly staging: DeployEnv;
readonly production: DeployEnv;
}
export interface ExampleAppProps {
/**
* What sub-accounts should it be deployed into?
*/
readonly deployTarget: DeployTarget;
// ... other properties
}
export class ExampleApp extends cdk.App {
deployEnvs: DeployEnvs;
constructor(props: ExampleAppProps) {
this.deployEnvs =
props.deployTarget == DeployTarget.RED
? {
development: {
env: core.Environment.PlatformDevelopment,
...
},
staging: {
env: core.Environment.HipaaStaging,
...
},
production: {
env: core.Environment.HipaaProduction,
...
},
} : {
development: {
env: core.Environment.PlatformDevelopment,
...
},
staging: {
env: core.Environment.PlatformStaging,
...
},
production: {
env: core.Environment.PlatformProduction,
...
},
};
// Stacks that are deployed in each deploy env:
Object.values(this.deployEnvs).forEach((deployEnv: DeployEnv) => {
const auroraStack =
deployEnv.env.organizationalUnit == core.OrganizationalUnit.DEVELOPMENT
? new SharedTennancyAuroraWithProxy(this, '...', {
namedEnv: deployEnv.env,
// ...
})
: new DedicatedAuroraWithProxy(this, '...', {
namedEnv: deployEnv.env,
// ...
});
new SomeLambda(this, '...', {
namedEnv: deployEnv.env,
dbCluster: auroraStack.cluster,
// ...
};)
};
// stacks that are deployed only once
new SomePipeline(this, 'foo-pipeline', {
namedEnv: core.Environment.CI,
// ...
});
}
}
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago