caccl-deploy
Version:
A cli tool for managing ECS/Fargate app deployments
204 lines (178 loc) • 6.28 kB
text/typescript
import { aws_ec2 as ec2, aws_ecs as ecs, Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { CacclAppEnvironment } from './appEnvironment';
import { CacclSshBastion } from './bastion';
import { CacclCache, CacclCacheOptions } from './cache';
import { CacclMonitoring } from './dashboard';
import { CacclDbOptions, createDbConstruct } from './db';
import {
CacclLoadBalancer,
LoadBalancerSecurityGoups,
CacclLoadBalancerExtraOptions,
} from './lb';
import { CacclNotifications, CacclNotificationsProps } from './notify';
import { CacclScheduledTask, CacclScheduledTasks } from './scheduledTasks';
import { CacclService } from './service';
import { CacclTaskDef, CacclTaskDefProps } from './taskdef';
export interface CacclDeployStackProps extends StackProps {
vpcId?: string;
certificateArn: string;
ecsClusterName?: string;
appEnvironment: { [key: string]: string };
taskDefProps: CacclTaskDefProps;
taskCount: number;
notifications: CacclNotificationsProps;
albLogBucketName?: string;
cacheOptions?: CacclCacheOptions;
dbOptions?: CacclDbOptions;
scheduledTasks?: { [key: string]: CacclScheduledTask };
lbOptions?: CacclLoadBalancerExtraOptions;
firewallSgId?: string;
enableExecuteCommand?: boolean;
}
export class CacclDeployStack extends Stack {
constructor(scope: Construct, id: string, props: CacclDeployStackProps) {
super(scope, id, props as StackProps);
let vpc;
let cluster;
// should we create an ssh bastion for access to db/cache/etc
let createBastion = false;
if (props.vpcId !== undefined) {
vpc = ec2.Vpc.fromLookup(this, 'Vpc', {
vpcId: props.vpcId,
}) as ec2.Vpc;
} else {
throw new Error('deployConfig must define a vpcId');
}
const appEnv = new CacclAppEnvironment(this, 'AppEnvironment', {
envVars: props.appEnvironment,
});
appEnv.addEnvironmentVar('CIDR_NET', vpc.vpcCidrBlock);
/**
* create the docdb if needed so we can add it's endpoint url
* to the app container's env
*/
let db = null;
if (props.dbOptions) {
createBastion = true;
db = createDbConstruct(this, {
vpc,
options: props.dbOptions,
appEnv,
});
}
if (props.cacheOptions) {
createBastion = true;
new CacclCache(this, 'Cache', {
vpc,
options: props.cacheOptions,
appEnv,
});
}
if (props.ecsClusterName !== undefined) {
cluster = ecs.Cluster.fromClusterAttributes(this, 'Cluster', {
vpc,
clusterName: props.ecsClusterName,
securityGroups: [],
}) as ecs.Cluster;
} else {
cluster = new ecs.Cluster(this, 'Cluster', {
clusterName: props.stackName,
containerInsights: true,
vpc,
});
}
const taskDef = new CacclTaskDef(this, 'TaskDef', {
vpcCidrBlock: vpc.vpcCidrBlock,
appEnvironment: appEnv,
...props.taskDefProps,
});
const lbSecurityGroups: LoadBalancerSecurityGoups = {};
if (props.firewallSgId) {
// primary firewall will be the imported, shared security group
lbSecurityGroups.primary = ec2.SecurityGroup.fromSecurityGroupId(
this,
'SecurityGroupImport',
props.firewallSgId,
{
allowAllOutbound: true,
},
) as ec2.SecurityGroup;
// we also need a separate, app-specific security group for miscellaneous
lbSecurityGroups.misc = new ec2.SecurityGroup(this, 'MiscSecurityGroup', {
vpc,
description:
'security group for miscellaneous app-specific ingress rules',
});
} else {
// primary will be a new, "open" security group
const newSg = new ec2.SecurityGroup(this, 'FirewallSecurityGroup', {
vpc,
description: 'security group for the load balancer and app service',
});
// by default apps are public
newSg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80));
newSg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(443));
lbSecurityGroups.primary = newSg;
}
const service = new CacclService(this, 'EcsService', {
cluster,
taskDef,
/**
* The security group passed into the CacclService is used in a traffic source ingress rule.
* i.e., the resulting ECS servcie will get its own security group with a single ingress rule that allows
* traffic from any resource associated with the security group passed in here.
*/
loadBalancerSg: lbSecurityGroups.primary,
taskCount: props.taskCount,
enableExecuteCommand: props.enableExecuteCommand,
});
const loadBalancer = new CacclLoadBalancer(this, 'LoadBalancer', {
certificateArn: props.certificateArn,
loadBalancerTarget: service.loadBalancerTarget,
albLogBucketName: props.albLogBucketName,
extraOptions: props.lbOptions,
securityGroups: lbSecurityGroups,
vpc,
});
const dashboard = new CacclMonitoring(this, 'Dashboard', {
cacclLoadBalancer: loadBalancer,
cacclService: service,
});
const notifyProps: CacclNotificationsProps = {
...props.notifications,
service,
loadBalancer,
};
if (db) {
notifyProps.db = db;
}
new CacclNotifications(this, 'Notifications', notifyProps);
if (db) {
dashboard.addDbSection(db);
}
if (createBastion) {
let bastionSg;
if (props.firewallSgId) {
bastionSg = lbSecurityGroups.primary;
} else {
bastionSg = new ec2.SecurityGroup(this, 'BastionSecurityGroup', {
vpc,
description: 'security group for the ssh bastion host',
});
bastionSg.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(22));
}
new CacclSshBastion(this, 'SshBastion', { vpc, sg: bastionSg });
}
if (props.scheduledTasks) {
const scheduledTasks = new CacclScheduledTasks(this, 'ScheduledTasks', {
vpc,
scheduledTasks: props.scheduledTasks,
clusterName: cluster.clusterName,
serviceName: service.ecsService.serviceName,
taskDefinition: taskDef.appOnlyTaskDef,
});
dashboard.addScheduledTasksSection(scheduledTasks);
}
}
}