@yhonda/gcloud-secrets 2.0.5 → 2.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli.js +213 -81
- package/package.json +1 -1
- package/skills/secrets.md +65 -23
package/cli.js
CHANGED
|
@@ -9,28 +9,64 @@ import { execSync } from "child_process";
|
|
|
9
9
|
// SDK クライアント初期化
|
|
10
10
|
const client = new SecretManagerServiceClient();
|
|
11
11
|
|
|
12
|
+
// 引数パース (--env / -e オプション抽出)
|
|
13
|
+
function parseArgs(args) {
|
|
14
|
+
const result = { positional: [], env: null };
|
|
15
|
+
for (let i = 0; i < args.length; i++) {
|
|
16
|
+
if (args[i] === '--env' || args[i] === '-e') {
|
|
17
|
+
result.env = args[i + 1];
|
|
18
|
+
i++;
|
|
19
|
+
} else if (args[i].startsWith('--env=')) {
|
|
20
|
+
result.env = args[i].split('=')[1];
|
|
21
|
+
} else {
|
|
22
|
+
result.positional.push(args[i]);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
12
28
|
// 設定読み込み
|
|
13
29
|
function getConfig() {
|
|
14
30
|
const configFile = `${homedir()}/.secrets-manager.conf`;
|
|
31
|
+
const config = {
|
|
32
|
+
centralProject: process.env.SECRETS_CENTRAL_PROJECT || "",
|
|
33
|
+
defaultEnvironment: process.env.DEFAULT_ENVIRONMENT || "dev"
|
|
34
|
+
};
|
|
15
35
|
if (existsSync(configFile)) {
|
|
16
36
|
const content = readFileSync(configFile, "utf-8");
|
|
17
|
-
const
|
|
18
|
-
if (
|
|
19
|
-
|
|
37
|
+
const projectMatch = content.match(/SECRETS_CENTRAL_PROJECT=(.+)/);
|
|
38
|
+
if (projectMatch) {
|
|
39
|
+
config.centralProject = projectMatch[1].trim();
|
|
40
|
+
}
|
|
41
|
+
const envMatch = content.match(/DEFAULT_ENVIRONMENT=(.+)/);
|
|
42
|
+
if (envMatch) {
|
|
43
|
+
config.defaultEnvironment = envMatch[1].trim();
|
|
20
44
|
}
|
|
21
45
|
}
|
|
22
|
-
return
|
|
46
|
+
return config;
|
|
23
47
|
}
|
|
24
48
|
|
|
25
|
-
// シークレット名生成
|
|
26
|
-
function makeSecretName(folder, key) {
|
|
49
|
+
// シークレット名生成 (環境対応)
|
|
50
|
+
function makeSecretName(folder, key, env = null) {
|
|
51
|
+
if (env) {
|
|
52
|
+
return `${folder}_${env}_${key}`;
|
|
53
|
+
}
|
|
27
54
|
return `${folder}_${key}`;
|
|
28
55
|
}
|
|
29
56
|
|
|
30
|
-
//
|
|
31
|
-
function getKeyFromSecret(secretName) {
|
|
32
|
-
const
|
|
33
|
-
|
|
57
|
+
// シークレット名からキーと環境を抽出
|
|
58
|
+
function getKeyFromSecret(secretName, folderName) {
|
|
59
|
+
const prefix = folderName + "_";
|
|
60
|
+
if (!secretName.startsWith(prefix)) {
|
|
61
|
+
return { key: secretName, env: null };
|
|
62
|
+
}
|
|
63
|
+
const rest = secretName.slice(prefix.length);
|
|
64
|
+
const parts = rest.split("_");
|
|
65
|
+
// 2つ以上のパートがあり、最初がアルファベット小文字のみなら環境名と判断
|
|
66
|
+
if (parts.length >= 2 && /^[a-z]+$/.test(parts[0])) {
|
|
67
|
+
return { key: parts.slice(1).join("_"), env: parts[0] };
|
|
68
|
+
}
|
|
69
|
+
return { key: rest, env: null };
|
|
34
70
|
}
|
|
35
71
|
|
|
36
72
|
// フォルダ名を正規化 (camelCase → kebab-case)
|
|
@@ -112,8 +148,10 @@ function compareValues(a, b) {
|
|
|
112
148
|
|
|
113
149
|
// CLI モード
|
|
114
150
|
async function runCli(args) {
|
|
115
|
-
const
|
|
151
|
+
const parsed = parseArgs(args);
|
|
152
|
+
const command = parsed.positional[0];
|
|
116
153
|
const config = getConfig();
|
|
154
|
+
const targetEnv = parsed.env || config.defaultEnvironment;
|
|
117
155
|
|
|
118
156
|
if (!config.centralProject && command !== "init") {
|
|
119
157
|
console.error("エラー: 先に init を実行してください");
|
|
@@ -123,40 +161,49 @@ async function runCli(args) {
|
|
|
123
161
|
try {
|
|
124
162
|
switch (command) {
|
|
125
163
|
case "init": {
|
|
126
|
-
const projectId =
|
|
164
|
+
const projectId = parsed.positional[1];
|
|
165
|
+
const defaultEnv = parsed.env || "dev";
|
|
127
166
|
if (!projectId) {
|
|
128
|
-
console.error("使い方: gcloud-secrets init <project-id>");
|
|
167
|
+
console.error("使い方: gcloud-secrets init <project-id> [--env <default-env>]");
|
|
129
168
|
process.exit(1);
|
|
130
169
|
}
|
|
131
170
|
const configFile = `${homedir()}/.secrets-manager.conf`;
|
|
132
|
-
|
|
133
|
-
|
|
171
|
+
const configContent = `SECRETS_CENTRAL_PROJECT=${projectId}\nDEFAULT_ENVIRONMENT=${defaultEnv}\n`;
|
|
172
|
+
writeFileSync(configFile, configContent);
|
|
173
|
+
console.log(`設定完了: ${projectId} (デフォルト環境: ${defaultEnv})`);
|
|
134
174
|
break;
|
|
135
175
|
}
|
|
136
176
|
|
|
137
177
|
case "list": {
|
|
138
|
-
const folder =
|
|
178
|
+
const folder = parsed.positional[1];
|
|
139
179
|
const parent = `projects/${config.centralProject}`;
|
|
140
180
|
const [secrets] = await client.listSecrets({ parent });
|
|
141
181
|
|
|
142
182
|
if (!folder) {
|
|
143
|
-
|
|
183
|
+
// フォルダ一覧 (環境ごとにグループ化)
|
|
184
|
+
const folderEnvs = new Map();
|
|
144
185
|
for (const secret of secrets) {
|
|
145
|
-
const [
|
|
146
|
-
if (
|
|
147
|
-
|
|
186
|
+
const [secretData] = await client.getSecret({ name: secret.name });
|
|
187
|
+
if (secretData.labels?.folder) {
|
|
188
|
+
const f = secretData.labels.folder;
|
|
189
|
+
const e = secretData.labels?.environment || "(default)";
|
|
190
|
+
if (!folderEnvs.has(f)) folderEnvs.set(f, new Set());
|
|
191
|
+
folderEnvs.get(f).add(e);
|
|
148
192
|
}
|
|
149
193
|
}
|
|
150
194
|
console.log("フォルダ一覧:");
|
|
151
|
-
for (const f of
|
|
152
|
-
|
|
195
|
+
for (const [f, envs] of folderEnvs) {
|
|
196
|
+
const envList = Array.from(envs).sort().join(', ');
|
|
197
|
+
console.log(` ${f} [${envList}]`);
|
|
153
198
|
}
|
|
154
199
|
} else {
|
|
155
|
-
|
|
200
|
+
// 特定フォルダのシークレット一覧 (環境でフィルタ)
|
|
201
|
+
console.log(`${folder} (${targetEnv}) のシークレット:`);
|
|
156
202
|
for (const secret of secrets) {
|
|
157
203
|
const [secretData] = await client.getSecret({ name: secret.name });
|
|
158
|
-
|
|
159
|
-
|
|
204
|
+
const secretEnv = secretData.labels?.environment || null;
|
|
205
|
+
if (secretData.labels?.folder === folder && secretEnv === targetEnv) {
|
|
206
|
+
const { key } = getKeyFromSecret(secret.name.split("/").pop(), folder);
|
|
160
207
|
console.log(` ${key}`);
|
|
161
208
|
}
|
|
162
209
|
}
|
|
@@ -165,15 +212,16 @@ async function runCli(args) {
|
|
|
165
212
|
}
|
|
166
213
|
|
|
167
214
|
case "pull": {
|
|
168
|
-
const folder = normalizeFolder(
|
|
215
|
+
const folder = normalizeFolder(parsed.positional[1] || basename(process.cwd()));
|
|
169
216
|
const parent = `projects/${config.centralProject}`;
|
|
170
217
|
const [secrets] = await client.listSecrets({ parent });
|
|
171
218
|
|
|
172
219
|
const envLines = [];
|
|
173
220
|
for (const secret of secrets) {
|
|
174
221
|
const [secretData] = await client.getSecret({ name: secret.name });
|
|
175
|
-
|
|
176
|
-
|
|
222
|
+
const secretEnv = secretData.labels?.environment || null;
|
|
223
|
+
if (secretData.labels?.folder === folder && secretEnv === targetEnv) {
|
|
224
|
+
const { key } = getKeyFromSecret(secret.name.split("/").pop(), folder);
|
|
177
225
|
const [version] = await client.accessSecretVersion({
|
|
178
226
|
name: `${secret.name}/versions/latest`,
|
|
179
227
|
});
|
|
@@ -185,13 +233,17 @@ async function runCli(args) {
|
|
|
185
233
|
}
|
|
186
234
|
}
|
|
187
235
|
}
|
|
188
|
-
|
|
236
|
+
if (envLines.length === 0) {
|
|
237
|
+
console.error(`警告: ${folder} (${targetEnv}) にシークレットが見つかりません`);
|
|
238
|
+
} else {
|
|
239
|
+
console.log(envLines.join("\n"));
|
|
240
|
+
}
|
|
189
241
|
break;
|
|
190
242
|
}
|
|
191
243
|
|
|
192
244
|
case "push": {
|
|
193
|
-
const folder = normalizeFolder(
|
|
194
|
-
const envFile =
|
|
245
|
+
const folder = normalizeFolder(parsed.positional[1] || basename(process.cwd()));
|
|
246
|
+
const envFile = parsed.positional[2] || ".env";
|
|
195
247
|
|
|
196
248
|
if (!existsSync(envFile)) {
|
|
197
249
|
console.error(`ファイルが見つかりません: ${envFile}`);
|
|
@@ -201,6 +253,7 @@ async function runCli(args) {
|
|
|
201
253
|
const content = readFileSync(envFile, "utf-8");
|
|
202
254
|
const lines = content.split("\n");
|
|
203
255
|
const parent = `projects/${config.centralProject}`;
|
|
256
|
+
const labels = { folder, environment: targetEnv };
|
|
204
257
|
let count = 0;
|
|
205
258
|
|
|
206
259
|
for (const line of lines) {
|
|
@@ -208,11 +261,16 @@ async function runCli(args) {
|
|
|
208
261
|
const match = line.match(/^([A-Z_][A-Z0-9_]*)=(.*)$/i);
|
|
209
262
|
if (match) {
|
|
210
263
|
const [, key, value] = match;
|
|
211
|
-
const secretId = makeSecretName(folder, key);
|
|
264
|
+
const secretId = makeSecretName(folder, key, targetEnv);
|
|
212
265
|
const secretName = `${parent}/secrets/${secretId}`;
|
|
213
266
|
|
|
214
267
|
try {
|
|
215
268
|
await client.getSecret({ name: secretName });
|
|
269
|
+
// 既存シークレットのラベルも更新
|
|
270
|
+
await client.updateSecret({
|
|
271
|
+
secret: { name: secretName, labels },
|
|
272
|
+
updateMask: { paths: ['labels'] }
|
|
273
|
+
});
|
|
216
274
|
await client.addSecretVersion({
|
|
217
275
|
parent: secretName,
|
|
218
276
|
payload: { data: Buffer.from(value) },
|
|
@@ -221,7 +279,7 @@ async function runCli(args) {
|
|
|
221
279
|
await client.createSecret({
|
|
222
280
|
parent,
|
|
223
281
|
secretId,
|
|
224
|
-
secret: { replication: { automatic: {} }, labels
|
|
282
|
+
secret: { replication: { automatic: {} }, labels },
|
|
225
283
|
});
|
|
226
284
|
await client.addSecretVersion({
|
|
227
285
|
parent: secretName,
|
|
@@ -231,24 +289,27 @@ async function runCli(args) {
|
|
|
231
289
|
count++;
|
|
232
290
|
}
|
|
233
291
|
}
|
|
234
|
-
console.log(`${count} 件のシークレットをアップロードしました (${folder})`);
|
|
292
|
+
console.log(`${count} 件のシークレットをアップロードしました (${folder}/${targetEnv})`);
|
|
235
293
|
break;
|
|
236
294
|
}
|
|
237
295
|
|
|
238
296
|
case "scan": {
|
|
239
|
-
const basePath =
|
|
297
|
+
const basePath = parsed.positional[1] || homedir();
|
|
298
|
+
const filterEnv = parsed.env; // null の場合は全環境を表示
|
|
240
299
|
const repos = findGitRepositories(basePath, 5);
|
|
241
300
|
const parent = `projects/${config.centralProject}`;
|
|
242
301
|
const [allSecrets] = await client.listSecrets({ parent });
|
|
243
302
|
|
|
244
|
-
//
|
|
245
|
-
const
|
|
303
|
+
// フォルダ+環境ごとにグループ化
|
|
304
|
+
const secretsByFolderEnv = new Map();
|
|
246
305
|
for (const secret of allSecrets) {
|
|
247
306
|
const [secretData] = await client.getSecret({ name: secret.name });
|
|
248
307
|
const f = secretData.labels?.folder;
|
|
308
|
+
const e = secretData.labels?.environment || null;
|
|
249
309
|
if (f) {
|
|
250
|
-
|
|
251
|
-
|
|
310
|
+
const key = `${f}|${e || ''}`;
|
|
311
|
+
if (!secretsByFolderEnv.has(key)) secretsByFolderEnv.set(key, []);
|
|
312
|
+
secretsByFolderEnv.get(key).push({ secret, env: e });
|
|
252
313
|
}
|
|
253
314
|
}
|
|
254
315
|
|
|
@@ -270,51 +331,64 @@ async function runCli(args) {
|
|
|
270
331
|
const localEntries = parseEnvFile(content);
|
|
271
332
|
if (localEntries.length === 0) continue;
|
|
272
333
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
334
|
+
// 環境フィルタがある場合はその環境のみ、なければ全環境をチェック
|
|
335
|
+
const envsToCheck = filterEnv ? [filterEnv] : [null, ...Array.from(new Set(
|
|
336
|
+
Array.from(secretsByFolderEnv.keys())
|
|
337
|
+
.filter(k => k.startsWith(normalizedFolder + '|'))
|
|
338
|
+
.map(k => k.split('|')[1])
|
|
339
|
+
.filter(Boolean)
|
|
340
|
+
))];
|
|
341
|
+
|
|
342
|
+
for (const checkEnv of envsToCheck) {
|
|
343
|
+
const mapKey = `${normalizedFolder}|${checkEnv || ''}`;
|
|
344
|
+
const folderSecrets = secretsByFolderEnv.get(mapKey) || [];
|
|
345
|
+
const envLabel = checkEnv || "(default)";
|
|
346
|
+
|
|
347
|
+
if (folderSecrets.length === 0) {
|
|
348
|
+
results.push({ status: "NEW", repo: repoName, file: envFile.filename, env: envLabel, keyCount: localEntries.length, gitIgnored: envFile.gitIgnored });
|
|
349
|
+
newCount++;
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
280
352
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
353
|
+
// リモート値取得・比較
|
|
354
|
+
let hasDiff = false;
|
|
355
|
+
const remoteKeys = new Set();
|
|
356
|
+
const remoteValues = new Map();
|
|
357
|
+
|
|
358
|
+
for (const { secret } of folderSecrets) {
|
|
359
|
+
const { key } = getKeyFromSecret(secret.name.split('/').pop(), normalizedFolder);
|
|
360
|
+
remoteKeys.add(key);
|
|
361
|
+
try {
|
|
362
|
+
const [version] = await client.accessSecretVersion({ name: `${secret.name}/versions/latest` });
|
|
363
|
+
remoteValues.set(key, version.payload.data.toString('utf8'));
|
|
364
|
+
} catch { }
|
|
365
|
+
}
|
|
294
366
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
367
|
+
for (const entry of localEntries) {
|
|
368
|
+
if (!remoteKeys.has(entry.key) || !compareValues(entry.value, remoteValues.get(entry.key) || '')) {
|
|
369
|
+
hasDiff = true;
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
299
372
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
373
|
+
if (!hasDiff) {
|
|
374
|
+
for (const key of remoteKeys) {
|
|
375
|
+
if (!localEntries.find(e => e.key === key)) { hasDiff = true; break; }
|
|
376
|
+
}
|
|
304
377
|
}
|
|
305
|
-
}
|
|
306
378
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
379
|
+
if (hasDiff) {
|
|
380
|
+
results.push({ status: "DIFF", repo: repoName, file: envFile.filename, env: envLabel, keyCount: localEntries.length, gitIgnored: envFile.gitIgnored });
|
|
381
|
+
diffCount++;
|
|
382
|
+
} else {
|
|
383
|
+
results.push({ status: "OK", repo: repoName, file: envFile.filename, env: envLabel, keyCount: localEntries.length, gitIgnored: envFile.gitIgnored });
|
|
384
|
+
syncedCount++;
|
|
385
|
+
}
|
|
313
386
|
}
|
|
314
387
|
}
|
|
315
388
|
}
|
|
316
389
|
|
|
317
|
-
|
|
390
|
+
const envSuffix = filterEnv ? ` (${filterEnv})` : "";
|
|
391
|
+
console.log(`=== Secret Manager 同期状況${envSuffix} ===\n`);
|
|
318
392
|
if (results.length === 0) {
|
|
319
393
|
console.log(".env / .dev.vars ファイルが見つかりませんでした");
|
|
320
394
|
} else {
|
|
@@ -322,7 +396,7 @@ async function runCli(args) {
|
|
|
322
396
|
const label = r.status === "OK" ? "[OK] " : r.status === "DIFF" ? "[DIFF]" : "[NEW] ";
|
|
323
397
|
const suffix = r.status === "DIFF" ? " - 差分あり" : r.status === "NEW" ? " - 未登録" : "";
|
|
324
398
|
const warn = !r.gitIgnored ? " ⚠" : "";
|
|
325
|
-
console.log(`${label} ${r.repo}/ ${r.file} (${r.keyCount} keys)${suffix}${warn}`);
|
|
399
|
+
console.log(`${label} ${r.repo}/ ${r.file} [${r.env}] (${r.keyCount} keys)${suffix}${warn}`);
|
|
326
400
|
}
|
|
327
401
|
console.log(`\n---\n合計: ${results.length} ファイル`);
|
|
328
402
|
console.log(` 登録済み: ${syncedCount}`);
|
|
@@ -334,15 +408,73 @@ async function runCli(args) {
|
|
|
334
408
|
break;
|
|
335
409
|
}
|
|
336
410
|
|
|
411
|
+
case "search": {
|
|
412
|
+
const keyword = parsed.positional[1];
|
|
413
|
+
if (!keyword) {
|
|
414
|
+
console.log("使い方: gcloud-secrets search <keyword> [--env <env>]");
|
|
415
|
+
process.exit(1);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const filterEnv = parsed.env;
|
|
419
|
+
const parent = `projects/${config.centralProject}`;
|
|
420
|
+
const [secrets] = await client.listSecrets({ parent });
|
|
421
|
+
|
|
422
|
+
console.log(`Searching for: "${keyword}"`);
|
|
423
|
+
if (filterEnv) console.log(` 環境: ${filterEnv}`);
|
|
424
|
+
console.log(`\nScanning ${secrets.length} secrets...\n`);
|
|
425
|
+
|
|
426
|
+
const matches = [];
|
|
427
|
+
const folders = new Set();
|
|
428
|
+
|
|
429
|
+
for (const secret of secrets) {
|
|
430
|
+
const [secretData] = await client.getSecret({ name: secret.name });
|
|
431
|
+
const folder = secretData.labels?.folder;
|
|
432
|
+
const env = secretData.labels?.environment || "(default)";
|
|
433
|
+
|
|
434
|
+
// 環境フィルタ
|
|
435
|
+
if (filterEnv && secretData.labels?.environment !== filterEnv) continue;
|
|
436
|
+
|
|
437
|
+
// 値を取得してキーワード検索
|
|
438
|
+
try {
|
|
439
|
+
const [version] = await client.accessSecretVersion({
|
|
440
|
+
name: `${secret.name}/versions/latest`,
|
|
441
|
+
});
|
|
442
|
+
const value = version.payload.data.toString("utf-8");
|
|
443
|
+
if (value.includes(keyword)) {
|
|
444
|
+
const { key } = getKeyFromSecret(secret.name.split("/").pop(), folder);
|
|
445
|
+
matches.push({ folder, env, key });
|
|
446
|
+
folders.add(folder);
|
|
447
|
+
}
|
|
448
|
+
} catch {
|
|
449
|
+
// バージョンがない場合はスキップ
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (matches.length === 0) {
|
|
454
|
+
console.log("No matches found");
|
|
455
|
+
} else {
|
|
456
|
+
for (const m of matches) {
|
|
457
|
+
console.log(`[FOUND] ${m.folder} / ${m.env} - ${m.key}`);
|
|
458
|
+
}
|
|
459
|
+
console.log(`\nFound ${matches.length} matches in ${folders.size} folders`);
|
|
460
|
+
}
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
|
|
337
464
|
default:
|
|
338
465
|
console.log(`gcloud-secrets - GCP Secret Manager CLI
|
|
339
466
|
|
|
340
467
|
使い方:
|
|
341
|
-
gcloud-secrets init <project-id>
|
|
342
|
-
gcloud-secrets list [folder]
|
|
343
|
-
gcloud-secrets pull [folder]
|
|
344
|
-
gcloud-secrets push [folder] [file] シークレットをアップロード
|
|
345
|
-
gcloud-secrets scan [basePath]
|
|
468
|
+
gcloud-secrets init <project-id> [--env <default>] 中央プロジェクトを設定
|
|
469
|
+
gcloud-secrets list [folder] [--env <env>] 一覧表示
|
|
470
|
+
gcloud-secrets pull [folder] [--env <env>] シークレットを取得
|
|
471
|
+
gcloud-secrets push [folder] [file] [--env <env>] シークレットをアップロード
|
|
472
|
+
gcloud-secrets scan [basePath] [--env <env>] Git リポジトリの .env 同期状況をスキャン
|
|
473
|
+
gcloud-secrets search <keyword> [--env <env>] 値から逆引き検索
|
|
474
|
+
|
|
475
|
+
オプション:
|
|
476
|
+
--env, -e <env> 環境を指定 (dev, staging, prod など)
|
|
477
|
+
省略時は設定ファイルの DEFAULT_ENVIRONMENT を使用
|
|
346
478
|
`);
|
|
347
479
|
}
|
|
348
480
|
} catch (error) {
|
package/package.json
CHANGED
package/skills/secrets.md
CHANGED
|
@@ -6,58 +6,84 @@ GCP Secret Manager を使って .env ファイルを管理するスキル
|
|
|
6
6
|
|
|
7
7
|
### 初期化
|
|
8
8
|
```bash
|
|
9
|
-
gcloud-secrets init <project-id>
|
|
9
|
+
gcloud-secrets init <project-id> [--env <default>]
|
|
10
10
|
```
|
|
11
|
-
GCP プロジェクト ID
|
|
11
|
+
GCP プロジェクト ID を設定します。`--env` でデフォルト環境を指定できます(省略時は `dev`)。
|
|
12
12
|
|
|
13
13
|
### 一覧表示
|
|
14
14
|
```bash
|
|
15
|
-
# フォルダ一覧
|
|
15
|
+
# フォルダ一覧 (環境ごとにグループ化)
|
|
16
16
|
gcloud-secrets list
|
|
17
17
|
|
|
18
|
-
#
|
|
19
|
-
gcloud-secrets list <folder>
|
|
18
|
+
# 特定フォルダ・環境のシークレット一覧
|
|
19
|
+
gcloud-secrets list <folder> --env dev
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
### シークレット取得 (pull)
|
|
23
23
|
```bash
|
|
24
24
|
# カレントディレクトリ名をフォルダ名として取得
|
|
25
|
-
gcloud-secrets pull
|
|
25
|
+
gcloud-secrets pull --env dev
|
|
26
26
|
|
|
27
27
|
# 指定フォルダから取得
|
|
28
|
-
gcloud-secrets pull <folder>
|
|
28
|
+
gcloud-secrets pull <folder> --env prod
|
|
29
29
|
```
|
|
30
30
|
Secret Manager から .env 形式でシークレットを取得します。
|
|
31
31
|
|
|
32
32
|
### シークレット登録 (push)
|
|
33
33
|
```bash
|
|
34
|
-
# .env ファイルをアップロード
|
|
35
|
-
gcloud-secrets push
|
|
34
|
+
# .env ファイルをアップロード (dev 環境)
|
|
35
|
+
gcloud-secrets push --env dev
|
|
36
36
|
|
|
37
|
-
# 指定フォルダにアップロード
|
|
38
|
-
gcloud-secrets push <folder>
|
|
37
|
+
# 指定フォルダにアップロード (prod 環境)
|
|
38
|
+
gcloud-secrets push <folder> --env prod
|
|
39
39
|
|
|
40
40
|
# 指定ファイルをアップロード
|
|
41
|
-
gcloud-secrets push <folder> <file>
|
|
41
|
+
gcloud-secrets push <folder> <file> --env staging
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
### 同期状況スキャン (scan)
|
|
45
45
|
```bash
|
|
46
|
-
# ホームディレクトリ以下をスキャン
|
|
46
|
+
# ホームディレクトリ以下をスキャン (全環境)
|
|
47
47
|
gcloud-secrets scan
|
|
48
48
|
|
|
49
|
+
# 特定環境のみスキャン
|
|
50
|
+
gcloud-secrets scan --env dev
|
|
51
|
+
|
|
49
52
|
# 指定ディレクトリ以下をスキャン
|
|
50
|
-
gcloud-secrets scan <path>
|
|
53
|
+
gcloud-secrets scan <path> --env prod
|
|
51
54
|
```
|
|
52
55
|
Git リポジトリ内の .env / .dev.vars ファイルと Secret Manager の同期状況を確認します。
|
|
53
56
|
|
|
57
|
+
### 値から逆引き検索 (search)
|
|
58
|
+
```bash
|
|
59
|
+
# 特定の値がどのフォルダ・環境で使われているか検索
|
|
60
|
+
gcloud-secrets search "api-key-12345"
|
|
61
|
+
|
|
62
|
+
# 特定環境のみ検索
|
|
63
|
+
gcloud-secrets search "client-id" --env prod
|
|
64
|
+
```
|
|
65
|
+
シークレットの値から、使用しているフォルダ・環境・キーを逆引き検索します。
|
|
66
|
+
|
|
54
67
|
出力例:
|
|
55
68
|
```
|
|
69
|
+
Searching for: "api-key-12345"
|
|
70
|
+
|
|
71
|
+
Scanning 45 secrets...
|
|
72
|
+
|
|
73
|
+
[FOUND] my-app / dev - EXTERNAL_API_KEY
|
|
74
|
+
[FOUND] my-app / prod - EXTERNAL_API_KEY
|
|
75
|
+
[FOUND] other-service / dev - LINE_CLIENT_ID
|
|
76
|
+
|
|
77
|
+
Found 3 matches in 2 folders
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
#### scan 出力例:
|
|
81
|
+
```
|
|
56
82
|
=== Secret Manager 同期状況 ===
|
|
57
83
|
|
|
58
|
-
[OK] project-a/ .env (3 keys)
|
|
59
|
-
[DIFF] project-b/ .env (2 keys) - 差分あり
|
|
60
|
-
[NEW] project-c/ .dev.vars (5 keys) - 未登録
|
|
84
|
+
[OK] project-a/ .env [dev] (3 keys)
|
|
85
|
+
[DIFF] project-b/ .env [prod] (2 keys) - 差分あり
|
|
86
|
+
[NEW] project-c/ .dev.vars [dev] (5 keys) - 未登録
|
|
61
87
|
|
|
62
88
|
---
|
|
63
89
|
合計: 3 ファイル
|
|
@@ -66,18 +92,34 @@ Git リポジトリ内の .env / .dev.vars ファイルと Secret Manager の同
|
|
|
66
92
|
未登録: 1
|
|
67
93
|
```
|
|
68
94
|
|
|
95
|
+
## 環境 (Environment) オプション
|
|
96
|
+
|
|
97
|
+
`--env` または `-e` で環境を指定できます:
|
|
98
|
+
- `dev` - 開発環境
|
|
99
|
+
- `staging` - ステージング環境
|
|
100
|
+
- `prod` - 本番環境
|
|
101
|
+
- その他任意の文字列
|
|
102
|
+
|
|
103
|
+
デフォルト環境は `~/.secrets-manager.conf` の `DEFAULT_ENVIRONMENT` で設定されます。
|
|
104
|
+
|
|
69
105
|
## 使用例
|
|
70
106
|
|
|
71
107
|
```bash
|
|
72
|
-
# 1. 初期化
|
|
73
|
-
gcloud-secrets init my-gcp-project
|
|
108
|
+
# 1. 初期化 (デフォルト環境を dev に設定)
|
|
109
|
+
gcloud-secrets init my-gcp-project --env dev
|
|
74
110
|
|
|
75
|
-
# 2.
|
|
76
|
-
gcloud-secrets push
|
|
111
|
+
# 2. dev 環境に .env を登録
|
|
112
|
+
gcloud-secrets push --env dev
|
|
77
113
|
|
|
78
|
-
# 3.
|
|
79
|
-
gcloud-secrets pull > .env
|
|
114
|
+
# 3. prod 環境から取得
|
|
115
|
+
gcloud-secrets pull --env prod > .env.prod
|
|
80
116
|
|
|
81
117
|
# 4. 全リポジトリの同期状況を確認
|
|
82
118
|
gcloud-secrets scan ~/
|
|
119
|
+
|
|
120
|
+
# 5. dev 環境のみスキャン
|
|
121
|
+
gcloud-secrets scan ~/ --env dev
|
|
122
|
+
|
|
123
|
+
# 6. 特定の値がどこで使われているか検索
|
|
124
|
+
gcloud-secrets search "line-client-id-xxx"
|
|
83
125
|
```
|