This repository has been archived by the owner on May 5, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 71
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add a new GitHub action to inject SSM secrets into builds
- Loading branch information
1 parent
9f2207d
commit f4b98c0
Showing
14 changed files
with
380 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
const core = jest.requireActual('@actions/core'); | ||
|
||
module.exports = { | ||
...core, | ||
setSecret: jest.fn(), | ||
exportVariable: jest.fn(), | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
export const mockGetParameter = jest.fn().mockImplementation(() => ({ | ||
promise: jest.fn().mockResolvedValue({ | ||
Parameter: { | ||
Value: 'super-secret-value', | ||
}, | ||
}), | ||
})); | ||
|
||
export default jest.fn().mockImplementation(() => ({ | ||
getParameter: mockGetParameter, | ||
})); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
/* eslint-disable @typescript-eslint/no-var-requires */ | ||
const eslintrc = require('../../.eslintrc'); | ||
module.exports = eslintrc; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
/* eslint-disable @typescript-eslint/no-var-requires */ | ||
const prettierrc = require('../../.prettierrc'); | ||
module.exports = prettierrc; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
# AWS SSM Build Secrets for GitHub Actions | ||
|
||
This action injects AWS SSM Parameter Store secrets as environment variables into your GitHub Actions builds. | ||
|
||
It makes it easier to follow [Amazon IAM best practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html) in respect to principle of least privilege and tracking credentials usage. Combined with the `aws-actions/configure-aws-credentials` action, this allows you to inject any combination of secrets from multiple stores, using different credential contexts. | ||
|
||
## Contents | ||
|
||
1. [Usage Examples](#usage-examples) | ||
1. [Supported Parameters](#supported-parameters) | ||
1. [Event Triggers](#event-triggers) | ||
1. [Versioning](#versioning) | ||
1. [How to get help](#how-to-get-help) | ||
1. [License](#license) | ||
|
||
> **NOTE**: The `marvinpinto/action-inject-ssm-secrets` repository is an automatically generated mirror of the [marvinpinto/actions](https://github.com/marvinpinto/actions) monorepo containing this and other actions. Please file issues and pull requests over there. | ||
## Usage Examples | ||
|
||
### Inject your production Cloudflare API tokens into a build | ||
|
||
```yaml | ||
--- | ||
name: "build-and-invalidate-cf-cache" | ||
|
||
on: | ||
push: | ||
branches: | ||
- "master" | ||
|
||
jobs: | ||
ci: | ||
runs-on: "ubuntu-latest" | ||
env: | ||
BUILD_STAGE: "production" | ||
|
||
steps: | ||
# ... | ||
|
||
- uses: "aws-actions/configure-aws-credentials@v1" | ||
with: | ||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | ||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | ||
aws-region: "us-east-1" | ||
role-to-assume: "arn:aws:iam::111111111111:role/build-and-deploy-website" | ||
role-duration-seconds: 1800 # 30 mins | ||
|
||
- uses: "marvinpinto/action-inject-ssm-secrets@v1" | ||
with: | ||
ssm_parameter: "/build-secrets/${{ env.BUILD_STAGE }}/cloudflare-account-id" | ||
env_variable_name: "cloudflare_account_id" | ||
|
||
- uses: "marvinpinto/action-inject-ssm-secrets@v1" | ||
with: | ||
ssm_parameter: "/build-secrets/${{ env.BUILD_STAGE }}/cloudflare-api-token" | ||
env_variable_name: "cloudflare_api_token" | ||
|
||
- name: "Build & Deploy" | ||
run: | | ||
echo "You will now have access to the CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN environment variables in all your subsequent build steps" | ||
``` | ||
## Supported Parameters | ||
| Parameter | Description | Default | | ||
| ---------------- | -------------------------------------------------------------------------------------------------------- | ------- | | ||
| `parameters`\*\* | A mapped object consisting of an environment variable (to set), and its corresponding AWS SSM parameter. | `null` | | ||
|
||
### Notes: | ||
|
||
- Parameters denoted with `**` are required. | ||
|
||
## Versioning | ||
|
||
Every commit that lands on master for this project triggers an automatic build as well as a tagged release called `latest`. If you don't wish to live on the bleeding edge you may use a stable release instead. See [releases](../../releases/latest) for the available versions. | ||
|
||
```yaml | ||
- uses: "marvinpinto/action-inject-ssm-secrets@<VERSION>" | ||
``` | ||
|
||
## How to get help | ||
|
||
The main [README](https://github.com/marvinpinto/actions/blob/master/README.md) for this project has a bunch of information related to debugging & submitting issues. If you're still stuck, try and get a hold of me on [keybase](https://keybase.io/marvinpinto) and I will do my best to help you out. | ||
|
||
## License | ||
|
||
The source code for this project is released under the [MIT License](/LICENSE). This project is not associated with GitHub or AWS. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import * as core from '@actions/core'; | ||
import {mockGetParameter} from 'aws-sdk/clients/ssm'; | ||
import {main} from '../src/main'; | ||
|
||
describe('main handler', () => { | ||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
process.env['INPUT_SSM_PARAMETER'] = '/build-secrets/production/cloudflare-account-id'; | ||
process.env['INPUT_ENV_VARIABLE_NAME'] = 'cloudflare_account_id'; | ||
}); | ||
|
||
afterEach(() => { | ||
delete process.env.INPUT_SSM_PARAMETER; | ||
delete process.env.INPUT_ENV_VARIABLE_NAME; | ||
}); | ||
|
||
it('throws an error if any of the required parameters is not supplied', async () => { | ||
delete process.env.INPUT_SSM_PARAMETER; | ||
await expect(main()).rejects.toThrow('Input required and not supplied: ssm_parameter'); | ||
}); | ||
|
||
it('is able to successfully inject a secret as an environment variable', async () => { | ||
await main(); | ||
|
||
expect(mockGetParameter).toHaveBeenCalledTimes(1); | ||
expect(mockGetParameter).toHaveBeenCalledWith({ | ||
Name: '/build-secrets/production/cloudflare-account-id', | ||
WithDecryption: true, | ||
}); | ||
|
||
expect(core.setSecret).toHaveBeenCalledTimes(1); | ||
expect(core.setSecret).toHaveBeenCalledWith('super-secret-value'); | ||
|
||
expect(core.exportVariable).toHaveBeenCalledTimes(1); | ||
expect(core.exportVariable).toHaveBeenCalledWith('CLOUDFLARE_ACCOUNT_ID', 'super-secret-value'); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
name: "AWS SSM Build Secrets for GitHub Actions" | ||
author: "marvinpinto" | ||
description: "Inject AWS SSM Parameter Store secrets as enviroment variables into your GitHub Actions builds" | ||
inputs: | ||
ssm_parameter: | ||
description: "The SSM key to look up" | ||
required: true | ||
env_variable_name: | ||
description: "The corresponding environment variable name to assign the secret to" | ||
required: true | ||
runs: | ||
using: "node12" | ||
main: "dist/index.js" | ||
branding: | ||
icon: "lock" | ||
color: "orange" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
{ | ||
"name": "aws-ssm-secrets", | ||
"version": "1.0.1", | ||
"private": true, | ||
"description": "Inject AWS SSM Parameter Store secrets as enviroment variables into your GitHub Actions builds", | ||
"config": { | ||
"eslintPaths": "src/**/*.ts __tests__/**/*.ts .*.js *.js", | ||
"prettierPaths": "**/*.{json,md,yaml,yml} !package.json" | ||
}, | ||
"scripts": { | ||
"build": "webpack --config webpack.config.js --colors", | ||
"clean": "rm -rf node_modules yarn-error.log dist", | ||
"lint": "yarn run lint:eslint && yarn run lint:prettier", | ||
"lint:eslint": "eslint --color --max-warnings=0 $npm_package_config_eslintPaths", | ||
"lint:prettier": "prettier --color --list-different $npm_package_config_prettierPaths", | ||
"lintfix": "yarn run lintfix:eslint && yarn run lintfix:prettier", | ||
"lintfix:eslint": "eslint --color --fix $npm_package_config_eslintPaths", | ||
"lintfix:prettier": "prettier --color --write $npm_package_config_prettierPaths" | ||
}, | ||
"dependencies": { | ||
"@actions/core": "^1.2.5", | ||
"aws-sdk": "^2.748.0" | ||
}, | ||
"main": "dist/index.js", | ||
"license": "MIT", | ||
"eslintIgnore": [ | ||
"!.*.js" | ||
], | ||
"devDependencies": { | ||
"terser-webpack-plugin": "^2.3.1", | ||
"ts-loader": "^6.2.1", | ||
"webpack": "^4.41.4", | ||
"webpack-cli": "^3.3.10" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
import {main} from './main'; | ||
main(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import * as core from '@actions/core'; | ||
import SSM from 'aws-sdk/clients/ssm'; | ||
|
||
type ActionParams = { | ||
envVariable: string; | ||
ssmParameter: string; | ||
}; | ||
|
||
const getAndValidateArgs = (): ActionParams => { | ||
core.startGroup('Validating action arguments'); | ||
|
||
const ssmParameter = core.getInput('ssm_parameter', {required: true}); | ||
const envVariable = core.getInput('env_variable_name', {required: true}); | ||
|
||
const param: ActionParams = { | ||
envVariable, | ||
ssmParameter, | ||
}; | ||
core.debug(`Final actionParam: ${JSON.stringify(param)}`); | ||
|
||
core.info('Validation successful'); | ||
core.endGroup(); | ||
|
||
return param; | ||
}; | ||
|
||
export const main = async () => { | ||
const actionParam = getAndValidateArgs(); | ||
const ssm = new SSM(); | ||
core.startGroup('Injecting secret environment variables'); | ||
|
||
const result = await ssm | ||
.getParameter({ | ||
Name: actionParam.ssmParameter, | ||
WithDecryption: true, // NOTE: this flag is ignored for String and StringList parameter types | ||
}) | ||
.promise(); | ||
|
||
const envVar = actionParam.envVariable.toUpperCase(); | ||
const secret = result?.Parameter?.Value; | ||
// istanbul ignore next | ||
if (!secret) { | ||
core.warning(`Secret value for environment variable ${envVar} appears to be empty`); | ||
} | ||
|
||
core.setSecret(secret || ''); | ||
core.exportVariable(envVar, secret); | ||
core.info(`Successfully set secret environment variable: ${envVar}`); | ||
|
||
core.endGroup(); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"extends": "../../tsconfig-base", | ||
"rootDir": "./src" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
/* eslint-disable @typescript-eslint/no-var-requires */ | ||
const path = require('path'); | ||
const webpack = require('webpack'); | ||
const TerserPlugin = require('terser-webpack-plugin'); | ||
|
||
module.exports = { | ||
target: 'node', | ||
mode: 'production', | ||
|
||
entry: { | ||
index: './src/index.ts', | ||
}, | ||
|
||
module: { | ||
rules: [ | ||
{ | ||
test: /\.tsx?$/, | ||
use: 'ts-loader', | ||
exclude: /node_modules/, | ||
}, | ||
], | ||
}, | ||
|
||
resolve: { | ||
extensions: ['.tsx', '.ts', '.js'], | ||
mainFields: ['main'], | ||
}, | ||
|
||
output: { | ||
filename: '[name].js', | ||
path: path.resolve(__dirname, 'dist'), | ||
library: 'main', | ||
libraryTarget: 'commonjs2', | ||
}, | ||
|
||
optimization: { | ||
minimize: true, | ||
minimizer: [ | ||
new TerserPlugin({ | ||
terserOptions: { | ||
output: { | ||
comments: false, | ||
}, | ||
}, | ||
sourceMap: true, | ||
extractComments: false, | ||
}), | ||
], | ||
}, | ||
|
||
plugins: [new webpack.IgnorePlugin(/\/iconv-loader$/)], | ||
}; |
Oops, something went wrong.