ð§¹.env ã«ãããªãïŒå šãŠã®ãµãŒãã¹éçºè ã宿ãããã«ãããã»ã¹æ¹åðâ±ïžâïž
ã¯ããã«
ããã«ã¡ã¯ïŒæ ªåŒäŒç€Ÿãã€ããŒã® Platform Team ã«æå±ããŠããŸãã0tanyã§ãã
ã¢ãã³ãªã¢ãžã¥ã©ãŒã¢ããªã¹ã¢ãŒããã¯ãã£ã§ã¯ãç°å¢å€æ°ã®ç®¡çãéèŠãªèª²é¡ã®äžã€ã§ããäºæ¥æé·ãšãšãã«ãµãŒãã¹æ°ãšç°å¢æ°ãå¢å ãããšããã®ç®¡çè€éæ§ã¯ææ°é¢æ°çã«å¢å€§ããŠãããŸãã
æ¬èšäºã§ã¯ããã®ç§»è¡ãéããŠåŸãããç¥èŠãå ±æããŸããåæ§ã®èª²é¡ãæ±ããããŒã ã®åèã«ãªãã°å¹žãã§ãã
åŸæ¥ã®ã¢ãŒããã¯ãã£ã®åé¡ç¹
ç°å¢å¥ã®.env ãã¡ã€ã«ãš Docker ã€ã¡ãŒãžã®ç®¡çå°ç
äžèšãåŸæ¥ã®ã¢ãŒããã¯ãã£ã§ãããã€ããŒã§ã¯é£²é£åºåãã®ã¢ãã€ã«ãªãŒããŒãµãŒãã¹ã 4 ã€ã®ç°å¢ïŒdevelop/staging/beta/productionïŒã§éçšããŠãããããããã®ç°å¢ã«å¯Ÿã㊠4 ã€ã®ãµãŒãã¹ïŒwebãbackendãbackend-online-paymentãbackend-reservationïŒããããã€ãããŠããŸãã
backend 系㮠3 ãµãŒãã¹ã¯å ±éã®ã³ãŒãããŒã¹ã䜿çšããŠããŸãããåãµãŒãã¹ã§ç°ãªãç°å¢å€æ°ãå¿ èŠãšãããããããããåå¥ã® Docker ã€ã¡ãŒãžãšããŠãã«ãããŠããŸãããããã®ãµãŒãã¹ã Cloud Run ã§ãã¹ãã£ã³ã°ããŠããŸãã
åŸæ¥ã®éçšãããŒïŒ
- éçºè ã®äœæ¥ïŒåãµãŒãã¹ã»åç°å¢çšã®.env ãã¡ã€ã«ã 16 å管ç
- æå·ååŠçïŒKMS ã䜿çšããŠ.env ãã¡ã€ã«ã.env.enc ãšããŠæå·å
- ããŒãžã§ã³ç®¡çïŒæå·åããã 16 åã®.env.enc ãã¡ã€ã«ã GitHub ã«ã³ããã
- CI/CD ãã«ãïŒç°å¢å¥ã« 16 åã®ç°ãªã Docker ã€ã¡ãŒãžããã«ã
- ãããã€ïŒåç°å¢å°çšã® Artifact Registry ãã.env å èµã€ã¡ãŒãžã Cloud Run ãžãããã€
ãã®éçšã¯æ©èœã¯ããŠããŸããããèŠãŠããã ããšãããããã«ãã ãã¶ç¡é§ã®å€ãè€éãªæ§æã«ãªã£ãŠããŸãã
å ·äœçãªåé¡ãæŽçãããšïŒ
ð ã»ãã¥ãªãã£ã®åé¡
- ããŒã«ã«ç°å¢ã«åŸ©å·åããã.env ãã¡ã€ã«ãæ®åããããšã«ããã»ãã¥ãªãã£ãªã¹ã¯
- production ã®.env ãéçºè ããŒã«ã«ã«ååšãåŸãããæå°ç¹æš©ã®ååã«åãã
â±ïž ãã«ãå¹çã®åé¡
- ã³ãŒãããŒã¹ã¯åããªã®ã«.env ãã¡ã€ã«ãéãã ãã®éè€ã€ã¡ãŒãžã倿°çæããã
- ç°å¢å€æ°ã 1 ã€å€æŽããã ãã§é¢é£ããå šãŠã®ç°å¢ã®åãã«ããå¿ èŠïŒçŽ 120 åïŒ
- 4 ç°å¢ à 4 ãµãŒãã¹ = 16 åã®ç°ãªã Docker ã€ã¡ãŒãžã管ç
察象ãµãŒãã¹ã®å èš³ïŒ
- web: ããã³ããšã³ãã¢ããªã±ãŒã·ã§ã³ïŒç¬ç«ããã³ãŒãããŒã¹ïŒ
-
backend ç³»: å
±éã³ãŒãããŒã¹ã§å®è£
ããã 3 ã€ã®ã¢ãžã¥ã©ãŒã¢ããªã¹
- backend: ã¡ã€ã³ã®ããã¯ãšã³ã API
- backend-online-payment: ãªã³ã©ã€ã³æ±ºæžãµãŒãã¹
- backend-reservation: äºçŽç®¡çãµãŒãã¹
ð ããŒãžã§ã³ç®¡çã®åé¡
- .env.enc ãã¡ã€ã«å šäœãæå·åãããããåå¥ Secret ã®å€æŽå±¥æŽã远跡äžå¯èœ
- .env ã®èª€ç·šéãçºçãããšãã«è¿œè·¡ãç ©éãè©²åœ commit ã®.env.enc ã埩å·åããã¡ã€ã«å·®åããã§ãã¯ããå¿ èŠããã
ãã®ç¶æ³ãæ¹åãããããGoogle Secret Manager ãžã®ç§»è¡ã決æããŸããã
Secret Manager ãæŽ»çšããæ°ã¢ãŒããã¯ãã£
èšèšåå
- ç°å¢ãšã³ãŒãã®å®å šåé¢: 12 Factor App ã®ååã«åŸã
- Single Source of Truth: Secret Manager ãå¯äžã®æ å ±æºãšãã
- æå°æš©éã®åå: ãµãŒãã¹ã¢ã«ãŠã³ãããšã«å¿ èŠæå°éã®æš©éã®ã¿ä»äž
- Infrastructure as Code: Cloud Development Kit for Terraform(cdktf) ã«ãã宣èšç管ç
ã¢ãŒããã¯ãã£æŠèŠ
æ°ããã¢ãŒããã¯ãã£ã§ã¯ãåŸæ¥ã®è€éãªå³ãšå¯Ÿç §çã«ã·ã³ãã«ãªæ§æãå®çŸããŸããã
ããŒãã€ã³ãïŒ
- éçºè 㯠.env ãã¡ã€ã«ã管çããªã
- çµ±äžããã Docker ã€ã¡ãŒãžãå šç°å¢ã§äœ¿ãåã
- Secret Manager ãå¯äžã®æ©å¯æ å ±æºãšããŠæ©èœ
- ç°å¢ããšã®å·®åã¯å®è¡æã«åçã«æ³šå ¥
åŸæ¥ã® 16 åã®ãã«ãããã»ã¹ããããã 2 åã®çµ±äžãã«ãã«éçŽãããŸããå ·äœçã«ã¯ãweb çšã®çµ±äžã€ã¡ãŒãžãšbackend ç³» 3 ãµãŒãã¹å ±éã®çµ±äžã€ã¡ãŒãžã® 2 ã€ã§ãã
backend ç³»ã¯å ±éã®ã³ãŒãããŒã¹ã掻çšãã1 ã€ã® Docker ã€ã¡ãŒãžã«çµ±åããŸããããã®ã€ã¡ãŒãžã backendãbackend-online-paymentãbackend-reservation ã® 3 ã€ã® Cloud Run ãµãŒãã¹ã§å ±æããŠäœ¿çšããŸããåãµãŒãã¹ã®éãã¯ãSecret Manager ããæ³šå ¥ãããç°å¢å€æ°ã«ãã£ãŠå®çŸããããããã€ã¡ãŒãžèªäœã¯å®å šã«åäžã§ãã
ãã®æ¹åŒã«ãããã³ãŒãããŒã¹ãåããµãŒãã¹ã§ç¡é§ã«ã€ã¡ãŒãžãåããå¿ èŠããªããªããŸãããå³ãèŠãŠããã ããšãç·ã®æ°ã®éãã¯äžç®çç¶ã§ãã
å®è£ 詳现
ãã€ããŒã§ã¯å šç€Ÿçãªæšæºæè¡ãšã㊠TypeScript ãæ¡çšããæ¹éãšãªã£ãŠããŸãã
ãã®æ¹éã®èæ¯ãçµç·¯ã«ã€ããŠèå³ã®ããæ¹ã¯ã以äžã®èšäºãããããŠã芧ãã ããã
ãã®ããã€ã³ãã©ã®æ§æç®¡çãéåžžã® Terraform(HCL)ã§ã¯ãªã Cloud Development Kit for Terraform (CDKTF) ãå©çšããTypeScript ã«ãŠå®è£ ã»ç®¡çãããŠããŸãã
åè¿°ã®ã¢ãŒããã¯ãã£æŠèŠã§ç€ºããæ°ããæ§æãå®çŸããããã«ã3 ã€ã®äž»èŠã³ã³ããŒãã³ããå®è£ ããŸããã®ã§ãããããã®å ·äœçãªå®è£ äŸãèŠãŠãããŸãããã
1. Secret Manager ã®å®çŸ©ïŒcdktfïŒ
Secret Manager äžã® Secret 管çã«é¢é£ããå®è£
ã以äžãšãªããŸãã
ãããã®å®è£
ã§ç®¡çããã®ã¯ secretId ã secret ã®ããŒãžã§ã³ç Secret Manager ãšããŠã®èšå®åšãã®ã¿ã®ã§ã Secret ãã®ãã®(SecretValue)ã®å€ã®ç»é²ã¯ gcloud ã³ãã³ãããç»é²ããæ³å®ã§ãã
// SecretManagerå®è£
æŠèŠ
type SecretConfig = {
secret_id: string;
version: string;
replication:
| { type: "auto" }
| { type: "userManaged"; locations: readonly string[] };
enabled: boolean; // 察象Secretã®æå¹åç¶æ
ã®flag
shouldImport: boolean; // 察象Secretãtfstateãžimportããéã®flag
};
const createSecret = (
secret_id: string,
overrides?: Partial<Omit<SecretConfig, "secret_id">>
): SecretConfig => ({
secret_id,
version: "1", // Secretã®default versionã¯1
replication: { type: "userManaged", locations: ["asia-northeast1"] },
enabled: true,
shouldImport: true,
...overrides,
});
// ç°å¢å¥ã®Secretå®çŸ©
const productionSecrets: readonly SecretConfig[] = [
createSecret("backend-app-env"),
createSecret("backend-default-database-url", { version: "2" }), // Secret versionãæŽæ°ãããšãã¯åŒæ°ã§versionãæå®
createSecret("backend-redis-host"),
// ... ä»ã®Secrets
];
2. Cloud Run ã§ã® Secret åç §
Secret Manager ã§å®çŸ©ãã Secret ããCloud Run ã®ç°å¢å€æ°ãšããŠåçã«æ³šå ¥ããŸãã
// ç°å¢å€æ°åãšSecret IDã®ãããã³ã°
const secretEnvMappings = [
{ env: "DATABASE_URL", secret: "backend-database-url" },
{ env: "REDIS_HOST", secret: "backend-redis-host" },
{ env: "PARTNER_API_KEY", secret: "backend-partner-api-key" },
// ç°å¢å¥ã§å¿
èŠãªSecretã®ã¿ãéžæ
// Secret IDã¯å¿
ã {serviceName}-{secret-name} ã®åœ¢åŒ
];
// Secretç°å¢å€æ°ã®çæ
const createSecretEnvVar = (
envName: string,
secretId: string,
secretVersionMap: SecretVersionMap
) => ({
name: envName,
valueFrom: {
secretKeyRef: {
name: secretId,
key: secretVersionMap.get(secretId)?.version || "latest",
},
},
});
// Cloud RunãµãŒãã¹å®çŸ©(æç²)
const cloudRunService = new CloudRunService(scope, serviceName, {
template: {
spec: {
containers: [
{
env: secretEnvMappings.map(({ env, secret }) =>
createSecretEnvVar(env, secret, secretVersionMap)
),
},
],
},
},
});
ããã«ãããCloud Run èµ·åæã« Secret Manager ããæå®ããŒãžã§ã³ã®å€ãååŸããã¢ããªã±ãŒã·ã§ã³ã®ç°å¢å€æ°ãšããŠå©çšã§ããŸãã
3. Secret Manager ãžã®ã¢ã¯ã»ã¹æš©é
Cloud Run ãã Secret Manager ã®å€ãèªã¿åãããããµãŒãã¹ã¢ã«ãŠã³ãã«æå°éã®æš©éã远å ä»äžããŸãã
// å¿
èŠãªæš©é
roles: ["roles/secretmanager.secretAccessor"]; // Secretèªã¿åãã®ã¿ã远å
ç§»è¡æã®ãã¹ããã©ã¯ãã£ã¹
1. æ¢åã®.env ãã Secret Manager ãžã®èªåç»é²
ç°å¢å€æ°ã¯ UPPER_SNAKE_CASE ã§èšå®ãããŸãããSecret Manager äžã® Secret ID 㯠kebab-case ã§ãªããšç»é²ã§ããŸããããã®ãã以äžã®å€æèŠåãå®ããŸããã
# 倿ã«ãŒã«ïŒENV_VAR â Secret IDïŒ
API_KEY â ${serviceName}-api-key
DATABASE_URL â ${serviceName}-database-url
REDIS_HOST â ${serviceName}-redis-host
以äžã¯ç§»è¡ã¹ã¯ãªããã®äŸã§ãã
// ç§»è¡ã¹ã¯ãªããå®è£
æŠèŠ
const migrateEnvToSecretManager = async (
envFile: string,
serviceName: string
) => {
const envContent = readFileSync(envFile, "utf-8");
await Promise.all(
envContent
.split("\\n")
.filter((line) => line && !line.startsWith("#"))
.map(async (line) => {
const [key, ...valueParts] = line.split("=");
const value = valueParts.join("=");
// ã¢ã³ããŒã¹ã³ã¢ããã€ãã³ã«å€æãå°æåå
const secretId = `${serviceName}-${key
.replace(/_/g, "-")
.toLowerCase()}`;
// gcloud ã³ãã³ãã§ Secret ãäœæ
await $`echo -n ${value} | gcloud secrets create ${secretId} --data-file=-`;
})
);
};
2. å·®åãã§ãã¯ããŒã«ã®å®è£
ãŸããç§»è¡æéäž.env ãã¢ããããŒãããç§»è¡æŒãã® Secret ãçãŸããªãããã.env ãã¡ã€ã«ãš Secret Manager ã®åæã確èªããããŒã«ãå®è£
ããŸããã
åŸæ¥ã®.env.enc ãã¡ã€ã«ãæŽæ°ããããšããéã«ãã§ãã¯ã¹ã¯ãªãããçºç«ãããSecret Manager ãžã®ç§»è¡ã®éçšã§ç§»è¡æŒããçºçããªãããã«ããŸãã
// å·®åãã§ãã¯ã¹ã¯ãªããå®è£
æŠèŠ
const checkSecret = async (
environment: string,
targetEnvVar: string,
serviceName: string
) => {
const projectId = ENV_PROJECT_MAP[serviceName][environment];
const envFilePath = path.join(
"deployments",
serviceName,
environment,
".env"
);
// .envãã¡ã€ã«ã®èªã¿èŸŒã¿
const envContent = await fs.promises.readFile(envFilePath, "utf-8");
const envVars = dotenv.parse(envContent);
// Secret IDã®çæïŒç°å¢å€æ°åã kebab-case ã«å€æïŒ
const secretId = targetEnvVar.toLowerCase().replace(/_/g, "-");
const secretName = `projects/${projectId}/secrets/${serviceName}-${secretId}`;
// Secret Managerããææ°ããŒãžã§ã³ãååŸ
const [versions] = await client.listSecretVersions({
parent: secretName,
filter: "state:ENABLED",
});
const [latestVersion] = versions;
const [version] = await client.accessSecretVersion({
name: latestVersion.name,
});
const secretValue = version?.payload?.data?.toString() ?? "";
// å€ã®æ¯èŒ
if (secretValue !== envVars[targetEnvVar]) {
throw new Error(`${environment}: ${targetEnvVar} value mismatch`);
}
return `${environment}: ${targetEnvVar} is up to date`;
};
// ãããåŠçã§äžŠåå®è¡ã倧éå®è¡ãããšSecretManagerã®Ratelimitã«åŒã£ãããã®ã§æ³šæ
const results = await processInBatches({
items: Object.keys(envVars),
processFn: async (envVar) => checkSecret(environment, envVar, serviceName),
batchSize: 10, // åæå®è¡æ°ã®å¶é
});
ç§»è¡ã®ææãšä»åŸã®å±æ
解決ãã 3 ã€ã®èª²é¡
1. ã»ãã¥ãªãã£ã®é£èºçåäž
- éçºè ããŒã«ã«ç°å¢ãã.env ãã¡ã€ã«ãå®å šã«æé€
- Cloud Run ã Secret Manager ããåçã«ç°å¢å€æ°ãååŸããä»çµã¿ãå®çŸ
2. ãã«ãå¹çã®åçæ¹å
- çµ±äž Docker ã€ã¡ãŒãžã«ããç°å¢éäŸåãã«ãã®å®çŸ
- ç·ãã«ãæé: 120 å â 16 åïŒ87%åæžïŒ
- åŸæ¥: web(9 å Ã4 ç°å¢) + backend ç³» 3 ã€(7 å Ã4 ç°å¢ Ã3) = 120 å
- æ°èŠ: web(9 å) + backend çµ±å(7 å) = 16 å
- 管ç察象ã€ã¡ãŒãžæ°: 16 å â 2 åïŒ87.5%åæžïŒ
3. Secret 管çã®éææ§ç¢ºä¿
- Secret Manager äžã® Secret ã®åå¥ç®¡çã«ãã詳现ãªå€æŽç®¡çã®å®çŸ
-
.env.enc
ãã¡ã€ã«ã®æå·å埩å·åäœæ¥ã廿¢
å®éçææ
ææš | Before | After | æ¹åç |
---|---|---|---|
ç·ãã«ãæé | 120 å | 16 å | 87%åæž |
管ç察象ã€ã¡ãŒãžæ° | 16 å | 2 å | 87.5%åæž |
ä»åŸã®å±æ
å®ã¯ä»åã®ç§»è¡å¯Ÿè±¡ã¯ Cloud Run ã§ãã¹ãã£ã³ã°ãããŠãããµãŒãã¹ã®ã¿ãšããŸããããã€ããŒã§ã¯ Firebase Hosting ã§ãã¹ããããŠãããµãŒãã¹ãããããã¡ãã¯ãŸã åŸæ¥ã®.env éçšãæ®ã£ãŠããŸãã
Firebase Hosting ã§ã¯ãã©ã¡ãŒã¿åãããæ§æãšãã¡ã€ã«ããŒã¹ã®ç°å¢å€æ°ã®æ§æãæ¡çšå¯èœã§ãããSecret 管çã Secret Manager ã«çµ±äžãã€ã€ããã©ã¡ãŒã¿åãããæ§æãžã®ç§»è¡ãæ€èšããŠããŸãã
ãŸãšã
.env ãã¡ã€ã«ãã Secret Manager ãžã®ç§»è¡ã¯ãåãªãããŒã«ã®çœ®ãæãã§ã¯ãããŸããã12 Factor App ã®ååã«æ²¿ã£ããããã¹ã±ãŒã©ãã«ã§å®å šãªã¢ãŒããã¯ãã£ãžã®é²åã§ãã
ç¹ã«éèŠãªã®ã¯ãç°å¢ãšã³ãŒãã®åé¢ãšããååã培åºããããšã§ããã«ãæéã®åæžã ãã§ãªããã»ãã¥ãªãã£ãšéçšå¹çã®å€§å¹ ãªæ¹åãå®çŸã§ããããšã§ãã
åæ§ã®èª²é¡ãæ±ããŠããããŒã ã®åèã«ãªãã°å¹žãã§ãã
æåŸã«
ãã€ããŒã§ã¯ããã®ãããªæè¡çãªèª²é¡è§£æ±ºã«åãçµããšã³ãžãã¢ãåéããŠããŸããã¢ãŒããã¯ãã£ã®æé©åãDeveloperExperienceã®åäžã«èå³ãããæ¹ã¯ããã²äžåºŠã話ããŸãããïŒ
https://diniinote.notion.site/Dinii-Engineering-EntranceBook-1df71045ad748062afe9c672363bdcab
Discussion