perf: 프로필 공용 정적 자산 경로를 제공한다
Caddy가 commit-digest 공용 asset과 sourcemap을 immutable URL로 제공하고, runtime에 공용 공개 경로를 명시한다.
This commit is contained in:
@@ -152,12 +152,13 @@ Caddy의 runtime logger는 reverse proxy 오류에도 요청 정보를 남길
|
|||||||
|
|
||||||
## 프런트엔드 캐시 정책
|
## 프런트엔드 캐시 정책
|
||||||
|
|
||||||
Gateway와 profile frontend의 Vite build는 `/gateway/assets/`,
|
Gateway frontend의 Vite build는 `/gateway/assets/`, profile 공용 Vite build는
|
||||||
`/<profile>/assets/` 아래 파일명에 8자리 content hash를 붙입니다. 내부 Caddy는
|
`/gateway/profile-assets/<commit-digest>/assets/` 아래 파일명에 8자리 content
|
||||||
이 형식을 만족하는 JS, CSS, source map과 기타 build asset에
|
hash를 붙입니다. 내부 Caddy는 이 형식을 만족하는 JS, CSS, source map과 기타 build asset에
|
||||||
`Cache-Control: public, max-age=31536000, immutable`을 적용합니다. 새 build는
|
`Cache-Control: public, max-age=31536000, immutable`을 적용합니다. 새 build는
|
||||||
내용이 달라지면 URL도 달라지므로 배포와 rollback 뒤에도 기존 URL의 장기 cache를
|
내용이 달라지면 URL도 달라지므로 배포와 rollback 뒤에도 기존 URL의 장기 cache를
|
||||||
재사용할 수 있습니다.
|
재사용할 수 있습니다. 같은 commit을 쓰는 일곱 profile은 같은 공용 URL을 사용하므로
|
||||||
|
브라우저가 profile을 옮겨도 이미 받은 JS, CSS와 source map을 다시 전송하지 않습니다.
|
||||||
|
|
||||||
`index.html`, router fallback, `terms.*.html`처럼 URL이 고정된 HTML은 이 정책에
|
`index.html`, router fallback, `terms.*.html`처럼 URL이 고정된 HTML은 이 정책에
|
||||||
포함하지 않습니다. Caddy는 이 응답에 `Cache-Control: no-cache`를 적용하여
|
포함하지 않습니다. Caddy는 이 응답에 `Cache-Control: no-cache`를 적용하여
|
||||||
@@ -168,13 +169,20 @@ content-hashed URL로 옮겨야 합니다.
|
|||||||
|
|
||||||
## 정적 frontend와 격리 builder
|
## 정적 frontend와 격리 builder
|
||||||
|
|
||||||
운영 frontend는 `vite preview`로 제공하지 않습니다. Gateway와 profile build는
|
운영 frontend는 `vite preview`로 제공하지 않습니다. Gateway build와 profile 공용
|
||||||
`frontend-artifacts` named volume의 commit/digest 기반 불변 디렉터리에 복사되고,
|
build는 `frontend-artifacts` named volume의 commit/digest 기반 불변 디렉터리에 복사되고,
|
||||||
runtime은 검증된 release의 `current` 심볼릭 링크만 원자적으로 교체합니다. Caddy는
|
runtime은 검증된 release의 `current` 심볼릭 링크만 원자적으로 교체합니다. Caddy는
|
||||||
이 volume을 read-only로 mount하여 직접 제공합니다. 첫 전환과 명시적 개발 모드를
|
이 volume을 read-only로 mount하여 직접 제공합니다. 첫 전환과 명시적 개발 모드를
|
||||||
위한 preview reverse-proxy fallback은 남아 있지만 운영 PM2에는 Vite process가
|
위한 preview reverse-proxy fallback은 남아 있지만 운영 PM2에는 Vite process가
|
||||||
없습니다.
|
없습니다.
|
||||||
|
|
||||||
|
Profile Vite bundle은 commit과 공개 환경이 같으면 한 번만 생성되어
|
||||||
|
`game-assets/releases/<commit-digest>`에 게시됩니다. 각 profile의 `current`에는
|
||||||
|
profile base path, API/SSE URL과 Gateway URL을 담은 JSON `<script>`가 삽입된 작은
|
||||||
|
`index.html`, `deployment-version.json`과 manifest만 둡니다. 따라서 profile별 설정은
|
||||||
|
runtime에 결정하면서도 content hash asset과 sourcemap URL은 모두 공유합니다. 이전
|
||||||
|
profile 전용 정적 artifact도 전환·rollback 호환을 위해 계속 읽을 수 있습니다.
|
||||||
|
|
||||||
`builder` service는 Core source/worktree volume만 공유하고 release build를 한 번에
|
`builder` service는 Core source/worktree volume만 공유하고 release build를 한 번에
|
||||||
하나씩 실행합니다. DB, Redis, game token, OAuth 비밀값과 Docker socket은 받지
|
하나씩 실행합니다. DB, Redis, game token, OAuth 비밀값과 Docker socket은 받지
|
||||||
않으며, runtime은 공개 `VITE_*`와 heap/thread/cache 설정만 build 요청에
|
않으며, runtime은 공개 `VITE_*`와 heap/thread/cache 설정만 build 요청에
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
# Defer this header so it replaces vite preview's default no-cache value.
|
# Defer this header so it replaces vite preview's default no-cache value.
|
||||||
@immutableFrontendAssets path_regexp immutableFrontendAssets ^/(gateway|che|kwe|pwe|twe|nya|pya|hwe)/assets/.+-[A-Za-z0-9_-]{8}\.[^/]+$
|
@immutableFrontendAssets path_regexp immutableFrontendAssets ^/(gateway|che|kwe|pwe|twe|nya|pya|hwe)/assets/.+-[A-Za-z0-9_-]{8}\.[^/]+$
|
||||||
header @immutableFrontendAssets >Cache-Control "public, max-age=31536000, immutable"
|
header @immutableFrontendAssets >Cache-Control "public, max-age=31536000, immutable"
|
||||||
|
@immutableSharedProfileAssets path_regexp immutableSharedProfileAssets ^/gateway/profile-assets/[0-9a-f]{40,64}-[0-9a-f]{16}/assets/.+-[A-Za-z0-9_-]{8}\.[^/]+$
|
||||||
|
header @immutableSharedProfileAssets >Cache-Control "public, max-age=31536000, immutable"
|
||||||
@frontendRequests path /gateway/* /che/* /kwe/* /pwe/* /twe/* /nya/* /pya/* /hwe/*
|
@frontendRequests path /gateway/* /che/* /kwe/* /pwe/* /twe/* /nya/* /pya/* /hwe/*
|
||||||
header @frontendRequests Cache-Control "no-cache"
|
header @frontendRequests Cache-Control "no-cache"
|
||||||
|
|
||||||
@@ -42,6 +44,12 @@
|
|||||||
uri strip_prefix /gateway/api
|
uri strip_prefix /gateway/api
|
||||||
reverse_proxy runtime:15001
|
reverse_proxy runtime:15001
|
||||||
}
|
}
|
||||||
|
@sharedProfileAsset path_regexp sharedProfileAsset ^/gateway/profile-assets/[0-9a-f]{40,64}-[0-9a-f]{16}/assets/.+$
|
||||||
|
handle @sharedProfileAsset {
|
||||||
|
uri strip_prefix /gateway/profile-assets
|
||||||
|
root * /srv/frontend-artifacts/game-assets/releases
|
||||||
|
file_server
|
||||||
|
}
|
||||||
handle /gateway/* {
|
handle /gateway/* {
|
||||||
@gatewayPublished file {
|
@gatewayPublished file {
|
||||||
root /srv/frontend-artifacts/gateway/current
|
root /srv/frontend-artifacts/gateway/current
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ services:
|
|||||||
GATEWAY_FRONTEND_PORT: '15000'
|
GATEWAY_FRONTEND_PORT: '15000'
|
||||||
FRONTEND_SERVE_MODE: static
|
FRONTEND_SERVE_MODE: static
|
||||||
FRONTEND_ARTIFACT_ROOT: /srv/frontend-artifacts
|
FRONTEND_ARTIFACT_ROOT: /srv/frontend-artifacts
|
||||||
|
FRONTEND_SHARED_ASSET_PUBLIC_PATH: /gateway/profile-assets
|
||||||
FRONTEND_READINESS_ORIGIN: http://caddy
|
FRONTEND_READINESS_ORIGIN: http://caddy
|
||||||
RELEASE_BUILDER_URL: http://builder:15100
|
RELEASE_BUILDER_URL: http://builder:15100
|
||||||
RELEASE_BUILD_NODE_OPTIONS: ${BUILDER_NODE_OPTIONS:---max-old-space-size=3072}
|
RELEASE_BUILD_NODE_OPTIONS: ${BUILDER_NODE_OPTIONS:---max-old-space-size=3072}
|
||||||
|
|||||||
@@ -24,6 +24,26 @@ test('Caddy caches only content-hashed Vite frontend assets as immutable', async
|
|||||||
caddyfile,
|
caddyfile,
|
||||||
/header @immutableFrontendAssets >Cache-Control "public, max-age=31536000, immutable"/,
|
/header @immutableFrontendAssets >Cache-Control "public, max-age=31536000, immutable"/,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sharedMatcher = caddyfile.match(
|
||||||
|
/@immutableSharedProfileAssets path_regexp immutableSharedProfileAssets (\S+)/,
|
||||||
|
);
|
||||||
|
assert.ok(sharedMatcher, 'immutable shared profile asset matcher must exist');
|
||||||
|
const sharedAssetPath = new RegExp(sharedMatcher[1]);
|
||||||
|
const releaseId = `${'a'.repeat(40)}-${'b'.repeat(16)}`;
|
||||||
|
assert.equal(
|
||||||
|
sharedAssetPath.test(`/gateway/profile-assets/${releaseId}/assets/index-CcXtLSVk.js`),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
sharedAssetPath.test(`/gateway/profile-assets/${releaseId}/assets/index-CcXtLSVk.js.map`),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
assert.equal(sharedAssetPath.test(`/gateway/profile-assets/${releaseId}/index.html`), false);
|
||||||
|
assert.match(
|
||||||
|
caddyfile,
|
||||||
|
/header @immutableSharedProfileAssets >Cache-Control "public, max-age=31536000, immutable"/,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Caddy serves published frontend artifacts directly and keeps a transition fallback', async () => {
|
test('Caddy serves published frontend artifacts directly and keeps a transition fallback', async () => {
|
||||||
@@ -47,4 +67,10 @@ test('Caddy serves published frontend artifacts directly and keeps a transition
|
|||||||
}
|
}
|
||||||
assert.match(caddyfile, /try_files \{path\} \/index\.html/);
|
assert.match(caddyfile, /try_files \{path\} \/index\.html/);
|
||||||
assert.match(caddyfile, /header @frontendRequests Cache-Control "no-cache"/);
|
assert.match(caddyfile, /header @frontendRequests Cache-Control "no-cache"/);
|
||||||
|
assert.match(
|
||||||
|
caddyfile,
|
||||||
|
/@sharedProfileAsset path_regexp sharedProfileAsset \^\/gateway\/profile-assets\//,
|
||||||
|
);
|
||||||
|
assert.match(caddyfile, /root \* \/srv\/frontend-artifacts\/game-assets\/releases/);
|
||||||
|
assert.match(caddyfile, /uri strip_prefix \/gateway\/profile-assets/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const safeRuntime = {
|
|||||||
VITE_IMAGE_PUBLIC_URL: 'https://sam-image.hided.net',
|
VITE_IMAGE_PUBLIC_URL: 'https://sam-image.hided.net',
|
||||||
VITE_GATEWAY_USER_ICON_BASE_URL: 'https://sam-image.hided.net/icons',
|
VITE_GATEWAY_USER_ICON_BASE_URL: 'https://sam-image.hided.net/icons',
|
||||||
FRONTEND_SERVE_MODE: 'static',
|
FRONTEND_SERVE_MODE: 'static',
|
||||||
|
FRONTEND_SHARED_ASSET_PUBLIC_PATH: '/gateway/profile-assets',
|
||||||
RELEASE_BUILDER_URL: 'http://builder:15100',
|
RELEASE_BUILDER_URL: 'http://builder:15100',
|
||||||
GATEWAY_ACTIVE_RELEASE_GIT_REF: 'refs/sammo/active-gateway',
|
GATEWAY_ACTIVE_RELEASE_GIT_REF: 'refs/sammo/active-gateway',
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user