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

Add to production sample #132

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions production/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
certs
4 changes: 3 additions & 1 deletion production/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
lib
node_modules
workflow-bundle.js
workflow-bundle.js
workflow-bundle.js.map
certs
20 changes: 20 additions & 0 deletions production/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# syntax=docker/dockerfile:1

FROM node:16-bullseye-slim

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"
Copy link
Member

Choose a reason for hiding this comment

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

Why put these in args?
It's typically something you'd provide at container run time, not bake into the image since this image could be used in different environments.

ENV TEMPORAL_SERVER=$TEMPORAL_SERVER

ARG NAMESPACE="default"
ENV NAMESPACE=$NAMESPACE

COPY . .
CMD [ "node", "lib/worker.js" ]
28 changes: 26 additions & 2 deletions production/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,29 @@ 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 -p 3000:3000 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:

- 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
```
1 change: 1 addition & 0 deletions production/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
]
},
"dependencies": {
"micri": "^4.5.0",
"@temporalio/activity": "^1.0.0-rc.1",
"@temporalio/client": "^1.0.0-rc.1",
"@temporalio/worker": "^1.0.0-rc.1",
Expand Down
7 changes: 3 additions & 4 deletions production/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { Connection, WorkflowClient } from '@temporalio/client';
import { connectionOptions, namespace } from './connection';
import { example } from './workflows';

async function run() {
const connection = await Connection.connect(); // 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 = await Connection.connect(connectionOptions);

const client = new WorkflowClient({
connection,
// In production you will likely specify `namespace` here; it is 'default' if omitted
namespace,
});

const result = await client.execute(example, {
Expand Down
37 changes: 37 additions & 0 deletions production/src/connection.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
20 changes: 20 additions & 0 deletions production/src/errors.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>).code === 'string'
);
}

export function getErrorCode(error: unknown) {
if (isErrorWithCode(error)) return error.code;
}

export function fileNotFound(error: unknown) {
return getErrorCode(error) === 'ENOENT';
}
23 changes: 22 additions & 1 deletion production/src/worker.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Worker } from '@temporalio/worker';
import { NativeConnection, Worker } from '@temporalio/worker';
import { serve } from 'micri';
import * as activities from './activities';
import { connectionOptions, namespace } from './connection';

// @@@SNIPSTART typescript-production-worker
const workflowOption = () =>
Expand All @@ -13,12 +15,31 @@ const workflowOption = () =>
: { workflowsPath: require.resolve('./workflows') };

async function run() {
console.log('connectionOptions:', connectionOptions);
const connection = await NativeConnection.connect(connectionOptions);

const worker = await Worker.create({
connection,
namespace,
...workflowOption(),
activities,
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
Expand Down