Skip to content

Commit dcac8c6

Browse files
author
Siddharth Shah
committed
feat: ✨ serverless-dynamic-secrets plugin created and publishing first time
0 parents  commit dcac8c6

8 files changed

Lines changed: 6677 additions & 0 deletions

File tree

.github/workflows/release.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: CI/CD & Semantic Release
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
11+
jobs:
12+
test:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout Code
16+
uses: actions/checkout@v3
17+
18+
- name: Set up Node.js
19+
uses: actions/setup-node@v3
20+
with:
21+
node-version: 22
22+
cache: 'yarn'
23+
24+
- name: Install Dependencies
25+
run: yarn install --frozen-lockfile
26+
27+
- name: Run Security Audit (Critical Issues Only)
28+
run: yarn audit --level critical --groups dependencies
29+
30+
release:
31+
runs-on: ubuntu-latest
32+
needs: test
33+
steps:
34+
- name: Checkout Code
35+
uses: actions/checkout@v3
36+
37+
- name: Set up Node.js
38+
uses: actions/setup-node@v3
39+
with:
40+
node-version: 22
41+
registry-url: "https://registry.npmjs.org/"
42+
cache: 'yarn'
43+
44+
- name: Install Dependencies
45+
run: yarn install --frozen-lockfile
46+
47+
- name: Semantic Release
48+
env:
49+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
50+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
51+
run: npx semantic-release

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules/
2+
.serverless/

.releaserc.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"branches": ["main"],
3+
"plugins": [
4+
"@semantic-release/commit-analyzer",
5+
"@semantic-release/release-notes-generator",
6+
"@semantic-release/changelog",
7+
[
8+
"@semantic-release/npm",
9+
{
10+
"npmPublish": true
11+
}
12+
],
13+
[
14+
"@semantic-release/github",
15+
{
16+
"assets": []
17+
}
18+
],
19+
[
20+
"@semantic-release/git",
21+
{
22+
"assets": ["package.json", "CHANGELOG.md"],
23+
"message": "chore(release): ${nextRelease.version} [skip ci]"
24+
}
25+
]
26+
]
27+
}
28+

LICENCE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2021 Ryan Sonshine
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Serverless Dynamic Secrets Plugin
2+
3+
🚀 **Automate Parameter & Secret Management in Serverless Framework**
4+
5+
6+
## 📌 Overview
7+
8+
**Serverless Dynamic Secrets Plugin** is a **custom Serverless Framework plugin** that:
9+
- **Automatically creates CloudFormation parameters** from a JSON file.
10+
- **Generates AWS Secrets Manager resources dynamically** using parameter values.
11+
- **Supports parameter overrides** via CLI and files.
12+
- **Prevents secret exposure** by setting `NoEcho: true`.
13+
14+
This eliminates the **manual effort of defining parameters and secrets** in your `serverless.yml`!
15+
16+
---
17+
18+
## 🛠 Installation
19+
20+
### Using npm
21+
```bash
22+
npm install --save-dev serverless-dynamic-secrets
23+
```
24+
25+
### Using yarn
26+
```bash
27+
yarn add -D serverless-dynamic-secrets
28+
```

