caccl-deploy
Version:
A cli tool for managing ECS/Fargate app deployments
198 lines (168 loc) • 6.27 kB
text/typescript
import {
aws_ecs as ecs,
aws_logs as logs,
aws_iam as iam,
Stack,
RemovalPolicy,
CfnOutput,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { CacclAppEnvironment } from './appEnvironment';
import { CacclContainerImage } from './image';
import { CacclGitRepoVolumeContainer } from './volumeContainer';
const DEFAULT_PROXY_REPO_NAME = 'hdce/nginx-ssl-proxy';
export interface CacclTaskDefProps {
appImage: string;
proxyImage?: string;
vpcCidrBlock?: string;
appEnvironment?: CacclAppEnvironment;
taskCpu?: number;
taskMemory?: number;
logRetentionDays?: number;
gitRepoVolume?: { [key: string]: string };
}
export class CacclTaskDef extends Construct {
taskDef: ecs.FargateTaskDefinition;
appOnlyTaskDef: ecs.FargateTaskDefinition;
proxyContainer: ecs.ContainerDefinition;
appContainer: ecs.ContainerDefinition;
logGroup: logs.LogGroup;
constructor(scope: Construct, id: string, props: CacclTaskDefProps) {
super(scope, id);
const {
appImage,
proxyImage = `${DEFAULT_PROXY_REPO_NAME}:latest`,
appEnvironment,
taskCpu = 256, // in cpu units; 256 == .25 vCPU
taskMemory = 512, // in MiB
logRetentionDays = 90,
} = props;
const appContainerImage = new CacclContainerImage(this, 'AppImage', {
appImage,
});
// this is the task def that our fargate service will run
this.taskDef = new ecs.FargateTaskDefinition(this, 'Task', {
cpu: taskCpu,
memoryLimitMiB: taskMemory,
});
// this task def will have only the app container and be used for one-off tasks
this.appOnlyTaskDef = new ecs.FargateTaskDefinition(this, 'AppOnlyTask', {
cpu: taskCpu,
memoryLimitMiB: taskMemory,
});
const sendEmailPolicy = new iam.PolicyStatement({
actions: ['ses:SendEmail', 'ses:SendRawEmail'],
resources: ['*'],
});
this.taskDef.addToTaskRolePolicy(sendEmailPolicy);
this.appOnlyTaskDef.addToTaskRolePolicy(sendEmailPolicy);
// params for the fargate service's app container
const appContainerParams = {
image: appContainerImage.image,
taskDefinition: this.taskDef, // using the standard task def
essential: true,
environment: appEnvironment?.env,
secrets: appEnvironment?.secrets,
logging: ecs.LogDriver.awsLogs({
streamPrefix: 'app',
logGroup: new logs.LogGroup(this, 'AppLogGroup', {
logGroupName: `/${Stack.of(this).stackName}/app`,
removalPolicy: RemovalPolicy.DESTROY,
retention: logRetentionDays,
}),
}),
};
// the container definition associated with our fargate service task def
this.appContainer = new ecs.ContainerDefinition(
this,
'AppContainer',
appContainerParams,
);
this.appContainer.addPortMappings({
containerPort: 8080,
hostPort: 8080,
});
// now create a copy of the container params but use the one-off app only task def
const appOnlyContainerParams = {
...appContainerParams,
taskDefinition: this.appOnlyTaskDef,
};
// and a 2nd container definition used by the one-off app only task deff
new ecs.ContainerDefinition(
this,
'AppOnlyContainer',
appOnlyContainerParams,
);
const proxyContainerImage = new CacclContainerImage(this, 'ProxyImage', {
appImage: proxyImage,
});
const environment: { [key: string]: string } = {
APP_PORT: '8080',
};
/**
* this should never be undefined at this point but we have flag it
* as '?' and wrap in a undefined check as
* because of how the value can't come with the
* rest of the task def configuration
*/
if (props.vpcCidrBlock !== undefined) {
environment.VPC_CIDR = props.vpcCidrBlock;
} else {
throw new Error('proxy contianer environment needs the vpc cidr!');
}
// this container is the proxy
this.proxyContainer = new ecs.ContainerDefinition(this, 'ProxyContainer', {
image: proxyContainerImage.image,
environment,
essential: true,
taskDefinition: this.taskDef,
logging: ecs.LogDriver.awsLogs({
streamPrefix: 'proxy',
logGroup: new logs.LogGroup(this, 'ProxyLogGroup', {
logGroupName: `/${Stack.of(this).stackName}/proxy`,
removalPolicy: RemovalPolicy.DESTROY,
retention: logRetentionDays,
}),
}),
});
this.proxyContainer.addPortMappings({
containerPort: 443,
hostPort: 443,
});
new CfnOutput(this, 'TaskDefinitionArn', {
exportName: `${Stack.of(this).stackName}-task-def-name`,
// "family" is synonymous with "name", or at least aws frequently treats it that way
value: this.taskDef.family,
});
new CfnOutput(this, 'AppOnlyTaskDefinitionArn', {
exportName: `${Stack.of(this).stackName}-app-only-task-def-name`,
// "family" is synonymous with "name", or at least aws frequently treats it that way
value: this.appOnlyTaskDef.family,
});
/**
* for edge cases where we have some data in a git repo that we want to make available to the app.
* this adds a third container to the task definition that uses an alpine/git image to clone
* a repo into a configured mount point. the repo can be private, in which case the url would need
* to include the user:pass info, therefore the repo url value has to come from secrets manager
*/
if (props.gitRepoVolume) {
const { repoUrlSecretArn, appContainerPath } = props.gitRepoVolume;
if (repoUrlSecretArn === undefined) {
throw new Error(
'You must provide the ARN of a SecretsManager secret containing the git repo url as `deployConfig.gitRepoVolume.repoUrlSecretArn!`',
);
}
if (appContainerPath === undefined) {
throw new Error(
'You must set `deployConfig.gitRepoVolume.appContainerPath` to the path you want the git repo volume to be mounted in your app',
);
}
new CacclGitRepoVolumeContainer(this, 'VolumeContainer', {
repoUrlSecretArn,
appContainerPath,
taskDefinition: this.taskDef,
appContainer: this.appContainer,
});
}
}
}