From 57b45d62b923ef903b4cee03286aca3e90918111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loren=20=F0=9F=A4=93?= Date: Tue, 12 Apr 2022 15:17:10 -0500 Subject: [PATCH 1/7] Add Dockerfile and shared connectionOptions --- production/.dockerignore | 1 + production/.gitignore | 3 ++- production/Dockerfile | 20 +++++++++++++++++++ production/README.md | 20 +++++++++++++++++-- production/src/client.ts | 7 +++---- production/src/connection.ts | 37 ++++++++++++++++++++++++++++++++++++ production/src/errors.ts | 20 +++++++++++++++++++ production/src/worker.ts | 7 ++++++- 8 files changed, 107 insertions(+), 8 deletions(-) create mode 100644 production/.dockerignore create mode 100644 production/Dockerfile create mode 100644 production/src/connection.ts create mode 100644 production/src/errors.ts diff --git a/production/.dockerignore b/production/.dockerignore new file mode 100644 index 00000000..b512c09d --- /dev/null +++ b/production/.dockerignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/production/.gitignore b/production/.gitignore index 6f4292a8..9a8134df 100644 --- a/production/.gitignore +++ b/production/.gitignore @@ -1,3 +1,4 @@ lib node_modules -workflow-bundle.js \ No newline at end of file +workflow-bundle.js +certs \ No newline at end of file diff --git a/production/Dockerfile b/production/Dockerfile new file mode 100644 index 00000000..64b29533 --- /dev/null +++ b/production/Dockerfile @@ -0,0 +1,20 @@ +# syntax=docker/dockerfile:1 + +FROM node:16.14.2-bullseye + +RUN apt update && apt install -y ca-certificates + +ENV NODE_ENV=production +WORKDIR /app + +COPY ["package.json", "./"] +RUN npm install --production + +ARG TEMPORAL_SERVER="host.docker.internal:7233" +ENV TEMPORAL_SERVER=$TEMPORAL_SERVER + +ARG NAMESPACE="default" +ENV NAMESPACE=$NAMESPACE + +COPY . . +CMD [ "node", "lib/worker.js" ] \ No newline at end of file diff --git a/production/README.md b/production/README.md index 33a6bd98..a945ff68 100644 --- a/production/README.md +++ b/production/README.md @@ -23,5 +23,21 @@ Hello, Temporal! ### Running this sample in production 1. `npm run build` to build the Worker script and Activities code. -1. `npm run build:workflow` to build the Workflow code bundle. -1. `NODE_ENV=production node lib/worker.js` to run the production Worker. +2. `npm run build:workflow` to build the Workflow code bundle. +3. `NODE_ENV=production node lib/worker.js` to run the production Worker. + +If you use Docker in production, replace step 3 with: + +``` +docker build . --tag my-temporal-worker --build-arg TEMPORAL_SERVER=host.docker.internal:7233 +docker run my-temporal-worker +``` + +### Connecting to deployed Temporal Server + +We use [`src/connection.ts`](./src/connection.ts) for connecting to Temporal Server from both the Client and Worker. When connecting to Temporal Server running on our local machine, the defaults (`localhost:7233` for `node lib/worker.js` and `host.docker.internal:7233` for Docker) work. When connecting to a production Temporal Server, we need to: + +- Put the TLS certificate in `certs/server.pem` +- Put the TLS private key in `certs/server.key` +- Provide the GRPC endpoint, like `TEMPORAL_SERVER=loren.temporal-dev.tmprl.cloud:7233` +- Provide the namespace, like `NAMESPACE=loren.temporal-dev` diff --git a/production/src/client.ts b/production/src/client.ts index eaecc266..eebbf665 100644 --- a/production/src/client.ts +++ b/production/src/client.ts @@ -1,13 +1,12 @@ import { Connection, WorkflowClient } from '@temporalio/client'; +import { connectionOptions, namespace } from './connection'; import { example } from './workflows'; async function run() { - const connection = new Connection(); // Connect to localhost with default ConnectionOptions. - // In production, pass options to the Connection constructor to configure TLS and other settings. - // This is optional but we leave this here to remind you there is a gRPC connection being established. + const connection = new Connection(connectionOptions); const client = new WorkflowClient(connection.service, { - // In production you will likely specify `namespace` here; it is 'default' if omitted + namespace, }); const result = await client.execute(example, { diff --git a/production/src/connection.ts b/production/src/connection.ts new file mode 100644 index 00000000..0ca661c0 --- /dev/null +++ b/production/src/connection.ts @@ -0,0 +1,37 @@ +import { readFileSync } from 'fs'; +import { fileNotFound } from './errors'; + +const { TEMPORAL_SERVER, NODE_ENV = 'development', NAMESPACE = 'default' } = process.env; + +export { NAMESPACE as namespace }; + +const isDeployed = ['production', 'staging'].includes(NODE_ENV); + +interface ConnectionOptions { + address: string; + tls?: { clientCertPair: { crt: Buffer; key: Buffer } }; +} + +export const connectionOptions: ConnectionOptions = { + address: TEMPORAL_SERVER || 'localhost:7233', +}; + +if (isDeployed) { + try { + const crt = readFileSync('./certs/server.pem'); + const key = readFileSync('./certs/server.key'); + + if (crt && key) { + connectionOptions.tls = { + clientCertPair: { + crt, + key, + }, + }; + } + } catch (e) { + if (!fileNotFound(e)) { + throw e; + } + } +} diff --git a/production/src/errors.ts b/production/src/errors.ts new file mode 100644 index 00000000..98354173 --- /dev/null +++ b/production/src/errors.ts @@ -0,0 +1,20 @@ +type ErrorWithCode = { + code: string; +}; + +function isErrorWithCode(error: unknown): error is ErrorWithCode { + return ( + typeof error === 'object' && + error !== null && + 'code' in error && + typeof (error as Record).code === 'string' + ); +} + +export function getErrorCode(error: unknown) { + if (isErrorWithCode(error)) return error.code; +} + +export function fileNotFound(error: unknown) { + return getErrorCode(error) === 'ENOENT'; +} diff --git a/production/src/worker.ts b/production/src/worker.ts index 2e6e45d2..09334f62 100644 --- a/production/src/worker.ts +++ b/production/src/worker.ts @@ -1,5 +1,6 @@ -import { Worker } from '@temporalio/worker'; +import { NativeConnection, Worker } from '@temporalio/worker'; import * as activities from './activities'; +import { connectionOptions, namespace } from './connection'; // @@@SNIPSTART typescript-production-worker const workflowOption = () => @@ -8,7 +9,11 @@ const workflowOption = () => : { workflowsPath: require.resolve('./workflows') }; async function run() { + const connection = await NativeConnection.create(connectionOptions); + const worker = await Worker.create({ + connection, + namespace, ...workflowOption(), activities, taskQueue: 'production-sample', From 649c4f975ae86a11e5229baef946267145af458c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loren=20=F0=9F=A4=93?= Date: Tue, 12 Apr 2022 22:08:58 -0500 Subject: [PATCH 2/7] Add HTTP server to provide status --- production/README.md | 2 +- production/package.json | 3 ++- production/src/worker.ts | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/production/README.md b/production/README.md index a945ff68..163c0846 100644 --- a/production/README.md +++ b/production/README.md @@ -30,7 +30,7 @@ If you use Docker in production, replace step 3 with: ``` docker build . --tag my-temporal-worker --build-arg TEMPORAL_SERVER=host.docker.internal:7233 -docker run my-temporal-worker +docker run -p 3000:3000 my-temporal-worker ``` ### Connecting to deployed Temporal Server diff --git a/production/package.json b/production/package.json index 3900f49c..4a332c36 100644 --- a/production/package.json +++ b/production/package.json @@ -21,7 +21,8 @@ ] }, "dependencies": { - "temporalio": "0.20.x" + "micri": "^4.5.0", + "temporalio": "^0.20.1" }, "devDependencies": { "@tsconfig/node16": "^1.0.0", diff --git a/production/src/worker.ts b/production/src/worker.ts index 09334f62..33e6e371 100644 --- a/production/src/worker.ts +++ b/production/src/worker.ts @@ -1,4 +1,5 @@ import { NativeConnection, Worker } from '@temporalio/worker'; +import { serve } from 'micri'; import * as activities from './activities'; import { connectionOptions, namespace } from './connection'; @@ -19,6 +20,20 @@ async function run() { taskQueue: 'production-sample', }); + const server = serve(async () => { + return worker.getStatus(); + }); + + server.listen(process.env.PORT || 3000); + + server.on('error', (err) => { + console.error(err); + }); + + for (const signal of ['SIGINT', 'SIGTERM', 'SIGQUIT', 'SIGUSR2']) { + process.on(signal, () => server.close()); + } + await worker.run(); } // @@@SNIPEND From 2c647301eadc2b546cd6ebf6bdf61950348f56f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loren=20=F0=9F=A4=93?= Date: Tue, 12 Apr 2022 22:46:34 -0500 Subject: [PATCH 3/7] Don't copy certs to image on build --- production/.dockerignore | 3 ++- production/README.md | 12 ++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/production/.dockerignore b/production/.dockerignore index b512c09d..d3e62887 100644 --- a/production/.dockerignore +++ b/production/.dockerignore @@ -1 +1,2 @@ -node_modules \ No newline at end of file +node_modules +certs \ No newline at end of file diff --git a/production/README.md b/production/README.md index 163c0846..9bcaa0e0 100644 --- a/production/README.md +++ b/production/README.md @@ -37,7 +37,15 @@ docker run -p 3000:3000 my-temporal-worker We use [`src/connection.ts`](./src/connection.ts) for connecting to Temporal Server from both the Client and Worker. When connecting to Temporal Server running on our local machine, the defaults (`localhost:7233` for `node lib/worker.js` and `host.docker.internal:7233` for Docker) work. When connecting to a production Temporal Server, we need to: -- Put the TLS certificate in `certs/server.pem` -- Put the TLS private key in `certs/server.key` - Provide the GRPC endpoint, like `TEMPORAL_SERVER=loren.temporal-dev.tmprl.cloud:7233` - Provide the namespace, like `NAMESPACE=loren.temporal-dev` +- Put the TLS certificate in `certs/server.pem` +- Put the TLS private key in `certs/server.key` +- If using Docker, mount `certs/` into the container by adding `--mount type=bind,source="$(pwd)"/certs,target=/app/certs` to `docker run` + +With Docker, the full commands would be: + +``` +docker build . --tag my-temporal-worker --build-arg TEMPORAL_SERVER=loren.temporal-dev.tmprl.cloud:7233 --build-arg NAMESPACE=loren.temporal-dev +docker run -p 3000:3000 --mount type=bind,source="$(pwd)"/certs,target=/app/certs my-temporal-worker +``` From 9e5c308a3b017da039fda6ccd5e567e474a009d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loren=20=F0=9F=A4=93?= Date: Wed, 13 Apr 2022 10:43:51 -0500 Subject: [PATCH 4/7] Use slim --- production/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/production/Dockerfile b/production/Dockerfile index 64b29533..46aa51f3 100644 --- a/production/Dockerfile +++ b/production/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 -FROM node:16.14.2-bullseye +FROM node:16-bullseye-slim RUN apt update && apt install -y ca-certificates From 5681ae807cf214062c0a0f3773749edc73be0d45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loren=20=F0=9F=A4=93?= Date: Wed, 20 Jul 2022 20:40:27 -0400 Subject: [PATCH 5/7] Merge branch 'main' into docker From 5fa93053152b2ddead6ee075f4b4cb03042b642b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loren=20=F0=9F=A4=93?= Date: Wed, 20 Jul 2022 21:18:34 -0400 Subject: [PATCH 6/7] Add sourcemap to gitignore --- production/.gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/production/.gitignore b/production/.gitignore index 9a8134df..c60c025d 100644 --- a/production/.gitignore +++ b/production/.gitignore @@ -1,4 +1,5 @@ lib node_modules workflow-bundle.js +workflow-bundle.js.map certs \ No newline at end of file From 8841c6cc4963d3e31f240301db6629658185910a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Loren=20=F0=9F=A4=93?= Date: Fri, 9 Sep 2022 23:11:42 -0400 Subject: [PATCH 7/7] update --- production/src/client.ts | 5 +++-- production/src/worker.ts | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/production/src/client.ts b/production/src/client.ts index eebbf665..ec85459a 100644 --- a/production/src/client.ts +++ b/production/src/client.ts @@ -3,9 +3,10 @@ import { connectionOptions, namespace } from './connection'; import { example } from './workflows'; async function run() { - const connection = new Connection(connectionOptions); + const connection = await Connection.connect(connectionOptions); - const client = new WorkflowClient(connection.service, { + const client = new WorkflowClient({ + connection, namespace, }); diff --git a/production/src/worker.ts b/production/src/worker.ts index efd0e1a0..ff21b9ba 100644 --- a/production/src/worker.ts +++ b/production/src/worker.ts @@ -15,7 +15,8 @@ const workflowOption = () => : { workflowsPath: require.resolve('./workflows') }; async function run() { - const connection = await NativeConnection.create(connectionOptions); + console.log('connectionOptions:', connectionOptions); + const connection = await NativeConnection.connect(connectionOptions); const worker = await Worker.create({ connection,