index.js

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
const fs = require('fs');
2+
const parseArgs = require('yargs-parser');
3+
const OPTION_PARAMETER_FILE = 'parameter-file';
4+
const OPTION_PARAMETER_OVERRIDES = 'parameter-overrides';
5+
6+
module.exports = class CfParametersPlugin {
7+
constructor(serverless, options) {
8+
console.log("Dynamic CF Parameters and Secrets Plugin Initialized");
9+
this.serverless = serverless;
10+
this.options = options;
11+
12+
this.commands = {
13+
deploy: {
14+
options: {
15+
[OPTION_PARAMETER_FILE]: {
16+
usage: 'Provide a JSON file to define secrets and parameters',
17+
required: true,
18+
type: 'string'
19+
},
20+
[OPTION_PARAMETER_OVERRIDES]: {
21+
usage: 'Override parameters dynamically',
22+
required: false,
23+
type: 'multiple'
24+
}
25+
}
26+
},
27+
package: {
28+
options: {
29+
[OPTION_PARAMETER_FILE]: {
30+
usage: 'Provide a JSON file for parameter overrides',
31+
required: false,
32+
type: 'string'
33+
}
34+
}
35+
}
36+
};
37+
this.hooks = {
38+
'before:package:finalize': this.addSecretsAndParameters.bind(this),
39+
'before:aws:deploy:deploy:updateStack': this.injectParameterOverrides.bind(this)
40+
};
41+
this.providerRequest = this.getProvider().request.bind(this.provider);
42+
43+
}
44+
45+
addSecretsAndParameters() {
46+
const parameterFile = this.options[OPTION_PARAMETER_FILE];
47+
if (!parameterFile) {
48+
throw new Error("The --parameter-file option is required to define secrets and parameters.");
49+
}
50+
51+
const secretsData = JSON.parse(fs.readFileSync(parameterFile, 'utf-8'));
52+
const cloudFormationTemplate = this.serverless.service.provider.compiledCloudFormationTemplate;
53+
54+
// console.log("Loaded Secrets Data:", JSON.stringify(secretsData, null, 2));
55+
56+
// Add Parameters
57+
// Note: Here need to enhancement to check if same name of other resource or paramteres exist in stack or not
58+
cloudFormationTemplate.Parameters = {
59+
...(cloudFormationTemplate.Parameters || {}),
60+
...this.generateParameters(secretsData)
61+
};
62+
63+
// console.log("Generated Parameters:", JSON.stringify(cloudFormationTemplate.Parameters, null, 2));
64+
65+
// Add Secrets Manager Resources
66+
// Note: Here need to enhancement to check if same name of other resource or secret manager exist in stack or not
67+
cloudFormationTemplate.Resources = {
68+
...(cloudFormationTemplate.Resources || {}),
69+
...this.generateSecrets(secretsData)
70+
};
71+
72+
// console.log("Generated Secrets Manager Resources:", JSON.stringify(cloudFormationTemplate.Resources, null, 2));
73+
}
74+
75+
generateParameters(secretsData) {
76+
const parameters = {};
77+
78+
for (const [key] of Object.entries(secretsData)) {
79+
parameters[key] = {
80+
Type: 'String',
81+
NoEcho: true
82+
};
83+
}
84+
85+
return parameters;
86+
}
87+
88+
generateSecrets(secretsData) {
89+
const resources = {};
90+
91+
for (const [key] of Object.entries(secretsData)) {
92+
resources[`SecretFor${key}`] = {
93+
Type: 'AWS::SecretsManager::Secret',
94+
Properties: {
95+
Name: key,
96+
Description: `Secret dynamically created for ${key}`,
97+
SecretString: {
98+
Ref: key
99+
}
100+
}
101+
};
102+
}
103+
104+
return resources;
105+
}
106+
getProvider() {
107+
if (!this.provider) {
108+
this.provider = this.serverless.getProvider('aws');
109+
if (!this.provider) {
110+
throw new Error("AWS provider not found. Ensure this plugin is used with the AWS provider.");
111+
}
112+
}
113+
return this.provider;
114+
}
115+
injectParameterOverrides() {
116+
117+
this.provider.request = async (...args) => {
118+
let [service, method, params] = args;
119+
120+
121+
const compiledParametersTemplate =
122+
this.serverless.service.provider.compiledCloudFormationTemplate.Parameters || {};
123+
// console.log("compiledParametersTemplate:", compiledParametersTemplate);
124+
125+
if (service === 'CloudFormation' && (method === 'updateStack' || method === 'createChangeSet')) {
126+
// Fetch current parameters from the deployed template
127+
const response = await this.providerRequest('CloudFormation', 'getTemplate', {
128+
StackName: params.StackName
129+
});
130+
const currentParameters = JSON.parse(response.TemplateBody).Parameters || {};
131+
// console.log("currentParameters:", JSON.stringify(currentParameters));
132+
133+
// Build the list of parameters
134+
const overrides = this.getOverrides();
135+
// console.log("overrides:", JSON.stringify(overrides));
136+
137+
params.Parameters = Object.keys(compiledParametersTemplate)
138+
.map((paramKey) => {
139+
if (overrides[paramKey] !== undefined) {
140+
return {
141+
ParameterKey: paramKey,
142+
ParameterValue: overrides[paramKey]
143+
};
144+
} else if (currentParameters[paramKey]) {
145+
return {
146+
ParameterKey: paramKey,
147+
UsePreviousValue: true
148+
};
149+
}
150+
})
151+
.filter(Boolean);
152+
153+
// console.log("Final Parameters:", JSON.stringify(params.Parameters));
154+
}
155+
156+
return this.providerRequest(...args);
157+
};
158+
}
159+
160+
161+
162+
163+
getOverrides() {
164+
let parameterOverrides = this.options[OPTION_PARAMETER_OVERRIDES] || [];
165+
if (!Array.isArray(parameterOverrides)) {
166+
parameterOverrides = [parameterOverrides];
167+
}
168+
169+
const cliOverrides = parseArgs(
170+
parameterOverrides.map((param) => `--${param}`).join(' '),
171+
{ configuration: { 'parse-numbers': false } }
172+
);
173+
// console.log("Parameter Overrides from CLI:", JSON.stringify(cliOverrides));
174+
175+
const fileOverrides = this.options[OPTION_PARAMETER_FILE]
176+
? JSON.parse(fs.readFileSync(this.options[OPTION_PARAMETER_FILE], 'utf-8'))
177+
: {};
178+
179+
// console.log("Parameter Overrides from File:", JSON.stringify(fileOverrides));
180+
181+
return { ...fileOverrides, ...cliOverrides };
182+
}
183+
};

package.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"name": "serverless-dynamic-secrets",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"description": "A Serverless Framework plugin for dynamic parameter and secret creation",
6+
"scripts": {
7+
"test": "jest",
8+
"lint": "eslint .",
9+
"semantic-release": "semantic-release"
10+
},
11+
"files": [
12+
"index.js",
13+
"package.json",
14+
"README.md"
15+
],
16+
"keywords": [
17+
"serverless",
18+
"plugin",
19+
"aws",
20+
"secrets-manager"
21+
],
22+
"repository": {
23+
"type": "git",
24+
"url": "https://github.com/distinction-dev/serverless-dynamic-secrets.git"
25+
},
26+
"publishConfig": {
27+
"access": "public"
28+
},
29+
"author": "Sid2601",
30+
"license": "MIT",
31+
"dependencies": {
32+
"yargs-parser": "^21.1.1"
33+
},
34+
"devDependencies": {
35+
"@semantic-release/changelog": "^6.0.0",
36+
"@semantic-release/git": "^10.0.0",
37+
"@semantic-release/github": "^10.0.0",
38+
"@semantic-release/npm": "^10.0.0",
39+
"jest": "^29.0.0",
40+
"semantic-release": "^20.0.0"
41+
}
42+
}

0 commit comments

Comments
 (0)