Skip to content

Commit

Permalink
Start/stop fargate service at start/end of day (#854)
Browse files Browse the repository at this point in the history
  • Loading branch information
chriswilty committed Apr 15, 2024
1 parent cbc35a8 commit 393f4b1
Show file tree
Hide file tree
Showing 5 changed files with 1,369 additions and 15 deletions.
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

0 comments on commit 393f4b1

Please sign in to comment.