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

[WIP] feat(infra): First pass at microservice TS project [experiment] #210

Closed
wants to merge 12 commits into from
Closed
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
4 changes: 4 additions & 0 deletions .projen/deps.json

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

3 changes: 3 additions & 0 deletions .projen/tasks.json

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

6 changes: 5 additions & 1 deletion .projenrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const project = new cdk.JsiiProject({
// gitignore: [ '/.npmrc' ],

bundledDeps,
deps: [...bundledDeps, 'projen', 'semver'],
deps: [...bundledDeps, 'projen', 'semver', 'multi-convention-namer'],
devDeps: ['projen', '@types/semver'],
peerDeps: ['projen'],

Expand Down Expand Up @@ -86,6 +86,10 @@ const project = new cdk.JsiiProject({
codeCovTokenSecret: 'CODECOV_TOKEN',
});

project.projectBuild.postCompileTask.exec('rm -r ./lib/samples; cp -r ./src/samples ./lib/', {});

//project.addScripts({ compile: 'rm -rf ./lib/ && npx projen compile && cp -r ./src/samples ./lib/samples' });

new YamlFile(project, 'codecov.yml', {
obj: {
coverage: {
Expand Down
6,340 changes: 5,049 additions & 1,291 deletions API.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json

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

2 changes: 1 addition & 1 deletion src/clickup-cdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ export module clickupCdk {
// This cdkVersion is actually the minimum version that's compatible. It only affects devDeps.
// This really only affects users when they try to deploy directly from their laptop.
// When deploying from cdkPipelines, it will use whatever version the library is currently on per yarn.lock.
let cdkVersion = undefined;
let cdkVersion: string | undefined = undefined;
if (semver.lt(options.cdkVersion, '2.64.0')) {
cdkVersion = '2.87.0'; // Arbitrary newish version. Most developers will want the latest.
console.warn(
Expand Down
104 changes: 104 additions & 0 deletions src/clickup-ecs-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import * as fs from 'fs';
import * as path from 'path';
import { Component, SampleFile } from 'projen';
import merge from 'ts-deepmerge';
import { clickupTs } from './clickup-ts';
import { renovateWorkflow } from './renovate-workflow';
import { ecsServiceCDWorkflow } from './workflows/ecs-service-cd';
import { ecsServiceCIWorkflow } from './workflows/ecs-service-ci';
import { ecsServiceDeployWorkflow } from './workflows/ecs-service-deploy';

export module clickupEcsService {
export const defaults: Omit<ClickUpTypeScriptEcsServiceProjectOptions, 'name' | 'serviceName'> = {
...clickupTs.defaults,
workflowNodeVersion: '16',
};

export interface ClickUpTypeScriptEcsServiceProjectOptions extends clickupTs.ClickUpTypeScriptProjectOptions {
readonly deployWorkflowOptions?: ecsServiceDeployWorkflow.EcsServiceDeployOptionsConfig;
readonly serviceName: string;
readonly serviceHealthcheckPath?: string;
}

export class ClickUpTypeScriptEcsServiceProject extends clickupTs.ClickUpTypeScriptProject {
readonly workflowNodeVersion: string;
readonly serviceName: string;
readonly serviceHealthcheckPath: string;

constructor(options: ClickUpTypeScriptEcsServiceProjectOptions) {
const serviceHealthcheckPath = options.serviceHealthcheckPath ?? `${options.serviceName}/health`;
let mergedOptions = merge(
defaults,
{
deps: clickupTs.deps,
deployWorkflowOptions: {
deployEnvs: [ecsServiceDeployWorkflow.ClickUpHarnessEnv.QA],
serviceHealthcheckPath,
},
},
options,
{
// Disable projen's built-in docgen class
docgen: undefined,
name: clickupTs.normalizeName(options.name),
renovatebotOptions: renovateWorkflow.getRenovateOptions(options.renovateOptionsConfig),
},
);
super(mergedOptions);
this.workflowNodeVersion = mergedOptions.workflowNodeVersion!;
this.serviceName = options.serviceName;
this.serviceHealthcheckPath = serviceHealthcheckPath;

new AppSampleCode(this);

ecsServiceCIWorkflow.createContinuousIntegrationWorkflow(this, {
...mergedOptions,
nodeVersion: this.workflowNodeVersion,
});

ecsServiceCDWorkflow.createContinuousDeliveryWorkflow(this, {
...mergedOptions,
...mergedOptions.deployWorkflowOptions!,
nodeVersion: this.workflowNodeVersion,
});
}
}

class AppSampleCode extends Component {
constructor(project: ClickUpTypeScriptEcsServiceProject) {
super(project);

new SampleFile(project, 'Dockerfile', {
contents: this.renderSample('Dockerfile.sample'),
});
Comment on lines +71 to +73
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I love/hate the use of SampleFile. Does it make sense to go managed instead? What level of change is reasonable to expect / support?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, the Dockerfile and build scripts likely need to be samples because otherwise there's little-to-no customization that can be added easily. I don't necessarily want additional Dockerfile steps submitted through .projenrc.ts. In fact ideally there would be a base image that the sample Dockerfile would pull from.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As for docker-healthcheck...that could almost certainly be managed. Can actually be a mechanism of enforcing a particular healthcheck pattern.


new SampleFile(project, 'scripts/docker-healthcheck.sh', {
contents: this.renderSample('docker-healthcheck.sh.sample', {
'<%SERVICE_HEALTHCHECK_PATH%>': project.serviceHealthcheckPath,
}),
});

new SampleFile(project, 'scripts/docker-build.sh', {
contents: this.renderSample('docker-build.sh.sample', {
'<%SERVICE_NAME%>': project.serviceName,
}),
});
}

private getSample(fileName: string): string {
return fs.readFileSync(path.join(__dirname, 'samples', fileName), 'utf-8');
}

private renderSample(sampleFileName: string, vars?: { [toReplace: string]: string }): string {
let contents = this.getSample(sampleFileName);

if (vars) {
const toReplace = Object.keys(vars);
toReplace.map((key) => {
contents = contents.replace(new RegExp(key, 'g'), vars[key]);
});
}
return contents;
}
}
}
15 changes: 2 additions & 13 deletions src/datadog.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { stringify } from 'cson-parser';
import { JobPermission } from 'projen/lib/github/workflows-model';
import { NodeProject } from 'projen/lib/javascript';
import { getVersionSteps } from './workflows/utils/getVersion';

export module datadog {
export interface ReleaseEventTags {
Expand Down Expand Up @@ -83,19 +84,7 @@ export module datadog {
CI: 'true',
},
steps: [
{
name: 'Download build artifacts',
uses: 'actions/download-artifact@v3',
with: {
name: 'build-artifact',
path: project.release!.artifactsDirectory,
},
},
{
name: 'Get version',
id: 'event_metadata',
run: `echo "release_tag=$(cat ${project.release!.artifactsDirectory}/releasetag.txt)" >> $GITHUB_OUTPUT`,
},
...getVersionSteps(project),
{
name: 'Send Datadog event',
// https://github.com/Glennmen/datadog-event-action/releases/tag/1.1.0
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
export * from './add-to-project';
export * from './cdk-context-json';
export * from './cdk-diff-workflow';
export * from './workflows/ecs-service-deploy';
export * from './clickup-cdk';
export * from './clickup-ts';
export * from './clickup-ecs-service';
export * from './datadog';
export * from './datadog-service-catalog';
export * from './renovate-workflow';
Expand Down
67 changes: 67 additions & 0 deletions src/samples/Dockerfile.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
FROM amazonlinux:latest@sha256:66d22a2b8749283aa0764e6983fbb8bf8c143511cf16c9773598acca2a1cd6d2

COPY .nvmrc ./

# Download and install node
RUN curl -fsSL https://rpm.nodesource.com/setup_$(cat .nvmrc | cut -d. -f1).x | bash - \
&& yum clean metadata \
&& yum -y update-minimal \
&& yum -y install nodejs-$(cat .nvmrc)-1nodesource \
&& yum clean all \
&& rm -rf /var/cache/yum \
# smoke tests
&& node --version \
&& npm --version

# Install other dependencies
RUN \
yum -y update-minimal \
&& yum -y install \
# Common util
curl \
# To make pnpm install work
gcc-c++ make python3 \
# Runtime debugging
psmisc \
&& yum clean all \
&& rm -rf /var/cache/yum

RUN node --version

WORKDIR /app

# Files required by pnpm install
COPY .npmrc package.json pnpm-lock.yaml ./
COPY ./patches ./patches

# Install package manager
RUN corepack enable

# Forces pnpm to only install production dependencies and prevents pnpm post install error from husky
ARG NODE_ENV=production

# Mount the npmrc file containing the npm token with permission to read private packages
RUN --mount=type=secret,id=npmrc,dst=/root/.npmrc npm run ci:install

# Copy your dist and source files here, set NODE_CONFIG_DIR, etc.
COPY ./dist/ .

COPY ./dist/config ./config/app/
COPY ./src/config ./config/
ENV NODE_CONFIG_DIR=./config


ARG VERSION=0.0.1
ENV VERSION=${VERSION}

ENV PORT=80
EXPOSE 80

# Tell the app to listen on this port.
ENV DOCKER_APP_PORT=8080

# Setup docker healthcheck
COPY ./scripts/docker-healthcheck.sh ./
HEALTHCHECK --start-period=300s CMD ./docker-healthcheck.sh

CMD node ./main.js
14 changes: 14 additions & 0 deletions src/samples/docker-build.sh.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/bash
set -euox pipefail

cd $(dirname "${BASH_SOURCE[0]}")
cd ../../

pnpm ci:install
pnpm exec nx build <%SERVICE_NAME%>

# To use `--secrets` it requires BuildKit. Enabling this.
# https://docs.docker.com/develop/develop-images/build_enhancements/#to-enable-buildkit-builds
export DOCKER_BUILDKIT=1

exec docker build --secret id=npmrc,src=$HOME/.npmrc -t <%SERVICE_NAME%>:1.0 -f ./Dockerfile . $@
14 changes: 14 additions & 0 deletions src/samples/docker-healthcheck.sh.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/bash
set -e -o pipefail
cd $(dirname "${BASH_SOURCE[0]}")

# Perform basic healthcheck: Check that the express service is running and can
# response on basic http request. We are not checking the full health and only
# checking that node is up and running. We must check the response status AND
# the content, since our backend returns a gif for any non-defined URL.

# TODO: Do a curl against your service, check for thumbs up like so:
curl --fail http://localhost:80/<%SERVICE_HEALTHCHECK_PATH%> | grep "Thumbs up" || exit 1

# TODO: Set up worker-type healthchecks in case ECS does not have load balancer
# attached to it which performs full healthcheck.
Loading