packages/cdk/lib/prisma-migrate-task.ts (159 lines of code) (raw):
import type { GuStack } from '@guardian/cdk/lib/constructs/core';
import type { GuSecurityGroup } from '@guardian/cdk/lib/constructs/ec2';
import type { ICluster, Volume } from 'aws-cdk-lib/aws-ecs';
import {
FargateTaskDefinition,
FireLensLogDriver,
FirelensLogRouterType,
LogDrivers,
PropagatedTagSource,
Secret,
} from 'aws-cdk-lib/aws-ecs';
import { Rule } from 'aws-cdk-lib/aws-events';
import { EcsTask } from 'aws-cdk-lib/aws-events-targets';
import type { PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
import { RetentionDays } from 'aws-cdk-lib/aws-logs';
import type { DatabaseInstance } from 'aws-cdk-lib/aws-rds';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import { StringParameter } from 'aws-cdk-lib/aws-ssm';
import { Images } from './cloudquery/images';
interface PrismaMigrateTaskProps {
loggingStreamName: string;
logShippingPolicy: PolicyStatement;
db: DatabaseInstance;
dbAccess: GuSecurityGroup;
cluster: ICluster;
}
export function addPrismaMigrateTask(
scope: GuStack,
{
loggingStreamName,
logShippingPolicy,
db,
dbAccess,
cluster,
}: PrismaMigrateTaskProps,
) {
const app = 'prisma-migrate-task';
const { stack, stage, region } = scope;
const roleName = `${app}-${stage}`;
const taskRole = new Role(scope, roleName, {
assumedBy: new ServicePrincipal('ecs-tasks.amazonaws.com'),
roleName,
});
const fireLensLogDriver = new FireLensLogDriver({
options: {
Name: `kinesis_streams`,
region,
stream: loggingStreamName,
retry_limit: '2',
},
});
const taskDefinition = new FargateTaskDefinition(
scope,
`${app}TaskDefinition`,
{
cpu: 512,
memoryLimitMiB: 1024,
taskRole,
},
);
const firelensLogRouter = taskDefinition.addFirelensLogRouter(
`${app}Firelens`,
{
image: Images.devxLogs,
logging: LogDrivers.awsLogs({
streamPrefix: [stack, stage, app].join('/'),
logRetention: RetentionDays.ONE_DAY,
}),
environment: {
STACK: stack,
STAGE: stage,
APP: app,
GU_REPO: 'guardian/service-catalogue',
},
firelensConfig: {
type: FirelensLogRouterType.FLUENTBIT,
},
readonlyRootFilesystem: true,
},
);
const firelensVolume: Volume = {
name: 'firelens-volume',
};
taskDefinition.addVolume(firelensVolume);
firelensLogRouter.addMountPoints({
containerPath: '/init',
sourceVolume: firelensVolume.name,
readOnly: false,
});
/**
* This error shouldn't ever be thrown as AWS CDK creates a secret by default,
* it is just typed as optional.
*
* @see https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_rds.DatabaseInstance.html#credentials.
*
* TODO: Remove this once IAM auth is working.
*/
if (!db.secret) {
throw new Error('DB Secret is missing');
}
const prismaArtifactKey = `${stack}/${stage}/service-catalogue-prisma-migrations/prisma.zip`;
const artifactBucketName = StringParameter.valueForStringParameter(
scope,
'/account/services/artifact.bucket',
);
const artifactBucket = Bucket.fromBucketName(
scope,
'artifact-bucket',
artifactBucketName,
);
const prismaTask = taskDefinition.addContainer(`${app}Container`, {
image: Images.prismaMigrate,
environment: {
// These are required so the task can retrieve the Prisma directory
// from the artifact bucket
ARTIFACT_BUCKET: artifactBucketName,
PRISMA_ARTIFACT_KEY: prismaArtifactKey,
},
secrets: {
DB_USERNAME: Secret.fromSecretsManager(db.secret, 'username'),
DB_HOST: Secret.fromSecretsManager(db.secret, 'host'),
DB_PASSWORD: Secret.fromSecretsManager(db.secret, 'password'),
},
dockerLabels: {
Stack: stack,
Stage: stage,
App: app,
},
logging: fireLensLogDriver,
readonlyRootFilesystem: true,
});
taskDefinition.addToTaskRolePolicy(logShippingPolicy);
db.grantConnect(taskDefinition.taskRole);
artifactBucket.grantRead(taskDefinition.taskRole, prismaArtifactKey);
const prismaArtifactVolume: Volume = {
name: 'artifact-volume',
};
taskDefinition.addVolume(prismaArtifactVolume);
prismaTask.addMountPoints({
// So that we can download the prisma.zip from the artifact bucket
containerPath: '/usr/src/app/prisma',
sourceVolume: prismaArtifactVolume.name,
readOnly: false,
});
// --- EvenBridge rule + target ---
// Rule that is triggered when the prisma.zip is PUT into the artifact bucket
const rule = new Rule(scope, 'PrismaMigrateArtifactPutRule', {
eventPattern: {
source: ['aws.s3'],
detailType: ['AWS API Call via CloudTrail'],
detail: {
eventSource: ['s3.amazonaws.com'],
eventName: ['PutObject'],
requestParameters: {
bucketName: [artifactBucketName],
key: [prismaArtifactKey],
},
},
},
});
rule.addTarget(
new EcsTask({
cluster,
taskDefinition,
subnetSelection: { subnets: cluster.vpc.privateSubnets },
securityGroups: [dbAccess],
propagateTags: PropagatedTagSource.TASK_DEFINITION,
}),
);
return taskDefinition;
}