diff --git a/src/constructs/ad-hoc/app/index.ts b/src/constructs/ad-hoc/app/index.ts index 1586ac7..c19b817 100644 --- a/src/constructs/ad-hoc/app/index.ts +++ b/src/constructs/ad-hoc/app/index.ts @@ -1,4 +1,4 @@ -import { Stack } from 'aws-cdk-lib'; +import { CfnOutput, Stack } from 'aws-cdk-lib'; import { IVpc, ISecurityGroup } from 'aws-cdk-lib/aws-ec2'; import { Repository } from 'aws-cdk-lib/aws-ecr'; import { Cluster, EcrImage } from 'aws-cdk-lib/aws-ecs'; @@ -9,6 +9,7 @@ import { PrivateDnsNamespace } from 'aws-cdk-lib/aws-servicediscovery'; import { Construct } from 'constructs'; // import { HighestPriorityRule } from '../../internal/customResources/highestPriorityRule'; import { EcsRoles } from '../../internal/ecs/iam'; +import { ManagementCommandTask } from '../../internal/ecs/management-command'; import { WebService } from '../../internal/ecs/web'; export interface AdHocAppProps { @@ -113,13 +114,25 @@ export class AdHocApp extends Construct { healthCheckPath: '/', }); - // ensure that the backend service listener rule has a higher priority than the frontend service listener rule - // backendService.node.addDependency(frontendService); - // worker service // scheduler service // management command task definition + const backendUpdateTask = new ManagementCommandTask(this, 'BackendUpdateTask', { + cluster, + environmentVariables, + vpc: props.vpc, + appSecurityGroup: props.appSecurityGroup, + taskRole: ecsRoles.ecsTaskRole, + executionRole: ecsRoles.taskExecutionRole, + image: backendImage, + command: ['python', 'manage.py', 'backend_update'], + containerName: 'backendUpdate', + family: 'backendUpdate', + }); + + // define stack output use for running the management command + new CfnOutput(this, 'backendUpdateCommand', { value: backendUpdateTask.executionScript }); } } diff --git a/src/constructs/internal/ecs/management-command/index.ts b/src/constructs/internal/ecs/management-command/index.ts index e69de29..822da7f 100644 --- a/src/constructs/internal/ecs/management-command/index.ts +++ b/src/constructs/internal/ecs/management-command/index.ts @@ -0,0 +1,96 @@ +import { RemovalPolicy, Stack } from 'aws-cdk-lib'; +import { ISecurityGroup, IVpc } from 'aws-cdk-lib/aws-ec2'; +import { + LogDriver, + Cluster, + ContainerImage, + FargateTaskDefinition, +} from 'aws-cdk-lib/aws-ecs'; +import { Role } from 'aws-cdk-lib/aws-iam'; +import { + LogGroup, + LogStream, + RetentionDays, +} from 'aws-cdk-lib/aws-logs'; +import { Construct } from 'constructs'; + + +export interface ManagementCommandTaskProps { + readonly cluster: Cluster; + readonly vpc: IVpc; + readonly cpu?: number; + readonly memorySize?: number; + readonly appSecurityGroup: ISecurityGroup; + readonly image: ContainerImage; + readonly command: string[]; + readonly containerName: string; + readonly taskRole: Role; + readonly executionRole: Role; + readonly family: string; + readonly environmentVariables: { [key: string]: string }; +}; + +export class ManagementCommandTask extends Construct { + + /** + * Script to invoke run-task and send task logs to standard output + */ + public executionScript: string; + + constructor(scope: Construct, id: string, props: ManagementCommandTaskProps) { + super(scope, id); + + const stackName = Stack.of(this).stackName; + + // define log group and logstream + const logGroupName = `/ecs/${stackName}/${props.containerName}/`; + const streamPrefix = props.containerName; + const logGroup = new LogGroup(this, 'LogGroup', { + logGroupName, + retention: RetentionDays.ONE_DAY, + removalPolicy: RemovalPolicy.DESTROY, + }); + + new LogStream(this, 'LogStream', { + logGroup, + logStreamName: props.containerName, + }); + + // task definition + const taskDefinition = new FargateTaskDefinition(this, 'TaskDefinition', { + cpu: props.cpu ?? 256, + executionRole: props.executionRole, + taskRole: props.taskRole, + family: props.family, + }); + + taskDefinition.addContainer(props.containerName, { + image: props.image, + command: props.command, + containerName: props.containerName, + environment: props.environmentVariables, + essential: true, + logging: LogDriver.awsLogs({ + streamPrefix, + logGroup, + }), + hostname: props.containerName, + }); + + // this script is called once on initial setup from GitHub Actions + const executionScript = ` +START_TIME=$(date +%s000) + +TASK_ID=$(aws ecs run-task --cluster ${props.cluster.clusterArn} --task-definition ${taskDefinition.taskDefinitionArn} --network-configuration "awsvpcConfiguration={subnets=[${props.vpc.privateSubnets.map(x=>{x.subnetId;}).join(',')}],securityGroups=[${props.appSecurityGroup}],assignPublicIp=ENABLED}" | jq -r '.tasks[0].taskArn') + +aws ecs wait tasks-stopped --tasks $TASK_ID --cluster ${props.cluster.clusterArn} + +END_TIME=$(date +%s000) + +aws logs get-log-events --log-group-name ${logGroupName} --log-stream-name ${streamPrefix}/${props.containerName}/\${TASK_ID##*/} --start-time $START_TIME --end-time $END_TIME | jq -r '.events[].message' + `; + + this.executionScript = executionScript; + + } +} \ No newline at end of file diff --git a/src/constructs/internal/ecs/web/index.ts b/src/constructs/internal/ecs/web/index.ts index 2d0a552..ad41e41 100644 --- a/src/constructs/internal/ecs/web/index.ts +++ b/src/constructs/internal/ecs/web/index.ts @@ -1,10 +1,6 @@ -import { - Duration, - RemovalPolicy, -} from 'aws-cdk-lib'; +import { Duration, RemovalPolicy, Stack } from 'aws-cdk-lib'; import { ISecurityGroup, IVpc } from 'aws-cdk-lib/aws-ec2'; import { - // AwsLogDriver, LogDriver, Cluster, ContainerImage, @@ -56,19 +52,22 @@ export class WebService extends Construct { constructor(scope: Construct, id: string, props: WebProps) { super(scope, id); - // Getting circular dependency error when using `FargateTaskDefinition` - // https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.FargateService.html#example + const stackName = Stack.of(this).stackName; + + // define log group and logstream + const logGroupName = `/ecs/${stackName}/${props.containerName}/`; + const streamPrefix = props.containerName; // define log group and logstream const logGroup = new LogGroup(this, 'LogGroup', { - logGroupName: `/ecs/test/${props.containerName}`, + logGroupName, retention: RetentionDays.ONE_DAY, removalPolicy: RemovalPolicy.DESTROY, }); new LogStream(this, 'LogStream', { logGroup, - logStreamName: 'web-logs', + logStreamName: props.containerName, }); // task definition @@ -86,14 +85,14 @@ export class WebService extends Construct { environment: props.environmentVariables, essential: true, logging: LogDriver.awsLogs({ - streamPrefix: 'web', - // logGroup + streamPrefix, + logGroup, }), portMappings: [{ containerPort: props.port, hostPort: props.port, }], - hostname: `stackName-${props.containerName}`, + hostname: props.containerName, }); const useSpot = props.useSpot ?? false; @@ -115,7 +114,7 @@ export class WebService extends Construct { desiredCount: 1, enableExecuteCommand: true, securityGroups: [props.appSecurityGroup], - serviceName: `stackName-${props.containerName}`, + serviceName: `${stackName}-${props.containerName}`, vpcSubnets: { subnets: props.vpc.privateSubnets, }, @@ -143,7 +142,7 @@ export class WebService extends Construct { // targetGroups: [targetGroup], conditions: [ ListenerCondition.pathPatterns(props.pathPatterns), - ListenerCondition.hostHeaders([`stackName.${props.domainName}`]), + ListenerCondition.hostHeaders([`${stackName}.${props.domainName}`]), ], action: ListenerAction.forward([targetGroup]), });