feat(runtime): 정적 프런트엔드와 격리 빌더를 구성한다
Caddy가 불변 아티팩트를 직접 제공하고 runtime과 builder의 자원·비밀 경계를 분리한다. Gateway active release ref를 재기동에 복원하고 production/development/smoke 모델 검증을 확장한다.
This commit is contained in:
@@ -10,6 +10,10 @@ export const validateComposeModel = (model, mode) => {
|
||||
const errors = [];
|
||||
const runtime = model?.services?.runtime;
|
||||
if (!runtime) return ['runtime service is missing'];
|
||||
const builder = model?.services?.builder;
|
||||
const caddy = model?.services?.caddy;
|
||||
if (!builder) errors.push('builder service is missing');
|
||||
if (!caddy) errors.push('caddy service is missing');
|
||||
|
||||
const requiredImageEnvironment = {
|
||||
GATEWAY_IMAGE_UPLOAD_SECRET_FILE: '/run/secrets/image_upload_core2026_secret',
|
||||
@@ -41,6 +45,16 @@ export const validateComposeModel = (model, mode) => {
|
||||
errors.push(`${target} must be a read-only bind mount`);
|
||||
}
|
||||
}
|
||||
const artifactMount = mounts.find((candidate) => candidate?.target === '/srv/frontend-artifacts');
|
||||
if (!artifactMount || artifactMount.read_only === true) {
|
||||
errors.push('/srv/frontend-artifacts must be a writable runtime volume');
|
||||
}
|
||||
const caddyArtifactMount = (Array.isArray(caddy?.volumes) ? caddy.volumes : []).find(
|
||||
(candidate) => candidate?.target === '/srv/frontend-artifacts',
|
||||
);
|
||||
if (!caddyArtifactMount || caddyArtifactMount.read_only !== true) {
|
||||
errors.push('/srv/frontend-artifacts must be a read-only Caddy volume');
|
||||
}
|
||||
|
||||
if (!positive(runtime.mem_limit)) errors.push('runtime memory limit must be positive');
|
||||
if (!positive(runtime.memswap_limit)) errors.push('runtime memory+swap limit must be positive');
|
||||
@@ -70,6 +84,34 @@ export const validateComposeModel = (model, mode) => {
|
||||
errors.push('runtime Rayon thread count must be between 1 and 4');
|
||||
}
|
||||
|
||||
if (builder) {
|
||||
if (!positive(builder.mem_limit)) errors.push('builder memory limit must be positive');
|
||||
if (!positive(builder.memswap_limit)) errors.push('builder memory+swap limit must be positive');
|
||||
if (Number(builder.memswap_limit) !== Number(builder.mem_limit)) {
|
||||
errors.push('builder memory+swap limit must equal its memory limit');
|
||||
}
|
||||
if (!positive(builder.cpus)) errors.push('builder CPU limit must be positive');
|
||||
if (!positive(builder.pids_limit)) errors.push('builder PID limit must be positive');
|
||||
const builderNodeOptions = builder.environment?.NODE_OPTIONS ?? '';
|
||||
const builderHeapMatch = /(?:^|\s)--max-old-space-size=(\d+)(?:\s|$)/.exec(builderNodeOptions);
|
||||
const builderHeapMiB = Number(builderHeapMatch?.[1] ?? 0);
|
||||
if (!builderHeapMatch || builderHeapMiB < 512 || builderHeapMiB > 3584) {
|
||||
errors.push('builder Node heap limit must be between 512 and 3584 MiB');
|
||||
}
|
||||
if (builderHeapMiB * 1024 * 1024 >= Number(builder.mem_limit)) {
|
||||
errors.push('builder Node heap limit must stay below the builder memory limit');
|
||||
}
|
||||
const builderRayonThreads = Number(builder.environment?.RAYON_NUM_THREADS ?? 0);
|
||||
if (!Number.isInteger(builderRayonThreads) || builderRayonThreads < 1 || builderRayonThreads > 4) {
|
||||
errors.push('builder Rayon thread count must be between 1 and 4');
|
||||
}
|
||||
for (const name of Object.keys(builder.environment ?? {})) {
|
||||
if (/(?:SECRET|TOKEN|PASSWORD|DATABASE_URL|REDIS_URL)/i.test(name)) {
|
||||
errors.push(`builder must not receive sensitive environment variable ${name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === 'development') {
|
||||
if (runtime.environment?.RUNTIME_MODE !== 'development') {
|
||||
errors.push('development runtime mode must be literal development');
|
||||
@@ -82,9 +124,20 @@ export const validateComposeModel = (model, mode) => {
|
||||
} else if (runtime.environment?.RUNTIME_MODE === 'development') {
|
||||
errors.push(`${mode} runtime must not use development mode`);
|
||||
}
|
||||
if (mode !== 'development') {
|
||||
if (runtime.environment?.FRONTEND_SERVE_MODE !== 'static') {
|
||||
errors.push(`${mode} frontend serve mode must be static`);
|
||||
}
|
||||
if (runtime.environment?.RELEASE_BUILDER_URL !== 'http://builder:15100') {
|
||||
errors.push(`${mode} runtime must use the isolated release builder`);
|
||||
}
|
||||
if (runtime.environment?.GATEWAY_ACTIVE_RELEASE_GIT_REF !== 'refs/sammo/active-gateway') {
|
||||
errors.push(`${mode} runtime must persist the active Gateway release ref`);
|
||||
}
|
||||
}
|
||||
|
||||
if (mode === 'development' || mode === 'smoke') {
|
||||
for (const name of ['postgres', 'redis', 'runtime', 'caddy']) {
|
||||
for (const name of ['postgres', 'redis', 'builder', 'runtime', 'caddy']) {
|
||||
if (model?.services?.[name]?.restart !== 'no') errors.push(`${name} ${mode} restart policy must be no`);
|
||||
}
|
||||
if (Number(runtime.mem_limit) > MAX_SAFE_DEV_MEMORY) errors.push(`${mode} runtime memory limit exceeds 4 GiB`);
|
||||
|
||||
Reference in New Issue
Block a user