Skip to content

Commit

Permalink
[TM-1273] Make the service deployment generic to avoid too much copy-…
Browse files Browse the repository at this point in the history
…pasta when creating a new service.
  • Loading branch information
roguenet committed Oct 23, 2024
1 parent bb2c94e commit 7bad1b7
Show file tree
Hide file tree
Showing 13 changed files with 103 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
name: User Service Deploy
name: Service Deploy
run-name: 'Service Deploy [service: ${{ inputs.service }}, env: ${{ inputs.env }}]'

on:
workflow_dispatch:
inputs:
service:
description: 'Service to deploy'
type: choice
required: true
options:
- user-service
- job-service
env:
description: 'Deployment target environment'
type: choice
Expand All @@ -20,8 +28,8 @@ permissions:
env:
AWS_REGION: eu-west-1
AWS_ROLE_TO_ASSUME: arn:aws:iam::603634817705:role/terramatch-microservices-github-actions
AWS_ROLE_SESSION_NAME: terramatch-microservices-cicd-user-service
ECR_REPOSITORY: terramatch-microservices/user-service-${{ inputs.env }}
AWS_ROLE_SESSION_NAME: terramatch-microservices-cicd-${{ inputs.service }}
ECR_REPOSITORY: terramatch-microservices/${{ inputs.service }}-${{ inputs.env }}
ECR_REGISTRY: 603634817705.dkr.ecr.eu-west-1.amazonaws.com
IMAGE_TAG: ${{ github.sha }}

Expand Down Expand Up @@ -51,15 +59,15 @@ jobs:
echo "DB_PASSWORD=\"${{ secrets.DB_PASSWORD }}\"" >> .env
: # Don't build the base image with NODE_ENV because it'll limit the packages that are installed
docker build -t terramatch-microservices-base:nx-base .
USER_SERVICE_IMAGE=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker build --build-arg NODE_ENV=production --build-arg BUILD_FLAG=--prod -f apps/user-service/Dockerfile -t $USER_SERVICE_IMAGE .
docker push $USER_SERVICE_IMAGE
echo "image=$USER_SERVICE_IMAGE"
SERVICE_IMAGE=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker build --build-arg NODE_ENV=production --build-arg BUILD_FLAG=--prod -f apps/${{ inputs.service }}/Dockerfile -t $SERVICE_IMAGE .
docker push $SERVICE_IMAGE
echo "image=$SERVICE_IMAGE"
- name: Launch new task definition
id: launch
run: |
cd apps/user-service/stack
cd service-stack
npm i
IMAGE_TAG=$IMAGE_TAG TM_ENV=${{ inputs.env }} npx --yes cdk deploy --require-approval never
IMAGE_TAG=$IMAGE_TAG TM_SERVICE=${{ inputs.service }} TM_ENV=${{ inputs.env }} npx --yes cdk deploy --require-approval never
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,27 @@ Repository for the Microservices API backend of the TerraMatch service
* `NEXT_PUBLIC_API_BASE_URL='http://localhost:4000'`

# Deployment
Deployment is handled via manual trigger of GitHub actions. There is one for each service, and one for the ApiGateway. The
Deployment is handled via manual trigger of GitHub actions. There is one for services, and one for the ApiGateway. The
ApiGateway only needs to be redeployed if its code changes; it does not need to be redeployed for updates to individual services
to take effect.

Once this project is live in production, we can explore continuous deployment to at least staging and prod envs on the staging
and main branches.

# Creating a new service
* In the root directory: `nx g @nx/nest:app apps/foo-service`
* Set up the new `main.ts` similarly to existing services.
* Make sure swagger docs and the `/health` endpoint are implemented
* Pick a default local port that is unique from other services
* In your `.env` and `.env.local.sample`, add `_PROXY_PORT` and `_PROXY_TARGET` for the new service
* In `api-gateway-stack.ts`, add the new service and namespace to `V3_SERVICES`
* Add a Dockerfile in the new app directory. A simple copy and modify from user-service is sufficient
* In AWS:
* Add ECR repositories for each env (follow the naming scheme from user-service, e.g. `terramatch-microservices/foo-service-staging`, etc)
* Set the repo to Immutable
* After creation, set a Lifecycle Policy. In lower envs, we retain the most recent 2 images, and in prod it's set to 5
* In CloudWatch, create a log group for each env (follow the naming scheme from user-service, e.g. `ecs/foo-service-staging`, etc).

# Database work
For now, Laravel is the source of truth for all things related to the DB schema. As such, TypeORM is not allowed to modify the
schema, and is expected to interface with exactly the schema that is managed by Laravel. This note is included in user.entity.ts,
Expand Down
15 changes: 15 additions & 0 deletions apps/job-service/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FROM terramatch-microservices-base:nx-base AS builder

ARG BUILD_FLAG
WORKDIR /app/builder
COPY . .
RUN npx nx build job-service ${BUILD_FLAG}

