-
Notifications
You must be signed in to change notification settings - Fork 67
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
websites-integration: init the new deployment system
- Loading branch information
Showing
22 changed files
with
1,053 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
name: Build about.daangn.com | ||
|
||
on: | ||
workflow_dispatch: | ||
inputs: | ||
deployment_id: | ||
description: Deployment ID | ||
type: string | ||
required: true | ||
bind_url: | ||
description: Bind URL from the websites-deployment worker | ||
type: string | ||
required: true | ||
callback_url: | ||
description: Callback URL from the websites-deployment worker | ||
type: string | ||
required: true | ||
|
||
jobs: | ||
build: | ||
name: Build | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Bind this to the deployment | ||
run: | | ||
curl -fsSL -X POST \ | ||
-H "Authorization: AdminKey ${{ secrets.WEBSITES_ADMIN_KEY }}" \ | ||
--json '{ "run_id": "${{ github.run_id }}" }' \ | ||
"${{ inputs.bind_url }}" | ||
- uses: actions/checkout@v4 | ||
|
||
- name: Setup Rclone | ||
uses: cometkim/rclone-actions/setup-rclone@main | ||
env: | ||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
|
||
- name: Configure Rclone | ||
run: | | ||
mkdir -p ~/.config/rclone | ||
{ | ||
echo "[r2]" | ||
echo "type = s3" | ||
echo "provider = Cloudflare" | ||
echo "access_key_id = ${{ secrets.CLOUDFLARE_R2_ACCESS_KEY_ID }}" | ||
echo "secret_access_key = ${{ secrets.CLOUDFLARE_R2_SECRET_ACCESS_KEY }}" | ||
echo "endpoint = https://${{ secrets.CLOUDFLARE_ACCOUNT_ID }}.r2.cloudflarestorage.com" | ||
echo "acl = private" | ||
} > ~/.config/rclone/rclone.conf | ||
- name: Setup Node.js | ||
uses: actions/setup-node@v4 | ||
with: | ||
node-version: 20 | ||
cache: yarn | ||
|
||
- name: Install Dependencies | ||
run: yarn install --immutable | ||
|
||
- name: Restore cache | ||
id: content-cache | ||
uses: actions/cache/restore@v4 | ||
with: | ||
path: | | ||
about.daangn.com/public | ||
about.daangn.com/.cache | ||
key: cache-about_daangn_com | ||
|
||
- name: Build about.daangn.com | ||
id: build | ||
run: yarn workspace about.daangn.com build | ||
|
||
- name: Build artifact | ||
run: | | ||
cd about.daangn.com && \ | ||
tar -cvf --use-compress-program="zstd -T0 --adapt --exclude-compressed" \ | ||
"public.tar.zst" \ | ||
"public/" | ||
- name: Upload artifact | ||
run: | | ||
cd about.daangn.com && \ | ||
rclone copyto --log-level INFO \ | ||
"public.tar.zst" \ | ||
"r2:websites-artifacts/about.daangn.com/${{ inputs.deployment_id }}.tar.zst" | ||
- name: Execute callback | ||
if: ${{ always() }} | ||
run: | | ||
curl -fsSL -X POST \ | ||
-H "Authorization: AdminKey ${{ secrets.WEBSITES_ADMIN_KEY }}" \ | ||
--json '{' \ | ||
--json '"run_id": "${{ github.run_id }}",' \ | ||
--json '"status": "${{ job.status }}",' \ | ||
--json '"artifact_name": "about.daangn.com/${{ inputs.deployment_id }}.tar.zst"' \ | ||
--json '}' \ | ||
"${{ inputs.callback_url }}" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
name: Deploy websites-integration | ||
|
||
on: | ||
workflow_dispatch: | ||
push: | ||
paths: | ||
- _workers/websites-integration/** | ||
- .github/workflows/deploy-websites-integration.yml | ||
branches: | ||
- main | ||
|
||
jobs: | ||
deploy: | ||
runs-on: ubuntu-latest | ||
name: Deploy | ||
steps: | ||
- uses: actions/checkout@v4 | ||
|
||
- name: Setup Node.js | ||
uses: actions/setup-node@v4 | ||
with: | ||
node-version: 20 | ||
cache: yarn | ||
|
||
- name: Install Dependencies | ||
run: yarn install --immutable | ||
|
||
- name: Deploy workers (Durable Objects only) | ||
run: yarn workspace websites-integration deploy:pages | ||
env: | ||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | ||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
|
||
- name: Deploy pages | ||
run: yarn workspace websites-integration deploy:pages | ||
env: | ||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | ||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,7 @@ | |
.ultra.cache.json | ||
.env | ||
.envrc | ||
.dev.vars | ||
|
||
*.DS_Store | ||
*.log | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
# 웹사이트 배포 구성 | ||
|
||
- Build: GitHub Actions | ||
- Deployment: Cloudflare Pages | ||
|
||
CI/CD 가 통합된 플랫폼 전체 기능을 있는 그대로 사용하는 대신 필요에 따라 커스텀한 구성을 사용합니다. | ||
|
||
## Motivation | ||
|
||
Cloudflare Pages나 Vercel 등 통합 플랫폼이 자체적으로 빌드 시스템을 제공하지만 몇몇 사이트는 제약사항 있는 그대로 사용하기에 한계가 있습니다. 대표적으로 `about.daangn.com` 의 경우 전체 700MB 정도의 아티팩트 사이즈, 빌드 과정 중 무거운 이미지 빌드가 많다보니 제약사항이 생깁니다. | ||
|
||
e.g) 발견된 제약사항 예시 | ||
|
||
- 클린 빌드와 캐시 빌드가 엄청나게 차이남 (거의 2분 vs 45분) | ||
- CI의 빌드 캐시 여부를 커스터마이징 할 수 없음 | ||
- CI가 빌드 중 메모리 제약으로 실패함 | ||
- CI가 빌드 중 시간을 너무 많이 써서 타임아웃으로 실패함 | ||
- 외부에서 빌드해서 업로드만 하는 것을 지원하지 않음 | ||
- 외부에서 빌드해서 업로드하는 것을 지원하나, 업로드 가능한 바이트 수에 제한이 있음 | ||
|
||
## Build Caching | ||
|
||
Cron 스케줄을 통해 매일 빌드 캐시를 만들어 Actions Cache로 저장합니다. | ||
|
||
컨텐츠 수명이 길어 빌드 빈도를 높게 가져가는 것이 큰 효용은 없습니다. | ||
|
||
반면, Actions Cache는 프로젝트 별로 10GB의 저장소 제약이 있습니다. 모든 웹사이트가 캐시를 유지하여도 충분하도록, 불변 캐시를 만드는 대신 매번 기존 캐시를 덮어 씁니다. | ||
|
||
## Configuration | ||
|
||
![Deployment Configuration Overview](images/deployment-configuration-overview.png) | ||
|
||
(Enterprise 도메인이 있는 경우) Cloudflare Pages를 주 플랫폼으로 사용합니다. | ||
|
||
구성을 위해 필요한 기능들(Deployment webhook, Custom build script, Branch preview)을 동등하게 제공하는 다른 배포 플랫폼을 사용할 수 있습니다. | ||
|
||
### Components | ||
|
||
Shared config: | ||
- R2 artifacts bucket | ||
- Cloudflare Worker for integration | ||
|
||
Per-site config: | ||
|
||
- GitHub Actions "cache" worfklow | ||
- GitHub Actions "build" workflow | ||
- Cloudflare Pages Project | ||
|
||
### How it works | ||
|
||
전체 사이트 배포의 세부적인 프로세스는 다음과 같습니다. | ||
|
||
```mermaid | ||
sequenceDiagram | ||
actor CMS | ||
participant Cloudflare Pages | ||
participant Deployment Awaiter | ||
participant Deployment Object | ||
participant GitHub Actions | ||
participant R2 Bucket | ||
CMS->>+Cloudflare Pages: Deploy hook | ||
Cloudflare Pages-->>CMS: 200 OK | ||
Cloudflare Pages->>+Deployment Awaiter: Initialize deployment | ||
Deployment Awaiter->>+Deployment Object: Spawn deployment object | ||
Deployment Object->>+GitHub Actions: Trigger build | ||
Deployment Object-->>Deployment Awaiter: Bind deployment | ||
loop every 5 secs | ||
Deployment Awaiter->>Deployment Object: Check build status | ||
Deployment Object-->>Deployment Awaiter: | ||
end | ||
GitHub Actions->>R2 Bucket: Upload artifacts | ||
GitHub Actions->>-Deployment Object: Notify build status | ||
Deployment Awaiter-->>-Cloudflare Pages: Release | ||
Cloudflare Pages->>R2 Bucket: Retrive artifacts | ||
R2 Bucket-->>Cloudflare Pages: | ||
Cloudflare Pages-->>-Cloudflare Pages: Extract & Batch artifacts | ||
``` | ||
|
||
동작 방식은 프로덕션과 브랜치 프리뷰가 동일합니다. |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export { Deployment } from './functions/$lib/objects/Deployment.ts'; | ||
|
||
export default {}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import { parseArgs } from 'node:util'; | ||
import { setInterval } from 'node:timers/promises'; | ||
|
||
import { $ } from 'zx'; | ||
|
||
const { | ||
WEBSITES_ADMIN_KEY, | ||
WEBSITES_INTEGRATION_ENDPOINT, | ||
|
||
// This script is available only on the Cloudflare Pages build, and some env vars will be injected from. | ||
// See https://developers.cloudflare.com/pages/configuration/build-configuration/ | ||
CF_PAGES, | ||
CF_PAGES_COMMIT_SHA, | ||
CF_PAGES_BRANCH, | ||
CF_PAGES_URL, | ||
} = process.env; | ||
|
||
if (CF_PAGES !== '1') { | ||
throw new Error("deployment-awaiter should be executed only on Cloudflare Pages' build"); | ||
} | ||
|
||
const baseUrl = new URL(WEBSITES_INTEGRATION_ENDPOINT); | ||
|
||
const { values } = parseArgs({ | ||
args: process.argv.slice(2), | ||
options: { | ||
workflowId: { | ||
type: 'string', | ||
}, | ||
timeout: { | ||
type: 'string', | ||
default: (10 * 1000 * 60).toString(), // 10 mins | ||
}, | ||
}, | ||
}); | ||
|
||
const params = { | ||
workflow_id: values.workflowId, | ||
ref: CF_PAGES_BRANCH, | ||
commit_sha: CF_PAGES_COMMIT_SHA, | ||
}; | ||
|
||
const initResponse = await fetch(new URL('/deployments', baseUrl), { | ||
method: 'POST', | ||
headers: { | ||
Authorization: `AdminKey ${WEBSITES_ADMIN_KEY}`, | ||
Accept: 'application/json', | ||
'Content-Type': 'application/json', | ||
}, | ||
body: JSON.stringify(params), | ||
}); | ||
const initData = await initResponse.json(); | ||
if (!initResponse.ok) { | ||
console.error({ status: initResponse.status, data: initData }); | ||
process.exit(1); | ||
} | ||
|
||
let state = initData.state; | ||
const checkUrl = new URL(initData.check_url); | ||
const artifactUrl = new URL(initData.artifact_url); | ||
|
||
const timeout = Number.parseInt(values.timeout); | ||
for await (const startTime of setInterval(5000, timeout)) { | ||
const now = Date.now(); | ||
if (now - startTime >= timeout) { | ||
console.error(`Timeout exceeded (${timeout} ms)`); | ||
process.exit(1); | ||
} | ||
|
||
const res = await fetch(checkUrl); | ||
const data = await res.json(); | ||
if (!res.ok) { | ||
console.error({ status: res.status, data }); | ||
process.exit(1); | ||
} | ||
|
||
state = data.state; | ||
if (state.type === 'IDLE') { | ||
throw new Error('invariant'); | ||
} | ||
if (state.type === 'IN_PROGRESS') { | ||
continue; | ||
} | ||
if (state.type === 'DONE') { | ||
if (state.status === 'failure') { | ||
console.error( | ||
`Workflow run failed: https://github.com/daangn/websites/actions/runs/${state.runId}`, | ||
); | ||
process.exit(1); | ||
} | ||
if (state.status === 'cancelled') { | ||
console.error( | ||
`Workflow run cancelled: https://github.com/daangn/websites/actions/runs/${state.runId}`, | ||
); | ||
process.exit(1); | ||
} | ||
if (state.status === 'success') { | ||
break; | ||
} | ||
} | ||
} | ||
|
||
console.log('Downloading artifact...'); | ||
await $`curl -fL -H "Authorization: ${WEBSITES_ADMIN_KEY}" "${artifactUrl.toString()}" -o public.tar.zst`; | ||
|
||
console.log('Extracting artifact...'); | ||
await $`tar --use-compress-program="zstd -d" -xvf public.tar.zst`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
interface Env { | ||
/** | ||
* A shared authorization key for websites administrators | ||
*/ | ||
WEBSITES_ADMIN_KEY: string; | ||
|
||
/** | ||
* A GitHub fine-grained token with the following permissions | ||
* - "Actions" repository permissions (write) | ||
* | ||
* @see https://docs.github.com/en/rest/actions/workflows?apiVersion=2022-11-28#create-a-workflow-dispatch-event | ||
*/ | ||
GITHUB_API_TOKEN: string; | ||
|
||
/** | ||
* R2 bucket binding for build artifacts | ||
*/ | ||
ARTIFACTS: R2Bucket; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export function json(body: unknown, init?: ResponseInit): Response { | ||
const headers = new Headers(init?.headers); | ||
headers.set('Content-Type', 'application/json'); | ||
|
||
return new Response(JSON.stringify(body), { ...init, headers }); | ||
} |
Oops, something went wrong.