├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── documentation ├── application-load-balancer │ └── CustomApplicationLoadBalancer.md ├── cloudwatch-data-protection │ └── LogPropertiesUpdater.md ├── dynamodb │ └── DynamoDBTable.md ├── ecs │ └── fargate │ │ ├── alb-fargate-service.md │ │ └── nlb-fargate-service.md ├── kms │ └── CustomerManagedKey.md ├── s3 │ └── S3.md ├── sns │ └── SnsTopic.md └── sqs │ └── SQSQueue.md ├── package.json └── service-constructs ├── alb ├── alb.ts └── domain-properties.ts ├── cloudwatch-data-protection ├── data-identifiers.ts ├── data-protection-policy.ts └── log-properties-updater-custom-resource.ts ├── dynamodb └── dynamodb-table.ts ├── ecs └── fargate │ ├── alb-fargate-service.ts │ ├── fargate-data-protection-policy-util.ts │ ├── jlb-relay.ts │ ├── nlb-fargate-service.ts │ └── sidecar.ts ├── kms └── customer-managed-key.ts ├── s3 ├── access-bucket-log.ts └── s3-bucket.ts ├── sns └── topic.ts ├── sqs └── queue.ts ├── tags └── default-tag-handler.ts └── version.ts /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 13 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 14 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 15 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## CDKConstructs 2 | 3 | This package extends common CDK constructs for use in your organizations services. The `service-constructs` folder provides constructs for some of the more commonly used AWS services with opinionated best practices. The `documentation` folder provides an overview of each construct and details around defaults and usage. 4 | 5 | 6 | ## Security 7 | 8 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 9 | 10 | ## License 11 | 12 | This library is licensed under the MIT-0 License. See the LICENSE file. 13 | 14 | -------------------------------------------------------------------------------- /documentation/application-load-balancer/CustomApplicationLoadBalancer.md: -------------------------------------------------------------------------------- 1 | ## Features provided on top of Application Load Balancer(ALB) 2 | - Listener port of the ALB that will serve traffic to the service defaulted to port 443 3 | - The port on the target group defaulted to 8080 4 | - Integration with S3 for ALB access logs encrypted with SSE-S3 5 | - ALB timeout set to 60 seconds 6 | - Delete Protection enabled for ALB 7 | 8 | ### Usage of CustomApplicationLoadBalancer Construct 9 | 10 | The `CustomApplicaitonLoadBalancer` construct mirrors AWS CDK's [`ApplicationLoadBalancer`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_elasticloadbalancingv2.ApplicationLoadBalancer.html) construct, but enforces opinionated practices and security considerations. 11 | To use: 12 | ``` 13 | new CustomApplicationLoadBalancer(this, "MyALB",{ 14 | vpc: , 15 | domainProps: { 16 | hostedZone: 17 | domainNamePrefix:"" 18 | } 19 | }); 20 | ``` 21 | 22 | The general recommendation is to use `REGIONAL` endpoints. By default, the ALB is optimized for `EDGE` instances and can result in a higher cost if not using a `REGIONAL` endpoint properly. Hence, our opinionated solution provides the creation of edge-optimized endpoints and provides the ability to migrate between endpoint types. Our suggestion is to use `EDGE` endpoint types and migrate to `REGIONAL` once it has been confirmed there are no latency issues. 23 | 24 | ### Steps to migrate from EDGE to REGIONAL: 25 | 1. Starting state - enableRegionalEndpoint flag not set or set to false . 26 | 2. Set enableRegionalEndpoint to false, and endpointTypes to include EDGE and REGIONAL, and deploy. 27 | 3. Set enableRegionalEndpoint to true, keep both types in endpointTypes, and deploy. 28 | 4. Keep enableRegionalEndpoint set to true, remove the endpointTypes setting altogether, and deploy. -------------------------------------------------------------------------------- /documentation/cloudwatch-data-protection/LogPropertiesUpdater.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | Developers can create instances of the construct, which includes inline resources. Developers need to be careful to specify the correct dependencies when using inline code. For example, Lambda has its own version of `aws-sdk` that does not include the required Cloudwatch APIs to update `data-retention-policy`. To avoid these issues, we will use a layer to bundle the required version of `aws-sdk`. To use please update the `log-properties-updater-custom-resource.ts` class with the corresponding location of the code for your organizations log update policy. 4 | 5 | `code: Code.fromAsset( 6 | "" 7 | ),` 8 | 9 | 10 | ## General Guidance/Advice on Logging 11 | 12 | Service logs should answer the question “Who did what when?” without including sensitive information such as passwords or secrets. If you use inadequate log information, this could negatively impact forensics investigations, preventing root cause analysis. Engineers will be unable to appropriately trace and identify an attackers steps. This is important for identifying the attack vector and sandboxing it to contain the blast radius. Additionally, only appropriate log information should be included in logs to prevent sensitive data from being leaked into CloudTrail or other places. Ensure logs are being created in all lifecycle states of your system. 13 | 14 | Ensure that you are continuously monitoring logs for sensitive data. In the event that sensitive data is accidentally logged, you should become aware of it automatically. 15 | 16 | One way to accomplish this is by utilizing CloudWatch log data protection policies. You can enable sensitive data masking for relevant CloudWatch log groups. When sensitive data is accidentally logged, you are alerted so that you can respond and scrub the data. Operators cannot see masked data without specific additional access. The easiest path to support data protection policies is to use the CDK Construct. 17 | 18 | It is important to remove sensitive data from logs and not just mask it. Masking is a temporary fix as it can be unmasked by any individual with admin access to the AWS account. Note as well that CloudWatch Log's data protection policy feature can mask and audit logs for certain pre-defined types of user data but cannot capture all types of data. This is a layer of defense in depth against leaking sensitive data in to service logs. Logs used for purposes of investigation for a security incident are recommended to be kept for 10 years. -------------------------------------------------------------------------------- /documentation/dynamodb/DynamoDBTable.md: -------------------------------------------------------------------------------- 1 | 2 | The `DynamoDBTable` construct mirrors AWS CDK's [`Table`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-dynamodb.Table.html) 3 | construct, but enforces opinionated practices. 4 | 5 | There are several ways a table may be declared. To use this, you can create table in either Provisioned or OnDemand mode: 6 | 7 | For creating a OnDemand table: 8 | ``` 9 | new DynamoDBTable(stack, 'TableIdentifier', { 10 | tableName: 'ExampleTableOnDemand', 11 | partitionKey: {name: 'id', type: dynamodb.AttributeType.STRING} 12 | }); 13 | ``` 14 | For creating a Provisioned table: 15 | ``` 16 | new DynamoDBTable(stack, 'TableIdentifier', { 17 | tableName: 'ExampleTableProvisioned', 18 | partitionKey: {name: 'id', type: dynamodb.AttributeType.STRING}, 19 | billingMode: dynamodb.BillingMode.PROVISIONED, 20 | readScalingProps: { 21 | minCapacity: 10, 22 | maxCapacity: 100 23 | }, 24 | writeScalingProps: { 25 | minCapacity: 10, 26 | maxCapacity: 100 27 | } 28 | }); 29 | ``` 30 | 31 | If the `encryption` field is not specified, DynamoDBTable defaults to `dynamodb.TableEncryption.CUSTOMER_MANAGED`. 32 | If `encryption` field is specified (or inferred) to be dynamodb.TableEncryption.CUSTOMER_MANAGED and an `encryptionKey` is not specified, 33 | DynamoDB CDK construct automatically will create a new KMS key and use that for the table encryption. 34 | 35 | Note that a customer managed KMS key is preferred for table storage by default. 36 | DynamoDB server-side encryption (SSE) only allows for a single key per table. 37 | This makes isolation of customer content in DynamoDB difficult. 38 | Encrypt customer content for multiple customers using different customer owned CMKs. 39 | That said, KMS keys will result in additional costs and are not suitable to all use cases. 40 | If your use case requires a different server-side encryption, 41 | you can set `encryption` to one of the other supported `dynamodb.TableEncryption` values. 42 | 43 | For Provisioned table, DynamoDB by default configures capacity auto-scaling for table and global secondary indexes. 44 | Default auto-scaling properties can be overridden by invoking api explicitly. 45 | e.g. for overriding autoscale read configuration: 46 | ``` 47 | table.autoScaleReadCapacity(readScalingProps); 48 | ``` 49 | 50 | ### Features provided on top of DynamoDB Table: 51 | - Creates OnDemand table(PAY_PER_REQUEST) by default. 52 | - SSE w/ customer managed KMS enabled by default. 53 | - For Provisioned table, enforces creation of auto-scaling for table and global secondary indexes. Provides by default scale on utilization. 54 | - Enables point-in-time recovery by default. 55 | - Enables deletion protection by default. 56 | 57 | Properties of TableProps can be customized to opt-out of any of the above. 58 | -------------------------------------------------------------------------------- /documentation/ecs/fargate/alb-fargate-service.md: -------------------------------------------------------------------------------- 1 | ## Features provided on top of Basic CDK Constructs 2 | - Access logging to the ALB using opinionated construct 3 | - ALB using opinionated defaults provided in CustomApplicationLoadBalancer 4 | - Public access disabled 5 | - Fargate Platform version defaulted to LATEST 6 | - Task count total set to 1 7 | - During deployment min healthy amount 100% and max is 200% 8 | - Health check grace period set to 60 seconds 9 | - Deployment circuit breaker enabled 10 | - CloudWatch Data Protection enabled 11 | 12 | ### General Guidance 13 | 14 | This construct creates a fargate service fronted by an Application Load Balancer. The following diagram displays the high level architecture. 15 | 16 | ``` 17 | +-------------+ +-------------+ 18 | HTTPS | | HTTP | | 19 | 443 | ALB | 8080 | | 20 | Client--------->| |----------->| Service | 21 | | | | | 22 | +-------------+ +-------------+ 23 | | 24 | v 25 | +-------------+ 26 | | | 27 | | Route 53 | 28 | | | 29 | +-------------+ 30 | ``` 31 | Connection to fargate task containers is configured to host port 8080. Please note traffic between ALB and container is not encrypted in transit. By default, access logs to the ALB are enabled. -------------------------------------------------------------------------------- /documentation/ecs/fargate/nlb-fargate-service.md: -------------------------------------------------------------------------------- 1 | ## Features provided on top of Basic CDK Constructs 2 | - End to end TLS termination with JLB Relay and Sidecar 3 | - Access logging to the NLB 4 | - Public access disabled 5 | - Fargate Platform version defaulted to LATEST 6 | - Task count total set to 1 7 | - During deployment min healthy amount 100% and max is 200% 8 | - Health check grace period set to 60 seconds 9 | - Deployment circuit breaker enabled 10 | - CloudWatch Data Protection enabled 11 | 12 | ### General Guidance 13 | 14 | This construct creates a fargate service fronted by a Network Load Balancer. There are two ways to create this construct: with or without end to end TLS termination. The following diagrams display high level architecture of both options. 15 | 16 | Without end to end TLS termination: 17 | ``` 18 | ------------- -------------- 19 | TCP | | HTTP | | 20 | 80 | NLB | 8080 | Service | 21 | ApiGw+--------->+ +----------->+ | 22 | | | | | 23 | ------------- -------------- 24 | ``` 25 | With end to end TLS termination: 26 | ``` 27 | ------------- -------------- -------------- 28 | TCP | | HTTPS | | HTTP | | 29 | 443 | NLB | 9001 | JlbRelay | 8080 | Service | 30 | ApiGw+--------->+ +----------->+ Sidecar +------------>+ | 31 | | | | | | | 32 | ------------- -------------- -------------- 33 | ``` -------------------------------------------------------------------------------- /documentation/kms/CustomerManagedKey.md: -------------------------------------------------------------------------------- 1 | The `CustomerManagedKey` construct mirrors AWS CDK's [`Key`](https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-kms.Key.html) 2 | construct, but enforces opinionated practices with creating a customer managed key. 3 | 4 | For creating a customer managed key: 5 | ``` 6 | const key = new CustomerManagedKey(scope, 'MyKey', { 7 | keyAlias: 'MyKeyAlias', 8 | keyDescription: "MyKeyDescription", 9 | actions: [ 10 | 'kms:Encrypt', 11 | 'kms:Decrypt', 12 | 'kinesis:*' 13 | ... 14 | ] 15 | }); 16 | ``` 17 | 18 | -------------------------------------------------------------------------------- /documentation/s3/S3.md: -------------------------------------------------------------------------------- 1 | 2 | The `Bucket` construct mirrors AWS CDK's [`Bucket`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.Bucket.html) 3 | construct, but enforces opinionated practices. 4 | 5 | There are several ways a bucket may be declared. The recommended way is to simply use: 6 | 7 | ``` 8 | new Bucket(stack, 'BucketIdentifier', { }); 9 | ``` 10 | 11 | If the `encryption` field is not specified, Bucket defaults to `s3.BucketEncryption.KMS_MANAGED`. 12 | If `encryption` field is specified (or inferred) to be s3.BucketEncryption.KMS_MANAGED and an `encryptionKey` is not specified, 13 | Bucket will create a new KMS key and use that for the bucket encryption. In that case, by default, the bucket will 14 | not have read access for anyone (including "Admin" users). You will need to call `bucket.grantRead` with the desired Principal to allow 15 | read access to the bucket. For example, to grant the "root user" access: 16 | 17 | ``` 18 | // Get stack reference 19 | const stack = Stack.of(this); 20 | // Grant read 21 | bucket.grantRead(new iam.AccountPrincipal(stack.account)); 22 | ``` 23 | 24 | Note that a customer managed KMS key is preferred for bucket storage by default. 25 | It is preferred as a threat actor with access to an AWS service or physical data at rest 26 | can potentially access customer content in a way the customer can neither prevent nor detect, 27 | which leads to information disclosure of customer content. 28 | That said, KMS keys will result in additional costs and are not suitable to all use cases. If your use case requires a different server-side encryption, 29 | you can set `encryption` to one of the other supported `s3.BucketEncryption` values. 30 | 31 | ### Features provided on top of S3 bucket 32 | - SSE w/ customer managed KMS enabled by default. 33 | - If no `encryptionKey` is specified, creates a KMS key. 34 | - If KMS key encryption is specified (or inferred), use of SSE with kms to put objects in buckets enforced by default. 35 | - Access logging enabled by default. 36 | - Versioning enabled by default. 37 | - SecureTransport required by default. 38 | - Blocks all public access by default 39 | - Keep non-current versions for 90 days only. 40 | - Keep access logs for 10 years. 41 | 42 | Properties of BucketProps can be customized to opt-out of any of the above. 43 | -------------------------------------------------------------------------------- /documentation/sns/SnsTopic.md: -------------------------------------------------------------------------------- 1 | ## Features provided on top of SNS Topic 2 | - SSE with customer managed KMS enabled by default. 3 | - If 'encryption' is not set, then default encryption is added by creating a KMS key. 4 | - If KMS master key encryption is specified, use of that key to publish messages and subscribe messages is enforced by default. 5 | - SecureTransport required by default [`Topic - Encryption In Transit`](https://docs.aws.amazon.com/sns/latest/dg/sns-security-best-practices.html#enforce-encryption-data-in-transit). 6 | - Giving only the required principals access to publish messages. 7 | - Giving only the required principals access to subscribe messages. 8 | Properties of SnsTopic can be customized to opt-out of any of the above. 9 | 10 | The `SnsTopic` construct mirrors AWS CDK's [`SNS`](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-sns-readme.html) 11 | construct, but enforces opinionated practices and security considerations. 12 | To use: 13 | ``` 14 | new SnsTopic(stack, 'SNSTopicIdentifier', {}); 15 | ``` 16 | If the encryption attribute of props is set to `true` then a masterKey is expected. If the masterKey is not specified then 17 | SNSTopic will create a new KMS key and use that for the topic encryption. In that case, by default, the topic will not have subscribe and publish access for anyone (including "Admin" users). And you might receive such error: 18 | 19 | `Error code: KMS.AccessDeniedException. Error message: The ciphertext refers to a customer master key that does not exist, does not exist in this region, or you are not allowed to access. 20 | (Service: AWS KMS; Status Code: 400; Error Code: AccessDeniedException; Request ID: ; Proxy: null)` 21 | 22 | To grant access to principals, you will need to call `topic.grantPublish` with the desired Principal to allow 23 | publishing message access to the topic. For example, to grant the "root user" access: 24 | ``` 25 | const stack = Stack.of(this); 26 | topic.grantPublish(new iam.AccountPrincipal(stack.account)); 27 | ``` 28 | Similar action is needed to subscribe messages too using `topic.grantSubscribe` 29 | 30 | Note that a customer managed KMS key is preferred for topic by default. 31 | That said, KMS keys will result in additional costs and are not suitable to all use cases. If your use case requires a different server-side encryption, 32 | you can set `encryption` to `false`. -------------------------------------------------------------------------------- /documentation/sqs/SQSQueue.md: -------------------------------------------------------------------------------- 1 | ## Features provided on top of SQS Queue 2 | - SSE with customer managed KMS enabled by default. 3 | - If no `encryptionMasterKey` is specified, create a KMS key. 4 | - If KMS key encryption is specified, use of that key to send messages and consume messages is enforced by default. 5 | - SecureTransport required by default. 6 | - Giving only the required principals access to send messages. 7 | - Giving only the required principals access to consume messages. 8 | - If a DLQ is not explicitly disabled and no DLQ is specified, then a DLQ will be created with receive count of 100 9 | - Setting retention period to max by default for the queue unless specified explicitly. 10 | Properties of SQSQueueProps can be customized to opt-out of any of the above. 11 | 12 | ### SQSQueue Construct 13 | 14 | The `SQSQueue` construct mirrors AWS CDK's [`SQS`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-sqs.Queue.html) 15 | construct, but enforces opinionated practices and security considerations. 16 | To use this construct: 17 | ``` 18 | new SQSQueue(stack, 'SQSQueueIdentifier', {}); 19 | ``` 20 | If the encryption type is QueueEncryption.KMS_MANAGED then a masterKey is expected. If the masterKey is not specified then 21 | Queue will create a new KMS key and use that for the queue encryption. 22 | In that case, by default, the queue will not have read access for anyone (including "Admin" users). And you might receive such error: 23 | 24 | `Error code: KMS.AccessDeniedException. Error message: The ciphertext refers to a customer master key that does not exist, does not exist in this region, or you are not allowed to access. 25 | (Service: AWS KMS; Status Code: 400; Error Code: AccessDeniedException; Request ID: ; Proxy: null)` 26 | 27 | Hence you need to grant access to the principals for send and receive on queue. To do so, you will need to call `queue.grantSendMessages` with the desired Principal to allow sending message access to the queue. For example, to grant the "root user" access: 28 | ``` 29 | const stack = Stack.of(this); 30 | queue.grantSendMessages(new iam.AccountPrincipal(stack.account)); 31 | ``` 32 | Similar action would be needed to consume messages too using `queue.grantConsumeMessages` Note that a customer managed KMS key is preferred for queue by default. That said, KMS keys will result in additional costs and are not suitable to all use cases. If your use case requires a different server-side encryption, you can set `encryption` to `QueueEncryption.UNENCRYPTED`. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@amzn/cdk-constructs", 3 | "version": "1.0.0", 4 | "license": "UNLICENSED", 5 | "main": "dist/lib/index.js", 6 | "types": "dist/lib/index.d.ts", 7 | "scripts": { 8 | "prebuild": "node -p \\\"'// This file is generated automatically during npm pre-build. Any modifications will be overwritten.\\\\n\\\\nexport const LIB_VERSION = \\\\'$(npm --silent run get-version)\\\\';'\\\" > service-constructs/version.ts", 9 | "build": "jsii && npm run copy-files", 10 | "clean": "npm run clean:artifacts && npm run clean:dependencies && npm run clean:build", 11 | "get-version": "[[ $NODE_ENV = test ]] && echo 5.45.0 || echo $npm_package_version", 12 | "clean:artifacts": "rm -f $(find service-constructs test assets -type f -name '*.js' -o -name '*.d.ts')", 13 | "clean:build": "rm -rf dist && rm -rf build && rm -rf .jsii && rm -f tsconfig.json", 14 | "clean:dependencies": "rm -rf node_modules", 15 | "lint": "eslint 'service-constructs/**/*.ts' 'test/**/*.ts' 'assets/**/*.ts' --fix --ignore-pattern '**/*.d.ts'", 16 | "pretest": "export NODE_ENV=test || set NODE_ENV=test && npm run build && npm run lint", 17 | "test": "BRAZIL_PACKAGE_VERSION=1 NODE_OPTIONS=--max-old-space-size=8192 jest --silent --maxWorkers=25%", 18 | "posttest": "generate-coverage-data -language typescript", 19 | "prepublishOnly": "npm run build && npm run lint", 20 | "watch": "jsii -w" 21 | }, 22 | "engines": { 23 | "node": ">=14.0.0" 24 | }, 25 | "npm-pretty-much": { 26 | "ciBuild": "always", 27 | "runTest": "release" 28 | }, 29 | "dependencies": { 30 | "@aws-sdk/client-resource-groups-tagging-api": "3.360.0", 31 | "@types/aws-lambda": "^8.10.101", 32 | "@types/jest": "^27.4.1", 33 | "aws-embedded-metrics": "^4.0.0", 34 | "aws-sdk": "^2.952.0", 35 | "js-yaml": "3.13.1", 36 | "openapi-types": "^7.2.3", 37 | "yaml": "^2.3.2" 38 | }, 39 | "devDependencies": { 40 | "@types/js-yaml": "3.12.1", 41 | "@types/node": "*", 42 | "@types/prettier": "2.6.0", 43 | "@typescript-eslint/eslint-plugin": "^4.2.0", 44 | "@typescript-eslint/parser": "^4.2.0", 45 | "ajv": "8.12.0", 46 | "aws-cdk": "^2.87.0", 47 | "aws-cdk-lib": "^2.87.0", 48 | "compressjs": "^1.0.3", 49 | "constructs": "^10.0.79", 50 | "esbuild": "^0.17.18", 51 | "eslint": "^7.32.0", 52 | "eslint-config-airbnb-base": "^15.0.0", 53 | "eslint-plugin-import": "^2.26.0", 54 | "eslint-webpack-plugin": "^2.5.2", 55 | "jest": "^27.4.1", 56 | "jsii": "~1.80.0", 57 | "ts-jest": "27.0.0", 58 | "typescript": "~4.2.0", 59 | "webpack": "^5.88.2" 60 | }, 61 | "peerDependencies": { 62 | "aws-cdk-lib": "^2.87.0", 63 | "constructs": "^10.0.79" 64 | }, 65 | "bundleDependencies": [ 66 | "@aws-sdk/client-resource-groups-tagging-api", 67 | "@types/aws-lambda", 68 | "@types/jest", 69 | "aws-embedded-metrics", 70 | "aws-sdk", 71 | "js-yaml", 72 | "openapi-types", 73 | "yaml" 74 | ], 75 | "author": { 76 | "name": "amazon" 77 | }, 78 | "repository": { 79 | "type": "git", 80 | "url": "https://gitlab.aws.dev/santos-sa/cdk-constructs" 81 | }, 82 | "jsii": { 83 | "excludeTypescript": [ 84 | "build/*" 85 | ], 86 | "outdir": "generated", 87 | "targets": { 88 | "java": { 89 | "package": "software.amazon.amzn.cdkconstructs", 90 | "maven": { 91 | "groupId": "software.amazon.amzn.cdkconstructs", 92 | "artifactId": "cdk-constructs" 93 | } 94 | } 95 | }, 96 | "tsc": { 97 | "outDir": "./dist", 98 | "rootDir": "./" 99 | } 100 | }, 101 | "lint-staged": { 102 | "**/*": "prettier --write --ignore-unknown" 103 | }, 104 | "files": [ 105 | "!.eslintrc", 106 | "!coverage", 107 | "!dist/test", 108 | "!global.d.ts", 109 | "!jest.config.js", 110 | ".jsii", 111 | "dist/**/*.d.ts", 112 | "dist/**/*.js" 113 | ] 114 | } 115 | -------------------------------------------------------------------------------- /service-constructs/alb/alb.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { Duration } from 'aws-cdk-lib'; 3 | import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; 4 | import { 5 | ApplicationLoadBalancer, ApplicationProtocol, ApplicationTargetGroup, ListenerCertificate, SslPolicy, 6 | } from 'aws-cdk-lib/aws-elasticloadbalancingv2'; 7 | import * as ec2 from 'aws-cdk-lib/aws-ec2'; 8 | import { SecurityGroup } from 'aws-cdk-lib/aws-ec2'; 9 | import { BucketEncryption } from 'aws-cdk-lib/aws-s3'; 10 | import { CertificateValidation, Certificate } from 'aws-cdk-lib/aws-certificatemanager'; 11 | import { ARecord, RecordTarget } from 'aws-cdk-lib/aws-route53'; 12 | import { LoadBalancerTarget } from 'aws-cdk-lib/aws-route53-targets'; 13 | import { AccessLogsBucket } from '../s3/access-bucket-log'; 14 | import { DomainProps } from './domain-properties'; 15 | import { DefaultTagHandler } from '../tags/default-tag-handler'; 16 | 17 | const DEFAULT_LISTENER_PORT = 443; 18 | const DEFAULT_TARGET_GROUP_PORT = 8080; 19 | const SECURE_PROTOCOL = ApplicationProtocol.HTTPS; 20 | 21 | export interface ApplicationLoadBalancerProps { 22 | 23 | //The VPC where the container instances will be launched 24 | // or the elastic network interfaces (ENIs) will be deployed. 25 | readonly vpc: ec2.IVpc; 26 | 27 | 28 | //Route53 Domain properties 29 | readonly domainProps: DomainProps; 30 | 31 | /** 32 | * Listener port of the application load balancer that will serve traffic to the service. 33 | * @default - 443 34 | */ 35 | readonly listenerPort?: number; 36 | 37 | /** 38 | * The port on the target group 39 | * @default - 8080 40 | */ 41 | readonly targetGroupPort?: number; 42 | 43 | //The name for the S3 bucket containing ALB Access logs 44 | readonly accessLogsS3BucketName?: string; 45 | 46 | /** 47 | * The load balancer idle timeout, in seconds. 48 | * 49 | * As an attempt to reduce the amount of 502 and 504, 50 | * have the ALB timeout a bit larger than the target 51 | * 52 | * @default 60 53 | */ 54 | readonly idleTimeout?: Duration; 55 | } 56 | 57 | export class CustomApplicationLoadBalancer extends Construct { 58 | public readonly targetGroup: ApplicationTargetGroup; 59 | 60 | public readonly securityGroup: SecurityGroup; 61 | 62 | public readonly loadBalancer: ApplicationLoadBalancer; 63 | 64 | constructor(scope: Construct, id: string, props: ApplicationLoadBalancerProps) { 65 | super(scope, id); 66 | DefaultTagHandler.applyTags(this); 67 | 68 | this.securityGroup = new SecurityGroup(this, 'ALBSecurityGroup', { 69 | vpc: props.vpc, 70 | allowAllOutbound: false, 71 | }); 72 | 73 | this.loadBalancer = new ApplicationLoadBalancer(this, 'ApplicationLoadBalancer', { 74 | vpc: props.vpc, 75 | vpcSubnets: { 76 | subnets: props.vpc.publicSubnets, 77 | }, 78 | securityGroup: this.securityGroup, 79 | internetFacing: true, 80 | // It is a recommendation to enable deletion protection for the ALB 81 | deletionProtection: true, 82 | idleTimeout: props.idleTimeout, 83 | }); 84 | 85 | // Omitting or disabling the routing.http.drop_invalid_header_fields.enabled option 86 | // results in potential exposure to HTTP DeSync attacks. 87 | this.loadBalancer.setAttribute('routing.http.drop_invalid_header_fields.enabled', 'true'); 88 | 89 | /* 90 | * Enabling ALB Access logs by default. 91 | * When we enable access logs, we must specify an S3 bucket for the access logs. 92 | * The bucket should use Amazon S3-Managed Encryption Keys (SSE-S3). 93 | * Additional requirement details can be found in : 94 | * https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-access-logs.html 95 | */ 96 | const bucket = new AccessLogsBucket(this, 'alb-access-logs', { 97 | bucketName: props.accessLogsS3BucketName, 98 | encryption: BucketEncryption.S3_MANAGED, 99 | }); 100 | 101 | this.loadBalancer.logAccessLogs(bucket, 'alb-access-logs'); 102 | 103 | const domainName = `${props.domainProps.domainNamePrefix}.${props.domainProps.hostedZone.zoneName}`; 104 | 105 | const listener = this.loadBalancer.addListener('PublicListener', { 106 | protocol: SECURE_PROTOCOL, 107 | port: props.listenerPort ?? DEFAULT_LISTENER_PORT, 108 | open: true, 109 | sslPolicy: SslPolicy.RECOMMENDED_TLS, 110 | }); 111 | 112 | const certificate = new Certificate(this, 'DnsValidatedCertificate', { 113 | domainName, 114 | validation: CertificateValidation.fromDns(props.domainProps.hostedZone), 115 | }); 116 | listener.addCertificates('Arns', [ListenerCertificate.fromCertificateManager(certificate)]); 117 | 118 | this.targetGroup = listener.addTargets('ECS', { 119 | protocol: ApplicationProtocol.HTTP, 120 | port: props.targetGroupPort ?? DEFAULT_TARGET_GROUP_PORT, 121 | }); 122 | 123 | this.targetGroup.configureHealthCheck({ 124 | port: 'traffic-port', 125 | path: '/ping', 126 | protocol: elbv2.Protocol.HTTP, 127 | }); 128 | 129 | new ARecord(this, 'DNS', { 130 | zone: props.domainProps.hostedZone, 131 | recordName: domainName, 132 | target: RecordTarget.fromAlias(new LoadBalancerTarget(this.loadBalancer)), 133 | }); 134 | } 135 | } -------------------------------------------------------------------------------- /service-constructs/alb/domain-properties.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HostedZone, 3 | } from 'aws-cdk-lib/aws-route53'; 4 | import { 5 | EndpointType, 6 | } from 'aws-cdk-lib/aws-apigateway'; 7 | 8 | /** 9 | * Route53 Domain properties 10 | */ 11 | export interface DomainProps { 12 | /** 13 | * Route53 hosted zone. 14 | */ 15 | readonly hostedZone: HostedZone; 16 | /** 17 | * Prefix of the URL. 18 | * Example: 'api-na'. 19 | * Complete URL will be formatted as domainNamePrefix + '.' + HostedZone.zoneName. 20 | */ 21 | readonly domainNamePrefix: string; 22 | /** 23 | * Specifies whether Route53 ARecord should route traffic to the regional API endpoint. 24 | * Defaults to false if not set. 25 | * WARNING: If your service is currently deployed using only the EDGE endpoint, 26 | * before you enable this you need to first do a deployment 27 | * adding the REGIONAL endpoint (using endpointTypes below). 28 | */ 29 | readonly enableRegionalEndpoint?: boolean; 30 | /** 31 | * Specifies which endpoint types to create, and provides migration between endpoint types. 32 | * If not set, the defaults will be based on enableRegionalEndpoint, 33 | * if enableRegionalEndpoint is not set or false the default to EDGE, 34 | * otherwise REGIONAL. Aimed at backwards compatibility. 35 | */ 36 | readonly endpointTypes?: EndpointType[]; 37 | /** 38 | * Specifies the region to create a DNS validated certificate 39 | * managed by AWS Certificate Manager to be used by a regional endpoint. 40 | * Regional endpoints require certs in the same region. 41 | * @default us-east-1 42 | */ 43 | readonly region?: string; 44 | } 45 | -------------------------------------------------------------------------------- /service-constructs/cloudwatch-data-protection/data-identifiers.ts: -------------------------------------------------------------------------------- 1 | export const ADDRESS_DATA_IDENTIFIER = 'arn:aws:dataprotection::aws:data-identifier/Address'; 2 | export const AWS_SECRET_KEY_DATA_IDENTIFIER = 'arn:aws:dataprotection::aws:data-identifier/AwsSecretKey'; 3 | export const EMAIL_DATA_IDENTIFIER = 'arn:aws:dataprotection::aws:data-identifier/EmailAddress'; 4 | export const SSN_US_DATA_IDENTIFIER = 'arn:aws:dataprotection::aws:data-identifier/Ssn-US'; 5 | export const SSN_ES_DATA_IDENTIFIER = 'arn:aws:dataprotection::aws:data-identifier/Ssn-ES'; 6 | export const PHONE_NUM_US_DATA_IDENTIFIER = 'arn:aws:dataprotection::aws:data-identifier/PhoneNumber-US'; 7 | export const NAME_US_DATA_IDENTIFIER = 'arn:aws:dataprotection::aws:data-identifier/Name'; 8 | export const PUTTY_PRIVATE_KEY_DATA_IDENTIFIER = 'arn:aws:dataprotection::aws:data-identifier/PuttyPrivateKey'; 9 | export const DRIVERS_LICENSE_US_DATA_IDENTIFIER = 'arn:aws:dataprotection::aws:data-identifier/DriversLicense-US'; 10 | export const PASSPORT_NUMBER_DATA_IDENTIFIER = 'arn:aws:dataprotection::aws:data-identifier/PassportNumber-US'; 11 | export const ZIP_CODE_DATA_IDENTIFIER = 'arn:aws:dataprotection::aws:data-identifier/ZipCode-US'; 12 | 13 | /** 14 | * Maintains the list of default data protection policy to logs. 15 | * Refer to this doc for all fields : 16 | * https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/protect-sensitive-log-data-types.html 17 | */ 18 | export const DEFAULT_SENSITIVE_DATA_IDENTIFIERS: string[] = [ 19 | ADDRESS_DATA_IDENTIFIER, 20 | EMAIL_DATA_IDENTIFIER, 21 | SSN_US_DATA_IDENTIFIER, 22 | SSN_ES_DATA_IDENTIFIER, 23 | PHONE_NUM_US_DATA_IDENTIFIER, 24 | AWS_SECRET_KEY_DATA_IDENTIFIER, 25 | ]; -------------------------------------------------------------------------------- /service-constructs/cloudwatch-data-protection/data-protection-policy.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { CfnLogGroup, LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; 3 | import { IKey, Key } from 'aws-cdk-lib/aws-kms'; 4 | import { Effect, PolicyStatement, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; 5 | import { Stack } from 'aws-cdk-lib'; 6 | import { DEFAULT_SENSITIVE_DATA_IDENTIFIERS } from './data-identifiers'; 7 | 8 | export interface CwDataProtectionPolicyProps { 9 | 10 | /** 11 | * Create a logGroup and provide the name to this to write audit logs. 12 | * More details about audit log group can be found at : 13 | * https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/mask-sensitive-log-data-audit-findings.html 14 | * 15 | * It is not mandatory to have an audit log group, 16 | * but it would help in collecting metrics on type of data-identifier, 17 | * number of occurrences, & position of sensitive data that is present in log lines. 18 | * Currently, the construct supports cloudwatch logs based audit report. 19 | * There is no additional access configuration needed for audit log group. 20 | * Format and examples are in usage doc above. 21 | * Can add support for other resources(S3 & Firehose). 22 | * 23 | * @default will not add any audit log group. 24 | */ 25 | readonly auditLogGroupName?: string, 26 | 27 | /** 28 | * List of data identifiers to add to default list. 29 | * @default does not add any identifier to default list. 30 | */ 31 | readonly addToDefaultDataIdentifiers?: string[], 32 | 33 | /** 34 | * List of data identifiers to exclude from default list 35 | * @default does not remove any identifier from default list. 36 | */ 37 | readonly excludeFromDefaultDataIdentifiers?: string[], 38 | } 39 | 40 | const getCloudwatchAuditDestination = (auditLogGroupName: string) => ({ 41 | CloudWatchLogs: { 42 | LogGroup: auditLogGroupName, 43 | }, 44 | }); 45 | 46 | /** 47 | * Function to retrieve data protection policy as JSON. 48 | * @param props cloudwatch data protection policy properties. 49 | */ 50 | export const cloudwatchDataProtectionPolicy = (props: CwDataProtectionPolicyProps): JSON => { 51 | let dataIdentifiers = Array.from(DEFAULT_SENSITIVE_DATA_IDENTIFIERS); 52 | 53 | // Add additional identifiers in list 54 | dataIdentifiers = dataIdentifiers.concat(...props.addToDefaultDataIdentifiers ?? []); 55 | 56 | // remove excluded identifiers from list 57 | if (props.excludeFromDefaultDataIdentifiers) { 58 | props.excludeFromDefaultDataIdentifiers.forEach((identifier) => { 59 | const index = dataIdentifiers.indexOf(identifier); 60 | if (index !== -1) { 61 | dataIdentifiers.splice(index, 1); 62 | } 63 | }); 64 | } 65 | 66 | // remove duplicates and sort 67 | dataIdentifiers = Array.from(new Set(dataIdentifiers)).sort(); 68 | 69 | if (dataIdentifiers.length === 0) { 70 | throw new Error('There should be at least one data identifier in the policy.'); 71 | } 72 | 73 | /** 74 | * The schema for data protection policy should 75 | * * have a Name, Version. 76 | * * have Statement array of size two. 77 | * * have one statement as Deidentify operation. 78 | * * have another statement as Audit operation. 79 | * * FindingsDestination should be empty if there is no audit destination needed. 80 | * * have at least one data-identifier and must have unique elements. 81 | */ 82 | return { 83 | Name: 'data-protection-policy', 84 | Description: 'Data protection policy for cloudwatch logs', 85 | Version: '2021-06-01', 86 | Statement: [{ 87 | Sid: 'redact-data-protection-policy', 88 | Operation: { 89 | Deidentify: { 90 | MaskConfig: {}, 91 | }, 92 | }, 93 | DataIdentifier: dataIdentifiers, 94 | }, 95 | { 96 | Sid: 'audit-data-protection-policy', 97 | Operation: { 98 | Audit: { 99 | FindingsDestination: props.auditLogGroupName ? getCloudwatchAuditDestination(props.auditLogGroupName) : {}, 100 | }, 101 | }, 102 | DataIdentifier: dataIdentifiers, 103 | }], 104 | }; 105 | }; 106 | 107 | /** 108 | * Adds data protection policy defined in this package to L1 construct of LogGroup 109 | * Use this only when L2 construct "LogGroup" is not available in CfnResource like in APIGAccessLogGroups 110 | * Otherwise if L2 construct is available use addDataProtectionPolicy() below 111 | * @param cfnLogGroup L1 construct for LogGroup. 112 | * @param props data protection policy properties. 113 | */ 114 | export const addDataProtectionPolicyForCfnLogGroup = (cfnLogGroup: CfnLogGroup, props: CwDataProtectionPolicyProps): void => { 115 | // Adding property override since this is available only from cdk-lib 2.54 116 | cfnLogGroup.addPropertyOverride('DataProtectionPolicy', cloudwatchDataProtectionPolicy(props)); 117 | }; 118 | 119 | /** 120 | * Adds data protection policy defined in this package to log group. 121 | * @param logGroup input log group to add data protection policy. 122 | * @param props data protection policy properties. 123 | */ 124 | export const addDataProtectionPolicy = (logGroup: LogGroup, props: CwDataProtectionPolicyProps): void => { 125 | const cfnLogGroup = logGroup.node.defaultChild as CfnLogGroup; 126 | addDataProtectionPolicyForCfnLogGroup(cfnLogGroup, props); 127 | }; 128 | 129 | /** 130 | * Create encryption key used for encrypting CW data protection audit log group 131 | * @param scope construct 132 | * @returns encryption key 133 | */ 134 | const createEncryptionKeyForAuditLogGroup = ( 135 | scope: Construct, 136 | ) : IKey => { 137 | const encryptionKey = new Key(scope, 'CwDataProtectionKmsKey', { 138 | description: 'KMS Key for encrypting CW data protection audit log group', 139 | enableKeyRotation: true, 140 | }); 141 | 142 | const stack = Stack.of(scope); 143 | const policyStatement = new PolicyStatement({ 144 | actions: [ 145 | 'kms:Decrypt*', 146 | 'kms:DescribeKey*', 147 | 'kms:Encrypt*', 148 | 'kms:GenerateDataKey*', 149 | 'kms:ReEncrypt*', 150 | ], 151 | effect: Effect.ALLOW, 152 | principals: [new ServicePrincipal(`logs.${stack.region}.amazonaws.com`)], 153 | resources: ['*'], 154 | }); 155 | policyStatement.addCondition('ArnLike', { 156 | 'kms:EncryptionContext:aws:logs:arn': `arn:${stack.partition}:logs:${stack.region}:${stack.account}:*`, 157 | }); 158 | 159 | // Add the policystatement to the key so that it restricts usage to the specified service principals 160 | encryptionKey.addToResourcePolicy(policyStatement); 161 | 162 | return encryptionKey; 163 | }; 164 | 165 | /** 166 | * Create audit log group for CW data protection audit findings 167 | * @param scope construct 168 | * @param encryptionKey 169 | * @returns audit log group 170 | */ 171 | export const createDataProtectionAuditLogGroup = ( 172 | scope: Construct, 173 | encryptionKey?: IKey, 174 | ) : LogGroup => new LogGroup(scope, 'AuditLogGroup', { 175 | retention: RetentionDays.TEN_YEARS, 176 | encryptionKey: encryptionKey || createEncryptionKeyForAuditLogGroup(scope), 177 | }); -------------------------------------------------------------------------------- /service-constructs/cloudwatch-data-protection/log-properties-updater-custom-resource.ts: -------------------------------------------------------------------------------- 1 | import { PolicyStatement } from 'aws-cdk-lib/aws-iam'; 2 | import {Code, Function, Runtime} from 'aws-cdk-lib/aws-lambda'; 3 | import { CustomResource, Duration } from 'aws-cdk-lib'; 4 | import { Provider } from 'aws-cdk-lib/custom-resources'; 5 | import { Construct } from 'constructs'; 6 | import { DefaultTagHandler } from '../tags/default-tag-handler'; 7 | 8 | export interface LogPropertiesUpdaterProps { 9 | //Name of log group that needs an update to its properties. 10 | readonly logGroupName: string; 11 | /** 12 | * Data protection policy for log groups. This is string form of a JSON format. 13 | * See : https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/cloudwatch-logs-data-protection-policies.html 14 | * for syntax. 15 | * 16 | * @default 17 | * * If there is an existing data protection policy for log group it will be removed. 18 | * * If there is no existing data protection policy, no update will be done. 19 | */ 20 | readonly dataProtectionPolicy?: string; 21 | } 22 | 23 | /** 24 | * Creates a custom resource to update resource properties for cloudwatch logs. 25 | * Supports only DataProtectionPolicy for now. 26 | * Use this when log group is not created as part of stack 27 | * (like lambda service) and log group name is known. 28 | */ 29 | export class LogPropertiesUpdater extends Construct { 30 | private readonly resourceProviderServiceToken: string; 31 | 32 | constructor(scope: Construct, id: string) { 33 | super(scope, id); 34 | DefaultTagHandler.applyTags(this); 35 | 36 | /** 37 | * Lambda handler for custom resource. 38 | * 39 | * Using Function construct here since we cannot use Lambda construct - 40 | * This custom resource will be used in Lambda construct and 41 | * using Lambda here again "might" cause infinite loop. 42 | */ 43 | const logPropertiesUpdaterCustomResourceHandler = new Function(scope, 'LogPropertiesUpdaterCustomResourceLambda', { 44 | code: Code.fromAsset( 45 | "" 46 | ), 47 | runtime: Runtime.NODEJS_18_X, 48 | /** 49 | * This lambda can update log retention and data protection policy 50 | * BUT, CDKConstructs currently updates only data-protection policy 51 | * since LogRetention is handled by other custom resource. 52 | */ 53 | handler: 'dist/update-data-protection-policy.handler', 54 | memorySize: 256, 55 | timeout: Duration.minutes(1), 56 | // create and delete logGroup permissions are already added with default role creation. 57 | initialPolicy: [ 58 | new PolicyStatement({ 59 | actions: [ 60 | 'logs:GetDataProtectionPolicy', 61 | 'logs:PutDataProtectionPolicy', 62 | 'logs:CreateLogDelivery', 63 | 'logs:PutResourcePolicy', 64 | 'logs:DescribeResourcePolicies', 65 | 'logs:DescribeLogGroups', 66 | 'logs:DeleteDataProtectionPolicy'], 67 | resources: ['*'], 68 | }), 69 | ], 70 | }); 71 | 72 | const customResourceProvider = new Provider(scope, 'LogPropertiesUpdaterCustomResourceProvider', { 73 | onEventHandler: logPropertiesUpdaterCustomResourceHandler, 74 | }); 75 | 76 | this.resourceProviderServiceToken = customResourceProvider.serviceToken; 77 | } 78 | 79 | public createCustomResource(id: string, props: LogPropertiesUpdaterProps): void { 80 | // Create one custom resource for each log group props with same lambda handler 81 | new CustomResource(this, id, { 82 | serviceToken: this.resourceProviderServiceToken, 83 | properties: { 84 | LogGroupName: props.logGroupName, 85 | DataProtectionPolicy: props.dataProtectionPolicy, 86 | }, 87 | }); 88 | } 89 | } -------------------------------------------------------------------------------- /service-constructs/dynamodb/dynamodb-table.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; 3 | import { Construct } from 'constructs'; 4 | import { DefaultTagHandler } from '../tags/default-tag-handler'; 5 | 6 | /** 7 | * Opinionated defaults for DynamoDB table. All options available to the AWS CDK's 8 | * [`TableProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-dynamodb.TableProps.html) are available here as well. 9 | */ 10 | export interface DynamoDBTableProps extends dynamodb.TableProps { 11 | 12 | /** 13 | * For Provisioned tables, provide minimum & maximum auto-scaling read capacity. 14 | */ 15 | readonly readScalingProps?: dynamodb.EnableScalingProps; 16 | 17 | /** 18 | * For Provisioned tables, provide minimum & maximum auto-scaling write capacity for the table. 19 | */ 20 | readonly writeScalingProps?: dynamodb.EnableScalingProps; 21 | } 22 | 23 | /** 24 | * Opinionated defaults for DynamoDB global secondary index. All options available to the AWS CDK's 25 | * [`GlobalSecondaryIndexProps`](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-dynamodb.GlobalSecondaryIndexProps.html) are available here as well. 26 | */ 27 | export interface DynamoDBGlobalSecondaryIndexProps extends dynamodb.GlobalSecondaryIndexProps { 28 | 29 | /** 30 | * For Provisioned tables, provide minimum & maximum auto-scaling read capacity for the index. 31 | */ 32 | readonly readScalingProps?: dynamodb.EnableScalingProps; 33 | 34 | /** 35 | * For Provisioned tables, provide minimum & maximum auto-scaling write capacity for the index. 36 | */ 37 | readonly writeScalingProps?: dynamodb.EnableScalingProps; 38 | } 39 | 40 | /** 41 | * Default values for DynamoDB utilization tracking. 42 | */ 43 | const DEFAULT_UTILIZATION_SCALING_PROPS: dynamodb.UtilizationScalingProps = { 44 | /** 45 | * Target utilization percentage for the attribute. 46 | * Setting it to default 60 percent. 47 | */ 48 | targetUtilizationPercent: 60, 49 | /** 50 | * Period after a scale in activity completes before another scale in activity can start. 51 | * Setting it to default 60 seconds. 52 | */ 53 | scaleInCooldown: cdk.Duration.seconds(60), 54 | /** 55 | * Period after a scale out activity completes before another scale out activity can start. 56 | * Setting it to default 60 seconds. 57 | */ 58 | scaleOutCooldown: cdk.Duration.seconds(60), 59 | }; 60 | 61 | /** 62 | * This `DynamoDBTable` construct builds on AWS CDK's Table construct by enforcing opinionated practices 63 | * within the organization. 64 | * 65 | * Read more about this construct at /documentation/dynamodb/DynamoDBTable.md 66 | */ 67 | export class DynamoDBTable extends dynamodb.Table { 68 | 69 | // Indicates whether table is using Provisioned billing mode. 70 | 71 | private readonly usesProvisionedBillingMode: boolean; 72 | 73 | constructor(scope: Construct, id: string, props: DynamoDBTableProps) { 74 | if (!props) { 75 | throw new Error('DynamoDB table props must be provided'); 76 | } 77 | 78 | let { billingMode, readCapacity, writeCapacity, pointInTimeRecovery, deletionProtection, encryption } = props; 79 | // If billingMode is not explicitly specified, setting it to PAY_PER_REQUEST(OnDemand mode). 80 | if (!billingMode) { 81 | billingMode = dynamodb.BillingMode.PAY_PER_REQUEST; 82 | } 83 | 84 | if (props.billingMode === dynamodb.BillingMode.PROVISIONED) { 85 | if (!props.readScalingProps || !props.writeScalingProps) { 86 | throw new Error('For provisioned DynamoDB table, auto-scaling readScalingProps & writeScalingProps are required.'); 87 | } 88 | // Setting default read & write capacity of table to auto-scaling minimum capacity. 89 | if (!readCapacity) { 90 | readCapacity = props.readScalingProps.minCapacity; 91 | } 92 | if (!writeCapacity) { 93 | writeCapacity = props.writeScalingProps.minCapacity; 94 | } 95 | } 96 | 97 | // If pointInTimeRecovery is not explicitly specified, enabling it by default. 98 | if (pointInTimeRecovery === undefined) { 99 | pointInTimeRecovery = true; 100 | } 101 | 102 | // If deletionProtection is not explicitly specified, enabling it by default. 103 | if (deletionProtection === undefined) { 104 | deletionProtection = true; 105 | } 106 | 107 | // If encryption is not explicitly specified, use dynamodb.TableEncryption.CUSTOMER_MANAGED. 108 | // If an `encryptionKey` is not specified, DynamoDB automatically creates the encryption key. 109 | if (!encryption) { 110 | encryption = dynamodb.TableEncryption.CUSTOMER_MANAGED; 111 | } 112 | 113 | const updatedProps: DynamoDBTableProps = { 114 | billingMode, 115 | encryption, 116 | pointInTimeRecovery, 117 | deletionProtection, 118 | readCapacity, 119 | writeCapacity, 120 | ...props, 121 | }; 122 | 123 | super(scope, id, updatedProps); 124 | DefaultTagHandler.applyTags(this); 125 | 126 | if (updatedProps.billingMode === dynamodb.BillingMode.PROVISIONED) { 127 | this.usesProvisionedBillingMode = true; 128 | 129 | const autoScaleReadCapacity = this.autoScaleReadCapacity(updatedProps.readScalingProps!); 130 | autoScaleReadCapacity.scaleOnUtilization(DEFAULT_UTILIZATION_SCALING_PROPS); 131 | 132 | const autoScaleWriteCapacity = this.autoScaleWriteCapacity(updatedProps.writeScalingProps!); 133 | autoScaleWriteCapacity.scaleOnUtilization(DEFAULT_UTILIZATION_SCALING_PROPS); 134 | } else { 135 | this.usesProvisionedBillingMode = false; 136 | } 137 | } 138 | 139 | /** 140 | * Add a global secondary index of table. 141 | * 142 | * For Provisioned table, it by default configures capacity auto-scaling for global secondary indexes. 143 | * Default auto-scaling properties can be overridden by invoking api explicitly. 144 | * e.g. for overriding autoscale read configuration: 145 | * ``` 146 | * table.autoScaleGlobalSecondaryIndexReadCapacity(indexName, readScalingProps); 147 | * ``` 148 | * @param props the property of global secondary index. 149 | */ 150 | addGlobalSecondaryIndex(props: DynamoDBGlobalSecondaryIndexProps): void { 151 | let { readCapacity, writeCapacity } = props; 152 | if (this.usesProvisionedBillingMode) { 153 | if (!props.readScalingProps || !props.writeScalingProps) { 154 | throw new Error('For provisioned DynamoDB table, auto-scaling readScalingProps & writeScalingProps for global secondary index are required.'); 155 | } 156 | if (!readCapacity) { 157 | readCapacity = props.readScalingProps.minCapacity; 158 | } 159 | if (!writeCapacity) { 160 | writeCapacity = props.writeScalingProps.minCapacity; 161 | } 162 | } 163 | 164 | const updatedProps: DynamoDBGlobalSecondaryIndexProps = { 165 | readCapacity, 166 | writeCapacity, 167 | ...props, 168 | }; 169 | 170 | super.addGlobalSecondaryIndex(updatedProps); 171 | 172 | if (this.usesProvisionedBillingMode) { 173 | const autoScaleReadCapacity = this.autoScaleGlobalSecondaryIndexReadCapacity(updatedProps.indexName, updatedProps.readScalingProps!); 174 | autoScaleReadCapacity.scaleOnUtilization(DEFAULT_UTILIZATION_SCALING_PROPS); 175 | 176 | const autoScaleWriteCapacity = this.autoScaleGlobalSecondaryIndexWriteCapacity(updatedProps.indexName, updatedProps.writeScalingProps!); 177 | autoScaleWriteCapacity.scaleOnUtilization(DEFAULT_UTILIZATION_SCALING_PROPS); 178 | } 179 | } 180 | } -------------------------------------------------------------------------------- /service-constructs/ecs/fargate/alb-fargate-service.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import * as ecs from 'aws-cdk-lib/aws-ecs'; 3 | import { FargateService } from 'aws-cdk-lib/aws-ecs'; 4 | import * as ec2 from 'aws-cdk-lib/aws-ec2'; 5 | import { SecurityGroup } from 'aws-cdk-lib/aws-ec2'; 6 | import { Duration } from 'aws-cdk-lib'; 7 | import { CustomApplicationLoadBalancer } from '../../alb/alb'; 8 | import { addDataProtectionPolicyToFargateContainerLogs } from './fargate-data-protection-policy-util'; 9 | import { CwDataProtectionPolicyProps } from '../../cloudwatch-data-protection/data-protection-policy'; 10 | import { DefaultTagHandler } from '../../tags/default-tag-handler'; 11 | 12 | const DEFAULT_TASK_COUNT = 1; 13 | const DEFAULT_PLATFORM_VERSION = ecs.FargatePlatformVersion.LATEST; 14 | const DEFAULT_MIN_HEALTHY_PERCENT = 100; 15 | const DEFAULT_MAX_HEALTHY_PERCENT = 200; 16 | const DEFAULT_HEALTH_CHECK_GRACE_PERIOD = Duration.seconds(60); 17 | const DEFAULT_CIRCUIT_BREAKER = { 18 | rollback: true, 19 | }; 20 | const DEFAULT_CONTAINER_PORT = 8080; 21 | 22 | export interface AlbFargateServiceProps { 23 | 24 | 25 | //The name of the cluster that hosts the service. 26 | readonly ecsCluster: ecs.ICluster; 27 | 28 | /** 29 | * The application load balancer that will serve traffic to the service. 30 | * The VPC attribute of a load balancer must be specified for it to be used 31 | * to create a new service with this pattern. 32 | */ 33 | readonly loadBalancer: CustomApplicationLoadBalancer; 34 | 35 | /** 36 | * The properties required to create a new task definition, like memory limits, 37 | * cpu units, task role, image/service packages in the task definition. 38 | */ 39 | readonly taskDefinition: ecs.FargateTaskDefinition; 40 | 41 | /** 42 | * Available platform versions: 43 | * https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ecs.FargatePlatformVersion.html 44 | * 45 | * @default LATEST 46 | */ 47 | readonly platformVersion?: ecs.FargatePlatformVersion; 48 | 49 | /** 50 | * The desired number of instantiations of the task definition to keep running on the service. 51 | * The minimum value is 1 52 | * 53 | * @default 1 54 | */ 55 | readonly desiredCount?: number; 56 | 57 | /** 58 | * The minimum number of tasks, specified as a percentage of the Amazon ECS service's DesiredCount value, 59 | * that must continue to run and remain healthy during a deployment. 60 | * 61 | * @default 100 62 | */ 63 | readonly minHealthyPercent?: number; 64 | 65 | /** 66 | * The maximum number of tasks, specified as a percentage of the Amazon ECS service's DesiredCount value, 67 | * that can run in a service during a deployment. 68 | * 69 | * @default 200 70 | */ 71 | readonly maxHealthyPercent?: number; 72 | 73 | /** 74 | * The period of time, in seconds, that the Amazon ECS service scheduler ignores 75 | * unhealthy Elastic Load Balancing target health checks after a task has first started. 76 | * 77 | * @default cdk.Duration.seconds(60) 78 | */ 79 | readonly healthCheckGracePeriod?: Duration; 80 | 81 | /** 82 | * The name of the service. 83 | * 84 | * @default When serviceName is not provided, service name will not be in the generated template 85 | */ 86 | readonly serviceName?: string; 87 | 88 | /** 89 | * Properties for cloudwatch data protection policy on service logGroup. 90 | * 91 | * @default no data protection policy will be enabled. 92 | */ 93 | readonly dataProtectionPolicyProps?: CwDataProtectionPolicyProps; 94 | } 95 | 96 | export class AlbFargateService extends Construct { 97 | public readonly service: FargateService; 98 | 99 | public readonly ecsCluster: ecs.ICluster; 100 | 101 | public readonly loadBalancer: CustomApplicationLoadBalancer; 102 | 103 | public readonly serviceSecurityGroup: SecurityGroup; 104 | 105 | constructor(scope: Construct, id: string, props: AlbFargateServiceProps) { 106 | super(scope, id); 107 | DefaultTagHandler.applyTags(this); 108 | 109 | this.serviceSecurityGroup = new SecurityGroup(this, 'ServiceSecurityGroup', { 110 | vpc: props.ecsCluster.vpc, 111 | allowAllOutbound: true, 112 | }); 113 | 114 | this.ecsCluster = props.ecsCluster; 115 | this.loadBalancer = props.loadBalancer; 116 | this.service = new FargateService(this, 'FargateService', { 117 | cluster: this.ecsCluster, 118 | taskDefinition: props.taskDefinition, 119 | desiredCount: props.desiredCount || DEFAULT_TASK_COUNT, 120 | platformVersion: props.platformVersion || DEFAULT_PLATFORM_VERSION, 121 | minHealthyPercent: props.minHealthyPercent || DEFAULT_MIN_HEALTHY_PERCENT, 122 | maxHealthyPercent: props.maxHealthyPercent || DEFAULT_MAX_HEALTHY_PERCENT, 123 | healthCheckGracePeriod: props.healthCheckGracePeriod || DEFAULT_HEALTH_CHECK_GRACE_PERIOD, 124 | circuitBreaker: DEFAULT_CIRCUIT_BREAKER, 125 | securityGroups: [this.serviceSecurityGroup], 126 | ...(props.serviceName && { serviceName: props.serviceName }), 127 | }); 128 | 129 | // Add data protection policy to aws-logs 130 | addDataProtectionPolicyToFargateContainerLogs(this, this.service, this.service.taskDefinition, props.dataProtectionPolicyProps); 131 | 132 | const containerPort = props.taskDefinition.defaultContainer?.containerPort ?? DEFAULT_CONTAINER_PORT; 133 | 134 | this.serviceSecurityGroup.addIngressRule( 135 | this.loadBalancer.securityGroup, 136 | ec2.Port.tcp(containerPort), 137 | `Allow the inbound traffic on port ${containerPort}`, 138 | ); 139 | 140 | this.loadBalancer.targetGroup.addTarget(this.service); 141 | } 142 | } -------------------------------------------------------------------------------- /service-constructs/ecs/fargate/fargate-data-protection-policy-util.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { 3 | ContainerDefinition, FargateService, FargateTaskDefinition, 4 | } from 'aws-cdk-lib/aws-ecs'; 5 | import { LogGroup } from 'aws-cdk-lib/aws-logs'; 6 | import { 7 | addDataProtectionPolicy, 8 | CwDataProtectionPolicyProps, 9 | createDataProtectionAuditLogGroup, 10 | } from '../../cloudwatch-data-protection/data-protection-policy'; 11 | 12 | /** 13 | * Get LogGroup cdk resource in CDK template chain. 14 | * @param container container image for fargate. 15 | * 16 | * returns LogGroup construct. 17 | * 18 | * @error Throws an error when LogGroup construct is not found. 19 | * * This can happen if - 20 | * * You are using fire-lens sidecar - Setup data-protection with properties in fire-lens construct 21 | * * You are using a logGroup created with different id - You would already have LogGroup Construct. 22 | * Setup data protection by calling addDataProtectionPolicy() on this logGroup 23 | */ 24 | const getLogGroupForFargateService = ( 25 | container: ContainerDefinition, 26 | ): LogGroup => { 27 | // default aws-logs logGroup is created with id "LogGroup". 28 | const logGroup = container.node.tryFindChild('LogGroup') as LogGroup; 29 | 30 | if (!logGroup) { 31 | const errorMessage = 'No child found with id: \'LogGroup\'.' 32 | + ' This means you have setup Logs with fire-lens ' 33 | + 'or created a new log group for service logs with different Id.'; 34 | throw new Error(errorMessage); 35 | } 36 | 37 | return logGroup; 38 | }; 39 | 40 | /** 41 | * Get service container from Fargate service task definition. 42 | * @param taskDefinition Fargate task definition. 43 | * 44 | * returns ContainerDefinition or undefined. 45 | */ 46 | const getServiceContainerForFargateService = ( 47 | taskDefinition: FargateTaskDefinition, 48 | ): ContainerDefinition | undefined => taskDefinition.defaultContainer; 49 | 50 | /** 51 | * Adds data protection policy to fargate container aws-logs 52 | * @param scope 53 | * @param service 54 | * @param taskDefinition 55 | * @param dataProtectionPolicyProps properties for cw dataProtection policy 56 | */ 57 | export const addDataProtectionPolicyToFargateContainerLogs = ( 58 | scope: Construct, 59 | service: FargateService, 60 | taskDefinition: FargateTaskDefinition, 61 | dataProtectionPolicyProps?: CwDataProtectionPolicyProps, 62 | ) : void => { 63 | const serviceContainer = getServiceContainerForFargateService(taskDefinition); 64 | if (serviceContainer) { 65 | let serviceLogGroup; 66 | try { 67 | serviceLogGroup = getLogGroupForFargateService(serviceContainer); 68 | } catch (ex) { 69 | // No log group found, this means you have to set up Logs with fire-lens or 70 | // create a new log group for service logs with a different id. 71 | return; 72 | } 73 | // If no auditLogGroupName provided in props, create default audit log group. 74 | let dataProtectionPolicyPropsOverride = dataProtectionPolicyProps; 75 | if (!dataProtectionPolicyProps?.auditLogGroupName) { 76 | // The audit log group must already exist prior to creating the data protection policy. 77 | // https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_logs.DataProtectionPolicyProps.html 78 | const auditLogGroup = createDataProtectionAuditLogGroup(scope); 79 | dataProtectionPolicyPropsOverride = { ...dataProtectionPolicyProps, auditLogGroupName: auditLogGroup.logGroupName }; 80 | // Audit log group needs to be created before using in data protection policy 81 | service.node.addDependency(auditLogGroup); 82 | } 83 | addDataProtectionPolicy(serviceLogGroup, dataProtectionPolicyPropsOverride!); 84 | } 85 | }; -------------------------------------------------------------------------------- /service-constructs/ecs/fargate/jlb-relay.ts: -------------------------------------------------------------------------------- 1 | import * as ecs from 'aws-cdk-lib/aws-ecs'; 2 | import { RetentionDays } from 'aws-cdk-lib/aws-logs'; 3 | import { Duration } from 'aws-cdk-lib'; 4 | import { Construct } from 'constructs'; 5 | import { SidecarProps } from './sidecar'; 6 | import { DefaultTagHandler } from '../../tags/default-tag-handler'; 7 | import {StageContext} from "aws-sdk/clients/codepipeline"; 8 | import {FargateTaskDefinition, RepositoryImage} from "aws-cdk-lib/aws-ecs"; 9 | 10 | export const JLB_RELAY_PORT = 9001; 11 | export const DEFAULT_SERVICE_PORT = 8080; 12 | 13 | export interface JlbRelaySidecarProps extends SidecarProps { 14 | //The appName will be used as JlbRelay metric namespace prefix and metric prefix. 15 | readonly appName: string; 16 | 17 | readonly appContainerDefinition: ecs.ContainerDefinition; 18 | 19 | readonly stageContext: StageContext; 20 | 21 | //Certificate material set used for on host tls termination. 22 | readonly certificateMaterialSet: string; 23 | 24 | //Limits the number of connections JLBRelay can open to the backend service 25 | readonly maxConnections: number; 26 | 27 | /* ------------- -------------- -------------- 28 | * TCP | | HTTPS | | HTTP | | 29 | * 443 | NLB | 9001 | JlbRelay | 8080 | | 30 | * +----------->+ +----------->+ Sidecar +------------>+ Service | 31 | * | | | | | | 32 | * ------------- -------------- -------------- 33 | */ 34 | //(default: 8080) - your application port that will accept requests coming from an HTTP listener 35 | readonly servicePort?: number; 36 | 37 | } 38 | 39 | export class JlbRelaySidecar extends Construct { 40 | readonly appName: string; 41 | 42 | readonly metricNamespace: string; 43 | 44 | readonly jlbRelayContainer: ecs.ContainerDefinition; 45 | 46 | constructor(taskDef: ecs.FargateTaskDefinition, id: string, props: JlbRelaySidecarProps) { 47 | super(taskDef, id); 48 | DefaultTagHandler.applyTags(this); 49 | this.appName = props.appName; 50 | this.metricNamespace = `${props.appName}-JlbRelay`; 51 | 52 | const environmentVars: { [key: string]: string } = { 53 | STAGE: props.stageContext.name, 54 | METRICS_NAMESPACE: this.metricNamespace, 55 | RELAY_METRICS_PREFIX: props.appName, 56 | RELAY_METRICS_MARKETPLACE_PREFIX: props.appName, 57 | // JlbRelay recommends 1 worker per 100 backend connections 58 | RELAY_WORKERS: (Math.trunc(props.maxConnections / 100) + 1).toString(), 59 | // 2 connections reserved for JlbRelay health checks 60 | // for reserved health check connections, service needs to enable ppv2 for NLB 61 | RELAY_UPSTREAM_MAX_CONNECTIONS: (props.maxConnections - 2).toString(), 62 | RELAY_UPSTREAM_PORT: props.servicePort ? props.servicePort.toString() : DEFAULT_SERVICE_PORT.toString(), 63 | RELAY_HTTPS_UPSTREAM_PORT: props.servicePort ? props.servicePort.toString() : DEFAULT_SERVICE_PORT.toString(), 64 | RELAY_HTTPS_DOWNSTREAM_PORT: JLB_RELAY_PORT.toString(), 65 | CERTIFICATE_MATERIAL_SET: props.certificateMaterialSet, 66 | }; 67 | // Ensure latest ECR image is used on each deployment on dev AWS account 68 | if (props.stageContext.name === 'Dev') { 69 | environmentVars.DEPLOYMENT_ID = Math.random().toString(36).substring(8); 70 | } 71 | 72 | this.jlbRelayContainer = taskDef.addContainer('JlbRelay', { 73 | essential: true, 74 | image: new RepositoryImage("JlbRelayImage",{ 75 | //Properties related to repository 76 | }), 77 | logging: new ecs.AwsLogDriver({ 78 | streamPrefix: `${props.appName}-JlbRelay`, 79 | logRetention: RetentionDays.TEN_YEARS, 80 | }), 81 | cpu: props.cpu, 82 | memoryLimitMiB: props.memoryLimitMiB, 83 | memoryReservationMiB: props.memoryReservationMiB, 84 | environment: environmentVars, 85 | healthCheck: { 86 | command: ['CMD-SHELL', '/opt/amazon/bin/health-check.sh'], 87 | // After 5 minute we'll expect the service to be up 88 | startPeriod: Duration.minutes(5), 89 | timeout: Duration.seconds(5), 90 | }, 91 | }); 92 | 93 | this.jlbRelayContainer.addPortMappings({ 94 | containerPort: JLB_RELAY_PORT, 95 | protocol: ecs.Protocol.TCP, 96 | }); 97 | 98 | this.jlbRelayContainer.addContainerDependencies({ 99 | container: props.appContainerDefinition, 100 | condition: ecs.ContainerDependencyCondition.HEALTHY, 101 | }); 102 | 103 | // JlbRelay must be the default container, so that this is the one receiving traffic from the NLB 104 | taskDef.defaultContainer = this.jlbRelayContainer; 105 | 106 | this.addAccessToCertificate(taskDef, props); 107 | } 108 | 109 | private addAccessToCertificate(taskDef: FargateTaskDefinition, props: JlbRelaySidecarProps) { 110 | //TODO: Add Access to your certificate 111 | } 112 | } -------------------------------------------------------------------------------- /service-constructs/ecs/fargate/nlb-fargate-service.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as cdk from 'aws-cdk-lib'; 3 | import * as ecsPatterns from 'aws-cdk-lib/aws-ecs-patterns'; 4 | import * as ec2 from 'aws-cdk-lib/aws-ec2'; 5 | import * as ecs from 'aws-cdk-lib/aws-ecs'; 6 | import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; 7 | import { BucketEncryption } from 'aws-cdk-lib/aws-s3'; 8 | import { DeploymentCircuitBreaker } from 'aws-cdk-lib/aws-ecs'; 9 | import { Construct } from 'constructs'; 10 | import { AccessLogsBucket } from '../../s3/access-bucket-log'; 11 | import { JLB_RELAY_PORT } from './jlb-relay'; 12 | import { addDataProtectionPolicyToFargateContainerLogs } from './fargate-data-protection-policy-util'; 13 | import { CwDataProtectionPolicyProps } from '../../cloudwatch-data-protection/data-protection-policy'; 14 | import { DefaultTagHandler } from '../../tags/default-tag-handler'; 15 | 16 | /* 17 | * Without end to end TLS termination 18 | * ------------- -------------- 19 | * TCP | | HTTP | | 20 | * 80 | NLB | 8080 | Service | 21 | * ApiGw+--------->+ +----------->+ | 22 | * | | | | 23 | * ------------- -------------- 24 | * 25 | * With end to end TLS termination 26 | * ------------- -------------- -------------- 27 | * TCP | | HTTPS | | HTTP | | 28 | * 443 | NLB | 9001 | JlbRelay | 8080 | Service | 29 | * ApiGw+--------->+ +----------->+ Sidecar +------------>+ | 30 | * | | | | | | 31 | * ------------- -------------- -------------- 32 | */ 33 | export interface EndToEndTlsConfig { 34 | } 35 | 36 | export interface NlbFargateServiceProps { 37 | 38 | 39 | //The ECS cluster. Your VPC configuration is defined here. 40 | readonly ecsCluster: ecs.ICluster; 41 | 42 | 43 | //Define your task memory limits, cpu units, task role, image/service packages in the task definition. 44 | readonly taskDefinition: ecs.FargateTaskDefinition; 45 | 46 | 47 | //Define name for S3 bucket containing NLB Access logs 48 | readonly accessLogsS3BucketName?: string; 49 | 50 | /** 51 | * Whether the NLB is public. Setting this to true is discouraged. 52 | * @default false 53 | */ 54 | readonly publicAccessEnabled?: boolean; 55 | 56 | /** 57 | * Available platform versions: 58 | * https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ecs.FargatePlatformVersion.html 59 | * @default LATEST 60 | */ 61 | readonly platformVersion?: ecs.FargatePlatformVersion; 62 | 63 | /** 64 | * Count of Fargate tasks to launch in the service. 65 | * @default 1 66 | */ 67 | readonly desiredCount?: number; 68 | 69 | /** 70 | * The lower limit on the number of your service's tasks that must remain in the RUNNING state during a deployment, 71 | * as a percentage of the desiredCount (rounded up to the nearest integer). 72 | * 73 | * @default 100 74 | */ 75 | readonly minHealthyPercent?: number; 76 | 77 | /** 78 | * The upper limit on the number of your service's tasks that are allowed in the RUNNING or PENDING state during a deployment, 79 | * as a percentage of the desiredCount (rounded down to the nearest integer). 80 | * 81 | * @default 200 82 | */ 83 | readonly maxHealthyPercent?: number; 84 | 85 | /** 86 | * The period of time that ECS service scheduler should ignore unhealthy ELB target health checks 87 | * after a task has first started. 88 | * 89 | * @default cdk.Duration.seconds(60) 90 | */ 91 | readonly healthCheckGracePeriod?: cdk.Duration; 92 | 93 | /** 94 | * ELB waits this much time before completing the deregistration process, which can help in-flight requests 95 | * to the target group to complete. 96 | * 97 | * @default cdk.Duration.seconds(60) 98 | */ 99 | readonly deregistrationDelay?: cdk.Duration; 100 | 101 | readonly endToEndTlsConfig?: EndToEndTlsConfig; 102 | 103 | /** 104 | * Whether to enable deployment circuit breaker for your fargate service. 105 | * 106 | * @default true 107 | * @experimental 108 | */ 109 | readonly circuitBreakerEnabled?: boolean; 110 | 111 | /** 112 | * The deployment circuit breaker to use for the service. 113 | * A default circuit breaker (rollback: false) will be added if circuitBreakerEnabled is true and no circuitBreaker is provided. 114 | * 115 | * @experimental 116 | */ 117 | readonly circuitBreaker?: DeploymentCircuitBreaker; 118 | 119 | /** 120 | * Properties for cloudwatch data protection policy on service logGroup. 121 | * 122 | * @default no data protection policy will be enabled. 123 | */ 124 | readonly dataProtectionPolicyProps?: CwDataProtectionPolicyProps; 125 | } 126 | 127 | const DEFAULT_PUBLIC_ACCESS = false; 128 | const DEFAULT_TASK_COUNT = 1; 129 | const DEFAULT_PLATFORM_VERSION = ecs.FargatePlatformVersion.LATEST; 130 | const DEFAULT_MIN_HEALTHY_PERCENT = 100; 131 | const DEFAULT_MAX_HEALTHY_PERCENT = 200; 132 | const DEFAULT_HEALTH_CHECK_GRACE_PERIOD = cdk.Duration.seconds(60); 133 | const DEFAULT_DEREGISTRATION_DELAY = cdk.Duration.seconds(60); 134 | const DEFAULT_CIRCUIT_BREAKER_ENABLED = true; 135 | const DEFAULT_CIRCUIT_BREAKER = { 136 | rollback: false, 137 | }; 138 | 139 | export class NlbFargateService extends Construct { 140 | readonly nlbFargateService: ecsPatterns.NetworkLoadBalancedFargateService; 141 | 142 | constructor(scope: Construct, id: string, props: NlbFargateServiceProps) { 143 | super(scope, id); 144 | DefaultTagHandler.applyTags(this); 145 | 146 | const circuitBreakerEnabled = props.circuitBreakerEnabled === undefined ? DEFAULT_CIRCUIT_BREAKER_ENABLED : props.circuitBreakerEnabled; 147 | const circuitBreaker = circuitBreakerEnabled ? (props.circuitBreaker || DEFAULT_CIRCUIT_BREAKER) : undefined; 148 | 149 | this.nlbFargateService = new ecsPatterns.NetworkLoadBalancedFargateService(this, 'FargateService', { 150 | cluster: props.ecsCluster, 151 | taskDefinition: props.taskDefinition, 152 | publicLoadBalancer: props.publicAccessEnabled || DEFAULT_PUBLIC_ACCESS, 153 | desiredCount: props.desiredCount || DEFAULT_TASK_COUNT, 154 | platformVersion: props.platformVersion || DEFAULT_PLATFORM_VERSION, 155 | minHealthyPercent: props.minHealthyPercent || DEFAULT_MIN_HEALTHY_PERCENT, 156 | maxHealthyPercent: props.maxHealthyPercent || DEFAULT_MAX_HEALTHY_PERCENT, 157 | healthCheckGracePeriod: props.healthCheckGracePeriod || DEFAULT_HEALTH_CHECK_GRACE_PERIOD, 158 | circuitBreaker, 159 | }); 160 | 161 | // Add data protection policy to aws-logs 162 | addDataProtectionPolicyToFargateContainerLogs(this, this.nlbFargateService.service, this.nlbFargateService.taskDefinition, props.dataProtectionPolicyProps); 163 | 164 | /* 165 | * Enabling ALB Access logs by default. 166 | * When we enable access logs, we must specify an S3 bucket for the access logs. 167 | * The bucket should use Amazon S3-Managed Encryption Keys (SSE-S3). 168 | * Additional requirement details can be found in : 169 | * https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-access-logs.html 170 | */ 171 | const bucket = new AccessLogsBucket(this, 'nlb-access-logs', { 172 | bucketName: props.accessLogsS3BucketName, 173 | encryption: BucketEncryption.S3_MANAGED, 174 | }); 175 | this.nlbFargateService.loadBalancer.logAccessLogs(bucket, 'nlb-fargate-access-logs'); 176 | 177 | /* 178 | * By default, the security groups created by NetworkLoadBalancedFargateService allow all ingress traffic. 179 | * Those security groups are for the ECS service tasks since the NLB itself does not have security groups 180 | * associated with it. It allows the NLB health checks to work and for traffic to reach the ECS stack from 181 | * the NLB. The following changes the security groups to only allow inbound traffic originating from within the VPC. 182 | */ 183 | if (props.endToEndTlsConfig) { 184 | // TCP Passthrough from ApiGw to NLB 185 | const listener: elbv2.CfnListener = this.nlbFargateService.listener.node.defaultChild as elbv2.CfnListener; 186 | listener.port = 443; 187 | listener.protocol = elbv2.Protocol.TCP; 188 | 189 | this.nlbFargateService.service.connections.allowFrom( 190 | ec2.Peer.ipv4(this.nlbFargateService.cluster.vpc.vpcCidrBlock), 191 | ec2.Port.tcp(JLB_RELAY_PORT), 192 | `Allow only the traffic that originated from within the VPC on port ${JLB_RELAY_PORT}`, 193 | ); 194 | 195 | // Point nlb to jlb relay instead of the service 196 | const targetGroup = this.nlbFargateService.targetGroup.node.defaultChild as elbv2.CfnTargetGroup; 197 | targetGroup.port = JLB_RELAY_PORT; 198 | targetGroup.protocol = elbv2.Protocol.TCP; 199 | 200 | this.nlbFargateService.targetGroup.configureHealthCheck({ 201 | port: JLB_RELAY_PORT.toString(), 202 | protocol: elbv2.Protocol.TCP, 203 | }); 204 | // This flag tells NLB whether to attach “nuggets” to the messages using PPv2 format. 205 | // Nuggets provide ID of the source VPC and VPC endpoint as well as the IP of the message originator. 206 | // We recommend enabling PPv2 at NLB for supporting VPC endpoints. 207 | this.nlbFargateService.targetGroup.setAttribute('proxy_protocol_v2.enabled', 'false'); 208 | } else { 209 | this.nlbFargateService.service.connections.allowFrom( 210 | ec2.Peer.ipv4(this.nlbFargateService.cluster.vpc.vpcCidrBlock), 211 | ec2.Port.tcp(80), 212 | 'Allow only the traffic that originated from within the VPC on port 80', 213 | ); 214 | 215 | this.nlbFargateService.service.connections.allowFrom( 216 | ec2.Peer.ipv4(this.nlbFargateService.cluster.vpc.vpcCidrBlock), 217 | ec2.Port.tcp(8080), 218 | 'Allow only the traffic that originated from within the VPC on port 8080', 219 | ); 220 | 221 | // ELB health checks configuration 222 | this.nlbFargateService.targetGroup.configureHealthCheck({ 223 | port: 'traffic-port', 224 | protocol: elbv2.Protocol.TCP, 225 | }); 226 | } 227 | 228 | // ELB deregistration delay configuration 229 | this.nlbFargateService.targetGroup.setAttribute( 230 | 'deregistration_delay.timeout_seconds', 231 | (props.deregistrationDelay && props.deregistrationDelay.toSeconds().toString()) || DEFAULT_DEREGISTRATION_DELAY.toSeconds().toString(), 232 | ); 233 | } 234 | } -------------------------------------------------------------------------------- /service-constructs/ecs/fargate/sidecar.ts: -------------------------------------------------------------------------------- 1 | export interface SidecarProps { 2 | /** 3 | * The minimum number of CPU units to reserve for the container. 4 | * 5 | * @default - No minimum CPU units reserved. 6 | */ 7 | readonly cpu?: number; 8 | 9 | /** 10 | * The amount (in MiB) of memory to present to the container. 11 | * If your container attempts to exceed the allocated memory, the container 12 | * is terminated. 13 | * 14 | * @default - No memory limit. 15 | */ 16 | readonly memoryLimitMiB?: number; 17 | 18 | /** 19 | * The soft limit (in MiB) of memory to reserve for the container. 20 | * 21 | * When system memory is under heavy contention, Docker attempts to keep the 22 | * container memory to this soft limit. However, your container can consume more 23 | * memory when it needs to, up to either the hard limit specified with the memory 24 | * parameter (if applicable), or all of the available memory on the container 25 | * instance, whichever comes first. 26 | * 27 | * @default - No memory reserved. 28 | */ 29 | readonly memoryReservationMiB?: number; 30 | } -------------------------------------------------------------------------------- /service-constructs/kms/customer-managed-key.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import iam = require('aws-cdk-lib/aws-iam'); 3 | import kms = require('aws-cdk-lib/aws-kms'); 4 | import { DefaultTagHandler } from '../tags/default-tag-handler'; 5 | 6 | export interface CMKProps { 7 | //Initial alias to add to the key 8 | readonly keyAlias: string; 9 | 10 | //A description of the key. 11 | readonly keyDescription: string; 12 | 13 | //A list of IAM Principals to be granted access for KMS Key 14 | readonly principals?: iam.IPrincipal[]; 15 | 16 | //A list of actions to be granted on the KMS key 17 | // https://docs.aws.amazon.com/kms/latest/APIReference/API_Operations.html 18 | readonly actions: string[]; 19 | } 20 | 21 | /** 22 | * CMK construct creates CMK construct with opinionated practices enforced. 23 | * This enforces keyRotation by default to true, not enabling key rotation could be a security risk 24 | */ 25 | export class CustomerManagedKey extends Construct { 26 | public readonly encryptionKey: kms.Key; 27 | 28 | constructor(scope: Construct, id: string, props: CMKProps) { 29 | super(scope, id); 30 | DefaultTagHandler.applyTags(this); 31 | 32 | this.encryptionKey = new kms.Key(this, props.keyAlias, { 33 | alias: props.keyAlias, 34 | description: props.keyDescription, 35 | enableKeyRotation: true, 36 | }); 37 | 38 | /** 39 | * Grant root account permissions to perform actions on CMK 40 | * In AWS KMS, you must attach resource-based policies to your customer master keys (CMKs). In a key policy, 41 | * you use "*" for the resource, which effectively means "this CMK." A key policy applies only to the CMK it is 42 | * attached to. See: https://docs.aws.amazon.com/kms/latest/developerguide/control-access-overview.html 43 | */ 44 | this.encryptionKey.addToResourcePolicy( 45 | new iam.PolicyStatement({ 46 | effect: iam.Effect.ALLOW, 47 | principals: [new iam.AccountRootPrincipal()], 48 | actions: props.actions, 49 | resources: ['*'], 50 | }), 51 | ); 52 | 53 | // grant permissions to additional roles/principals provided in the props 54 | if (props.principals !== undefined) { 55 | props.principals.forEach((role) => { 56 | this.encryptionKey.grant(role, ...props.actions); 57 | }); 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /service-constructs/s3/access-bucket-log.ts: -------------------------------------------------------------------------------- 1 | import {Construct} from "constructs"; 2 | import * as s3 from "aws-cdk-lib/aws-s3"; 3 | import * as cdk from "aws-cdk-lib"; 4 | import {Bucket, BucketProps} from "./s3-bucket"; 5 | 6 | const LOGS_CURRENT_VERSION_EXPIRATION_DAYS = 3650; 7 | const LOGS_CURRENT_VERSION_TRANSITION_DAYS = 365; 8 | export class AccessLogsBucket extends Bucket { 9 | constructor(scope: Construct, id: string, props: BucketProps) { 10 | const updatedProps = { 11 | // Restrict public log bucket access always 12 | blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, 13 | 14 | bucketName: props.bucketName, 15 | 16 | // Default bucket encryption on the target bucket can only be used if AES256 (SSE-S3) is selected. 17 | // SSE-KMS encryption is not supported. 18 | // https://docs.aws.amazon.com/AmazonS3/latest/dev/ServerLogs.html 19 | encryption: s3.BucketEncryption.S3_MANAGED, 20 | 21 | // We don't need to create a new s3 bucket for access logging, since the logs will go directly to this bucket. 22 | enableAccessLogging: false, 23 | 24 | // Access-logging for access logs bucket 25 | // Creating a folder "logs" within the logs bucket to avoid circular dependency. 26 | serverAccessLogsPrefix: 'logs/', 27 | 28 | // Keep access logs for 10 years but move to Glacier after 1 year for lower cost. 29 | lifecycleRules: [ 30 | { 31 | id: 'CurrentVersionPolicyForLogs', 32 | enabled: true, 33 | expiration: cdk.Duration.days(LOGS_CURRENT_VERSION_EXPIRATION_DAYS), 34 | transitions: [ 35 | { 36 | storageClass: s3.StorageClass.GLACIER, 37 | transitionAfter: cdk.Duration.days(LOGS_CURRENT_VERSION_TRANSITION_DAYS), 38 | }, 39 | ], 40 | }, 41 | ], 42 | 43 | // Set require secure transport to true 44 | requireSecureTransport: props.requireSecureTransport ?? true, 45 | 46 | // Versioning for bucket access logging should be set to true as this ensures that even 47 | // if the bucket is accidentally deleted bucket can be reconstructed and logs can be revisited 48 | versioned: true, 49 | 50 | // Server Access Logging buckets have LogDeliveryWrite ACL enabled by default which causes 51 | // InvalidBucketAclWithObjectOwnership error. 52 | // To mitigate this issue ObjectOwnership should be set to BucketOwnerPreferred. 53 | objectOwnership: s3.ObjectOwnership.BUCKET_OWNER_PREFERRED, 54 | }; 55 | super(scope, id, updatedProps); 56 | } 57 | } -------------------------------------------------------------------------------- /service-constructs/s3/s3-bucket.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import * as iam from 'aws-cdk-lib/aws-iam'; 3 | import * as kms from 'aws-cdk-lib/aws-kms'; 4 | import * as s3 from 'aws-cdk-lib/aws-s3'; 5 | import { Construct } from 'constructs'; 6 | import { DefaultTagHandler } from '../tags/default-tag-handler'; 7 | import {AccessLogsBucket} from "./access-bucket-log"; 8 | 9 | const MAX_ALLOWED_BUCKET_NAME_LENGTH = 63; 10 | const NON_CURRENT_VERSION_EXPIRATION_DAYS = 90; 11 | 12 | /** 13 | * Opinionated defaults for S3 Bucket. All options available to the AWS CDK's 14 | * BucketProps are available here: 15 | * https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-s3.BucketProps.html. 16 | */ 17 | export interface BucketProps extends s3.BucketProps { 18 | /** 19 | * Enforce secure access to S3 buckets. 20 | * Marking this as `true` will enable bucket access through HTTPS requests only. 21 | * 22 | * Note that if the `requireSecureTransport` is not explicitly set to false, 23 | * Bucket will treat `requireSecureTransport` as `true`. 24 | * If you have a good reason to disable this, you must explicitly set `requireSecureTransport` to `false`. 25 | */ 26 | readonly requireSecureTransport?: boolean; 27 | 28 | /** 29 | * Enable access logging via a new S3 bucket. 30 | * 31 | * Note that if the `enableAccessLogging` is not explicitly set to false, Bucket will treat `enableAccessLogging` as `true`. 32 | * If you have a good reason to disable this, you must explicitly set `enableAccessLogging` to `false`. 33 | */ 34 | readonly enableAccessLogging?: boolean; 35 | 36 | /** 37 | * Enable default lifecycle rule on the bucket to adhere to compliance(GDPR etc.) policy. 38 | * 39 | * Note that if the `enableDefaultLifecycleRules` is not explicitly set to false, Bucket will treat `enableDefaultLifecycleRules` as `true`. 40 | * If you have a good reason to disable this, you must explicitly set `enableDefaultLifecycleRules` to `false`. 41 | */ 42 | readonly enableDefaultLifecycleRules?: boolean; 43 | } 44 | 45 | /** 46 | * This `S3` construct builds on AWS CDK's S3 construct by enforcing opinionated practices 47 | * within the organization. 48 | * 49 | * Read more about this construct at /documentation/s3/S3.md 50 | */ 51 | export class Bucket extends s3.Bucket { 52 | /** 53 | * Expose the underlying access log bucket. This allows the users of Bucket to control the properties of the underlying 54 | * serverAccessLogsBucket if we end up creating one on their behalf. 55 | */ 56 | readonly serverAccessLogsBucket?: s3.IBucket; 57 | 58 | constructor(scope: Construct, id: string, props: BucketProps) { 59 | if (!props) { 60 | throw new Error('BucketProps must be provided'); 61 | } 62 | 63 | // If encryption is not explicitly specified, use s3.BucketEncryption.KMS 64 | const encryption = props.encryption ?? s3.BucketEncryption.KMS; 65 | 66 | let { encryptionKey, serverAccessLogsBucket, lifecycleRules } = props; 67 | 68 | /** 69 | * If the `encryption` field is set to `s3.BucketEncryption.KMS_MANAGED`, and an `encryptionKey` is not specified, 70 | * create a new KMS key and use that for the bucket encryption. In that case, by default, the bucket will 71 | * not have read access for anyone (including "Admin" users). You will need to call `bucket.grantRead` with the desired Principal to allow 72 | * read access to the bucket. 73 | */ 74 | if (encryption === s3.BucketEncryption.KMS && !encryptionKey) { 75 | encryptionKey = new kms.Key(scope, `${id}Key`, { 76 | description: `Created by ${id} resource`, 77 | enableKeyRotation: true, 78 | }); 79 | } 80 | 81 | // If the `enableAccessLogging` field is not explicitly set to false, treat `enableAccessLogging` as `true`. 82 | const enableAccessLogging = props.enableAccessLogging ?? true; 83 | 84 | // If the `enableAccessLogging` field is set to true, create a new S3 bucket and use that for access logging. 85 | if (enableAccessLogging && !serverAccessLogsBucket) { 86 | // If the primary bucket has a name specified, and if the bucketName + '-logs' is less than MAX_ALLOWED_BUCKET_NAME_LENGTH, 87 | // then use that as the access log bucket's name. Otherwise, let CloudFormation generate the access log bucket name. 88 | let accessLogBucketName: string | undefined; 89 | if (props.bucketName) { 90 | accessLogBucketName = `${props.bucketName}-logs`; 91 | 92 | if (accessLogBucketName.length > MAX_ALLOWED_BUCKET_NAME_LENGTH) { 93 | accessLogBucketName = undefined; 94 | } 95 | } 96 | 97 | // eslint-disable-next-line no-use-before-define 98 | serverAccessLogsBucket = new AccessLogsBucket(scope, `${id}AccessLog`, { 99 | bucketName: accessLogBucketName, 100 | requireSecureTransport: props.requireSecureTransport, 101 | }); 102 | } 103 | 104 | // If the `versioned` field is not explicitly set to false, treat `versioned` as `true`. 105 | const versioned = props.versioned ?? true; 106 | 107 | // If the `enableDefaultLifecycleRules` field is not explicitly set to false, treat `enableDefaultLifecycleRules` as `true`. 108 | const enableDefaultLifecycleRules = props.enableDefaultLifecycleRules ?? true; 109 | 110 | // Add default lifecycle rule about non-current versions. It can be added only on versioned enabled bucket. 111 | if (enableDefaultLifecycleRules && versioned && !lifecycleRules) { 112 | const defaultLifecyclePolicy = { 113 | id: 'NonCurrentVersionPolicyForCompliance', 114 | enabled: true, 115 | noncurrentVersionExpiration: cdk.Duration.days(NON_CURRENT_VERSION_EXPIRATION_DAYS), 116 | }; 117 | lifecycleRules = [defaultLifecyclePolicy]; 118 | } 119 | 120 | // If the `blockPublicAccess` field is not explicitly set, set to block all 121 | const blockPublicAccess = props.blockPublicAccess ?? s3.BlockPublicAccess.BLOCK_ALL; 122 | 123 | const updatedProps: BucketProps = { 124 | ...{ 125 | blockPublicAccess, 126 | encryption, 127 | encryptionKey, 128 | enableAccessLogging, 129 | lifecycleRules, 130 | serverAccessLogsBucket, 131 | versioned, 132 | }, 133 | ...props, 134 | }; 135 | 136 | super(scope, id, updatedProps); 137 | DefaultTagHandler.applyTags(this); 138 | 139 | this.serverAccessLogsBucket = serverAccessLogsBucket; 140 | 141 | // If requireSecureTransport is not explicitly set to false, treat `requireSecureTransport` as `true`. 142 | const requireSecureTransport = props.requireSecureTransport ?? true; 143 | 144 | // Add bucket policy requiring SecureTransport 145 | if (requireSecureTransport) { 146 | this.addToResourcePolicy( 147 | new iam.PolicyStatement({ 148 | effect: iam.Effect.DENY, 149 | actions: ['s3:*'], 150 | principals: [new iam.AnyPrincipal()], 151 | resources: [this.arnForObjects('*')], 152 | conditions: { 153 | Bool: { 'aws:SecureTransport': 'false' }, 154 | }, 155 | }), 156 | ); 157 | } 158 | 159 | // If s3.BucketEncryption.KMS is desired, 160 | // require that some AWS KMS CMK be used to encrypt the objects in the bucket 161 | // https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingKMSEncryption.html#require-sse-kms 162 | if (updatedProps.encryption === s3.BucketEncryption.KMS) { 163 | this.addToResourcePolicy(new iam.PolicyStatement({ 164 | effect: iam.Effect.DENY, 165 | actions: ['s3:PutObject'], 166 | resources: [this.arnForObjects('*')], 167 | principals: [new iam.AnyPrincipal()], 168 | conditions: { 169 | Null: { 170 | 's3:x-amz-server-side-encryption-aws-kms-key-id': true, 171 | }, 172 | }, 173 | })); 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /service-constructs/sns/topic.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AnyPrincipal, 3 | Effect, 4 | Grant, 5 | IGrantable, 6 | IPrincipal, 7 | PolicyStatement, 8 | } from 'aws-cdk-lib/aws-iam'; 9 | import { IKey, Key } from 'aws-cdk-lib/aws-kms'; 10 | import { Topic, TopicProps } from 'aws-cdk-lib/aws-sns'; 11 | import { Construct } from 'constructs'; 12 | import { DefaultTagHandler } from '../tags/default-tag-handler'; 13 | 14 | /** 15 | * All SNS Actions, this is required because 'sns:*' throws error while adding resource policy. 16 | */ 17 | export const ALL_SNS_ACTIONS = [ 18 | 'sns:GetTopicAttributes', 19 | 'sns:SetTopicAttributes', 20 | 'sns:AddPermission', 21 | 'sns:RemovePermission', 22 | 'sns:DeleteTopic', 23 | 'sns:Subscribe', 24 | 'sns:ListSubscriptionsByTopic', 25 | 'sns:Publish', 26 | 'sns:Receive', 27 | 'sns:TagResource', 28 | 'sns:UntagResource', 29 | 'sns:ListTagsForResource', 30 | ]; 31 | 32 | /** 33 | * Best practices defaults and security considerations for SNS construct. 34 | * For referring all Topic props options: https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-sns.Topic.html 35 | * 36 | */ 37 | export interface SnsTopicProps extends TopicProps { 38 | /** 39 | * Only allow secure actions on the SNS topic via HTTPS, if set to true 40 | * 41 | * @default true 42 | */ 43 | readonly requireSecureTransport?: boolean; 44 | 45 | /** 46 | * If set to true then creates a default KMS key 47 | * unless a master encryption Key is specified by the client. 48 | * This would enable SSE with customer managed KMS. 49 | * 50 | * @default true 51 | */ 52 | readonly enableEncryption?: boolean; 53 | 54 | /** 55 | * Allow the list of principals to publish message to the topic 56 | * Enable only required users to use the resource capabilities. 57 | */ 58 | readonly principalsAllowedToPublish?: IPrincipal[]; 59 | 60 | /** 61 | * Allow the list of principals to subscribe to the topic 62 | * Enable only required users to use the resource capabilities. 63 | */ 64 | readonly principalsAllowedToSubscribe?: IPrincipal[]; 65 | } 66 | 67 | 68 | /** 69 | * This `SnsTopic` construct builds on AWS CDK's Topic construct by 70 | * enforcing opinionated practice within the organization. 71 | * 72 | * Read more about this construct at /documentation/sns/SnsTopic.md 73 | */ 74 | export class SnsTopic extends Topic { 75 | /** 76 | * Expose the master key for the topic. Allows the clients to control the properties of the 77 | * masterKey, if we end up creating one on their behalf. 78 | */ 79 | readonly masterKey?: IKey; 80 | 81 | constructor(scope: Construct, id: string, props: SnsTopicProps) { 82 | 83 | //If topicProps are null or not sent then fail early. 84 | if (!props) { 85 | throw new Error('SnsTopicProps must be provided.'); 86 | } 87 | 88 | let { masterKey, enableEncryption, requireSecureTransport } = props; 89 | 90 | //If encryption mechanism is not defined, then default to true 91 | if (props.enableEncryption === undefined) { 92 | enableEncryption = true; 93 | } 94 | 95 | //If encryption is required but no master Key is specified then create a default KMS key. 96 | if (enableEncryption && !masterKey) { 97 | masterKey = SnsTopic.createEncryptionMasterKey(scope, id); 98 | } 99 | 100 | super(scope, id, { 101 | masterKey, 102 | ...props, 103 | }); 104 | DefaultTagHandler.applyTags(this); 105 | 106 | this.masterKey = masterKey; 107 | 108 | 109 | //Allow the principal to publish messages and encrypt permissions for the master key. 110 | props.principalsAllowedToPublish?.forEach((principal : IPrincipal) => { 111 | this.masterKey?.grantEncrypt(principal); 112 | // kms:Decrypt necessary to execute grantPublish to an SSE enabled SQS queue 113 | // Refer : https://docs.amazonaws.cn/en_us/sns/latest/dg/sns-key-management.html#send-to-encrypted-topic:~:text=Allow%20a%20user%20to%20send%20messages%20to%20a%20topic%20with%20SSE 114 | this.masterKey?.grantDecrypt(principal); 115 | this.grantPublish(principal); 116 | }); 117 | 118 | 119 | //Allow the principal to subscribe for messages and decrypt permissions for the master key. 120 | props.principalsAllowedToSubscribe?.forEach((principal : IPrincipal) => { 121 | this.masterKey?.grantDecrypt(principal); 122 | this.grantSubscribe(principal); 123 | }); 124 | 125 | 126 | //If require secure transport is not present, then default to true 127 | if (requireSecureTransport === undefined) { 128 | requireSecureTransport = true; 129 | } 130 | 131 | /** 132 | * If HTTPS is required then Deny HTTP requests for all actions 133 | * to the SNS Topic using the condition 'aws:SecureTransport: false'. 134 | * By default, we would assume this to be true. 135 | * Hence, if not explicitly set to false, create the DENY HTTP policy. 136 | */ 137 | if (requireSecureTransport) { 138 | const secureTopicPolicyStatement = new PolicyStatement({ 139 | actions: ALL_SNS_ACTIONS, 140 | effect: Effect.DENY, 141 | principals: [new AnyPrincipal()], 142 | resources: [this.topicArn], 143 | }); 144 | 145 | secureTopicPolicyStatement.sid = 'Enforce - HTTPS'; 146 | secureTopicPolicyStatement.addCondition('Bool', { 'aws:SecureTransport': 'false' }); 147 | this.addToResourcePolicy(secureTopicPolicyStatement); 148 | } 149 | } 150 | 151 | /** 152 | * This method creates a new Key which would be used for encryption of topic. 153 | * 154 | * For more information: https://docs.aws.amazon.com/sns/latest/dg/sns-key-management.html 155 | * @param scope 156 | * @param id 157 | */ 158 | private static createEncryptionMasterKey(scope: Construct, id: string) { 159 | return new Key(scope, `${id}Key`, { 160 | description: `Created by ${id} resource`, 161 | enableKeyRotation: true 162 | }); 163 | } 164 | 165 | /** 166 | * This method grants subscribe access to the principal specified. 167 | * 168 | * @param grantee 169 | */ 170 | private grantSubscribe(grantee: IGrantable): Grant { 171 | return Grant.addToPrincipalAndResource({ 172 | resource: this, 173 | resourceArns: [this.topicArn], 174 | actions: ['sns:Subscribe'], 175 | grantee, 176 | }); 177 | } 178 | } -------------------------------------------------------------------------------- /service-constructs/sqs/queue.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DeadLetterQueue, Queue, QueueEncryption, QueueProps 3 | } from 'aws-cdk-lib/aws-sqs'; 4 | import { Key } from 'aws-cdk-lib/aws-kms'; 5 | import { Duration } from 'aws-cdk-lib'; 6 | import { Construct } from 'constructs'; 7 | import { 8 | AnyPrincipal, Effect, IPrincipal, PolicyStatement, 9 | } from 'aws-cdk-lib/aws-iam'; 10 | import { DefaultTagHandler } from '../tags/default-tag-handler'; 11 | 12 | /** 13 | * SQS defaults for SQS Queue. 14 | */ 15 | const SQS_DEFAULTS = { 16 | MAX_ALLOWED_QUEUE_NAME_LENGTH: 80, 17 | DEFAULT_RECEIVE_COUNT: 100, 18 | MAX_RETENTION_PERIOD: Duration.days(14), 19 | }; 20 | 21 | /** 22 | * Opinionated defaults and security considerations for SQS construct. 23 | * For referring all queue props options : https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-sqs.QueueProps.html 24 | */ 25 | export interface SQSQueueProps extends QueueProps { 26 | /** 27 | * Only allow access to SQS queue via HTTPS if set to true. 28 | * 29 | * @default true 30 | */ 31 | readonly requireSecureTransport?: boolean; 32 | /** 33 | * Allow the list of principal to send in the queue 34 | * Enable only required users to use the resource capabilities. 35 | */ 36 | readonly principalsAllowedToSend?: IPrincipal[]; 37 | 38 | /** 39 | * Allow the list of principal to receive message from the queue 40 | * Enable only required users to use the resource capabilities. 41 | */ 42 | readonly principalsAllowedToConsume?: IPrincipal[]; 43 | 44 | /** 45 | * Enable a dead letter queue for future re-drive. DLQ hold messages that failed to be processed. 46 | * @default true 47 | */ 48 | readonly enableDeadLetterQueue?: boolean; 49 | 50 | /** 51 | * When enabling a dead letter queue, specify the number of times a message 52 | * can be unsuccessfully dequeued before being moved to the dead-letter queue. 53 | * @default 100 54 | */ 55 | readonly dlqMaxReceiveCount?: number; 56 | } 57 | 58 | /** 59 | * This `SQSQueue` construct builds on AWS CDK's Queue construct by enforcing opinionated practices 60 | * within the organization. 61 | * 62 | * Read more about this construct at /documentation/sqs/SQSQueue.md 63 | */ 64 | export class SQSQueue extends Queue { 65 | /** 66 | * Expose the dead letter queue for the primary queue. 67 | * Allows the clients to control the properties of the 68 | * deadLetterQueue, if we end up creating one on their behalf. 69 | */ 70 | readonly deadLetterQueue?: DeadLetterQueue; 71 | 72 | constructor(scope: Construct, id: string, props: SQSQueueProps) { 73 | /** 74 | * if the queueProps are null or not sent, then fail. 75 | */ 76 | if (!props) { 77 | throw new Error('SQSQueueProps must be provided.'); 78 | } 79 | 80 | let { 81 | encryption, encryptionMasterKey, enableDeadLetterQueue, requireSecureTransport, deadLetterQueue, 82 | } = props; 83 | 84 | 85 | //If encryption mechanism is not present, then default to KMS encryption 86 | if (!props.encryption) { 87 | encryption = QueueEncryption.KMS; 88 | } 89 | 90 | 91 | //If encryptionMasterKey is not specified and encryption type is KMS then create a default key. 92 | if (encryption === QueueEncryption.KMS && !encryptionMasterKey) { 93 | encryptionMasterKey = SQSQueue.createEncryptionMasterKey(scope, id); 94 | } 95 | 96 | 97 | //If enable dead letter queue is not present, then default to true and make a DLQ 98 | if (enableDeadLetterQueue === undefined) { 99 | enableDeadLetterQueue = true; 100 | } 101 | 102 | /** 103 | * If the primary queue has a name specified, and if the queueName + '-dlq' is less than MAX_ALLOWED_QUEUE_NAME_LENGTH 104 | * then use that as the dead letter queue name. Otherwise, let CloudFormation generate the dead letter queue name. 105 | * 106 | * For fifo queues, the main queue's name must end with `.fifo` suffix. 107 | * Refer : https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues.html 108 | */ 109 | let deadLetterQueueName = deadLetterQueue?.queue.queueName; 110 | if (props.queueName && !deadLetterQueue) { 111 | // If a dead letter queue has been specified then use its queue name, else use existing queue's name and append `-dlq` 112 | // For fifo queues, add `-dlq` before the `.fifo` suffix 113 | deadLetterQueueName = props.fifo ? SQSQueue.getQueueNameForFifoDlq(props.queueName) : `${props.queueName}-dlq`; 114 | 115 | if (deadLetterQueueName.length > SQS_DEFAULTS.MAX_ALLOWED_QUEUE_NAME_LENGTH) { 116 | deadLetterQueueName = undefined; 117 | } 118 | } 119 | 120 | 121 | //If require secure transport is not present, then default to true and make the transport secured 122 | 123 | if (requireSecureTransport === undefined) { 124 | requireSecureTransport = true; 125 | } 126 | 127 | // If `requireDeadLetterQueue` is set to true and deadLetterQueue is not provided by the client 128 | // then create a new SQS queue and use that as a DLQ. 129 | if (enableDeadLetterQueue && !deadLetterQueue) { 130 | deadLetterQueue = { 131 | queue: new SQSQueue(scope, `${id}Dlq`, { 132 | // DLQ Name 133 | queueName: deadLetterQueueName, 134 | 135 | // Same encryption as the primary queue 136 | encryption, 137 | 138 | // Same encryption master key as the primary queue 139 | encryptionMasterKey, 140 | 141 | // Same secure transport choice as the primary queue 142 | requireSecureTransport, 143 | 144 | // Don't need DLQ for DLQ 145 | enableDeadLetterQueue: false, 146 | 147 | // MAX_RETENTION_PERIOD being 14 days = 1209600 seconds 148 | retentionPeriod: SQS_DEFAULTS.MAX_RETENTION_PERIOD, 149 | 150 | // Principals allowed to consume messages 151 | principalsAllowedToConsume: props.principalsAllowedToConsume, 152 | 153 | // Principals allowed to send messages 154 | principalsAllowedToSend: props.principalsAllowedToSend, 155 | 156 | // For a fifo queue, the DLQ should also be a Fifo. 157 | fifo: props.fifo, 158 | }), 159 | 160 | // If client has sent max receive count then consider that, else the default receive count 161 | // DEFAULT_RECEIVE_COUNT being 100 162 | maxReceiveCount: props.dlqMaxReceiveCount ?? SQS_DEFAULTS.DEFAULT_RECEIVE_COUNT, 163 | }; 164 | } 165 | 166 | super(scope, id, { 167 | ...{ 168 | encryption, 169 | encryptionMasterKey, 170 | deadLetterQueue, 171 | retentionPeriod: props.retentionPeriod ?? SQS_DEFAULTS.MAX_RETENTION_PERIOD, 172 | }, 173 | ...props, 174 | }); 175 | DefaultTagHandler.applyTags(this); 176 | 177 | this.deadLetterQueue = deadLetterQueue; 178 | 179 | 180 | //Allow the principal to consume messages and also allow the principal 181 | // decrypt permissions for the master key provided for encryption. 182 | props.principalsAllowedToConsume?.forEach((principal: IPrincipal) => { 183 | this.grantConsumeMessages(principal); 184 | }); 185 | 186 | 187 | //Allow the principal to send Messages and also allow the principal 188 | // encrypt permissions for the master key provided for encryption. 189 | props.principalsAllowedToSend?.forEach((principal: IPrincipal) => { 190 | this.grantSendMessages(principal); 191 | }); 192 | 193 | 194 | //If HTTPS is required then Deny all HTTP requests to the SQS Queue using 'aws:SecureTransport: false'. 195 | //By default we would assume this to be true. 196 | // Hence if not explicitly set to false, create the DENY HTTP policy. 197 | if (requireSecureTransport) { 198 | const secureQueuePolicyStatement = new PolicyStatement({ 199 | actions: ['sqs:*'], 200 | effect: Effect.DENY, 201 | principals: [new AnyPrincipal()], 202 | }); 203 | 204 | secureQueuePolicyStatement.sid = 'Enforce - HTTPS'; 205 | secureQueuePolicyStatement.addCondition('Bool', { 'aws:SecureTransport': 'false' }); 206 | this.addToResourcePolicy(secureQueuePolicyStatement); 207 | } 208 | } 209 | 210 | /** 211 | * This method creates a new Key which would be used for encryption of queue. 212 | * 213 | * For more information: https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-key-management.html 214 | * @param scope 215 | * @param id 216 | */ 217 | private static createEncryptionMasterKey(scope: Construct, id: string) { 218 | return new Key(scope, `${id}Key`, { 219 | description: `Created by ${id} resource`, 220 | enableKeyRotation: true, 221 | }); 222 | } 223 | 224 | /** 225 | * This method returns the dlq name based on the fifo main queue name 226 | * 227 | * @param queueName 228 | */ 229 | private static getQueueNameForFifoDlq(queueName: string) { 230 | const fifoSuffix = '.fifo'; 231 | if (!queueName.endsWith(fifoSuffix)) { 232 | throw new Error(`Fifo queue must have name ending with .fifo suffix. Present queue name : ${queueName}`); 233 | } 234 | // Find the last index of suffix occurrence 235 | const fifoSuffixIndex = queueName.lastIndexOf(fifoSuffix); 236 | // Final string is [0, fifoSuffixIndex] + '-dlq' + [fifoSuffixIndex+1 till end] 237 | return `${queueName.substr(0, fifoSuffixIndex)}-dlq${queueName.substring(fifoSuffixIndex)}`; 238 | } 239 | } -------------------------------------------------------------------------------- /service-constructs/tags/default-tag-handler.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from 'constructs'; 2 | import { Tags } from 'aws-cdk-lib'; 3 | import { LIB_VERSION } from '../version'; 4 | 5 | const DefaultTags = { CDKConstructs: LIB_VERSION }; 6 | 7 | /** 8 | * The DefaultTagHandler utility allows default tags to be attached to Constructs. 9 | */ 10 | export class DefaultTagHandler { 11 | /** 12 | * Applies a default tag for version of the construct repo used 13 | * @param construct 14 | */ 15 | static applyTags(construct: Construct): void { 16 | for (const [key, value] of Object.entries(DefaultTags)) { 17 | Tags.of(construct).add(key, value); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /service-constructs/version.ts: -------------------------------------------------------------------------------- 1 | // This file is generated automatically during npm pre-build. Any modifications will be overwritten. 2 | 3 | export const LIB_VERSION = '$(npm --silent run get-version)'; 4 | --------------------------------------------------------------------------------