-
Notifications
You must be signed in to change notification settings - Fork 1
New: [AEA-6258] - Add SSM Parameter construct #619
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 20 commits
02b0af2
455b657
26fbada
db95a9f
d3594ca
f3a9207
c7dbb39
f7e421d
2afccf1
dc6f2d3
bb83c29
74d0de2
c7d714d
f2e9325
0ac46b1
b500519
734254b
e26e480
50ebe0a
bd610d0
4912c82
a784142
7b17511
503d44c
0ae7177
2b92465
70e7f20
a5a01ac
3176a52
87cba70
dcab80b
1f2297d
f5a4f8d
d15b6f7
92376db
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,3 +27,4 @@ _site/ | |
| .jekyll-metadata | ||
| vendor | ||
| .trivy_out/ | ||
| *.tgz | ||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2,21 +2,36 @@ import {CloudFormationClient, DescribeStacksCommand} from "@aws-sdk/client-cloud | |||||
| import {S3Client, HeadObjectCommand} from "@aws-sdk/client-s3" | ||||||
| import {StandardStackProps} from "../apps/createApp" | ||||||
|
|
||||||
| export function getConfigFromEnvVar(varName: string, prefix: string = "CDK_CONFIG_"): string { | ||||||
| export function getConfigFromEnvVar( | ||||||
| varName: string, | ||||||
| prefix: string = "CDK_CONFIG_", | ||||||
| defaultValue: string | undefined = undefined | ||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know the idiomatic way to do this would be to use an options object, but that would break downstream things and for this I don't want to mess about with that if we can avoid it...
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
explicitly initialising to undefined feels a bit excessive. |
||||||
| ): string { | ||||||
| const value = process.env[prefix + varName] | ||||||
| if (!value) { | ||||||
| if (defaultValue !== undefined) { | ||||||
| return defaultValue | ||||||
| } | ||||||
| throw new Error(`Environment variable ${prefix}${varName} is not set`) | ||||||
| } | ||||||
|
wildjames marked this conversation as resolved.
|
||||||
| return value | ||||||
| } | ||||||
|
|
||||||
| export function getBooleanConfigFromEnvVar(varName: string, prefix: string = "CDK_CONFIG_"): boolean { | ||||||
| const value = getConfigFromEnvVar(varName, prefix) | ||||||
| export function getBooleanConfigFromEnvVar( | ||||||
| varName: string, | ||||||
| prefix: string = "CDK_CONFIG_", | ||||||
| defaultValue: string | undefined = undefined | ||||||
| ): boolean { | ||||||
| const value = getConfigFromEnvVar(varName, prefix, defaultValue) | ||||||
| return value.toLowerCase().trim() === "true" | ||||||
| } | ||||||
|
|
||||||
| export function getNumberConfigFromEnvVar(varName: string, prefix: string = "CDK_CONFIG_"): number { | ||||||
| const value = getConfigFromEnvVar(varName, prefix) | ||||||
| export function getNumberConfigFromEnvVar( | ||||||
| varName: string, | ||||||
| prefix: string = "CDK_CONFIG_", | ||||||
| defaultValue: string | undefined = undefined | ||||||
| ): number { | ||||||
| const value = getConfigFromEnvVar(varName, prefix, defaultValue) | ||||||
| return Number(value) | ||||||
| } | ||||||
|
|
||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,234 @@ | ||
| import {Fn, RemovalPolicy} from "aws-cdk-lib" | ||
| import { | ||
| CfnStage, | ||
| EndpointType, | ||
| LogGroupLogDestination, | ||
| MethodLoggingLevel, | ||
| MTLSConfig, | ||
| RestApi, | ||
| SecurityPolicy | ||
| } from "aws-cdk-lib/aws-apigateway" | ||
| import { | ||
| IManagedPolicy, | ||
| IRole, | ||
| ManagedPolicy, | ||
| PolicyStatement, | ||
| Role, | ||
| ServicePrincipal | ||
| } from "aws-cdk-lib/aws-iam" | ||
| import {Stream} from "aws-cdk-lib/aws-kinesis" | ||
| import {Key} from "aws-cdk-lib/aws-kms" | ||
| import {CfnSubscriptionFilter, LogGroup} from "aws-cdk-lib/aws-logs" | ||
| import {Construct} from "constructs" | ||
| import {accessLogFormat} from "./RestApiGateway/accessLogFormat.js" | ||
| import {Certificate, CertificateValidation} from "aws-cdk-lib/aws-certificatemanager" | ||
| import {Bucket} from "aws-cdk-lib/aws-s3" | ||
| import {BucketDeployment, Source} from "aws-cdk-lib/aws-s3-deployment" | ||
| import {ARecord, HostedZone, RecordTarget} from "aws-cdk-lib/aws-route53" | ||
| import {ApiGateway as ApiGatewayTarget} from "aws-cdk-lib/aws-route53-targets" | ||
| import {NagSuppressions} from "cdk-nag" | ||
|
|
||
| export interface RestApiGatewayProps { | ||
| readonly stackName: string | ||
| readonly logRetentionInDays: number | ||
| readonly mutualTlsTrustStoreKey: string | undefined | ||
| readonly forwardCsocLogs: boolean | ||
| readonly csocApiGatewayDestination: string | ||
| readonly executionPolicies: Array<IManagedPolicy> | ||
| } | ||
|
|
||
| export class RestApiGateway extends Construct { | ||
| public readonly api: RestApi | ||
| public readonly role: IRole | ||
|
|
||
| public constructor(scope: Construct, id: string, props: RestApiGatewayProps) { | ||
| super(scope, id) | ||
|
|
||
| if (props.forwardCsocLogs && props.csocApiGatewayDestination === "") { | ||
| throw new Error("csocApiGatewayDestination must be provided when forwardCsocLogs is true") | ||
| } | ||
|
|
||
| // Imports | ||
| const cloudWatchLogsKmsKey = Key.fromKeyArn( | ||
| this, "cloudWatchLogsKmsKey", Fn.importValue("account-resources:CloudwatchLogsKmsKeyArn")) | ||
|
|
||
| const splunkDeliveryStream = Stream.fromStreamArn( | ||
| this, "SplunkDeliveryStream", Fn.importValue("lambda-resources:SplunkDeliveryStream")) | ||
|
|
||
| const splunkSubscriptionFilterRole = Role.fromRoleArn( | ||
| this, "splunkSubscriptionFilterRole", Fn.importValue("lambda-resources:SplunkSubscriptionFilterRole")) | ||
|
|
||
| const trustStoreBucket = Bucket.fromBucketArn( | ||
| this, "TrustStoreBucket", Fn.importValue("account-resources:TrustStoreBucket")) | ||
|
|
||
| const trustStoreDeploymentBucket = Bucket.fromBucketArn( | ||
| this, "TrustStoreDeploymentBucket", Fn.importValue("account-resources:TrustStoreDeploymentBucket")) | ||
|
|
||
| const trustStoreBucketKmsKey = Key.fromKeyArn( | ||
| this, "TrustStoreBucketKmsKey", Fn.importValue("account-resources:TrustStoreBucketKMSKey")) | ||
|
|
||
| const epsDomainName: string = Fn.importValue("eps-route53-resources:EPS-domain") | ||
| const hostedZone = HostedZone.fromHostedZoneAttributes(this, "HostedZone", { | ||
| hostedZoneId: Fn.importValue("eps-route53-resources:EPS-ZoneID"), | ||
| zoneName: epsDomainName | ||
| }) | ||
| const serviceDomainName = `${props.stackName}.${epsDomainName}` | ||
|
|
||
| // Resources | ||
| const logGroup = new LogGroup(this, "ApiGatewayAccessLogGroup", { | ||
| encryptionKey: cloudWatchLogsKmsKey, | ||
| logGroupName: `/aws/apigateway/${props.stackName}-apigw`, | ||
| retention: props.logRetentionInDays, | ||
| removalPolicy: RemovalPolicy.DESTROY | ||
| }) | ||
|
|
||
| new CfnSubscriptionFilter(this, "ApiGatewayAccessLogsSplunkSubscriptionFilter", { | ||
| destinationArn: splunkDeliveryStream.streamArn, | ||
| filterPattern: "", | ||
| logGroupName: logGroup.logGroupName, | ||
| roleArn: splunkSubscriptionFilterRole.roleArn | ||
| }) | ||
|
|
||
| if (props.forwardCsocLogs) { | ||
| new CfnSubscriptionFilter(this, "ApiGatewayAccessLogsCSOCSubscriptionFilter", { | ||
| destinationArn: props.csocApiGatewayDestination, | ||
| filterPattern: "", | ||
| logGroupName: logGroup.logGroupName, | ||
| roleArn: splunkSubscriptionFilterRole.roleArn | ||
| }) | ||
| } | ||
|
|
||
| const certificate = new Certificate(this, "Certificate", { | ||
| domainName: serviceDomainName, | ||
| validation: CertificateValidation.fromDns(hostedZone) | ||
| }) | ||
|
|
||
| let mtlsConfig: MTLSConfig | undefined | ||
|
|
||
| if (props.mutualTlsTrustStoreKey) { | ||
| const trustStoreKeyPrefix = `cpt-api/${props.stackName}-truststore` | ||
| const logGroup = new LogGroup(this, "LambdaLogGroup", { | ||
| encryptionKey: cloudWatchLogsKmsKey, | ||
| logGroupName: `/aws/lambda/${props.stackName}-truststore-deployment`, | ||
| retention: props.logRetentionInDays, | ||
| removalPolicy: RemovalPolicy.DESTROY | ||
| }) | ||
| const trustStoreDeploymentPolicy = new ManagedPolicy(this, "TrustStoreDeploymentPolicy", { | ||
| statements: [ | ||
| new PolicyStatement({ | ||
| actions: [ | ||
| "s3:ListBucket" | ||
| ], | ||
| resources: [ | ||
| trustStoreBucket.bucketArn, | ||
| trustStoreDeploymentBucket.bucketArn | ||
| ] | ||
| }), | ||
| new PolicyStatement({ | ||
| actions: [ | ||
| "s3:GetObject" | ||
| ], | ||
| resources: [trustStoreBucket.arnForObjects(props.mutualTlsTrustStoreKey)] | ||
| }), | ||
| new PolicyStatement({ | ||
| actions: [ | ||
| "s3:DeleteObject", | ||
| "s3:PutObject" | ||
| ], | ||
| resources: [ | ||
| trustStoreDeploymentBucket.arnForObjects(trustStoreKeyPrefix + "/" + props.mutualTlsTrustStoreKey) | ||
| ] | ||
| }), | ||
| new PolicyStatement({ | ||
| actions: [ | ||
| "kms:Decrypt", | ||
| "kms:Encrypt", | ||
| "kms:GenerateDataKey" | ||
| ], | ||
| resources: [trustStoreBucketKmsKey.keyArn] | ||
| }), | ||
| new PolicyStatement({ | ||
| actions: [ | ||
| "logs:CreateLogStream", | ||
| "logs:PutLogEvents" | ||
| ], | ||
| resources: [ | ||
| logGroup.logGroupArn, | ||
| `${logGroup.logGroupArn}:log-stream:*` | ||
| ] | ||
| }) | ||
| ] | ||
| }) | ||
| NagSuppressions.addResourceSuppressions(trustStoreDeploymentPolicy, [ | ||
| { | ||
| id: "AwsSolutions-IAM5", | ||
| // eslint-disable-next-line max-len | ||
| reason: "Suppress error for not having wildcards in permissions. This is a fine as we need to have permissions on all log streams under path" | ||
| } | ||
| ]) | ||
| const trustStoreDeploymentRole = new Role(this, "TrustStoreDeploymentRole", { | ||
| assumedBy: new ServicePrincipal("lambda.amazonaws.com"), | ||
| managedPolicies: [trustStoreDeploymentPolicy] | ||
| }).withoutPolicyUpdates() | ||
| const deployment = new BucketDeployment(this, "TrustStoreDeployment", { | ||
| sources: [Source.bucket(trustStoreBucket, props.mutualTlsTrustStoreKey)], | ||
| destinationBucket: trustStoreDeploymentBucket, | ||
| destinationKeyPrefix: trustStoreKeyPrefix, | ||
| extract: false, | ||
| retainOnDelete: false, | ||
| role: trustStoreDeploymentRole, | ||
| logGroup: logGroup | ||
| }) | ||
| mtlsConfig = { | ||
| bucket: deployment.deployedBucket, | ||
| key: trustStoreKeyPrefix + "/" + props.mutualTlsTrustStoreKey | ||
| } | ||
| } | ||
|
|
||
| const apiGateway = new RestApi(this, "ApiGateway", { | ||
| restApiName: `${props.stackName}-apigw`, | ||
| domainName: { | ||
| domainName: serviceDomainName, | ||
| certificate: certificate, | ||
| securityPolicy: SecurityPolicy.TLS_1_2, | ||
| endpointType: EndpointType.REGIONAL, | ||
| mtls: mtlsConfig | ||
| }, | ||
| disableExecuteApiEndpoint: mtlsConfig ? true : false, // NOSONAR | ||
| endpointConfiguration: { | ||
| types: [EndpointType.REGIONAL] | ||
| }, | ||
| deploy: true, | ||
| deployOptions: { | ||
| accessLogDestination: new LogGroupLogDestination(logGroup), | ||
| accessLogFormat: accessLogFormat(), | ||
| loggingLevel: MethodLoggingLevel.INFO, | ||
| metricsEnabled: true | ||
| } | ||
| }) | ||
|
|
||
| const role = new Role(this, "ApiGatewayRole", { | ||
| assumedBy: new ServicePrincipal("apigateway.amazonaws.com"), | ||
| managedPolicies: props.executionPolicies | ||
| }).withoutPolicyUpdates() | ||
|
|
||
| new ARecord(this, "ARecord", { | ||
| recordName: props.stackName, | ||
| target: RecordTarget.fromAlias(new ApiGatewayTarget(apiGateway)), | ||
| zone: hostedZone | ||
| }) | ||
|
|
||
| const cfnStage = apiGateway.deploymentStage.node.defaultChild as CfnStage | ||
| cfnStage.cfnOptions.metadata = { | ||
| guard: { | ||
| SuppressedRules: [ | ||
| "API_GW_CACHE_ENABLED_AND_ENCRYPTED" | ||
| ] | ||
| } | ||
| } | ||
|
|
||
| // Outputs | ||
| this.api = apiGateway | ||
| this.role = role | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import {IResource, LambdaIntegration} from "aws-cdk-lib/aws-apigateway" | ||
| import {IRole} from "aws-cdk-lib/aws-iam" | ||
| import {HttpMethod, IFunction} from "aws-cdk-lib/aws-lambda" | ||
| import {Construct} from "constructs" | ||
|
|
||
| export interface LambdaFunctionHolder { | ||
| readonly function: IFunction | ||
| } | ||
|
|
||
| export interface LambdaEndpointProps { | ||
| parentResource: IResource | ||
| readonly resourceName: string | ||
| readonly method: HttpMethod | ||
| restApiGatewayRole: IRole | ||
| lambdaFunction: LambdaFunctionHolder | ||
| } | ||
|
|
||
| export class LambdaEndpoint extends Construct { | ||
| resource: IResource | ||
|
|
||
| public constructor(scope: Construct, id: string, props: LambdaEndpointProps) { | ||
| super(scope, id) | ||
|
|
||
| const resource = props.parentResource.addResource(props.resourceName) | ||
| resource.addMethod(props.method, new LambdaIntegration(props.lambdaFunction.function, { | ||
| credentialsRole: props.restApiGatewayRole | ||
| })) | ||
|
|
||
| this.resource = resource | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import {IResource, PassthroughBehavior, StepFunctionsIntegration} from "aws-cdk-lib/aws-apigateway" | ||
| import {IRole} from "aws-cdk-lib/aws-iam" | ||
| import {HttpMethod} from "aws-cdk-lib/aws-lambda" | ||
| import {Construct} from "constructs" | ||
| import {stateMachineRequestTemplate} from "./templates/stateMachineRequest.js" | ||
| import {stateMachine200ResponseTemplate, stateMachineErrorResponseTemplate} from "./templates/stateMachineResponses.js" | ||
| import {ExpressStateMachine} from "../StateMachine.js" | ||
|
|
||
| export interface StateMachineEndpointProps { | ||
| parentResource: IResource | ||
| readonly resourceName: string | ||
| readonly method: HttpMethod | ||
| restApiGatewayRole: IRole | ||
| stateMachine: ExpressStateMachine | ||
| } | ||
|
|
||
| export class StateMachineEndpoint extends Construct { | ||
| resource: IResource | ||
|
|
||
| public constructor(scope: Construct, id: string, props: StateMachineEndpointProps) { | ||
| super(scope, id) | ||
|
|
||
| const requestTemplate = stateMachineRequestTemplate(props.stateMachine.stateMachine.stateMachineArn) | ||
|
|
||
| const resource = props.parentResource.addResource(props.resourceName) | ||
| resource.addMethod(props.method, StepFunctionsIntegration.startExecution(props.stateMachine.stateMachine, { | ||
| credentialsRole: props.restApiGatewayRole, | ||
| passthroughBehavior: PassthroughBehavior.WHEN_NO_MATCH, | ||
| requestTemplates: { | ||
| "application/json": requestTemplate, | ||
| "application/fhir+json": requestTemplate | ||
| }, | ||
| integrationResponses: [ | ||
| { | ||
| statusCode: "200", | ||
| responseTemplates: { | ||
| "application/json": stateMachine200ResponseTemplate | ||
| } | ||
| }, | ||
| { | ||
| statusCode: "400", | ||
| selectionPattern: "^4\\d{2}.*", | ||
|
Check warning on line 42 in packages/cdkConstructs/src/constructs/RestApiGateway/StateMachineEndpoint.ts
|
||
| responseTemplates: { | ||
| "application/json": stateMachineErrorResponseTemplate("400") | ||
| } | ||
| }, | ||
| { | ||
| statusCode: "500", | ||
| selectionPattern: "^5\\d{2}.*", | ||
|
Check warning on line 49 in packages/cdkConstructs/src/constructs/RestApiGateway/StateMachineEndpoint.ts
|
||
| responseTemplates: { | ||
| "application/json": stateMachineErrorResponseTemplate("500") | ||
| } | ||
| } | ||
| ] | ||
| }), { | ||
| methodResponses: [] | ||
| }) | ||
|
|
||
| this.resource = resource | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.