-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #15 from reapit/construct/remote-parameters
construct: add cross-region-stack-export
- Loading branch information
Showing
33 changed files
with
615 additions
and
101 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,4 @@ | ||
src | ||
tests | ||
.eslintrc.js | ||
tsconfig.json |
52 changes: 52 additions & 0 deletions
52
packages/constructs/cross-region-stack-export/package.json
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 @@ | ||
{ | ||
"name": "@reapit-cdk/cross-region-stack-export", | ||
"version": "0.0.0", | ||
"description": "Allows you to share values between stack across regions and accounts.", | ||
"homepage": "https://github.com/reapit/ts-cdk-constructs/blob/main/packages/constructs/cross-region-stack-export", | ||
"readme": "https://github.com/reapit/ts-cdk-constructs/blob/main/packages/constructs/cross-region-stack-export/readme.md", | ||
"bugs": { | ||
"url": "https://github.com/reapit/ts-cdk-constructs/issues" | ||
}, | ||
"license": "MIT", | ||
"author": { | ||
"name": "Josh Balfour", | ||
"email": "jbalfour@reapit.com" | ||
}, | ||
"repository": { | ||
"url": "https://github.com/reapit/ts-cdk-constructs.git" | ||
}, | ||
"scripts": { | ||
"build": "reapit-cdk-tsup --lambda", | ||
"check": "yarn run root:check -p $(pwd)", | ||
"lint": "reapit-cdk-eslint", | ||
"test": "yarn run root:test -- $(pwd)", | ||
"prepack": "reapit-version-package && yarn build", | ||
"integ": "yarn run root:integ -- $(pwd)", | ||
"jsii:build": "rpt-cdk-jsii", | ||
"jsii:publish": "rpt-cdk-jsii --publish" | ||
}, | ||
"main": "src/index.ts", | ||
"types": "src/index.ts", | ||
"publishConfig": { | ||
"main": "dist/index.js", | ||
"types": "dist/index.d.ts" | ||
}, | ||
"peerDependencies": { | ||
"aws-cdk-lib": "^2.96.2", | ||
"constructs": "^10.2.70" | ||
}, | ||
"devDependencies": { | ||
"@aws-sdk/client-ssm": "3.414.0", | ||
"@aws-sdk/client-sts": "3.414.0", | ||
"@reapit-cdk/custom-resource-wrapper": "workspace:^", | ||
"@reapit-cdk/eslint-config": "workspace:^", | ||
"@reapit-cdk/integration-tests": "workspace:^", | ||
"@reapit-cdk/jsii": "workspace:^", | ||
"@reapit-cdk/tsup": "workspace:^", | ||
"@reapit-cdk/version-package": "workspace:^", | ||
"aws-cdk-lib": "^2.96.2", | ||
"aws-lambda": "^1.0.7", | ||
"aws-sdk-client-mock": "^3.0.0", | ||
"constructs": "^10.2.70" | ||
} | ||
} |
65 changes: 65 additions & 0 deletions
65
packages/constructs/cross-region-stack-export/src/cross-region-stack-export.ts
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,65 @@ | ||
import { PhysicalName, Stack, Token } from 'aws-cdk-lib' | ||
import { AccountPrincipal, ManagedPolicy, Role } from 'aws-cdk-lib/aws-iam' | ||
import { StringParameter } from 'aws-cdk-lib/aws-ssm' | ||
import { RemoteParameters } from './remote-parameters' | ||
import { Construct } from 'constructs' | ||
|
||
export class CrossRegionStackImport<ExportKey> extends Construct { | ||
exporter: CrossRegionStackExport<ExportKey> | ||
parameters: RemoteParameters | ||
constructor(scope: Construct, id: string, fromExporter: CrossRegionStackExport<ExportKey>, roleArn: string) { | ||
super(scope, id) | ||
this.exporter = fromExporter | ||
|
||
const stack = Stack.of(this) | ||
this.parameters = new RemoteParameters(this, 'parameters', { | ||
path: this.exporter.parameterPath, | ||
region: this.exporter.sourceStack.region, | ||
role: Role.fromRoleArn(this, 'readOnlyRole', roleArn), | ||
}) | ||
stack.addDependency(this.exporter.sourceStack) | ||
} | ||
|
||
getValue(stackExport: ExportKey | string) { | ||
return this.parameters.get(this.exporter.getParameterName(stackExport)) | ||
} | ||
} | ||
|
||
export class CrossRegionStackExport<ExportKey> extends Construct { | ||
parameterPath: string | ||
sourceStack: Stack | ||
|
||
constructor(scope: Construct, id: string) { | ||
super(scope, id) | ||
const stack = Stack.of(scope) | ||
this.sourceStack = stack | ||
if (Token.isUnresolved(stack.account)) { | ||
throw new Error('stack account is unresolved') | ||
} | ||
if (Token.isUnresolved(stack.region)) { | ||
throw new Error('stack region is unresolved') | ||
} | ||
this.parameterPath = `/${stack.account}/${stack.region}/${stack.stackName}/exports` | ||
} | ||
|
||
getParameterName(stackExport: ExportKey | string) { | ||
return `${this.parameterPath}/${stackExport}` | ||
} | ||
|
||
setValue(id: ExportKey | string, value: string) { | ||
new StringParameter(this, id as string, { | ||
parameterName: this.getParameterName(id), | ||
stringValue: value, | ||
}) | ||
} | ||
|
||
getImporter(scope: Construct, id: string) { | ||
const { account } = Stack.of(scope) | ||
const cdkReadOnlyRole = new Role(this, `${account}-readOnlyRole`, { | ||
assumedBy: new AccountPrincipal(account), | ||
roleName: PhysicalName.GENERATE_IF_NEEDED, | ||
managedPolicies: [ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMReadOnlyAccess')], | ||
}) | ||
return new CrossRegionStackImport<ExportKey>(scope, id, this, cdkReadOnlyRole.roleArn) | ||
} | ||
} |
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 @@ | ||
export * from './remote-parameters' | ||
export * from './cross-region-stack-export' |
79 changes: 79 additions & 0 deletions
79
packages/constructs/cross-region-stack-export/src/lambda/get-parameters.ts
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,79 @@ | ||
import { AssumeRoleCommand, STSClient } from '@aws-sdk/client-sts' | ||
import { Parameter, SSMClient, paginateGetParametersByPath } from '@aws-sdk/client-ssm' | ||
|
||
const getSsmClient = async (region: string, roleArn?: string, sessionName?: string): Promise<SSMClient> => { | ||
if (roleArn && sessionName) { | ||
const client = new STSClient({}) | ||
console.log('assuming role', roleArn) | ||
const res = await client.send( | ||
new AssumeRoleCommand({ | ||
RoleArn: roleArn, | ||
RoleSessionName: sessionName, | ||
}), | ||
) | ||
const { Credentials } = res | ||
if (!Credentials) { | ||
throw new Error(`no credentials after assuming role ${roleArn}`) | ||
} | ||
const { AccessKeyId, Expiration, SecretAccessKey, SessionToken } = Credentials | ||
if (!AccessKeyId || !SecretAccessKey) { | ||
throw new Error(`no AccessKeyId and/or SecretAccessKey after assuming role ${roleArn}`) | ||
} | ||
console.log('role assumed', roleArn) | ||
return new SSMClient({ | ||
region, | ||
credentials: { | ||
accessKeyId: AccessKeyId, | ||
secretAccessKey: SecretAccessKey, | ||
expiration: Expiration, | ||
sessionToken: SessionToken, | ||
}, | ||
}) | ||
} | ||
|
||
if (roleArn && !sessionName) { | ||
throw new Error('recieved roleArn but no sessionName') | ||
} | ||
|
||
console.log('no role, not assuming') | ||
return new SSMClient({ | ||
region, | ||
}) | ||
} | ||
|
||
const getPaginatedParams = async (ssmClient: SSMClient, path: string): Promise<Parameter[]> => { | ||
console.log('getting params from', path) | ||
const paginator = paginateGetParametersByPath( | ||
{ | ||
client: ssmClient, | ||
}, | ||
{ | ||
Path: path, | ||
Recursive: true, | ||
WithDecryption: true, | ||
MaxResults: 100, | ||
}, | ||
) | ||
const results = [] | ||
for await (const page of paginator) { | ||
results.push(...(page.Parameters || [])) | ||
console.log('paging', results.length) | ||
} | ||
return results | ||
} | ||
|
||
export const getParameters = async (region: string, path: string, roleArn: string, sessionName: string) => { | ||
const ssmClient = await getSsmClient(region, roleArn, sessionName) | ||
|
||
const params: Record<string, string> = {} | ||
const results = await getPaginatedParams(ssmClient, path) | ||
|
||
results.forEach(({ Name, Value }) => { | ||
if (Name && Value) { | ||
params[Name] = Value | ||
} | ||
}) | ||
|
||
console.log('done') | ||
return params | ||
} |
9 changes: 9 additions & 0 deletions
9
packages/constructs/cross-region-stack-export/src/lambda/lambda.ts
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,9 @@ | ||
import { getParameters } from './get-parameters' | ||
import { customResourceWrapper } from '@reapit-cdk/custom-resource-wrapper' | ||
|
||
export const onEvent = customResourceWrapper({ | ||
onCreate: ({ stackName, regionName, parameterPath, role }) => | ||
getParameters(regionName, parameterPath, role, stackName), | ||
onUpdate: ({ stackName, regionName, parameterPath, role }) => | ||
getParameters(regionName, parameterPath, role, stackName), | ||
}) |
99 changes: 99 additions & 0 deletions
99
packages/constructs/cross-region-stack-export/src/remote-parameters.ts
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,99 @@ | ||
import { Stack, CustomResource, aws_iam as iam, aws_logs as logs, custom_resources as cr, Duration } from 'aws-cdk-lib' | ||
import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda' | ||
import { Construct } from 'constructs' | ||
import * as path from 'path' | ||
|
||
/** | ||
* Properties of the RemoteParameters | ||
*/ | ||
export interface RemoteParametersProps { | ||
// /** | ||
// * The remote CDK stack to get the parameters from. | ||
// */ | ||
// readonly stack: cdk.Stack; | ||
/** | ||
* The region code of the remote stack. | ||
*/ | ||
readonly region: string | ||
/** | ||
* The assumed role used to get remote parameters. | ||
*/ | ||
readonly role?: iam.IRole | ||
/** | ||
* The parameter path. | ||
*/ | ||
readonly path: string | ||
/** | ||
* Indicate whether always update the custom resource to get the new stack output | ||
* @default true | ||
*/ | ||
readonly alwaysUpdate?: boolean | ||
} | ||
|
||
/** | ||
* Represents the RemoteParameters of the remote CDK stack | ||
*/ | ||
export class RemoteParameters extends Construct { | ||
/** | ||
* The parameters in the SSM parameter store for the remote stack. | ||
*/ | ||
readonly parameters: CustomResource | ||
|
||
constructor(scope: Construct, id: string, props: RemoteParametersProps) { | ||
super(scope, id) | ||
|
||
const onEvent = new Function(this, 'func', { | ||
handler: 'lambda.onEvent', | ||
timeout: Duration.seconds(60), | ||
runtime: Runtime.NODEJS_18_X, | ||
code: Code.fromAsset(path.join(__dirname, '..', 'dist', 'lambda')), | ||
}) | ||
|
||
const myProvider = new cr.Provider(this, 'MyProvider', { | ||
onEventHandler: onEvent, | ||
logRetention: logs.RetentionDays.ONE_DAY, | ||
}) | ||
|
||
onEvent.addToRolePolicy( | ||
new iam.PolicyStatement({ | ||
actions: ['ssm:GetParametersByPath'], | ||
resources: ['*'], | ||
}), | ||
) | ||
|
||
this.parameters = new CustomResource(this, 'SsmParameters', { | ||
serviceToken: myProvider.serviceToken, | ||
properties: { | ||
stackName: Stack.of(this).stackName, | ||
regionName: props.region, | ||
parameterPath: props.path, | ||
randomString: props.alwaysUpdate === false ? undefined : randomString(), | ||
role: props.role?.roleArn, | ||
}, | ||
}) | ||
|
||
if (props.role) { | ||
myProvider.onEventHandler.addToRolePolicy( | ||
new iam.PolicyStatement({ | ||
actions: ['sts:AssumeRole'], | ||
resources: [props.role.roleArn], | ||
}), | ||
) | ||
} | ||
} | ||
|
||
/** | ||
* Get the parameter. | ||
* @param key output key | ||
*/ | ||
public get(key: string) { | ||
return this.parameters.getAttString(key) | ||
} | ||
} | ||
|
||
function randomString() { | ||
// Crazy | ||
return Math.random() | ||
.toString(36) | ||
.replace(/[^a-z0-9]+/g, '') | ||
} |
26 changes: 26 additions & 0 deletions
26
packages/constructs/cross-region-stack-export/tests/construct.test.ts
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,26 @@ | ||
import { Template } from 'aws-cdk-lib/assertions' | ||
import * as cdk from 'aws-cdk-lib' | ||
import { RemoteParameters } from '../src' | ||
|
||
const synth = () => { | ||
const app = new cdk.App() | ||
const stack = new cdk.Stack(app) | ||
const remoteParameters = new RemoteParameters(stack, 'params', { | ||
path: 'asdf', | ||
region: 'eu-west-2', | ||
}) | ||
const template = Template.fromStack(stack) | ||
return { | ||
remoteParameters, | ||
template, | ||
stack, | ||
} | ||
} | ||
|
||
describe('active-ruleset', () => { | ||
test('synthesizes', () => { | ||
const { remoteParameters, template } = synth() | ||
expect(remoteParameters).toBeDefined() | ||
expect(template).toBeDefined() | ||
}) | ||
}) |
Oops, something went wrong.