├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── node.js.yml ├── .gitignore ├── .npmignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin └── vpc-builder.ts ├── cdk.json ├── config ├── README.md ├── config-walkthrough.yaml ├── sample-central-egress-inspected.vpcBuilder.yaml ├── sample-central-egress.md ├── sample-central-egress.vpcBuilder.yaml ├── sample-central-ingress-inspected.vpcBuilder.yaml ├── sample-central-ingress.md ├── sample-central-ingress.vpcBuilder.yaml ├── sample-complex-endpoints-us-east-1.txt ├── sample-complex.md ├── sample-complex.vpcBuilder.yaml ├── sample-firewall-blog.vpcBuilder.yaml ├── sample-vpc-endpoints-us-east-1.txt ├── sample-vpc-endpoints.md ├── sample-vpc-endpoints.vpcBuilder.yaml ├── sample-vpn-onprem.md └── sample-vpn-onprem.vpcBuilder.yaml ├── discovery ├── endpoints-ap-northeast-1.json ├── endpoints-ap-northeast-2.json ├── endpoints-ap-northeast-3.json ├── endpoints-ap-south-1.json ├── endpoints-ap-southeast-1.json ├── endpoints-ap-southeast-2.json ├── endpoints-ca-central-1.json ├── endpoints-eu-central-1.json ├── endpoints-eu-north-1.json ├── endpoints-eu-west-1.json ├── endpoints-eu-west-2.json ├── endpoints-eu-west-3.json ├── endpoints-sa-east-1.json ├── endpoints-us-east-1.json ├── endpoints-us-east-2.json ├── endpoints-us-west-1.json └── endpoints-us-west-2.json ├── extras └── vpn-gateway-strongswan.yml ├── images ├── index-1024x523.png ├── sample-central-egress.png ├── sample-central-ingress.png ├── sample-complex.png ├── sample-vpc-endpoints.png ├── sample-vpn-onprem.jpg └── sample-vpn-onprem.png ├── jest.config.js ├── lambda ├── findVpnTransitGatewayAttachId │ └── index.ts ├── parseAwsFirewallEndpoints │ └── index.ts └── transitGatewayRemoveStaticRoute │ └── index.ts ├── lib ├── abstract-builderdxgw.ts ├── abstract-buildertgwpeer.ts ├── abstract-buildervpc.ts ├── abstract-buildervpn.ts ├── abstract-transitgateway.ts ├── cdk-export-presistence-stack.ts ├── config │ ├── config-schema.json │ ├── config-types.ts │ └── parser.ts ├── direct-connect-gateway-stack.ts ├── dns-route53-private-hosted-zones-stack.ts ├── stack-builder.ts ├── stack-mapper.ts ├── transit-gateway-peer-stack.ts ├── transit-gateway-routes-stack.ts ├── transit-gateway-stack.ts ├── types.ts ├── vpc-aws-network-firewall-stack.ts ├── vpc-interface-endpoints-stack.ts ├── vpc-nat-egress-stack.ts ├── vpc-route53-resolver-endpoints-stack.ts ├── vpc-workload-isolated-stack.ts ├── vpc-workload-public-stack.ts └── vpn-to-transit-gateway-stack.ts ├── package-lock.json ├── package.json ├── test ├── config-parser.test.ts ├── direct-connect-gateway-stack.test.ts ├── dns-route53-private-hosted-zones-stack.test.ts ├── stack-builder-helper.ts ├── transit-gateway-peer-stack.test.ts ├── transit-gateway-routes-stack-subnet-routes.test.ts ├── transit-gateway-routes-stack-tgw-routes.test.ts ├── transit-gateway-stack.test.ts ├── vpc-aws-network-firewall-stack.test.ts ├── vpc-interface-endpoints-stack.test.ts ├── vpc-nat-egress-stack.test.ts ├── vpc-route53-resolver-endpoints-stack.test.ts ├── vpc-workload-isolated-stack.test.ts ├── vpc-workload-public-stack.test.ts └── vpn-to-transit-gateway-stack.test.ts ├── tools └── discoverEndpoints │ └── index.ts └── tsconfig.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Config file contents that reproduces the bug. 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Additional context** 20 | Add any other context about the problem here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | 22 | **Could you submit a PR to implement this feature?** 23 | (Yes/No) 24 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [18.x, 20.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npx prettier --check **/**/*.ts 30 | - run: npm ci 31 | - run: npm run build --if-present 32 | # Run our test cases 33 | - run: npm test 34 | # Synthesize each of our sample configuration files to assure they continue to build templates 35 | - run: npm install -g cdk 36 | - run: cdk ls -c config=sample-central-egress.vpcBuilder.yaml 37 | - run: cdk ls -c config=sample-central-egress-inspected.vpcBuilder.yaml 38 | - run: cdk ls -c config=sample-central-ingress.vpcBuilder.yaml 39 | - run: cdk ls -c config=sample-central-ingress-inspected.vpcBuilder.yaml 40 | - run: cdk ls -c config=sample-complex.vpcBuilder.yaml 41 | - run: cdk ls -c config=sample-firewall-blog.vpcBuilder.yaml 42 | - run: cdk ls -c config=sample-vpc-endpoints.vpcBuilder.yaml 43 | - run: cdk ls -c config=sample-vpn-onprem.vpcBuilder.yaml 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /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 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /bin/vpc-builder.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import "source-map-support/register"; 3 | import { StackBuilderClass } from "../lib/stack-builder"; 4 | import { Stack } from "aws-cdk-lib" 5 | 6 | (async () => { 7 | try { 8 | const stackBuilder = new StackBuilderClass({}); 9 | const cdkApp = stackBuilder.stackMapper.app; 10 | 11 | const configFile = cdkApp.node.tryGetContext("config"); 12 | // If a configuration file is provided we will use it to build our stacks 13 | if (configFile) { 14 | stackBuilder.configure(configFile); 15 | await stackBuilder.build(); 16 | } else { 17 | // When no configuration context provided, we will warn but not fail. This allows 'cdk bootstrap', 'cdk help' 18 | // to continue to work as expected. 19 | new Stack(cdkApp, 'dummyStack', {}) 20 | console.warn( 21 | "\nNo configuration provided. Use a configuration file from the 'config' directory using the '-c config=[filename]' argument\n" 22 | ); 23 | } 24 | } catch (e) { 25 | console.error(e); 26 | process.exit(1); 27 | } 28 | })(); 29 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/vpc-builder.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 21 | "@aws-cdk/core:stackRelativeExports": true, 22 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 23 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 24 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, 25 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 26 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 27 | "@aws-cdk/aws-iam:minimizePolicies": true, 28 | "@aws-cdk/core:target-partitions": [ 29 | "aws", 30 | "aws-cn" 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /config/README.md: -------------------------------------------------------------------------------- 1 | # Sample Configuration Files 2 | 3 | - A complete configuration file walkthrough [here](config-walkthrough.yaml) 4 | 5 | - Centralized internet egress for an isolated VPC [here](sample-central-egress.md) 6 | 7 | - Centralized internet ingress for an isolated VPC [here](sample-central-ingress.md) 8 | 9 | - Centralized VPC Endpoints for an isolated VPC [here](sample-vpc-endpoints.md) 10 | 11 | - VPN Attached to the Transit Gateway and routing to an Isolated VPC [here](sample-vpn-onprem.md) 12 | 13 | - A complex network architecture including all the components above (plus a few more) [here](sample-complex.md) -------------------------------------------------------------------------------- /config/sample-central-egress-inspected.vpcBuilder.yaml: -------------------------------------------------------------------------------- 1 | global: 2 | stackNamePrefix: sample-central-egress 3 | ssmPrefix: /sample-central-egress/network 4 | region: us-east-1 5 | availabilityZones: 6 | - us-east-1a 7 | - us-east-1b 8 | tags: 9 | - aws-vpc-builder: sample-central-egress 10 | 11 | providers: 12 | internet: 13 | central-egress: 14 | vpcCidr: 10.1.63.192/26 15 | style: natEgress 16 | useTransit: central 17 | firewall: 18 | egress-inspection: 19 | vpcCidr: 100.64.0.0/16 20 | useTransit: central 21 | style: awsNetworkFirewall 22 | firewallDescription: sample-central-egress-tgw AWS Network Firewall 23 | firewallName: InspectionEgress 24 | 25 | vpcs: 26 | isolatedVpcOne: 27 | style: workloadIsolated 28 | vpcCidr: 10.10.0.0/19 29 | providerInternet: central-egress 30 | subnets: 31 | workload: 32 | cidrMask: 20 33 | isolatedVpcTwo: 34 | style: workloadIsolated 35 | vpcCidr: 10.11.0.0/19 36 | providerInternet: central-egress 37 | subnets: 38 | workload: 39 | cidrMask: 20 40 | 41 | transitGateways: 42 | central: 43 | style: transitGateway 44 | tgwDescription: sample-central-egress-tgw Transit Gateway 45 | defaultRoutes: 46 | - vpcName: isolatedVpcOne 47 | routesTo: central-egress 48 | inspectedBy: egress-inspection 49 | - vpcName: isolatedVpcTwo 50 | routesTo: central-egress 51 | inspectedBy: egress-inspection 52 | blackholeRoutes: 53 | - vpcName: isolatedVpcOne 54 | blackholeCidrs: 55 | - isolatedVpcTwo 56 | - vpcName: isolatedVpcTwo 57 | blackholeCidrs: 58 | - isolatedVpcOne -------------------------------------------------------------------------------- /config/sample-central-egress.md: -------------------------------------------------------------------------------- 1 | # Functional Description 2 | 3 | We will create a VPC that is responsible for egress access to the internet via NAT gateways. 4 | 5 | Our Transit Gateway will default route traffic to the egress VPC. This gives us a central location to govern our egress internet connections and apply rules / inspection uniformly. It can also save cost by reducing the number of NAT Gateways used by our AWS Estate. 6 | 7 | # Architecture Diagram 8 | 9 | ![](../images/sample-central-egress.png) 10 | 11 | # Resources 12 | 13 | Assure you have available capacity in your account in us-east-1 (Virginia) before starting! 14 | 15 | - 3 VPCs (4 if you add inspection below) 16 | - 2 Elastic IPs 17 | - 2 NAT Gateways 18 | - 1 Transit Gateway 19 | - 1 Internet Gateway 20 | 21 | # Deployment 22 | 23 | Assure you've followed the 'Environment Setup' section in the repositories main README [here](../README.md) 24 | 25 | Review the contents of the configuration we will deploy by viewing the [configuration file](sample-central-egress.vpcBuilder.yaml) for this sample. 26 | 27 | Execute the deployment by running: 28 | 29 | ```text 30 | export AWS_DEFAULT_REGION=us-east-1 31 | cdk bootstrap -c config=sample-central-egress.vpcBuilder.yaml 32 | cdk deploy -c config=sample-central-egress.vpcBuilder.yaml --all --require-approval=never 33 | ``` 34 | 35 | # Exploring this example 36 | 37 | - Start an EC2 instance in Private Isolated VPC One. 38 | - Use session manager to connect to the instance. Setup details for session manager can be found [here](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-getting-started.html) if needed. 39 | - Test internet connectivity to the instance using curl as an example `curl www.amazon.com` 40 | - Start an EC2 instance in the other Private Isolated VPC Two. 41 | - Verify it has internet connectivity in the same fashion. 42 | - Attempt to ping or connect (assure your security group allows the traffic) the instance in One to Two. Will this work? 43 | - What would you change in the configuration file to allow traffic to flow between the isolated VPCs? Make that change and run the cdk deploy command shown above and test! 44 | 45 | # Adding Inspection 46 | 47 | An advantage of centralizing egress access to the internet is you can centralize inspection of the outbound traffic and enforce rules across your estate. 48 | 49 | Review the contents of the configuration file with AWS Network Firewall added [here](sample-central-egress-inspected.vpcBuilder.yaml) 50 | 51 | Re-run the deploy command to add the Network Firewall and update all the route tables to send traffic first to the firewall VPC for inspection. 52 | 53 | ``` 54 | cdk deploy -c config=sample-central-egress-inspected.vpcBuilder.yaml --all --require-approval=never 55 | ``` 56 | 57 | # Exploring inspection 58 | 59 | - Review the transit gateway route tables. You'll notice now traffic goes to the Firewall VPC First before getting to the Central Egress. 60 | - Load the AWS Network Firewall service. You'll see an existing Firewall Policy that was created by our deployment. 61 | - Within the policy, create a new stateful [rule group](https://docs.aws.amazon.com/network-firewall/latest/developerguide/rule-groups.html). Try adding domain inspection and denying all domains except amazon.com, check [here](https://docs.aws.amazon.com/network-firewall/latest/developerguide/rule-group-managing.html) for details. Take special note of the section around 'inspection from outside the deployment VPC' which is what we're doing. 62 | 63 | # Teardown 64 | 65 | Terminate / delete any resources you created by hand. (ec2 instances, security groups, etc). 66 | 67 | If you explored all the way to inspection run: 68 | 69 | ``` 70 | cdk destroy -c config=sample-central-egress-inspected.vpcBuilder.yaml --all --require-approval=never 71 | ``` 72 | 73 | If you didn't provision inspection examples run: 74 | 75 | ``` 76 | cdk destroy -c config=sample-central-egress.vpcBuilder.yaml --all --require-approval=never 77 | ``` 78 | 79 | ### Troubleshooting Teardown 80 | 81 | Sometimes a stack will fail to delete because a resource is in use. This can happen when a VPC is set to be deleted, but resources that the stack didn't create are still present. 82 | 83 | The simplest path forward is to delete the VPC using the AWS Console and answering yes to remain any remaining resources. Then re-runing the destroy command above! 84 | -------------------------------------------------------------------------------- /config/sample-central-egress.vpcBuilder.yaml: -------------------------------------------------------------------------------- 1 | global: 2 | stackNamePrefix: sample-central-egress 3 | ssmPrefix: /sample-central-egress/network 4 | region: us-east-1 5 | availabilityZones: 6 | - us-east-1a 7 | - us-east-1b 8 | tags: 9 | - aws-vpc-builder: sample-central-egress 10 | 11 | providers: 12 | internet: 13 | central-egress: 14 | vpcCidr: 10.1.63.192/26 15 | style: natEgress 16 | useTransit: central 17 | 18 | vpcs: 19 | isolatedVpcOne: 20 | style: workloadIsolated 21 | vpcCidr: 10.10.0.0/19 22 | providerInternet: central-egress 23 | subnets: 24 | workload: 25 | cidrMask: 20 26 | isolatedVpcTwo: 27 | style: workloadIsolated 28 | vpcCidr: 10.11.0.0/19 29 | providerInternet: central-egress 30 | subnets: 31 | workload: 32 | cidrMask: 20 33 | 34 | transitGateways: 35 | central: 36 | style: transitGateway 37 | tgwDescription: "sample-central-egress-tgw" 38 | defaultRoutes: 39 | - vpcName: isolatedVpcOne 40 | routesTo: central-egress 41 | - vpcName: isolatedVpcTwo 42 | routesTo: central-egress 43 | blackholeRoutes: 44 | - vpcName: isolatedVpcOne 45 | blackholeCidrs: 46 | - 10.11.0.0/19 47 | - vpcName: isolatedVpcTwo 48 | blackholeCidrs: 49 | - 10.10.0.0/19 -------------------------------------------------------------------------------- /config/sample-central-ingress-inspected.vpcBuilder.yaml: -------------------------------------------------------------------------------- 1 | global: 2 | stackNamePrefix: sample-central-ingress 3 | ssmPrefix: /sample-central-ingress/network 4 | region: us-east-1 5 | availabilityZones: 6 | - us-east-1a 7 | - us-east-1b 8 | tags: 9 | - aws-vpc-builder: sample-central-ingress 10 | 11 | providers: 12 | firewall: 13 | ingress-inspection: 14 | vpcCidr: 100.64.0.0/16 15 | useTransit: central 16 | style: awsNetworkFirewall 17 | firewallDescription: sample-central-ingress-tgw AWS Network Firewall 18 | firewallName: InspectionIngress 19 | 20 | vpcs: 21 | centralPublic: 22 | style: workloadPublic 23 | vpcCidr: 10.10.0.0/19 24 | subnets: 25 | workload: 26 | cidrMask: 20 27 | workloadIsolated: 28 | style: workloadIsolated 29 | vpcCidr: 10.11.0.0/19 30 | subnets: 31 | workload: 32 | cidrMask: 20 33 | 34 | transitGateways: 35 | central: 36 | style: transitGateway 37 | tgwDescription: "sample-central-ingress-tgw" 38 | dynamicRoutes: 39 | - vpcName: centralPublic 40 | routesTo: workloadIsolated 41 | inspectedBy: ingress-inspection 42 | -------------------------------------------------------------------------------- /config/sample-central-ingress.md: -------------------------------------------------------------------------------- 1 | # Functional Description 2 | 3 | We will create an Amazon VPC that is responsible for ingress internet. Using facilities like RAM [sharing subnets](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-sharing.html#vpc-sharing-share-subnet) this VPC type can be used within an estate to centralize the internet ingress function. 4 | 5 | This centralization aids governance and response to internet based threats and allows for inspection of ingress traffic prior to being sent to downstream workload endpoints. 6 | 7 | # Architecture Diagram 8 | 9 | ![](../images/sample-central-ingress.png) 10 | 11 | # Resources 12 | 13 | Assure you have available capacity in your account in us-east-1 (Virginia) before starting! 14 | 15 | - 2 VPCs (3 if you add inspection below) 16 | - 1 Transit Gateway 17 | - 1 Internet Gateway 18 | 19 | # Deployment 20 | 21 | Assure you've followed the 'Environment Setup' section in the repositories main README [here](../README.md) 22 | 23 | Review the contents of the configuration we will deploy by viewing the [configuration file](sample-central-ingress.vpcBuilder.yaml) for this sample. 24 | 25 | Execute the deployment by running: 26 | 27 | ```text 28 | export AWS_DEFAULT_REGION=us-east-1 29 | cdk bootstrap -c config=sample-central-ingress.vpcBuilder.yaml 30 | cdk deploy -c config=sample-central-ingress.vpcBuilder.yaml --all --require-approval=never 31 | ``` 32 | 33 | # Exploring this example 34 | 35 | - Launch an ec2 instance running a web server in the private vpc, then provision a public facing load balancer in the public VPC. 36 | 37 | # Adding Inspection 38 | 39 | Improve our governance and security posture by sending all traffic from the public VPC to the prviate VPC through a firewall. 40 | 41 | Review the contents of the configuration file with AWS Network Firewall added [here](sample-central-ingress-inspected.vpcBuilder.yaml) 42 | 43 | Re-run the deploy command to add the Network Firewall and update all the route tables to send traffic first to the firewall VPC for inspection. 44 | 45 | ``` 46 | cdk deploy -c config=sample-central-ingress-inspected.vpcBuilder.yaml --all --require-approval=never 47 | ``` 48 | 49 | # Exploring inspection 50 | 51 | - Load the AWS Network Firewall service. You'll see an existing Firewall Policy that was created by our deployment. 52 | - Within the policy, create a new stateful (or stateless if preferred) [rule group](https://docs.aws.amazon.com/network-firewall/latest/developerguide/rule-groups.html). Add a rule that only permits one TCP Port (for example TCP 443) between public and private VPCs. Then test your rules are working! 53 | - If you're configured to do so, RAM share the public and workload isolated VPC subnets to another account. Add resources to that account and test. The firewall essentially is transparent to that account! 54 | 55 | # Teardown 56 | 57 | Terminate / delete any resources you created by hand. (ec2 instances, security groups, etc). 58 | 59 | If you explored all the way to inspection run: 60 | 61 | ``` 62 | cdk destroy -c config=sample-central-ingress-inspected.vpcBuilder.yaml --all --require-approval=never 63 | ``` 64 | 65 | If you didn't provision inspection examples run: 66 | 67 | ``` 68 | cdk destroy -c config=sample-central-ingress.vpcBuilder.yaml --all --require-approval=never 69 | ``` 70 | 71 | ### Troubleshooting Teardown 72 | 73 | Sometimes a stack will fail to delete because a resource is in use. This can happen when a VPC is set to be deleted, but resources that the stack didn't create are still present. 74 | 75 | The simplest path forward is to delete the VPC using the AWS Console and answering yes to remain any remaining resources. Then re-running the destroy command above! 76 | -------------------------------------------------------------------------------- /config/sample-central-ingress.vpcBuilder.yaml: -------------------------------------------------------------------------------- 1 | global: 2 | stackNamePrefix: sample-central-ingress 3 | ssmPrefix: /sample-central-ingress/network 4 | region: us-east-1 5 | availabilityZones: 6 | - us-east-1a 7 | - us-east-1b 8 | tags: 9 | - aws-vpc-builder: sample-central-ingress 10 | 11 | vpcs: 12 | centralPublic: 13 | style: workloadPublic 14 | vpcCidr: 10.10.0.0/19 15 | subnets: 16 | workload: 17 | cidrMask: 20 18 | workloadIsolated: 19 | style: workloadIsolated 20 | vpcCidr: 10.11.0.0/19 21 | subnets: 22 | workload: 23 | cidrMask: 20 24 | 25 | transitGateways: 26 | central: 27 | style: transitGateway 28 | tgwDescription: "sample-central-ingress-tgw" 29 | dynamicRoutes: 30 | - vpcName: centralPublic 31 | routesTo: workloadIsolated -------------------------------------------------------------------------------- /config/sample-complex-endpoints-us-east-1.txt: -------------------------------------------------------------------------------- 1 | com.amazonaws.us-east-1.ec2 2 | com.amazonaws.us-east-1.ec2messages 3 | com.amazonaws.us-east-1.ssm 4 | com.amazonaws.us-east-1.ssmmessages 5 | com.amazonaws.us-east-1.kms -------------------------------------------------------------------------------- /config/sample-complex.md: -------------------------------------------------------------------------------- 1 | # Functional Description 2 | 3 | Our complex example is going to bring everything together (and more). 4 | 5 | We will: 6 | - Provide a VPC for Dev Workloads isolated from Prod 7 | - Provide a VPC for Prod Workloads isolated from Dev 8 | - Centralize ingress and egress internet access. 9 | - Pass all ingress and egress internet traffic through an AWS Network Firewall. 10 | - Centralize VPC Endpoints. 11 | - Provide a private hosted zone for both dev.workload.net and prod.workload.net 12 | - Provide Route53 Endpoints for 'ground to cloud' and 'cloud to ground' name resolution. 13 | - Provision On-premises VPN connectivity 14 | - (optionally) RAM Share our networks to our ou-'s 15 | 16 | # Architecture Diagram 17 | 18 | ![](../images/sample-complex.png) 19 | 20 | # Resources 21 | 22 | Assure you have available reosurces in us-east-1 (Virginia). You will need your VPC limit raised if you've not already done so. The default account limit is 5. 23 | 24 | - 6 VPCs 25 | - 1 AWS Network Firewall 26 | - 1 VPN 27 | - 1 IGW 28 | - 2 NAT Gateways 29 | - 2 Route53 resolver endpoints (inbound and outbound) 30 | - 8 private hosted zones 31 | 32 | # Deployment 33 | 34 | Assure you've followed the 'Environment Setup' section in the repositories main README [here](../README.md) 35 | 36 | Review the contents of the configuration we will deploy by viewing the [configuration file](sample-complex.vpcBuilder.yaml) for this sample. 37 | 38 | If you plan to actually establish the VPN for exploration, provide a *REAL* Customer Gateway IP address. This can be an Elastic IP Address from an existing VPC that you will provision an Ec2 Instance in to establish the VPN connectivity. 39 | 40 | Execute the deployment by running: 41 | 42 | ```text 43 | export AWS_DEFAULT_REGION=us-east-1 44 | cdk bootstrap -c config=sample-complex.vpcBuilder.yaml 45 | cdk deploy -c config=sample-complex.vpcBuilder.yaml --all --require-approval=never 46 | ``` 47 | 48 | # Exploration 49 | 50 | Perhaps of interest is this configuration example synthesizes to 14 CloudFormation Templates and ~11,000 lines of JSON! 51 | 52 | - Provision EC2 instances in the Dev and Prod VPCs and attempt to communicate with each other. What would we change if we wanted dev to prod communication? 53 | - Try and resolve `workload-dev` addresses from Prod and vice versa. If we wanted this to work, what would we change? 54 | - Set up an ALB in the public subnet and direct traffic to either of the Dev or Prod EC2 instances. What would we add if we wanted separate ingress for dev and prod? 55 | - Configure a firewall rule that blocks the TCP port/traffic between the ALB and the EC2 instance. Notice if you used RAM sharing, and sign in to the Dev or Prod account - the account isn't even aware its traffic is inspected! 56 | - Establish a VPN connection from another VPC. Provision an EC2 instance within the 'on-prem' VPC and attempt to resolve DNS entries from 'cloud' using the dig command ie: `dig @172.16.1.2 workload-dev`. Substitute `172.16.1.2` with the inbound resolver IP that the stack provisioned. 57 | 58 | # Teardown 59 | 60 | Terminate / delete any resources you created by hand. (ec2 instances, security groups, etc) then run: 61 | 62 | ``` 63 | cdk destroy -c config=sample-complex.vpcBuilder.yaml --all --require-approval=never 64 | ``` 65 | -------------------------------------------------------------------------------- /config/sample-complex.vpcBuilder.yaml: -------------------------------------------------------------------------------- 1 | global: 2 | # ** Replace with overall organizational ID and Account ID owning the organization ** 3 | # Uncomment below if you'd like to use RAM Sharing 4 | # organizationId: o-REPLACEME 5 | # organizationMainAccountId: 123456789012 6 | stackNamePrefix: sample-complex 7 | ssmPrefix: /sample-complex/network 8 | region: us-east-1 9 | availabilityZones: 10 | - us-east-1a 11 | - us-east-1b 12 | tags: 13 | - aws-vpc-builder: sample-complex 14 | 15 | vpns: 16 | on-premises: 17 | style: transitGatewayAttached 18 | ## Substitute for a REAL public IP Address of a VPN device if you plan to establish a VPN connection. Also adjust the ASN number. 19 | newCustomerGatewayIp: 1.2.3.4 20 | newCustomerGatewayAsn: 64501 21 | newCustomerGatewayName: sample-vpn-onprem CGW 22 | useTransit: central 23 | 24 | providers: 25 | firewall: 26 | internet-inspection: 27 | vpcCidr: 100.64.0.0/16 28 | useTransit: central 29 | style: awsNetworkFirewall 30 | firewallDescription: sample-complex AWS Network Firewall 31 | firewallName: InspectionInternet 32 | internet: 33 | central-egress: 34 | vpcCidr: 10.10.63.192/26 35 | style: natEgress 36 | useTransit: central 37 | endpoints: 38 | vpc-endpoints: 39 | style: serviceInterfaceEndpoint 40 | vpcCidr: 10.20.0.0/19 41 | endpointMask: 24 42 | endpointConfigFile: sample-complex-endpoints 43 | useTransit: central 44 | dns-resolvers: 45 | vpcCidr: 10.10.63.128/26 46 | # ** Narrow to specific inbound resolver source IPs if desired ** 47 | resolveRequestsFromCidrs: 48 | - 10.0.0.0/8 49 | - 172.16.0.0/12 50 | - 192.168.0.0/16 51 | forwardRequests: 52 | # ** Narrow to specific on-premesis domains ** 53 | forDomains: 54 | - onprem.net 55 | # ** Replace with the IPs of on-premesis DNS servers ** 56 | toIps: 57 | - 172.31.1.10 58 | - 172.31.2.10 59 | # ** All VPCs are permitted to resolve on prem ** 60 | forVpcs: 61 | - workload-dev 62 | - workload-prod 63 | style: route53ResolverEndpoint 64 | useTransit: central 65 | 66 | dns: 67 | base: 68 | domains: 69 | - cloud.net 70 | # ** Everyone can resolve our base domain ** 71 | shareWithVpcs: 72 | - workload-dev 73 | - workload-prod 74 | prod: 75 | domains: 76 | - prod.cloud.net 77 | # ** Prod domain can be resolved from prod ** 78 | shareWithVpcs: 79 | - workload-prod 80 | dev: 81 | domains: 82 | - dev.cloud.net 83 | # ** Dev domain can be resolved from prod ** 84 | shareWithVpcs: 85 | - workload-dev 86 | 87 | vpcs: 88 | workload-dev: 89 | style: workloadIsolated 90 | vpcCidr: 10.10.0.0/19 91 | providerInternet: central-egress 92 | providerEndpoints: vpc-endpoints 93 | subnets: 94 | workload: 95 | cidrMask: 20 96 | # ** Update with the Dev OU if you wish to RAM Share ** 97 | #sharedWith: 98 | # - ou-REPLACEME 99 | workload-prod: 100 | style: workloadIsolated 101 | vpcCidr: 10.10.32.0/20 102 | providerInternet: central-egress 103 | providerEndpoints: vpc-endpoints 104 | subnets: 105 | workload: 106 | cidrMask: 21 107 | # ** Update with the Prod OU if you wish to RAM Share ** 108 | #sharedWith: 109 | # - ou-REPLACEME 110 | central-ingress: 111 | style: workloadPublic 112 | vpcCidr: 10.10.64.0/19 113 | subnets: 114 | workload: 115 | cidrMask: 20 116 | # ** Update with the Prod and Dev OU if you wish to RAM Share ** 117 | #sharedWith: 118 | # - ou-REPLACEMEDev 119 | # - ou-REPLACEMEProd 120 | 121 | transitGateways: 122 | central: 123 | style: transitGateway 124 | tgwDescription: sample-complex TGW 125 | defaultRoutes: 126 | # Dev and Prod default to internet egress inspected by a firewall. 127 | - vpcName: workload-dev 128 | routesTo: central-egress 129 | inspectedBy: internet-inspection 130 | - vpcName: workload-prod 131 | routesTo: central-egress 132 | inspectedBy: internet-inspection 133 | dynamicRoutes: 134 | # Dev and Prod can both go on-prem via VPN. BGP will advertise routes for us to use 135 | - vpcName: workload-dev 136 | routesTo: on-premises 137 | - vpcName: workload-prod 138 | routesTo: on-premises 139 | blackholeRoutes: 140 | # Dev may not communicate with prod and vice versa 141 | - vpcName: workload-dev 142 | blackholeCidrs: 143 | - 10.10.32.0/20 144 | - vpcName: workload-prod 145 | blackholeCidrs: 146 | - 10.10.0.0/19 -------------------------------------------------------------------------------- /config/sample-firewall-blog.vpcBuilder.yaml: -------------------------------------------------------------------------------- 1 | global: 2 | stackNamePrefix: sample-firewall-blog 3 | ssmPrefix: /sample-firewall-blog/network 4 | region: us-east-1 5 | availabilityZones: 6 | - us-east-1a 7 | tags: 8 | - aws-vpc-builder: sample-firewall-blog 9 | 10 | vpns: 11 | vpnToGround: 12 | style: transitGatewayAttached 13 | newCustomerGatewayIp: 5.6.7.8 14 | newCustomerGatewayAsn: 65012 15 | newCustomerGatewayName: toGround-DataCenterA 16 | useTransit: central 17 | 18 | providers: 19 | internet: 20 | centralEgress: 21 | vpcCidr: 10.10.0.0/16 22 | useTransit: central 23 | style: natEgress 24 | firewall: 25 | inspectionVpc: 26 | vpcCidr: 100.64.0.0/16 27 | useTransit: central 28 | style: awsNetworkFirewall 29 | firewallDescription: For Inspection Vpc 30 | firewallName: InspectionEgress 31 | 32 | vpcs: 33 | SpokeVpcA: 34 | style: workloadIsolated 35 | vpcCidr: 10.1.0.0/16 36 | providerInternet: centralEgress 37 | subnets: 38 | workloadSubnet: 39 | cidrMask: 24 40 | SpokeVpc: 41 | style: workloadIsolated 42 | vpcCidr: 10.3.0.0/16 43 | providerInternet: centralEgress 44 | subnets: 45 | workloadSubnet: 46 | cidrMask: 24 47 | 48 | transitGateways: 49 | central: 50 | style: transitGateway 51 | tgwDescription: Central Transit Gateway 52 | dynamicRoutes: 53 | - vpcName: SpokeVpcA 54 | routesTo: SpokeVpc 55 | inspectedBy: inspectionVpc 56 | defaultRoutes: 57 | - vpcName: SpokeVpcA 58 | routesTo: centralEgress 59 | inspectedBy: inspectionVpc 60 | - vpcName: SpokeVpc 61 | routesTo: centralEgress 62 | inspectedBy: inspectionVpc 63 | - vpcName: inspectionVpc 64 | routesTo: centralEgress 65 | staticRoutes: 66 | - vpcName: SpokeVpcA 67 | routesTo: vpnToGround 68 | staticCidr: 192.168.168.0/24 69 | - vpcName: SpokeVpc 70 | routesTo: vpnToGround 71 | staticCidr: 192.168.168.0/24 -------------------------------------------------------------------------------- /config/sample-vpc-endpoints-us-east-1.txt: -------------------------------------------------------------------------------- 1 | com.amazonaws.us-east-1.ec2 2 | com.amazonaws.us-east-1.ec2messages 3 | com.amazonaws.us-east-1.ssm 4 | com.amazonaws.us-east-1.ssmmessages 5 | com.amazonaws.us-east-1.kms -------------------------------------------------------------------------------- /config/sample-vpc-endpoints.md: -------------------------------------------------------------------------------- 1 | # Functional Description 2 | 3 | We will create a VPC that is responsible for providing VPC Endpoints. We can route traffic to these endpoints via our Transit Gateway, and provide name resolution via Route53 Private Hosted zones. 4 | 5 | This allows us to use Private Isolated workloads, and effectively not use internet Egress, but still be able to reach selected AWS services endpoints required by our workload. 6 | 7 | # Architecture Diagram 8 | 9 | ![](../images/sample-vpc-endpoints.png) 10 | 11 | # Resources 12 | 13 | Assure you have available capacity in your account in us-east-1 (Virginia) before starting! 14 | 15 | - 2 VPCs 16 | - 5 VPC Endpoints 17 | - 5 Private Hosted Zones 18 | - 1 Transit Gateway 19 | 20 | # Deployment 21 | 22 | Assure you've followed the 'Environment Setup' section in the repositories main README [here](../README.md) 23 | 24 | Review the contents of the configuration we will deploy by viewing the [configuration file](sample-vpc-endpoints.vpcBuilder.yaml) for this sample. 25 | 26 | The VPC Endpoints that should be created are kept in a separate configuration file. Review [this file](sample-vpc-endpoints-us-east-1.txt) as well. 27 | 28 | Execute the deployment by running: 29 | 30 | ```text 31 | export AWS_DEFAULT_REGION=us-east-1 32 | cdk bootstrap -c config=sample-vpc-endpoints.vpcBuilder.yaml 33 | cdk deploy -c config=sample-vpc-endpoints.vpcBuilder.yaml --all --require-approval=never 34 | ``` 35 | 36 | # Exploring this example 37 | 38 | - Start an EC2 instance in the workloadIsolated VPC. 39 | - Use session manager to connect to the instance. Setup details for session manager can be found [here](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-getting-started.html) if needed. 40 | - Even though this VPC has no internet connectivity it can reach the SSM services required for session manager through endpoints. 41 | - Confirm you cannot get to the internet via curl `curl http://www.amazon.com` 42 | - Confirm you resolve `ssm.us-east-1.amazonaws.com` to a private endpoint instead of a public one. 43 | - Edit the endpoint configuration file `sample-vpc-endpoints-us-east-1.txt` and add/remove endpoints. Test their function and impact. ie: Remove `com.amazonaws.us-east-1.ssm` and run the deployment. Notice you can no longer connect to the instance via SSM. 44 | 45 | # Teardown 46 | 47 | Terminate / delete any resources you created by hand. (ec2 instances, security groups, etc) then run: 48 | 49 | ``` 50 | cdk destroy -c config=sample-vpc-endpoints.vpcBuilder.yaml --all --require-approval=never 51 | ``` 52 | 53 | ### Troubleshooting Teardown 54 | 55 | Sometimes a stack will fail to delete because a resource is in use. This can happen when a VPC is set to be deleted, but resources that the stack didn't create are still present. 56 | 57 | The simplest path forward is to delete the VPC using the AWS Console and answering yes to remain any remaining resources. Then re-running the destroy command above! 58 | -------------------------------------------------------------------------------- /config/sample-vpc-endpoints.vpcBuilder.yaml: -------------------------------------------------------------------------------- 1 | global: 2 | stackNamePrefix: sample-vpc-endpoints 3 | ssmPrefix: /sample-vpc-endpoints/network 4 | region: us-east-1 5 | availabilityZones: 6 | - us-east-1a 7 | - us-east-1b 8 | tags: 9 | - aws-vpc-builder: sample-vpc-endpoints 10 | 11 | providers: 12 | endpoints: 13 | vpcEndpoints: 14 | style: serviceInterfaceEndpoint 15 | vpcCidr: 10.2.0.0/19 16 | endpointMask: 24 17 | endpointConfigFile: sample-vpc-endpoints 18 | useTransit: central 19 | 20 | vpcs: 21 | workloadIsolated: 22 | style: workloadIsolated 23 | vpcCidr: 10.11.0.0/19 24 | providerEndpoints: vpcEndpoints 25 | subnets: 26 | workload: 27 | cidrMask: 20 28 | 29 | transitGateways: 30 | central: 31 | style: transitGateway 32 | tgwDescription: "sample-vpc-endpoints-tgw" -------------------------------------------------------------------------------- /config/sample-vpn-onprem.md: -------------------------------------------------------------------------------- 1 | # Functional Description 2 | 3 | We will create an isolated Amazon VPC within the AWS Cloud and a VPN connection that can be used to get back to on-premises. This Isolated VPC will be connected to the TGW with a default route. 4 | 5 | So we can explore our example we will also create a 'standalone' Public VPC that we can provision an EC2 instance inside-of to establish a VPN Tunnel. 6 | 7 | We will set a default route to back through the VPN to provide all network functions (ingress / egress etc). 8 | 9 | # Architecture Diagram 10 | 11 | ![](../images/sample-vpn-onprem.jpg) 12 | 13 | # Resources 14 | 15 | Assure you have available capacity in your account in us-east-1 (Virginia) before starting! 16 | 17 | - 2 Amazon VPCs 18 | - 1 Transit Gateway 19 | - 1 AWS Managed VPN 20 | 21 | # Deployment 22 | 23 | Assure you've followed the 'Environment Setup' section in the repositories main README [here](../README.md) 24 | 25 | Review the contents of the configuration we will deploy by viewing the [configuration file](sample-vpn-onprem.vpcBuilder.yaml) for this sample. 26 | 27 | If you plan to actually establish the VPN for exploration, provide a *REAL* Customer Gateway IP address. This can be an Elastic IP Address from an existing VPC that you will provision an Ec2 Instance in to establish the VPN connectivity. 28 | 29 | Execute the deployment by running: 30 | 31 | ```text 32 | export AWS_DEFAULT_REGION=us-east-1 33 | cdk bootstrap -c config=sample-vpn-onprem.vpcBuilder.yaml 34 | cdk deploy -c config=sample-vpn-onprem.vpcBuilder.yaml --all --require-approval=never 35 | ``` 36 | 37 | # Exploring this example 38 | 39 | Follow these instructions to connect to this VPN connection the testing VPC to actually test that things are working! 40 | 41 | ## Testing the VPN with an Ec2 instance 42 | 43 | **NOTE** If you didn't allocate an ElasticIP Address in `us-east-1` you will need to tear down and start over. Assure the EIP is/was set to the 'newCustomerGatewayIp' in the configuration file example. 44 | 45 | ### PSK into Secrets manager 46 | 47 | Navigate to the 'Site-to-site VPN Connections' and Download the VPN configuration that was deployed. Use the 'generic' settings versus anything for a specific vendor. 48 | 49 | Modify the values below to reflect the region and AZ. Replace '[value-from-config-file]' with the PSK value. 50 | 51 | ``` 52 | aws secretsmanager create-secret \ 53 | --name "/vpn/connect/Tunnel1" \ 54 | --description "Pre shared key for VPN Tunnel One" \ 55 | --secret-string '{"psk":"place-value-here"}' \ 56 | --region us-east-1 57 | ``` 58 | 59 | ``` 60 | aws secretsmanager create-secret \ 61 | --name "/vpn/connect/Tunnel2" \ 62 | --description "Pre shared key for VPN Tunnel Two" \ 63 | --secret-string '{"psk":"place-value-here"}' \ 64 | --region us-east-1 65 | ``` 66 | 67 | The template for the Instance portion of the VPN is taken from this blog post: [here](https://aws.amazon.com/blogs/networking-and-content-delivery/simulating-site-to-site-vpn-customer-gateways-strongswan/) 68 | 69 | You can find the template you'll need in this repository in `extras/vpn-gateway-strongswan.yml` 70 | 71 | In the instructions in the blog linked above, skip steps 1 to 4 and go to `5. Deploy strongSwan VPN gateway stack to your on-premises VPC` 72 | 73 | When deploying - the blog shows you the values you need from your VPN configuration file that go into the template. Much of it is left blank since it allows for certificate based VPN versus a PSK. 74 | 75 | Note you'll need your eipalloc- EIP ID for deployment so note it down before you deploy. 76 | 77 | Make sure you deploy to the 'onpremsimulator' VPC and not the 'workloadisolated' VPC. 78 | 79 | It will take a little while to deploy. 80 | 81 | Once Finished modify the route tables of your VPC to send traffic matching your VPC CIDR on the Amazon managed side to the interface ID of your ec2 instance. 82 | 83 | **VERY IMPORTANT** Your ec2 instance is an availability zone and has an ENI in that AZ. When you deploy a 'test' resource it must be in the same AZ in order to work! 84 | 85 | So if your VPN ec2 instance is 'us-east-1b' you must deploy your 'test workload' in us-east-1b' as well after you've modified the subnet route tables to route to the ENI of the EC2 VPN instance. 86 | 87 | ### Verify the tunnels show up 88 | 89 | Give 5 minutes or so for the tunnels to stabilize after install is done. 90 | 91 | They BOTH will come up. If only one comes up delete the template and try again. A copy/paste error likely occurred. 92 | 93 | ### Verify your transit routes 94 | 95 | The transit routes likely showed blackholed when the tunnels were down. Now they should show as Active routes. 96 | 97 | Deploy a resource on either side and test connectivity! Make sure your security groups are set up the right way to allow traffic from each-other's CIDR. 98 | 99 | Enjoy! 100 | 101 | # Teardown 102 | 103 | Terminate / delete any resources you created / Delete the EC2 based VPN endpoint you deployed. (ec2 instances, security groups, etc) then run: 104 | 105 | ``` 106 | cdk destroy -c config=sample-vpn-onprem.vpcBuilder.yaml --all --require-approval=never 107 | ``` 108 | 109 | ### Troubleshooting Teardown 110 | 111 | For this stack it will sometimes fail to remove the Transit Gateway since the VPN has not completely deleted itself. Simply re-run the delete command above and it should succeed. 112 | 113 | Sometimes a stack will fail to delete because a resource is in use. This can happen when a VPC is set to be deleted, but resources that the stack didn't create are still present. 114 | 115 | The simplest path forward is to delete the VPC using the AWS Console and answering yes to remain any remaining resources. Then re-running the destroy command above! 116 | -------------------------------------------------------------------------------- /config/sample-vpn-onprem.vpcBuilder.yaml: -------------------------------------------------------------------------------- 1 | global: 2 | stackNamePrefix: sample-vpn-onprem 3 | ssmPrefix: /sample-vpn-onprem/network 4 | region: us-east-1 5 | availabilityZones: 6 | - us-east-1a 7 | - us-east-1b 8 | tags: 9 | - aws-vpc-builder: sample-vpn-onprem 10 | 11 | vpns: 12 | on-premises: 13 | style: transitGatewayAttached 14 | ## Note - Substitute the ACTUAL Elastic IP that you've allocated before deploying. 15 | newCustomerGatewayIp: 1.2.3.4 16 | newCustomerGatewayAsn: 64501 17 | newCustomerGatewayName: sample-vpn-onprem CGW 18 | useTransit: central 19 | 20 | vpcs: 21 | workloadIsolated: 22 | style: workloadIsolated 23 | vpcCidr: 10.11.0.0/19 24 | subnets: 25 | workload: 26 | cidrMask: 20 27 | onPremSimulator: 28 | style: workloadPublic 29 | vpcCidr: 10.12.0.0/19 30 | # Make this explicit since we want this to stand-alone for our exploration 31 | attachTgw: false 32 | subnets: 33 | public: 34 | cidrMask: 20 35 | 36 | transitGateways: 37 | central: 38 | style: transitGateway 39 | tgwDescription: "sample-vpc-endpoints-tgw" 40 | defaultRoutes: 41 | - vpcName: workloadIsolated 42 | routesTo: on-premises -------------------------------------------------------------------------------- /images/index-1024x523.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-vpc-builder-cdk/1bc10ff35f127d63349e74a48ea4b86851250dce/images/index-1024x523.png -------------------------------------------------------------------------------- /images/sample-central-egress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-vpc-builder-cdk/1bc10ff35f127d63349e74a48ea4b86851250dce/images/sample-central-egress.png -------------------------------------------------------------------------------- /images/sample-central-ingress.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-vpc-builder-cdk/1bc10ff35f127d63349e74a48ea4b86851250dce/images/sample-central-ingress.png -------------------------------------------------------------------------------- /images/sample-complex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-vpc-builder-cdk/1bc10ff35f127d63349e74a48ea4b86851250dce/images/sample-complex.png -------------------------------------------------------------------------------- /images/sample-vpc-endpoints.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-vpc-builder-cdk/1bc10ff35f127d63349e74a48ea4b86851250dce/images/sample-vpc-endpoints.png -------------------------------------------------------------------------------- /images/sample-vpn-onprem.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-vpc-builder-cdk/1bc10ff35f127d63349e74a48ea4b86851250dce/images/sample-vpn-onprem.jpg -------------------------------------------------------------------------------- /images/sample-vpn-onprem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-vpc-builder-cdk/1bc10ff35f127d63349e74a48ea4b86851250dce/images/sample-vpn-onprem.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: ['/test'], 4 | testMatch: ['**/*.test.ts'], 5 | transform: { 6 | '^.+\\.tsx?$': 'ts-jest' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /lambda/findVpnTransitGatewayAttachId/index.ts: -------------------------------------------------------------------------------- 1 | import { CdkCustomResourceEvent, CdkCustomResourceResponse } from "aws-lambda"; 2 | import { ICustomResourceTGWFindVpnAttach } from "../../lib/types"; 3 | import { 4 | EC2Client, 5 | DescribeTransitGatewayAttachmentsCommand, 6 | } from "@aws-sdk/client-ec2"; 7 | 8 | const client = new EC2Client({ region: process.env.AWS_REGION }); 9 | 10 | const findVpnTransitGatewayAttachId = async ( 11 | requestProps: ICustomResourceTGWFindVpnAttach, 12 | ) => { 13 | const vpnResponse = await client.send( 14 | new DescribeTransitGatewayAttachmentsCommand({ 15 | Filters: [ 16 | { 17 | Name: "transit-gateway-id", 18 | Values: [requestProps.transitGatewayId], 19 | }, 20 | { 21 | Name: "resource-type", 22 | Values: ["vpn"], 23 | }, 24 | { 25 | Name: "resource-id", 26 | Values: [requestProps.vpnId], 27 | }, 28 | ], 29 | }), 30 | ); 31 | if (vpnResponse.TransitGatewayAttachments) { 32 | return vpnResponse.TransitGatewayAttachments[0] 33 | .TransitGatewayAttachmentId as string; 34 | } else { 35 | throw new Error( 36 | `Failed to retrieve any transit gateway attachments for vpn ${requestProps.vpnId} TGW ${requestProps.transitGatewayId}`, 37 | ); 38 | } 39 | }; 40 | 41 | export const onEvent = async (event: CdkCustomResourceEvent) => { 42 | console.info(event); 43 | const requestProps: ICustomResourceTGWFindVpnAttach = 44 | event.ResourceProperties as any; 45 | 46 | const responseProps: CdkCustomResourceResponse = { 47 | PhysicalResourceId: `${requestProps.transitGatewayId}:${requestProps.vpnId}`, 48 | }; 49 | 50 | if (event.RequestType == "Create" || event.RequestType == "Update") { 51 | const transitGatewayAttachId = 52 | await findVpnTransitGatewayAttachId(requestProps); 53 | console.info(`Retrieved identifier: ${transitGatewayAttachId}`); 54 | responseProps.Data = { 55 | transitGatewayAttachId: transitGatewayAttachId, 56 | }; 57 | return responseProps; 58 | } else if (event.RequestType == "Delete") { 59 | console.info("Delete. No action taken"); 60 | return responseProps; 61 | } else { 62 | console.log("Called without Create, Update, Or Delete"); 63 | return responseProps; 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /lambda/parseAwsFirewallEndpoints/index.ts: -------------------------------------------------------------------------------- 1 | import { CdkCustomResourceEvent, CdkCustomResourceResponse } from "aws-lambda"; 2 | import { ICustomResourceParseAwsFirewallEndpoints } from "../../lib/types"; 3 | 4 | export const onEvent = async (event: CdkCustomResourceEvent) => { 5 | console.info(event); 6 | const responseProps: CdkCustomResourceResponse = {}; 7 | if ( 8 | event.RequestType == "Create" || 9 | event.RequestType == "Update" || 10 | event.RequestType == "Delete" 11 | ) { 12 | const requestProps: ICustomResourceParseAwsFirewallEndpoints = 13 | event.ResourceProperties as any; 14 | requestProps.firewallEndpoints.forEach((endpoint) => { 15 | const endpointDetails = endpoint.split(":"); 16 | if (endpointDetails[0] == requestProps.availabilityZone) { 17 | responseProps.PhysicalResourceId = endpointDetails[1]; 18 | responseProps.Data = { 19 | endpointId: endpointDetails[1], 20 | }; 21 | } 22 | }); 23 | // Our CDK framework will trap this and send a failure back to the Template as desired. 24 | if (!responseProps.hasOwnProperty("PhysicalResourceId")) { 25 | throw new Error( 26 | `Unable to find ${requestProps.availabilityZone} in endpoint details ${requestProps.firewallEndpoints}`, 27 | ); 28 | } 29 | } 30 | console.info(responseProps); 31 | return responseProps; 32 | }; 33 | -------------------------------------------------------------------------------- /lambda/transitGatewayRemoveStaticRoute/index.ts: -------------------------------------------------------------------------------- 1 | import { CdkCustomResourceEvent, CdkCustomResourceResponse } from "aws-lambda"; 2 | import { ICustomResourceTGWStaticRoute } from "../../lib/types"; 3 | import { 4 | EC2Client, 5 | DeleteTransitGatewayRouteCommand, 6 | CreateTransitGatewayRouteCommand, 7 | ReplaceTransitGatewayRouteCommand, 8 | } from "@aws-sdk/client-ec2"; 9 | 10 | const client = new EC2Client({ region: process.env.AWS_REGION }); 11 | 12 | const createTransitGatewayStaticRoute = async ( 13 | requestProps: ICustomResourceTGWStaticRoute, 14 | ) => { 15 | await client.send( 16 | new CreateTransitGatewayRouteCommand({ 17 | DestinationCidrBlock: requestProps.destinationCidrBlock, 18 | TransitGatewayAttachmentId: requestProps.transitGatewayAttachmentId, 19 | TransitGatewayRouteTableId: requestProps.transitGatewayRouteTableId, 20 | }), 21 | ); 22 | }; 23 | 24 | const replaceTransitGatewayRoute = async ( 25 | requestProps: ICustomResourceTGWStaticRoute, 26 | ) => { 27 | await client.send( 28 | new ReplaceTransitGatewayRouteCommand({ 29 | DestinationCidrBlock: requestProps.destinationCidrBlock, 30 | TransitGatewayAttachmentId: requestProps.transitGatewayAttachmentId, 31 | TransitGatewayRouteTableId: requestProps.transitGatewayRouteTableId, 32 | }), 33 | ); 34 | }; 35 | 36 | const deleteTransitGatewayStaticRoute = async ( 37 | requestProps: ICustomResourceTGWStaticRoute, 38 | ) => { 39 | await client.send( 40 | new DeleteTransitGatewayRouteCommand({ 41 | DestinationCidrBlock: requestProps.destinationCidrBlock, 42 | TransitGatewayRouteTableId: requestProps.transitGatewayRouteTableId, 43 | }), 44 | ); 45 | }; 46 | 47 | export const onEvent = async (event: CdkCustomResourceEvent) => { 48 | console.info(event); 49 | const requestProps: ICustomResourceTGWStaticRoute = 50 | event.ResourceProperties as any; 51 | 52 | const responseProps: CdkCustomResourceResponse = { 53 | PhysicalResourceId: `${requestProps.transitGatewayRouteTableId}:${requestProps.destinationCidrBlock}`, 54 | }; 55 | 56 | if (event.RequestType == "Create") { 57 | await createTransitGatewayStaticRoute(requestProps); 58 | console.info("Created Route"); 59 | return { responseProps }; 60 | } else if (event.RequestType == "Update") { 61 | await replaceTransitGatewayRoute(requestProps); 62 | console.info("Updated Route"); 63 | return { responseProps }; 64 | } else if (event.RequestType == "Delete") { 65 | await deleteTransitGatewayStaticRoute(requestProps); 66 | console.info("Deleted Route"); 67 | return { responseProps }; 68 | } else { 69 | console.log("Called without Create, Update, Or Delete"); 70 | return { responseProps }; 71 | } 72 | }; 73 | -------------------------------------------------------------------------------- /lib/abstract-builderdxgw.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import { 4 | IBuilderTgwStaticRoutes, 5 | IBuilderDxGw, 6 | IBuilderDxGwProps, 7 | ITgwAttachType, 8 | ITgwPropagateRouteAttachmentName, 9 | ssmParameterImport, 10 | ITgw, 11 | ITgwRouteTable, 12 | ITgwAttachment, 13 | } from "./types"; 14 | import * as ssm from "aws-cdk-lib/aws-ssm"; 15 | 16 | export abstract class BuilderDxGw extends cdk.Stack implements IBuilderDxGw { 17 | name: string; 18 | globalPrefix: string; 19 | // Always attached to a Transit Gateway 20 | withTgw: true; 21 | // Always false since this isn't VPC Based 22 | tgwCreateTgwSubnets: false; 23 | tgwAttachType: ITgwAttachType = "dxgw" 24 | tgw: ITgw; 25 | tgwRouteTable: ITgwRouteTable; 26 | tgwRouteTableSsm: ssmParameterImport; 27 | tgwAttachment: ITgwAttachment; 28 | tgwAttachmentSsm: ssmParameterImport; 29 | tgwPropagateRouteAttachmentNames: Array = 30 | []; 31 | // Blackhole CIDRs not applicable for an imported DxGw 32 | readonly tgwBlackHoleCidrs: []; 33 | tgwStaticRoutes: Array = []; 34 | tgwDefaultRouteAttachmentName: ITgwPropagateRouteAttachmentName; 35 | props: IBuilderDxGwProps; 36 | 37 | protected constructor(scope: Construct, id: string, props: IBuilderDxGwProps) { 38 | super(scope, id, props); 39 | this.props = props; 40 | this.globalPrefix = props.globalPrefix.toLowerCase(); 41 | } 42 | 43 | // We only support imports, but this method is common to all stacks so needs to be present 44 | saveTgwRouteInformation() { 45 | } 46 | 47 | async init() {} 48 | 49 | createSsmParameters() { 50 | const prefix = 51 | `${this.props.ssmParameterPrefix}/networking/${this.globalPrefix}/dxgw/${this.name}`.toLowerCase(); 52 | 53 | this.tgwRouteTableSsm = { 54 | name: `${prefix}/tgwRouteId`, 55 | }; 56 | new ssm.StringParameter(this, `ssmDxGwTgwRouteTableSsm`, { 57 | parameterName: `${prefix}/tgwRouteId`, 58 | stringValue: this.tgwRouteTable.ref, 59 | }); 60 | 61 | this.tgwAttachmentSsm = { 62 | name: `${prefix}/tgwAttachId`, 63 | }; 64 | new ssm.StringParameter(this, `ssmDxGwTgwAttachIdSsm`, { 65 | parameterName: `${prefix}/tgwAttachId`, 66 | stringValue: this.tgwAttachment.attrId, 67 | }); 68 | } 69 | 70 | // We only support imports, but this method is common to all stacks so needs to be present 71 | attachToTGW() { 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/abstract-buildertgwpeer.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import { 4 | IBuilderTgwStaticRoutes, 5 | IBuilderTgwPeer, 6 | IBuilderTgwPeerProps, 7 | ITgwAttachType, 8 | ITgwPropagateRouteAttachmentName, 9 | ssmParameterImport, 10 | ITgw, 11 | ITgwRouteTable, 12 | ITgwAttachment, 13 | } from "./types"; 14 | import * as ssm from "aws-cdk-lib/aws-ssm"; 15 | 16 | export abstract class BuilderTgwPeer extends cdk.Stack implements IBuilderTgwPeer { 17 | name: string; 18 | globalPrefix: string; 19 | // Always attached to a Transit Gateway 20 | withTgw: true; 21 | // Always false since this isn't VPC Based 22 | tgwCreateTgwSubnets: false; 23 | tgwAttachType: ITgwAttachType = "tgwPeer" 24 | tgw: ITgw; 25 | tgwRouteTable: ITgwRouteTable; 26 | tgwRouteTableSsm: ssmParameterImport; 27 | tgwAttachment: ITgwAttachment; 28 | tgwAttachmentSsm: ssmParameterImport; 29 | tgwPropagateRouteAttachmentNames: Array = 30 | []; 31 | // Blackhole CIDRs not applicable for an imported peer connection 32 | readonly tgwBlackHoleCidrs: []; 33 | tgwStaticRoutes: Array = []; 34 | tgwDefaultRouteAttachmentName: ITgwPropagateRouteAttachmentName; 35 | props: IBuilderTgwPeerProps; 36 | 37 | protected constructor(scope: Construct, id: string, props: IBuilderTgwPeerProps) { 38 | super(scope, id, props); 39 | this.props = props; 40 | this.globalPrefix = props.globalPrefix.toLowerCase(); 41 | } 42 | 43 | // We only support imports, but this method is common to all stacks so needs to be present 44 | saveTgwRouteInformation() { 45 | } 46 | 47 | async init() {} 48 | 49 | createSsmParameters() { 50 | const prefix = 51 | `${this.props.ssmParameterPrefix}/networking/${this.globalPrefix}/tgwPeer/${this.name}`.toLowerCase(); 52 | 53 | this.tgwRouteTableSsm = { 54 | name: `${prefix}/tgwRouteId`, 55 | }; 56 | new ssm.StringParameter(this, `ssmTgwPeerTgwRouteTableSsm`, { 57 | parameterName: `${prefix}/tgwRouteId`, 58 | stringValue: this.tgwRouteTable.ref, 59 | }); 60 | 61 | this.tgwAttachmentSsm = { 62 | name: `${prefix}/tgwAttachId`, 63 | }; 64 | new ssm.StringParameter(this, `ssmTgwPeerTgwAttachIdSsm`, { 65 | parameterName: `${prefix}/tgwAttachId`, 66 | stringValue: this.tgwAttachment.attrId, 67 | }); 68 | } 69 | 70 | // We only support imports, but this method is common to all stacks so needs to be present 71 | attachToTGW() { 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lib/abstract-buildervpc.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import { 4 | IBuilderTgwStaticRoutes, 5 | IBuilderVpc, 6 | IBuilderVpcStyle, 7 | ITgwAttachType, 8 | IBuilderVpcProps, 9 | IVpcParameterModel, 10 | ITgwPropagateRouteAttachmentName, 11 | IBuildVpcProvides, 12 | ssmParameterImport, 13 | ITgw, 14 | ITgwRouteTable, 15 | ITgwAttachment, 16 | } from "./types"; 17 | import * as ec2 from "aws-cdk-lib/aws-ec2"; 18 | import * as ssm from "aws-cdk-lib/aws-ssm"; 19 | 20 | export abstract class BuilderVpc extends cdk.Stack implements IBuilderVpc { 21 | vpc: ec2.Vpc; 22 | vpcStyle: IBuilderVpcStyle; 23 | provides: IBuildVpcProvides; 24 | vpcInspects: boolean = false; 25 | publicSubnetNames: Array = []; 26 | privateSubnetNames: Array = []; 27 | privateIsolatedSubnetNames: Array = []; 28 | ssmParameterPaths: IVpcParameterModel; 29 | globalPrefix: string; 30 | name: string; 31 | withTgw: boolean = false; 32 | tgwAttachType: ITgwAttachType = "vpc"; 33 | tgw: ITgw; 34 | tgwCreateTgwSubnets: boolean = true; 35 | tgwAttachment: ITgwAttachment; 36 | tgwAttachmentSsm: ssmParameterImport; 37 | tgwRouteTable: ITgwRouteTable; 38 | tgwRouteTableSsm: ssmParameterImport; 39 | tgwPropagateRouteAttachmentNames: Array = 40 | []; 41 | tgwBlackHoleCidrs: Array = []; 42 | tgwStaticRoutes: Array = []; 43 | tgwDefaultRouteAttachmentName: ITgwPropagateRouteAttachmentName; 44 | props: IBuilderVpcProps; 45 | 46 | protected constructor(scope: Construct, id: string, props: IBuilderVpcProps) { 47 | super(scope, id, props); 48 | this.props = props; 49 | this.globalPrefix = props.globalPrefix.toLowerCase(); 50 | } 51 | 52 | saveTgwRouteInformation() { 53 | // Sometimes we will declare withTgw true and ignore props 54 | if (this.props.withTgw || this.withTgw) { 55 | this.withTgw = this.props.withTgw as boolean; 56 | if (!this.props.tgw) { 57 | throw new Error( 58 | `When property 'withTgw' is set to true, a 'tgw' must be specified as well.` 59 | ); 60 | } 61 | this.tgw = this.props.tgw; 62 | // Save off any other routing based material we got in our constructor. We will implement after TGW Attach 63 | if (this.props.tgwPropagateRouteAttachmentNames) { 64 | this.tgwPropagateRouteAttachmentNames.push( 65 | ...this.props.tgwPropagateRouteAttachmentNames 66 | ); 67 | } 68 | if (this.props.tgwBlackHoleCidrs) { 69 | this.tgwBlackHoleCidrs.push(...this.props.tgwBlackHoleCidrs); 70 | } 71 | if (this.props.tgwStaticRoutes) { 72 | this.tgwStaticRoutes.push(...this.props.tgwStaticRoutes); 73 | } 74 | // Finally if we've been provided a default attachment to send traffic to, save it 75 | if (this.props.tgwDefaultRouteAttachmentName) { 76 | this.tgwDefaultRouteAttachmentName = 77 | this.props.tgwDefaultRouteAttachmentName; 78 | } 79 | } 80 | } 81 | 82 | async init() {} 83 | 84 | attachToTGW() { 85 | if (this.props.withTgw && this.props.tgw) { 86 | // Attachment to our TGW first. If we're permitted to make a TGW specific subnet, use that. 87 | let subnetGroupName = "transit-gateway"; 88 | // Not permitted to use a custom transit gateway subnet, use isolated followed by private 89 | if (!this.tgwCreateTgwSubnets) { 90 | if (this.privateIsolatedSubnetNames) { 91 | subnetGroupName = this.privateIsolatedSubnetNames[0]; 92 | } else if (this.privateSubnetNames) { 93 | subnetGroupName = this.privateSubnetNames[0]; 94 | } else { 95 | throw new Error( 96 | `Vpc ${this.name} attaching to TGW. tgwCreateTgwSubnets is false but unable to find suitable private subnets to attach to` 97 | ); 98 | } 99 | } 100 | this.tgwAttachment = new ec2.CfnTransitGatewayVpcAttachment( 101 | this, 102 | `TGWAttachment-${this.name}`, 103 | { 104 | vpcId: this.vpc.vpcId, 105 | transitGatewayId: this.tgw.attrId, 106 | tags: [{ key: "Name", value: this.name }], 107 | subnetIds: this.vpc.selectSubnets({ 108 | subnetGroupName: subnetGroupName, 109 | }).subnetIds, 110 | } 111 | ); 112 | // One route able per attachment 113 | this.tgwRouteTable = new ec2.CfnTransitGatewayRouteTable( 114 | this, 115 | `TGWRouteTable-${this.name}`, 116 | { 117 | transitGatewayId: this.tgw.attrId, 118 | tags: [{ key: "Name", value: this.name }], 119 | } 120 | ); 121 | // Associate the route table with our attachment 122 | new ec2.CfnTransitGatewayRouteTableAssociation( 123 | this, 124 | `TGWRTAssoc-${this.name}`, 125 | { 126 | transitGatewayAttachmentId: this.tgwAttachment.attrId, 127 | transitGatewayRouteTableId: this.tgwRouteTable.ref, 128 | } 129 | ); 130 | } 131 | } 132 | /* 133 | For VPC: vpcName must be known 134 | For Subnet: subnetName and Availability Zone must be known 135 | 136 | networking/vpcs/{vpcName} 137 | vpcId: 138 | vpcCidr: 139 | az1 .... vpc/az2: 140 | tgwAttachId: 141 | tgwRouteId: 142 | subnets/{subnetName} 143 | {availabilityZone}/ 144 | subnetId: 145 | subnetCidr: 146 | routeTableId: 147 | 148 | */ 149 | createSsmParameters() { 150 | const prefix = 151 | `${this.props.ssmParameterPrefix}/networking/${this.globalPrefix}/vpcs/${this.name}`.toLowerCase(); 152 | this.ssmParameterPaths = { 153 | vpcName: this.name, 154 | vpcId: `${prefix}/vpcId`, 155 | vpcCidr: `${prefix}/vpcCidr`, 156 | availabilityZones: this.availabilityZones, 157 | subnets: [], 158 | }; 159 | new ssm.StringParameter(this, "ssmVpcId", { 160 | parameterName: this.ssmParameterPaths.vpcId, 161 | stringValue: this.vpc.vpcId, 162 | }); 163 | new ssm.StringParameter(this, "ssmVpcCidr", { 164 | parameterName: this.ssmParameterPaths.vpcCidr, 165 | stringValue: this.vpc.vpcCidrBlock, 166 | }); 167 | this.props.availabilityZones.forEach((availabilityZone, index) => { 168 | new ssm.StringParameter(this, `ssmVpcAz${index}`, { 169 | parameterName: `${prefix}/az${index}`, 170 | stringValue: availabilityZone, 171 | }); 172 | }); 173 | this.publicSubnetNames.forEach((subnetName) => { 174 | this.ssmSubnetParameterBuilder(subnetName, prefix); 175 | }); 176 | this.privateSubnetNames.forEach((subnetName) => { 177 | this.ssmSubnetParameterBuilder(subnetName, prefix); 178 | }); 179 | this.privateIsolatedSubnetNames.forEach((subnetName) => { 180 | this.ssmSubnetParameterBuilder(subnetName, prefix); 181 | }); 182 | 183 | if (this.withTgw && this.tgw) { 184 | this.ssmParameterPaths.tgwAttachId = `${prefix}/tgwAttachId`; 185 | this.ssmParameterPaths.tgwRouteId = `${prefix}/tgwRouteId`; 186 | this.ssmParameterPaths.tgwId = `${prefix}/tgwId`; 187 | new ssm.StringParameter(this, "tgwAttachId", { 188 | parameterName: this.ssmParameterPaths.tgwAttachId, 189 | stringValue: this.tgwAttachment.attrId, 190 | }); 191 | this.tgwAttachmentSsm = { 192 | name: this.ssmParameterPaths.tgwId, 193 | }; 194 | new ssm.StringParameter(this, "tgwRouteId", { 195 | parameterName: this.ssmParameterPaths.tgwRouteId, 196 | stringValue: this.tgwRouteTable.ref, 197 | }); 198 | this.tgwRouteTableSsm = { 199 | name: this.ssmParameterPaths.tgwRouteId, 200 | }; 201 | new ssm.StringParameter(this, "tgwId", { 202 | parameterName: this.ssmParameterPaths.tgwId, 203 | stringValue: this.tgw.attrId, 204 | }); 205 | } 206 | } 207 | 208 | ssmSubnetParameterBuilder(subnetName: string, prefix: string) { 209 | this.vpc 210 | .selectSubnets({ subnetGroupName: subnetName }) 211 | .subnets.forEach((subnet, index) => { 212 | const subnetPrefix = `${prefix}/subnets/${subnetName}/${subnet.availabilityZone}`; 213 | this.ssmParameterPaths.subnets.push({ 214 | availabilityZone: subnet.availabilityZone, 215 | routeTableId: `${subnetPrefix}/routeTableId`, 216 | subnetCidr: `${subnetPrefix}/subnetCidr`, 217 | subnetId: `${subnetPrefix}/subnetId`, 218 | subnetName: subnetName, 219 | }); 220 | new ssm.StringParameter( 221 | this, 222 | `ssmVpcSubnetAz${subnetName}${index}RouteTableId`, 223 | { 224 | parameterName: `${subnetPrefix}/routeTableId`, 225 | stringValue: subnet.routeTable.routeTableId, 226 | } 227 | ); 228 | new ssm.StringParameter( 229 | this, 230 | `ssmVpcSubnetAz${subnetName}${index}SubnetCidr`, 231 | { 232 | parameterName: `${subnetPrefix}/subnetCidr`, 233 | stringValue: (subnet as ec2.Subnet).ipv4CidrBlock, 234 | } 235 | ); 236 | new ssm.StringParameter( 237 | this, 238 | `ssmVpcSubnetAz${subnetName}${index}subnetId`, 239 | { 240 | parameterName: `${subnetPrefix}/subnetId`, 241 | stringValue: (subnet as ec2.Subnet).subnetId, 242 | } 243 | ); 244 | }); 245 | } 246 | 247 | get availabilityZones(): string[] { 248 | return this.props.availabilityZones; 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /lib/abstract-buildervpn.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import { 4 | IBuilderTgwStaticRoutes, 5 | IBuilderVpn, 6 | IBuilderVpnProps, 7 | ITgwAttachType, 8 | ITgwPropagateRouteAttachmentName, 9 | IBuilderVpnStyle, 10 | IBuilderVpnProvides, 11 | ssmParameterImport, 12 | ITgw, 13 | ITgwRouteTable, 14 | ITgwAttachment, 15 | IVpn, 16 | } from "./types"; 17 | import { IConfigVpnTunnelOptions } from "./config/config-types"; 18 | import * as ec2 from "aws-cdk-lib/aws-ec2"; 19 | import * as ssm from "aws-cdk-lib/aws-ssm"; 20 | 21 | export abstract class BuilderVpn extends cdk.Stack implements IBuilderVpn { 22 | name: string; 23 | globalPrefix: string; 24 | vpn: IVpn; 25 | vpnStyle: IBuilderVpnStyle; 26 | vpnProvides: IBuilderVpnProvides; 27 | tunnelOneOptions: IConfigVpnTunnelOptions; 28 | tunnelTwoOptions: IConfigVpnTunnelOptions; 29 | withTgw: boolean; 30 | tgwCreateTgwSubnets: boolean = true; 31 | tgwAttachType: ITgwAttachType; 32 | tgw: ITgw; 33 | tgwRouteTable: ITgwRouteTable; 34 | tgwRouteTableSsm: ssmParameterImport; 35 | tgwAttachment: ITgwAttachment; 36 | tgwAttachmentSsm: ssmParameterImport; 37 | tgwPropagateRouteAttachmentNames: Array = 38 | []; 39 | tgwBlackHoleCidrs: Array = []; 40 | tgwStaticRoutes: Array = []; 41 | tgwDefaultRouteAttachmentName: ITgwPropagateRouteAttachmentName; 42 | props: IBuilderVpnProps; 43 | 44 | protected constructor(scope: Construct, id: string, props: IBuilderVpnProps) { 45 | super(scope, id, props); 46 | this.props = props; 47 | this.globalPrefix = props.globalPrefix.toLowerCase(); 48 | } 49 | 50 | saveTgwRouteInformation() { 51 | // Sometimes we will declare withTgw true and ignore props 52 | if (this.props.withTgw || this.withTgw) { 53 | this.withTgw = this.props.withTgw as boolean; 54 | if (!this.props.tgw) { 55 | throw new Error( 56 | `When property 'withTgw' is set to true, a 'tgw' must be specified as well.` 57 | ); 58 | } 59 | this.tgw = this.props.tgw; 60 | // Save off any other routing based material we got in our constructor. We will implement after TGW Attach 61 | if (this.props.tgwPropagateRouteAttachmentNames) { 62 | this.tgwPropagateRouteAttachmentNames.push( 63 | ...this.props.tgwPropagateRouteAttachmentNames 64 | ); 65 | } 66 | if (this.props.tgwBlackHoleCidrs) { 67 | this.tgwBlackHoleCidrs.push(...this.props.tgwBlackHoleCidrs); 68 | } 69 | if (this.props.tgwStaticRoutes) { 70 | this.tgwStaticRoutes.push(...this.props.tgwStaticRoutes); 71 | } 72 | // Finally if we've been provided a default attachment to send traffic to, save it 73 | if (this.props.tgwDefaultRouteAttachmentName) { 74 | this.tgwDefaultRouteAttachmentName = 75 | this.props.tgwDefaultRouteAttachmentName; 76 | } 77 | } 78 | } 79 | 80 | async init() {} 81 | 82 | createSsmParameters() { 83 | const prefix = 84 | `${this.props.ssmParameterPrefix}/networking/${this.globalPrefix}/vpns/${this.name}`.toLowerCase(); 85 | 86 | this.tgwRouteTableSsm = { 87 | name: `${prefix}/tgwRouteId`, 88 | }; 89 | new ssm.StringParameter(this, `ssmVpnTgwRouteTableSsm`, { 90 | parameterName: `${prefix}/tgwRouteId`, 91 | stringValue: this.tgwRouteTable.ref, 92 | }); 93 | 94 | this.tgwAttachmentSsm = { 95 | name: `${prefix}/tgwAttachId`, 96 | }; 97 | new ssm.StringParameter(this, `ssmVpnTgwAttachIdSsm`, { 98 | parameterName: `${prefix}/tgwAttachId`, 99 | stringValue: this.tgwAttachment.attrId, 100 | }); 101 | } 102 | 103 | // We're already attached when created, but this will create our RouteTable and associate it unless we're dealing with an import 104 | attachToTGW() { 105 | if (this.props.withTgw && this.props.tgw) { 106 | // If our tgwRouteTable is already set due to import we will skip 107 | if (!this.tgwRouteTable) { 108 | this.tgwRouteTable = new ec2.CfnTransitGatewayRouteTable( 109 | this, 110 | `TGWRouteTable-${this.name}`, 111 | { 112 | transitGatewayId: this.tgw.attrId, 113 | tags: [{ key: "Name", value: this.name }], 114 | } 115 | ); 116 | new ec2.CfnTransitGatewayRouteTableAssociation( 117 | this, 118 | `TGWRTAssoc-${this.name}`, 119 | { 120 | transitGatewayAttachmentId: this.tgwAttachment.attrId, 121 | transitGatewayRouteTableId: this.tgwRouteTable.ref, 122 | } 123 | ); 124 | } 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /lib/abstract-transitgateway.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import { 4 | ITransitGatewayBase, 5 | ITransitGatewayProvides, 6 | ITransitGatewayStyle, 7 | ITransitGatewayBaseProps, 8 | } from "./types"; 9 | import * as ec2 from "aws-cdk-lib/aws-ec2"; 10 | 11 | export abstract class TransitGateway 12 | extends cdk.Stack 13 | implements ITransitGatewayBase 14 | { 15 | name: string; 16 | props: ITransitGatewayBaseProps; 17 | tgwStyle: ITransitGatewayProvides; 18 | provides: ITransitGatewayStyle; 19 | tgw: ec2.CfnTransitGateway; 20 | 21 | protected constructor( 22 | scope: Construct, 23 | id: string, 24 | props: ITransitGatewayBaseProps 25 | ) { 26 | super(scope, id, props); 27 | } 28 | 29 | async init() {} 30 | } 31 | -------------------------------------------------------------------------------- /lib/cdk-export-presistence-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import { BuilderVpc } from "./abstract-buildervpc"; 4 | import {IBuilderDxGw, IBuilderVpc, IBuilderVpn} from "./types"; 5 | 6 | export interface ICdkExportPersistenceProps extends cdk.StackProps { 7 | persistExports: Array; 8 | } 9 | 10 | export class CdkExportPersistenceStack extends cdk.Stack { 11 | constructor(scope: Construct, id: string, props: ICdkExportPersistenceProps) { 12 | super(scope, id, props); 13 | 14 | // We will just create outputs for our saved exports from underlying stacks so we can assure 15 | // they will always exist and allow the CDK to understand relationships/orders between stacks. 16 | props.persistExports.forEach((persistExports) => { 17 | if (persistExports instanceof BuilderVpc) { 18 | new cdk.CfnOutput(this, `${persistExports.name}-vpcId`, { 19 | value: persistExports.vpc.vpcId, 20 | }); 21 | } 22 | if (persistExports.withTgw) { 23 | new cdk.CfnOutput(this, `${persistExports.name}}-tgwAttachmentId`, { 24 | value: persistExports.tgwAttachment.attrId, 25 | }); 26 | new cdk.CfnOutput(this, `${persistExports.name}-tgwId`, { 27 | value: persistExports.tgw.attrId, 28 | }); 29 | } 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/config/config-types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | ****** global: 3 | */ 4 | 5 | export interface IConfigConfigTag { 6 | [key: string]: string; 7 | } 8 | 9 | export interface IConfigGlobal { 10 | stackNamePrefix: string; 11 | organizationId?: string; 12 | organizationMainAccountId?: string; 13 | tags?: Array; 14 | ssmPrefix: string; 15 | region: string; 16 | availabilityZones: Array; 17 | discoveryFolder?: string; 18 | useLegacyIdentifiers?: boolean; 19 | } 20 | 21 | /* 22 | ****** providers: 23 | */ 24 | 25 | export type IConfigProvidersEndpointsStyles = 26 | | "serviceInterfaceEndpoint" 27 | | "route53ResolverEndpoint"; 28 | export type IConfigProvidersFirewallStyles = "awsNetworkFirewall"; 29 | export type IConfigProvidersInternetStyles = "natEgress"; 30 | 31 | export interface IConfigProviderRoute53EndpointsForExistingVpcs { 32 | name: string; 33 | vpcId: string; 34 | } 35 | 36 | export interface IConfigProviderRoute53EndpointsForwardRequests { 37 | forDomains: Array; 38 | toIps: Array; 39 | forVpcs?: Array; 40 | forExistingVpcs?: Array; 41 | } 42 | 43 | export interface IConfigProviderEndpoints { 44 | vpcCidr: string; 45 | availabilityZones?: Array; 46 | style: IConfigProvidersEndpointsStyles; 47 | useTransit: string; 48 | endpointMask?: number; 49 | endpointConfigFile?: string; 50 | forwardRequests?: IConfigProviderRoute53EndpointsForwardRequests; 51 | resolveRequestsFromCidrs?: Array; 52 | } 53 | 54 | export interface IConfigProviderFirewall { 55 | vpcCidr: string; 56 | availabilityZones?: Array; 57 | firewallName: string; 58 | firewallDescription: string; 59 | style: IConfigProvidersFirewallStyles; 60 | useTransit: string; 61 | awsFirewallExistingRuleArn?: string; 62 | } 63 | 64 | export interface IConfigProviderInternet { 65 | vpcCidr: string; 66 | availabilityZones?: Array; 67 | useTransit: string; 68 | style: IConfigProvidersInternetStyles; 69 | } 70 | 71 | export interface IConfigProviderEndpointsNamed { 72 | [key: string]: IConfigProviderEndpoints; 73 | } 74 | 75 | export interface IConfigProviderFirewallNamed { 76 | [key: string]: IConfigProviderFirewall; 77 | } 78 | 79 | export interface IConfigProviderInternetNamed { 80 | [key: string]: IConfigProviderInternet; 81 | } 82 | 83 | export interface IConfigProviders { 84 | endpoints?: IConfigProviderEndpointsNamed; 85 | internet?: IConfigProviderInternetNamed; 86 | firewall?: IConfigProviderFirewallNamed; 87 | } 88 | 89 | /* 90 | ****** vpns: 91 | */ 92 | 93 | export interface IConfigVpnTunnelOptions { 94 | tunnelInsideCidr: string; 95 | } 96 | 97 | export type IConfigVpnStyles = "transitGatewayAttached"; 98 | export interface IConfigVpn { 99 | style: IConfigVpnStyles; 100 | existingCustomerGatewayId?: string; 101 | newCustomerGatewayIp?: string; 102 | newCustomerGatewayAsn?: number; 103 | newCustomerGatewayName?: string; 104 | tunnelOneOptions?: IConfigVpnTunnelOptions; 105 | tunnelTwoOptions?: IConfigVpnTunnelOptions; 106 | existingVpnConnectionId?: string; 107 | existingVpnTransitGatewayAttachId?: string; 108 | existingVpnTransitGatewayRouteTableId?: string; 109 | useTransit: string; 110 | } 111 | 112 | export interface IConfigVpns { 113 | [key: string]: IConfigVpn; 114 | } 115 | 116 | /* 117 | ****** Direct Connect Gateways: 118 | */ 119 | 120 | export interface IConfigDxGw { 121 | existingTgwId: string; 122 | existingDxGwTransitGatewayAttachId: string; 123 | existingDxGwTransitGatewayRouteTableId: string; 124 | } 125 | 126 | export interface IConfigDxGws { 127 | [key: string]: IConfigDxGw; 128 | } 129 | 130 | /* 131 | ****** Transit Gateway Peers 132 | */ 133 | 134 | export interface IConfigTgwPeer { 135 | existingTgwId: string; 136 | existingTgwPeerTransitGatewayAttachId: string; 137 | existingTgwPeerTransitGatewayRouteTableId: string; 138 | } 139 | 140 | export interface IConfigTgwPeers { 141 | [key: string]: IConfigTgwPeer; 142 | } 143 | 144 | /* 145 | ****** dns: 146 | */ 147 | 148 | export interface IConfigDnsShareWithExistingVpc { 149 | vpcId: string; 150 | vpcRegion: string; 151 | } 152 | 153 | export interface IConfigDnsEntry { 154 | domains: Array; 155 | shareWithVpcs?: Array; 156 | shareWithExistingVpcs?: Array; 157 | } 158 | 159 | export interface IConfigDns { 160 | [key: string]: IConfigDnsEntry; 161 | } 162 | 163 | /* 164 | ****** vpcs: 165 | */ 166 | 167 | export interface IConfigVpcSubnet { 168 | cidrMask: number; 169 | sharedWith?: Array; 170 | } 171 | 172 | export interface IConfigVpcNamedSubnets { 173 | [key: string]: IConfigVpcSubnet; 174 | } 175 | 176 | export type IConfigVpcStyles = "workloadIsolated" | "workloadPublic"; 177 | export interface IConfigVpc { 178 | vpcCidr: string; 179 | availabilityZones?: Array; 180 | style: IConfigVpcStyles; 181 | subnets: IConfigVpcNamedSubnets; 182 | legacyRamShare?: boolean; 183 | attachTgw?: boolean; 184 | providerEndpoints?: string; 185 | providerInternet?: string; 186 | } 187 | 188 | export interface IConfigVpcs { 189 | [key: string]: IConfigVpc; 190 | } 191 | 192 | /* 193 | ******* TransitGateways 194 | */ 195 | 196 | export interface IConfigTgwDefaultRoutes { 197 | vpcName: string; 198 | routesTo: string; 199 | inspectedBy?: string; 200 | } 201 | 202 | export interface IConfigTgwDynamicRoutes { 203 | vpcName: string; 204 | routesTo: string; 205 | inspectedBy?: string; 206 | } 207 | 208 | export interface IConfigTgwStaticRoutes { 209 | vpcName: string; 210 | staticCidr: string; 211 | routesTo: string; 212 | inspectedBy?: string; 213 | } 214 | 215 | export interface IConfigTgwBlackholeRoutes { 216 | vpcName: string; 217 | blackholeCidrs: Array; 218 | } 219 | 220 | export type IConfigTgwStyles = "transitGateway"; 221 | 222 | export interface IConfigTgwRoutes { 223 | style: IConfigTgwStyles; 224 | tgwDescription: string; 225 | useExistingTgwId?: string; 226 | amazonSideAsn?: number; 227 | defaultRoutes?: Array; 228 | dynamicRoutes?: Array; 229 | staticRoutes?: Array; 230 | blackholeRoutes?: Array; 231 | } 232 | 233 | export interface IConfigTgws { 234 | [key: string]: IConfigTgwRoutes; 235 | } 236 | 237 | /* 238 | ******* Config 239 | */ 240 | 241 | export interface IConfig { 242 | global: IConfigGlobal; 243 | providers?: IConfigProviders; 244 | vpcs: IConfigVpcs; 245 | vpns?: IConfigVpns; 246 | dxgws?: IConfigDxGws; 247 | tgwPeers?: IConfigTgwPeers; 248 | dns?: IConfigDns; 249 | transitGateways?: IConfigTgws; 250 | } 251 | -------------------------------------------------------------------------------- /lib/direct-connect-gateway-stack.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * NOTE: There is no Cloudformation support for Direct Connect at the moment. This will serve as an abstract model 3 | * so we can import the tgw attachments and create static routes and propagations 4 | * See: https://github.com/aws-cloudformation/cloudformation-coverage-roadmap/issues/876 5 | * Expand in the future to support creation of the Dx Gateway itself when support is added. 6 | */ 7 | 8 | import { Construct } from "constructs"; 9 | import { 10 | IBuilderDxGwProps, 11 | } from "./types"; 12 | import { BuilderDxGw } from "./abstract-builderdxgw"; 13 | 14 | export interface IDirectConnectGatewayProps extends IBuilderDxGwProps { 15 | existingTransitGatewayId: string; 16 | existingDxGwTransitGatewayAttachId: string 17 | existingDxGwTransitGatewayRouteTableId: string 18 | } 19 | 20 | export class DirectConnectGatewayStack extends BuilderDxGw { 21 | props: IDirectConnectGatewayProps; 22 | 23 | constructor(scope: Construct, id: string, props: IDirectConnectGatewayProps) { 24 | super(scope, id, props); 25 | 26 | this.name = `${props.namePrefix}-dxgw`.toLowerCase(); 27 | this.withTgw = true; 28 | this.tgw = { 29 | attrId: this.props.existingTransitGatewayId 30 | } 31 | this.tgwRouteTable = { 32 | ref: this.props.existingDxGwTransitGatewayRouteTableId, 33 | }; 34 | this.tgwAttachment = { 35 | attrId: this.props.existingDxGwTransitGatewayAttachId, 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/dns-route53-private-hosted-zones-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import * as r53 from "aws-cdk-lib/aws-route53"; 4 | import { IBuilderVpc } from "./types"; 5 | import { IConfigDnsShareWithExistingVpc } from "./config/config-types"; 6 | // We'll need our 'shareWithVpcs' to be IBuilderVpcs to establish a relationship and assure order 7 | // When we're deploying within the app. Our existing ones are less important and can be strings. 8 | interface IDnsEntriesProps { 9 | domains: Array; 10 | shareWithVpcs?: Array; 11 | shareWithExistingVpcs?: Array; 12 | } 13 | 14 | export interface IDnsRoute53PrivateHostedZonesProps extends cdk.StackProps { 15 | namePrefix: string; 16 | dnsEntries: IDnsEntriesProps; 17 | } 18 | 19 | interface IPrivateZoneName { 20 | domain: string; 21 | phz: r53.CfnHostedZone; 22 | } 23 | 24 | export class DnsRoute53PrivateHostedZonesClass extends cdk.Stack { 25 | name: string; 26 | props: IDnsRoute53PrivateHostedZonesProps; 27 | privateZoneNames: Array = []; 28 | 29 | constructor( 30 | scope: Construct, 31 | id: string, 32 | props: IDnsRoute53PrivateHostedZonesProps 33 | ) { 34 | super(scope, id, props); 35 | 36 | this.props = props; 37 | this.name = `${props.namePrefix}-dns-private-hosted-zones`.toLowerCase(); 38 | 39 | for (const domain of props.dnsEntries.domains) { 40 | const privateHostedZone = new r53.CfnHostedZone( 41 | this, 42 | `PrivateHostedZone-${domain}`, 43 | { 44 | hostedZoneConfig: { 45 | comment: `Private Hosted Zone for ${domain}`, 46 | }, 47 | name: domain, 48 | vpcs: this.buildVpcList(), 49 | } 50 | ); 51 | // Record our creation. 52 | const privateHostedZoneNamed: IPrivateZoneName = { 53 | domain: domain, 54 | phz: privateHostedZone, 55 | }; 56 | this.privateZoneNames.push(privateHostedZoneNamed); 57 | } 58 | } 59 | 60 | buildVpcList() { 61 | const dnsEntries = this.props.dnsEntries; 62 | const vpcs: Array = []; 63 | if (dnsEntries.shareWithVpcs) { 64 | for (const builderVpc of dnsEntries.shareWithVpcs) { 65 | vpcs.push({ 66 | vpcId: builderVpc.vpc.vpcId, 67 | vpcRegion: this.region, 68 | }); 69 | } 70 | } 71 | if (dnsEntries.shareWithExistingVpcs) { 72 | for (const importVpc of dnsEntries.shareWithExistingVpcs) { 73 | vpcs.push({ 74 | vpcId: importVpc.vpcId, 75 | vpcRegion: importVpc.vpcRegion, 76 | }); 77 | } 78 | } 79 | return vpcs; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/stack-mapper.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IBuilderVpcStyle, 3 | ITransitGatewayStyle, 4 | IBuilderVpnStyle, 5 | IVpcWorkloadProps, 6 | } from "./types"; 7 | import { IConfig } from "./config/config-types"; 8 | import { 9 | ITransitGatewayProps, 10 | TransitGatewayStack, 11 | } from "./transit-gateway-stack"; 12 | import { 13 | IVpcInterfaceEndpointsProps, 14 | VpcInterfaceEndpointsStack, 15 | } from "./vpc-interface-endpoints-stack"; 16 | import { 17 | VpcRoute53ResolverEndpointsStack, 18 | IVpcRoute53ResolverEndpointsProps, 19 | } from "./vpc-route53-resolver-endpoints-stack"; 20 | import { IVpcNatEgressProps, VpcNatEgressStack } from "./vpc-nat-egress-stack"; 21 | import { 22 | IVpcAwsNetworkFirewallProps, 23 | VpcAwsNetworkFirewallStack, 24 | } from "./vpc-aws-network-firewall-stack"; 25 | import { VpcWorkloadIsolatedStack } from "./vpc-workload-isolated-stack"; 26 | import { VpcWorkloadPublicStack } from "./vpc-workload-public-stack"; 27 | import { 28 | ITransitGatewayRoutesProps, 29 | TransitGatewayRoutesStack, 30 | } from "./transit-gateway-routes-stack"; 31 | import { 32 | ICdkExportPersistenceProps, 33 | CdkExportPersistenceStack, 34 | } from "./cdk-export-presistence-stack"; 35 | import { 36 | IVpnToTransitGatewayProps, 37 | VpnToTransitGatewayStack, 38 | } from "./vpn-to-transit-gateway-stack"; 39 | import { 40 | IDirectConnectGatewayProps, 41 | DirectConnectGatewayStack 42 | } from "./direct-connect-gateway-stack" 43 | import { 44 | IDnsRoute53PrivateHostedZonesProps, 45 | DnsRoute53PrivateHostedZonesClass, 46 | } from "./dns-route53-private-hosted-zones-stack"; 47 | import { 48 | ITransitGatewayPeerProps, 49 | TransitGatewayPeerStack 50 | } from "./transit-gateway-peer-stack"; 51 | import * as cdk from "aws-cdk-lib"; 52 | 53 | 54 | export type workloadStackProps = IVpcWorkloadProps; 55 | export type firewallStackProps = IVpcAwsNetworkFirewallProps; 56 | export type endpointStackProps = 57 | | IVpcInterfaceEndpointsProps 58 | | IVpcRoute53ResolverEndpointsProps; 59 | export type internetStackProps = IVpcNatEgressProps; 60 | export type transitGatewayStackProps = ITransitGatewayProps; 61 | export type directConnectGatewayProps = IDirectConnectGatewayProps; 62 | export type transitGatewayPeerProps = ITransitGatewayPeerProps; 63 | 64 | export interface StackMapperProps {} 65 | 66 | export class StackMapper { 67 | app: cdk.App = new cdk.App(); 68 | c: IConfig; 69 | props: StackMapperProps 70 | constructor(props: StackMapperProps) { 71 | this.props = props 72 | } 73 | 74 | configure(c: IConfig) { 75 | this.c = c; 76 | } 77 | 78 | async workloadStacks( 79 | style: IBuilderVpcStyle, 80 | stackName: string, 81 | props: workloadStackProps 82 | ) { 83 | if (style == "workloadIsolated" || style == "workloadPublic") { 84 | const cfnStackName = 85 | `${this.c.global.stackNamePrefix}-${stackName}`.toLowerCase(); 86 | let stackClass; 87 | if (style == "workloadPublic") { 88 | stackClass = new VpcWorkloadPublicStack(this.app, cfnStackName, props); 89 | } else { 90 | stackClass = new VpcWorkloadIsolatedStack( 91 | this.app, 92 | cfnStackName, 93 | props 94 | ); 95 | } 96 | await stackClass.init(); 97 | stackClass.saveTgwRouteInformation(); 98 | stackClass.attachToTGW(); 99 | stackClass.createSsmParameters(); 100 | this.tagStack(stackClass); 101 | return stackClass; 102 | } else { 103 | throw new Error(`Workload - style ${style} is not implemented or mapped`); 104 | } 105 | } 106 | 107 | async vpnStacks( 108 | style: IBuilderVpnStyle, 109 | stackName: string, 110 | props: IVpnToTransitGatewayProps 111 | ) { 112 | if (style === "transitGatewayAttached") { 113 | const cfnStackName = 114 | `${this.c.global.stackNamePrefix}-${stackName}`.toLowerCase(); 115 | const stackClass = new VpnToTransitGatewayStack( 116 | this.app, 117 | cfnStackName, 118 | props 119 | ); 120 | await stackClass.init(); 121 | stackClass.saveTgwRouteInformation(); 122 | stackClass.attachToTGW(); 123 | stackClass.createSsmParameters(); 124 | this.tagStack(stackClass); 125 | return stackClass; 126 | } else { 127 | throw new Error(`Workload - style ${style} is not implemented or mapped`); 128 | } 129 | } 130 | 131 | async dxGwStacks( 132 | stackName: string, 133 | props: directConnectGatewayProps 134 | ) { 135 | const cfnStackName = 136 | `${this.c.global.stackNamePrefix}-${stackName}`.toLowerCase(); 137 | const stackClass = new DirectConnectGatewayStack( 138 | this.app, 139 | cfnStackName, 140 | props 141 | ); 142 | await stackClass.init(); 143 | stackClass.saveTgwRouteInformation(); 144 | stackClass.attachToTGW(); 145 | stackClass.createSsmParameters(); 146 | this.tagStack(stackClass); 147 | return stackClass; 148 | } 149 | 150 | async tgwPeerStacks( 151 | stackName: string, 152 | props: transitGatewayPeerProps 153 | ) { 154 | const cfnStackName = 155 | `${this.c.global.stackNamePrefix}-${stackName}`.toLowerCase(); 156 | const stackClass = new TransitGatewayPeerStack( 157 | this.app, 158 | cfnStackName, 159 | props 160 | ); 161 | await stackClass.init(); 162 | stackClass.saveTgwRouteInformation(); 163 | stackClass.attachToTGW(); 164 | stackClass.createSsmParameters(); 165 | this.tagStack(stackClass); 166 | return stackClass; 167 | } 168 | 169 | async providerFirewallStacks( 170 | style: IBuilderVpcStyle, 171 | stackName: string, 172 | props: firewallStackProps 173 | ) { 174 | if (style == "awsNetworkFirewall") { 175 | const cfnStackName = 176 | `${this.c.global.stackNamePrefix}-${stackName}`.toLowerCase(); 177 | const stackClass = new VpcAwsNetworkFirewallStack( 178 | this.app, 179 | cfnStackName, 180 | props 181 | ); 182 | await stackClass.init(); 183 | stackClass.saveTgwRouteInformation(); 184 | stackClass.attachToTGW(); 185 | stackClass.createSsmParameters(); 186 | this.tagStack(stackClass); 187 | return stackClass; 188 | } else { 189 | throw new Error( 190 | `Provider: firewall - style ${style} is not implemented or mapped` 191 | ); 192 | } 193 | } 194 | 195 | async providerEndpointStacks( 196 | style: IBuilderVpcStyle, 197 | stackName: string, 198 | props: endpointStackProps 199 | ) { 200 | if (style == "serviceInterfaceEndpoint") { 201 | const cfnStackName = 202 | `${this.c.global.stackNamePrefix}-${stackName}`.toLowerCase(); 203 | const stackClass = new VpcInterfaceEndpointsStack( 204 | this.app, 205 | cfnStackName, 206 | props 207 | ); 208 | await stackClass.init(); 209 | stackClass.saveTgwRouteInformation(); 210 | stackClass.attachToTGW(); 211 | stackClass.createSsmParameters(); 212 | this.tagStack(stackClass); 213 | return stackClass; 214 | } else if (style == "route53ResolverEndpoint") { 215 | const cfnStackName = 216 | `${this.c.global.stackNamePrefix}-${stackName}`.toLowerCase(); 217 | const stackClass = new VpcRoute53ResolverEndpointsStack( 218 | this.app, 219 | cfnStackName, 220 | props 221 | ); 222 | await stackClass.init(); 223 | stackClass.saveTgwRouteInformation(); 224 | stackClass.attachToTGW(); 225 | stackClass.createSsmParameters(); 226 | this.tagStack(stackClass); 227 | return stackClass; 228 | } else { 229 | throw new Error( 230 | `Provider: endpoint - style ${style} is not implemented or mapped` 231 | ); 232 | } 233 | } 234 | 235 | async providerInternetStacks( 236 | style: IBuilderVpcStyle, 237 | stackName: string, 238 | props: internetStackProps 239 | ) { 240 | if (style == "natEgress") { 241 | const cfnStackName = 242 | `${this.c.global.stackNamePrefix}-${stackName}`.toLowerCase(); 243 | const stackClass = new VpcNatEgressStack(this.app, cfnStackName, props); 244 | await stackClass.init(); 245 | stackClass.saveTgwRouteInformation(); 246 | stackClass.attachToTGW(); 247 | stackClass.createSsmParameters(); 248 | this.tagStack(stackClass); 249 | return stackClass; 250 | } else { 251 | throw new Error( 252 | `Provider: internet - style ${style} is not implemented or mapped` 253 | ); 254 | } 255 | } 256 | 257 | async transitGatewayStacks( 258 | style: ITransitGatewayStyle, 259 | stackName: string, 260 | props: transitGatewayStackProps 261 | ) { 262 | if (style == "transitGateway") { 263 | const cfnStackName = 264 | `${this.c.global.stackNamePrefix}-${stackName}`.toLowerCase(); 265 | const stackClass = new TransitGatewayStack(this.app, cfnStackName, props); 266 | await stackClass.init(); 267 | this.tagStack(stackClass); 268 | return stackClass; 269 | } else { 270 | throw new Error( 271 | `TransitGateway - style ${style} is not implemented or mapped` 272 | ); 273 | } 274 | } 275 | 276 | dnsPrivateHostedZoneStack( 277 | stackName: string, 278 | props: IDnsRoute53PrivateHostedZonesProps 279 | ) { 280 | const cfnStackName = 281 | `${this.c.global.stackNamePrefix}-${stackName}`.toLowerCase(); 282 | const stackClass = new DnsRoute53PrivateHostedZonesClass( 283 | this.app, 284 | cfnStackName, 285 | props 286 | ); 287 | this.tagStack(stackClass); 288 | return stackClass; 289 | } 290 | 291 | transitGatewayRoutesStack( 292 | stackName: string, 293 | props: ITransitGatewayRoutesProps 294 | ) { 295 | const cfnStackName = 296 | `${this.c.global.stackNamePrefix}-${stackName}`.toLowerCase(); 297 | const stackClass = new TransitGatewayRoutesStack( 298 | this.app, 299 | cfnStackName, 300 | props 301 | ); 302 | this.tagStack(stackClass); 303 | return stackClass; 304 | } 305 | 306 | cdkExportPersistStack(stackName: string, props: ICdkExportPersistenceProps) { 307 | const cfnStackName = 308 | `${this.c.global.stackNamePrefix}-${stackName}`.toLowerCase(); 309 | const stackClass = new CdkExportPersistenceStack( 310 | this.app, 311 | cfnStackName, 312 | props 313 | ); 314 | this.tagStack(stackClass); 315 | return stackClass; 316 | } 317 | 318 | tagStack(stack: cdk.Stack) { 319 | this.c.global.tags?.forEach((tag) => { 320 | const key = Object.keys(tag)[0]; 321 | cdk.Tags.of(stack).add(key, tag[key]); 322 | }); 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /lib/transit-gateway-peer-stack.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from "constructs"; 2 | import { 3 | IBuilderTgwPeerProps, 4 | } from "./types"; 5 | import {BuilderTgwPeer} from "./abstract-buildertgwpeer"; 6 | 7 | export interface ITransitGatewayPeerProps extends IBuilderTgwPeerProps { 8 | existingTransitGatewayId: string; 9 | existingPeerTransitGatewayAttachId: string 10 | existingPeerTransitGatewayRouteTableId: string 11 | } 12 | 13 | export class TransitGatewayPeerStack extends BuilderTgwPeer { 14 | props: ITransitGatewayPeerProps; 15 | 16 | constructor(scope: Construct, id: string, props: ITransitGatewayPeerProps) { 17 | super(scope, id, props); 18 | 19 | this.name = `${props.namePrefix}-tgwPeer`.toLowerCase(); 20 | this.withTgw = true; 21 | this.tgw = { 22 | attrId: this.props.existingTransitGatewayId 23 | } 24 | this.tgwRouteTable = { 25 | ref: this.props.existingPeerTransitGatewayRouteTableId, 26 | }; 27 | this.tgwAttachment = { 28 | attrId: this.props.existingPeerTransitGatewayAttachId, 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/transit-gateway-stack.ts: -------------------------------------------------------------------------------- 1 | import * as ec2 from "aws-cdk-lib/aws-ec2"; 2 | import { Construct } from "constructs"; 3 | import { 4 | ITransitGatewayProvides, 5 | ITransitGatewayStyle, 6 | ITransitGatewayBaseProps, 7 | } from "./types"; 8 | import { TransitGateway } from "./abstract-transitgateway"; 9 | 10 | export interface ITransitGatewayProps extends ITransitGatewayBaseProps { 11 | amazonSideAsn?: number; 12 | } 13 | 14 | export class TransitGatewayStack extends TransitGateway { 15 | name: string; 16 | props: ITransitGatewayProps; 17 | tgwStyle: ITransitGatewayStyle = "transitGateway"; 18 | provides: ITransitGatewayProvides = "transitGateway"; 19 | tgw: ec2.CfnTransitGateway; 20 | 21 | constructor(scope: Construct, id: string, props: ITransitGatewayProps) { 22 | super(scope, id, props); 23 | 24 | this.props = props; 25 | this.name = `${props.namePrefix}-transit-gateway`.toLowerCase(); 26 | 27 | this.tgw = new ec2.CfnTransitGateway(this, "TransitGateway", { 28 | amazonSideAsn: props.amazonSideAsn ? props.amazonSideAsn : 65521, 29 | autoAcceptSharedAttachments: "enable", 30 | defaultRouteTableAssociation: "disable", 31 | defaultRouteTablePropagation: "disable", 32 | vpnEcmpSupport: "enable", 33 | description: props.tgwDescription, 34 | tags: [ 35 | { 36 | key: "Name", 37 | value: this.name, 38 | }, 39 | ], 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/types.ts: -------------------------------------------------------------------------------- 1 | import * as ec2 from "aws-cdk-lib/aws-ec2"; 2 | import * as cdk from "aws-cdk-lib/core"; 3 | import { IConfigVpnTunnelOptions } from "./config/config-types"; 4 | 5 | /* 6 | * Base for our Transit Gateway 7 | */ 8 | export type ITransitGatewayProvides = "transitGateway"; 9 | export type ITransitGatewayStyle = "transitGateway"; 10 | export interface ITransitGatewayBase { 11 | name: string; 12 | props: ITransitGatewayBaseProps; 13 | tgwStyle: ITransitGatewayProvides; 14 | provides: ITransitGatewayStyle; 15 | tgw: ITgw; 16 | } 17 | 18 | export interface ITransitGatewayBaseProps extends cdk.StackProps { 19 | namePrefix: string; 20 | tgwDescription: string; 21 | } 22 | 23 | /* 24 | * Base for anything transit gateway attached that needs to route 25 | */ 26 | export type ITgwAttachType = "vpc" | "vpn" | "dxgw" | "tgwPeer"; 27 | export interface ITransitGatewayAttachImport { 28 | attrId: string; 29 | } 30 | export type ITgwAttachment = 31 | | ec2.CfnTransitGatewayVpcAttachment 32 | | ITransitGatewayAttachImport; 33 | export interface ITransitGatewayRouteImport { 34 | ref: string; 35 | } 36 | export type ITgwRouteTable = 37 | | ec2.CfnTransitGatewayRouteTable 38 | | ITransitGatewayRouteImport; 39 | export interface ITgwImport { 40 | attrId: string; 41 | } 42 | export type ITgw = ec2.CfnTransitGateway | ITgwImport; 43 | export interface IBuilderBase { 44 | name: string; 45 | withTgw: boolean; 46 | tgwAttachType: ITgwAttachType; 47 | globalPrefix: string; 48 | tgw: ITgw; 49 | tgwCreateTgwSubnets: boolean; 50 | tgwRouteTable: ITgwRouteTable; 51 | tgwRouteTableSsm: ssmParameterImport; 52 | tgwAttachment: ITgwAttachment; 53 | tgwAttachmentSsm: ssmParameterImport; 54 | tgwPropagateRouteAttachmentNames: Array; 55 | tgwBlackHoleCidrs: Array; 56 | tgwStaticRoutes: Array; 57 | tgwDefaultRouteAttachmentName?: ITgwPropagateRouteAttachmentName; 58 | } 59 | 60 | export interface IBuilderBaseProps extends cdk.StackProps { 61 | namePrefix: string; 62 | globalPrefix: string; 63 | ssmParameterPrefix: string; 64 | withTgw?: boolean; 65 | tgw?: ITgw; 66 | tgwPropagateRouteAttachmentNames?: Array; 67 | tgwBlackHoleCidrs?: Array; 68 | tgwStaticRoutes?: Array; 69 | tgwDefaultRouteAttachmentName?: ITgwPropagateRouteAttachmentName; 70 | } 71 | 72 | /* 73 | * Base VPC Class and base properties for our VPCs 74 | */ 75 | export type IBuilderVpcStyle = 76 | | "serviceInterfaceEndpoint" 77 | | "route53ResolverEndpoint" 78 | | "natEgress" 79 | | "awsNetworkFirewall" 80 | | "workloadIsolated" 81 | | "workloadPublic"; 82 | export type IBuildVpcProvides = 83 | | "endpoints" 84 | | "internet" 85 | | "firewall" 86 | | "workload"; 87 | export interface IBuilderVpc extends IBuilderBase { 88 | vpc: ec2.Vpc; 89 | vpcStyle: IBuilderVpcStyle; 90 | provides: IBuildVpcProvides; 91 | vpcInspects: boolean; 92 | ssmParameterPaths: IVpcParameterModel; 93 | publicSubnetNames: Array; 94 | privateSubnetNames: Array; 95 | privateIsolatedSubnetNames: Array; 96 | } 97 | export interface IBuilderVpcProps extends IBuilderBaseProps { 98 | availabilityZones: Array; 99 | vpcCidr: string; 100 | tgw?: ITgw; 101 | } 102 | 103 | /* 104 | * Common interface for our workload stacks. 105 | */ 106 | export interface IVpcWorkloadProps extends IBuilderVpcProps { 107 | createSubnets: Array; 108 | organizationId?: string; 109 | organizationMainAccountId?: string; 110 | legacyRamShare?: boolean 111 | } 112 | 113 | /* 114 | * Base VPN Class and base properties for our VPNs 115 | */ 116 | export type IBuilderVpnStyle = "transitGatewayAttached"; 117 | export type IBuilderVpnProvides = "amazonManagedVpn"; 118 | export interface IVpnImport { 119 | ref: string; 120 | } 121 | export type IVpn = IVpnImport | ec2.CfnVPNConnection; 122 | export interface IBuilderVpn extends IBuilderBase { 123 | vpn: IVpn; 124 | vpnStyle: IBuilderVpnStyle; 125 | vpnProvides: IBuilderVpnProvides; 126 | tunnelOneOptions: IConfigVpnTunnelOptions; 127 | tunnelTwoOptions: IConfigVpnTunnelOptions; 128 | } 129 | export interface IBuilderVpnProps extends IBuilderBaseProps { 130 | tunnelOneOptions?: IConfigVpnTunnelOptions; 131 | tunnelTwoOptions?: IConfigVpnTunnelOptions; 132 | } 133 | 134 | /* 135 | * Base Direct Connect Gateway (DxGw) Class and base properties for our Direct Connect Gateway Imports 136 | * 137 | * NOTE: There is no Cloudformation support for Direct Connect at the moment. This will serve as an abstract model so 138 | * so we can import the tgw attachments and create static routes and propagations 139 | * See: https://github.com/aws-cloudformation/cloudformation-coverage-roadmap/issues/876 140 | * Expand in the future to support creation of the Dx Gateway itself when support is added. 141 | */ 142 | export interface IBuilderDxGw extends IBuilderBase { 143 | } 144 | export interface IBuilderDxGwProps extends IBuilderBaseProps { 145 | } 146 | 147 | /* 148 | * Cross Region / Cross region peering is not implemented (PRs welcome!) 149 | * however we can support them for existing / manually created peers to be able to route to them 150 | */ 151 | export interface IBuilderTgwPeer extends IBuilderBase { 152 | } 153 | export interface IBuilderTgwPeerProps extends IBuilderBaseProps { 154 | } 155 | 156 | export interface ICustomResourceParseAwsFirewallEndpoints { 157 | firewallEndpoints: Array; 158 | availabilityZone: string; 159 | } 160 | 161 | export interface ICustomResourceTGWStaticRoute { 162 | transitGatewayAttachmentId: string; 163 | destinationCidrBlock: string; 164 | transitGatewayRouteTableId: string; 165 | } 166 | 167 | export interface ICustomResourceTGWFindVpnAttach { 168 | transitGatewayId: string; 169 | vpnId: string; 170 | } 171 | 172 | export type IVpcSubnetParameterNames = 173 | | "subnetId" 174 | | "subnetCidr" 175 | | "routeTableId"; 176 | export interface IVpcSubnetParameterModel { 177 | subnetName: string; 178 | subnetId: string; 179 | subnetCidr: string; 180 | availabilityZone: string; 181 | routeTableId: string; 182 | } 183 | 184 | export type IVpcParameterNames = 185 | | "vpcId" 186 | | "vpcCidr" 187 | | "tgwAttachId" 188 | | "tgwRouteId" 189 | | "tgwId"; 190 | export interface IVpcParameterModel { 191 | vpcName: string; 192 | vpcId: string; 193 | vpcCidr: string; 194 | availabilityZones: Array; 195 | subnets: Array; 196 | tgwId?: string; 197 | tgwAttachId?: string; 198 | tgwRouteId?: string; 199 | } 200 | 201 | export interface INamedSubnet { 202 | name: string; 203 | subnet: ec2.Subnet; 204 | } 205 | 206 | export interface ITgwPropagateRouteAttachmentName { 207 | attachTo: IBuilderVpc | IBuilderVpn | IBuilderDxGw; 208 | inspectBy?: IBuilderVpc | IBuilderVpn; 209 | } 210 | 211 | export interface IBuilderTgwStaticRoutes 212 | extends ITgwPropagateRouteAttachmentName { 213 | cidrAddress: string; 214 | } 215 | 216 | export interface SubnetNamedMasks { 217 | name: string; 218 | cidrMask: number; 219 | sharedWith?: Array; 220 | } 221 | 222 | export type ssmParameterImport = { 223 | name: string; 224 | token?: string; 225 | }; 226 | -------------------------------------------------------------------------------- /lib/vpc-aws-network-firewall-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import * as ec2 from "aws-cdk-lib/aws-ec2"; 4 | import * as anfw from "aws-cdk-lib/aws-networkfirewall"; 5 | import * as cr from "aws-cdk-lib/custom-resources"; 6 | import * as nodeLambda from "aws-cdk-lib/aws-lambda-nodejs"; 7 | import { 8 | ITgw, 9 | IBuilderVpcProps, 10 | IBuilderVpcStyle, 11 | IBuildVpcProvides, 12 | ICustomResourceParseAwsFirewallEndpoints, 13 | } from "./types"; 14 | import { BuilderVpc } from "./abstract-buildervpc"; 15 | 16 | export interface IVpcAwsNetworkFirewallProps extends IBuilderVpcProps { 17 | tgw: ITgw; 18 | firewallName: string; 19 | firewallDescription: string; 20 | firewallPolicyArn?: string; 21 | } 22 | 23 | export class VpcAwsNetworkFirewallStack extends BuilderVpc { 24 | vpcStyle: IBuilderVpcStyle = "awsNetworkFirewall"; 25 | vpcInspects: boolean = true; 26 | withTgw: boolean = true; 27 | provides: IBuildVpcProvides = "firewall"; 28 | props: IVpcAwsNetworkFirewallProps; 29 | firewallPolicy: anfw.CfnFirewallPolicy; 30 | firewall: anfw.CfnFirewall; 31 | firewallPolicyArn: string; 32 | 33 | constructor( 34 | scope: Construct, 35 | id: string, 36 | props: IVpcAwsNetworkFirewallProps 37 | ) { 38 | super(scope, id, props); 39 | 40 | this.name = `${props.namePrefix}-provider-firewall`.toLowerCase(); 41 | 42 | this.vpc = new ec2.Vpc(this, this.name, { 43 | ipAddresses: ec2.IpAddresses.cidr(this.props.vpcCidr), 44 | enableDnsHostnames: true, 45 | enableDnsSupport: true, 46 | maxAzs: this.props.availabilityZones.length, 47 | subnetConfiguration: [ 48 | { 49 | name: "firewall-services", 50 | cidrMask: 28, 51 | subnetType: ec2.SubnetType.PRIVATE_ISOLATED, 52 | }, 53 | { 54 | name: "transit-gateway", 55 | cidrMask: 28, 56 | subnetType: ec2.SubnetType.PRIVATE_ISOLATED, 57 | }, 58 | ], 59 | }); 60 | 61 | this.privateIsolatedSubnetNames.push( 62 | ...["firewall-services", "transit-gateway"] 63 | ); 64 | 65 | if (this.props.firewallPolicyArn) { 66 | this.firewallPolicyArn = this.props.firewallPolicyArn; 67 | } else { 68 | this.buildFirewallPolicy(); 69 | this.firewallPolicyArn = this.firewallPolicy.attrFirewallPolicyArn; 70 | } 71 | 72 | this.firewall = new anfw.CfnFirewall(this, props.firewallName, { 73 | firewallName: this.props.firewallName, 74 | description: this.props.firewallDescription, 75 | firewallPolicyArn: this.firewallPolicyArn, 76 | subnetMappings: this.vpc 77 | .selectSubnets({ subnetGroupName: "firewall-services" }) 78 | .subnetIds.map((subnetId) => { 79 | return { subnetId: subnetId }; 80 | }), 81 | vpcId: this.vpc.vpcId, 82 | }); 83 | 84 | const endpointParserBackend = new cr.Provider( 85 | this, 86 | "EndpointParserBackend", 87 | { 88 | onEventHandler: new nodeLambda.NodejsFunction( 89 | this, 90 | "FirewallEndpointParser", 91 | { 92 | entry: "lambda/parseAwsFirewallEndpoints/index.ts", 93 | handler: "onEvent", 94 | } 95 | ), 96 | } 97 | ); 98 | 99 | // Network firewall 'helpfully' returns an unordered array with a string mapping of availability zone 100 | // to interface. No easy way to manipulate this to get a route associated in CloudFormation so I'll use 101 | // a custom resource to return the interface ID for each AZ. 102 | // If you've got a clever way to do this wout a custom resource please let me know! 103 | props.availabilityZones.forEach((availabilityZone) => { 104 | const properties: ICustomResourceParseAwsFirewallEndpoints = { 105 | availabilityZone: availabilityZone, 106 | firewallEndpoints: this.firewall.attrEndpointIds, 107 | }; 108 | const mappedEndpoint = new cdk.CustomResource( 109 | this, 110 | `EndpointParser${availabilityZone}`, 111 | { 112 | properties: properties, 113 | serviceToken: endpointParserBackend.serviceToken, 114 | } 115 | ); 116 | this.vpc 117 | .selectSubnets({ 118 | subnetGroupName: "transit-gateway", 119 | }) 120 | .subnets.forEach((subnet) => { 121 | if (subnet.availabilityZone == availabilityZone) { 122 | new ec2.CfnRoute(this, `RouteToNFW-${availabilityZone}`, { 123 | routeTableId: (subnet as ec2.Subnet).routeTable.routeTableId, 124 | destinationCidrBlock: "0.0.0.0/0", 125 | vpcEndpointId: mappedEndpoint.getAttString("endpointId"), 126 | }); 127 | } 128 | }); 129 | }); 130 | } 131 | 132 | buildFirewallPolicy() { 133 | // This creates basically what you'd get in the AWS Console by clicking through the defaults 134 | this.firewallPolicy = new anfw.CfnFirewallPolicy(this, "FirewallPolicy", { 135 | firewallPolicyName: `${this.props.firewallName}-policy`, 136 | description: `${this.props.firewallDescription} Policy`, 137 | firewallPolicy: { 138 | statelessRuleGroupReferences: [], 139 | statelessDefaultActions: ["aws:forward_to_sfe"], 140 | statelessFragmentDefaultActions: ["aws:forward_to_sfe"], 141 | statelessCustomActions: [], 142 | statefulRuleGroupReferences: [], 143 | statefulEngineOptions: { 144 | ruleOrder: "DEFAULT_ACTION_ORDER", 145 | }, 146 | }, 147 | }); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /lib/vpc-interface-endpoints-stack.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from "constructs"; 2 | import { ServiceDetail } from "@aws-sdk/client-ec2"; 3 | import * as ec2 from "aws-cdk-lib/aws-ec2"; 4 | import * as r53 from "aws-cdk-lib/aws-route53"; 5 | import * as r53t from "aws-cdk-lib/aws-route53-targets"; 6 | import { 7 | IBuilderVpcProps, 8 | IBuilderVpcStyle, 9 | IBuildVpcProvides, 10 | ITgwAttachType, 11 | ITgwPropagateRouteAttachmentName, 12 | ITgw, 13 | } from "./types"; 14 | import { BuilderVpc } from "./abstract-buildervpc"; 15 | 16 | export interface IVpcInterfaceEndpointsProps extends IBuilderVpcProps { 17 | /** 18 | * List of Interfaces to create. Use Discovery formatting (ie: [ 'com.amazonaws.us-east-1.ec2', 'com.amazonaws.us-east-1.ec2messages' ] 19 | */ 20 | interfaceList: Array; 21 | /** 22 | * Entire list of interfaces available within the union of all Availability Zones specified in availabilityZones. Use Discovery process to popaulte. 23 | */ 24 | interfaceDiscovery: Array; 25 | /** 26 | * Existing Security Group for our Interfaces to use. 27 | * @default One is created permitting from 0.0.0.0/0 port 443 28 | */ 29 | interfaceEndpointSecurityGroup?: ec2.SecurityGroup; 30 | /** 31 | * The Mask to use when creating the Interface Subnets. 32 | * @default 22 33 | */ 34 | perSubnetCidrMask?: number; 35 | /** 36 | * The VPCs that will be using the interfaces. 37 | */ 38 | interfaceEndpointSharedWithVpcs?: Array; 39 | // not supporting a non-transit gateway version of this 40 | tgw: ITgw; 41 | } 42 | 43 | export class VpcInterfaceEndpointsStack extends BuilderVpc { 44 | vpcStyle: IBuilderVpcStyle = "serviceInterfaceEndpoint"; 45 | tgwAttachType: ITgwAttachType = "vpc"; 46 | withTgw: boolean = true; 47 | provides: IBuildVpcProvides = "endpoints"; 48 | props: IVpcInterfaceEndpointsProps; 49 | interfaceEndpointSecurityGroup: ec2.SecurityGroup; 50 | 51 | constructor( 52 | scope: Construct, 53 | id: string, 54 | props: IVpcInterfaceEndpointsProps 55 | ) { 56 | super(scope, id, props); 57 | 58 | // Build our Name 59 | this.name = 60 | `${props.namePrefix}-provider-endpoint-service-interface`.toLowerCase(); 61 | // Create the VPC 62 | this.createEndpointVpc(); 63 | 64 | // If we have a security group passed use it, otherwise create a sane default 65 | props.interfaceEndpointSecurityGroup 66 | ? (this.interfaceEndpointSecurityGroup = 67 | props.interfaceEndpointSecurityGroup) 68 | : this.endpointSecurityGroup(); 69 | 70 | // Now the meat of it, create our Interface Endpoints 71 | // This whole section creates a 'dependsOn' chain that assures that no more than 3 endpoints 72 | // private hosted zones, and records are created at once. Otherwise CloudFormation will happily exceed 73 | // the throttles in place and fail out. 74 | let previousEndpoint: ec2.InterfaceVpcEndpoint; 75 | props.interfaceList.forEach((endpointName, index) => { 76 | // Our first three positions are com.amazonaws.{region}. We'll retain after that and sub our . for a - 77 | let endpointNameTemp = endpointName.split("."); 78 | endpointNameTemp.splice(0, 3); 79 | const endpointNameShort = endpointNameTemp.join("-"); 80 | 81 | const endpoint = new ec2.InterfaceVpcEndpoint( 82 | this, 83 | `InterfaceEndpoint-${endpointNameShort}`, 84 | { 85 | privateDnsEnabled: false, 86 | service: new ec2.InterfaceVpcEndpointAwsService( 87 | endpointNameShort as string 88 | ), 89 | vpc: this.vpc, 90 | securityGroups: [this.interfaceEndpointSecurityGroup], 91 | } 92 | ); 93 | 94 | // We will do endpoints in batches of three to keep from hitting service throttles. 95 | if (index == 0) { 96 | previousEndpoint = endpoint; 97 | } else if (index % 3 == 0) { 98 | endpoint.node.addDependency(previousEndpoint); 99 | previousEndpoint = endpoint; 100 | } else { 101 | endpoint.node.addDependency(previousEndpoint); 102 | } 103 | 104 | // Create our private hosted zone where we have a private DNS name is available from our service 105 | const endpointPrivateDnsName = this.lookupPrivateDnsName(endpointName); 106 | // Confirm this endpoint is available in all the AZs our stack will be deployed to 107 | if(!this.serviceAvailableInAllAzs(endpointName)) { 108 | throw new Error(`Endpoint ${endpointName} is not available in all Availability Zones: ${this.availabilityZones.join(',')}`) 109 | } 110 | if (endpointPrivateDnsName) { 111 | const privateHostedZone = new r53.PrivateHostedZone( 112 | this, 113 | `PrivateHostedZone-${endpointNameShort}`, 114 | { 115 | vpc: this.vpc, 116 | zoneName: endpointPrivateDnsName, 117 | } 118 | ); 119 | 120 | // Where additional VPCs are to be associated, do that here 121 | if (props.interfaceEndpointSharedWithVpcs) { 122 | for (const sharedWith of props.interfaceEndpointSharedWithVpcs) { 123 | if (sharedWith.attachTo instanceof BuilderVpc) { 124 | privateHostedZone.addVpc(sharedWith.attachTo.vpc); 125 | } 126 | } 127 | } 128 | privateHostedZone.node.addDependency(endpoint); 129 | 130 | // Create our recordset 131 | const recordSet = new r53.ARecord( 132 | this, 133 | `EndpointRecord-${endpointNameShort}`, 134 | { 135 | target: r53.RecordTarget.fromAlias( 136 | new r53t.InterfaceVpcEndpointTarget(endpoint) 137 | ), 138 | zone: privateHostedZone, 139 | } 140 | ); 141 | recordSet.node.addDependency(privateHostedZone); 142 | } 143 | }); 144 | } 145 | 146 | createEndpointVpc() { 147 | const vpcProps: ec2.VpcProps = { 148 | ipAddresses: ec2.IpAddresses.cidr(this.props.vpcCidr), 149 | enableDnsHostnames: true, 150 | enableDnsSupport: true, 151 | maxAzs: this.props.availabilityZones.length, 152 | subnetConfiguration: [ 153 | { 154 | name: "interface-endpoints", 155 | // 1024 Addresses per subnet unless we're over-ridden 156 | cidrMask: this.props.perSubnetCidrMask 157 | ? this.props.perSubnetCidrMask 158 | : 22, 159 | subnetType: ec2.SubnetType.PRIVATE_ISOLATED, 160 | }, 161 | ], 162 | }; 163 | this.privateIsolatedSubnetNames.push("interface-endpoints"); 164 | 165 | if (this.props.tgw) { 166 | vpcProps.subnetConfiguration?.push({ 167 | name: "transit-gateway", 168 | cidrMask: 28, 169 | subnetType: ec2.SubnetType.PRIVATE_ISOLATED, 170 | }); 171 | this.privateIsolatedSubnetNames.push("transit-gateway"); 172 | } 173 | 174 | this.vpc = new ec2.Vpc(this, this.name, vpcProps); 175 | } 176 | 177 | endpointSecurityGroup() { 178 | this.interfaceEndpointSecurityGroup = new ec2.SecurityGroup( 179 | this, 180 | "VPCEndpointSecurityGroup", 181 | { 182 | allowAllOutbound: true, 183 | description: "Security Group for VPC Interface Endpoints", 184 | securityGroupName: "VPCEndpointSecurityGroup", 185 | vpc: this.vpc, 186 | } 187 | ); 188 | this.interfaceEndpointSecurityGroup.addIngressRule( 189 | ec2.Peer.anyIpv4(), 190 | ec2.Port.tcp(443), 191 | "Allow endpoint use from any address via HTTPS" 192 | ); 193 | } 194 | 195 | // Confirm the serviceName requested exists in the Availability Zones our VPC will be created in 196 | serviceAvailableInAllAzs(serviceName: string): boolean { 197 | const service = this.props.interfaceDiscovery.find( 198 | (service) => service.ServiceName == serviceName 199 | ); 200 | if(service) { 201 | if (service.AvailabilityZones) { 202 | return this.availabilityZones.every( 203 | (i) => service.AvailabilityZones?.includes(i), 204 | ); 205 | } 206 | } 207 | return false; 208 | } 209 | 210 | lookupPrivateDnsName(serviceName: string): string | undefined { 211 | const service = this.props.interfaceDiscovery.find( 212 | (service) => service.ServiceName == serviceName 213 | ); 214 | if (service) { 215 | return service.PrivateDnsName; 216 | } else { 217 | throw new Error( 218 | `Interface Endpoint named ${serviceName} not found in discovery files` 219 | ); 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /lib/vpc-nat-egress-stack.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from "constructs"; 2 | import * as ec2 from "aws-cdk-lib/aws-ec2"; 3 | import { 4 | IBuilderVpcProps, 5 | IBuilderVpcStyle, 6 | IBuildVpcProvides, 7 | ITgw, 8 | } from "./types"; 9 | import { BuilderVpc } from "./abstract-buildervpc"; 10 | 11 | export interface IVpcNatEgressProps extends IBuilderVpcProps { 12 | // Pattern requires a TGW 13 | tgw: ITgw; 14 | } 15 | 16 | export class VpcNatEgressStack extends BuilderVpc { 17 | vpcStyle: IBuilderVpcStyle = "natEgress"; 18 | props: IVpcNatEgressProps; 19 | withTgw: true; 20 | provides: IBuildVpcProvides = "internet"; 21 | 22 | constructor(scope: Construct, id: string, props: IVpcNatEgressProps) { 23 | super(scope, id, props); 24 | this.props = props; 25 | this.name = `${props.namePrefix}-provider-internet`.toLowerCase(); 26 | 27 | this.vpc = new ec2.Vpc(this, this.name, { 28 | ipAddresses: ec2.IpAddresses.cidr(this.props.vpcCidr), 29 | enableDnsHostnames: true, 30 | enableDnsSupport: true, 31 | maxAzs: this.props.availabilityZones.length, 32 | subnetConfiguration: [ 33 | { 34 | name: "nat-egress", 35 | cidrMask: 28, 36 | subnetType: ec2.SubnetType.PUBLIC, 37 | }, 38 | { 39 | name: "transit-gateway", 40 | cidrMask: 28, 41 | subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, 42 | }, 43 | ], 44 | }); 45 | this.publicSubnetNames.push("nat-egress"); 46 | // We're NATing our transit gateway connections, so we consider it a 'private' in this use-case. 47 | this.privateSubnetNames.push("transit-gateway"); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/vpc-route53-resolver-endpoints-stack.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from "constructs"; 2 | import * as ec2 from "aws-cdk-lib/aws-ec2"; 3 | import * as r53r from "aws-cdk-lib/aws-route53resolver"; 4 | const md5 = require("md5"); 5 | 6 | import { 7 | IBuilderVpcProps, 8 | IBuilderVpcStyle, 9 | IBuildVpcProvides, 10 | ITgwAttachType, 11 | ITgw, 12 | IBuilderVpc, 13 | } from "./types"; 14 | import { IConfigProviderRoute53EndpointsForExistingVpcs } from "./config/config-types"; 15 | import { BuilderVpc } from "./abstract-buildervpc"; 16 | 17 | interface forwardRequestsProps { 18 | forDomains: Array; 19 | toIps: Array; 20 | forVpcs?: Array; 21 | forExistingVpcs?: Array; 22 | } 23 | 24 | export interface IVpcRoute53ResolverEndpointsProps extends IBuilderVpcProps { 25 | forwardRequests?: forwardRequestsProps; 26 | resolveRequestsFromCidrs?: Array; 27 | // not supporting a non-transit gateway version of this 28 | tgw: ITgw; 29 | } 30 | 31 | export class VpcRoute53ResolverEndpointsStack extends BuilderVpc { 32 | vpc: ec2.Vpc; 33 | vpcStyle: IBuilderVpcStyle = "route53ResolverEndpoint"; 34 | tgwAttachType: ITgwAttachType = "vpc"; 35 | withTgw: boolean = true; 36 | provides: IBuildVpcProvides = "endpoints"; 37 | props: IVpcRoute53ResolverEndpointsProps; 38 | inboundResolverSg: ec2.SecurityGroup; 39 | inboundResolver: r53r.CfnResolverEndpoint; 40 | outboundResolverSg: ec2.SecurityGroup; 41 | outboundResolver: r53r.CfnResolverEndpoint; 42 | 43 | constructor( 44 | scope: Construct, 45 | id: string, 46 | props: IVpcRoute53ResolverEndpointsProps 47 | ) { 48 | super(scope, id, props); 49 | 50 | // Build our Name 51 | this.name = 52 | `${props.namePrefix}-provider-endpoint-route53-resolver`.toLowerCase(); 53 | this.createVpc(); 54 | 55 | // CloudFormation will fail if our AZs are less than two. We'll catch and throw about that here. 56 | if (props.availabilityZones.length < 2) { 57 | throw new Error( 58 | "To use Route53 Resolver Endpoints you must provide at least two availability zones" 59 | ); 60 | } 61 | 62 | // Configure our inbound if present 63 | if (props.resolveRequestsFromCidrs) { 64 | this.createInboundResolverSg(); 65 | let ipAddressProps: Array = 66 | []; 67 | this.vpc 68 | .selectSubnets({ subnetGroupName: "resolver-endpoints" }) 69 | .subnetIds.forEach((subnetId) => { 70 | ipAddressProps.push({ subnetId: subnetId }); 71 | }); 72 | this.inboundResolver = new r53r.CfnResolverEndpoint( 73 | this, 74 | "InboundResolver", 75 | { 76 | name: "InboundRoute53Resolver", 77 | direction: "INBOUND", 78 | ipAddresses: ipAddressProps, 79 | securityGroupIds: [this.inboundResolverSg.securityGroupId], 80 | } 81 | ); 82 | } 83 | 84 | // Configure our outbound if present 85 | if (props.forwardRequests?.forDomains || props.forwardRequests?.toIps) { 86 | this.createOutboundResolverSg(); 87 | let ipAddressProps: Array = 88 | []; 89 | this.vpc 90 | .selectSubnets({ subnetGroupName: "resolver-endpoints" }) 91 | .subnetIds.forEach((subnetId) => { 92 | ipAddressProps.push({ subnetId: subnetId }); 93 | }); 94 | this.outboundResolver = new r53r.CfnResolverEndpoint( 95 | this, 96 | "OutboundResolver", 97 | { 98 | direction: "OUTBOUND", 99 | ipAddresses: ipAddressProps, 100 | securityGroupIds: [this.outboundResolverSg.securityGroupId], 101 | } 102 | ); 103 | // Now for the rules 104 | const targetIpsProps = props.forwardRequests.toIps.map((targetIp) => { 105 | return { 106 | ip: targetIp, 107 | port: "53", 108 | } as r53r.CfnResolverRule.TargetAddressProperty; 109 | }); 110 | for (const domain of props.forwardRequests.forDomains) { 111 | const domainWithDash = domain.replace(".", "-"); 112 | const resolveEndpoint = new r53r.CfnResolverRule( 113 | this, 114 | `resolverForwardRule-${md5(domain)}`, 115 | { 116 | name: `Forward-${domainWithDash}`, 117 | domainName: domain, 118 | resolverEndpointId: this.outboundResolver.attrResolverEndpointId, 119 | ruleType: "FORWARD", 120 | targetIps: targetIpsProps, 121 | } 122 | ); 123 | 124 | new r53r.CfnResolverRuleAssociation( 125 | this, 126 | `resolverRuleAssociateSelf-${md5(domain)}`, 127 | { 128 | name: `${this.name}-association`, 129 | resolverRuleId: resolveEndpoint.attrResolverRuleId, 130 | vpcId: this.vpc.vpcId, 131 | } 132 | ); 133 | // Associate the rule for any additional VPCs 134 | if (props.forwardRequests.forVpcs) { 135 | props.forwardRequests.forVpcs.forEach((vpc) => { 136 | // We need to use the config file name for our existing vpc since its a string literal. 137 | // Using a vpc.vpc.vpcId here would create a token that changes between synths and changes our resources. 138 | const ruleCfnId = `${vpc.name}${domain}`; 139 | new r53r.CfnResolverRuleAssociation( 140 | this, 141 | `resolverRuleAssociateVpc-${md5(ruleCfnId)}`, 142 | { 143 | name: `${vpc.name}-association`, 144 | resolverRuleId: resolveEndpoint.attrResolverRuleId, 145 | vpcId: vpc.vpc.vpcId, 146 | } 147 | ); 148 | }); 149 | } 150 | if (props.forwardRequests.forExistingVpcs) { 151 | props.forwardRequests.forExistingVpcs.forEach((vpc) => { 152 | // We can use the identifier in md5 since it is a string literal and won't change 153 | const ruleCfnId = `${vpc.vpcId}${domain}`; 154 | new r53r.CfnResolverRuleAssociation( 155 | this, 156 | `resolverRuleAssociateVpc-${md5(ruleCfnId)}`, 157 | { 158 | name: `${vpc.name}-association`, 159 | resolverRuleId: resolveEndpoint.attrResolverRuleId, 160 | vpcId: vpc.vpcId, 161 | } 162 | ); 163 | }); 164 | } 165 | } 166 | } 167 | } 168 | 169 | createVpc() { 170 | this.vpc = new ec2.Vpc(this, this.name, { 171 | ipAddresses: ec2.IpAddresses.cidr(this.props.vpcCidr), 172 | enableDnsHostnames: true, 173 | enableDnsSupport: true, 174 | maxAzs: this.props.availabilityZones.length, 175 | subnetConfiguration: [ 176 | { 177 | name: "resolver-endpoints", 178 | cidrMask: 28, 179 | subnetType: ec2.SubnetType.PRIVATE_ISOLATED, 180 | }, 181 | { 182 | name: "transit-gateway", 183 | cidrMask: 28, 184 | subnetType: ec2.SubnetType.PRIVATE_ISOLATED, 185 | }, 186 | ], 187 | }); 188 | this.privateIsolatedSubnetNames.push( 189 | ...["resolver-endpoints", "transit-gateway"] 190 | ); 191 | } 192 | 193 | createInboundResolverSg() { 194 | this.inboundResolverSg = new ec2.SecurityGroup(this, "r53InboundResolver", { 195 | allowAllOutbound: true, 196 | description: "Security Group for Route53 Inbound Resolver", 197 | securityGroupName: "r53InboundResolver", 198 | vpc: this.vpc, 199 | }); 200 | for (const inboundCidr of this.props.resolveRequestsFromCidrs!) { 201 | this.inboundResolverSg.addIngressRule( 202 | ec2.Peer.ipv4(inboundCidr), 203 | ec2.Port.tcp(53), 204 | `Resolver TCP DNS Query from ${inboundCidr}` 205 | ); 206 | this.inboundResolverSg.addIngressRule( 207 | ec2.Peer.ipv4(inboundCidr), 208 | ec2.Port.udp(53), 209 | `Resolver UDP DNS Query from ${inboundCidr}` 210 | ); 211 | } 212 | } 213 | 214 | createOutboundResolverSg() { 215 | // No ingress rules required since this is an outbound resolver only. 216 | this.outboundResolverSg = new ec2.SecurityGroup( 217 | this, 218 | "r53OutboundResolver", 219 | { 220 | allowAllOutbound: true, 221 | description: "Security Group for Route53 Outbound Resolver", 222 | securityGroupName: "r53OutboundResolver", 223 | vpc: this.vpc, 224 | } 225 | ); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /lib/vpc-workload-isolated-stack.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from "constructs"; 2 | import * as ec2 from "aws-cdk-lib/aws-ec2"; 3 | import { FlowLogTrafficType, SubnetType } from "aws-cdk-lib/aws-ec2"; 4 | import { 5 | IBuilderVpcStyle, 6 | IBuildVpcProvides, 7 | IVpcWorkloadProps, 8 | } from "./types"; 9 | import { BuilderVpc } from "./abstract-buildervpc"; 10 | import * as ram from "aws-cdk-lib/aws-ram"; 11 | 12 | // export interface IVpcWorkloadIsolatedProps extends IBuilderVpcProps { 13 | // createSubnets: Array 14 | // organizationId?: string 15 | // } 16 | 17 | export class VpcWorkloadIsolatedStack extends BuilderVpc { 18 | vpcStyle: IBuilderVpcStyle = "workloadIsolated"; 19 | props: IVpcWorkloadProps; 20 | provides: IBuildVpcProvides = "workload"; 21 | tgwCreateTgwSubnets: boolean = false; 22 | 23 | constructor(scope: Construct, id: string, props: IVpcWorkloadProps) { 24 | super(scope, id, props); 25 | 26 | this.name = `${props.namePrefix}-vpc-workload`.toLowerCase(); 27 | 28 | const vpcProps: ec2.VpcProps = { 29 | ipAddresses: ec2.IpAddresses.cidr(this.props.vpcCidr), 30 | enableDnsHostnames: true, 31 | enableDnsSupport: true, 32 | maxAzs: this.props.availabilityZones.length, 33 | subnetConfiguration: [], 34 | gatewayEndpoints: { 35 | S3: { 36 | service: ec2.GatewayVpcEndpointAwsService.S3, 37 | }, 38 | DDB: { 39 | service: ec2.GatewayVpcEndpointAwsService.DYNAMODB, 40 | }, 41 | }, 42 | }; 43 | 44 | props.createSubnets.forEach((createSubnet) => { 45 | vpcProps.subnetConfiguration?.push({ 46 | name: createSubnet.name.toLowerCase(), 47 | cidrMask: createSubnet.cidrMask, 48 | subnetType: SubnetType.PRIVATE_ISOLATED, 49 | }); 50 | this.privateIsolatedSubnetNames.push(createSubnet.name.toLowerCase()); 51 | }); 52 | 53 | this.vpc = new ec2.Vpc(this, this.name, vpcProps); 54 | this.vpc.addFlowLog("VpcFlowLogs", { 55 | destination: ec2.FlowLogDestination.toCloudWatchLogs(), 56 | trafficType: FlowLogTrafficType.ALL, 57 | }); 58 | 59 | this.props.createSubnets.forEach((createSubnet) => { 60 | if (createSubnet.sharedWith) { 61 | new ram.CfnResourceShare(this, `RamShare${createSubnet.name}`, { 62 | allowExternalPrincipals: false, 63 | name: `Share-${this.name}`, 64 | permissionArns: [ 65 | "arn:aws:ram::aws:permission/AWSRAMDefaultPermissionSubnet", 66 | ], 67 | principals: this.ramPrincipals(createSubnet.sharedWith), 68 | resourceArns: this.subnetArnsByName(createSubnet.name), 69 | }); 70 | } 71 | }); 72 | } 73 | 74 | subnetArnsByName(subnetName: string) { 75 | const subnetArns: Array = []; 76 | this.vpc 77 | .selectSubnets({ subnetGroupName: subnetName }) 78 | .subnets.forEach((subnet) => { 79 | const subnetId = (subnet as ec2.Subnet).subnetId; 80 | subnetArns.push( 81 | `arn:aws:ec2:${this.region}:${this.account}:subnet/${subnetId}` 82 | ); 83 | }); 84 | return subnetArns; 85 | } 86 | 87 | ramPrincipals(sharedWithList: Array) { 88 | const ramPrincipals: Array = []; 89 | // AWS Account Identifier 90 | for (const sharedWith of sharedWithList) { 91 | if (Number.isInteger(sharedWith)) { 92 | ramPrincipals.push(`${sharedWith}`); 93 | } else { 94 | const sharedWithString = sharedWith.toString(); 95 | let organizationMainAccountId = this.props.organizationMainAccountId 96 | // Historically we could use the deployment accounts ID to form our OU ARN. That no longer works 97 | // However we want to allow users to annotate existing VPCs to use this old approach to not trigger an update 98 | if(this.props.legacyRamShare) { 99 | organizationMainAccountId = this.account 100 | } 101 | // Entire Organization share 102 | if (sharedWithString.startsWith("o-")) { 103 | ramPrincipals.push( 104 | `arn:aws:organizations::${organizationMainAccountId}/${sharedWith}` 105 | ); 106 | } else if (sharedWithString.startsWith("ou-")) { 107 | if (this.props.organizationId) { 108 | ramPrincipals.push( 109 | `arn:aws:organizations::${organizationMainAccountId}:ou/${this.props.organizationId}/${sharedWith}` 110 | ); 111 | } 112 | } else { 113 | throw new Error( 114 | `SharedWith contained string: ${sharedWithString} which could not be mapped` 115 | ); 116 | } 117 | } 118 | } 119 | return ramPrincipals; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /lib/vpc-workload-public-stack.ts: -------------------------------------------------------------------------------- 1 | import { Construct } from "constructs"; 2 | import * as ec2 from "aws-cdk-lib/aws-ec2"; 3 | import { FlowLogTrafficType, SubnetType } from "aws-cdk-lib/aws-ec2"; 4 | import { 5 | IBuilderVpcStyle, 6 | IBuildVpcProvides, 7 | IVpcWorkloadProps, 8 | } from "./types"; 9 | import { BuilderVpc } from "./abstract-buildervpc"; 10 | import * as ram from "aws-cdk-lib/aws-ram"; 11 | 12 | // export interface IVpcWorkloadPublicProps extends IBuilderVpcProps { 13 | // createSubnets: Array 14 | // organizationId?: string 15 | // } 16 | 17 | export class VpcWorkloadPublicStack extends BuilderVpc { 18 | vpcStyle: IBuilderVpcStyle = "workloadPublic"; 19 | props: IVpcWorkloadProps; 20 | provides: IBuildVpcProvides = "workload"; 21 | tgwCreateTgwSubnets: boolean = false; 22 | 23 | constructor(scope: Construct, id: string, props: IVpcWorkloadProps) { 24 | super(scope, id, props); 25 | 26 | this.name = `${props.namePrefix}-vpc-public-workload`.toLowerCase(); 27 | 28 | const vpcProps: ec2.VpcProps = { 29 | ipAddresses: ec2.IpAddresses.cidr(this.props.vpcCidr), 30 | enableDnsHostnames: true, 31 | enableDnsSupport: true, 32 | maxAzs: this.props.availabilityZones.length, 33 | subnetConfiguration: [], 34 | }; 35 | 36 | props.createSubnets.forEach((createSubnet) => { 37 | vpcProps.subnetConfiguration?.push({ 38 | name: createSubnet.name.toLowerCase(), 39 | cidrMask: createSubnet.cidrMask, 40 | subnetType: SubnetType.PUBLIC, 41 | }); 42 | this.publicSubnetNames.push(createSubnet.name.toLowerCase()); 43 | }); 44 | 45 | this.vpc = new ec2.Vpc(this, this.name, vpcProps); 46 | this.vpc.addFlowLog("VpcFlowLogs", { 47 | destination: ec2.FlowLogDestination.toCloudWatchLogs(), 48 | trafficType: FlowLogTrafficType.ALL, 49 | }); 50 | 51 | this.props.createSubnets.forEach((createSubnet) => { 52 | if (createSubnet.sharedWith) { 53 | new ram.CfnResourceShare(this, `RamShare${createSubnet.name}`, { 54 | allowExternalPrincipals: false, 55 | name: `Share-${this.name}`, 56 | permissionArns: [ 57 | "arn:aws:ram::aws:permission/AWSRAMDefaultPermissionSubnet", 58 | ], 59 | principals: this.ramPrincipals(createSubnet.sharedWith), 60 | resourceArns: this.subnetArnsByName(createSubnet.name), 61 | }); 62 | } 63 | }); 64 | } 65 | 66 | subnetArnsByName(subnetName: string) { 67 | const subnetArns: Array = []; 68 | this.vpc 69 | .selectSubnets({ subnetGroupName: subnetName }) 70 | .subnets.forEach((subnet) => { 71 | const subnetId = (subnet as ec2.Subnet).subnetId; 72 | subnetArns.push( 73 | `arn:aws:ec2:${this.region}:${this.account}:subnet/${subnetId}` 74 | ); 75 | }); 76 | return subnetArns; 77 | } 78 | 79 | ramPrincipals(sharedWithList: Array) { 80 | const ramPrincipals: Array = []; 81 | // AWS Account Identifier 82 | for (const sharedWith of sharedWithList) { 83 | if (Number.isInteger(sharedWith)) { 84 | ramPrincipals.push(`${sharedWith}`); 85 | } else { 86 | const sharedWithString = sharedWith.toString(); 87 | // Entire Organization share 88 | let organizationMainAccountId = this.props.organizationMainAccountId 89 | // Historically we could use the deployment accounts ID to form our OU ARN. That no longer works 90 | // However we want to allow users to annotate existing VPCs to use this old approach to not trigger an update 91 | if(this.props.legacyRamShare) { 92 | organizationMainAccountId = this.account 93 | } 94 | if (sharedWithString.startsWith("o-")) { 95 | ramPrincipals.push( 96 | `arn:aws:organizations::${organizationMainAccountId}/${sharedWith}` 97 | ); 98 | } else if (sharedWithString.startsWith("ou-")) { 99 | if (this.props.organizationId) { 100 | ramPrincipals.push( 101 | `arn:aws:organizations::${organizationMainAccountId}:ou/${this.props.organizationId}/${sharedWith}` 102 | ); 103 | } 104 | } else { 105 | throw new Error( 106 | `SharedWith contained string: ${sharedWithString} which could not be mapped` 107 | ); 108 | } 109 | } 110 | } 111 | return ramPrincipals; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /lib/vpn-to-transit-gateway-stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "aws-cdk-lib"; 2 | import { Construct } from "constructs"; 3 | import * as ec2 from "aws-cdk-lib/aws-ec2"; 4 | import * as cr from "aws-cdk-lib/custom-resources"; 5 | import * as nodeLambda from "aws-cdk-lib/aws-lambda-nodejs"; 6 | import { 7 | IBuilderVpnProvides, 8 | IBuilderVpnStyle, 9 | IBuilderVpnProps, 10 | ICustomResourceTGWFindVpnAttach, 11 | ITgw, 12 | } from "./types"; 13 | import { BuilderVpn } from "./abstract-buildervpn"; 14 | import * as iam from "aws-cdk-lib/aws-iam"; 15 | import { IConfigVpnTunnelOptions } from "./config/config-types"; 16 | 17 | export interface IVpnToTransitGatewayProps extends IBuilderVpnProps { 18 | tgw: ITgw; 19 | existingCustomerGatewayId?: string; 20 | newCustomerGatewayIpAddress?: string; 21 | newCustomerGatewayAsn?: number; 22 | newCustomerGatewayName?: string; 23 | existingVpnConnectionId?: string; 24 | existingVpnTransitGatewayAttachId?: string; 25 | existingVpnTransitGatewayRouteTableId?: string; 26 | } 27 | 28 | // The CDK version of this is readonly for some reason? 29 | interface VpnTunnelOptionsSpecificationProperty { 30 | preSharedKey?: string; 31 | tunnelInsideCidr?: string; 32 | } 33 | 34 | export class VpnToTransitGatewayStack extends BuilderVpn { 35 | vpnStyle: IBuilderVpnStyle = "transitGatewayAttached"; 36 | vpnProvides: IBuilderVpnProvides = "amazonManagedVpn"; 37 | withTgw: boolean = true; 38 | tgwAttachType: "vpn"; 39 | customerGateway: ec2.CfnCustomerGateway; 40 | customerGatewayId: string; 41 | props: IVpnToTransitGatewayProps; 42 | findVpnTgwAttachCR: cr.Provider; 43 | 44 | constructor(scope: Construct, id: string, props: IVpnToTransitGatewayProps) { 45 | super(scope, id, props); 46 | 47 | this.name = `${props.namePrefix}-vpn`.toLowerCase(); 48 | 49 | // Determine if we're handling an import or creating a new resource 50 | if (this.props.existingVpnConnectionId) { 51 | if (!this.props.existingVpnTransitGatewayAttachId) { 52 | throw new Error( 53 | "Importing an existing VPN requires existingVpnTransitGatewayAttachId to be defined" 54 | ); 55 | } 56 | if (!this.props.existingVpnTransitGatewayRouteTableId) { 57 | throw new Error( 58 | "Importing an existing VPN requires existingVpnTransitGatewayRouteTableId to be defined" 59 | ); 60 | } 61 | // Verified our base properties exist, now we can do our import 62 | this.vpn = { 63 | ref: this.props.existingVpnConnectionId, 64 | }; 65 | this.tgwRouteTable = { 66 | ref: this.props.existingVpnTransitGatewayRouteTableId, 67 | }; 68 | this.tgwAttachment = { 69 | attrId: this.props.existingVpnTransitGatewayAttachId, 70 | }; 71 | } else { 72 | if (props.existingCustomerGatewayId) { 73 | this.customerGatewayId = props.existingCustomerGatewayId; 74 | } else { 75 | this.createNewCustomerGateway(); 76 | } 77 | // There is no property on the VPN connection that gives us our Transit Gateway Attachment which we need for routing 78 | // So we need to use a custom resources which describes the connection and gets the attachmentId so we can make a route table. 79 | const findVpnTgwAttachIdCRFunction = new nodeLambda.NodejsFunction( 80 | this, 81 | "findVpnTgwAttachIdCRFunction", 82 | { 83 | entry: "lambda/findVpnTransitGatewayAttachId/index.ts", 84 | handler: "onEvent", 85 | } 86 | ); 87 | findVpnTgwAttachIdCRFunction.addToRolePolicy( 88 | new iam.PolicyStatement({ 89 | effect: iam.Effect.ALLOW, 90 | actions: ["ec2:DescribeTransitGatewayAttachments"], 91 | resources: ["*"], 92 | }) 93 | ); 94 | this.findVpnTgwAttachCR = new cr.Provider( 95 | this, 96 | "findVpnTgwAttachCRBackend", 97 | { 98 | onEventHandler: findVpnTgwAttachIdCRFunction, 99 | } 100 | ); 101 | 102 | // Ugliness of this code is mostly a workaround for 'vpnTunnelOptionsSpecifications' being readonly in the CDK 103 | let vpnPropsBase: ec2.CfnVPNConnectionProps = { 104 | customerGatewayId: this.customerGatewayId, 105 | tags: [ 106 | { 107 | key: "Name", 108 | value: `${this.name}`, 109 | }, 110 | ], 111 | type: "ipsec.1", 112 | transitGatewayId: props.tgw.attrId, 113 | }; 114 | let vpnProps: ec2.CfnVPNConnectionProps = { 115 | ...vpnPropsBase, 116 | }; 117 | 118 | // If tunnel options exist, we will need to add them to our props 119 | if (props.tunnelOneOptions || props.tunnelTwoOptions) { 120 | vpnProps = { 121 | ...vpnPropsBase, 122 | vpnTunnelOptionsSpecifications: [ 123 | this.buildTunnelOptions(props.tunnelOneOptions), 124 | this.buildTunnelOptions(props.tunnelTwoOptions), 125 | ], 126 | }; 127 | } 128 | 129 | this.vpn = new ec2.CfnVPNConnection(this, "VpnConnection", vpnProps); 130 | 131 | const findVpnTgwAttachRequest: ICustomResourceTGWFindVpnAttach = { 132 | transitGatewayId: props.tgw.attrId, 133 | vpnId: this.vpn.ref, 134 | }; 135 | 136 | const transitGatewayAttachId = new cdk.CustomResource( 137 | this, 138 | "FindVpnTgwAttachId", 139 | { 140 | properties: findVpnTgwAttachRequest, 141 | serviceToken: this.findVpnTgwAttachCR.serviceToken, 142 | } 143 | ); 144 | 145 | this.tgwAttachment = { 146 | attrId: transitGatewayAttachId.getAttString("transitGatewayAttachId"), 147 | }; 148 | } 149 | } 150 | 151 | createNewCustomerGateway() { 152 | if ( 153 | !this.props.newCustomerGatewayIpAddress && 154 | !this.props.newCustomerGatewayAsn 155 | ) { 156 | throw new Error( 157 | "No existingCustomerGatewayId provided. Creating a new one requires newCustomerGatewayIpAddress and newCustomerGatewayAsn to be set" 158 | ); 159 | } 160 | let customerGatewayName = `${this.name}-customer-gateway`; 161 | if (this.props.newCustomerGatewayName) { 162 | customerGatewayName = `${this.props.newCustomerGatewayName}-customer-gateway`; 163 | } 164 | this.customerGateway = new ec2.CfnCustomerGateway( 165 | this, 166 | "VpnCustomerGateway", 167 | { 168 | bgpAsn: this.props.newCustomerGatewayAsn!, 169 | ipAddress: this.props.newCustomerGatewayIpAddress!, 170 | type: "ipsec.1", 171 | tags: [ 172 | { 173 | key: "Name", 174 | value: customerGatewayName, 175 | }, 176 | ], 177 | } 178 | ); 179 | this.customerGatewayId = this.customerGateway.ref; 180 | } 181 | 182 | buildTunnelOptions(tunnelConfiguration: IConfigVpnTunnelOptions | undefined) { 183 | let tunnelOptions: VpnTunnelOptionsSpecificationProperty = { 184 | // Don't do this unless we can get it from a secret 185 | preSharedKey: undefined, 186 | tunnelInsideCidr: undefined, 187 | }; 188 | 189 | // If we have specifics for this tunnel we will configure them 190 | if (tunnelConfiguration) { 191 | // NOTE That although you're able to specify the PSK for the tunnel in cloudformation it (as of today) 192 | // does NOT support using a 'secure string lookup' which is the only safe way to do this. So we won't offer the option. 193 | // A PSK in plain-text in a CloudFormation template doesn't sit well. Perhaps the whole VPN connection needs to be a 194 | // custom resource? 195 | if (tunnelConfiguration.tunnelInsideCidr) { 196 | tunnelOptions.tunnelInsideCidr = tunnelConfiguration.tunnelInsideCidr; 197 | } 198 | } 199 | 200 | return tunnelOptions; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-vpc-builder-cdk", 3 | "version": "0.2.5", 4 | "bin": { 5 | "vpc-builder": "bin/vpc-builder.js" 6 | }, 7 | "scripts": { 8 | "discoverEndpoints": "tsc tools/discoverEndpoints/index.ts; node tools/discoverEndpoints/index.js", 9 | "build": "tsc", 10 | "watch": "tsc -w", 11 | "test": "jest --transformIgnorePatterns", 12 | "cdk": "cdk" 13 | }, 14 | "devDependencies": { 15 | "@types/jest": "^29.5.14", 16 | "@types/node": "20.11.30", 17 | "aws-cdk": "2.178.1", 18 | "esbuild": "^0.24.2", 19 | "jest": "^29.7.0", 20 | "prettier": "^3.4.2", 21 | "ts-jest": "^29.2.5", 22 | "ts-node": "^10.9.2", 23 | "typescript": "~5.6.3", 24 | "typescript-json-schema": "^0.65.1" 25 | }, 26 | "dependencies": { 27 | "@aws-cdk/region-info": "2.178.1", 28 | "@aws-sdk/client-ec2": "^3.744.0", 29 | "@types/aws-lambda": "^8.10.147", 30 | "ajv": "^8.17.1", 31 | "aws-cdk-lib": "2.178.1", 32 | "constructs": "^10.0.0", 33 | "ip-cidr": "^3.1.0", 34 | "md5": "^2.3.0", 35 | "source-map-support": "^0.5.21", 36 | "yaml": "^2.7.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/direct-connect-gateway-stack.test.ts: -------------------------------------------------------------------------------- 1 | import { Template } from "aws-cdk-lib/assertions"; 2 | import { newDxGwStack } from "./stack-builder-helper"; 3 | import * as cdk from "aws-cdk-lib"; 4 | 5 | // This stack is just a placeholder for SSM parameters to support our Transit Gateway Route creation etc. 6 | // Confirm our SSM parameters are created and the correct path/value. 7 | test("SsmParametersCreated", () => { 8 | const app = new cdk.App(); 9 | const dxStack = newDxGwStack({}, app) 10 | dxStack.saveTgwRouteInformation(); 11 | dxStack.attachToTGW(); 12 | dxStack.createSsmParameters(); 13 | const template = Template.fromStack(dxStack); 14 | 15 | // We expect SSM Exports that our stacks above can consume: 16 | template.hasResourceProperties("AWS::SSM::Parameter", { 17 | Name: "/ssm/prefix/networking/globalprefix/dxgw/test-dxgw/tgwRouteId", 18 | Value: "tgw-rtb-12345", 19 | }); 20 | template.hasResourceProperties("AWS::SSM::Parameter", { 21 | Name: "/ssm/prefix/networking/globalprefix/dxgw/test-dxgw/tgwAttachId", 22 | Value: "tgw-attach-12345", 23 | }); 24 | }) 25 | -------------------------------------------------------------------------------- /test/dns-route53-private-hosted-zones-stack.test.ts: -------------------------------------------------------------------------------- 1 | import { Template, Match } from "aws-cdk-lib/assertions"; 2 | 3 | import { DnsRoute53PrivateHostedZonesClass } from "../lib/dns-route53-private-hosted-zones-stack"; 4 | import * as cdk from "aws-cdk-lib"; 5 | import { IVpcWorkloadProps } from "../lib/types"; 6 | import { VpcWorkloadIsolatedStack } from "../lib/vpc-workload-isolated-stack"; 7 | import { TransitGatewayStack } from "../lib/transit-gateway-stack"; 8 | 9 | const newTransitGateway = (app: cdk.App) => { 10 | return new TransitGatewayStack(app, "TransitGatewayStack", { 11 | tgwDescription: "Test Transit Gateway", 12 | namePrefix: "Testing", 13 | }); 14 | }; 15 | 16 | const newVpcWorkloadIsolatedStack = ( 17 | props: Partial, 18 | app: cdk.App 19 | ) => { 20 | const transitGatewayStack = newTransitGateway(app); 21 | const commonProps: IVpcWorkloadProps = { 22 | globalPrefix: "globalPrefix", 23 | ssmParameterPrefix: "/ssm/prefix", 24 | namePrefix: "Test", 25 | vpcCidr: "10.1.0.0/16", 26 | availabilityZones: ["us-east-1a", "us-east-1b"], 27 | withTgw: true, 28 | tgw: transitGatewayStack.tgw, 29 | createSubnets: [ 30 | { 31 | name: "testing", 32 | cidrMask: 21, 33 | }, 34 | ], 35 | ...props, 36 | }; 37 | 38 | return new VpcWorkloadIsolatedStack( 39 | app, 40 | "VpcWorkloadIsolatedStack", 41 | commonProps 42 | ); 43 | }; 44 | 45 | test("DnsPrivateHostedZonesBase", () => { 46 | const app = new cdk.App(); 47 | const dnsStack = new DnsRoute53PrivateHostedZonesClass(app, "DnsStack", { 48 | namePrefix: "testing", 49 | dnsEntries: { 50 | domains: ["amclean.org"], 51 | }, 52 | }); 53 | 54 | const template = Template.fromStack(dnsStack); 55 | // Hosted zone for amclean.org with no VPCs attached 56 | template.hasResourceProperties("AWS::Route53::HostedZone", { 57 | Name: "amclean.org", 58 | VPCs: [], 59 | }); 60 | }); 61 | 62 | test("DnsPrivateHostedZonesImportVpcs", () => { 63 | const app = new cdk.App(); 64 | const dnsStack = new DnsRoute53PrivateHostedZonesClass(app, "DnsStack", { 65 | namePrefix: "testing", 66 | dnsEntries: { 67 | domains: ["amclean.org"], 68 | shareWithExistingVpcs: [ 69 | { 70 | vpcId: "vpc-1234", 71 | vpcRegion: "us-east-2", 72 | }, 73 | ], 74 | }, 75 | }); 76 | 77 | const template = Template.fromStack(dnsStack); 78 | // VPCs now with ID and Region 79 | template.hasResourceProperties("AWS::Route53::HostedZone", { 80 | Name: "amclean.org", 81 | VPCs: [ 82 | { 83 | VPCId: "vpc-1234", 84 | VPCRegion: "us-east-2", 85 | }, 86 | ], 87 | }); 88 | }); 89 | 90 | test("DnsPrivateHostedZonesSameStackVpcs", () => { 91 | const app = new cdk.App(); 92 | const workloadStack = newVpcWorkloadIsolatedStack({}, app); 93 | workloadStack.saveTgwRouteInformation(); 94 | workloadStack.attachToTGW(); 95 | workloadStack.createSsmParameters(); 96 | 97 | const dnsStack = new DnsRoute53PrivateHostedZonesClass(app, "DnsStack", { 98 | namePrefix: "testing", 99 | dnsEntries: { 100 | domains: ["amclean.org"], 101 | shareWithVpcs: [workloadStack], 102 | }, 103 | }); 104 | 105 | const template = Template.fromStack(dnsStack); 106 | // VPC is an import reference to our dependant stack (so CDK can order appropriately) 107 | template.hasResourceProperties("AWS::Route53::HostedZone", { 108 | Name: "amclean.org", 109 | VPCs: [ 110 | { 111 | VPCId: Match.objectLike({ "Fn::ImportValue": Match.anyValue() }), 112 | VPCRegion: Match.objectLike({ Ref: "AWS::Region" }), 113 | }, 114 | ], 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /test/stack-builder-helper.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IVpcWorkloadProps, 3 | ITgw, 4 | } from "../lib/types"; 5 | import { TransitGatewayStack } from "../lib/transit-gateway-stack"; 6 | import { 7 | IVpcInterfaceEndpointsProps, 8 | VpcInterfaceEndpointsStack, 9 | } from "../lib/vpc-interface-endpoints-stack"; 10 | import { 11 | VpcRoute53ResolverEndpointsStack, 12 | IVpcRoute53ResolverEndpointsProps, 13 | } from "../lib/vpc-route53-resolver-endpoints-stack"; 14 | import { 15 | IVpcNatEgressProps, 16 | VpcNatEgressStack, 17 | } from "../lib/vpc-nat-egress-stack"; 18 | import { 19 | IVpcAwsNetworkFirewallProps, 20 | VpcAwsNetworkFirewallStack, 21 | } from "../lib/vpc-aws-network-firewall-stack"; 22 | import { VpcWorkloadIsolatedStack } from "../lib/vpc-workload-isolated-stack"; 23 | import { VpcWorkloadPublicStack } from "../lib/vpc-workload-public-stack"; 24 | import { 25 | IVpnToTransitGatewayProps, 26 | VpnToTransitGatewayStack, 27 | } from "../lib/vpn-to-transit-gateway-stack"; 28 | import { 29 | IDirectConnectGatewayProps, 30 | DirectConnectGatewayStack, 31 | } from "../lib/direct-connect-gateway-stack"; 32 | import * as cdk from "aws-cdk-lib"; 33 | import * as ec2 from "aws-cdk-lib/aws-ec2"; 34 | import * as fs from "fs"; 35 | import * as path from "path"; 36 | import {ITransitGatewayPeerProps, TransitGatewayPeerStack} from "../lib/transit-gateway-peer-stack"; 37 | 38 | const interfaceDiscovery = JSON.parse( 39 | fs.readFileSync(path.join("discovery", `endpoints-us-east-1.json`), { 40 | encoding: "utf8", 41 | }) 42 | ); 43 | 44 | export const newTransitGateway = (app: cdk.App) => { 45 | return new TransitGatewayStack(app, "TransitGatewayStack", { 46 | tgwDescription: "Test Transit Gateway", 47 | namePrefix: "Testing", 48 | }); 49 | }; 50 | 51 | export const newVpcWorkloadStack = ( 52 | props: Partial, 53 | app: cdk.App, 54 | style: "workloadIsolated" | "workloadPublic", 55 | tgw?: ec2.CfnTransitGateway | ITgw 56 | ) => { 57 | let transitGateway = tgw; 58 | if (!transitGateway) { 59 | const transitGatewayStack = newTransitGateway(app); 60 | transitGateway = transitGatewayStack.tgw; 61 | } 62 | const commonProps: IVpcWorkloadProps = { 63 | globalPrefix: "globalPrefix", 64 | ssmParameterPrefix: "/ssm/prefix", 65 | namePrefix: "Test", 66 | vpcCidr: "10.1.0.0/16", 67 | availabilityZones: ["us-east-1a", "us-east-1b"], 68 | withTgw: true, 69 | tgw: transitGateway, 70 | createSubnets: [ 71 | { 72 | name: "testing", 73 | cidrMask: 21, 74 | }, 75 | ], 76 | ...props, 77 | }; 78 | 79 | if (style == "workloadIsolated") { 80 | return new VpcWorkloadIsolatedStack( 81 | app, 82 | `${props.namePrefix}VpcWorkloadIsolatedStack`, 83 | commonProps 84 | ); 85 | } else { 86 | return new VpcWorkloadPublicStack( 87 | app, 88 | `${props.namePrefix}VpcWorkloadPublicStack`, 89 | commonProps 90 | ); 91 | } 92 | }; 93 | 94 | export const newNatEgressStack = ( 95 | props: Partial, 96 | app: cdk.App, 97 | tgw?: ec2.CfnTransitGateway | ITgw 98 | ) => { 99 | let transitGateway = tgw; 100 | if (!transitGateway) { 101 | const transitGatewayStack = newTransitGateway(app); 102 | transitGateway = transitGatewayStack.tgw; 103 | } 104 | 105 | const commonProps: IVpcNatEgressProps = { 106 | globalPrefix: "globalPrefix", 107 | ssmParameterPrefix: "/ssm/prefix", 108 | namePrefix: "Test", 109 | vpcCidr: "10.2.0.0/16", 110 | availabilityZones: ["us-east-1a", "us-east-1b"], 111 | withTgw: true, 112 | tgw: transitGateway, 113 | ...props, 114 | }; 115 | 116 | return new VpcNatEgressStack( 117 | app, 118 | `${props.namePrefix}VpcNatEgressStack`, 119 | commonProps 120 | ); 121 | }; 122 | 123 | export const newVpcInterfaceEndpointsStack = ( 124 | props: Partial, 125 | app: cdk.App, 126 | interfaceList: Array, 127 | tgw?: ec2.CfnTransitGateway | ITgw 128 | ) => { 129 | let transitGateway = tgw; 130 | if (!transitGateway) { 131 | const transitGatewayStack = newTransitGateway(app); 132 | transitGateway = transitGatewayStack.tgw; 133 | } 134 | const commonProps: IVpcInterfaceEndpointsProps = { 135 | globalPrefix: "globalPrefix", 136 | ssmParameterPrefix: "/ssm/prefix", 137 | namePrefix: "Test", 138 | vpcCidr: "10.3.0.0/16", 139 | availabilityZones: ["us-east-1a", "us-east-1b"], 140 | interfaceList: interfaceList, 141 | interfaceDiscovery: interfaceDiscovery, 142 | withTgw: true, 143 | tgw: transitGateway, 144 | ...props, 145 | }; 146 | 147 | return new VpcInterfaceEndpointsStack( 148 | app, 149 | `${props.namePrefix}VpcInterfaceEndpointsStack`, 150 | commonProps 151 | ); 152 | }; 153 | 154 | export const newAwsNetworkFirewallStack = ( 155 | props: Partial, 156 | app: cdk.App, 157 | tgw?: ec2.CfnTransitGateway | ITgw 158 | ) => { 159 | let transitGateway = tgw; 160 | if (!transitGateway) { 161 | const transitGatewayStack = newTransitGateway(app); 162 | transitGateway = transitGatewayStack.tgw; 163 | } 164 | 165 | const commonProps: IVpcAwsNetworkFirewallProps = { 166 | globalPrefix: "globalPrefix", 167 | ssmParameterPrefix: "/ssm/prefix", 168 | namePrefix: "Test", 169 | vpcCidr: "10.4.0.0/16", 170 | availabilityZones: ["us-east-1a", "us-east-1b"], 171 | withTgw: true, 172 | tgw: transitGateway, 173 | firewallName: "FirewallName", 174 | firewallDescription: "Firewall Description", 175 | ...props, 176 | }; 177 | 178 | return new VpcAwsNetworkFirewallStack( 179 | app, 180 | `${props.namePrefix}VpcAwsNetworkFirewallStack`, 181 | commonProps 182 | ); 183 | }; 184 | 185 | export const newVpnStack = ( 186 | props: Partial, 187 | app: cdk.App, 188 | tgw?: ITgw 189 | ) => { 190 | let transitGateway = tgw; 191 | if (!transitGateway) { 192 | const transitGatewayStack = newTransitGateway(app); 193 | transitGateway = transitGatewayStack.tgw; 194 | } 195 | 196 | const commonProps: IVpnToTransitGatewayProps = { 197 | globalPrefix: "globalPrefix", 198 | ssmParameterPrefix: "/ssm/prefix", 199 | namePrefix: "Test", 200 | withTgw: true, 201 | tgw: transitGateway, 202 | ...props, 203 | }; 204 | 205 | return new VpnToTransitGatewayStack( 206 | app, 207 | `${props.namePrefix}VpnToTransitGatewayStack`, 208 | commonProps 209 | ); 210 | }; 211 | 212 | export const newDxGwStack = ( 213 | props: Partial, 214 | app: cdk.App, 215 | ) => { 216 | const commonProps: IDirectConnectGatewayProps = { 217 | globalPrefix: "globalPrefix", 218 | ssmParameterPrefix: "/ssm/prefix", 219 | namePrefix: "Test", 220 | existingDxGwTransitGatewayAttachId: "tgw-attach-12345", 221 | existingDxGwTransitGatewayRouteTableId: "tgw-rtb-12345", 222 | existingTransitGatewayId: "tgw-12345", 223 | ...props, 224 | }; 225 | 226 | return new DirectConnectGatewayStack( 227 | app, 228 | `${props.namePrefix}DirectConnectGatewayStack`, 229 | commonProps 230 | ); 231 | }; 232 | 233 | export const newTgwPeerStack = ( 234 | props: Partial, 235 | app: cdk.App, 236 | ) => { 237 | const commonProps: ITransitGatewayPeerProps = { 238 | globalPrefix: "globalPrefix", 239 | ssmParameterPrefix: "/ssm/prefix", 240 | namePrefix: "Test", 241 | existingPeerTransitGatewayAttachId: "tgw-attach-678910", 242 | existingPeerTransitGatewayRouteTableId: "tgw-rtb-678910", 243 | existingTransitGatewayId: "tgw-678910", 244 | ...props, 245 | }; 246 | 247 | return new TransitGatewayPeerStack( 248 | app, 249 | `${props.namePrefix}TransitGatewayPeerStack`, 250 | commonProps 251 | ); 252 | }; 253 | 254 | export const newVpcRoute53ResolverStack = ( 255 | props: Partial, 256 | app: cdk.App, 257 | tgw?: ec2.CfnTransitGateway | ITgw 258 | ) => { 259 | let transitGateway = tgw; 260 | if (!transitGateway) { 261 | const transitGatewayStack = newTransitGateway(app); 262 | transitGateway = transitGatewayStack.tgw; 263 | } 264 | const commonProps: IVpcRoute53ResolverEndpointsProps = { 265 | globalPrefix: "globalPrefix", 266 | ssmParameterPrefix: "/ssm/prefix", 267 | namePrefix: "Test", 268 | vpcCidr: "10.5.0.0/16", 269 | availabilityZones: ["us-east-1a", "us-east-1b"], 270 | withTgw: true, 271 | tgw: transitGateway, 272 | ...props, 273 | }; 274 | 275 | return new VpcRoute53ResolverEndpointsStack( 276 | app, 277 | `${props.namePrefix}VpcInterfaceEndpointsStack`, 278 | commonProps 279 | ); 280 | }; 281 | -------------------------------------------------------------------------------- /test/transit-gateway-peer-stack.test.ts: -------------------------------------------------------------------------------- 1 | import { Template } from "aws-cdk-lib/assertions"; 2 | import { newTgwPeerStack } from "./stack-builder-helper"; 3 | import * as cdk from "aws-cdk-lib"; 4 | 5 | // This stack is just a placeholder for SSM parameters to support our Transit Gateway Route creation etc. 6 | // Confirm our SSM parameters are created and the correct path/value. 7 | test("SsmParametersCreated", () => { 8 | const app = new cdk.App(); 9 | const dxStack = newTgwPeerStack({}, app) 10 | dxStack.saveTgwRouteInformation(); 11 | dxStack.attachToTGW(); 12 | dxStack.createSsmParameters(); 13 | const template = Template.fromStack(dxStack); 14 | 15 | // We expect SSM Exports that our stacks above can consume: 16 | template.hasResourceProperties("AWS::SSM::Parameter", { 17 | Name: "/ssm/prefix/networking/globalprefix/tgwpeer/test-tgwpeer/tgwRouteId", 18 | Value: "tgw-rtb-678910", 19 | }); 20 | template.hasResourceProperties("AWS::SSM::Parameter", { 21 | Name: "/ssm/prefix/networking/globalprefix/tgwpeer/test-tgwpeer/tgwAttachId", 22 | Value: "tgw-attach-678910", 23 | }); 24 | }) 25 | -------------------------------------------------------------------------------- /test/transit-gateway-stack.test.ts: -------------------------------------------------------------------------------- 1 | import { Template } from "aws-cdk-lib/assertions"; 2 | 3 | import { 4 | TransitGatewayStack, 5 | ITransitGatewayProps, 6 | } from "../lib/transit-gateway-stack"; 7 | import * as cdk from "aws-cdk-lib"; 8 | 9 | const newTransitStack = (props: ITransitGatewayProps) => { 10 | const app = new cdk.App(); 11 | return new TransitGatewayStack(app, "TransitGatewayStack", props); 12 | }; 13 | 14 | test("BaseNoOptionalParams", () => { 15 | const transitStack = newTransitStack({ 16 | namePrefix: "string", 17 | tgwDescription: "this is a description", 18 | }); 19 | const template = Template.fromStack(transitStack); 20 | // One Transit Gateway resource 21 | template.resourceCountIs("AWS::EC2::TransitGateway", 1); 22 | // Default ASN we've embedded in our template 23 | template.findResources("AWS::EC2::TransitGateway", { 24 | AmazonSideAsn: 65521, 25 | Description: "this is a description", 26 | }); 27 | }); 28 | 29 | // We specify an ASN and it is accepted 30 | test("SpecifyOurAsn", () => { 31 | const transitStack = newTransitStack({ 32 | namePrefix: "string", 33 | tgwDescription: "this is a description", 34 | amazonSideAsn: 64000, 35 | }); 36 | const template = Template.fromStack(transitStack); 37 | template.findResources("AWS::EC2::TransitGateway", { 38 | AmazonSideAsn: 64000, 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/vpc-aws-network-firewall-stack.test.ts: -------------------------------------------------------------------------------- 1 | import { Template } from "aws-cdk-lib/assertions"; 2 | import { newAwsNetworkFirewallStack } from "./stack-builder-helper"; 3 | import * as cdk from "aws-cdk-lib"; 4 | import { ITgw } from "../lib/types"; 5 | 6 | test("NetworkFirewallBase", () => { 7 | for (const transitStyle of ["stack", "imported"]) { 8 | const app = new cdk.App(); 9 | let ITgwedId: ITgw | undefined = undefined; 10 | if (transitStyle == "imported") { 11 | ITgwedId = { 12 | attrId: "tgw-12392488", 13 | }; 14 | } 15 | const awsFirewall = newAwsNetworkFirewallStack({}, app, ITgwedId); 16 | awsFirewall.saveTgwRouteInformation(); 17 | awsFirewall.attachToTGW(); 18 | awsFirewall.createSsmParameters(); 19 | const template = Template.fromStack(awsFirewall); 20 | // We've provided 2 AZ so expect to see 4 subnets. 2 for Transit, and 2 our firewall 21 | template.resourceCountIs("AWS::EC2::Subnet", 4); 22 | 23 | // Public subnets do not exist 24 | expect(awsFirewall.publicSubnetNames).toEqual([]); 25 | // Private subnet (NATed) do not exist 26 | expect(awsFirewall.privateSubnetNames).toEqual([]); 27 | // Two private isolated subnets, one for firewall services and another for the transit gateway 28 | expect(awsFirewall.privateIsolatedSubnetNames).toEqual([ 29 | "firewall-services", 30 | "transit-gateway", 31 | ]); 32 | 33 | // We expect NAT, and IGW resources do Not exist 34 | expect(() => template.hasResource("AWS::EC2::NatGateway", {})).toThrow(); 35 | expect(() => 36 | template.hasResource("AWS::EC2::InternetGateway", {}) 37 | ).toThrow(); 38 | 39 | // We expect to have associated to the Transit Gateway and Created a Route Table 40 | template.resourceCountIs( 41 | "AWS::EC2::TransitGatewayRouteTableAssociation", 42 | 1 43 | ); 44 | template.resourceCountIs("AWS::EC2::TransitGatewayRouteTable", 1); 45 | template.resourceCountIs( 46 | "AWS::EC2::TransitGatewayRouteTableAssociation", 47 | 1 48 | ); 49 | 50 | // We expect SSM named exports within our construct are prepared with Transit Route Table, and Association. 51 | expect(awsFirewall.tgwAttachmentSsm.name).toEqual( 52 | "/ssm/prefix/networking/globalprefix/vpcs/test-provider-firewall/tgwId" 53 | ); 54 | expect(awsFirewall.tgwRouteTableSsm.name).toEqual( 55 | "/ssm/prefix/networking/globalprefix/vpcs/test-provider-firewall/tgwRouteId" 56 | ); 57 | 58 | const prefix = "/ssm/prefix/networking/globalprefix"; 59 | for (const parameterName of [ 60 | `${prefix}/vpcs/test-provider-firewall/vpcId`, 61 | `${prefix}/vpcs/test-provider-firewall/vpcCidr`, 62 | `${prefix}/vpcs/test-provider-firewall/az0`, 63 | `${prefix}/vpcs/test-provider-firewall/az1`, 64 | `${prefix}/vpcs/test-provider-firewall/subnets/firewall-services/us-east-1a/routeTableId`, 65 | `${prefix}/vpcs/test-provider-firewall/subnets/firewall-services/us-east-1a/subnetCidr`, 66 | `${prefix}/vpcs/test-provider-firewall/subnets/firewall-services/us-east-1a/subnetId`, 67 | `${prefix}/vpcs/test-provider-firewall/subnets/transit-gateway/us-east-1a/routeTableId`, 68 | `${prefix}/vpcs/test-provider-firewall/subnets/transit-gateway/us-east-1a/subnetCidr`, 69 | `${prefix}/vpcs/test-provider-firewall/subnets/transit-gateway/us-east-1a/subnetId`, 70 | `${prefix}/vpcs/test-provider-firewall/subnets/firewall-services/us-east-1b/routeTableId`, 71 | `${prefix}/vpcs/test-provider-firewall/subnets/firewall-services/us-east-1b/subnetCidr`, 72 | `${prefix}/vpcs/test-provider-firewall/subnets/firewall-services/us-east-1b/subnetId`, 73 | `${prefix}/vpcs/test-provider-firewall/subnets/transit-gateway/us-east-1b/routeTableId`, 74 | `${prefix}/vpcs/test-provider-firewall/subnets/transit-gateway/us-east-1b/subnetCidr`, 75 | `${prefix}/vpcs/test-provider-firewall/subnets/transit-gateway/us-east-1b/subnetId`, 76 | `${prefix}/vpcs/test-provider-firewall/tgwAttachId`, 77 | `${prefix}/vpcs/test-provider-firewall/tgwRouteId`, 78 | `${prefix}/vpcs/test-provider-firewall/tgwId`, 79 | ]) { 80 | template.hasResourceProperties("AWS::SSM::Parameter", { 81 | Name: parameterName, 82 | }); 83 | } 84 | } 85 | }); 86 | -------------------------------------------------------------------------------- /test/vpc-interface-endpoints-stack.test.ts: -------------------------------------------------------------------------------- 1 | import { Template, Match, Capture } from "aws-cdk-lib/assertions"; 2 | import { 3 | newVpcInterfaceEndpointsStack, 4 | newVpcWorkloadStack, 5 | } from "./stack-builder-helper"; 6 | import { ITgw } from "../lib/types"; 7 | import * as cdk from "aws-cdk-lib"; 8 | 9 | const interfaceList = [ 10 | "com.amazonaws.us-east-1.ec2", 11 | "com.amazonaws.us-east-1.ec2messages", 12 | "com.amazonaws.us-east-1.ssm", 13 | "com.amazonaws.us-east-1.ssmmessages", 14 | "com.amazonaws.us-east-1.kms", 15 | ]; 16 | 17 | test("InterfaceEndpointsBase", () => { 18 | for (const transitStyle of ["stack", "imported"]) { 19 | const app = new cdk.App(); 20 | let ITgwedId: ITgw | undefined = undefined; 21 | if (transitStyle == "imported") { 22 | ITgwedId = { 23 | attrId: "tgw-12392488", 24 | }; 25 | } 26 | const interfaceEndpoints = newVpcInterfaceEndpointsStack( 27 | {}, 28 | app, 29 | interfaceList, 30 | ITgwedId 31 | ); 32 | interfaceEndpoints.saveTgwRouteInformation(); 33 | interfaceEndpoints.attachToTGW(); 34 | interfaceEndpoints.createSsmParameters(); 35 | const template = Template.fromStack(interfaceEndpoints); 36 | // We've provided 2 AZ so expect to see 4 subnets. 2 for Transit, and 2 for hosting our endpoints 37 | template.resourceCountIs("AWS::EC2::Subnet", 4); 38 | 39 | // Public subnets do not exist 40 | expect(interfaceEndpoints.publicSubnetNames).toEqual([]); 41 | // Private subnet (NATed) do not exist 42 | expect(interfaceEndpoints.privateSubnetNames).toEqual([]); 43 | // Two private isolated subnets, one for interface endpoints and another for the transit gateway 44 | expect(interfaceEndpoints.privateIsolatedSubnetNames).toEqual([ 45 | "interface-endpoints", 46 | "transit-gateway", 47 | ]); 48 | 49 | // We expect NAT, and IGW resources do Not exist 50 | expect(() => template.hasResource("AWS::EC2::NatGateway", {})).toThrow(); 51 | expect(() => 52 | template.hasResource("AWS::EC2::InternetGateway", {}) 53 | ).toThrow(); 54 | 55 | // We expect to have associated to the Transit Gateway and Created a Route Table 56 | template.resourceCountIs("AWS::EC2::TransitGatewayVpcAttachment", 1); 57 | template.resourceCountIs( 58 | "AWS::EC2::TransitGatewayRouteTableAssociation", 59 | 1 60 | ); 61 | template.resourceCountIs("AWS::EC2::TransitGatewayRouteTable", 1); 62 | 63 | // We expect route53 private hosted zones for each interface we asked for with our dependency tree in place to rate limit 64 | for (const interfaceName of interfaceList) { 65 | const interfaceDnsName = 66 | interfaceEndpoints.lookupPrivateDnsName(interfaceName); 67 | template.hasResource("AWS::Route53::HostedZone", { 68 | Properties: { 69 | Name: `${interfaceDnsName}.`, 70 | // Shared with a single vpc (self) 71 | VPCs: [Match.anyValue()], 72 | }, 73 | DependsOn: Match.anyValue(), 74 | }); 75 | template.hasResource("AWS::Route53::RecordSet", { 76 | Properties: { Name: `${interfaceDnsName}.` }, 77 | DependsOn: Match.anyValue(), 78 | }); 79 | template.hasResource("AWS::EC2::VPCEndpoint", { 80 | Properties: { 81 | // We're creating the hosted zones ourselves. 82 | PrivateDnsEnabled: false, 83 | // Attached to two subnets 84 | SubnetIds: [Match.anyValue(), Match.anyValue()], 85 | }, 86 | DependsOn: Match.anyValue(), 87 | }); 88 | } 89 | 90 | // We expect SSM named exports within our construct are prepared with Transit Route Table, and Association. 91 | expect(interfaceEndpoints.tgwAttachmentSsm.name).toEqual( 92 | "/ssm/prefix/networking/globalprefix/vpcs/test-provider-endpoint-service-interface/tgwId" 93 | ); 94 | expect(interfaceEndpoints.tgwRouteTableSsm.name).toEqual( 95 | "/ssm/prefix/networking/globalprefix/vpcs/test-provider-endpoint-service-interface/tgwRouteId" 96 | ); 97 | 98 | const prefix = "/ssm/prefix/networking/globalprefix"; 99 | for (const parameterName of [ 100 | `${prefix}/vpcs/test-provider-endpoint-service-interface/vpcId`, 101 | `${prefix}/vpcs/test-provider-endpoint-service-interface/vpcCidr`, 102 | `${prefix}/vpcs/test-provider-endpoint-service-interface/az0`, 103 | `${prefix}/vpcs/test-provider-endpoint-service-interface/az1`, 104 | `${prefix}/vpcs/test-provider-endpoint-service-interface/subnets/interface-endpoints/us-east-1a/routeTableId`, 105 | `${prefix}/vpcs/test-provider-endpoint-service-interface/subnets/interface-endpoints/us-east-1a/subnetCidr`, 106 | `${prefix}/vpcs/test-provider-endpoint-service-interface/subnets/interface-endpoints/us-east-1a/subnetId`, 107 | `${prefix}/vpcs/test-provider-endpoint-service-interface/subnets/transit-gateway/us-east-1a/routeTableId`, 108 | `${prefix}/vpcs/test-provider-endpoint-service-interface/subnets/transit-gateway/us-east-1a/subnetCidr`, 109 | `${prefix}/vpcs/test-provider-endpoint-service-interface/subnets/transit-gateway/us-east-1a/subnetId`, 110 | `${prefix}/vpcs/test-provider-endpoint-service-interface/subnets/interface-endpoints/us-east-1b/routeTableId`, 111 | `${prefix}/vpcs/test-provider-endpoint-service-interface/subnets/interface-endpoints/us-east-1b/subnetCidr`, 112 | `${prefix}/vpcs/test-provider-endpoint-service-interface/subnets/interface-endpoints/us-east-1b/subnetId`, 113 | `${prefix}/vpcs/test-provider-endpoint-service-interface/subnets/transit-gateway/us-east-1b/routeTableId`, 114 | `${prefix}/vpcs/test-provider-endpoint-service-interface/subnets/transit-gateway/us-east-1b/subnetCidr`, 115 | `${prefix}/vpcs/test-provider-endpoint-service-interface/subnets/transit-gateway/us-east-1b/subnetId`, 116 | `${prefix}/vpcs/test-provider-endpoint-service-interface/tgwAttachId`, 117 | `${prefix}/vpcs/test-provider-endpoint-service-interface/tgwRouteId`, 118 | `${prefix}/vpcs/test-provider-endpoint-service-interface/tgwId`, 119 | ]) { 120 | template.hasResourceProperties("AWS::SSM::Parameter", { 121 | Name: parameterName, 122 | }); 123 | } 124 | } 125 | }); 126 | 127 | test("InterfaceEndpointSpecifyCidrMask", () => { 128 | for (const transitStyle of ["stack", "imported"]) { 129 | const app = new cdk.App(); 130 | let ITgwedId: ITgw | undefined = undefined; 131 | if (transitStyle == "imported") { 132 | ITgwedId = { 133 | attrId: "tgw-12392488", 134 | }; 135 | } 136 | const interfaceEndpoints = newVpcInterfaceEndpointsStack( 137 | { 138 | perSubnetCidrMask: 26, 139 | }, 140 | app, 141 | interfaceList, 142 | ITgwedId 143 | ); 144 | interfaceEndpoints.saveTgwRouteInformation(); 145 | interfaceEndpoints.attachToTGW(); 146 | interfaceEndpoints.createSsmParameters(); 147 | const template = Template.fromStack(interfaceEndpoints); 148 | 149 | // Subnets for InterfaceEndpoints should be /26 as specified 150 | const interfaceSubnetCapture = new Capture(); 151 | template.hasResourceProperties("AWS::EC2::Subnet", { 152 | CidrBlock: interfaceSubnetCapture, 153 | Tags: Match.arrayWith([ 154 | { 155 | Key: "aws-cdk:subnet-name", 156 | Value: "interface-endpoints", 157 | }, 158 | ]), 159 | }); 160 | expect(interfaceSubnetCapture.asString().split("/")[1]).toEqual("26"); 161 | 162 | // Subnets for TransitGateway should remain /28 163 | const transitGatewaySubnetCapture = new Capture(); 164 | template.hasResourceProperties("AWS::EC2::Subnet", { 165 | CidrBlock: transitGatewaySubnetCapture, 166 | Tags: Match.arrayWith([ 167 | { 168 | Key: "aws-cdk:subnet-name", 169 | Value: "transit-gateway", 170 | }, 171 | ]), 172 | }); 173 | expect(transitGatewaySubnetCapture.asString().split("/")[1]).toEqual("28"); 174 | } 175 | }); 176 | 177 | test("InterfaceEndpointSharedWithVpc", () => { 178 | for (const transitStyle of ["stack", "imported"]) { 179 | const app = new cdk.App(); 180 | let ITgwedId: ITgw | undefined = undefined; 181 | if (transitStyle == "imported") { 182 | ITgwedId = { 183 | attrId: "tgw-12392488", 184 | }; 185 | } 186 | // we will need a candidate workload VPC to share with. Let's initialize one simply 187 | const shareEndpointsWith = newVpcWorkloadStack( 188 | {}, 189 | app, 190 | "workloadIsolated", 191 | ITgwedId 192 | ); 193 | shareEndpointsWith.saveTgwRouteInformation(); 194 | shareEndpointsWith.attachToTGW(); 195 | shareEndpointsWith.createSsmParameters(); 196 | const interfaceEndpoints = newVpcInterfaceEndpointsStack( 197 | { 198 | interfaceEndpointSharedWithVpcs: [{ attachTo: shareEndpointsWith }], 199 | }, 200 | app, 201 | interfaceList, 202 | shareEndpointsWith.tgw 203 | ); 204 | interfaceEndpoints.saveTgwRouteInformation(); 205 | interfaceEndpoints.attachToTGW(); 206 | interfaceEndpoints.createSsmParameters(); 207 | const template = Template.fromStack(interfaceEndpoints); 208 | 209 | // Our Private hosted zone should now reflect two VPC attachments to serve (self and shared) 210 | for (const interfaceName of interfaceList) { 211 | const interfaceDnsName = 212 | interfaceEndpoints.lookupPrivateDnsName(interfaceName); 213 | template.hasResource("AWS::Route53::HostedZone", { 214 | Properties: { 215 | Name: `${interfaceDnsName}.`, 216 | // Shared with Two VPCs now (self plus our one provided to constructor) 217 | VPCs: [Match.anyValue(), Match.anyValue()], 218 | }, 219 | DependsOn: Match.anyValue(), 220 | }); 221 | } 222 | } 223 | }); 224 | 225 | test("InterfaceEndpointsNotInAllAZs", () => { 226 | for (const transitStyle of ["stack", "imported"]) { 227 | const app = new cdk.App(); 228 | let ITgwedId: ITgw | undefined = undefined; 229 | if (transitStyle == "imported") { 230 | ITgwedId = { 231 | attrId: "tgw-12392488", 232 | }; 233 | } 234 | // We will add an AZ that we know doesn't exist expecting an error that we don't have coverage in that AZ for the endpoints we've requested 235 | expect(() => { 236 | const interfaceEndpoints = newVpcInterfaceEndpointsStack( 237 | { 238 | availabilityZones: ["us-east-1a", "us-east-1z"] 239 | }, 240 | app, 241 | interfaceList, 242 | ITgwedId 243 | ); 244 | interfaceEndpoints.saveTgwRouteInformation(); 245 | interfaceEndpoints.attachToTGW(); 246 | interfaceEndpoints.createSsmParameters(); 247 | }).toThrow( 248 | "Endpoint com.amazonaws.us-east-1.ec2 is not available in all Availability Zones: us-east-1a,us-east-1z" 249 | ) 250 | } 251 | }); -------------------------------------------------------------------------------- /test/vpc-nat-egress-stack.test.ts: -------------------------------------------------------------------------------- 1 | import { Template } from "aws-cdk-lib/assertions"; 2 | import { newNatEgressStack } from "./stack-builder-helper"; 3 | import * as cdk from "aws-cdk-lib"; 4 | import { ITgw } from "../lib/types"; 5 | 6 | test("NatEgressStackBase", () => { 7 | for (const transitStyle of ["stack", "imported"]) { 8 | const app = new cdk.App(); 9 | let tgwImportedId: ITgw | undefined = undefined; 10 | if (transitStyle == "imported") { 11 | tgwImportedId = { 12 | attrId: "tgw-12392488", 13 | }; 14 | } 15 | const natEgress = newNatEgressStack( 16 | { 17 | globalPrefix: "globalPrefix", 18 | ssmParameterPrefix: "/ssm/prefix", 19 | namePrefix: "Test", 20 | availabilityZones: ["us-east-1a", "us-east-1b"], 21 | }, 22 | app, 23 | tgwImportedId 24 | ); 25 | natEgress.saveTgwRouteInformation(); 26 | natEgress.attachToTGW(); 27 | natEgress.createSsmParameters(); 28 | const template = Template.fromStack(natEgress); 29 | // We've provided 2 AZ so expect to see 4 subnets. 2 for Transit, and 2 for our other AZs 30 | template.resourceCountIs("AWS::EC2::Subnet", 4); 31 | 32 | // Public subnets exist 33 | expect(natEgress.publicSubnetNames).toEqual(["nat-egress"]); 34 | // Private subnet exists with NAT services attached (by the route stack) to the TGW 35 | expect(natEgress.privateSubnetNames).toEqual(["transit-gateway"]); 36 | // We expect private isolated subnets are empty. 37 | expect(natEgress.privateIsolatedSubnetNames).toEqual([]); 38 | 39 | // We expect NAT, and IGW resources exist (one per AZ) 40 | template.resourceCountIs("AWS::EC2::NatGateway", 2); 41 | template.resourceCountIs("AWS::EC2::InternetGateway", 1); 42 | 43 | // We expect to have associated to the Transit Gateway and Created a Route Table 44 | template.resourceCountIs( 45 | "AWS::EC2::TransitGatewayRouteTableAssociation", 46 | 1 47 | ); 48 | template.resourceCountIs("AWS::EC2::TransitGatewayRouteTable", 1); 49 | template.resourceCountIs( 50 | "AWS::EC2::TransitGatewayRouteTableAssociation", 51 | 1 52 | ); 53 | 54 | // We expect SSM named exports within our construct are prepared with Transit Route Table, and Association. 55 | expect(natEgress.tgwAttachmentSsm.name).toEqual( 56 | "/ssm/prefix/networking/globalprefix/vpcs/test-provider-internet/tgwId" 57 | ); 58 | expect(natEgress.tgwRouteTableSsm.name).toEqual( 59 | "/ssm/prefix/networking/globalprefix/vpcs/test-provider-internet/tgwRouteId" 60 | ); 61 | 62 | const prefix = "/ssm/prefix/networking/globalprefix"; 63 | for (const parameterName of [ 64 | `${prefix}/vpcs/test-provider-internet/vpcId`, 65 | `${prefix}/vpcs/test-provider-internet/vpcCidr`, 66 | `${prefix}/vpcs/test-provider-internet/az0`, 67 | `${prefix}/vpcs/test-provider-internet/az1`, 68 | `${prefix}/vpcs/test-provider-internet/subnets/nat-egress/us-east-1a/routeTableId`, 69 | `${prefix}/vpcs/test-provider-internet/subnets/nat-egress/us-east-1a/subnetCidr`, 70 | `${prefix}/vpcs/test-provider-internet/subnets/nat-egress/us-east-1a/subnetId`, 71 | `${prefix}/vpcs/test-provider-internet/subnets/transit-gateway/us-east-1a/routeTableId`, 72 | `${prefix}/vpcs/test-provider-internet/subnets/transit-gateway/us-east-1a/subnetCidr`, 73 | `${prefix}/vpcs/test-provider-internet/subnets/transit-gateway/us-east-1a/subnetId`, 74 | `${prefix}/vpcs/test-provider-internet/subnets/nat-egress/us-east-1b/routeTableId`, 75 | `${prefix}/vpcs/test-provider-internet/subnets/nat-egress/us-east-1b/subnetCidr`, 76 | `${prefix}/vpcs/test-provider-internet/subnets/nat-egress/us-east-1b/subnetId`, 77 | `${prefix}/vpcs/test-provider-internet/subnets/transit-gateway/us-east-1b/routeTableId`, 78 | `${prefix}/vpcs/test-provider-internet/subnets/transit-gateway/us-east-1b/subnetCidr`, 79 | `${prefix}/vpcs/test-provider-internet/subnets/transit-gateway/us-east-1b/subnetId`, 80 | `${prefix}/vpcs/test-provider-internet/tgwAttachId`, 81 | `${prefix}/vpcs/test-provider-internet/tgwRouteId`, 82 | `${prefix}/vpcs/test-provider-internet/tgwId`, 83 | ]) { 84 | template.hasResourceProperties("AWS::SSM::Parameter", { 85 | Name: parameterName, 86 | }); 87 | } 88 | } 89 | }); 90 | -------------------------------------------------------------------------------- /test/vpc-route53-resolver-endpoints-stack.test.ts: -------------------------------------------------------------------------------- 1 | import { Template, Match } from "aws-cdk-lib/assertions"; 2 | import { 3 | newVpcRoute53ResolverStack, 4 | newVpcWorkloadStack, 5 | } from "./stack-builder-helper"; 6 | import { ITgw } from "../lib/types"; 7 | import * as cdk from "aws-cdk-lib"; 8 | 9 | test("Route53ResolverBase", () => { 10 | for (const transitStyle of ["stack", "imported"]) { 11 | const app = new cdk.App(); 12 | let ITgwedId: ITgw | undefined = undefined; 13 | if (transitStyle == "imported") { 14 | ITgwedId = { 15 | attrId: "tgw-12392488", 16 | }; 17 | } 18 | const resolverEndpoints = newVpcRoute53ResolverStack( 19 | { 20 | resolveRequestsFromCidrs: ["10.0.0.0/8"], 21 | forwardRequests: { 22 | forDomains: ["amclean.org"], 23 | toIps: ["10.10.1.2"], 24 | }, 25 | }, 26 | app, 27 | ITgwedId 28 | ); 29 | resolverEndpoints.saveTgwRouteInformation(); 30 | resolverEndpoints.attachToTGW(); 31 | resolverEndpoints.createSsmParameters(); 32 | const template = Template.fromStack(resolverEndpoints); 33 | // We've provided 2 AZ so expect to see 4 subnets. 2 for Transit, and 2 for hosting our endpoints 34 | template.resourceCountIs("AWS::EC2::Subnet", 4); 35 | 36 | // Public subnets do not exist 37 | expect(resolverEndpoints.publicSubnetNames).toEqual([]); 38 | // Private subnet (NATed) do not exist 39 | expect(resolverEndpoints.privateSubnetNames).toEqual([]); 40 | // Two private isolated subnets, one for interface endpoints and another for the transit gateway 41 | expect(resolverEndpoints.privateIsolatedSubnetNames).toEqual([ 42 | "resolver-endpoints", 43 | "transit-gateway", 44 | ]); 45 | 46 | // We expect NAT, and IGW resources do Not exist 47 | expect(() => template.hasResource("AWS::EC2::NatGateway", {})).toThrow(); 48 | expect(() => 49 | template.hasResource("AWS::EC2::InternetGateway", {}) 50 | ).toThrow(); 51 | 52 | // We expect to have associated to the Transit Gateway and Created a Route Table 53 | template.resourceCountIs("AWS::EC2::TransitGatewayVpcAttachment", 1); 54 | template.resourceCountIs( 55 | "AWS::EC2::TransitGatewayRouteTableAssociation", 56 | 1 57 | ); 58 | template.resourceCountIs("AWS::EC2::TransitGatewayRouteTable", 1); 59 | 60 | // We expect an Inbound and Outbound resolver 61 | template.hasResource("AWS::Route53Resolver::ResolverEndpoint", { 62 | Properties: { 63 | Direction: "INBOUND", 64 | }, 65 | }); 66 | template.hasResource("AWS::Route53Resolver::ResolverEndpoint", { 67 | Properties: { 68 | Direction: "OUTBOUND", 69 | }, 70 | }); 71 | // Security group that permits inbound on 53 udp and tcp to our resolveRequestFromCidrs 72 | template.hasResource("AWS::EC2::SecurityGroup", { 73 | Properties: { 74 | SecurityGroupIngress: [ 75 | { 76 | CidrIp: "10.0.0.0/8", 77 | Description: "Resolver TCP DNS Query from 10.0.0.0/8", 78 | FromPort: 53, 79 | IpProtocol: "tcp", 80 | ToPort: 53, 81 | }, 82 | { 83 | CidrIp: "10.0.0.0/8", 84 | Description: "Resolver UDP DNS Query from 10.0.0.0/8", 85 | FromPort: 53, 86 | IpProtocol: "udp", 87 | ToPort: 53, 88 | }, 89 | ], 90 | }, 91 | }); 92 | // We expect a forward rule to mclean.org 93 | template.hasResource("AWS::Route53Resolver::ResolverRule", { 94 | Properties: { 95 | RuleType: "FORWARD", 96 | DomainName: "amclean.org", 97 | TargetIps: [ 98 | { 99 | Ip: "10.10.1.2", 100 | Port: "53", 101 | }, 102 | ], 103 | }, 104 | }); 105 | // With an association 106 | template.resourceCountIs( 107 | "AWS::Route53Resolver::ResolverRuleAssociation", 108 | 1 109 | ); 110 | // We expect SSM named exports within our construct are prepared with Transit Route Table, and Association. 111 | expect(resolverEndpoints.tgwAttachmentSsm.name).toEqual( 112 | "/ssm/prefix/networking/globalprefix/vpcs/test-provider-endpoint-route53-resolver/tgwId" 113 | ); 114 | expect(resolverEndpoints.tgwRouteTableSsm.name).toEqual( 115 | "/ssm/prefix/networking/globalprefix/vpcs/test-provider-endpoint-route53-resolver/tgwRouteId" 116 | ); 117 | 118 | const prefix = "/ssm/prefix/networking/globalprefix"; 119 | for (const parameterName of [ 120 | `${prefix}/vpcs/test-provider-endpoint-route53-resolver/vpcId`, 121 | `${prefix}/vpcs/test-provider-endpoint-route53-resolver/vpcCidr`, 122 | `${prefix}/vpcs/test-provider-endpoint-route53-resolver/az0`, 123 | `${prefix}/vpcs/test-provider-endpoint-route53-resolver/az1`, 124 | `${prefix}/vpcs/test-provider-endpoint-route53-resolver/subnets/resolver-endpoints/us-east-1a/routeTableId`, 125 | `${prefix}/vpcs/test-provider-endpoint-route53-resolver/subnets/resolver-endpoints/us-east-1a/subnetCidr`, 126 | `${prefix}/vpcs/test-provider-endpoint-route53-resolver/subnets/resolver-endpoints/us-east-1a/subnetId`, 127 | `${prefix}/vpcs/test-provider-endpoint-route53-resolver/subnets/transit-gateway/us-east-1a/routeTableId`, 128 | `${prefix}/vpcs/test-provider-endpoint-route53-resolver/subnets/transit-gateway/us-east-1a/subnetCidr`, 129 | `${prefix}/vpcs/test-provider-endpoint-route53-resolver/subnets/transit-gateway/us-east-1a/subnetId`, 130 | `${prefix}/vpcs/test-provider-endpoint-route53-resolver/subnets/resolver-endpoints/us-east-1b/routeTableId`, 131 | `${prefix}/vpcs/test-provider-endpoint-route53-resolver/subnets/resolver-endpoints/us-east-1b/subnetCidr`, 132 | `${prefix}/vpcs/test-provider-endpoint-route53-resolver/subnets/resolver-endpoints/us-east-1b/subnetId`, 133 | `${prefix}/vpcs/test-provider-endpoint-route53-resolver/subnets/transit-gateway/us-east-1b/routeTableId`, 134 | `${prefix}/vpcs/test-provider-endpoint-route53-resolver/subnets/transit-gateway/us-east-1b/subnetCidr`, 135 | `${prefix}/vpcs/test-provider-endpoint-route53-resolver/subnets/transit-gateway/us-east-1b/subnetId`, 136 | `${prefix}/vpcs/test-provider-endpoint-route53-resolver/tgwAttachId`, 137 | `${prefix}/vpcs/test-provider-endpoint-route53-resolver/tgwRouteId`, 138 | `${prefix}/vpcs/test-provider-endpoint-route53-resolver/tgwId`, 139 | ]) { 140 | template.hasResourceProperties("AWS::SSM::Parameter", { 141 | Name: parameterName, 142 | }); 143 | } 144 | } 145 | }); 146 | 147 | test("Route53ResolverOnlyInbound", () => { 148 | for (const transitStyle of ["stack", "imported"]) { 149 | const app = new cdk.App(); 150 | let ITgwedId: ITgw | undefined = undefined; 151 | if (transitStyle == "imported") { 152 | ITgwedId = { 153 | attrId: "tgw-12392488", 154 | }; 155 | } 156 | const resolverEndpoints = newVpcRoute53ResolverStack( 157 | { 158 | resolveRequestsFromCidrs: ["10.0.0.0/8"], 159 | }, 160 | app, 161 | ITgwedId 162 | ); 163 | resolverEndpoints.saveTgwRouteInformation(); 164 | resolverEndpoints.attachToTGW(); 165 | resolverEndpoints.createSsmParameters(); 166 | const template = Template.fromStack(resolverEndpoints); 167 | 168 | // We expect an Inbound resolver 169 | template.hasResource("AWS::Route53Resolver::ResolverEndpoint", { 170 | Properties: { 171 | Direction: "INBOUND", 172 | }, 173 | }); 174 | // But not an outbound resolver 175 | expect(() => { 176 | template.hasResource("AWS::Route53Resolver::ResolverEndpoint", { 177 | Properties: { 178 | Direction: "OUTBOUND", 179 | }, 180 | }); 181 | }).toThrow(); 182 | } 183 | }); 184 | 185 | test("Route53ResolverOnlyOutbound", () => { 186 | for (const transitStyle of ["stack", "imported"]) { 187 | const app = new cdk.App(); 188 | let ITgwedId: ITgw | undefined = undefined; 189 | if (transitStyle == "imported") { 190 | ITgwedId = { 191 | attrId: "tgw-12392488", 192 | }; 193 | } 194 | const resolverEndpoints = newVpcRoute53ResolverStack( 195 | { 196 | forwardRequests: { 197 | forDomains: ["amclean.org"], 198 | toIps: ["10.10.1.2"], 199 | }, 200 | }, 201 | app, 202 | ITgwedId 203 | ); 204 | resolverEndpoints.saveTgwRouteInformation(); 205 | resolverEndpoints.attachToTGW(); 206 | resolverEndpoints.createSsmParameters(); 207 | const template = Template.fromStack(resolverEndpoints); 208 | 209 | // We expect an outbound resolver 210 | template.hasResource("AWS::Route53Resolver::ResolverEndpoint", { 211 | Properties: { 212 | Direction: "OUTBOUND", 213 | }, 214 | }); 215 | // But not an inbound resolver 216 | expect(() => { 217 | template.hasResource("AWS::Route53Resolver::ResolverEndpoint", { 218 | Properties: { 219 | Direction: "INBOUND", 220 | }, 221 | }); 222 | }).toThrow(); 223 | } 224 | }); 225 | 226 | test("Route53ResolverExternalVpcs", () => { 227 | for (const transitStyle of ["stack", "imported"]) { 228 | const app = new cdk.App(); 229 | let ITgwedId: ITgw | undefined = undefined; 230 | if (transitStyle == "imported") { 231 | ITgwedId = { 232 | attrId: "tgw-12392488", 233 | }; 234 | } 235 | const resolverEndpoints = newVpcRoute53ResolverStack( 236 | { 237 | forwardRequests: { 238 | forDomains: ["amclean.org"], 239 | toIps: ["10.10.1.2"], 240 | forExistingVpcs: [ 241 | { 242 | name: "importedVpc", 243 | vpcId: "vpc-1234", 244 | }, 245 | ], 246 | }, 247 | }, 248 | app, 249 | ITgwedId 250 | ); 251 | resolverEndpoints.saveTgwRouteInformation(); 252 | resolverEndpoints.attachToTGW(); 253 | resolverEndpoints.createSsmParameters(); 254 | const template = Template.fromStack(resolverEndpoints); 255 | 256 | // Expect our association will contain our imported vpc 257 | template.hasResourceProperties( 258 | "AWS::Route53Resolver::ResolverRuleAssociation", 259 | { 260 | VPCId: "vpc-1234", 261 | } 262 | ); 263 | } 264 | }); 265 | 266 | test("Route53ResolverAssociatedVpcs", () => { 267 | for (const transitStyle of ["stack", "imported"]) { 268 | const app = new cdk.App(); 269 | let ITgwedId: ITgw | undefined = undefined; 270 | if (transitStyle == "imported") { 271 | ITgwedId = { 272 | attrId: "tgw-12392488", 273 | }; 274 | } 275 | const workloadStack = newVpcWorkloadStack( 276 | {}, 277 | app, 278 | "workloadIsolated", 279 | ITgwedId 280 | ); 281 | workloadStack.saveTgwRouteInformation(); 282 | workloadStack.attachToTGW(); 283 | workloadStack.createSsmParameters(); 284 | 285 | const resolverEndpoints = newVpcRoute53ResolverStack( 286 | { 287 | forwardRequests: { 288 | forDomains: ["amclean.org"], 289 | toIps: ["10.10.1.2"], 290 | forVpcs: [workloadStack], 291 | }, 292 | }, 293 | app, 294 | workloadStack.tgw 295 | ); 296 | resolverEndpoints.saveTgwRouteInformation(); 297 | resolverEndpoints.attachToTGW(); 298 | resolverEndpoints.createSsmParameters(); 299 | const template = Template.fromStack(resolverEndpoints); 300 | 301 | // Expect our association will contain a reference to our existing VPC an explicit ID 302 | template.hasResourceProperties( 303 | "AWS::Route53Resolver::ResolverRuleAssociation", 304 | { 305 | VPCId: { Ref: Match.anyValue() }, 306 | } 307 | ); 308 | } 309 | }); 310 | -------------------------------------------------------------------------------- /test/vpc-workload-isolated-stack.test.ts: -------------------------------------------------------------------------------- 1 | import { Template, Match } from "aws-cdk-lib/assertions"; 2 | import { newVpcWorkloadStack } from "./stack-builder-helper"; 3 | import { ITgw } from "../lib/types"; 4 | import * as cdk from "aws-cdk-lib"; 5 | 6 | test("WorkloadIsolatedBase", () => { 7 | for (const transitStyle of ["stack", "imported"]) { 8 | const app = new cdk.App(); 9 | let ITgwedId: ITgw | undefined = undefined; 10 | if (transitStyle == "imported") { 11 | ITgwedId = { 12 | attrId: "tgw-12392488", 13 | }; 14 | } 15 | const workloadIsolated = newVpcWorkloadStack( 16 | { 17 | globalPrefix: "globalPrefix", 18 | ssmParameterPrefix: "/ssm/prefix", 19 | namePrefix: "Test", 20 | vpcCidr: "10.1.0.0/16", 21 | availabilityZones: ["us-east-1a", "us-east-1b"], 22 | withTgw: true, 23 | createSubnets: [ 24 | { 25 | name: "testing", 26 | cidrMask: 21, 27 | }, 28 | ], 29 | }, 30 | app, 31 | "workloadIsolated", 32 | ITgwedId 33 | ); 34 | workloadIsolated.saveTgwRouteInformation(); 35 | workloadIsolated.attachToTGW(); 36 | workloadIsolated.createSsmParameters(); 37 | const template = Template.fromStack(workloadIsolated); 38 | // We've provided 2 AZs and left our transit gateway specific to default of false so expect to see 2 subnets. 39 | template.resourceCountIs("AWS::EC2::Subnet", 2); 40 | 41 | // Public subnets do not exist 42 | expect(workloadIsolated.publicSubnetNames).toEqual([]); 43 | // Private subnet (NATed) do not exist 44 | expect(workloadIsolated.privateSubnetNames).toEqual([]); 45 | // One total privated ioslated subnet 46 | expect(workloadIsolated.privateIsolatedSubnetNames).toEqual(["testing"]); 47 | 48 | // We expect NAT, and IGW resources do Not exist 49 | expect(() => template.hasResource("AWS::EC2::NatGateway", {})).toThrow(); 50 | expect(() => 51 | template.hasResource("AWS::EC2::InternetGateway", {}) 52 | ).toThrow(); 53 | 54 | // We'll have an s3 and DynamoDB Gateway endpoint 55 | template.hasResourceProperties("AWS::EC2::VPCEndpoint", { 56 | ServiceName: { 57 | "Fn::Join": [ 58 | "", 59 | [ 60 | "com.amazonaws.", 61 | { 62 | Ref: "AWS::Region", 63 | }, 64 | ".s3", 65 | ], 66 | ], 67 | }, 68 | VpcEndpointType: "Gateway", 69 | }); 70 | template.hasResourceProperties("AWS::EC2::VPCEndpoint", { 71 | ServiceName: { 72 | "Fn::Join": [ 73 | "", 74 | [ 75 | "com.amazonaws.", 76 | { 77 | Ref: "AWS::Region", 78 | }, 79 | ".dynamodb", 80 | ], 81 | ], 82 | }, 83 | VpcEndpointType: "Gateway", 84 | }); 85 | 86 | // We'll have VPC flow logs enabled for CloudWatch 87 | template.resourceCountIs("AWS::Logs::LogGroup", 1); 88 | template.resourceCountIs("AWS::EC2::FlowLog", 1); 89 | 90 | // We expect to have associated to the Transit Gateway and Created a Route Table 91 | template.resourceCountIs("AWS::EC2::TransitGatewayVpcAttachment", 1); 92 | template.resourceCountIs( 93 | "AWS::EC2::TransitGatewayRouteTableAssociation", 94 | 1 95 | ); 96 | template.resourceCountIs("AWS::EC2::TransitGatewayRouteTable", 1); 97 | 98 | // We expect SSM named exports within our construct are prepared with Transit Route Table, and Association. 99 | expect(workloadIsolated.tgwAttachmentSsm.name).toEqual( 100 | "/ssm/prefix/networking/globalprefix/vpcs/test-vpc-workload/tgwId" 101 | ); 102 | expect(workloadIsolated.tgwRouteTableSsm.name).toEqual( 103 | "/ssm/prefix/networking/globalprefix/vpcs/test-vpc-workload/tgwRouteId" 104 | ); 105 | 106 | const prefix = "/ssm/prefix/networking/globalprefix"; 107 | for (const parameterName of [ 108 | `${prefix}/vpcs/test-vpc-workload/vpcId`, 109 | `${prefix}/vpcs/test-vpc-workload/vpcCidr`, 110 | `${prefix}/vpcs/test-vpc-workload/az0`, 111 | `${prefix}/vpcs/test-vpc-workload/az1`, 112 | `${prefix}/vpcs/test-vpc-workload/subnets/testing/us-east-1a/routeTableId`, 113 | `${prefix}/vpcs/test-vpc-workload/subnets/testing/us-east-1a/subnetCidr`, 114 | `${prefix}/vpcs/test-vpc-workload/subnets/testing/us-east-1a/subnetId`, 115 | `${prefix}/vpcs/test-vpc-workload/subnets/testing/us-east-1b/routeTableId`, 116 | `${prefix}/vpcs/test-vpc-workload/subnets/testing/us-east-1b/subnetCidr`, 117 | `${prefix}/vpcs/test-vpc-workload/subnets/testing/us-east-1b/subnetId`, 118 | `${prefix}/vpcs/test-vpc-workload/tgwAttachId`, 119 | `${prefix}/vpcs/test-vpc-workload/tgwRouteId`, 120 | `${prefix}/vpcs/test-vpc-workload/tgwId`, 121 | ]) { 122 | template.hasResourceProperties("AWS::SSM::Parameter", { 123 | Name: parameterName, 124 | }); 125 | } 126 | } 127 | }); 128 | 129 | test("WorkloadIsolatedBaseWithSharedSubnets", () => { 130 | for (const transitStyle of ["stack", "imported"]) { 131 | const app = new cdk.App(); 132 | let ITgwedId: ITgw | undefined = undefined; 133 | if (transitStyle == "imported") { 134 | ITgwedId = { 135 | attrId: "tgw-12392488", 136 | }; 137 | } 138 | const workloadIsolated = newVpcWorkloadStack( 139 | { 140 | globalPrefix: "globalPrefix", 141 | ssmParameterPrefix: "/ssm/prefix", 142 | namePrefix: "Test", 143 | vpcCidr: "10.1.0.0/16", 144 | availabilityZones: ["us-east-1a", "us-east-1b"], 145 | withTgw: true, 146 | organizationId: "o-12345", 147 | organizationMainAccountId: "012345678910", 148 | createSubnets: [ 149 | { 150 | name: "testing", 151 | cidrMask: 21, 152 | sharedWith: [12345678910, "o-12345", "ou-12345"], 153 | }, 154 | ], 155 | }, 156 | app, 157 | "workloadIsolated", 158 | ITgwedId 159 | ); 160 | workloadIsolated.saveTgwRouteInformation(); 161 | workloadIsolated.attachToTGW(); 162 | workloadIsolated.createSsmParameters(); 163 | const template = Template.fromStack(workloadIsolated); 164 | 165 | // We expect RAM share stanzas for our subnets. One account, one OU, and one full Org 166 | template.hasResourceProperties("AWS::RAM::ResourceShare", { 167 | Principals: Match.arrayWith(["12345678910"]), 168 | }); 169 | template.hasResourceProperties("AWS::RAM::ResourceShare", { 170 | Principals: Match.arrayWith([ 171 | "arn:aws:organizations::012345678910/o-12345", 172 | ]), 173 | }); 174 | template.hasResourceProperties("AWS::RAM::ResourceShare", { 175 | Principals: Match.arrayWith([ 176 | "arn:aws:organizations::012345678910:ou/o-12345/ou-12345", 177 | ]), 178 | }); 179 | // The name should match 'Share-${vpcName}' 180 | template.hasResourceProperties("AWS::RAM::ResourceShare", { 181 | Name: "Share-test-vpc-workload", 182 | }); 183 | } 184 | }); 185 | -------------------------------------------------------------------------------- /test/vpc-workload-public-stack.test.ts: -------------------------------------------------------------------------------- 1 | import { Template, Match } from "aws-cdk-lib/assertions"; 2 | import { newVpcWorkloadStack } from "./stack-builder-helper"; 3 | import { ITgw } from "../lib/types"; 4 | import * as cdk from "aws-cdk-lib"; 5 | 6 | test("WorkloadPublicBase", () => { 7 | for (const transitStyle of ["stack", "imported"]) { 8 | const app = new cdk.App(); 9 | let ITgwedId: ITgw | undefined = undefined; 10 | if (transitStyle == "imported") { 11 | ITgwedId = { 12 | attrId: "tgw-12392488", 13 | }; 14 | } 15 | const workloadPublic = newVpcWorkloadStack( 16 | { 17 | globalPrefix: "globalPrefix", 18 | ssmParameterPrefix: "/ssm/prefix", 19 | namePrefix: "Test", 20 | vpcCidr: "10.1.0.0/16", 21 | availabilityZones: ["us-east-1a", "us-east-1b"], 22 | withTgw: true, 23 | createSubnets: [ 24 | { 25 | name: "testing", 26 | cidrMask: 21, 27 | }, 28 | ], 29 | }, 30 | app, 31 | "workloadPublic", 32 | ITgwedId 33 | ); 34 | workloadPublic.saveTgwRouteInformation(); 35 | workloadPublic.attachToTGW(); 36 | workloadPublic.createSsmParameters(); 37 | const template = Template.fromStack(workloadPublic); 38 | // We've provided 2 AZs and left our transit gateway specific to default of false so expect to see 2 subnets. 39 | template.resourceCountIs("AWS::EC2::Subnet", 2); 40 | 41 | // Public subnets exist 42 | expect(workloadPublic.publicSubnetNames).toEqual(["testing"]); 43 | // Private subnet (NATed) do not exist 44 | expect(workloadPublic.privateSubnetNames).toEqual([]); 45 | // Private Subnets do not exist 46 | expect(workloadPublic.privateIsolatedSubnetNames).toEqual([]); 47 | 48 | // We expect NAT does not exist, but IGW does 49 | expect(() => template.hasResource("AWS::EC2::NatGateway", {})).toThrow(); 50 | expect(() => 51 | template.hasResource("AWS::EC2::InternetGateway", {}) 52 | ); 53 | 54 | // We'll have VPC flow logs enabled for CloudWatch 55 | template.resourceCountIs("AWS::Logs::LogGroup", 1); 56 | template.resourceCountIs("AWS::EC2::FlowLog", 1); 57 | 58 | // We expect to have associated to the Transit Gateway and Created a Route Table 59 | template.resourceCountIs("AWS::EC2::TransitGatewayVpcAttachment", 1); 60 | template.resourceCountIs( 61 | "AWS::EC2::TransitGatewayRouteTableAssociation", 62 | 1 63 | ); 64 | template.resourceCountIs("AWS::EC2::TransitGatewayRouteTable", 1); 65 | 66 | // We expect SSM named exports within our construct are prepared with Transit Route Table, and Association. 67 | expect(workloadPublic.tgwAttachmentSsm.name).toEqual( 68 | "/ssm/prefix/networking/globalprefix/vpcs/test-vpc-public-workload/tgwId" 69 | ); 70 | expect(workloadPublic.tgwRouteTableSsm.name).toEqual( 71 | "/ssm/prefix/networking/globalprefix/vpcs/test-vpc-public-workload/tgwRouteId" 72 | ); 73 | 74 | const prefix = "/ssm/prefix/networking/globalprefix"; 75 | for (const parameterName of [ 76 | `${prefix}/vpcs/test-vpc-public-workload/vpcId`, 77 | `${prefix}/vpcs/test-vpc-public-workload/vpcCidr`, 78 | `${prefix}/vpcs/test-vpc-public-workload/az0`, 79 | `${prefix}/vpcs/test-vpc-public-workload/az1`, 80 | `${prefix}/vpcs/test-vpc-public-workload/subnets/testing/us-east-1a/routeTableId`, 81 | `${prefix}/vpcs/test-vpc-public-workload/subnets/testing/us-east-1a/subnetCidr`, 82 | `${prefix}/vpcs/test-vpc-public-workload/subnets/testing/us-east-1a/subnetId`, 83 | `${prefix}/vpcs/test-vpc-public-workload/subnets/testing/us-east-1b/routeTableId`, 84 | `${prefix}/vpcs/test-vpc-public-workload/subnets/testing/us-east-1b/subnetCidr`, 85 | `${prefix}/vpcs/test-vpc-public-workload/subnets/testing/us-east-1b/subnetId`, 86 | `${prefix}/vpcs/test-vpc-public-workload/tgwAttachId`, 87 | `${prefix}/vpcs/test-vpc-public-workload/tgwRouteId`, 88 | `${prefix}/vpcs/test-vpc-public-workload/tgwId`, 89 | ]) { 90 | template.hasResourceProperties("AWS::SSM::Parameter", { 91 | Name: parameterName, 92 | }); 93 | } 94 | } 95 | }); 96 | 97 | test("WorkloadIsolatedBaseWithSharedSubnets", () => { 98 | for (const transitStyle of ["stack", "imported"]) { 99 | const app = new cdk.App(); 100 | let ITgwedId: ITgw | undefined = undefined; 101 | if (transitStyle == "imported") { 102 | ITgwedId = { 103 | attrId: "tgw-12392488", 104 | }; 105 | } 106 | const workloadPublic = newVpcWorkloadStack( 107 | { 108 | globalPrefix: "globalPrefix", 109 | ssmParameterPrefix: "/ssm/prefix", 110 | namePrefix: "Test", 111 | vpcCidr: "10.1.0.0/16", 112 | availabilityZones: ["us-east-1a", "us-east-1b"], 113 | withTgw: true, 114 | organizationId: "o-12345", 115 | organizationMainAccountId: "012345678910", 116 | createSubnets: [ 117 | { 118 | name: "testing", 119 | cidrMask: 21, 120 | sharedWith: [12345678910, "o-12345", "ou-12345"], 121 | }, 122 | ], 123 | }, 124 | app, 125 | "workloadPublic", 126 | ITgwedId 127 | ); 128 | workloadPublic.saveTgwRouteInformation(); 129 | workloadPublic.attachToTGW(); 130 | workloadPublic.createSsmParameters(); 131 | const template = Template.fromStack(workloadPublic); 132 | 133 | // We expect RAM share stanzas for our subnets. One account, one OU, and one full Org 134 | template.hasResourceProperties("AWS::RAM::ResourceShare", { 135 | Principals: Match.arrayWith(["12345678910"]), 136 | }); 137 | template.hasResourceProperties("AWS::RAM::ResourceShare", { 138 | Principals: Match.arrayWith([ 139 | "arn:aws:organizations::012345678910/o-12345", 140 | ]), 141 | }); 142 | template.hasResourceProperties("AWS::RAM::ResourceShare", { 143 | Principals: Match.arrayWith([ 144 | "arn:aws:organizations::012345678910:ou/o-12345/ou-12345", 145 | ]), 146 | }); 147 | // The name should match 'Share-${vpcName}' 148 | template.hasResourceProperties("AWS::RAM::ResourceShare", { 149 | Name: "Share-test-vpc-public-workload", 150 | }); 151 | } 152 | }); 153 | -------------------------------------------------------------------------------- /test/vpn-to-transit-gateway-stack.test.ts: -------------------------------------------------------------------------------- 1 | import { Template, Match } from "aws-cdk-lib/assertions"; 2 | import { newVpnStack } from "./stack-builder-helper"; 3 | import * as cdk from "aws-cdk-lib"; 4 | import { ITgw } from "../lib/types"; 5 | 6 | test("BaseWithNewCustomerGateway", () => { 7 | for (const transitStyle of ["stack", "imported"]) { 8 | const app = new cdk.App(); 9 | let tgwImportedId: ITgw | undefined = undefined; 10 | if (transitStyle == "imported") { 11 | tgwImportedId = { 12 | attrId: "tgw-12392488", 13 | }; 14 | } 15 | const vpnStack = newVpnStack( 16 | { 17 | newCustomerGatewayName: "testing", 18 | newCustomerGatewayAsn: 65321, 19 | newCustomerGatewayIpAddress: "1.2.3.4", 20 | }, 21 | app, 22 | tgwImportedId 23 | ); 24 | vpnStack.saveTgwRouteInformation(); 25 | vpnStack.attachToTGW(); 26 | vpnStack.createSsmParameters(); 27 | const template = Template.fromStack(vpnStack); 28 | // One VPN Resource and a Customer Gateway Resource 29 | template.resourceCountIs("AWS::EC2::VPNConnection", 1); 30 | template.resourceCountIs("AWS::EC2::CustomerGateway", 1); 31 | // We expect to have associated to the Transit Gateway and Created a Route Table 32 | template.resourceCountIs("AWS::EC2::TransitGatewayRouteTable", 1); 33 | template.resourceCountIs( 34 | "AWS::EC2::TransitGatewayRouteTableAssociation", 35 | 1 36 | ); 37 | // We expect our new customer gateway reflects the ASN, Address, and Name we specified 38 | template.hasResourceProperties("AWS::EC2::CustomerGateway", { 39 | IpAddress: "1.2.3.4", 40 | BgpAsn: 65321, 41 | Tags: [ 42 | { 43 | Key: "Name", 44 | Value: "testing-customer-gateway", 45 | }, 46 | ], 47 | }); 48 | // We expect SSM Exports that our stacks above can consume: 49 | template.hasResourceProperties("AWS::SSM::Parameter", { 50 | Name: "/ssm/prefix/networking/globalprefix/vpns/test-vpn/tgwRouteId", 51 | }); 52 | template.hasResourceProperties("AWS::SSM::Parameter", { 53 | Name: "/ssm/prefix/networking/globalprefix/vpns/test-vpn/tgwAttachId", 54 | }); 55 | } 56 | }); 57 | 58 | test("WithExistingCustomerGateway", () => { 59 | for (const transitStyle of ["stack", "imported"]) { 60 | const app = new cdk.App(); 61 | let tgwImportedId: ITgw | undefined = undefined; 62 | if (transitStyle == "imported") { 63 | tgwImportedId = { 64 | attrId: "tgw-12392488", 65 | }; 66 | } 67 | const vpnStack = newVpnStack( 68 | { 69 | existingCustomerGatewayId: "cgw-123451", 70 | }, 71 | app, 72 | tgwImportedId 73 | ); 74 | vpnStack.saveTgwRouteInformation(); 75 | vpnStack.attachToTGW(); 76 | const template = Template.fromStack(vpnStack); 77 | // One VPN Resource and a Customer Gateway Resource 78 | template.resourceCountIs("AWS::EC2::VPNConnection", 1); 79 | template.resourceCountIs("AWS::EC2::CustomerGateway", 0); 80 | // Our VPN connection should have our provided customer gateway from above 81 | template.hasResourceProperties("AWS::EC2::VPNConnection", { 82 | CustomerGatewayId: "cgw-123451", 83 | }); 84 | } 85 | }); 86 | 87 | test("BaseWithImport", () => { 88 | for (const transitStyle of ["stack", "imported"]) { 89 | const app = new cdk.App(); 90 | let tgwImportedId: ITgw | undefined = undefined; 91 | if (transitStyle == "imported") { 92 | tgwImportedId = { 93 | attrId: "tgw-12392488", 94 | }; 95 | } 96 | const vpnStack = newVpnStack( 97 | { 98 | existingVpnConnectionId: "vpn-1234", 99 | existingVpnTransitGatewayAttachId: "tgw-attach-1234", 100 | existingVpnTransitGatewayRouteTableId: "tgw-rtb-12313", 101 | }, 102 | app, 103 | tgwImportedId 104 | ); 105 | vpnStack.saveTgwRouteInformation(); 106 | vpnStack.attachToTGW(); 107 | vpnStack.createSsmParameters(); 108 | const template = Template.fromStack(vpnStack); 109 | // Expect no resources created (Stack exists for SSM exports only) 110 | template.resourceCountIs("AWS::EC2::VPNConnection", 0); 111 | template.resourceCountIs("AWS::EC2::CustomerGateway", 0); 112 | // We expect to have associated to the Transit Gateway and Created a Route Table 113 | template.resourceCountIs("AWS::EC2::TransitGatewayRouteTable", 0); 114 | template.resourceCountIs( 115 | "AWS::EC2::TransitGatewayRouteTableAssociation", 116 | 0 117 | ); 118 | // We expect our ref to vpn is working 119 | expect(vpnStack.vpn.ref).toEqual("vpn-1234"); 120 | // We expect SSM Exports that our stacks above can consume: 121 | template.hasResourceProperties("AWS::SSM::Parameter", { 122 | Name: "/ssm/prefix/networking/globalprefix/vpns/test-vpn/tgwRouteId", 123 | Value: "tgw-rtb-12313", 124 | }); 125 | template.hasResourceProperties("AWS::SSM::Parameter", { 126 | Name: "/ssm/prefix/networking/globalprefix/vpns/test-vpn/tgwAttachId", 127 | Value: "tgw-attach-1234", 128 | }); 129 | } 130 | }); 131 | 132 | test("BaseWithImportBadProps", () => { 133 | for (const transitStyle of ["stack", "imported"]) { 134 | const app = new cdk.App(); 135 | let tgwImportedId: ITgw | undefined = undefined; 136 | if (transitStyle == "imported") { 137 | tgwImportedId = { 138 | attrId: "tgw-12392488", 139 | }; 140 | } 141 | // Confirm we throw if we're missing an import value 142 | expect(() => 143 | newVpnStack( 144 | { 145 | existingVpnConnectionId: "vpn-1234", 146 | existingVpnTransitGatewayAttachId: "tgw-attach-1234", 147 | }, 148 | app, 149 | tgwImportedId 150 | ) 151 | ).toThrow( 152 | "Importing an existing VPN requires existingVpnTransitGatewayRouteTableId to be defined" 153 | ); 154 | } 155 | }); 156 | 157 | test("ExtraVpnPropertiesPresentButNotPSK", () => { 158 | for (const transitStyle of ["stack", "imported"]) { 159 | const app = new cdk.App(); 160 | let tgwImportedId: ITgw | undefined = undefined; 161 | if (transitStyle == "imported") { 162 | tgwImportedId = { 163 | attrId: "tgw-12392488", 164 | }; 165 | } 166 | const vpnStack = newVpnStack( 167 | { 168 | existingCustomerGatewayId: "cgw-123451", 169 | tunnelOneOptions: { 170 | tunnelInsideCidr: "169.254.10.1/30", 171 | }, 172 | tunnelTwoOptions: { 173 | tunnelInsideCidr: "169.254.11.1/30", 174 | }, 175 | }, 176 | app, 177 | tgwImportedId 178 | ); 179 | vpnStack.saveTgwRouteInformation(); 180 | vpnStack.attachToTGW(); 181 | const template = Template.fromStack(vpnStack); 182 | // Our VPN connection should have our provided customer gateway from above 183 | template.hasResourceProperties("AWS::EC2::VPNConnection", { 184 | CustomerGatewayId: "cgw-123451", 185 | VpnTunnelOptionsSpecifications: [ 186 | { 187 | TunnelInsideCidr: "169.254.10.1/30", 188 | }, 189 | { 190 | TunnelInsideCidr: "169.254.11.1/30", 191 | }, 192 | ], 193 | }); 194 | 195 | // Fail our tests if someone implements the PSK in the template. If you get this working safely / with s secret/secure string 196 | // Then you can remove this 197 | expect(() => 198 | template.hasResourceProperties("AWS::EC2::VPNConnection", { 199 | VpnTunnelOptionsSpecifications: [ 200 | { 201 | PreSharedKey: Match.anyValue(), 202 | }, 203 | { 204 | PreSharedKey: Match.anyValue(), 205 | }, 206 | ], 207 | }) 208 | ).toThrow(); 209 | } 210 | }); 211 | -------------------------------------------------------------------------------- /tools/discoverEndpoints/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EC2Client, 3 | DescribeVpcEndpointServicesCommand, 4 | } from "@aws-sdk/client-ec2"; 5 | import * as path from "path"; 6 | import * as fs from "fs"; 7 | import * as ri from "@aws-cdk/region-info"; 8 | 9 | (async () => { 10 | for (const regionInfo of ri.RegionInfo.regions) { 11 | if (!regionInfo.isOptInRegion && regionInfo.partition == "aws") { 12 | const region = regionInfo.name; 13 | console.log(`Connecting to Region ${region}`); 14 | const client = new EC2Client({ region: region }); 15 | 16 | console.log(`. . . Describing all interface endpoints`); 17 | const command = new DescribeVpcEndpointServicesCommand({ 18 | Filters: [ 19 | { 20 | Name: "service-type", 21 | Values: ["Interface"], 22 | }, 23 | ], 24 | }); 25 | const response = await client.send(command); 26 | if (response.ServiceDetails) { 27 | console.log( 28 | `. . . Saving all discovered endpoints to file discovery/endpoints-${region}.json`, 29 | ); 30 | fs.writeFileSync( 31 | path.join("discovery", `endpoints-${region}.json`), 32 | JSON.stringify(response.ServiceDetails, null, 2), 33 | { encoding: "utf8" }, 34 | ); 35 | } else { 36 | console.error( 37 | `ERROR: Empty or missing response for ServiceDetails when working with region ${region}`, 38 | ); 39 | process.exit(1); 40 | } 41 | } 42 | } 43 | })(); 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020", 7 | "dom" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ], 26 | "resolveJsonModule": true 27 | }, 28 | "exclude": [ 29 | "node_modules", 30 | "cdk.out" 31 | ] 32 | } 33 | --------------------------------------------------------------------------------