-
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
base: main
Are you sure you want to change the base?
Changes from all commits
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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,230 @@ | ||
| 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) | ||
|
|
||
| // 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(scope, "LambdaLogGroup", { | ||
| encryptionKey: cloudWatchLogsKmsKey, | ||
| logGroupName: `/aws/lambda/${props.stackName}-truststore-deployment`, | ||
| retention: props.logRetentionInDays, | ||
| removalPolicy: RemovalPolicy.DESTROY | ||
| }) | ||
|
Comment on lines
+104
to
+111
|
||
| 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, | ||
|
Check warning on line 193 in packages/cdkConstructs/src/constructs/RestApiGateway.ts
|
||
| 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,32 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import {IResource, LambdaIntegration} from "aws-cdk-lib/aws-apigateway" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import {IRole} from "aws-cdk-lib/aws-iam" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import {IFunction} from "aws-cdk-lib/aws-lambda" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Check warning on line 3 in packages/cdkConstructs/src/constructs/RestApiGateway/LambdaEndpoint.ts
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import {HttpMethod} from "aws-cdk-lib/aws-lambda" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Check warning on line 4 in packages/cdkConstructs/src/constructs/RestApiGateway/LambdaEndpoint.ts
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import {Construct} from "constructs" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface LambdaFunctionHolder { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly function: IFunction | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export interface LambdaEndpointProps { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parentResource: IResource | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly resourceName: string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| readonly method: HttpMethod | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+4
to
+14
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import {HttpMethod} 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 | |
| import {Construct} from "constructs" | |
| export interface LambdaFunctionHolder { | |
| readonly function: IFunction | |
| } | |
| export type RestApiHttpMethod = | |
| | "GET" | |
| | "POST" | |
| | "PUT" | |
| | "DELETE" | |
| | "PATCH" | |
| | "HEAD" | |
| | "OPTIONS" | |
| | "ANY" | |
| export interface LambdaEndpointProps { | |
| parentResource: IResource | |
| readonly resourceName: string | |
| readonly method: RestApiHttpMethod |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import {AccessLogFormat} from "aws-cdk-lib/aws-apigateway" | ||
|
|
||
| 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" | ||
| } | ||
| })) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
forwardCsocLogscan be set to true whilecsocApiGatewayDestinationis an empty string (as in tests). That will synthesize aSubscriptionFilterwith an invalid/emptyDestinationArnand fail deployment. Consider makingcsocApiGatewayDestinationoptional unlessforwardCsocLogsis true, and throw an explicit error whenforwardCsocLogsis true but the destination ARN is missing/blank.