FROM terramatch-microservices-base:nx-base

ARG NODE_ENV
WORKDIR /app
COPY --from=builder /app/builder ./
ENV NODE_ENV=${NODE_ENV}

CMD ["node", "./dist/apps/job-service/main.js"]
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { UserServiceStack } from '../lib/user-service-stack';
import { ServiceStack } from '../lib/service-stack';
import { upperFirst, camelCase } from 'lodash';

const app = new cdk.App();
new UserServiceStack(app, `UserServiceStack-${process.env.TM_ENV}`, {
const id = `${upperFirst(camelCase(process.env.TM_SERVICE))}Stack-${process.env.TM_ENV}`
new ServiceStack(app, id, {
env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
});
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"app": "npx ts-node --prefer-ts-exts bin/user-service.ts",
"app": "npx ts-node --prefer-ts-exts bin/service.ts",
"watch": {
"include": [
"**"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,26 @@ import { Repository } from 'aws-cdk-lib/aws-ecr';
import { Cluster, ContainerImage, LogDriver } from 'aws-cdk-lib/aws-ecs';
import { ApplicationLoadBalancedFargateService } from 'aws-cdk-lib/aws-ecs-patterns';
import { Role } from 'aws-cdk-lib/aws-iam';
import { upperFirst } from 'lodash';

export class UserServiceStack extends Stack {
const extractFromEnv = (...names: string[]) => names.map(name => {
const value = process.env[name];
if (value == null) throw new Error(`No ${name} defined`)
return value;
});

export class ServiceStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);

const env = process.env.TM_ENV;
if (env == null) throw new Error('No TM_ENV defined');

const envName = env[0].toUpperCase() + env.substring(1);

const imageTag = process.env.IMAGE_TAG;
if (imageTag == null) throw new Error('No IMAGE_TAG defined');
const [env, service, imageTag] = extractFromEnv('TM_ENV', 'TM_SERVICE', 'IMAGE_TAG');
const envName = upperFirst(env);

// Identify the most recently updated user service docker image
// Identify the most recently updated service docker image
const repository = Repository.fromRepositoryName(
this,
`Terramatch Microservices ${envName}`,
`terramatch-microservices/user-service-${env}`
`terramatch-microservices/${service}-${env}`
);
const image = ContainerImage.fromEcrRepository(repository, imageTag);

Expand Down Expand Up @@ -58,25 +60,25 @@ export class UserServiceStack extends Stack {
];

// Create a load-balanced Fargate service and make it public
const service = new ApplicationLoadBalancedFargateService(
const fargateService = new ApplicationLoadBalancedFargateService(
this,
`terramatch-user-service-${env}`,
`terramatch-${service}-${env}`,
{
serviceName: `terramatch-user-service-${env}`,
serviceName: `terramatch-${service}-${env}`,
cluster,
cpu: 512,
desiredCount: 1,
taskImageOptions: {
image,
family: `terramatch-user-service-${env}`,
containerName: `terramatch-user-service-${env}`,
family: `terramatch-${service}-${env}`,
containerName: `terramatch-${service}-${env}`,
logDriver: LogDriver.awsLogs({
logGroup: LogGroup.fromLogGroupName(
this,
`user-service-${env}`,
`ecs/user-service-${env}`
`${service}-${env}`,
`ecs/${service}-${env}`
),
streamPrefix: `user-service-${env}`,
streamPrefix: `${service}-${env}`,
}),
executionRole: Role.fromRoleName(
this,
Expand All @@ -89,12 +91,12 @@ export class UserServiceStack extends Stack {
memoryLimitMiB: 2048,
assignPublicIp: false,
publicLoadBalancer: false,
loadBalancerName: `user-service-${env}`,
loadBalancerName: `${service}-${env}`,
}
);
service.targetGroup.configureHealthCheck({
fargateService.targetGroup.configureHealthCheck({
path: '/health',
});
Tags.of(service.loadBalancer).add('service', `user-service-${env}`);
Tags.of(fargateService.loadBalancer).add('service', `${service}-${env}`);
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"name": "user-service",
"name": "service-stack",
"version": "0.1.0",
"bin": {
"stack": "bin/user-service.js"
"stack": "bin/service.js"
},
"scripts": {
"build": "tsc",
Expand All @@ -12,16 +12,18 @@
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/lodash": "^4.17.12",
"@types/node": "20.14.9",
"aws-cdk": "2.157.0",
"jest": "^29.7.0",
"ts-jest": "^29.1.5",
"aws-cdk": "2.157.0",
"ts-node": "^10.9.2",
"typescript": "~5.5.3"
},
"dependencies": {
"aws-cdk-lib": "2.157.0",
"constructs": "^10.0.0",
"lodash": "^4.17.21",
"source-map-support": "^0.5.21"
}
}
File renamed without changes.

0 comments on commit 7bad1b7

Please sign in to comment.