Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

853: Service availability in AWS #854

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cloud/.eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module.exports = {
root: true,
ignorePatterns: ['node_modules', 'cdk.out'],
rules: {
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
'@typescript-eslint/init-declarations': 'error',
'@typescript-eslint/no-misused-promises': [
'error',
Expand Down
92 changes: 92 additions & 0 deletions cloud/lib/api-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,31 @@ import {
import { ApplicationLoadBalancedFargateService } from 'aws-cdk-lib/aws-ecs-patterns';
//import { ListenerAction } from 'aws-cdk-lib/aws-elasticloadbalancingv2';
//import { AuthenticateCognitoAction } from 'aws-cdk-lib/aws-elasticloadbalancingv2-actions';
import {
Effect,
PolicyStatement,
Role,
ServicePrincipal,
} from 'aws-cdk-lib/aws-iam';
import { Runtime } from 'aws-cdk-lib/aws-lambda';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Bucket } from 'aws-cdk-lib/aws-s3';
import {
CronOptionsWithTimezone,
Group,
Schedule,
ScheduleExpression,
ScheduleTargetInput,
} from '@aws-cdk/aws-scheduler-alpha';
import { LambdaInvoke } from '@aws-cdk/aws-scheduler-targets-alpha';
import { Secret } from 'aws-cdk-lib/aws-secretsmanager';
import {
CfnOutput,
RemovalPolicy,
Stack,
StackProps,
Tags,
TimeZone,
} from 'aws-cdk-lib/core';
import { Construct } from 'constructs';
import { join } from 'node:path';
Expand All @@ -29,6 +46,7 @@ import {
resourceDescription,
resourceName,
} from './resourceNamingUtils';
import type { ServiceEventLambda } from './startStopServiceLambda';

type ApiStackProps = StackProps & {
// userPool: UserPool;
Expand Down Expand Up @@ -117,6 +135,80 @@ export class ApiStack extends Stack {
})
);

// Lambda to bring fargate service up or down
const startStopFunctionName = generateResourceName('fargate-switch');
const startStopServiceFunction = new NodejsFunction(
this,
startStopFunctionName,
{
functionName: startStopFunctionName,
description: generateResourceDescription(
'Fargate Service start/stop function'
),
runtime: Runtime.NODEJS_18_X,
handler: 'handler',
entry: join(__dirname, './startStopServiceLambda.ts'),
bundling: {
minify: true,
},
environment: {
CLUSTER_NAME: cluster.clusterName,
SERVICE_NAME: fargateService.service.serviceName,
},
}
);
startStopServiceFunction.addToRolePolicy(
new PolicyStatement({
effect: Effect.ALLOW,
actions: ['ecs:DescribeServices', 'ecs:UpdateService'],
resources: [fargateService.service.serviceArn],
})
);

// Schedule fargate service up at start of day, down at end
const schedulerRoleName = generateResourceName('scheduler-role');
const schedulerRole = new Role(this, schedulerRoleName, {
roleName: schedulerRoleName,
assumedBy: new ServicePrincipal('scheduler.amazonaws.com'),
});
const lambdaTarget = (operation: ServiceEventLambda['operation']) =>
new LambdaInvoke(startStopServiceFunction, {
input: ScheduleTargetInput.fromObject({ operation }),
role: schedulerRole,
});
const cronDef: CronOptionsWithTimezone = {
weekDay: 'MON-FRI',
minute: '0',
timeZone: TimeZone.EUROPE_LONDON,
};
const scheduleGroupName = generateResourceName('fargate-scheduler-group');
const scheduleGroup = new Group(this, scheduleGroupName, {
groupName: scheduleGroupName,
removalPolicy: RemovalPolicy.DESTROY,
});
const serverUpScheduleName = generateResourceName('server-up');
new Schedule(this, serverUpScheduleName, {
scheduleName: serverUpScheduleName,
description: generateResourceDescription('Scheduled server-up event'),
target: lambdaTarget('start'),
group: scheduleGroup,
schedule: ScheduleExpression.cron({
...cronDef,
hour: '9',
}),
});
const serverDownScheduleName = generateResourceName('server-down');
new Schedule(this, serverDownScheduleName, {
scheduleName: serverDownScheduleName,
description: generateResourceDescription('Scheduled server-down event'),
target: lambdaTarget('stop'),
group: scheduleGroup,
schedule: ScheduleExpression.cron({
...cronDef,
hour: '17',
}),
});

// Hook up Cognito to load balancer
// https://stackoverflow.com/q/71124324
// TODO Needs HTTPS and a Route53 domain, so for now we're using APIGateway and VPCLink:
Expand Down
24 changes: 24 additions & 0 deletions cloud/lib/startStopServiceLambda.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ECS } from '@aws-sdk/client-ecs';

export type ServiceEventLambda = {
operation: 'start' | 'stop';
};

export const handler = async (event: ServiceEventLambda) => {
const { operation } = event;
const { CLUSTER_NAME, SERVICE_NAME } = process.env;
const operationDesc = operation === 'start' ? 'started' : 'stopped';
const ecs = new ECS();

try {
await ecs.updateService({
cluster: CLUSTER_NAME,
service: SERVICE_NAME,
desiredCount: operation === 'start' ? 1 : 0,
});
console.log(`${SERVICE_NAME} was ${operationDesc}`);
} catch (err) {
console.log(`${SERVICE_NAME} could not be ${operationDesc}`);
console.warn(err);
}
};
Loading
Loading