-
Notifications
You must be signed in to change notification settings - Fork 1
New: [AEA-6254] - RestApiGateway construct #547
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
Merged
Merged
Changes from 11 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
02b0af2
refactor: api gateway from cpt for reuse
tstephen-nhs 455b657
Merge branch 'main' into aea-6254-cdk-api-gateway
tstephen-nhs d3594ca
Merge branch 'main' into aea-6254-cdk-api-gateway
tstephen-nhs bb83c29
fix: enforce expected role
tstephen-nhs 74d0de2
fix: make LogGroup child of API gateway
tstephen-nhs c7d714d
chore: add sonarqube plugin for 'clean as you code'
tstephen-nhs f2e9325
fix: protect against enabling csoc with no destination
tstephen-nhs 0ac46b1
chore: clean imports
tstephen-nhs b500519
chore: ignore SQ rather than add extra var declaration
tstephen-nhs 734254b
docs: JS doc
tstephen-nhs e26e480
docs: example of ApiGateway use
tstephen-nhs bd610d0
refactor: centralise constants
tstephen-nhs 503d44c
Merge branch 'main' into aea-6254-cdk-api-gateway
tstephen-nhs 0ae7177
revert git secrets install
tstephen-nhs 2b92465
fix: use postCreate to avoid git-secrets failing on second and subseq…
tstephen-nhs 70e7f20
docs: add copilot instructions and teach it to write JSDoc
tstephen-nhs a5a01ac
Merge branch 'main' into aea-6254-cdk-api-gateway
tstephen-nhs 3176a52
chore: adopt latest get-repo-config.yml
tstephen-nhs 87cba70
chore: strip comment from devcontainer.json
tstephen-nhs File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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 hidden or 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 |
|---|---|---|
|
|
@@ -27,3 +27,4 @@ _site/ | |
| .jekyll-metadata | ||
| vendor | ||
| .trivy_out/ | ||
| *.tgz | ||
This file contains hidden or 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
256 changes: 256 additions & 0 deletions
256
packages/cdkConstructs/src/constructs/RestApiGateway.ts
This file contains hidden or 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,256 @@ | ||
| 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 { | ||
| /** Stack name, used as prefix for resource naming and DNS records. */ | ||
| readonly stackName: string | ||
| /** Shared retention period for API and deployment-related log groups. */ | ||
| readonly logRetentionInDays: number | ||
| /** Truststore object key to enable mTLS; leave undefined to disable mTLS. */ | ||
| readonly mutualTlsTrustStoreKey: string | undefined | ||
| /** Enables creation of a second subscription filter to forward logs to CSOC. */ | ||
| readonly forwardCsocLogs: boolean | ||
| /** Destination ARN used by the optional CSOC subscription filter. */ | ||
| readonly csocApiGatewayDestination: string | ||
| /** Managed policies attached to the API Gateway execution role. */ | ||
| readonly executionPolicies: Array<IManagedPolicy> | ||
| } | ||
|
|
||
| /** Creates a regional REST API with standard logging, DNS, and optional mTLS/CSOC integration. */ | ||
| export class RestApiGateway extends Construct { | ||
| public readonly api: RestApi | ||
| public readonly role: IRole | ||
|
|
||
| /** | ||
| * Builds API Gateway infrastructure and validates CSOC forwarding configuration. | ||
| * @example | ||
| * ```ts | ||
| * const api = new RestApiGateway(this, "MyApi", { | ||
| * stackName: "my-service", | ||
| * logRetentionInDays: 30, | ||
| * mutualTlsTrustStoreKey: "truststore.pem", | ||
| * forwardCsocLogs: true, | ||
| * csocApiGatewayDestination: "arn:aws:logs:eu-west-2:123456789012:destination:csoc", | ||
| * executionPolicies: [myLambdaInvokePolicy] | ||
| * }) | ||
| * api.api.root.addResource("patients") | ||
| * ``` | ||
| */ | ||
| 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( | ||
|
tstephen-nhs marked this conversation as resolved.
|
||
| 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 | ||
| }) | ||
| } | ||
|
tstephen-nhs marked this conversation as resolved.
|
||
|
|
||
| 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 | ||
| }) | ||
|
tstephen-nhs marked this conversation as resolved.
|
||
| 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 | ||
| } | ||
| } | ||
39 changes: 39 additions & 0 deletions
39
packages/cdkConstructs/src/constructs/RestApiGateway/LambdaEndpoint.ts
This file contains hidden or 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,39 @@ | ||
| 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 { | ||
| /** Lambda invoked by this API resource method. */ | ||
| readonly function: IFunction | ||
| } | ||
|
|
||
| export interface LambdaEndpointProps { | ||
| /** Parent API resource under which this endpoint path is created. */ | ||
| parentResource: IResource | ||
| /** Path segment added beneath the parent resource. */ | ||
| readonly resourceName: string | ||
| /** HTTP method exposed on the created API resource. */ | ||
| readonly method: HttpMethod | ||
| /** Role assumed by API Gateway when invoking the integration Lambda. */ | ||
| restApiGatewayRole: IRole | ||
| /** Lambda reference used by the generated integration. */ | ||
| lambdaFunction: LambdaFunctionHolder | ||
| } | ||
|
|
||
| /** Adds a child API resource and wires it to a Lambda integration with explicit credentials. */ | ||
| export class LambdaEndpoint extends Construct { | ||
| resource: IResource | ||
|
|
||
| /** Creates the resource/method pair and stores the resulting API resource handle. */ | ||
| 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 | ||
| } | ||
| } |
39 changes: 39 additions & 0 deletions
39
packages/cdkConstructs/src/constructs/RestApiGateway/accessLogFormat.ts
This file contains hidden or 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,39 @@ | ||
| import {AccessLogFormat} from "aws-cdk-lib/aws-apigateway" | ||
|
|
||
| /** Returns the structured API Gateway access log schema expected by platform observability pipelines. */ | ||
| export const accessLogFormat = () => { | ||
| return AccessLogFormat.custom(JSON.stringify({ | ||
| requestId: "$context.requestId", | ||
| ip: "$context.identity.sourceIp", | ||
| caller: "$context.identity.caller", | ||
| user: "$context.identity.user", | ||
| requestTime: "$context.requestTime", | ||
| httpMethod: "$context.httpMethod", | ||
| resourcePath: "$context.resourcePath", | ||
| status: "$context.status", | ||
| protocol: "$context.protocol", | ||
| responseLength: "$context.responseLength", | ||
| accountId: "$context.accountId", | ||
| apiId: "$context.apiId", | ||
| stage: "$context.stage", | ||
| api_key: "$context.identity.apiKey", | ||
| identity: { | ||
| sourceIp: "$context.identity.sourceIp", | ||
| userAgent: "$context.identity.userAgent", | ||
| clientCert: { | ||
| subjectDN: "$context.identity.clientCert.subjectDN", | ||
| issuerDN: "$context.identity.clientCert.issuerDN", | ||
| serialNumber: "$context.identity.clientCert.serialNumber", | ||
| validityNotBefore: "$context.identity.clientCert.validity.notBefore", | ||
| validityNotAfter: "$context.identity.clientCert.validity.notAfter" | ||
| } | ||
| }, | ||
| integration:{ | ||
| error: "$context.integration.error", | ||
| integrationStatus: "$context.integration.integrationStatus", | ||
| latency: "$context.integration.latency", | ||
| requestId: "$context.integration.requestId", | ||
| status: "$context.integration.status" | ||
| } | ||
| })) | ||
| } |
This file contains hidden or 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
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.