mirror of
https://github.com/morten-olsen/with-ssm.git
synced 2026-02-08 00:46:23 +01:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1321ea4748 | ||
|
|
70f1cf9134 | ||
|
|
815ac86873 | ||
|
|
14c4d5c386 | ||
|
|
00786d5508 | ||
|
|
ef32edcb29 | ||
|
|
e2dfb3491d |
@@ -1 +0,0 @@
|
|||||||
PASSWORD=SSM:/test/hfd/rds/DB_USER
|
|
||||||
@@ -117,7 +117,7 @@ override the SSM-resolved values. To avoid this:
|
|||||||
|
|
||||||
- Use `.env.with-ssm` instead of `.env` for SSM references
|
- Use `.env.with-ssm` instead of `.env` for SSM references
|
||||||
- Or use environment variable substitution if your app supports it:
|
- Or use environment variable substitution if your app supports it:
|
||||||
`${API_KEY:-SSM:/myapp/api-key}`
|
`API_KEY=${API_KEY:-SSM:/myapp/api-key}`
|
||||||
|
|
||||||
### 🚀 Deployment Considerations
|
### 🚀 Deployment Considerations
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import '../dist/start.js';
|
import '../dist/index.js';
|
||||||
|
|||||||
32
package.json
32
package.json
@@ -6,37 +6,37 @@
|
|||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test:lint": "eslint",
|
"test:lint": "eslint",
|
||||||
"build": "tsc --build",
|
"build": "ncc build src/start.ts -s -o dist",
|
||||||
"build:dev": "tsc --build --watch",
|
"build:dev": "ncc build src/start.ts -s -o dist --watch",
|
||||||
"test:unit": "vitest --run --passWithNoTests",
|
"test:unit": "vitest --run --passWithNoTests",
|
||||||
"test": "pnpm run \"/^test:/\""
|
"test": "pnpm run \"/^test:/\""
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.6.0",
|
"packageManager": "pnpm@10.17.0",
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@aws-sdk/client-ssm": "^3.901.0",
|
||||||
|
"@aws-sdk/client-sts": "^3.901.0",
|
||||||
|
"@dotenvx/dotenvx": "^1.51.0",
|
||||||
"@eslint/eslintrc": "3.3.1",
|
"@eslint/eslintrc": "3.3.1",
|
||||||
"@eslint/js": "9.32.0",
|
"@eslint/js": "9.36.0",
|
||||||
"@pnpm/find-workspace-packages": "6.0.9",
|
"@pnpm/find-workspace-packages": "6.0.9",
|
||||||
"@types/node": "24.2.0",
|
"@types/node": "24.6.2",
|
||||||
"@types/yargs": "^17.0.33",
|
"@types/yargs": "^17.0.33",
|
||||||
|
"@vercel/ncc": "^0.38.4",
|
||||||
"@vitest/coverage-v8": "3.2.4",
|
"@vitest/coverage-v8": "3.2.4",
|
||||||
"eslint": "9.32.0",
|
"eslint": "9.36.0",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "10.1.8",
|
||||||
"eslint-plugin-import": "2.32.0",
|
"eslint-plugin-import": "2.32.0",
|
||||||
"eslint-plugin-prettier": "5.5.4",
|
"eslint-plugin-prettier": "5.5.4",
|
||||||
|
"execa": "^9.6.0",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"typescript": "5.9.2",
|
"typescript": "5.9.3",
|
||||||
"typescript-eslint": "8.39.0",
|
"typescript-eslint": "8.45.0",
|
||||||
"vitest": "3.2.4"
|
"vitest": "3.2.4",
|
||||||
|
"yargs": "^18.0.0"
|
||||||
},
|
},
|
||||||
"name": "@morten-olsen/with-ssm",
|
"name": "@morten-olsen/with-ssm",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0"
|
||||||
"dependencies": {
|
|
||||||
"@aws-sdk/client-ssm": "^3.859.0",
|
|
||||||
"dotenv": "^17.2.1",
|
|
||||||
"execa": "^9.6.0",
|
|
||||||
"yargs": "^18.0.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
2515
pnpm-lock.yaml
generated
2515
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,7 @@ const argv = await yargs(hideBin(process.argv))
|
|||||||
alias: 'f',
|
alias: 'f',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description: 'The file to use for environment variables. (multiple files can be specified)',
|
description: 'The file to use for environment variables. (multiple files can be specified)',
|
||||||
default: ['.env', '.env.with-ssm'],
|
default: ['.env.with-ssm', '.env'],
|
||||||
})
|
})
|
||||||
.demandCommand(1, 'Error: You must provide a command to execute after --')
|
.demandCommand(1, 'Error: You must provide a command to execute after --')
|
||||||
.alias('h', 'help')
|
.alias('h', 'help')
|
||||||
@@ -36,7 +36,10 @@ if (!command) {
|
|||||||
|
|
||||||
const files = argv.file && Array.isArray(argv.file) ? argv.file : [argv.file];
|
const files = argv.file && Array.isArray(argv.file) ? argv.file : [argv.file];
|
||||||
const hostEnv = await getEnv(files);
|
const hostEnv = await getEnv(files);
|
||||||
const env = await replaceParams(hostEnv);
|
const env = await replaceParams(hostEnv, {
|
||||||
|
region: argv.region,
|
||||||
|
profile: argv.profile,
|
||||||
|
});
|
||||||
|
|
||||||
exec({
|
exec({
|
||||||
command,
|
command,
|
||||||
|
|||||||
20
src/utils/aws.ts
Normal file
20
src/utils/aws.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts';
|
||||||
|
|
||||||
|
const ensureAWS = async (region?: string, profile?: string) => {
|
||||||
|
const sts = new STSClient({
|
||||||
|
region,
|
||||||
|
profile,
|
||||||
|
});
|
||||||
|
|
||||||
|
const command = new GetCallerIdentityCommand({});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sts.send(command);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
console.error('Failed to get caller identity', errorMessage);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { ensureAWS };
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
const splitArgs = (args: string[]) => {
|
|
||||||
const separatorIndex = args.indexOf('--');
|
|
||||||
const actionArgs = args.slice(0, separatorIndex);
|
|
||||||
const command = args[separatorIndex + 1];
|
|
||||||
const commandArgs = args.slice(separatorIndex + 2);
|
|
||||||
|
|
||||||
return {
|
|
||||||
actionArgs,
|
|
||||||
command,
|
|
||||||
commandArgs,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export { splitArgs };
|
|
||||||
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs';
|
|||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
|
|
||||||
import { parse } from 'dotenv';
|
import { parse } from '@dotenvx/dotenvx';
|
||||||
|
|
||||||
import { debug } from './debug.js';
|
import { debug } from './debug.js';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { GetParametersCommand, SSMClient } from '@aws-sdk/client-ssm';
|
import { GetParametersCommand, SSMClient, type Parameter } from '@aws-sdk/client-ssm';
|
||||||
|
|
||||||
import { debug } from './debug.js';
|
import { debug } from './debug.js';
|
||||||
|
import { ensureAWS } from './aws.js';
|
||||||
|
|
||||||
const PREFIX = 'SSM:';
|
const PREFIX = 'SSM:';
|
||||||
|
|
||||||
@@ -13,11 +14,6 @@ const replaceParams = async (
|
|||||||
env: Record<string, string | undefined>,
|
env: Record<string, string | undefined>,
|
||||||
{ region, profile }: ReplaceParamsOptions = {},
|
{ region, profile }: ReplaceParamsOptions = {},
|
||||||
) => {
|
) => {
|
||||||
const ssm = new SSMClient({
|
|
||||||
region,
|
|
||||||
profile,
|
|
||||||
});
|
|
||||||
|
|
||||||
const names = Object.entries(env)
|
const names = Object.entries(env)
|
||||||
.filter(([, value]) => value?.startsWith(PREFIX))
|
.filter(([, value]) => value?.startsWith(PREFIX))
|
||||||
.map(([, value]) => value?.slice(PREFIX.length))
|
.map(([, value]) => value?.slice(PREFIX.length))
|
||||||
@@ -30,18 +26,47 @@ const replaceParams = async (
|
|||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await ensureAWS(region, profile);
|
||||||
|
const ssm = new SSMClient({
|
||||||
|
region,
|
||||||
|
profile,
|
||||||
|
});
|
||||||
|
// Chunk names into groups of 10 (AWS SSM GetParametersCommand limit)
|
||||||
|
const chunks: string[][] = [];
|
||||||
|
debug(`Chunking ${names.length} names into groups of 10`);
|
||||||
|
for (let i = 0; i < names.length; i += 10) {
|
||||||
|
chunks.push(names.slice(i, i + 10));
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(`Processing ${chunks.length} chunks`);
|
||||||
|
|
||||||
|
// Fetch parameters in chunks and combine results
|
||||||
|
const allParams: Parameter[] = [];
|
||||||
|
const allInvalidParams: string[] = [];
|
||||||
|
|
||||||
|
for (const chunk of chunks) {
|
||||||
const command = new GetParametersCommand({
|
const command = new GetParametersCommand({
|
||||||
Names: names,
|
Names: chunk,
|
||||||
WithDecryption: true,
|
WithDecryption: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await ssm.send(command);
|
const response = await ssm.send(command);
|
||||||
if (response.InvalidParameters?.length || 0 > 0) {
|
|
||||||
console.error('Invalid SSM parameters', response.InvalidParameters);
|
if (response.Parameters) {
|
||||||
|
allParams.push(...response.Parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.InvalidParameters) {
|
||||||
|
allInvalidParams.push(...response.InvalidParameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allInvalidParams.length > 0) {
|
||||||
|
console.error('Invalid SSM parameters', allInvalidParams);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const params = response.Parameters ?? [];
|
const params = allParams;
|
||||||
|
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(env).map(([key, value]) => {
|
Object.entries(env).map(([key, value]) => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
"noEmit": true,
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user