Skip to content

Commit 666e842

Browse files
conico974Nicolas Dorseuil
and
Nicolas Dorseuil
authored
External middleware in cloudflare (#3254)
Co-authored-by: Nicolas Dorseuil <nicolas@gitbook.io>
1 parent df2fa42 commit 666e842

File tree

14 files changed

+616
-87
lines changed

14 files changed

+616
-87
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
name: Gradual Deploy to Cloudflare
2+
description: Use gradual deployment to deploy to Cloudflare. This action will upload the middleware and server versions to Cloudflare and kept them bound together
3+
inputs:
4+
apiToken:
5+
description: 'Cloudflare API token'
6+
required: true
7+
accountId:
8+
description: 'Cloudflare account ID'
9+
required: true
10+
environment:
11+
description: 'Cloudflare environment to deploy to (staging, production, preview)'
12+
required: true
13+
middlewareVersionId:
14+
description: 'Middleware version ID to deploy'
15+
required: true
16+
serverVersionId:
17+
description: 'Server version ID to deploy'
18+
required: true
19+
outputs:
20+
deployment-url:
21+
description: "Deployment URL"
22+
value: ${{ steps.deploy_middleware.outputs.deployment-url }}
23+
runs:
24+
using: 'composite'
25+
steps:
26+
- id: wrangler_status
27+
name: Check wrangler deployment status
28+
uses: cloudflare/wrangler-action@v3.14.0
29+
with:
30+
apiToken: ${{ inputs.apiToken }}
31+
accountId: ${{ inputs.accountId }}
32+
workingDirectory: ./
33+
wranglerVersion: '4.10.0'
34+
environment: ${{ inputs.environment }}
35+
command: deployments status --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
36+
37+
# This step is used to get the version ID that is currently deployed to Cloudflare.
38+
- id: extract_current_version
39+
name: Extract current version
40+
shell: bash
41+
run: |
42+
version_id=$(echo "${{ steps.wrangler_status.outputs.command-output }}" | grep -A 3 "(100%)" | grep -oP '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}')
43+
echo "version_id=$version_id" >> $GITHUB_OUTPUT
44+
45+
- id: deploy_server
46+
name: Deploy server to Cloudflare at 0%
47+
uses: cloudflare/wrangler-action@v3.14.0
48+
with:
49+
apiToken: ${{ inputs.apiToken }}
50+
accountId: ${{ inputs.accountId }}
51+
workingDirectory: ./
52+
wranglerVersion: '4.10.0'
53+
environment: ${{ inputs.environment }}
54+
command: versions deploy ${{ steps.extract_current_version.outputs.version_id }}@100% ${{ inputs.serverVersionId }}@0% -y --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
55+
56+
# Since we use version overrides headers, we can directly deploy the middleware to 100%.
57+
- id: deploy_middleware
58+
name: Deploy middleware to Cloudflare at 100%
59+
uses: cloudflare/wrangler-action@v3.14.0
60+
with:
61+
apiToken: ${{ inputs.apiToken }}
62+
accountId: ${{ inputs.accountId }}
63+
workingDirectory: ./
64+
wranglerVersion: '4.10.0'
65+
environment: ${{ inputs.environment }}
66+
command: versions deploy ${{ inputs.middlewareVersionId }}@100% -y --config ./packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc
67+
68+
- name: Deploy server to Cloudflare at 100%
69+
uses: cloudflare/wrangler-action@v3.14.0
70+
with:
71+
apiToken: ${{ inputs.apiToken }}
72+
accountId: ${{ inputs.accountId }}
73+
workingDirectory: ./
74+
wranglerVersion: '4.10.0'
75+
environment: ${{ inputs.environment }}
76+
command: versions deploy ${{ inputs.serverVersionId }}@100% -y --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
77+
78+
- name: Outputs
79+
shell: bash
80+
env:
81+
DEPLOYMENT_URL: ${{ steps.deploy_middleware.outputs.deployment-url }}
82+
run: |
83+
echo "URL: ${{ steps.deploy_middleware.outputs.deployment-url }}"

.github/composite/deploy-cloudflare/action.yaml

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ inputs:
2828
outputs:
2929
deployment-url:
3030
description: "Deployment URL"
31-
value: ${{ steps.deploy.outputs.deployment-url }}
31+
value: ${{ steps.upload_middleware.outputs.deployment-url }}
3232
runs:
3333
using: 'composite'
3434
steps:
@@ -63,30 +63,66 @@ runs:
6363
env:
6464
GITBOOK_RUNTIME: cloudflare
6565
shell: bash
66-
- id: deploy
67-
name: Deploy to Cloudflare
66+
67+
- id: upload_server
68+
name: Upload server to Cloudflare
6869
uses: cloudflare/wrangler-action@v3.14.0
6970
with:
7071
apiToken: ${{ inputs.apiToken }}
7172
accountId: ${{ inputs.accountId }}
7273
workingDirectory: ./
7374
wranglerVersion: '4.10.0'
7475
environment: ${{ inputs.environment }}
75-
command: ${{ inputs.deploy == 'true' && 'deploy' || format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/wrangler.jsonc
76-
77-
- name: Temporary deploy server CF worker
76+
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc
77+
78+
- name: Extract server version worker ID
79+
shell: bash
80+
id: extract_server_version_id
81+
run: |
82+
version_id=$(echo '${{ steps.upload_server.outputs.command-output }}' | grep "Worker Version ID" | awk '{print $4}')
83+
echo "version_id=$version_id" >> $GITHUB_OUTPUT
84+
85+
- name: Run updateWrangler scripts
86+
shell: bash
87+
run: |
88+
bun run ./packages/gitbook-v2/openNext/customWorkers/script/updateWrangler.ts ${{ steps.extract_server_version_id.outputs.version_id }}
89+
90+
- id: upload_middleware
91+
name: Upload middleware to Cloudflare
7892
uses: cloudflare/wrangler-action@v3.14.0
7993
with:
8094
apiToken: ${{ inputs.apiToken }}
8195
accountId: ${{ inputs.accountId }}
8296
workingDirectory: ./
8397
wranglerVersion: '4.10.0'
8498
environment: ${{ inputs.environment }}
85-
command: 'deploy --config ./packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc'
99+
command: ${{ format('versions upload --tag {0} --message "{1}"', inputs.commitTag, inputs.commitMessage) }} --config ./packages/gitbook-v2/openNext/customWorkers/middlewareWrangler.jsonc
86100

101+
- name: Extract middleware version worker ID
102+
shell: bash
103+
id: extract_middleware_version_id
104+
run: |
105+
version_id=$(echo '${{ steps.upload_middleware.outputs.command-output }}' | grep "Worker Version ID" | awk '{print $4}')
106+
echo "version_id=$version_id" >> $GITHUB_OUTPUT
107+
108+
- name: Deploy server and middleware to Cloudflare
109+
if: ${{ inputs.deploy == 'true' }}
110+
uses: ./.github/actions/gradual-deploy-cloudflare
111+
with:
112+
apiToken: ${{ inputs.apiToken }}
113+
accountId: ${{ inputs.accountId }}
114+
opServiceAccount: ${{ inputs.opServiceAccount }}
115+
opItem: ${{ inputs.opItem }}
116+
environment: ${{ inputs.environment }}
117+
serverVersionId: ${{ steps.extract_server_version_id.outputs.version_id }}
118+
middlewareVersionId: ${{ steps.extract_middleware_version_id.outputs.version_id }}
119+
deploy: ${{ inputs.deploy }}
120+
121+
87122
- name: Outputs
88123
shell: bash
89124
env:
90-
DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-url }}
125+
DEPLOYMENT_URL: ${{ steps.upload_middleware.outputs.deployment-url }}
91126
run: |
92-
echo "URL: ${{ steps.deploy.outputs.deployment-url }}"
127+
echo "URL: ${{ steps.upload_middleware.outputs.deployment-url }}"
128+
echo "Output server: ${{ steps.upload_server.outputs.command-output }}"
Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,29 @@
1-
import { defineCloudflareConfig } from '@opennextjs/cloudflare';
2-
import doShardedTagCache from '@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache';
3-
import {
4-
softTagFilter,
5-
withFilter,
6-
} from '@opennextjs/cloudflare/overrides/tag-cache/tag-cache-filter';
1+
import type { OpenNextConfig } from '@opennextjs/cloudflare';
72

8-
export default defineCloudflareConfig({
9-
incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default),
10-
tagCache: withFilter({
11-
tagCache: doShardedTagCache({
12-
baseShardSize: 12,
13-
regionalCache: true,
14-
shardReplication: {
15-
numberOfSoftReplicas: 2,
16-
numberOfHardReplicas: 1,
17-
},
18-
}),
19-
// We don't use `revalidatePath`, so we filter out soft tags
20-
filterFn: softTagFilter,
21-
}),
22-
queue: () => import('./openNext/queue').then((m) => m.default),
23-
24-
// Performance improvements as we don't use PPR
25-
enableCacheInterception: true,
26-
});
3+
export default {
4+
default: {
5+
override: {
6+
wrapper: 'cloudflare-node',
7+
converter: 'edge',
8+
proxyExternalRequest: 'fetch',
9+
queue: () => import('./openNext/queue/server').then((m) => m.default),
10+
incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default),
11+
tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default),
12+
},
13+
},
14+
middleware: {
15+
external: true,
16+
override: {
17+
wrapper: 'cloudflare-edge',
18+
converter: 'edge',
19+
proxyExternalRequest: 'fetch',
20+
queue: () => import('./openNext/queue/middleware').then((m) => m.default),
21+
incrementalCache: () => import('./openNext/incrementalCache').then((m) => m.default),
22+
tagCache: () => import('./openNext/tagCache/middleware').then((m) => m.default),
23+
},
24+
},
25+
dangerous: {
26+
enableCacheInterception: true,
27+
},
28+
edgeExternals: ['node:crypto'],
29+
} satisfies OpenNextConfig;
Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1+
import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/init.js';
2+
13
export default {
2-
async fetch() {
3-
return new Response('Hello World', {
4-
headers: {
5-
'Content-Type': 'text/plain',
6-
},
4+
async fetch(request, env, ctx) {
5+
return runWithCloudflareRequestContext(request, env, ctx, async () => {
6+
// We can't move the handler import to the top level, otherwise the runtime will not be properly initialized
7+
const { handler } = await import(
8+
'../../.open-next/server-functions/default/handler.mjs'
9+
);
10+
11+
// - `Request`s are handled by the Next server
12+
return handler(request, env, ctx);
713
});
814
},
915
};

packages/gitbook-v2/openNext/customWorkers/defaultWrangler.jsonc

Lines changed: 87 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,100 @@
1717
"dev": {
1818
"vars": {
1919
"STAGE": "dev"
20-
}
20+
},
21+
"r2_buckets": [
22+
{
23+
"binding": "NEXT_INC_CACHE_R2_BUCKET",
24+
"bucket_name": "gitbook-open-v2-cache-preview"
25+
}
26+
],
27+
"services": [
28+
{
29+
"binding": "WORKER_SELF_REFERENCE",
30+
"service": "gitbook-open-v2-server-dev"
31+
}
32+
]
2133
},
2234
"preview": {
2335
"vars": {
24-
"STAGE": "preview"
25-
}
36+
"STAGE": "preview",
37+
// Just as a test for the preview environment to check that everything works
38+
"NEXT_PRIVATE_DEBUG_CACHE": "true"
39+
},
40+
"r2_buckets": [
41+
{
42+
"binding": "NEXT_INC_CACHE_R2_BUCKET",
43+
"bucket_name": "gitbook-open-v2-cache-preview"
44+
}
45+
],
46+
"services": [
47+
{
48+
"binding": "WORKER_SELF_REFERENCE",
49+
"service": "gitbook-open-v2-server-preview"
50+
}
51+
]
52+
// No durable objects on preview, as they block the generation of preview URLs
53+
// and we don't need tags invalidation on preview
2654
},
2755
"staging": {
28-
"vars": {
29-
"STAGE": "staging"
30-
}
56+
"r2_buckets": [
57+
{
58+
"binding": "NEXT_INC_CACHE_R2_BUCKET",
59+
"bucket_name": "gitbook-open-v2-cache-staging"
60+
}
61+
],
62+
"services": [
63+
{
64+
"binding": "WORKER_SELF_REFERENCE",
65+
"service": "gitbook-open-v2-server-staging"
66+
}
67+
],
68+
"durable_objects": {
69+
"bindings": [
70+
// We do not need to define migrations for external DOs,
71+
// In fact, defining migrations for external DOs will crash
72+
{
73+
"name": "NEXT_TAG_CACHE_DO_SHARDED",
74+
"class_name": "DOShardedTagCache",
75+
"script_name": "gitbook-open-v2-staging"
76+
}
77+
]
78+
},
79+
"tail_consumers": [
80+
{
81+
"service": "gitbook-x-staging-tail"
82+
}
83+
]
3184
},
3285
"production": {
33-
"vars": {
34-
"STAGE": "production"
35-
}
86+
"r2_buckets": [
87+
{
88+
"binding": "NEXT_INC_CACHE_R2_BUCKET",
89+
"bucket_name": "gitbook-open-v2-cache-production"
90+
}
91+
],
92+
"services": [
93+
{
94+
"binding": "WORKER_SELF_REFERENCE",
95+
"service": "gitbook-open-v2-server-production"
96+
}
97+
],
98+
"durable_objects": {
99+
"bindings": [
100+
{
101+
// We do not need to define migrations for external DOs,
102+
// In fact, defining migrations for external DOs will crash
103+
"name": "NEXT_TAG_CACHE_DO_SHARDED",
104+
"class_name": "DOShardedTagCache",
105+
"script_name": "gitbook-open-v2-production"
106+
}
107+
]
108+
},
109+
"tail_consumers": [
110+
{
111+
"service": "gitbook-x-prod-tail"
112+
}
113+
]
36114
}
37115
}
38116
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { WorkerEntrypoint } from 'cloudflare:workers';
2+
import { runWithCloudflareRequestContext } from '../../.open-next/cloudflare/init.js';
3+
4+
import { handler as middlewareHandler } from '../../.open-next/middleware/handler.mjs';
5+
6+
export { DOQueueHandler } from '../../.open-next/.build/durable-objects/queue.js';
7+
8+
export { DOShardedTagCache } from '../../.open-next/.build/durable-objects/sharded-tag-cache.js';
9+
10+
export default class extends WorkerEntrypoint {
11+
async fetch(request) {
12+
return runWithCloudflareRequestContext(request, this.env, this.ctx, async () => {
13+
// - `Request`s are handled by the Next server
14+
const reqOrResp = await middlewareHandler(request, this.env, this.ctx);
15+
if (reqOrResp instanceof Response) {
16+
return reqOrResp;
17+
}
18+
19+
if (this.env.STAGE !== 'preview') {
20+
// https://developers.cloudflare.com/workers/configuration/versions-and-deployments/gradual-deployments/#version-affinity
21+
reqOrResp.headers.set(
22+
'Cloudflare-Workers-Version-Overrides',
23+
`gitbook-open-v2-${this.env.STAGE}="${this.env.WORKER_VERSION_ID}"`
24+
);
25+
return this.env.DEFAULT_WORKER?.fetch(reqOrResp, {
26+
cf: {
27+
cacheEverything: false,
28+
},
29+
});
30+
}
31+
// If we are in preview mode, we need to send the request to the preview URL
32+
const modifiedUrl = new URL(reqOrResp.url);
33+
modifiedUrl.hostname = this.env.PREVIEW_HOSTNAME;
34+
const nextRequest = new Request(modifiedUrl, reqOrResp);
35+
return fetch(nextRequest, {
36+
cf: {
37+
cacheEverything: false,
38+
},
39+
});
40+
});
41+
}
42+
}

0 commit comments

Comments
 (0)