├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── close_stale.yml │ ├── codeql-analysis.yml │ └── nodejs.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── SECURITY.md ├── __tests__ ├── .eslintrc ├── az.test.js ├── bastion.test.js ├── fixtures │ ├── vpc_multiple_az_multiple_natgw_no_db.json │ ├── vpc_multiple_az_natgw_db.json │ ├── vpc_multiple_az_natgw_no_db.json │ ├── vpc_multiple_az_no_natgw_db.json │ ├── vpc_multiple_az_no_natgw_no_db.json │ ├── vpc_multiple_az_single_natgw_no_db.json │ ├── vpc_single_az_natgw_db.json │ ├── vpc_single_az_natgw_no_db.json │ ├── vpc_single_az_no_natgw_db.json │ └── vpc_single_az_no_natgw_no_db.json ├── flow_logs.test.js ├── index.test.js ├── nacl.test.js ├── nat_instance.test.js ├── natgw.test.js ├── outputs.test.js ├── parameters.test.js ├── routes.test.js ├── subnet_groups.test.js ├── subnets.test.js ├── vpc.test.js └── vpce.test.js ├── example ├── .npmrc ├── index.js ├── package.json ├── resources │ ├── efs_cf.yml │ ├── iam_cf.yml │ └── rds_cf.yml ├── serverless.yml └── webpack.config.js ├── jest-config.json ├── package-lock.json ├── package.json └── src ├── az.js ├── bastion.js ├── constants.js ├── flow_logs.js ├── index.js ├── nacl.js ├── nat_instance.js ├── natgw.js ├── outputs.js ├── parameters.js ├── routes.js ├── subnet_groups.js ├── subnets.js ├── vpc.js └── vpce.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.yml] 15 | quote_style = double 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .git 2 | .serverless 3 | .vscode 4 | .webpack 5 | coverage 6 | node_modules 7 | tmp 8 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "es6": true 6 | }, 7 | "extends": ["airbnb-base", "plugin:prettier/recommended"], 8 | "plugins": ["prettier"], 9 | "settings": { 10 | "import/core-modules": ["aws-sdk"] 11 | }, 12 | "rules": { 13 | "no-console": "off", 14 | "max-len": ["error", { "code": 100, "ignoreUrls": true }], 15 | "comma-dangle": ["error", "always-multiline"] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.js text eol=lf 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: jplock # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: jplock # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | custom: # Replace with a single custom sponsorship URL 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "npm" 13 | directory: "/example" 14 | schedule: 15 | interval: "weekly" 16 | - package-ecosystem: "github-actions" 17 | directory: "/" 18 | schedule: 19 | interval: "weekly" 20 | -------------------------------------------------------------------------------- /.github/workflows/close_stale.yml: -------------------------------------------------------------------------------- 1 | name: "Close stale issues" 2 | on: 3 | schedule: 4 | - cron: "0 0 * * *" 5 | jobs: 6 | stale: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/stale@v9 10 | with: 11 | repo-token: ${{ secrets.GITHUB_TOKEN }} 12 | stale-issue-message: 'This issue is stale because it has been open 90 days with no activity. Remove the "stale" label or comment or this will be closed in 14 days.' 13 | stale-pr-message: 'This pull request is stale because it has been open 90 days with no activity. Remove the "stale" label or comment or this will be closed in 14 days.' 14 | days-before-stale: 90 15 | days-before-close: 14 16 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master, ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '0 4 * * 0' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | with: 21 | # We must fetch at least the immediate parents so that if this is 22 | # a pull request then we can checkout the head. 23 | fetch-depth: 2 24 | 25 | # If this run was triggered by a pull request event, then checkout 26 | # the head of the pull request instead of the merge commit. 27 | - run: git checkout HEAD^2 28 | if: ${{ github.event_name == 'pull_request' }} 29 | 30 | # Initializes the CodeQL tools for scanning. 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v3 33 | # Override language selection by uncommenting this and choosing your languages 34 | with: 35 | languages: javascript 36 | 37 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 38 | # If this step fails, then you should remove it and run the build manually (see below) 39 | - name: Autobuild 40 | uses: github/codeql-action/autobuild@v3 41 | 42 | # ℹ️ Command-line programs to run using the OS shell. 43 | # 📚 https://git.io/JvXDl 44 | 45 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 46 | # and modify them (or add more) to build your code if your project 47 | # uses a compiled language 48 | 49 | #- run: | 50 | # make bootstrap 51 | # make release 52 | 53 | - name: Perform CodeQL Analysis 54 | uses: github/codeql-action/analyze@v3 55 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [14.x, 16.x, 18.x] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | - name: npm install, build, and test 24 | run: | 25 | npm install 26 | npm run build --if-present 27 | npm test 28 | env: 29 | CI: true 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .git 2 | .vscode 3 | .serverless 4 | .webpack 5 | coverage 6 | node_modules 7 | example/package-lock.json 8 | *.tgz 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .eslintrc 2 | .eslintignore 3 | .editorconfig 4 | .gitattributes 5 | .gitignore 6 | .github/ 7 | .prettierrc 8 | .prettierignore 9 | .serverless/ 10 | .vscode/ 11 | *.tgz 12 | CODE_OF_CONDUCT.md 13 | SECURITY.md 14 | jest-config.json 15 | __tests__/ 16 | example/ 17 | coverage/ 18 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry = https://registry.npmjs.org 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .git 2 | .serverless 3 | .vscode 4 | .webpack 5 | coverage 6 | example 7 | node_modules 8 | package-lock.json 9 | tmp 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "endOfLine": "lf" 6 | } 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at github@smoketurner.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Smoke Turner, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # serverless-vpc-plugin 2 | 3 | [![serverless](http://public.serverless.com/badges/v3.svg)](http://www.serverless.com) 4 | [![npm version](https://badge.fury.io/js/serverless-vpc-plugin.svg)](https://badge.fury.io/js/serverless-vpc-plugin) 5 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/smoketurner/serverless-vpc-plugin/master/LICENSE) 6 | [![npm downloads](https://img.shields.io/npm/dt/serverless-vpc-plugin.svg?style=flat)](https://www.npmjs.com/package/serverless-vpc-plugin) 7 | 8 | Automatically creates an AWS Virtual Private Cloud (VPC) using all available Availability Zones (AZ) in a region. 9 | 10 | This plugin provisions the following resources: 11 | 12 | - `AWS::EC2::VPC` 13 | - `AWS::EC2::InternetGateway` (for outbound internet access from "Public" subnet) 14 | - `AWS::EC2::VPCGatewayAttachment` (to attach the `InternetGateway` to the VPC) 15 | - `AWS::EC2::SecurityGroup` (to execute Lambda functions [`AppSecurityGroup`]) 16 | 17 | If the VPC is allocated a /16 subnet, each availability zone within the region will be allocated a /20 subnet. Within each availability zone, this plugin will further divide the subnets: 18 | 19 | - `AWS::EC2::Subnet` "Public" (/22) - default route set to the `InternetGateway` 20 | - `AWS::EC2::Subnet` "Application" (/21) - no default route set (can be set to either a `NatGateway` or `NatInstance`) 21 | - `AWS::EC2::Subnet` "Database" (/22) - no default route set 22 | 23 | The subnetting layout was heavily inspired by the now shutdown [Skyliner](https://skyliner.io) platform. 😞 24 | 25 | Optionally, this plugin can also create `AWS::EC2::NatGateway` instances in each availability zone which requires provisioning `AWS::EC2::EIP` resources (AWS limits you to 5 per VPC, so if you want to provision your VPC across all 6 us-east availability zones, you'll need to request an VPC EIP limit increase from AWS). 26 | 27 | Instead of using the managed `AWS::EC2::NatGateway` instances, this plugin can also provision a single `t2.micro` NAT instance in `PublicSubnet1` which will allow HTTP/HTTPS traffic from the "Application" subnets to reach the Internet. 28 | 29 | Lambda functions will execute within the "Application" subnet and only be able to access: 30 | 31 | - S3 (via an S3 VPC endpoint) 32 | - DynamoDB (via an DynamoDB VPC endpoint) 33 | - RDS instances (provisioned within the "DB" subnet) 34 | - ElastiCache instances (provisioned within the "DB" subnet) 35 | - RedShift (provisioned within the "DB" subnet), 36 | - DAX clusters (provisioned within the "DB" subnet) 37 | - Neptune clusters (provisioned with the "DB" subnet) 38 | - Internet Access (if using a `NatGateway` or a `NatInstance`) 39 | 40 | If your Lambda functions need to [access the internet](https://docs.aws.amazon.com/lambda/latest/dg/vpc.html#vpc-internet), then you _MUST_ provision `NatGateway` resources or a NAT instance. 41 | 42 | By default, `AWS::EC2::VPCEndpoint` "Gateway" endpoints for S3 and DynamoDB will be provisioned within each availability zone to provide internal access to these services (there is no additional charge for using Gateway Type VPC endpoints). You can selectively control which `AWS::EC2::VPCEndpoint` "Interface" endpoints are available within your VPC using the `services` configuration option below. Not all AWS services are available in every region, so the plugin will query AWS to validate the services you have selected and notify you if any changes are required (there is an additional charge for using Interface Type VPC endpoints). 43 | 44 | If you specify more then one availability zone, this plugin will also provision the following database-related resources (controlled using the `subnetGroups` plugin option): 45 | 46 | - `AWS::RDS::DBSubnetGroup` 47 | - `AWS::ElastiCache::SubnetGroup` 48 | - `AWS::Redshift::ClusterSubnetGroup` 49 | - `AWS::DAX::SubnetGroup` 50 | 51 | to make it easier to create these resources across all of the availability zones. 52 | 53 | ## Installation 54 | 55 | ``` 56 | $ npx sls plugin install -n serverless-vpc-plugin 57 | ``` 58 | 59 | ## Configuration 60 | 61 | - All `vpcConfig` configuration parameters are optional 62 | 63 | ```yaml 64 | # add in your serverless.yml 65 | 66 | plugins: 67 | - serverless-vpc-plugin 68 | 69 | provider: 70 | # you do not need to provide the "vpc" section as this plugin will populate it automatically 71 | vpc: 72 | securityGroupIds: 73 | - # plugin will add LambdaExecutionSecurityGroup to this list 74 | subnetIds: 75 | - # plugin will add the "Application" subnets to this list 76 | 77 | custom: 78 | vpcConfig: 79 | # Whether plugin is enabled. Can be used to selectively disable plugin 80 | # on certain stages or configurations. Defaults to true. 81 | enabled: true 82 | 83 | cidrBlock: '10.0.0.0/16' 84 | 85 | # if createNatGateway is a boolean "true", a NAT Gateway and EIP will be provisioned in each zone 86 | # if createNatGateway is a number, that number of NAT Gateways will be provisioned 87 | createNatGateway: 2 88 | 89 | # When enabled, the DB subnet will only be accessible from the Application subnet 90 | # Both the Public and Application subnets will be accessible from 0.0.0.0/0 91 | createNetworkAcl: false 92 | 93 | # Whether to create the DB subnet 94 | createDbSubnet: true 95 | 96 | # Whether to enable VPC flow logging to an S3 bucket 97 | createFlowLogs: false 98 | 99 | # Whether to create a bastion host 100 | createBastionHost: false 101 | bastionHostKeyName: MyKey # required if creating a bastion host 102 | 103 | # Whether to create a NAT instance 104 | createNatInstance: false 105 | 106 | # Whether to create AWS Systems Manager (SSM) Parameters 107 | createParameters: false 108 | 109 | # Optionally specify AZs (defaults to auto-discover all availabile AZs) 110 | zones: 111 | - us-east-1a 112 | - us-east-1b 113 | - us-east-1c 114 | 115 | # By default, S3 and DynamoDB endpoints will be available within the VPC 116 | # see https://docs.aws.amazon.com/vpc/latest/userguide/vpc-endpoints.html 117 | # for a list of available service endpoints to provision within the VPC 118 | # (varies per region) 119 | services: 120 | - kms 121 | - secretsmanager 122 | 123 | # Optionally specify subnet groups to create. If not provided, subnet groups 124 | # for RDS, Redshift, ElasticCache and DAX will be provisioned. 125 | subnetGroups: 126 | - rds 127 | 128 | # Whether to export stack outputs so it may be consumed by other stacks 129 | exportOutputs: false 130 | ``` 131 | 132 | ## CloudFormation Outputs 133 | 134 | After executing `serverless deploy`, the following CloudFormation Stack Outputs will be provided: 135 | 136 | - `VPC`: VPC logical resource ID 137 | - `AppSecurityGroup`: Security Group ID that the applications use when executing within the VPC 138 | - `LambdaExecutionSecurityGroupId`: DEPRECATED - Please use AppSecurityGroupId instead 139 | - `BastionSSHUser`: SSH username to access the bastion host, if provisioned 140 | - `BastionEIP`: Elastic IP address associated to the bastion host, if provisioned 141 | - `RDSSubnetGroup`: SubnetGroup associated to RDS, if provisioned 142 | - `ElastiCacheSubnetGroup`: SubnetGroup associated to ElastiCache, if provisioned 143 | - `RedshiftSubnetGroup`: SubnetGroup associated to Redshift, if provisioned 144 | - `DAXSubnetGroup`: SubnetGroup associated to DAX, if provisioned 145 | - `AppSubnet{i}`: Each of the generated "Application" Subnets, where i is a 1 based index 146 | 147 | ### Exporting CloudFormation Outputs 148 | 149 | Setting `exportOutputs: true` will export stack outputs. The name of the exported value will be prefixed by the cloud formation stack name (`AWS::StackName`). For example, the value of the `VPC` output of a stack named `foo-prod` will be exported as `foo-prod-VPC`. 150 | 151 | ## SSM Parameters 152 | 153 | Setting `createParameters: true` will create the below parameters in the AWS Systems Manager (SSM) Parameter Store: 154 | 155 | - `/SLS/${AWS::StackName}/VPC`: VPC logical resource ID 156 | - `/SLS/${AWS::StackName}/AppSecurityGroup`: Security Group ID that the applications use when executing within the VPC 157 | - `/SLS/${AWS::StackName}/RDSSubnetGroup`: SubnetGroup associated to RDS, if provisioned 158 | - `/SLS/${AWS::StackName}/ElastiCacheSubnetGroup`: SubnetGroup associated to ElastiCache, if provisioned 159 | - `/SLS/${AWS::StackName}/RedshiftSubnetGroup`: SubnetGroup associated to Redshift, if provisioned 160 | - `/SLS/${AWS::StackName}/DAXSubnetGroup`: SubnetGroup associated to DAX, if provisioned 161 | - `/SLS/${AWS::StackName}/PublicSubnets`: Subnet ID's for the "Public" subnets 162 | - `/SLS/${AWS::StackName}/AppSubnets`: Subnet ID's for the "Application" subnets 163 | - `/SLS/${AWS::StackName}/DBSubnets`: Subnet ID's for the "Database" subnets 164 | 165 | As an example, if the stack name you want to reference is `new-service-dev`, you can then use Serverless' built-in [support](https://www.serverless.com/framework/docs/providers/aws/guide/variables/#reference-variables-using-the-ssm-parameter-store) for reading from SSM: 166 | 167 | ``` 168 | vpc: 169 | securityGroupIds: 170 | - ${ssm:/SLS/new-service-dev/AppSecurityGroup} 171 | subnetIds: ${ssm:/SLS/new-service-dev/AppSubnets} # sls will split this comma delimited list automatically because it's a StringList parameter type 172 | ``` 173 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | < 0.8.0 | :x: | 11 | | 0.8.x | :white_check_mark: | 12 | 13 | ## Reporting a Vulnerability 14 | 15 | Please email any security-related issues or concerns to security@smoketurner.com. 16 | You should expect an email reply within 24-48 hours. If the vulnerability is accepeted, 17 | a new pull request will be created to address the issue and a new patch release 18 | created. 19 | -------------------------------------------------------------------------------- /__tests__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": false, 3 | "env": { 4 | "jest": true, 5 | "jest/globals": true 6 | }, 7 | "extends": [ 8 | "plugin:jest/recommended" 9 | ], 10 | "plugins": [ 11 | "jest" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /__tests__/az.test.js: -------------------------------------------------------------------------------- 1 | const { buildAvailabilityZones } = require('../src/az'); 2 | const { splitSubnets } = require('../src/subnets'); 3 | 4 | // fixtures 5 | const vpcSingleAZNatGWDB = require('./fixtures/vpc_single_az_natgw_db.json'); 6 | const vpcSingleAZNoNatGWDB = require('./fixtures/vpc_single_az_no_natgw_db.json'); 7 | const vpcSingleAZNatGWNoDB = require('./fixtures/vpc_single_az_natgw_no_db.json'); 8 | const vpcSingleAZNoNatGWNoDB = require('./fixtures/vpc_single_az_no_natgw_no_db.json'); 9 | 10 | const vpcMultipleAZNatGWDB = require('./fixtures/vpc_multiple_az_natgw_db.json'); 11 | const vpcMultipleAZNoNatGWDB = require('./fixtures/vpc_multiple_az_no_natgw_db.json'); 12 | const vpcMultipleAZNatGWNoDB = require('./fixtures/vpc_multiple_az_natgw_no_db.json'); 13 | const vpcMultipleAZNoNatGWNoDB = require('./fixtures/vpc_multiple_az_no_natgw_no_db.json'); 14 | 15 | const vpcMultipleAZSingleNatGWNoDB = require('./fixtures/vpc_multiple_az_single_natgw_no_db.json'); 16 | // eslint-disable-next-line max-len 17 | const vpcMultipleAZMultipleNatGWNoDB = require('./fixtures/vpc_multiple_az_multiple_natgw_no_db.json'); 18 | 19 | describe('az', () => { 20 | describe('#buildAvailabilityZones', () => { 21 | it('builds no AZs without options', () => { 22 | const actual = buildAvailabilityZones(); 23 | expect(actual).toEqual({}); 24 | expect.assertions(1); 25 | }); 26 | 27 | it('builds a single AZ with a NAT Gateway and DBSubnet', () => { 28 | const expected = { ...vpcSingleAZNatGWDB }; 29 | 30 | const zones = ['us-east-1a']; 31 | const subnets = splitSubnets('10.0.0.0/16', zones); 32 | const actual = buildAvailabilityZones(subnets, zones, { 33 | numNatGateway: 1, 34 | createDbSubnet: true, 35 | }); 36 | 37 | expect(actual).toEqual(expected); 38 | expect.assertions(1); 39 | }); 40 | 41 | it('builds a single AZ without a NAT Gateway and DBSubnet', () => { 42 | const expected = { ...vpcSingleAZNoNatGWDB }; 43 | 44 | const zones = ['us-east-1a']; 45 | const subnets = splitSubnets('10.0.0.0/16', zones); 46 | const actual = buildAvailabilityZones(subnets, zones, { 47 | numNatGateway: 0, 48 | createDbSubnet: true, 49 | }); 50 | 51 | expect(actual).toEqual(expected); 52 | expect.assertions(1); 53 | }); 54 | 55 | it('builds a single AZ with a NAT Gateway and no DBSubnet', () => { 56 | const expected = { ...vpcSingleAZNatGWNoDB }; 57 | 58 | const zones = ['us-east-1a']; 59 | const subnets = splitSubnets('10.0.0.0/16', zones); 60 | const actual = buildAvailabilityZones(subnets, zones, { 61 | numNatGateway: 1, 62 | createDbSubnet: false, 63 | }); 64 | 65 | expect(actual).toEqual(expected); 66 | expect.assertions(1); 67 | }); 68 | 69 | it('builds a single AZ without a NAT Gateway and no DBSubnet', () => { 70 | const expected = { ...vpcSingleAZNoNatGWNoDB }; 71 | 72 | const zones = ['us-east-1a']; 73 | const subnets = splitSubnets('10.0.0.0/16', zones); 74 | const actual = buildAvailabilityZones(subnets, zones, { 75 | numNatGateway: 0, 76 | createDbSubnet: false, 77 | }); 78 | 79 | expect(actual).toEqual(expected); 80 | expect.assertions(1); 81 | }); 82 | 83 | it('builds multiple AZs with a NAT Gateway and DBSubnet', () => { 84 | const expected = { ...vpcMultipleAZNatGWDB }; 85 | 86 | const zones = ['us-east-1a', 'us-east-1b']; 87 | const subnets = splitSubnets('10.0.0.0/16', zones); 88 | const actual = buildAvailabilityZones(subnets, zones, { 89 | numNatGateway: 2, 90 | createDbSubnet: true, 91 | }); 92 | 93 | expect(actual).toEqual(expected); 94 | expect.assertions(1); 95 | }); 96 | 97 | it('builds multiple AZs without a NAT Gateway and DBSubnet', () => { 98 | const expected = { ...vpcMultipleAZNoNatGWDB }; 99 | 100 | const zones = ['us-east-1a', 'us-east-1b']; 101 | const subnets = splitSubnets('10.0.0.0/16', zones); 102 | const actual = buildAvailabilityZones(subnets, zones, { 103 | numNatGateway: 0, 104 | createDbSubnet: true, 105 | }); 106 | 107 | expect(actual).toEqual(expected); 108 | expect.assertions(1); 109 | }); 110 | 111 | it('builds multiple AZs with a NAT Gateway and no DBSubnet', () => { 112 | const expected = { ...vpcMultipleAZNatGWNoDB }; 113 | 114 | const zones = ['us-east-1a', 'us-east-1b']; 115 | const subnets = splitSubnets('10.0.0.0/16', zones); 116 | const actual = buildAvailabilityZones(subnets, zones, { 117 | numNatGateway: 2, 118 | createDbSubnet: false, 119 | }); 120 | 121 | expect(actual).toEqual(expected); 122 | expect.assertions(1); 123 | }); 124 | 125 | it('builds multiple AZs without a NAT Gateway and no DBSubnet', () => { 126 | const expected = { ...vpcMultipleAZNoNatGWNoDB }; 127 | 128 | const zones = ['us-east-1a', 'us-east-1b']; 129 | const subnets = splitSubnets('10.0.0.0/16', zones); 130 | const actual = buildAvailabilityZones(subnets, zones, { 131 | numNatGateway: 0, 132 | createDbSubnet: false, 133 | }); 134 | 135 | expect(actual).toEqual(expected); 136 | expect.assertions(1); 137 | }); 138 | 139 | it('builds multiple AZs with a single NAT Gateway and no DBSubnet', () => { 140 | const expected = { ...vpcMultipleAZSingleNatGWNoDB }; 141 | 142 | const zones = ['us-east-1a', 'us-east-1b']; 143 | const subnets = splitSubnets('10.0.0.0/16', zones); 144 | const actual = buildAvailabilityZones(subnets, zones, { 145 | numNatGateway: 1, 146 | createDbSubnet: false, 147 | }); 148 | 149 | expect(actual).toEqual(expected); 150 | expect.assertions(1); 151 | }); 152 | 153 | it('builds multiple AZs with a multple NAT Gateways and no DBSubnet', () => { 154 | const expected = { ...vpcMultipleAZMultipleNatGWNoDB }; 155 | 156 | const zones = ['us-east-1a', 'us-east-1b', 'us-east-1c']; 157 | const subnets = splitSubnets('10.0.0.0/16', zones); 158 | const actual = buildAvailabilityZones(subnets, zones, { 159 | numNatGateway: 2, 160 | createDbSubnet: false, 161 | }); 162 | 163 | expect(actual).toEqual(expected); 164 | expect.assertions(1); 165 | }); 166 | }); 167 | }); 168 | -------------------------------------------------------------------------------- /__tests__/fixtures/vpc_multiple_az_multiple_natgw_no_db.json: -------------------------------------------------------------------------------- 1 | { 2 | "EIP1": { 3 | "Type": "AWS::EC2::EIP", 4 | "Properties": { 5 | "Domain": "vpc" 6 | } 7 | }, 8 | "NatGateway1": { 9 | "Type": "AWS::EC2::NatGateway", 10 | "Properties": { 11 | "AllocationId": { 12 | "Fn::GetAtt": [ 13 | "EIP1", 14 | "AllocationId" 15 | ] 16 | }, 17 | "SubnetId": { 18 | "Ref": "PublicSubnet1" 19 | }, 20 | "Tags": [ 21 | { 22 | "Key": "Name", 23 | "Value": { 24 | "Fn::Sub": "${AWS::StackName}-${PublicSubnet1.AvailabilityZone}" 25 | } 26 | }, 27 | { 28 | "Key": "Network", 29 | "Value": "Public" 30 | } 31 | ] 32 | } 33 | }, 34 | "EIP2": { 35 | "Type": "AWS::EC2::EIP", 36 | "Properties": { 37 | "Domain": "vpc" 38 | } 39 | }, 40 | "NatGateway2": { 41 | "Type": "AWS::EC2::NatGateway", 42 | "Properties": { 43 | "AllocationId": { 44 | "Fn::GetAtt": [ 45 | "EIP2", 46 | "AllocationId" 47 | ] 48 | }, 49 | "SubnetId": { 50 | "Ref": "PublicSubnet2" 51 | }, 52 | "Tags": [ 53 | { 54 | "Key": "Name", 55 | "Value": { 56 | "Fn::Sub": "${AWS::StackName}-${PublicSubnet2.AvailabilityZone}" 57 | } 58 | }, 59 | { 60 | "Key": "Network", 61 | "Value": "Public" 62 | } 63 | ] 64 | } 65 | }, 66 | "AppSubnet1": { 67 | "Type": "AWS::EC2::Subnet", 68 | "Properties": { 69 | "AvailabilityZone": "us-east-1a", 70 | "CidrBlock": "10.0.0.0/21", 71 | "Tags": [ 72 | { 73 | "Key": "Name", 74 | "Value": { 75 | "Fn::Sub": "${AWS::StackName}-app-us-east-1a" 76 | } 77 | }, 78 | { 79 | "Key": "Network", 80 | "Value": "Private" 81 | } 82 | ], 83 | "VpcId": { 84 | "Ref": "VPC" 85 | } 86 | } 87 | }, 88 | "AppRouteTable1": { 89 | "Type": "AWS::EC2::RouteTable", 90 | "Properties": { 91 | "VpcId": { 92 | "Ref": "VPC" 93 | }, 94 | "Tags": [ 95 | { 96 | "Key": "Name", 97 | "Value": { 98 | "Fn::Sub": "${AWS::StackName}-app-${AppSubnet1.AvailabilityZone}" 99 | } 100 | }, 101 | { 102 | "Key": "Network", 103 | "Value": "Private" 104 | } 105 | ] 106 | } 107 | }, 108 | "AppRouteTableAssociation1": { 109 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 110 | "Properties": { 111 | "RouteTableId": { 112 | "Ref": "AppRouteTable1" 113 | }, 114 | "SubnetId": { 115 | "Ref": "AppSubnet1" 116 | } 117 | } 118 | }, 119 | "PublicSubnet1": { 120 | "Type": "AWS::EC2::Subnet", 121 | "Properties": { 122 | "AvailabilityZone": "us-east-1a", 123 | "CidrBlock": "10.0.8.0/22", 124 | "Tags": [ 125 | { 126 | "Key": "Name", 127 | "Value": { 128 | "Fn::Sub": "${AWS::StackName}-public-us-east-1a" 129 | } 130 | }, 131 | { 132 | "Key": "Network", 133 | "Value": "Public" 134 | } 135 | ], 136 | "VpcId": { 137 | "Ref": "VPC" 138 | } 139 | } 140 | }, 141 | "PublicRouteTable1": { 142 | "Type": "AWS::EC2::RouteTable", 143 | "Properties": { 144 | "VpcId": { 145 | "Ref": "VPC" 146 | }, 147 | "Tags": [ 148 | { 149 | "Key": "Name", 150 | "Value": { 151 | "Fn::Sub": "${AWS::StackName}-public-${PublicSubnet1.AvailabilityZone}" 152 | } 153 | }, 154 | { 155 | "Key": "Network", 156 | "Value": "Public" 157 | } 158 | ] 159 | } 160 | }, 161 | "PublicRouteTableAssociation1": { 162 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 163 | "Properties": { 164 | "RouteTableId": { 165 | "Ref": "PublicRouteTable1" 166 | }, 167 | "SubnetId": { 168 | "Ref": "PublicSubnet1" 169 | } 170 | } 171 | }, 172 | "PublicRoute1": { 173 | "Type": "AWS::EC2::Route", 174 | "Properties": { 175 | "DestinationCidrBlock": "0.0.0.0/0", 176 | "RouteTableId": { 177 | "Ref": "PublicRouteTable1" 178 | }, 179 | "GatewayId": { 180 | "Ref": "InternetGateway" 181 | } 182 | }, 183 | "DependsOn": [ 184 | "InternetGatewayAttachment" 185 | ] 186 | }, 187 | "AppRoute1": { 188 | "Type": "AWS::EC2::Route", 189 | "Properties": { 190 | "DestinationCidrBlock": "0.0.0.0/0", 191 | "RouteTableId": { 192 | "Ref": "AppRouteTable1" 193 | }, 194 | "NatGatewayId": { 195 | "Ref": "NatGateway1" 196 | } 197 | } 198 | }, 199 | "AppSubnet2": { 200 | "Type": "AWS::EC2::Subnet", 201 | "Properties": { 202 | "AvailabilityZone": "us-east-1b", 203 | "CidrBlock": "10.0.16.0/21", 204 | "Tags": [ 205 | { 206 | "Key": "Name", 207 | "Value": { 208 | "Fn::Sub": "${AWS::StackName}-app-us-east-1b" 209 | } 210 | }, 211 | { 212 | "Key": "Network", 213 | "Value": "Private" 214 | } 215 | ], 216 | "VpcId": { 217 | "Ref": "VPC" 218 | } 219 | } 220 | }, 221 | "AppRouteTable2": { 222 | "Type": "AWS::EC2::RouteTable", 223 | "Properties": { 224 | "VpcId": { 225 | "Ref": "VPC" 226 | }, 227 | "Tags": [ 228 | { 229 | "Key": "Name", 230 | "Value": { 231 | "Fn::Sub": "${AWS::StackName}-app-${AppSubnet2.AvailabilityZone}" 232 | } 233 | }, 234 | { 235 | "Key": "Network", 236 | "Value": "Private" 237 | } 238 | ] 239 | } 240 | }, 241 | "AppRouteTableAssociation2": { 242 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 243 | "Properties": { 244 | "RouteTableId": { 245 | "Ref": "AppRouteTable2" 246 | }, 247 | "SubnetId": { 248 | "Ref": "AppSubnet2" 249 | } 250 | } 251 | }, 252 | "PublicSubnet2": { 253 | "Type": "AWS::EC2::Subnet", 254 | "Properties": { 255 | "AvailabilityZone": "us-east-1b", 256 | "CidrBlock": "10.0.24.0/22", 257 | "Tags": [ 258 | { 259 | "Key": "Name", 260 | "Value": { 261 | "Fn::Sub": "${AWS::StackName}-public-us-east-1b" 262 | } 263 | }, 264 | { 265 | "Key": "Network", 266 | "Value": "Public" 267 | } 268 | ], 269 | "VpcId": { 270 | "Ref": "VPC" 271 | } 272 | } 273 | }, 274 | "PublicRouteTable2": { 275 | "Type": "AWS::EC2::RouteTable", 276 | "Properties": { 277 | "VpcId": { 278 | "Ref": "VPC" 279 | }, 280 | "Tags": [ 281 | { 282 | "Key": "Name", 283 | "Value": { 284 | "Fn::Sub": "${AWS::StackName}-public-${PublicSubnet2.AvailabilityZone}" 285 | } 286 | }, 287 | { 288 | "Key": "Network", 289 | "Value": "Public" 290 | } 291 | ] 292 | } 293 | }, 294 | "PublicRouteTableAssociation2": { 295 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 296 | "Properties": { 297 | "RouteTableId": { 298 | "Ref": "PublicRouteTable2" 299 | }, 300 | "SubnetId": { 301 | "Ref": "PublicSubnet2" 302 | } 303 | } 304 | }, 305 | "PublicRoute2": { 306 | "Type": "AWS::EC2::Route", 307 | "Properties": { 308 | "DestinationCidrBlock": "0.0.0.0/0", 309 | "RouteTableId": { 310 | "Ref": "PublicRouteTable2" 311 | }, 312 | "GatewayId": { 313 | "Ref": "InternetGateway" 314 | } 315 | }, 316 | "DependsOn": [ 317 | "InternetGatewayAttachment" 318 | ] 319 | }, 320 | "AppRoute2": { 321 | "Type": "AWS::EC2::Route", 322 | "Properties": { 323 | "DestinationCidrBlock": "0.0.0.0/0", 324 | "RouteTableId": { 325 | "Ref": "AppRouteTable2" 326 | }, 327 | "NatGatewayId": { 328 | "Ref": "NatGateway2" 329 | } 330 | } 331 | }, 332 | "AppSubnet3": { 333 | "Type": "AWS::EC2::Subnet", 334 | "Properties": { 335 | "AvailabilityZone": "us-east-1c", 336 | "CidrBlock": "10.0.32.0/21", 337 | "Tags": [ 338 | { 339 | "Key": "Name", 340 | "Value": { 341 | "Fn::Sub": "${AWS::StackName}-app-us-east-1c" 342 | } 343 | }, 344 | { 345 | "Key": "Network", 346 | "Value": "Private" 347 | } 348 | ], 349 | "VpcId": { 350 | "Ref": "VPC" 351 | } 352 | } 353 | }, 354 | "AppRouteTable3": { 355 | "Type": "AWS::EC2::RouteTable", 356 | "Properties": { 357 | "VpcId": { 358 | "Ref": "VPC" 359 | }, 360 | "Tags": [ 361 | { 362 | "Key": "Name", 363 | "Value": { 364 | "Fn::Sub": "${AWS::StackName}-app-${AppSubnet3.AvailabilityZone}" 365 | } 366 | }, 367 | { 368 | "Key": "Network", 369 | "Value": "Private" 370 | } 371 | ] 372 | } 373 | }, 374 | "AppRouteTableAssociation3": { 375 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 376 | "Properties": { 377 | "RouteTableId": { 378 | "Ref": "AppRouteTable3" 379 | }, 380 | "SubnetId": { 381 | "Ref": "AppSubnet3" 382 | } 383 | } 384 | }, 385 | "PublicSubnet3": { 386 | "Type": "AWS::EC2::Subnet", 387 | "Properties": { 388 | "AvailabilityZone": "us-east-1c", 389 | "CidrBlock": "10.0.40.0/22", 390 | "Tags": [ 391 | { 392 | "Key": "Name", 393 | "Value": { 394 | "Fn::Sub": "${AWS::StackName}-public-us-east-1c" 395 | } 396 | }, 397 | { 398 | "Key": "Network", 399 | "Value": "Public" 400 | } 401 | ], 402 | "VpcId": { 403 | "Ref": "VPC" 404 | } 405 | } 406 | }, 407 | "PublicRouteTable3": { 408 | "Type": "AWS::EC2::RouteTable", 409 | "Properties": { 410 | "VpcId": { 411 | "Ref": "VPC" 412 | }, 413 | "Tags": [ 414 | { 415 | "Key": "Name", 416 | "Value": { 417 | "Fn::Sub": "${AWS::StackName}-public-${PublicSubnet3.AvailabilityZone}" 418 | } 419 | }, 420 | { 421 | "Key": "Network", 422 | "Value": "Public" 423 | } 424 | ] 425 | } 426 | }, 427 | "PublicRouteTableAssociation3": { 428 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 429 | "Properties": { 430 | "RouteTableId": { 431 | "Ref": "PublicRouteTable3" 432 | }, 433 | "SubnetId": { 434 | "Ref": "PublicSubnet3" 435 | } 436 | } 437 | }, 438 | "PublicRoute3": { 439 | "Type": "AWS::EC2::Route", 440 | "Properties": { 441 | "DestinationCidrBlock": "0.0.0.0/0", 442 | "RouteTableId": { 443 | "Ref": "PublicRouteTable3" 444 | }, 445 | "GatewayId": { 446 | "Ref": "InternetGateway" 447 | } 448 | }, 449 | "DependsOn": [ 450 | "InternetGatewayAttachment" 451 | ] 452 | }, 453 | "AppRoute3": { 454 | "Type": "AWS::EC2::Route", 455 | "Properties": { 456 | "DestinationCidrBlock": "0.0.0.0/0", 457 | "RouteTableId": { 458 | "Ref": "AppRouteTable3" 459 | }, 460 | "NatGatewayId": { 461 | "Ref": "NatGateway1" 462 | } 463 | } 464 | } 465 | } 466 | -------------------------------------------------------------------------------- /__tests__/fixtures/vpc_multiple_az_natgw_db.json: -------------------------------------------------------------------------------- 1 | { 2 | "EIP1": { 3 | "Type": "AWS::EC2::EIP", 4 | "Properties": { 5 | "Domain": "vpc" 6 | } 7 | }, 8 | "NatGateway1": { 9 | "Type": "AWS::EC2::NatGateway", 10 | "Properties": { 11 | "AllocationId": { 12 | "Fn::GetAtt": [ 13 | "EIP1", 14 | "AllocationId" 15 | ] 16 | }, 17 | "SubnetId": { 18 | "Ref": "PublicSubnet1" 19 | }, 20 | "Tags": [ 21 | { 22 | "Key": "Name", 23 | "Value": { 24 | "Fn::Sub": "${AWS::StackName}-${PublicSubnet1.AvailabilityZone}" 25 | } 26 | }, 27 | { 28 | "Key": "Network", 29 | "Value": "Public" 30 | } 31 | ] 32 | } 33 | }, 34 | "EIP2": { 35 | "Type": "AWS::EC2::EIP", 36 | "Properties": { 37 | "Domain": "vpc" 38 | } 39 | }, 40 | "NatGateway2": { 41 | "Type": "AWS::EC2::NatGateway", 42 | "Properties": { 43 | "AllocationId": { 44 | "Fn::GetAtt": [ 45 | "EIP2", 46 | "AllocationId" 47 | ] 48 | }, 49 | "SubnetId": { 50 | "Ref": "PublicSubnet2" 51 | }, 52 | "Tags": [ 53 | { 54 | "Key": "Name", 55 | "Value": { 56 | "Fn::Sub": "${AWS::StackName}-${PublicSubnet2.AvailabilityZone}" 57 | } 58 | }, 59 | { 60 | "Key": "Network", 61 | "Value": "Public" 62 | } 63 | ] 64 | } 65 | }, 66 | "AppSubnet1": { 67 | "Type": "AWS::EC2::Subnet", 68 | "Properties": { 69 | "AvailabilityZone": "us-east-1a", 70 | "CidrBlock": "10.0.0.0/21", 71 | "Tags": [ 72 | { 73 | "Key": "Name", 74 | "Value": { 75 | "Fn::Sub": "${AWS::StackName}-app-us-east-1a" 76 | } 77 | }, 78 | { 79 | "Key": "Network", 80 | "Value": "Private" 81 | } 82 | ], 83 | "VpcId": { 84 | "Ref": "VPC" 85 | } 86 | } 87 | }, 88 | "AppRouteTable1": { 89 | "Type": "AWS::EC2::RouteTable", 90 | "Properties": { 91 | "VpcId": { 92 | "Ref": "VPC" 93 | }, 94 | "Tags": [ 95 | { 96 | "Key": "Name", 97 | "Value": { 98 | "Fn::Sub": "${AWS::StackName}-app-${AppSubnet1.AvailabilityZone}" 99 | } 100 | }, 101 | { 102 | "Key": "Network", 103 | "Value": "Private" 104 | } 105 | ] 106 | } 107 | }, 108 | "AppRouteTableAssociation1": { 109 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 110 | "Properties": { 111 | "RouteTableId": { 112 | "Ref": "AppRouteTable1" 113 | }, 114 | "SubnetId": { 115 | "Ref": "AppSubnet1" 116 | } 117 | } 118 | }, 119 | "PublicSubnet1": { 120 | "Type": "AWS::EC2::Subnet", 121 | "Properties": { 122 | "AvailabilityZone": "us-east-1a", 123 | "CidrBlock": "10.0.8.0/22", 124 | "Tags": [ 125 | { 126 | "Key": "Name", 127 | "Value": { 128 | "Fn::Sub": "${AWS::StackName}-public-us-east-1a" 129 | } 130 | }, 131 | { 132 | "Key": "Network", 133 | "Value": "Public" 134 | } 135 | ], 136 | "VpcId": { 137 | "Ref": "VPC" 138 | } 139 | } 140 | }, 141 | "PublicRouteTable1": { 142 | "Type": "AWS::EC2::RouteTable", 143 | "Properties": { 144 | "VpcId": { 145 | "Ref": "VPC" 146 | }, 147 | "Tags": [ 148 | { 149 | "Key": "Name", 150 | "Value": { 151 | "Fn::Sub": "${AWS::StackName}-public-${PublicSubnet1.AvailabilityZone}" 152 | } 153 | }, 154 | { 155 | "Key": "Network", 156 | "Value": "Public" 157 | } 158 | ] 159 | } 160 | }, 161 | "PublicRouteTableAssociation1": { 162 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 163 | "Properties": { 164 | "RouteTableId": { 165 | "Ref": "PublicRouteTable1" 166 | }, 167 | "SubnetId": { 168 | "Ref": "PublicSubnet1" 169 | } 170 | } 171 | }, 172 | "PublicRoute1": { 173 | "Type": "AWS::EC2::Route", 174 | "Properties": { 175 | "DestinationCidrBlock": "0.0.0.0/0", 176 | "RouteTableId": { 177 | "Ref": "PublicRouteTable1" 178 | }, 179 | "GatewayId": { 180 | "Ref": "InternetGateway" 181 | } 182 | }, 183 | "DependsOn": [ 184 | "InternetGatewayAttachment" 185 | ] 186 | }, 187 | "AppRoute1": { 188 | "Type": "AWS::EC2::Route", 189 | "Properties": { 190 | "DestinationCidrBlock": "0.0.0.0/0", 191 | "RouteTableId": { 192 | "Ref": "AppRouteTable1" 193 | }, 194 | "NatGatewayId": { 195 | "Ref": "NatGateway1" 196 | } 197 | } 198 | }, 199 | "DBSubnet1": { 200 | "Type": "AWS::EC2::Subnet", 201 | "Properties": { 202 | "AvailabilityZone": "us-east-1a", 203 | "CidrBlock": "10.0.12.0/22", 204 | "Tags": [ 205 | { 206 | "Key": "Name", 207 | "Value": { 208 | "Fn::Sub": "${AWS::StackName}-db-us-east-1a" 209 | } 210 | }, 211 | { 212 | "Key": "Network", 213 | "Value": "Private" 214 | } 215 | ], 216 | "VpcId": { 217 | "Ref": "VPC" 218 | } 219 | } 220 | }, 221 | "DBRouteTable1": { 222 | "Type": "AWS::EC2::RouteTable", 223 | "Properties": { 224 | "VpcId": { 225 | "Ref": "VPC" 226 | }, 227 | "Tags": [ 228 | { 229 | "Key": "Name", 230 | "Value": { 231 | "Fn::Sub": "${AWS::StackName}-db-${DBSubnet1.AvailabilityZone}" 232 | } 233 | }, 234 | { 235 | "Key": "Network", 236 | "Value": "Private" 237 | } 238 | ] 239 | } 240 | }, 241 | "DBRouteTableAssociation1": { 242 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 243 | "Properties": { 244 | "RouteTableId": { 245 | "Ref": "DBRouteTable1" 246 | }, 247 | "SubnetId": { 248 | "Ref": "DBSubnet1" 249 | } 250 | } 251 | }, 252 | "AppSubnet2": { 253 | "Type": "AWS::EC2::Subnet", 254 | "Properties": { 255 | "AvailabilityZone": "us-east-1b", 256 | "CidrBlock": "10.0.16.0/21", 257 | "Tags": [ 258 | { 259 | "Key": "Name", 260 | "Value": { 261 | "Fn::Sub": "${AWS::StackName}-app-us-east-1b" 262 | } 263 | }, 264 | { 265 | "Key": "Network", 266 | "Value": "Private" 267 | } 268 | ], 269 | "VpcId": { 270 | "Ref": "VPC" 271 | } 272 | } 273 | }, 274 | "AppRouteTable2": { 275 | "Type": "AWS::EC2::RouteTable", 276 | "Properties": { 277 | "VpcId": { 278 | "Ref": "VPC" 279 | }, 280 | "Tags": [ 281 | { 282 | "Key": "Name", 283 | "Value": { 284 | "Fn::Sub": "${AWS::StackName}-app-${AppSubnet2.AvailabilityZone}" 285 | } 286 | }, 287 | { 288 | "Key": "Network", 289 | "Value": "Private" 290 | } 291 | ] 292 | } 293 | }, 294 | "AppRouteTableAssociation2": { 295 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 296 | "Properties": { 297 | "RouteTableId": { 298 | "Ref": "AppRouteTable2" 299 | }, 300 | "SubnetId": { 301 | "Ref": "AppSubnet2" 302 | } 303 | } 304 | }, 305 | "PublicSubnet2": { 306 | "Type": "AWS::EC2::Subnet", 307 | "Properties": { 308 | "AvailabilityZone": "us-east-1b", 309 | "CidrBlock": "10.0.24.0/22", 310 | "Tags": [ 311 | { 312 | "Key": "Name", 313 | "Value": { 314 | "Fn::Sub": "${AWS::StackName}-public-us-east-1b" 315 | } 316 | }, 317 | { 318 | "Key": "Network", 319 | "Value": "Public" 320 | } 321 | ], 322 | "VpcId": { 323 | "Ref": "VPC" 324 | } 325 | } 326 | }, 327 | "PublicRouteTable2": { 328 | "Type": "AWS::EC2::RouteTable", 329 | "Properties": { 330 | "VpcId": { 331 | "Ref": "VPC" 332 | }, 333 | "Tags": [ 334 | { 335 | "Key": "Name", 336 | "Value": { 337 | "Fn::Sub": "${AWS::StackName}-public-${PublicSubnet2.AvailabilityZone}" 338 | } 339 | }, 340 | { 341 | "Key": "Network", 342 | "Value": "Public" 343 | } 344 | ] 345 | } 346 | }, 347 | "PublicRouteTableAssociation2": { 348 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 349 | "Properties": { 350 | "RouteTableId": { 351 | "Ref": "PublicRouteTable2" 352 | }, 353 | "SubnetId": { 354 | "Ref": "PublicSubnet2" 355 | } 356 | } 357 | }, 358 | "PublicRoute2": { 359 | "Type": "AWS::EC2::Route", 360 | "Properties": { 361 | "DestinationCidrBlock": "0.0.0.0/0", 362 | "RouteTableId": { 363 | "Ref": "PublicRouteTable2" 364 | }, 365 | "GatewayId": { 366 | "Ref": "InternetGateway" 367 | } 368 | }, 369 | "DependsOn": [ 370 | "InternetGatewayAttachment" 371 | ] 372 | }, 373 | "AppRoute2": { 374 | "Type": "AWS::EC2::Route", 375 | "Properties": { 376 | "DestinationCidrBlock": "0.0.0.0/0", 377 | "RouteTableId": { 378 | "Ref": "AppRouteTable2" 379 | }, 380 | "NatGatewayId": { 381 | "Ref": "NatGateway2" 382 | } 383 | } 384 | }, 385 | "DBSubnet2": { 386 | "Type": "AWS::EC2::Subnet", 387 | "Properties": { 388 | "AvailabilityZone": "us-east-1b", 389 | "CidrBlock": "10.0.28.0/22", 390 | "Tags": [ 391 | { 392 | "Key": "Name", 393 | "Value": { 394 | "Fn::Sub": "${AWS::StackName}-db-us-east-1b" 395 | } 396 | }, 397 | { 398 | "Key": "Network", 399 | "Value": "Private" 400 | } 401 | ], 402 | "VpcId": { 403 | "Ref": "VPC" 404 | } 405 | } 406 | }, 407 | "DBRouteTable2": { 408 | "Type": "AWS::EC2::RouteTable", 409 | "Properties": { 410 | "VpcId": { 411 | "Ref": "VPC" 412 | }, 413 | "Tags": [ 414 | { 415 | "Key": "Name", 416 | "Value": { 417 | "Fn::Sub": "${AWS::StackName}-db-${DBSubnet2.AvailabilityZone}" 418 | } 419 | }, 420 | { 421 | "Key": "Network", 422 | "Value": "Private" 423 | } 424 | ] 425 | } 426 | }, 427 | "DBRouteTableAssociation2": { 428 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 429 | "Properties": { 430 | "RouteTableId": { 431 | "Ref": "DBRouteTable2" 432 | }, 433 | "SubnetId": { 434 | "Ref": "DBSubnet2" 435 | } 436 | } 437 | } 438 | } 439 | -------------------------------------------------------------------------------- /__tests__/fixtures/vpc_multiple_az_natgw_no_db.json: -------------------------------------------------------------------------------- 1 | { 2 | "EIP1": { 3 | "Type": "AWS::EC2::EIP", 4 | "Properties": { 5 | "Domain": "vpc" 6 | } 7 | }, 8 | "NatGateway1": { 9 | "Type": "AWS::EC2::NatGateway", 10 | "Properties": { 11 | "AllocationId": { 12 | "Fn::GetAtt": [ 13 | "EIP1", 14 | "AllocationId" 15 | ] 16 | }, 17 | "SubnetId": { 18 | "Ref": "PublicSubnet1" 19 | }, 20 | "Tags": [ 21 | { 22 | "Key": "Name", 23 | "Value": { 24 | "Fn::Sub": "${AWS::StackName}-${PublicSubnet1.AvailabilityZone}" 25 | } 26 | }, 27 | { 28 | "Key": "Network", 29 | "Value": "Public" 30 | } 31 | ] 32 | } 33 | }, 34 | "EIP2": { 35 | "Type": "AWS::EC2::EIP", 36 | "Properties": { 37 | "Domain": "vpc" 38 | } 39 | }, 40 | "NatGateway2": { 41 | "Type": "AWS::EC2::NatGateway", 42 | "Properties": { 43 | "AllocationId": { 44 | "Fn::GetAtt": [ 45 | "EIP2", 46 | "AllocationId" 47 | ] 48 | }, 49 | "SubnetId": { 50 | "Ref": "PublicSubnet2" 51 | }, 52 | "Tags": [ 53 | { 54 | "Key": "Name", 55 | "Value": { 56 | "Fn::Sub": "${AWS::StackName}-${PublicSubnet2.AvailabilityZone}" 57 | } 58 | }, 59 | { 60 | "Key": "Network", 61 | "Value": "Public" 62 | } 63 | ] 64 | } 65 | }, 66 | "AppSubnet1": { 67 | "Type": "AWS::EC2::Subnet", 68 | "Properties": { 69 | "AvailabilityZone": "us-east-1a", 70 | "CidrBlock": "10.0.0.0/21", 71 | "Tags": [ 72 | { 73 | "Key": "Name", 74 | "Value": { 75 | "Fn::Sub": "${AWS::StackName}-app-us-east-1a" 76 | } 77 | }, 78 | { 79 | "Key": "Network", 80 | "Value": "Private" 81 | } 82 | ], 83 | "VpcId": { 84 | "Ref": "VPC" 85 | } 86 | } 87 | }, 88 | "AppRouteTable1": { 89 | "Type": "AWS::EC2::RouteTable", 90 | "Properties": { 91 | "VpcId": { 92 | "Ref": "VPC" 93 | }, 94 | "Tags": [ 95 | { 96 | "Key": "Name", 97 | "Value": { 98 | "Fn::Sub": "${AWS::StackName}-app-${AppSubnet1.AvailabilityZone}" 99 | } 100 | }, 101 | { 102 | "Key": "Network", 103 | "Value": "Private" 104 | } 105 | ] 106 | } 107 | }, 108 | "AppRouteTableAssociation1": { 109 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 110 | "Properties": { 111 | "RouteTableId": { 112 | "Ref": "AppRouteTable1" 113 | }, 114 | "SubnetId": { 115 | "Ref": "AppSubnet1" 116 | } 117 | } 118 | }, 119 | "PublicSubnet1": { 120 | "Type": "AWS::EC2::Subnet", 121 | "Properties": { 122 | "AvailabilityZone": "us-east-1a", 123 | "CidrBlock": "10.0.8.0/22", 124 | "Tags": [ 125 | { 126 | "Key": "Name", 127 | "Value": { 128 | "Fn::Sub": "${AWS::StackName}-public-us-east-1a" 129 | } 130 | }, 131 | { 132 | "Key": "Network", 133 | "Value": "Public" 134 | } 135 | ], 136 | "VpcId": { 137 | "Ref": "VPC" 138 | } 139 | } 140 | }, 141 | "PublicRouteTable1": { 142 | "Type": "AWS::EC2::RouteTable", 143 | "Properties": { 144 | "VpcId": { 145 | "Ref": "VPC" 146 | }, 147 | "Tags": [ 148 | { 149 | "Key": "Name", 150 | "Value": { 151 | "Fn::Sub": "${AWS::StackName}-public-${PublicSubnet1.AvailabilityZone}" 152 | } 153 | }, 154 | { 155 | "Key": "Network", 156 | "Value": "Public" 157 | } 158 | ] 159 | } 160 | }, 161 | "PublicRouteTableAssociation1": { 162 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 163 | "Properties": { 164 | "RouteTableId": { 165 | "Ref": "PublicRouteTable1" 166 | }, 167 | "SubnetId": { 168 | "Ref": "PublicSubnet1" 169 | } 170 | } 171 | }, 172 | "PublicRoute1": { 173 | "Type": "AWS::EC2::Route", 174 | "Properties": { 175 | "DestinationCidrBlock": "0.0.0.0/0", 176 | "RouteTableId": { 177 | "Ref": "PublicRouteTable1" 178 | }, 179 | "GatewayId": { 180 | "Ref": "InternetGateway" 181 | } 182 | }, 183 | "DependsOn": [ 184 | "InternetGatewayAttachment" 185 | ] 186 | }, 187 | "AppRoute1": { 188 | "Type": "AWS::EC2::Route", 189 | "Properties": { 190 | "DestinationCidrBlock": "0.0.0.0/0", 191 | "RouteTableId": { 192 | "Ref": "AppRouteTable1" 193 | }, 194 | "NatGatewayId": { 195 | "Ref": "NatGateway1" 196 | } 197 | } 198 | }, 199 | "AppSubnet2": { 200 | "Type": "AWS::EC2::Subnet", 201 | "Properties": { 202 | "AvailabilityZone": "us-east-1b", 203 | "CidrBlock": "10.0.16.0/21", 204 | "Tags": [ 205 | { 206 | "Key": "Name", 207 | "Value": { 208 | "Fn::Sub": "${AWS::StackName}-app-us-east-1b" 209 | } 210 | }, 211 | { 212 | "Key": "Network", 213 | "Value": "Private" 214 | } 215 | ], 216 | "VpcId": { 217 | "Ref": "VPC" 218 | } 219 | } 220 | }, 221 | "AppRouteTable2": { 222 | "Type": "AWS::EC2::RouteTable", 223 | "Properties": { 224 | "VpcId": { 225 | "Ref": "VPC" 226 | }, 227 | "Tags": [ 228 | { 229 | "Key": "Name", 230 | "Value": { 231 | "Fn::Sub": "${AWS::StackName}-app-${AppSubnet2.AvailabilityZone}" 232 | } 233 | }, 234 | { 235 | "Key": "Network", 236 | "Value": "Private" 237 | } 238 | ] 239 | } 240 | }, 241 | "AppRouteTableAssociation2": { 242 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 243 | "Properties": { 244 | "RouteTableId": { 245 | "Ref": "AppRouteTable2" 246 | }, 247 | "SubnetId": { 248 | "Ref": "AppSubnet2" 249 | } 250 | } 251 | }, 252 | "PublicSubnet2": { 253 | "Type": "AWS::EC2::Subnet", 254 | "Properties": { 255 | "AvailabilityZone": "us-east-1b", 256 | "CidrBlock": "10.0.24.0/22", 257 | "Tags": [ 258 | { 259 | "Key": "Name", 260 | "Value": { 261 | "Fn::Sub": "${AWS::StackName}-public-us-east-1b" 262 | } 263 | }, 264 | { 265 | "Key": "Network", 266 | "Value": "Public" 267 | } 268 | ], 269 | "VpcId": { 270 | "Ref": "VPC" 271 | } 272 | } 273 | }, 274 | "PublicRouteTable2": { 275 | "Type": "AWS::EC2::RouteTable", 276 | "Properties": { 277 | "VpcId": { 278 | "Ref": "VPC" 279 | }, 280 | "Tags": [ 281 | { 282 | "Key": "Name", 283 | "Value": { 284 | "Fn::Sub": "${AWS::StackName}-public-${PublicSubnet2.AvailabilityZone}" 285 | } 286 | }, 287 | { 288 | "Key": "Network", 289 | "Value": "Public" 290 | } 291 | ] 292 | } 293 | }, 294 | "PublicRouteTableAssociation2": { 295 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 296 | "Properties": { 297 | "RouteTableId": { 298 | "Ref": "PublicRouteTable2" 299 | }, 300 | "SubnetId": { 301 | "Ref": "PublicSubnet2" 302 | } 303 | } 304 | }, 305 | "PublicRoute2": { 306 | "Type": "AWS::EC2::Route", 307 | "Properties": { 308 | "DestinationCidrBlock": "0.0.0.0/0", 309 | "RouteTableId": { 310 | "Ref": "PublicRouteTable2" 311 | }, 312 | "GatewayId": { 313 | "Ref": "InternetGateway" 314 | } 315 | }, 316 | "DependsOn": [ 317 | "InternetGatewayAttachment" 318 | ] 319 | }, 320 | "AppRoute2": { 321 | "Type": "AWS::EC2::Route", 322 | "Properties": { 323 | "DestinationCidrBlock": "0.0.0.0/0", 324 | "RouteTableId": { 325 | "Ref": "AppRouteTable2" 326 | }, 327 | "NatGatewayId": { 328 | "Ref": "NatGateway2" 329 | } 330 | } 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /__tests__/fixtures/vpc_multiple_az_no_natgw_db.json: -------------------------------------------------------------------------------- 1 | { 2 | "AppSubnet1": { 3 | "Type": "AWS::EC2::Subnet", 4 | "Properties": { 5 | "AvailabilityZone": "us-east-1a", 6 | "CidrBlock": "10.0.0.0/21", 7 | "Tags": [ 8 | { 9 | "Key": "Name", 10 | "Value": { 11 | "Fn::Sub": "${AWS::StackName}-app-us-east-1a" 12 | } 13 | }, 14 | { 15 | "Key": "Network", 16 | "Value": "Private" 17 | } 18 | ], 19 | "VpcId": { 20 | "Ref": "VPC" 21 | } 22 | } 23 | }, 24 | "AppRouteTable1": { 25 | "Type": "AWS::EC2::RouteTable", 26 | "Properties": { 27 | "VpcId": { 28 | "Ref": "VPC" 29 | }, 30 | "Tags": [ 31 | { 32 | "Key": "Name", 33 | "Value": { 34 | "Fn::Sub": "${AWS::StackName}-app-${AppSubnet1.AvailabilityZone}" 35 | } 36 | }, 37 | { 38 | "Key": "Network", 39 | "Value": "Private" 40 | } 41 | ] 42 | } 43 | }, 44 | "AppRouteTableAssociation1": { 45 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 46 | "Properties": { 47 | "RouteTableId": { 48 | "Ref": "AppRouteTable1" 49 | }, 50 | "SubnetId": { 51 | "Ref": "AppSubnet1" 52 | } 53 | } 54 | }, 55 | "PublicSubnet1": { 56 | "Type": "AWS::EC2::Subnet", 57 | "Properties": { 58 | "AvailabilityZone": "us-east-1a", 59 | "CidrBlock": "10.0.8.0/22", 60 | "Tags": [ 61 | { 62 | "Key": "Name", 63 | "Value": { 64 | "Fn::Sub": "${AWS::StackName}-public-us-east-1a" 65 | } 66 | }, 67 | { 68 | "Key": "Network", 69 | "Value": "Public" 70 | } 71 | ], 72 | "VpcId": { 73 | "Ref": "VPC" 74 | } 75 | } 76 | }, 77 | "PublicRouteTable1": { 78 | "Type": "AWS::EC2::RouteTable", 79 | "Properties": { 80 | "VpcId": { 81 | "Ref": "VPC" 82 | }, 83 | "Tags": [ 84 | { 85 | "Key": "Name", 86 | "Value": { 87 | "Fn::Sub": "${AWS::StackName}-public-${PublicSubnet1.AvailabilityZone}" 88 | } 89 | }, 90 | { 91 | "Key": "Network", 92 | "Value": "Public" 93 | } 94 | ] 95 | } 96 | }, 97 | "PublicRouteTableAssociation1": { 98 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 99 | "Properties": { 100 | "RouteTableId": { 101 | "Ref": "PublicRouteTable1" 102 | }, 103 | "SubnetId": { 104 | "Ref": "PublicSubnet1" 105 | } 106 | } 107 | }, 108 | "PublicRoute1": { 109 | "Type": "AWS::EC2::Route", 110 | "Properties": { 111 | "DestinationCidrBlock": "0.0.0.0/0", 112 | "RouteTableId": { 113 | "Ref": "PublicRouteTable1" 114 | }, 115 | "GatewayId": { 116 | "Ref": "InternetGateway" 117 | } 118 | }, 119 | "DependsOn": [ 120 | "InternetGatewayAttachment" 121 | ] 122 | }, 123 | "DBSubnet1": { 124 | "Type": "AWS::EC2::Subnet", 125 | "Properties": { 126 | "AvailabilityZone": "us-east-1a", 127 | "CidrBlock": "10.0.12.0/22", 128 | "Tags": [ 129 | { 130 | "Key": "Name", 131 | "Value": { 132 | "Fn::Sub": "${AWS::StackName}-db-us-east-1a" 133 | } 134 | }, 135 | { 136 | "Key": "Network", 137 | "Value": "Private" 138 | } 139 | ], 140 | "VpcId": { 141 | "Ref": "VPC" 142 | } 143 | } 144 | }, 145 | "DBRouteTable1": { 146 | "Type": "AWS::EC2::RouteTable", 147 | "Properties": { 148 | "VpcId": { 149 | "Ref": "VPC" 150 | }, 151 | "Tags": [ 152 | { 153 | "Key": "Name", 154 | "Value": { 155 | "Fn::Sub": "${AWS::StackName}-db-${DBSubnet1.AvailabilityZone}" 156 | } 157 | }, 158 | { 159 | "Key": "Network", 160 | "Value": "Private" 161 | } 162 | ] 163 | } 164 | }, 165 | "DBRouteTableAssociation1": { 166 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 167 | "Properties": { 168 | "RouteTableId": { 169 | "Ref": "DBRouteTable1" 170 | }, 171 | "SubnetId": { 172 | "Ref": "DBSubnet1" 173 | } 174 | } 175 | }, 176 | "AppSubnet2": { 177 | "Type": "AWS::EC2::Subnet", 178 | "Properties": { 179 | "AvailabilityZone": "us-east-1b", 180 | "CidrBlock": "10.0.16.0/21", 181 | "Tags": [ 182 | { 183 | "Key": "Name", 184 | "Value": { 185 | "Fn::Sub": "${AWS::StackName}-app-us-east-1b" 186 | } 187 | }, 188 | { 189 | "Key": "Network", 190 | "Value": "Private" 191 | } 192 | ], 193 | "VpcId": { 194 | "Ref": "VPC" 195 | } 196 | } 197 | }, 198 | "AppRouteTable2": { 199 | "Type": "AWS::EC2::RouteTable", 200 | "Properties": { 201 | "VpcId": { 202 | "Ref": "VPC" 203 | }, 204 | "Tags": [ 205 | { 206 | "Key": "Name", 207 | "Value": { 208 | "Fn::Sub": "${AWS::StackName}-app-${AppSubnet2.AvailabilityZone}" 209 | } 210 | }, 211 | { 212 | "Key": "Network", 213 | "Value": "Private" 214 | } 215 | ] 216 | } 217 | }, 218 | "AppRouteTableAssociation2": { 219 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 220 | "Properties": { 221 | "RouteTableId": { 222 | "Ref": "AppRouteTable2" 223 | }, 224 | "SubnetId": { 225 | "Ref": "AppSubnet2" 226 | } 227 | } 228 | }, 229 | "PublicSubnet2": { 230 | "Type": "AWS::EC2::Subnet", 231 | "Properties": { 232 | "AvailabilityZone": "us-east-1b", 233 | "CidrBlock": "10.0.24.0/22", 234 | "Tags": [ 235 | { 236 | "Key": "Name", 237 | "Value": { 238 | "Fn::Sub": "${AWS::StackName}-public-us-east-1b" 239 | } 240 | }, 241 | { 242 | "Key": "Network", 243 | "Value": "Public" 244 | } 245 | ], 246 | "VpcId": { 247 | "Ref": "VPC" 248 | } 249 | } 250 | }, 251 | "PublicRouteTable2": { 252 | "Type": "AWS::EC2::RouteTable", 253 | "Properties": { 254 | "VpcId": { 255 | "Ref": "VPC" 256 | }, 257 | "Tags": [ 258 | { 259 | "Key": "Name", 260 | "Value": { 261 | "Fn::Sub": "${AWS::StackName}-public-${PublicSubnet2.AvailabilityZone}" 262 | } 263 | }, 264 | { 265 | "Key": "Network", 266 | "Value": "Public" 267 | } 268 | ] 269 | } 270 | }, 271 | "PublicRouteTableAssociation2": { 272 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 273 | "Properties": { 274 | "RouteTableId": { 275 | "Ref": "PublicRouteTable2" 276 | }, 277 | "SubnetId": { 278 | "Ref": "PublicSubnet2" 279 | } 280 | } 281 | }, 282 | "PublicRoute2": { 283 | "Type": "AWS::EC2::Route", 284 | "Properties": { 285 | "DestinationCidrBlock": "0.0.0.0/0", 286 | "RouteTableId": { 287 | "Ref": "PublicRouteTable2" 288 | }, 289 | "GatewayId": { 290 | "Ref": "InternetGateway" 291 | } 292 | }, 293 | "DependsOn": [ 294 | "InternetGatewayAttachment" 295 | ] 296 | }, 297 | "DBSubnet2": { 298 | "Type": "AWS::EC2::Subnet", 299 | "Properties": { 300 | "AvailabilityZone": "us-east-1b", 301 | "CidrBlock": "10.0.28.0/22", 302 | "Tags": [ 303 | { 304 | "Key": "Name", 305 | "Value": { 306 | "Fn::Sub": "${AWS::StackName}-db-us-east-1b" 307 | } 308 | }, 309 | { 310 | "Key": "Network", 311 | "Value": "Private" 312 | } 313 | ], 314 | "VpcId": { 315 | "Ref": "VPC" 316 | } 317 | } 318 | }, 319 | "DBRouteTable2": { 320 | "Type": "AWS::EC2::RouteTable", 321 | "Properties": { 322 | "VpcId": { 323 | "Ref": "VPC" 324 | }, 325 | "Tags": [ 326 | { 327 | "Key": "Name", 328 | "Value": { 329 | "Fn::Sub": "${AWS::StackName}-db-${DBSubnet2.AvailabilityZone}" 330 | } 331 | }, 332 | { 333 | "Key": "Network", 334 | "Value": "Private" 335 | } 336 | ] 337 | } 338 | }, 339 | "DBRouteTableAssociation2": { 340 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 341 | "Properties": { 342 | "RouteTableId": { 343 | "Ref": "DBRouteTable2" 344 | }, 345 | "SubnetId": { 346 | "Ref": "DBSubnet2" 347 | } 348 | } 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /__tests__/fixtures/vpc_multiple_az_no_natgw_no_db.json: -------------------------------------------------------------------------------- 1 | { 2 | "AppSubnet1": { 3 | "Type": "AWS::EC2::Subnet", 4 | "Properties": { 5 | "AvailabilityZone": "us-east-1a", 6 | "CidrBlock": "10.0.0.0/21", 7 | "Tags": [ 8 | { 9 | "Key": "Name", 10 | "Value": { 11 | "Fn::Sub": "${AWS::StackName}-app-us-east-1a" 12 | } 13 | }, 14 | { 15 | "Key": "Network", 16 | "Value": "Private" 17 | } 18 | ], 19 | "VpcId": { 20 | "Ref": "VPC" 21 | } 22 | } 23 | }, 24 | "AppRouteTable1": { 25 | "Type": "AWS::EC2::RouteTable", 26 | "Properties": { 27 | "VpcId": { 28 | "Ref": "VPC" 29 | }, 30 | "Tags": [ 31 | { 32 | "Key": "Name", 33 | "Value": { 34 | "Fn::Sub": "${AWS::StackName}-app-${AppSubnet1.AvailabilityZone}" 35 | } 36 | }, 37 | { 38 | "Key": "Network", 39 | "Value": "Private" 40 | } 41 | ] 42 | } 43 | }, 44 | "AppRouteTableAssociation1": { 45 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 46 | "Properties": { 47 | "RouteTableId": { 48 | "Ref": "AppRouteTable1" 49 | }, 50 | "SubnetId": { 51 | "Ref": "AppSubnet1" 52 | } 53 | } 54 | }, 55 | "PublicSubnet1": { 56 | "Type": "AWS::EC2::Subnet", 57 | "Properties": { 58 | "AvailabilityZone": "us-east-1a", 59 | "CidrBlock": "10.0.8.0/22", 60 | "Tags": [ 61 | { 62 | "Key": "Name", 63 | "Value": { 64 | "Fn::Sub": "${AWS::StackName}-public-us-east-1a" 65 | } 66 | }, 67 | { 68 | "Key": "Network", 69 | "Value": "Public" 70 | } 71 | ], 72 | "VpcId": { 73 | "Ref": "VPC" 74 | } 75 | } 76 | }, 77 | "PublicRouteTable1": { 78 | "Type": "AWS::EC2::RouteTable", 79 | "Properties": { 80 | "VpcId": { 81 | "Ref": "VPC" 82 | }, 83 | "Tags": [ 84 | { 85 | "Key": "Name", 86 | "Value": { 87 | "Fn::Sub": "${AWS::StackName}-public-${PublicSubnet1.AvailabilityZone}" 88 | } 89 | }, 90 | { 91 | "Key": "Network", 92 | "Value": "Public" 93 | } 94 | ] 95 | } 96 | }, 97 | "PublicRouteTableAssociation1": { 98 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 99 | "Properties": { 100 | "RouteTableId": { 101 | "Ref": "PublicRouteTable1" 102 | }, 103 | "SubnetId": { 104 | "Ref": "PublicSubnet1" 105 | } 106 | } 107 | }, 108 | "PublicRoute1": { 109 | "Type": "AWS::EC2::Route", 110 | "Properties": { 111 | "DestinationCidrBlock": "0.0.0.0/0", 112 | "RouteTableId": { 113 | "Ref": "PublicRouteTable1" 114 | }, 115 | "GatewayId": { 116 | "Ref": "InternetGateway" 117 | } 118 | }, 119 | "DependsOn": [ 120 | "InternetGatewayAttachment" 121 | ] 122 | }, 123 | "AppSubnet2": { 124 | "Type": "AWS::EC2::Subnet", 125 | "Properties": { 126 | "AvailabilityZone": "us-east-1b", 127 | "CidrBlock": "10.0.16.0/21", 128 | "Tags": [ 129 | { 130 | "Key": "Name", 131 | "Value": { 132 | "Fn::Sub": "${AWS::StackName}-app-us-east-1b" 133 | } 134 | }, 135 | { 136 | "Key": "Network", 137 | "Value": "Private" 138 | } 139 | ], 140 | "VpcId": { 141 | "Ref": "VPC" 142 | } 143 | } 144 | }, 145 | "AppRouteTable2": { 146 | "Type": "AWS::EC2::RouteTable", 147 | "Properties": { 148 | "VpcId": { 149 | "Ref": "VPC" 150 | }, 151 | "Tags": [ 152 | { 153 | "Key": "Name", 154 | "Value": { 155 | "Fn::Sub": "${AWS::StackName}-app-${AppSubnet2.AvailabilityZone}" 156 | } 157 | }, 158 | { 159 | "Key": "Network", 160 | "Value": "Private" 161 | } 162 | ] 163 | } 164 | }, 165 | "AppRouteTableAssociation2": { 166 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 167 | "Properties": { 168 | "RouteTableId": { 169 | "Ref": "AppRouteTable2" 170 | }, 171 | "SubnetId": { 172 | "Ref": "AppSubnet2" 173 | } 174 | } 175 | }, 176 | "PublicSubnet2": { 177 | "Type": "AWS::EC2::Subnet", 178 | "Properties": { 179 | "AvailabilityZone": "us-east-1b", 180 | "CidrBlock": "10.0.24.0/22", 181 | "Tags": [ 182 | { 183 | "Key": "Name", 184 | "Value": { 185 | "Fn::Sub": "${AWS::StackName}-public-us-east-1b" 186 | } 187 | }, 188 | { 189 | "Key": "Network", 190 | "Value": "Public" 191 | } 192 | ], 193 | "VpcId": { 194 | "Ref": "VPC" 195 | } 196 | } 197 | }, 198 | "PublicRouteTable2": { 199 | "Type": "AWS::EC2::RouteTable", 200 | "Properties": { 201 | "VpcId": { 202 | "Ref": "VPC" 203 | }, 204 | "Tags": [ 205 | { 206 | "Key": "Name", 207 | "Value": { 208 | "Fn::Sub": "${AWS::StackName}-public-${PublicSubnet2.AvailabilityZone}" 209 | } 210 | }, 211 | { 212 | "Key": "Network", 213 | "Value": "Public" 214 | } 215 | ] 216 | } 217 | }, 218 | "PublicRouteTableAssociation2": { 219 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 220 | "Properties": { 221 | "RouteTableId": { 222 | "Ref": "PublicRouteTable2" 223 | }, 224 | "SubnetId": { 225 | "Ref": "PublicSubnet2" 226 | } 227 | } 228 | }, 229 | "PublicRoute2": { 230 | "Type": "AWS::EC2::Route", 231 | "Properties": { 232 | "DestinationCidrBlock": "0.0.0.0/0", 233 | "RouteTableId": { 234 | "Ref": "PublicRouteTable2" 235 | }, 236 | "GatewayId": { 237 | "Ref": "InternetGateway" 238 | } 239 | }, 240 | "DependsOn": [ 241 | "InternetGatewayAttachment" 242 | ] 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /__tests__/fixtures/vpc_multiple_az_single_natgw_no_db.json: -------------------------------------------------------------------------------- 1 | { 2 | "EIP1": { 3 | "Type": "AWS::EC2::EIP", 4 | "Properties": { 5 | "Domain": "vpc" 6 | } 7 | }, 8 | "NatGateway1": { 9 | "Type": "AWS::EC2::NatGateway", 10 | "Properties": { 11 | "AllocationId": { 12 | "Fn::GetAtt": [ 13 | "EIP1", 14 | "AllocationId" 15 | ] 16 | }, 17 | "SubnetId": { 18 | "Ref": "PublicSubnet1" 19 | }, 20 | "Tags": [ 21 | { 22 | "Key": "Name", 23 | "Value": { 24 | "Fn::Sub": "${AWS::StackName}-${PublicSubnet1.AvailabilityZone}" 25 | } 26 | }, 27 | { 28 | "Key": "Network", 29 | "Value": "Public" 30 | } 31 | ] 32 | } 33 | }, 34 | "AppSubnet1": { 35 | "Type": "AWS::EC2::Subnet", 36 | "Properties": { 37 | "AvailabilityZone": "us-east-1a", 38 | "CidrBlock": "10.0.0.0/21", 39 | "Tags": [ 40 | { 41 | "Key": "Name", 42 | "Value": { 43 | "Fn::Sub": "${AWS::StackName}-app-us-east-1a" 44 | } 45 | }, 46 | { 47 | "Key": "Network", 48 | "Value": "Private" 49 | } 50 | ], 51 | "VpcId": { 52 | "Ref": "VPC" 53 | } 54 | } 55 | }, 56 | "AppRouteTable1": { 57 | "Type": "AWS::EC2::RouteTable", 58 | "Properties": { 59 | "VpcId": { 60 | "Ref": "VPC" 61 | }, 62 | "Tags": [ 63 | { 64 | "Key": "Name", 65 | "Value": { 66 | "Fn::Sub": "${AWS::StackName}-app-${AppSubnet1.AvailabilityZone}" 67 | } 68 | }, 69 | { 70 | "Key": "Network", 71 | "Value": "Private" 72 | } 73 | ] 74 | } 75 | }, 76 | "AppRouteTableAssociation1": { 77 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 78 | "Properties": { 79 | "RouteTableId": { 80 | "Ref": "AppRouteTable1" 81 | }, 82 | "SubnetId": { 83 | "Ref": "AppSubnet1" 84 | } 85 | } 86 | }, 87 | "PublicSubnet1": { 88 | "Type": "AWS::EC2::Subnet", 89 | "Properties": { 90 | "AvailabilityZone": "us-east-1a", 91 | "CidrBlock": "10.0.8.0/22", 92 | "Tags": [ 93 | { 94 | "Key": "Name", 95 | "Value": { 96 | "Fn::Sub": "${AWS::StackName}-public-us-east-1a" 97 | } 98 | }, 99 | { 100 | "Key": "Network", 101 | "Value": "Public" 102 | } 103 | ], 104 | "VpcId": { 105 | "Ref": "VPC" 106 | } 107 | } 108 | }, 109 | "PublicRouteTable1": { 110 | "Type": "AWS::EC2::RouteTable", 111 | "Properties": { 112 | "VpcId": { 113 | "Ref": "VPC" 114 | }, 115 | "Tags": [ 116 | { 117 | "Key": "Name", 118 | "Value": { 119 | "Fn::Sub": "${AWS::StackName}-public-${PublicSubnet1.AvailabilityZone}" 120 | } 121 | }, 122 | { 123 | "Key": "Network", 124 | "Value": "Public" 125 | } 126 | ] 127 | } 128 | }, 129 | "PublicRouteTableAssociation1": { 130 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 131 | "Properties": { 132 | "RouteTableId": { 133 | "Ref": "PublicRouteTable1" 134 | }, 135 | "SubnetId": { 136 | "Ref": "PublicSubnet1" 137 | } 138 | } 139 | }, 140 | "PublicRoute1": { 141 | "Type": "AWS::EC2::Route", 142 | "Properties": { 143 | "DestinationCidrBlock": "0.0.0.0/0", 144 | "RouteTableId": { 145 | "Ref": "PublicRouteTable1" 146 | }, 147 | "GatewayId": { 148 | "Ref": "InternetGateway" 149 | } 150 | }, 151 | "DependsOn": [ 152 | "InternetGatewayAttachment" 153 | ] 154 | }, 155 | "AppRoute1": { 156 | "Type": "AWS::EC2::Route", 157 | "Properties": { 158 | "DestinationCidrBlock": "0.0.0.0/0", 159 | "RouteTableId": { 160 | "Ref": "AppRouteTable1" 161 | }, 162 | "NatGatewayId": { 163 | "Ref": "NatGateway1" 164 | } 165 | } 166 | }, 167 | "AppSubnet2": { 168 | "Type": "AWS::EC2::Subnet", 169 | "Properties": { 170 | "AvailabilityZone": "us-east-1b", 171 | "CidrBlock": "10.0.16.0/21", 172 | "Tags": [ 173 | { 174 | "Key": "Name", 175 | "Value": { 176 | "Fn::Sub": "${AWS::StackName}-app-us-east-1b" 177 | } 178 | }, 179 | { 180 | "Key": "Network", 181 | "Value": "Private" 182 | } 183 | ], 184 | "VpcId": { 185 | "Ref": "VPC" 186 | } 187 | } 188 | }, 189 | "AppRouteTable2": { 190 | "Type": "AWS::EC2::RouteTable", 191 | "Properties": { 192 | "VpcId": { 193 | "Ref": "VPC" 194 | }, 195 | "Tags": [ 196 | { 197 | "Key": "Name", 198 | "Value": { 199 | "Fn::Sub": "${AWS::StackName}-app-${AppSubnet2.AvailabilityZone}" 200 | } 201 | }, 202 | { 203 | "Key": "Network", 204 | "Value": "Private" 205 | } 206 | ] 207 | } 208 | }, 209 | "AppRouteTableAssociation2": { 210 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 211 | "Properties": { 212 | "RouteTableId": { 213 | "Ref": "AppRouteTable2" 214 | }, 215 | "SubnetId": { 216 | "Ref": "AppSubnet2" 217 | } 218 | } 219 | }, 220 | "PublicSubnet2": { 221 | "Type": "AWS::EC2::Subnet", 222 | "Properties": { 223 | "AvailabilityZone": "us-east-1b", 224 | "CidrBlock": "10.0.24.0/22", 225 | "Tags": [ 226 | { 227 | "Key": "Name", 228 | "Value": { 229 | "Fn::Sub": "${AWS::StackName}-public-us-east-1b" 230 | } 231 | }, 232 | { 233 | "Key": "Network", 234 | "Value": "Public" 235 | } 236 | ], 237 | "VpcId": { 238 | "Ref": "VPC" 239 | } 240 | } 241 | }, 242 | "PublicRouteTable2": { 243 | "Type": "AWS::EC2::RouteTable", 244 | "Properties": { 245 | "VpcId": { 246 | "Ref": "VPC" 247 | }, 248 | "Tags": [ 249 | { 250 | "Key": "Name", 251 | "Value": { 252 | "Fn::Sub": "${AWS::StackName}-public-${PublicSubnet2.AvailabilityZone}" 253 | } 254 | }, 255 | { 256 | "Key": "Network", 257 | "Value": "Public" 258 | } 259 | ] 260 | } 261 | }, 262 | "PublicRouteTableAssociation2": { 263 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 264 | "Properties": { 265 | "RouteTableId": { 266 | "Ref": "PublicRouteTable2" 267 | }, 268 | "SubnetId": { 269 | "Ref": "PublicSubnet2" 270 | } 271 | } 272 | }, 273 | "PublicRoute2": { 274 | "Type": "AWS::EC2::Route", 275 | "Properties": { 276 | "DestinationCidrBlock": "0.0.0.0/0", 277 | "RouteTableId": { 278 | "Ref": "PublicRouteTable2" 279 | }, 280 | "GatewayId": { 281 | "Ref": "InternetGateway" 282 | } 283 | }, 284 | "DependsOn": [ 285 | "InternetGatewayAttachment" 286 | ] 287 | }, 288 | "AppRoute2": { 289 | "Type": "AWS::EC2::Route", 290 | "Properties": { 291 | "DestinationCidrBlock": "0.0.0.0/0", 292 | "RouteTableId": { 293 | "Ref": "AppRouteTable2" 294 | }, 295 | "NatGatewayId": { 296 | "Ref": "NatGateway1" 297 | } 298 | } 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /__tests__/fixtures/vpc_single_az_natgw_db.json: -------------------------------------------------------------------------------- 1 | { 2 | "EIP1": { 3 | "Type": "AWS::EC2::EIP", 4 | "Properties": { 5 | "Domain": "vpc" 6 | } 7 | }, 8 | "NatGateway1": { 9 | "Type": "AWS::EC2::NatGateway", 10 | "Properties": { 11 | "AllocationId": { 12 | "Fn::GetAtt": [ 13 | "EIP1", 14 | "AllocationId" 15 | ] 16 | }, 17 | "SubnetId": { 18 | "Ref": "PublicSubnet1" 19 | }, 20 | "Tags": [ 21 | { 22 | "Key": "Name", 23 | "Value": { 24 | "Fn::Sub": "${AWS::StackName}-${PublicSubnet1.AvailabilityZone}" 25 | } 26 | }, 27 | { 28 | "Key": "Network", 29 | "Value": "Public" 30 | } 31 | ] 32 | } 33 | }, 34 | "AppSubnet1": { 35 | "Type": "AWS::EC2::Subnet", 36 | "Properties": { 37 | "AvailabilityZone": "us-east-1a", 38 | "CidrBlock": "10.0.0.0/21", 39 | "Tags": [ 40 | { 41 | "Key": "Name", 42 | "Value": { 43 | "Fn::Sub": "${AWS::StackName}-app-us-east-1a" 44 | } 45 | }, 46 | { 47 | "Key": "Network", 48 | "Value": "Private" 49 | } 50 | ], 51 | "VpcId": { 52 | "Ref": "VPC" 53 | } 54 | } 55 | }, 56 | "AppRouteTable1": { 57 | "Type": "AWS::EC2::RouteTable", 58 | "Properties": { 59 | "VpcId": { 60 | "Ref": "VPC" 61 | }, 62 | "Tags": [ 63 | { 64 | "Key": "Name", 65 | "Value": { 66 | "Fn::Sub": "${AWS::StackName}-app-${AppSubnet1.AvailabilityZone}" 67 | } 68 | }, 69 | { 70 | "Key": "Network", 71 | "Value": "Private" 72 | } 73 | ] 74 | } 75 | }, 76 | "AppRouteTableAssociation1": { 77 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 78 | "Properties": { 79 | "RouteTableId": { 80 | "Ref": "AppRouteTable1" 81 | }, 82 | "SubnetId": { 83 | "Ref": "AppSubnet1" 84 | } 85 | } 86 | }, 87 | "PublicSubnet1": { 88 | "Type": "AWS::EC2::Subnet", 89 | "Properties": { 90 | "AvailabilityZone": "us-east-1a", 91 | "CidrBlock": "10.0.8.0/22", 92 | "Tags": [ 93 | { 94 | "Key": "Name", 95 | "Value": { 96 | "Fn::Sub": "${AWS::StackName}-public-us-east-1a" 97 | } 98 | }, 99 | { 100 | "Key": "Network", 101 | "Value": "Public" 102 | } 103 | ], 104 | "VpcId": { 105 | "Ref": "VPC" 106 | } 107 | } 108 | }, 109 | "PublicRouteTable1": { 110 | "Type": "AWS::EC2::RouteTable", 111 | "Properties": { 112 | "VpcId": { 113 | "Ref": "VPC" 114 | }, 115 | "Tags": [ 116 | { 117 | "Key": "Name", 118 | "Value": { 119 | "Fn::Sub": "${AWS::StackName}-public-${PublicSubnet1.AvailabilityZone}" 120 | } 121 | }, 122 | { 123 | "Key": "Network", 124 | "Value": "Public" 125 | } 126 | ] 127 | } 128 | }, 129 | "PublicRouteTableAssociation1": { 130 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 131 | "Properties": { 132 | "RouteTableId": { 133 | "Ref": "PublicRouteTable1" 134 | }, 135 | "SubnetId": { 136 | "Ref": "PublicSubnet1" 137 | } 138 | } 139 | }, 140 | "PublicRoute1": { 141 | "Type": "AWS::EC2::Route", 142 | "Properties": { 143 | "DestinationCidrBlock": "0.0.0.0/0", 144 | "RouteTableId": { 145 | "Ref": "PublicRouteTable1" 146 | }, 147 | "GatewayId": { 148 | "Ref": "InternetGateway" 149 | } 150 | }, 151 | "DependsOn": [ 152 | "InternetGatewayAttachment" 153 | ] 154 | }, 155 | "AppRoute1": { 156 | "Type": "AWS::EC2::Route", 157 | "Properties": { 158 | "DestinationCidrBlock": "0.0.0.0/0", 159 | "RouteTableId": { 160 | "Ref": "AppRouteTable1" 161 | }, 162 | "NatGatewayId": { 163 | "Ref": "NatGateway1" 164 | } 165 | } 166 | }, 167 | "DBSubnet1": { 168 | "Type": "AWS::EC2::Subnet", 169 | "Properties": { 170 | "AvailabilityZone": "us-east-1a", 171 | "CidrBlock": "10.0.12.0/22", 172 | "Tags": [ 173 | { 174 | "Key": "Name", 175 | "Value": { 176 | "Fn::Sub": "${AWS::StackName}-db-us-east-1a" 177 | } 178 | }, 179 | { 180 | "Key": "Network", 181 | "Value": "Private" 182 | } 183 | ], 184 | "VpcId": { 185 | "Ref": "VPC" 186 | } 187 | } 188 | }, 189 | "DBRouteTable1": { 190 | "Type": "AWS::EC2::RouteTable", 191 | "Properties": { 192 | "VpcId": { 193 | "Ref": "VPC" 194 | }, 195 | "Tags": [ 196 | { 197 | "Key": "Name", 198 | "Value": { 199 | "Fn::Sub": "${AWS::StackName}-db-${DBSubnet1.AvailabilityZone}" 200 | } 201 | }, 202 | { 203 | "Key": "Network", 204 | "Value": "Private" 205 | } 206 | ] 207 | } 208 | }, 209 | "DBRouteTableAssociation1": { 210 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 211 | "Properties": { 212 | "RouteTableId": { 213 | "Ref": "DBRouteTable1" 214 | }, 215 | "SubnetId": { 216 | "Ref": "DBSubnet1" 217 | } 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /__tests__/fixtures/vpc_single_az_natgw_no_db.json: -------------------------------------------------------------------------------- 1 | { 2 | "EIP1": { 3 | "Type": "AWS::EC2::EIP", 4 | "Properties": { 5 | "Domain": "vpc" 6 | } 7 | }, 8 | "NatGateway1": { 9 | "Type": "AWS::EC2::NatGateway", 10 | "Properties": { 11 | "AllocationId": { 12 | "Fn::GetAtt": [ 13 | "EIP1", 14 | "AllocationId" 15 | ] 16 | }, 17 | "SubnetId": { 18 | "Ref": "PublicSubnet1" 19 | }, 20 | "Tags": [ 21 | { 22 | "Key": "Name", 23 | "Value": { 24 | "Fn::Sub": "${AWS::StackName}-${PublicSubnet1.AvailabilityZone}" 25 | } 26 | }, 27 | { 28 | "Key": "Network", 29 | "Value": "Public" 30 | } 31 | ] 32 | } 33 | }, 34 | "AppSubnet1": { 35 | "Type": "AWS::EC2::Subnet", 36 | "Properties": { 37 | "AvailabilityZone": "us-east-1a", 38 | "CidrBlock": "10.0.0.0/21", 39 | "Tags": [ 40 | { 41 | "Key": "Name", 42 | "Value": { 43 | "Fn::Sub": "${AWS::StackName}-app-us-east-1a" 44 | } 45 | }, 46 | { 47 | "Key": "Network", 48 | "Value": "Private" 49 | } 50 | ], 51 | "VpcId": { 52 | "Ref": "VPC" 53 | } 54 | } 55 | }, 56 | "AppRouteTable1": { 57 | "Type": "AWS::EC2::RouteTable", 58 | "Properties": { 59 | "VpcId": { 60 | "Ref": "VPC" 61 | }, 62 | "Tags": [ 63 | { 64 | "Key": "Name", 65 | "Value": { 66 | "Fn::Sub": "${AWS::StackName}-app-${AppSubnet1.AvailabilityZone}" 67 | } 68 | }, 69 | { 70 | "Key": "Network", 71 | "Value": "Private" 72 | } 73 | ] 74 | } 75 | }, 76 | "AppRouteTableAssociation1": { 77 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 78 | "Properties": { 79 | "RouteTableId": { 80 | "Ref": "AppRouteTable1" 81 | }, 82 | "SubnetId": { 83 | "Ref": "AppSubnet1" 84 | } 85 | } 86 | }, 87 | "PublicSubnet1": { 88 | "Type": "AWS::EC2::Subnet", 89 | "Properties": { 90 | "AvailabilityZone": "us-east-1a", 91 | "CidrBlock": "10.0.8.0/22", 92 | "Tags": [ 93 | { 94 | "Key": "Name", 95 | "Value": { 96 | "Fn::Sub": "${AWS::StackName}-public-us-east-1a" 97 | } 98 | }, 99 | { 100 | "Key": "Network", 101 | "Value": "Public" 102 | } 103 | ], 104 | "VpcId": { 105 | "Ref": "VPC" 106 | } 107 | } 108 | }, 109 | "PublicRouteTable1": { 110 | "Type": "AWS::EC2::RouteTable", 111 | "Properties": { 112 | "VpcId": { 113 | "Ref": "VPC" 114 | }, 115 | "Tags": [ 116 | { 117 | "Key": "Name", 118 | "Value": { 119 | "Fn::Sub": "${AWS::StackName}-public-${PublicSubnet1.AvailabilityZone}" 120 | } 121 | }, 122 | { 123 | "Key": "Network", 124 | "Value": "Public" 125 | } 126 | ] 127 | } 128 | }, 129 | "PublicRouteTableAssociation1": { 130 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 131 | "Properties": { 132 | "RouteTableId": { 133 | "Ref": "PublicRouteTable1" 134 | }, 135 | "SubnetId": { 136 | "Ref": "PublicSubnet1" 137 | } 138 | } 139 | }, 140 | "PublicRoute1": { 141 | "Type": "AWS::EC2::Route", 142 | "Properties": { 143 | "DestinationCidrBlock": "0.0.0.0/0", 144 | "RouteTableId": { 145 | "Ref": "PublicRouteTable1" 146 | }, 147 | "GatewayId": { 148 | "Ref": "InternetGateway" 149 | } 150 | }, 151 | "DependsOn": [ 152 | "InternetGatewayAttachment" 153 | ] 154 | }, 155 | "AppRoute1": { 156 | "Type": "AWS::EC2::Route", 157 | "Properties": { 158 | "DestinationCidrBlock": "0.0.0.0/0", 159 | "RouteTableId": { 160 | "Ref": "AppRouteTable1" 161 | }, 162 | "NatGatewayId": { 163 | "Ref": "NatGateway1" 164 | } 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /__tests__/fixtures/vpc_single_az_no_natgw_db.json: -------------------------------------------------------------------------------- 1 | { 2 | "AppSubnet1": { 3 | "Type": "AWS::EC2::Subnet", 4 | "Properties": { 5 | "AvailabilityZone": "us-east-1a", 6 | "CidrBlock": "10.0.0.0/21", 7 | "Tags": [ 8 | { 9 | "Key": "Name", 10 | "Value": { 11 | "Fn::Sub": "${AWS::StackName}-app-us-east-1a" 12 | } 13 | }, 14 | { 15 | "Key": "Network", 16 | "Value": "Private" 17 | } 18 | ], 19 | "VpcId": { 20 | "Ref": "VPC" 21 | } 22 | } 23 | }, 24 | "AppRouteTable1": { 25 | "Type": "AWS::EC2::RouteTable", 26 | "Properties": { 27 | "VpcId": { 28 | "Ref": "VPC" 29 | }, 30 | "Tags": [ 31 | { 32 | "Key": "Name", 33 | "Value": { 34 | "Fn::Sub": "${AWS::StackName}-app-${AppSubnet1.AvailabilityZone}" 35 | } 36 | }, 37 | { 38 | "Key": "Network", 39 | "Value": "Private" 40 | } 41 | ] 42 | } 43 | }, 44 | "AppRouteTableAssociation1": { 45 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 46 | "Properties": { 47 | "RouteTableId": { 48 | "Ref": "AppRouteTable1" 49 | }, 50 | "SubnetId": { 51 | "Ref": "AppSubnet1" 52 | } 53 | } 54 | }, 55 | "PublicSubnet1": { 56 | "Type": "AWS::EC2::Subnet", 57 | "Properties": { 58 | "AvailabilityZone": "us-east-1a", 59 | "CidrBlock": "10.0.8.0/22", 60 | "Tags": [ 61 | { 62 | "Key": "Name", 63 | "Value": { 64 | "Fn::Sub": "${AWS::StackName}-public-us-east-1a" 65 | } 66 | }, 67 | { 68 | "Key": "Network", 69 | "Value": "Public" 70 | } 71 | ], 72 | "VpcId": { 73 | "Ref": "VPC" 74 | } 75 | } 76 | }, 77 | "PublicRouteTable1": { 78 | "Type": "AWS::EC2::RouteTable", 79 | "Properties": { 80 | "VpcId": { 81 | "Ref": "VPC" 82 | }, 83 | "Tags": [ 84 | { 85 | "Key": "Name", 86 | "Value": { 87 | "Fn::Sub": "${AWS::StackName}-public-${PublicSubnet1.AvailabilityZone}" 88 | } 89 | }, 90 | { 91 | "Key": "Network", 92 | "Value": "Public" 93 | } 94 | ] 95 | } 96 | }, 97 | "PublicRouteTableAssociation1": { 98 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 99 | "Properties": { 100 | "RouteTableId": { 101 | "Ref": "PublicRouteTable1" 102 | }, 103 | "SubnetId": { 104 | "Ref": "PublicSubnet1" 105 | } 106 | } 107 | }, 108 | "PublicRoute1": { 109 | "Type": "AWS::EC2::Route", 110 | "Properties": { 111 | "DestinationCidrBlock": "0.0.0.0/0", 112 | "RouteTableId": { 113 | "Ref": "PublicRouteTable1" 114 | }, 115 | "GatewayId": { 116 | "Ref": "InternetGateway" 117 | } 118 | }, 119 | "DependsOn": [ 120 | "InternetGatewayAttachment" 121 | ] 122 | }, 123 | "DBSubnet1": { 124 | "Type": "AWS::EC2::Subnet", 125 | "Properties": { 126 | "AvailabilityZone": "us-east-1a", 127 | "CidrBlock": "10.0.12.0/22", 128 | "Tags": [ 129 | { 130 | "Key": "Name", 131 | "Value": { 132 | "Fn::Sub": "${AWS::StackName}-db-us-east-1a" 133 | } 134 | }, 135 | { 136 | "Key": "Network", 137 | "Value": "Private" 138 | } 139 | ], 140 | "VpcId": { 141 | "Ref": "VPC" 142 | } 143 | } 144 | }, 145 | "DBRouteTable1": { 146 | "Type": "AWS::EC2::RouteTable", 147 | "Properties": { 148 | "VpcId": { 149 | "Ref": "VPC" 150 | }, 151 | "Tags": [ 152 | { 153 | "Key": "Name", 154 | "Value": { 155 | "Fn::Sub": "${AWS::StackName}-db-${DBSubnet1.AvailabilityZone}" 156 | } 157 | }, 158 | { 159 | "Key": "Network", 160 | "Value": "Private" 161 | } 162 | ] 163 | } 164 | }, 165 | "DBRouteTableAssociation1": { 166 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 167 | "Properties": { 168 | "RouteTableId": { 169 | "Ref": "DBRouteTable1" 170 | }, 171 | "SubnetId": { 172 | "Ref": "DBSubnet1" 173 | } 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /__tests__/fixtures/vpc_single_az_no_natgw_no_db.json: -------------------------------------------------------------------------------- 1 | { 2 | "AppSubnet1": { 3 | "Type": "AWS::EC2::Subnet", 4 | "Properties": { 5 | "AvailabilityZone": "us-east-1a", 6 | "CidrBlock": "10.0.0.0/21", 7 | "Tags": [ 8 | { 9 | "Key": "Name", 10 | "Value": { 11 | "Fn::Sub": "${AWS::StackName}-app-us-east-1a" 12 | } 13 | }, 14 | { 15 | "Key": "Network", 16 | "Value": "Private" 17 | } 18 | ], 19 | "VpcId": { 20 | "Ref": "VPC" 21 | } 22 | } 23 | }, 24 | "AppRouteTable1": { 25 | "Type": "AWS::EC2::RouteTable", 26 | "Properties": { 27 | "VpcId": { 28 | "Ref": "VPC" 29 | }, 30 | "Tags": [ 31 | { 32 | "Key": "Name", 33 | "Value": { 34 | "Fn::Sub": "${AWS::StackName}-app-${AppSubnet1.AvailabilityZone}" 35 | } 36 | }, 37 | { 38 | "Key": "Network", 39 | "Value": "Private" 40 | } 41 | ] 42 | } 43 | }, 44 | "AppRouteTableAssociation1": { 45 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 46 | "Properties": { 47 | "RouteTableId": { 48 | "Ref": "AppRouteTable1" 49 | }, 50 | "SubnetId": { 51 | "Ref": "AppSubnet1" 52 | } 53 | } 54 | }, 55 | "PublicSubnet1": { 56 | "Type": "AWS::EC2::Subnet", 57 | "Properties": { 58 | "AvailabilityZone": "us-east-1a", 59 | "CidrBlock": "10.0.8.0/22", 60 | "Tags": [ 61 | { 62 | "Key": "Name", 63 | "Value": { 64 | "Fn::Sub": "${AWS::StackName}-public-us-east-1a" 65 | } 66 | }, 67 | { 68 | "Key": "Network", 69 | "Value": "Public" 70 | } 71 | ], 72 | "VpcId": { 73 | "Ref": "VPC" 74 | } 75 | } 76 | }, 77 | "PublicRouteTable1": { 78 | "Type": "AWS::EC2::RouteTable", 79 | "Properties": { 80 | "VpcId": { 81 | "Ref": "VPC" 82 | }, 83 | "Tags": [ 84 | { 85 | "Key": "Name", 86 | "Value": { 87 | "Fn::Sub": "${AWS::StackName}-public-${PublicSubnet1.AvailabilityZone}" 88 | } 89 | }, 90 | { 91 | "Key": "Network", 92 | "Value": "Public" 93 | } 94 | ] 95 | } 96 | }, 97 | "PublicRouteTableAssociation1": { 98 | "Type": "AWS::EC2::SubnetRouteTableAssociation", 99 | "Properties": { 100 | "RouteTableId": { 101 | "Ref": "PublicRouteTable1" 102 | }, 103 | "SubnetId": { 104 | "Ref": "PublicSubnet1" 105 | } 106 | } 107 | }, 108 | "PublicRoute1": { 109 | "Type": "AWS::EC2::Route", 110 | "Properties": { 111 | "DestinationCidrBlock": "0.0.0.0/0", 112 | "RouteTableId": { 113 | "Ref": "PublicRouteTable1" 114 | }, 115 | "GatewayId": { 116 | "Ref": "InternetGateway" 117 | } 118 | }, 119 | "DependsOn": [ 120 | "InternetGatewayAttachment" 121 | ] 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /__tests__/flow_logs.test.js: -------------------------------------------------------------------------------- 1 | const { buildLogBucket, buildLogBucketPolicy, buildVpcFlowLogs } = require('../src/flow_logs'); 2 | 3 | describe('flow_logs', () => { 4 | describe('#buildVpcFlowLogs', () => { 5 | it('builds a VPC flow log', () => { 6 | const expected = { 7 | S3FlowLog: { 8 | Type: 'AWS::EC2::FlowLog', 9 | DependsOn: 'LogBucketPolicy', 10 | Properties: { 11 | DestinationOptions: { 12 | FileFormat: 'parquet', 13 | HiveCompatiblePartitions: true, 14 | PerHourPartition: true, 15 | }, 16 | LogDestinationType: 's3', 17 | LogDestination: { 18 | 'Fn::GetAtt': ['LogBucket', 'Arn'], 19 | }, 20 | LogFormat: '${version} ${account-id} ${interface-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${start} ${end} ${action} ${log-status} ${vpc-id} ${subnet-id} ${instance-id} ${tcp-flags} ${type} ${pkt-srcaddr} ${pkt-dstaddr} ${region} ${az-id} ${sublocation-type} ${sublocation-id} ${pkt-src-aws-service} ${pkt-dst-aws-service} ${flow-direction} ${traffic-path}', 21 | MaxAggregationInterval: 60, 22 | ResourceId: { 23 | Ref: 'VPC', 24 | }, 25 | ResourceType: 'VPC', 26 | TrafficType: 'ALL', 27 | }, 28 | }, 29 | }; 30 | 31 | const actual = buildVpcFlowLogs(); 32 | expect(actual).toEqual(expected); 33 | expect.assertions(1); 34 | }); 35 | }); 36 | 37 | describe('#buildLogBucketPolicy', () => { 38 | it('builds a log bucket policy', () => { 39 | const expected = { 40 | LogBucketPolicy: { 41 | Type: 'AWS::S3::BucketPolicy', 42 | Properties: { 43 | Bucket: { 44 | Ref: 'LogBucket', 45 | }, 46 | PolicyDocument: { 47 | Version: '2012-10-17', 48 | Statement: [ 49 | { 50 | Sid: 'AWSLogDeliveryAclCheck', 51 | Effect: 'Allow', 52 | Principal: { 53 | Service: 'delivery.logs.amazonaws.com', 54 | }, 55 | Action: ['s3:GetBucketAcl', 's3:ListBucket'], 56 | Resource: { 57 | 'Fn::GetAtt': ['LogBucket', 'Arn'], 58 | }, 59 | Condition: { 60 | StringEquals: { 61 | 'aws:SourceAccount': { 62 | Ref: 'AWS::AccountId', 63 | }, 64 | }, 65 | ArnLike: { 66 | 'aws:SourceArn': { 67 | // eslint-disable-next-line no-template-curly-in-string 68 | 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:*', 69 | }, 70 | }, 71 | }, 72 | }, 73 | { 74 | Sid: 'AWSLogDeliveryWrite', 75 | Effect: 'Allow', 76 | Principal: { 77 | Service: 'delivery.logs.amazonaws.com', 78 | }, 79 | Action: 's3:PutObject', 80 | Resource: { 81 | 'Fn::Sub': 82 | // eslint-disable-next-line no-template-curly-in-string 83 | 'arn:${AWS::Partition}:s3:::${LogBucket}/*', 84 | }, 85 | Condition: { 86 | StringEquals: { 87 | 's3:x-amz-acl': 'bucket-owner-full-control', 88 | 'aws:SourceAccount': { 89 | Ref: 'AWS::AccountId', 90 | }, 91 | }, 92 | ArnLike: { 93 | 'aws:SourceArn': { 94 | // eslint-disable-next-line no-template-curly-in-string 95 | 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:*', 96 | }, 97 | } 98 | }, 99 | }, 100 | { 101 | Sid: 'AllowSSLRequestsOnly', 102 | Effect: 'Deny', 103 | Principal: '*', 104 | Action: 's3:*', 105 | Resource: [ 106 | { 107 | // eslint-disable-next-line no-template-curly-in-string 108 | 'Fn::Sub': '${LogBucket.Arn}/*', 109 | }, 110 | { 111 | 'Fn::GetAtt': 'LogBucket.Arn', 112 | }, 113 | ], 114 | Condition: { 115 | Bool: { 116 | 'aws:SecureTransport': false, 117 | }, 118 | }, 119 | }, 120 | ], 121 | }, 122 | }, 123 | }, 124 | }; 125 | const actual = buildLogBucketPolicy(); 126 | expect(actual).toEqual(expected); 127 | expect.assertions(1); 128 | }); 129 | }); 130 | 131 | describe('#buildLogBucket', () => { 132 | it('builds a log bucket', () => { 133 | const expected = { 134 | LogBucket: { 135 | Type: 'AWS::S3::Bucket', 136 | DeletionPolicy: 'Retain', 137 | UpdateReplacePolicy: 'Retain', 138 | Properties: { 139 | BucketEncryption: { 140 | ServerSideEncryptionConfiguration: [ 141 | { 142 | ServerSideEncryptionByDefault: { 143 | SSEAlgorithm: 'AES256', 144 | }, 145 | }, 146 | ], 147 | }, 148 | LifecycleConfiguration: { 149 | Rules: [ 150 | { 151 | ExpirationInDays: 365, 152 | Id: 'RetentionRule', 153 | Prefix: 'AWSLogs', 154 | Status: 'Enabled', 155 | Transitions: [ 156 | { 157 | TransitionInDays: 30, 158 | StorageClass: 'STANDARD_IA', 159 | }, 160 | { 161 | TransitionInDays: 90, 162 | StorageClass: 'GLACIER', 163 | }, 164 | ], 165 | }, 166 | ], 167 | }, 168 | OwnershipControls: { 169 | Rules: [ 170 | { 171 | ObjectOwnership: 'BucketOwnerEnforced', 172 | }, 173 | ], 174 | }, 175 | PublicAccessBlockConfiguration: { 176 | BlockPublicAcls: true, 177 | BlockPublicPolicy: true, 178 | IgnorePublicAcls: true, 179 | RestrictPublicBuckets: true, 180 | }, 181 | Tags: [ 182 | { 183 | Key: 'Name', 184 | Value: { 185 | // eslint-disable-next-line no-template-curly-in-string 186 | 'Fn::Sub': '${AWS::StackName} Logs', 187 | }, 188 | }, 189 | ], 190 | VersioningConfiguration: { 191 | Status: 'Enabled', 192 | }, 193 | }, 194 | }, 195 | }; 196 | const actual = buildLogBucket(); 197 | expect(actual).toEqual(expected); 198 | expect.assertions(1); 199 | }); 200 | }); 201 | }); 202 | -------------------------------------------------------------------------------- /__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk-mock'); 2 | const nock = require('nock'); 3 | 4 | const Serverless = require('serverless'); 5 | const AwsProvider = require('serverless/lib/plugins/aws/provider'); 6 | const ServerlessVpcPlugin = require('../src/index'); 7 | 8 | describe('ServerlessVpcPlugin', () => { 9 | let serverless; 10 | let plugin; 11 | let mockedMethods = []; 12 | 13 | function mockHelper(serviceName, methodName, callback) { 14 | AWS.mock(serviceName, methodName, callback); 15 | mockedMethods.push([serviceName, methodName]); 16 | } 17 | 18 | beforeEach(() => { 19 | nock.disableNetConnect(); 20 | 21 | const options = { 22 | stage: 'dev', 23 | region: 'us-east-1', 24 | }; 25 | const config = { commands: [], options: options }; 26 | serverless = new Serverless(config); 27 | serverless.cli = new serverless.classes.CLI(); 28 | 29 | const provider = new AwsProvider(serverless, options); 30 | // provider.sdk.config.logger = console; 31 | AWS.setSDKInstance(provider.sdk); 32 | 33 | provider.cachedCredentials = { 34 | credentials: { 35 | accessKeyId: 'test', 36 | secretAccessKey: 'test', 37 | }, 38 | }; 39 | 40 | serverless.service.provider.compiledCloudFormationTemplate = { 41 | Resources: {}, 42 | Outputs: {}, 43 | Parameters: {}, 44 | }; 45 | 46 | plugin = new ServerlessVpcPlugin(serverless); 47 | }); 48 | 49 | afterEach(() => { 50 | while (mockedMethods.length > 0) { 51 | const [serviceName, methodName] = mockedMethods.pop(); 52 | AWS.restore(serviceName, methodName); 53 | } 54 | }); 55 | 56 | describe('#constructor', () => { 57 | it('should initialize without options', () => { 58 | expect(plugin.serverless).toBeInstanceOf(Serverless); 59 | expect(plugin.options).toEqual({}); 60 | expect.assertions(2); 61 | }); 62 | 63 | it('should initialize with empty options', () => { 64 | const newPlugin = new ServerlessVpcPlugin(serverless, {}); 65 | expect(newPlugin.serverless).toBeInstanceOf(Serverless); 66 | expect(newPlugin.options).toEqual({}); 67 | expect.assertions(2); 68 | }); 69 | 70 | it('should initialize with custom options', () => { 71 | const options = { 72 | zones: ['us-west-2a'], 73 | services: [], 74 | }; 75 | const newPlugin = new ServerlessVpcPlugin(serverless, options); 76 | expect(newPlugin.serverless).toBeInstanceOf(Serverless); 77 | expect(newPlugin.options).toEqual(options); 78 | expect.assertions(2); 79 | }); 80 | }); 81 | 82 | describe('#afterInitialize', () => { 83 | it('should require a bastion key name', async () => { 84 | serverless.service.custom.vpcConfig = { 85 | createBastionHost: true, 86 | }; 87 | 88 | await expect(plugin.afterInitialize()).rejects.toThrow( 89 | 'bastionHostKeyName must be provided if createBastionHost is true', 90 | ); 91 | expect.assertions(1); 92 | }); 93 | 94 | it('createNatGateway should be either boolean or a number', async () => { 95 | serverless.service.custom.vpcConfig = { 96 | createNatGateway: 'hello', 97 | zones: ['us-east-1a'], 98 | services: [], 99 | }; 100 | 101 | await expect(plugin.afterInitialize()).rejects.toThrow( 102 | 'createNatGateway must be either a boolean or a number', 103 | ); 104 | expect.assertions(1); 105 | }); 106 | 107 | it('should not allow NAT instance and NAT gateway as boolean', async () => { 108 | serverless.service.custom.vpcConfig = { 109 | createNatGateway: true, 110 | createNatInstance: true, 111 | }; 112 | 113 | await expect(plugin.afterInitialize()).rejects.toThrow( 114 | 'Please choose either createNatGateway or createNatInstance, not both', 115 | ); 116 | expect.assertions(1); 117 | }); 118 | 119 | it('should not allow NAT instance and NAT gateway as number', async () => { 120 | serverless.service.custom.vpcConfig = { 121 | createNatGateway: 3, 122 | createNatInstance: true, 123 | }; 124 | 125 | await expect(plugin.afterInitialize()).rejects.toThrow( 126 | 'Please choose either createNatGateway or createNatInstance, not both', 127 | ); 128 | expect.assertions(1); 129 | }); 130 | 131 | it('should discover available zones', async () => { 132 | const mockCallback = jest.fn((params, callback) => { 133 | expect(params.Filters[0].Values[0]).toEqual('us-east-1'); 134 | const response = { 135 | AvailabilityZones: [ 136 | { 137 | State: 'available', 138 | ZoneName: 'us-east-1a', 139 | }, 140 | ], 141 | }; 142 | return callback(null, response); 143 | }); 144 | 145 | mockHelper('EC2', 'describeAvailabilityZones', mockCallback); 146 | 147 | serverless.service.custom.vpcConfig = { 148 | services: [], 149 | }; 150 | 151 | const actual = await plugin.afterInitialize(); 152 | expect(actual).toBeUndefined(); 153 | expect(mockCallback).toHaveBeenCalled(); 154 | expect.assertions(3); 155 | }); 156 | 157 | it('rejects invalid subnet groups', async () => { 158 | serverless.service.custom.vpcConfig = { 159 | zones: ['us-east-1a', 'us-east-1b'], 160 | subnetGroups: ['invalid'], 161 | services: [], 162 | }; 163 | 164 | await expect(plugin.afterInitialize()).rejects.toThrow( 165 | 'WARNING: Invalid subnetGroups option. Valid options: rds, redshift, elasticache, dax', 166 | ); 167 | expect.assertions(1); 168 | }); 169 | }); 170 | 171 | describe('#getZonesPerRegion', () => { 172 | it('returns the zones in a region', async () => { 173 | const mockCallback = jest.fn((params, callback) => { 174 | expect(params.Filters[0].Values[0]).toEqual('us-east-1'); 175 | const response = { 176 | AvailabilityZones: [ 177 | { 178 | State: 'available', 179 | ZoneName: 'us-east-1b', 180 | }, 181 | { 182 | State: 'available', 183 | ZoneName: 'us-east-1a', 184 | }, 185 | ], 186 | }; 187 | return callback(null, response); 188 | }); 189 | 190 | mockHelper('EC2', 'describeAvailabilityZones', mockCallback); 191 | 192 | const actual = await plugin.getZonesPerRegion('us-east-1'); 193 | 194 | const expected = ['us-east-1a', 'us-east-1b']; 195 | 196 | expect(mockCallback).toHaveBeenCalled(); 197 | expect(actual).toEqual(expected); 198 | expect.assertions(3); 199 | }); 200 | }); 201 | 202 | describe('#getVpcEndpointServicesPerRegion', () => { 203 | it('returns the endpoint services in a region', async () => { 204 | const mockCallback = jest.fn((params, callback) => { 205 | const response = { 206 | ServiceNames: [ 207 | 'com.amazonaws.us-east-1.dynamodb', 208 | 'com.amazonaws.us-east-1.s3', 209 | 'com.amazonaws.us-east-1.kms', 210 | 'com.amazonaws.us-east-1.kinesis-streams', 211 | ], 212 | }; 213 | return callback(null, response); 214 | }); 215 | 216 | mockHelper('EC2', 'describeVpcEndpointServices', mockCallback); 217 | 218 | const actual = await plugin.getVpcEndpointServicesPerRegion('us-east-1'); 219 | 220 | const expected = [ 221 | 'com.amazonaws.us-east-1.dynamodb', 222 | 'com.amazonaws.us-east-1.kinesis-streams', 223 | 'com.amazonaws.us-east-1.kms', 224 | 'com.amazonaws.us-east-1.s3', 225 | ]; 226 | 227 | expect(mockCallback).toHaveBeenCalled(); 228 | expect(actual).toEqual(expected); 229 | expect.assertions(2); 230 | }); 231 | }); 232 | 233 | describe('#getImagesByName', () => { 234 | it('returns an AMI image by name', async () => { 235 | const mockCallback = jest.fn((params, callback) => { 236 | expect(params.Filters.find((f) => f.Name === 'name').Values).toEqual(['test']); 237 | const response = { 238 | Images: [ 239 | { 240 | ImageId: 'ami-test', 241 | CreationDate: '2019-04-12T00:00:00Z', 242 | }, 243 | ], 244 | }; 245 | return callback(null, response); 246 | }); 247 | 248 | mockHelper('EC2', 'describeImages', mockCallback); 249 | 250 | const actual = await plugin.getImagesByName('test'); 251 | expect(actual).toEqual(['ami-test']); 252 | expect(mockCallback).toHaveBeenCalled(); 253 | expect.assertions(3); 254 | }); 255 | }); 256 | 257 | describe('#validateServices', () => { 258 | it('returns validated services', async () => { 259 | const mockCallback = jest.fn((params, callback) => { 260 | const response = { 261 | ServiceNames: [ 262 | 'com.amazonaws.us-east-1.dynamodb', 263 | 'com.amazonaws.us-east-1.s3', 264 | 'com.amazonaws.us-east-1.kms', 265 | 'com.amazonaws.us-east-1.kinesis-streams', 266 | ], 267 | }; 268 | return callback(null, response); 269 | }); 270 | 271 | mockHelper('EC2', 'describeVpcEndpointServices', mockCallback); 272 | 273 | const actual = await plugin.validateServices('us-east-1', ['blah']); 274 | expect(actual).toEqual(['com.amazonaws.us-east-1.blah']); 275 | expect(mockCallback).toHaveBeenCalled(); 276 | expect.assertions(2); 277 | }); 278 | }); 279 | 280 | describe('#getPrefixLists', () => { 281 | it('returns the prefix lists', async () => { 282 | const mockCallback = jest.fn((params, callback) => { 283 | const response = { 284 | PrefixLists: [ 285 | { 286 | PrefixListId: 'pl-02cd2c6b', 287 | AddressFamily: 'IPv4', 288 | State: 'create-complete', 289 | PrefixListArn: 'arn:aws:ec2:us-east-1:aws:prefix-list/pl-02cd2c6b', 290 | PrefixListName: 'com.amazonaws.us-east-1.dynamodb', 291 | Tags: [], 292 | OwnerId: 'AWS', 293 | }, 294 | { 295 | PrefixListId: 'pl-63a5400a', 296 | AddressFamily: 'IPv4', 297 | State: 'create-complete', 298 | PrefixListArn: 'arn:aws:ec2:us-east-1:aws:prefix-list/pl-63a5400a', 299 | PrefixListName: 'com.amazonaws.us-east-1.s3', 300 | Tags: [], 301 | OwnerId: 'AWS', 302 | }, 303 | ], 304 | }; 305 | return callback(null, response); 306 | }); 307 | 308 | mockHelper('EC2', 'describeManagedPrefixLists', mockCallback); 309 | 310 | const expected = { 311 | s3: 'pl-63a5400a', 312 | dynamodb: 'pl-02cd2c6b', 313 | }; 314 | 315 | const actual = await plugin.getPrefixLists(); 316 | expect(actual).toEqual(expected); 317 | expect(mockCallback).toHaveBeenCalled(); 318 | expect.assertions(2); 319 | }); 320 | }); 321 | }); 322 | -------------------------------------------------------------------------------- /__tests__/nat_instance.test.js: -------------------------------------------------------------------------------- 1 | const { buildNatInstance, buildNatSecurityGroup } = require('../src/nat_instance'); 2 | 3 | describe('nat_instance', () => { 4 | describe('#buildNatSecurityGroup', () => { 5 | it('builds a security group', () => { 6 | const expected = { 7 | NatSecurityGroup: { 8 | Type: 'AWS::EC2::SecurityGroup', 9 | Properties: { 10 | GroupDescription: 'NAT Instance', 11 | VpcId: { 12 | Ref: 'VPC', 13 | }, 14 | SecurityGroupEgress: [ 15 | { 16 | Description: 'permit outbound HTTP to the Internet', 17 | IpProtocol: 'tcp', 18 | FromPort: 80, 19 | ToPort: 80, 20 | CidrIp: '0.0.0.0/0', 21 | }, 22 | { 23 | Description: 'permit outbound HTTPS to the Internet', 24 | IpProtocol: 'tcp', 25 | FromPort: 443, 26 | ToPort: 443, 27 | CidrIp: '0.0.0.0/0', 28 | }, 29 | ], 30 | SecurityGroupIngress: [ 31 | { 32 | Description: 'permit inbound HTTP from AppSecurityGroup', 33 | IpProtocol: 'tcp', 34 | FromPort: 80, 35 | ToPort: 80, 36 | SourceSecurityGroupId: { 37 | Ref: 'AppSecurityGroup', 38 | }, 39 | }, 40 | { 41 | Description: 'permit inbound HTTPS from AppSecurityGroup', 42 | IpProtocol: 'tcp', 43 | FromPort: 443, 44 | ToPort: 443, 45 | SourceSecurityGroupId: { 46 | Ref: 'AppSecurityGroup', 47 | }, 48 | }, 49 | ], 50 | Tags: [ 51 | { 52 | Key: 'Name', 53 | Value: { 54 | // eslint-disable-next-line no-template-curly-in-string 55 | 'Fn::Sub': '${AWS::StackName}-nat', 56 | }, 57 | }, 58 | ], 59 | }, 60 | }, 61 | }; 62 | 63 | const actual = buildNatSecurityGroup(); 64 | expect(actual).toEqual(expected); 65 | expect.assertions(1); 66 | }); 67 | }); 68 | 69 | describe('#buildNatInstance', () => { 70 | it('builds an EC2 instance', () => { 71 | const expected = { 72 | NatInstance: { 73 | Type: 'AWS::EC2::Instance', 74 | DependsOn: 'InternetGatewayAttachment', 75 | Properties: { 76 | AvailabilityZone: { 77 | 'Fn::Select': ['0', ['us-east-1a', 'us-east-1b']], 78 | }, 79 | BlockDeviceMappings: [ 80 | { 81 | DeviceName: '/dev/xvda', 82 | Ebs: { 83 | VolumeSize: 10, 84 | VolumeType: 'gp2', 85 | DeleteOnTermination: true, 86 | }, 87 | }, 88 | ], 89 | ImageId: 'ami-00a9d4a05375b2763', 90 | InstanceType: 't2.micro', 91 | Monitoring: false, 92 | NetworkInterfaces: [ 93 | { 94 | AssociatePublicIpAddress: true, 95 | DeleteOnTermination: true, 96 | Description: 'eth0', 97 | DeviceIndex: '0', 98 | GroupSet: [ 99 | { 100 | Ref: 'NatSecurityGroup', 101 | }, 102 | ], 103 | SubnetId: { 104 | Ref: 'PublicSubnet1', 105 | }, 106 | }, 107 | ], 108 | SourceDestCheck: false, 109 | Tags: [ 110 | { 111 | Key: 'Name', 112 | Value: { 113 | // eslint-disable-next-line no-template-curly-in-string 114 | 'Fn::Sub': '${AWS::StackName}-nat', 115 | }, 116 | }, 117 | ], 118 | }, 119 | }, 120 | }; 121 | 122 | const imageId = 'ami-00a9d4a05375b2763'; 123 | 124 | const actual = buildNatInstance(imageId, ['us-east-1a', 'us-east-1b']); 125 | expect(actual).toEqual(expected); 126 | expect.assertions(1); 127 | }); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /__tests__/natgw.test.js: -------------------------------------------------------------------------------- 1 | const { buildEIP, buildNatGateway } = require('../src/natgw'); 2 | 3 | describe('natgw', () => { 4 | describe('#buildEIP', () => { 5 | it('builds an EIP', () => { 6 | const expected = { 7 | EIP1: { 8 | Type: 'AWS::EC2::EIP', 9 | Properties: { 10 | Domain: 'vpc', 11 | }, 12 | }, 13 | }; 14 | const actual = buildEIP(1); 15 | expect(actual).toEqual(expected); 16 | expect.assertions(1); 17 | }); 18 | }); 19 | 20 | describe('#buildNatGateway', () => { 21 | it('builds a NAT Gateway', () => { 22 | const expected = { 23 | NatGateway1: { 24 | Type: 'AWS::EC2::NatGateway', 25 | Properties: { 26 | AllocationId: { 27 | 'Fn::GetAtt': ['EIP1', 'AllocationId'], 28 | }, 29 | SubnetId: { 30 | Ref: 'PublicSubnet1', 31 | }, 32 | Tags: [ 33 | { 34 | Key: 'Name', 35 | Value: { 36 | // eslint-disable-next-line no-template-curly-in-string 37 | 'Fn::Sub': '${AWS::StackName}-${PublicSubnet1.AvailabilityZone}', 38 | }, 39 | }, 40 | { 41 | Key: 'Network', 42 | Value: 'Public', 43 | }, 44 | ], 45 | }, 46 | }, 47 | }; 48 | const actual = buildNatGateway(1); 49 | expect(actual).toEqual(expected); 50 | expect.assertions(1); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /__tests__/parameters.test.js: -------------------------------------------------------------------------------- 1 | const { buildParameter } = require('../src/parameters'); 2 | 3 | describe('parameters', () => { 4 | describe('#buildParameter', () => { 5 | it('builds an SSM String parameter', () => { 6 | const expected = { 7 | ParameterVPC: { 8 | Type: 'AWS::SSM::Parameter', 9 | Properties: { 10 | Name: { 11 | // eslint-disable-next-line no-template-curly-in-string 12 | 'Fn::Sub': '/SLS/${AWS::StackName}/VPC', 13 | }, 14 | Tier: 'Standard', 15 | Type: 'String', 16 | Value: { 17 | Ref: 'VPC', 18 | }, 19 | }, 20 | }, 21 | }; 22 | 23 | const actual = buildParameter('VPC'); 24 | expect(actual).toEqual(expected); 25 | expect.assertions(1); 26 | }); 27 | 28 | it('builds an SSM StringList parameter', () => { 29 | const expected = { 30 | ParameterAppSubnets: { 31 | Type: 'AWS::SSM::Parameter', 32 | Properties: { 33 | Name: { 34 | // eslint-disable-next-line no-template-curly-in-string 35 | 'Fn::Sub': '/SLS/${AWS::StackName}/AppSubnets', 36 | }, 37 | Tier: 'Standard', 38 | Type: 'StringList', 39 | Value: { 40 | 'Fn::Join': [',', [{ Ref: 'sg-1' }, { Ref: 'sg-2' }]], 41 | }, 42 | }, 43 | }, 44 | }; 45 | 46 | const actual = buildParameter('AppSubnets', { Value: [{ Ref: 'sg-1' }, { Ref: 'sg-2' }] }); 47 | expect(actual).toEqual(expected); 48 | expect.assertions(1); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /__tests__/routes.test.js: -------------------------------------------------------------------------------- 1 | const { buildRoute, buildRouteTable, buildRouteTableAssociation } = require('../src/routes'); 2 | 3 | describe('routes', () => { 4 | describe('#buildRouteTable', () => { 5 | it('builds a private route table', () => { 6 | const expected = { 7 | AppRouteTable1: { 8 | Type: 'AWS::EC2::RouteTable', 9 | Properties: { 10 | VpcId: { 11 | Ref: 'VPC', 12 | }, 13 | Tags: [ 14 | { 15 | Key: 'Name', 16 | Value: { 17 | // eslint-disable-next-line no-template-curly-in-string 18 | 'Fn::Sub': '${AWS::StackName}-app-${AppSubnet1.AvailabilityZone}', 19 | }, 20 | }, 21 | { 22 | Key: 'Network', 23 | Value: 'Private', 24 | }, 25 | ], 26 | }, 27 | }, 28 | }; 29 | const actual = buildRouteTable('App', 1); 30 | expect(actual).toEqual(expected); 31 | expect.assertions(1); 32 | }); 33 | 34 | it('builds a public route table', () => { 35 | const expected = { 36 | PublicRouteTable1: { 37 | Type: 'AWS::EC2::RouteTable', 38 | Properties: { 39 | VpcId: { 40 | Ref: 'VPC', 41 | }, 42 | Tags: [ 43 | { 44 | Key: 'Name', 45 | Value: { 46 | // eslint-disable-next-line no-template-curly-in-string 47 | 'Fn::Sub': '${AWS::StackName}-public-${PublicSubnet1.AvailabilityZone}', 48 | }, 49 | }, 50 | { 51 | Key: 'Network', 52 | Value: 'Public', 53 | }, 54 | ], 55 | }, 56 | }, 57 | }; 58 | const actual = buildRouteTable('Public', 1); 59 | expect(actual).toEqual(expected); 60 | expect.assertions(1); 61 | }); 62 | }); 63 | 64 | describe('#buildRouteTableAssociation', () => { 65 | it('builds a route table association', () => { 66 | const expected = { 67 | AppRouteTableAssociation1: { 68 | Type: 'AWS::EC2::SubnetRouteTableAssociation', 69 | Properties: { 70 | RouteTableId: { 71 | Ref: 'AppRouteTable1', 72 | }, 73 | SubnetId: { 74 | Ref: 'AppSubnet1', 75 | }, 76 | }, 77 | }, 78 | }; 79 | const actual = buildRouteTableAssociation('App', 1); 80 | expect(actual).toEqual(expected); 81 | expect.assertions(1); 82 | }); 83 | }); 84 | 85 | describe('#buildRoute', () => { 86 | it('builds a route with a NAT Gateway', () => { 87 | const expected = { 88 | AppRoute1: { 89 | Type: 'AWS::EC2::Route', 90 | Properties: { 91 | DestinationCidrBlock: '0.0.0.0/0', 92 | NatGatewayId: { 93 | Ref: 'NatGateway1', 94 | }, 95 | RouteTableId: { 96 | Ref: 'AppRouteTable1', 97 | }, 98 | }, 99 | }, 100 | }; 101 | const actual = buildRoute('App', 1, { 102 | NatGatewayId: 'NatGateway1', 103 | }); 104 | expect(actual).toEqual(expected); 105 | expect.assertions(1); 106 | }); 107 | 108 | it('builds a route with an Internet Gateway', () => { 109 | const expected = { 110 | PublicRoute1: { 111 | Type: 'AWS::EC2::Route', 112 | DependsOn: ['InternetGatewayAttachment'], 113 | Properties: { 114 | DestinationCidrBlock: '0.0.0.0/0', 115 | GatewayId: { 116 | Ref: 'InternetGateway', 117 | }, 118 | RouteTableId: { 119 | Ref: 'PublicRouteTable1', 120 | }, 121 | }, 122 | }, 123 | }; 124 | const actual = buildRoute('Public', 1, { 125 | GatewayId: 'InternetGateway', 126 | }); 127 | expect(actual).toEqual(expected); 128 | expect.assertions(1); 129 | }); 130 | 131 | it('builds a route with an Instance Gateway', () => { 132 | const expected = { 133 | AppRoute1: { 134 | Type: 'AWS::EC2::Route', 135 | Properties: { 136 | DestinationCidrBlock: '0.0.0.0/0', 137 | InstanceId: { 138 | Ref: 'BastionInstance', 139 | }, 140 | RouteTableId: { 141 | Ref: 'AppRouteTable1', 142 | }, 143 | }, 144 | }, 145 | }; 146 | const actual = buildRoute('App', 1, { 147 | InstanceId: 'BastionInstance', 148 | }); 149 | expect(actual).toEqual(expected); 150 | expect.assertions(1); 151 | }); 152 | 153 | it('throws an error if no gateway provided', () => { 154 | expect(() => { 155 | buildRoute('App', 1); 156 | }).toThrow( 157 | 'Unable to create route: either NatGatewayId, GatewayId or InstanceId must be provided', 158 | ); 159 | expect.assertions(1); 160 | }); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /__tests__/subnet_groups.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | buildRDSSubnetGroup, 3 | buildElastiCacheSubnetGroup, 4 | buildRedshiftSubnetGroup, 5 | buildDAXSubnetGroup, 6 | buildSubnetGroups, 7 | } = require('../src/subnet_groups'); 8 | 9 | describe('subnet_groups', () => { 10 | describe('#buildRDSSubnetGroup', () => { 11 | it('skips building an RDS subnet group with no zones', () => { 12 | const actual = buildRDSSubnetGroup(); 13 | expect(actual).toEqual({}); 14 | expect.assertions(1); 15 | }); 16 | 17 | it('builds an RDS subnet group', () => { 18 | const expected = { 19 | RDSSubnetGroup: { 20 | Type: 'AWS::RDS::DBSubnetGroup', 21 | Properties: { 22 | DBSubnetGroupName: { 23 | Ref: 'AWS::StackName', 24 | }, 25 | DBSubnetGroupDescription: { 26 | Ref: 'AWS::StackName', 27 | }, 28 | SubnetIds: [ 29 | { 30 | Ref: 'DBSubnet1', 31 | }, 32 | { 33 | Ref: 'DBSubnet2', 34 | }, 35 | ], 36 | }, 37 | }, 38 | }; 39 | const actual = buildRDSSubnetGroup(2); 40 | expect(actual).toEqual(expected); 41 | expect.assertions(1); 42 | }); 43 | }); 44 | 45 | describe('#buildElastiCacheSubnetGroup', () => { 46 | it('skips building an ElastiCache subnet group with no zones', () => { 47 | const actual = buildElastiCacheSubnetGroup(); 48 | expect(actual).toEqual({}); 49 | expect.assertions(1); 50 | }); 51 | 52 | it('builds an ElastiCache subnet group', () => { 53 | const expected = { 54 | ElastiCacheSubnetGroup: { 55 | Type: 'AWS::ElastiCache::SubnetGroup', 56 | Properties: { 57 | CacheSubnetGroupName: { 58 | Ref: 'AWS::StackName', 59 | }, 60 | Description: { 61 | Ref: 'AWS::StackName', 62 | }, 63 | SubnetIds: [ 64 | { 65 | Ref: 'DBSubnet1', 66 | }, 67 | { 68 | Ref: 'DBSubnet2', 69 | }, 70 | ], 71 | }, 72 | }, 73 | }; 74 | const actual = buildElastiCacheSubnetGroup(2); 75 | expect(actual).toEqual(expected); 76 | expect.assertions(1); 77 | }); 78 | }); 79 | 80 | describe('#buildRedshiftSubnetGroup', () => { 81 | it('skips building a Redshift subnet group with no zones', () => { 82 | const actual = buildRedshiftSubnetGroup(); 83 | expect(actual).toEqual({}); 84 | expect.assertions(1); 85 | }); 86 | 87 | it('builds an Redshift subnet group', () => { 88 | const expected = { 89 | RedshiftSubnetGroup: { 90 | Type: 'AWS::Redshift::ClusterSubnetGroup', 91 | Properties: { 92 | Description: { 93 | Ref: 'AWS::StackName', 94 | }, 95 | SubnetIds: [ 96 | { 97 | Ref: 'DBSubnet1', 98 | }, 99 | { 100 | Ref: 'DBSubnet2', 101 | }, 102 | ], 103 | }, 104 | }, 105 | }; 106 | const actual = buildRedshiftSubnetGroup(2); 107 | expect(actual).toEqual(expected); 108 | expect.assertions(1); 109 | }); 110 | }); 111 | 112 | describe('#buildDAXSubnetGroup', () => { 113 | it('skips building an DAX subnet group with no zones', () => { 114 | const actual = buildDAXSubnetGroup(); 115 | expect(actual).toEqual({}); 116 | expect.assertions(1); 117 | }); 118 | 119 | it('builds an DAX subnet group', () => { 120 | const expected = { 121 | DAXSubnetGroup: { 122 | Type: 'AWS::DAX::SubnetGroup', 123 | Properties: { 124 | SubnetGroupName: { 125 | Ref: 'AWS::StackName', 126 | }, 127 | Description: { 128 | Ref: 'AWS::StackName', 129 | }, 130 | SubnetIds: [ 131 | { 132 | Ref: 'DBSubnet1', 133 | }, 134 | { 135 | Ref: 'DBSubnet2', 136 | }, 137 | ], 138 | }, 139 | }, 140 | }; 141 | const actual = buildDAXSubnetGroup(2); 142 | expect(actual).toEqual(expected); 143 | expect.assertions(1); 144 | }); 145 | }); 146 | 147 | describe('#buildSubnetGroups', () => { 148 | it('skips building if no groups specified', () => { 149 | const expected = {}; 150 | const actual = buildSubnetGroups(2, []); 151 | expect(actual).toEqual(expected); 152 | expect.assertions(1); 153 | }); 154 | 155 | it('builds an RDS and Redshift subnet groups', () => { 156 | const expected = { 157 | RDSSubnetGroup: { 158 | Properties: { 159 | DBSubnetGroupDescription: { 160 | Ref: 'AWS::StackName', 161 | }, 162 | DBSubnetGroupName: { 163 | Ref: 'AWS::StackName', 164 | }, 165 | SubnetIds: [ 166 | { 167 | Ref: 'DBSubnet1', 168 | }, 169 | { 170 | Ref: 'DBSubnet2', 171 | }, 172 | ], 173 | }, 174 | Type: 'AWS::RDS::DBSubnetGroup', 175 | }, 176 | RedshiftSubnetGroup: { 177 | Properties: { 178 | Description: { 179 | Ref: 'AWS::StackName', 180 | }, 181 | SubnetIds: [ 182 | { 183 | Ref: 'DBSubnet1', 184 | }, 185 | { 186 | Ref: 'DBSubnet2', 187 | }, 188 | ], 189 | }, 190 | Type: 'AWS::Redshift::ClusterSubnetGroup', 191 | }, 192 | }; 193 | const actual = buildSubnetGroups(2, ['rds', 'redshift']); 194 | expect(actual).toEqual(expected); 195 | expect.assertions(1); 196 | }); 197 | }); 198 | }); 199 | -------------------------------------------------------------------------------- /__tests__/subnets.test.js: -------------------------------------------------------------------------------- 1 | const { splitVpc, splitSubnets, buildSubnet } = require('../src/subnets'); 2 | 3 | describe('subnets', () => { 4 | describe('#splitVpc', () => { 5 | it('splits 10.0.0.0/16 into 16 /20s', () => { 6 | const actual = splitVpc('10.0.0.0/16').map((cidr) => cidr.toString()); 7 | const expected = [ 8 | '10.0.0.0/20', 9 | '10.0.16.0/20', 10 | '10.0.32.0/20', 11 | '10.0.48.0/20', 12 | '10.0.64.0/20', 13 | '10.0.80.0/20', 14 | '10.0.96.0/20', 15 | '10.0.112.0/20', 16 | '10.0.128.0/20', 17 | '10.0.144.0/20', 18 | '10.0.160.0/20', 19 | '10.0.176.0/20', 20 | '10.0.192.0/20', 21 | '10.0.208.0/20', 22 | '10.0.224.0/20', 23 | '10.0.240.0/20', 24 | ]; 25 | 26 | expect(actual).toEqual(expected); 27 | expect.assertions(1); 28 | }); 29 | 30 | it('splits 192.168.0.0/16 into 16 /20s', () => { 31 | const actual = splitVpc('192.168.0.0/16').map((cidr) => cidr.toString()); 32 | const expected = [ 33 | '192.168.0.0/20', 34 | '192.168.16.0/20', 35 | '192.168.32.0/20', 36 | '192.168.48.0/20', 37 | '192.168.64.0/20', 38 | '192.168.80.0/20', 39 | '192.168.96.0/20', 40 | '192.168.112.0/20', 41 | '192.168.128.0/20', 42 | '192.168.144.0/20', 43 | '192.168.160.0/20', 44 | '192.168.176.0/20', 45 | '192.168.192.0/20', 46 | '192.168.208.0/20', 47 | '192.168.224.0/20', 48 | '192.168.240.0/20', 49 | ]; 50 | 51 | expect(actual).toEqual(expected); 52 | expect.assertions(1); 53 | }); 54 | }); 55 | 56 | describe('#splitSubnets', () => { 57 | it('splits 10.0.0.0/16 separate subnets in each AZ', () => { 58 | const zones = ['us-east-1a', 'us-east-1b', 'us-east-1c']; 59 | const actual = splitSubnets('10.0.0.0/16', zones); 60 | 61 | const parts = [ 62 | [ 63 | 'us-east-1a', 64 | new Map([ 65 | ['App', '10.0.0.0/21'], 66 | ['Public', '10.0.8.0/22'], 67 | ['DB', '10.0.12.0/22'], 68 | ]), 69 | ], 70 | [ 71 | 'us-east-1b', 72 | new Map([ 73 | ['App', '10.0.16.0/21'], 74 | ['Public', '10.0.24.0/22'], 75 | ['DB', '10.0.28.0/22'], 76 | ]), 77 | ], 78 | [ 79 | 'us-east-1c', 80 | new Map([ 81 | ['App', '10.0.32.0/21'], 82 | ['Public', '10.0.40.0/22'], 83 | ['DB', '10.0.44.0/22'], 84 | ]), 85 | ], 86 | ['App', ['10.0.0.0/21', '10.0.16.0/21', '10.0.32.0/21']], 87 | ['Public', ['10.0.8.0/22', '10.0.24.0/22', '10.0.40.0/22']], 88 | ['DB', ['10.0.12.0/22', '10.0.28.0/22', '10.0.44.0/22']], 89 | ]; 90 | 91 | const expected = new Map(parts); 92 | 93 | expect(actual).toEqual(expected); 94 | expect.assertions(1); 95 | }); 96 | }); 97 | 98 | describe('#buildSubnet', () => { 99 | it('builds a subnet', () => { 100 | const expected = { 101 | AppSubnet1: { 102 | Type: 'AWS::EC2::Subnet', 103 | Properties: { 104 | AvailabilityZone: 'us-east-1a', 105 | CidrBlock: '10.0.0.0/22', 106 | Tags: [ 107 | { 108 | Key: 'Name', 109 | Value: { 110 | // eslint-disable-next-line no-template-curly-in-string 111 | 'Fn::Sub': '${AWS::StackName}-app-us-east-1a', 112 | }, 113 | }, 114 | { 115 | Key: 'Network', 116 | Value: 'Private', 117 | }, 118 | ], 119 | VpcId: { 120 | Ref: 'VPC', 121 | }, 122 | }, 123 | }, 124 | }; 125 | const actual = buildSubnet('App', 1, 'us-east-1a', '10.0.0.0/22'); 126 | expect(actual).toEqual(expected); 127 | expect.assertions(1); 128 | }); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /__tests__/vpc.test.js: -------------------------------------------------------------------------------- 1 | const { 2 | buildVpc, 3 | buildInternetGateway, 4 | buildAppSecurityGroup, 5 | buildDHCPOptions, 6 | } = require('../src/vpc'); 7 | 8 | describe('vpc', () => { 9 | describe('#buildVpc', () => { 10 | it('builds a VPC', () => { 11 | const expected = { 12 | VPC: { 13 | Type: 'AWS::EC2::VPC', 14 | Properties: { 15 | CidrBlock: '10.0.0.0/16', 16 | EnableDnsSupport: true, 17 | EnableDnsHostnames: true, 18 | InstanceTenancy: 'default', 19 | Tags: [ 20 | { 21 | Key: 'Name', 22 | Value: { 23 | Ref: 'AWS::StackName', 24 | }, 25 | }, 26 | ], 27 | }, 28 | }, 29 | }; 30 | 31 | const actual = buildVpc(); 32 | expect(actual).toEqual(expected); 33 | expect.assertions(1); 34 | }); 35 | 36 | it('builds a VPC with a custom parameters', () => { 37 | const expected = { 38 | VPC: { 39 | Type: 'AWS::EC2::VPC', 40 | Properties: { 41 | CidrBlock: '192.168.0.0/16', 42 | EnableDnsSupport: true, 43 | EnableDnsHostnames: true, 44 | InstanceTenancy: 'default', 45 | Tags: [ 46 | { 47 | Key: 'Name', 48 | Value: { 49 | Ref: 'AWS::StackName', 50 | }, 51 | }, 52 | ], 53 | }, 54 | }, 55 | }; 56 | 57 | const actual = buildVpc('192.168.0.0/16'); 58 | expect(actual).toEqual(expected); 59 | expect.assertions(1); 60 | }); 61 | }); 62 | 63 | describe('#buildInternetGateway', () => { 64 | it('builds an Internet Gateway with default name', () => { 65 | const expected = { 66 | InternetGateway: { 67 | Type: 'AWS::EC2::InternetGateway', 68 | Properties: { 69 | Tags: [ 70 | { 71 | Key: 'Name', 72 | Value: { 73 | // eslint-disable-next-line no-template-curly-in-string 74 | 'Fn::Sub': '${AWS::StackName}-igw', 75 | }, 76 | }, 77 | { 78 | Key: 'Network', 79 | Value: 'Public', 80 | }, 81 | ], 82 | }, 83 | }, 84 | InternetGatewayAttachment: { 85 | Type: 'AWS::EC2::VPCGatewayAttachment', 86 | Properties: { 87 | InternetGatewayId: { 88 | Ref: 'InternetGateway', 89 | }, 90 | VpcId: { 91 | Ref: 'VPC', 92 | }, 93 | }, 94 | }, 95 | }; 96 | 97 | const actual = buildInternetGateway(); 98 | expect(actual).toEqual(expected); 99 | expect.assertions(1); 100 | }); 101 | }); 102 | 103 | describe('#buildAppSecurityGroup', () => { 104 | it('builds a security group with no options', () => { 105 | const expected = { 106 | DefaultSecurityGroupEgress: { 107 | Type: 'AWS::EC2::SecurityGroupEgress', 108 | Properties: { 109 | IpProtocol: '-1', 110 | DestinationSecurityGroupId: { 111 | 'Fn::GetAtt': ['VPC', 'DefaultSecurityGroup'], 112 | }, 113 | GroupId: { 114 | 'Fn::GetAtt': ['VPC', 'DefaultSecurityGroup'], 115 | }, 116 | }, 117 | }, 118 | AppSecurityGroup: { 119 | Type: 'AWS::EC2::SecurityGroup', 120 | Properties: { 121 | GroupDescription: 'Application Security Group', 122 | SecurityGroupEgress: [ 123 | { 124 | Description: 'permit HTTPS outbound', 125 | IpProtocol: 'tcp', 126 | FromPort: 443, 127 | ToPort: 443, 128 | CidrIp: '0.0.0.0/0', 129 | }, 130 | { 131 | DestinationPrefixListId: 'pl-63a5400a', 132 | Description: 'permit HTTPS to S3', 133 | IpProtocol: 'tcp', 134 | FromPort: 443, 135 | ToPort: 443, 136 | }, 137 | { 138 | DestinationPrefixListId: 'pl-63a5400a', 139 | Description: 'permit HTTP to S3', 140 | IpProtocol: 'tcp', 141 | FromPort: 80, 142 | ToPort: 80, 143 | }, 144 | { 145 | DestinationPrefixListId: 'pl-02cd2c6b', 146 | Description: 'permit HTTPS to DynamoDB', 147 | IpProtocol: 'tcp', 148 | FromPort: 443, 149 | ToPort: 443, 150 | }, 151 | ], 152 | SecurityGroupIngress: [ 153 | { 154 | Description: 'permit HTTPS inbound', 155 | IpProtocol: 'tcp', 156 | FromPort: 443, 157 | ToPort: 443, 158 | CidrIp: '0.0.0.0/0', 159 | }, 160 | ], 161 | VpcId: { 162 | Ref: 'VPC', 163 | }, 164 | Tags: [ 165 | { 166 | Key: 'Name', 167 | Value: { 168 | // eslint-disable-next-line no-template-curly-in-string 169 | 'Fn::Sub': '${AWS::StackName}-sg', 170 | }, 171 | }, 172 | ], 173 | }, 174 | }, 175 | }; 176 | 177 | const prefixLists = { 178 | s3: 'pl-63a5400a', 179 | dynamodb: 'pl-02cd2c6b', 180 | }; 181 | const actual = buildAppSecurityGroup(prefixLists); 182 | expect(actual).toEqual(expected); 183 | expect.assertions(1); 184 | }); 185 | }); 186 | 187 | describe('#buildDHCPOptions', () => { 188 | it('builds a DHCP option set in us-east-1', () => { 189 | const expected = { 190 | DHCPOptions: { 191 | Type: 'AWS::EC2::DHCPOptions', 192 | Properties: { 193 | DomainName: 'ec2.internal', 194 | DomainNameServers: ['AmazonProvidedDNS'], 195 | Tags: [ 196 | { 197 | Key: 'Name', 198 | Value: { 199 | // eslint-disable-next-line no-template-curly-in-string 200 | 'Fn::Sub': '${AWS::StackName}-DHCPOptionsSet', 201 | }, 202 | }, 203 | ], 204 | }, 205 | }, 206 | VPCDHCPOptionsAssociation: { 207 | Type: 'AWS::EC2::VPCDHCPOptionsAssociation', 208 | Properties: { 209 | VpcId: { 210 | Ref: 'VPC', 211 | }, 212 | DhcpOptionsId: { 213 | Ref: 'DHCPOptions', 214 | }, 215 | }, 216 | }, 217 | }; 218 | const actual = buildDHCPOptions('us-east-1'); 219 | expect(actual).toEqual(expected); 220 | expect.assertions(1); 221 | }); 222 | 223 | it('builds a DHCP option set in us-west-2', () => { 224 | const expected = { 225 | DHCPOptions: { 226 | Type: 'AWS::EC2::DHCPOptions', 227 | Properties: { 228 | DomainName: { 229 | // eslint-disable-next-line no-template-curly-in-string 230 | 'Fn::Sub': '${AWS::Region}.compute.internal', 231 | }, 232 | DomainNameServers: ['AmazonProvidedDNS'], 233 | Tags: [ 234 | { 235 | Key: 'Name', 236 | Value: { 237 | // eslint-disable-next-line no-template-curly-in-string 238 | 'Fn::Sub': '${AWS::StackName}-DHCPOptionsSet', 239 | }, 240 | }, 241 | ], 242 | }, 243 | }, 244 | VPCDHCPOptionsAssociation: { 245 | Type: 'AWS::EC2::VPCDHCPOptionsAssociation', 246 | Properties: { 247 | VpcId: { 248 | Ref: 'VPC', 249 | }, 250 | DhcpOptionsId: { 251 | Ref: 'DHCPOptions', 252 | }, 253 | }, 254 | }, 255 | }; 256 | const actual = buildDHCPOptions('us-west-2'); 257 | expect(actual).toEqual(expected); 258 | expect.assertions(1); 259 | }); 260 | }); 261 | }); 262 | -------------------------------------------------------------------------------- /__tests__/vpce.test.js: -------------------------------------------------------------------------------- 1 | const { buildEndpointServices, buildVPCEndpoint } = require('../src/vpce'); 2 | 3 | describe('vpce', () => { 4 | describe('#buildEndpointServices', () => { 5 | it('skips building endpoints if none provided', () => { 6 | const actual = buildEndpointServices(); 7 | expect(actual).toEqual({}); 8 | expect.assertions(1); 9 | }); 10 | 11 | it('builds an S3 VPC Gateway endpoint', () => { 12 | const expected = { 13 | S3VPCEndpoint: { 14 | Type: 'AWS::EC2::VPCEndpoint', 15 | Properties: { 16 | RouteTableIds: [ 17 | { 18 | Ref: 'AppRouteTable1', 19 | }, 20 | ], 21 | ServiceName: { 22 | // eslint-disable-next-line no-template-curly-in-string 23 | 'Fn::Sub': 'com.amazonaws.${AWS::Region}.s3', 24 | }, 25 | PolicyDocument: { 26 | Statement: [ 27 | { 28 | Effect: 'Allow', 29 | Principal: '*', 30 | Action: 's3:*', 31 | Resource: '*', 32 | }, 33 | ], 34 | }, 35 | VpcEndpointType: 'Gateway', 36 | VpcId: { 37 | Ref: 'VPC', 38 | }, 39 | }, 40 | }, 41 | }; 42 | const actual = buildEndpointServices(['s3'], 1); 43 | expect(actual).toEqual(expected); 44 | expect.assertions(1); 45 | }); 46 | 47 | it('builds an DynamoDB VPC Gateway endpoint', () => { 48 | const expected = { 49 | DynamodbVPCEndpoint: { 50 | Type: 'AWS::EC2::VPCEndpoint', 51 | Properties: { 52 | RouteTableIds: [ 53 | { 54 | Ref: 'AppRouteTable1', 55 | }, 56 | ], 57 | ServiceName: { 58 | // eslint-disable-next-line no-template-curly-in-string 59 | 'Fn::Sub': 'com.amazonaws.${AWS::Region}.dynamodb', 60 | }, 61 | PolicyDocument: { 62 | Statement: [ 63 | { 64 | Effect: 'Allow', 65 | Principal: '*', 66 | Action: 'dynamodb:*', 67 | Resource: '*', 68 | }, 69 | ], 70 | }, 71 | VpcEndpointType: 'Gateway', 72 | VpcId: { 73 | Ref: 'VPC', 74 | }, 75 | }, 76 | }, 77 | }; 78 | const actual = buildEndpointServices(['dynamodb'], 1); 79 | expect(actual).toEqual(expected); 80 | expect.assertions(1); 81 | }); 82 | 83 | it('builds an KMS VPC Interface endpoint', () => { 84 | const expected = { 85 | KmsVPCEndpoint: { 86 | Type: 'AWS::EC2::VPCEndpoint', 87 | Properties: { 88 | PrivateDnsEnabled: true, 89 | SecurityGroupIds: [ 90 | { 91 | Ref: 'AppSecurityGroup', 92 | }, 93 | ], 94 | SubnetIds: [ 95 | { 96 | Ref: 'AppSubnet1', 97 | }, 98 | ], 99 | ServiceName: { 100 | // eslint-disable-next-line no-template-curly-in-string 101 | 'Fn::Sub': 'com.amazonaws.${AWS::Region}.kms', 102 | }, 103 | VpcEndpointType: 'Interface', 104 | VpcId: { 105 | Ref: 'VPC', 106 | }, 107 | }, 108 | }, 109 | }; 110 | const actual = buildEndpointServices(['kms'], 1); 111 | expect(actual).toEqual(expected); 112 | expect.assertions(1); 113 | }); 114 | 115 | it('builds an SageMaker Runtime FIPS VPC Interface endpoint', () => { 116 | const expected = { 117 | SagemakerRuntimeFipsVPCEndpoint: { 118 | Type: 'AWS::EC2::VPCEndpoint', 119 | Properties: { 120 | PrivateDnsEnabled: true, 121 | SecurityGroupIds: [ 122 | { 123 | Ref: 'AppSecurityGroup', 124 | }, 125 | ], 126 | SubnetIds: [ 127 | { 128 | Ref: 'AppSubnet1', 129 | }, 130 | ], 131 | ServiceName: { 132 | // eslint-disable-next-line no-template-curly-in-string 133 | 'Fn::Sub': 'com.amazonaws.${AWS::Region}.sagemaker.runtime-fips', 134 | }, 135 | VpcEndpointType: 'Interface', 136 | VpcId: { 137 | Ref: 'VPC', 138 | }, 139 | }, 140 | }, 141 | }; 142 | const actual = buildEndpointServices(['sagemaker.runtime-fips'], 1); 143 | expect(actual).toEqual(expected); 144 | expect.assertions(1); 145 | }); 146 | }); 147 | 148 | describe('#buildVPCEndpoint', () => { 149 | it('builds an S3 VPC Gateway endpoint', () => { 150 | const expected = { 151 | S3VPCEndpoint: { 152 | Type: 'AWS::EC2::VPCEndpoint', 153 | Properties: { 154 | RouteTableIds: [ 155 | { 156 | Ref: 'AppRouteTable1', 157 | }, 158 | ], 159 | ServiceName: { 160 | // eslint-disable-next-line no-template-curly-in-string 161 | 'Fn::Sub': 'com.amazonaws.${AWS::Region}.s3', 162 | }, 163 | PolicyDocument: { 164 | Statement: [ 165 | { 166 | Effect: 'Allow', 167 | Principal: '*', 168 | Action: 's3:*', 169 | Resource: '*', 170 | }, 171 | ], 172 | }, 173 | VpcEndpointType: 'Gateway', 174 | VpcId: { 175 | Ref: 'VPC', 176 | }, 177 | }, 178 | }, 179 | }; 180 | const actual = buildVPCEndpoint('s3', { 181 | routeTableIds: [{ Ref: 'AppRouteTable1' }], 182 | }); 183 | expect(actual).toEqual(expected); 184 | expect.assertions(1); 185 | }); 186 | 187 | it('builds an SageMaker Runtime FIPS VPC Interface endpoint', () => { 188 | const expected = { 189 | SagemakerRuntimeFipsVPCEndpoint: { 190 | Type: 'AWS::EC2::VPCEndpoint', 191 | Properties: { 192 | PrivateDnsEnabled: true, 193 | SecurityGroupIds: [ 194 | { 195 | Ref: 'AppSecurityGroup', 196 | }, 197 | ], 198 | SubnetIds: [ 199 | { 200 | Ref: 'AppSubnet1', 201 | }, 202 | ], 203 | ServiceName: { 204 | // eslint-disable-next-line no-template-curly-in-string 205 | 'Fn::Sub': 'com.amazonaws.${AWS::Region}.sagemaker.runtime-fips', 206 | }, 207 | VpcEndpointType: 'Interface', 208 | VpcId: { 209 | Ref: 'VPC', 210 | }, 211 | }, 212 | }, 213 | }; 214 | const actual = buildVPCEndpoint('sagemaker.runtime-fips', { 215 | subnetIds: [{ Ref: 'AppSubnet1' }], 216 | }); 217 | expect(actual).toEqual(expected); 218 | expect.assertions(1); 219 | }); 220 | }); 221 | }); 222 | -------------------------------------------------------------------------------- /example/.npmrc: -------------------------------------------------------------------------------- 1 | registry = https://registry.npmjs.org 2 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const https = require('https'); 3 | 4 | const AWS = require('aws-sdk'); 5 | 6 | const { SECRET_ARN, RESOURCE_ARN, DATABASE_NAME, SCHEMA_NAME } = process.env; 7 | 8 | AWS.config.logger = console; 9 | 10 | const FILENAME = '/mnt/efs0/counter'; 11 | 12 | /** 13 | * Return the public IP 14 | * 15 | * @return {Promise} 16 | */ 17 | function getPublicIp() { 18 | const options = { 19 | hostname: 'checkip.amazonaws.com', 20 | port: 443, 21 | path: '/', 22 | timeout: 1000, 23 | }; 24 | return new Promise((resolve, reject) => { 25 | const req = https.get(options, (res) => { 26 | const buffers = []; 27 | res.on('data', (data) => buffers.push(data)); 28 | res.once('end', () => resolve(Buffer.concat(buffers).toString())); 29 | res.once('error', reject); 30 | }); 31 | req.once('timeout', () => { 32 | req.destroy(); 33 | }); 34 | req.once('error', reject); 35 | }); 36 | } 37 | 38 | /** 39 | * Example Handler 40 | * 41 | * @param {Object} event 42 | * @param {Object} context 43 | * @return {Promise} 44 | */ 45 | // eslint-disable-next-line no-unused-vars 46 | exports.handler = async (event, context) => { 47 | let value; 48 | if (fs.existsSync(FILENAME)) { 49 | const curValue = parseInt(fs.readFileSync(FILENAME), 10) || 0; 50 | value = curValue + 1; 51 | } else { 52 | value = 0; 53 | } 54 | 55 | fs.writeFileSync(FILENAME, value); 56 | 57 | const rds = new AWS.RDSDataService(); 58 | 59 | const params = { 60 | resourceArn: RESOURCE_ARN, 61 | secretArn: SECRET_ARN, 62 | sql: 'SELECT NOW()', 63 | database: DATABASE_NAME, 64 | schema: SCHEMA_NAME, 65 | }; 66 | 67 | const response = await rds.executeStatement(params).promise(); 68 | 69 | const { records = [] } = response; 70 | 71 | const [row] = records || []; 72 | 73 | const publicIp = await getPublicIp(); 74 | 75 | const result = { 76 | 'Public IP': publicIp.trim(), 77 | 'RDS NOW()': row[0].stringValue, 78 | 'EFS Counter': value, 79 | }; 80 | 81 | console.log('result:', result); 82 | 83 | return result; 84 | }; 85 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sls-vpc-example", 3 | "private": true, 4 | "description": "Example project for serverless-vpc-plugin", 5 | "author": "Smoke Turner, LLC ", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/smoketurner/serverless-vpc-plugin" 10 | }, 11 | "main": "index.js", 12 | "scripts": { 13 | "package": "SLS_DEBUG=* sls package", 14 | "deploy": "sls deploy -v --force", 15 | "remove": "sls remove -v", 16 | "test": "sls invoke -f example" 17 | }, 18 | "devDependencies": { 19 | "aws-sdk": "2.1520.0", 20 | "serverless": "3.38.0", 21 | "serverless-vpc-plugin": "smoketurner/serverless-vpc-plugin#master", 22 | "serverless-webpack": "5.13.0", 23 | "webpack": "5.89.0", 24 | "webpack-node-externals": "3.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example/resources/efs_cf.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Resources: 3 | AppSecurityGroupEgressEFS: 4 | Type: "AWS::EC2::SecurityGroupEgress" 5 | Properties: 6 | Description: "permit NFS (2049) to EFSSecurityGroup" 7 | DestinationSecurityGroupId: !Ref EFSSecurityGroup 8 | FromPort: 2049 9 | GroupId: !GetAtt AppSecurityGroup.GroupId 10 | IpProtocol: tcp 11 | ToPort: 2049 12 | 13 | EFSSecurityGroup: 14 | Type: "AWS::EC2::SecurityGroup" 15 | Properties: 16 | GroupDescription: "EFS Security Group" 17 | SecurityGroupEgress: 18 | - Description: "deny all outbound" 19 | IpProtocol: "-1" 20 | CidrIp: "127.0.0.1/32" 21 | SecurityGroupIngress: 22 | - Description: "permit NFS (2049) from AppSecurityGroup" 23 | FromPort: 2049 24 | IpProtocol: tcp 25 | SourceSecurityGroupId: !GetAtt AppSecurityGroup.GroupId 26 | ToPort: 2049 27 | Tags: 28 | - Key: Name 29 | Value: !Join ["-", [!Ref "AWS::StackName", efs]] 30 | VpcId: !Ref VPC 31 | 32 | EFSFileSystem: 33 | Type: "AWS::EFS::FileSystem" 34 | Properties: 35 | BackupPolicy: 36 | Status: ENABLED 37 | Encrypted: true 38 | FileSystemTags: 39 | - Key: Name 40 | Value: !Join ["-", [!Ref "AWS::StackName", efs]] 41 | FileSystemPolicy: 42 | Version: "2012-10-17" 43 | Statement: 44 | - Effect: Allow 45 | Principal: 46 | AWS: "*" 47 | Action: 48 | - "elasticfilesystem:ClientMount" 49 | - "elasticfilesystem:ClientWrite" 50 | - Effect: Deny 51 | Principal: 52 | AWS: "*" 53 | Action: "*" 54 | Condition: 55 | Bool: 56 | "aws:SecureTransport": "false" 57 | LifecyclePolicies: 58 | - TransitionToIA: AFTER_7_DAYS 59 | PerformanceMode: generalPurpose 60 | ThroughputMode: bursting 61 | 62 | EFSMountTarget1: 63 | Type: "AWS::EFS::MountTarget" 64 | Properties: 65 | FileSystemId: !Ref EFSFileSystem 66 | SecurityGroups: 67 | - !Ref EFSSecurityGroup 68 | SubnetId: !Ref AppSubnet1 69 | 70 | EFSMountTarget2: 71 | Type: "AWS::EFS::MountTarget" 72 | Properties: 73 | FileSystemId: !Ref EFSFileSystem 74 | SecurityGroups: 75 | - !Ref EFSSecurityGroup 76 | SubnetId: !Ref AppSubnet2 77 | 78 | EFSAccessPoint: 79 | Type: "AWS::EFS::AccessPoint" 80 | Properties: 81 | AccessPointTags: 82 | - Key: Name 83 | Value: !Join ["-", [!Ref "AWS::StackName", efs]] 84 | FileSystemId: !Ref EFSFileSystem 85 | PosixUser: 86 | Uid: "1001" 87 | Gid: "1001" 88 | RootDirectory: 89 | CreationInfo: 90 | OwnerGid: "1001" 91 | OwnerUid: "1001" 92 | Permissions: "0755" 93 | Path: "/mnt/efs0" 94 | -------------------------------------------------------------------------------- /example/resources/iam_cf.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Resources: 3 | ExampleLambdaRole: 4 | Type: "AWS::IAM::Role" 5 | Properties: 6 | RoleName: "${self:service}-${self:provider.stage}-${self:provider.region}-ExampleLambdaRole" 7 | AssumeRolePolicyDocument: 8 | Statement: 9 | - Effect: Allow 10 | Principal: 11 | Service: "lambda.amazonaws.com" 12 | Action: "sts:AssumeRole" 13 | Policies: 14 | - PolicyName: ExampleLambdaPolicy 15 | PolicyDocument: 16 | Statement: 17 | - Effect: Allow 18 | Action: "secretsmanager:GetSecretValue" 19 | Resource: !Ref DBSecret 20 | - Effect: Allow 21 | Action: 22 | - "rds-data:BatchExecuteStatement" 23 | - "rds-data:BeginTransaction" 24 | - "rds-data:CommitTransaction" 25 | - "rds-data:ExecuteStatement" 26 | - "rds-data:RollbackTransaction" 27 | Resource: "*" 28 | ManagedPolicyArns: 29 | - "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" 30 | - "arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess" 31 | - "arn:aws:iam::aws:policy/AmazonElasticFileSystemClientReadWriteAccess" 32 | -------------------------------------------------------------------------------- /example/resources/rds_cf.yml: -------------------------------------------------------------------------------- 1 | --- 2 | Resources: 3 | DBClusterParameterGroup: 4 | Type: "AWS::RDS::DBClusterParameterGroup" 5 | Properties: 6 | Description: "Aurora PostgreSQL 11 Parameter Group" 7 | Family: aurora-postgresql11 8 | Parameters: 9 | rds.force_ssl: 1 10 | 11 | AppSecurityGroupEgressRDS: 12 | Type: "AWS::EC2::SecurityGroupEgress" 13 | Properties: 14 | Description: "permit PostgreSQL (5432) to DBSecurityGroup" 15 | DestinationSecurityGroupId: !Ref DBSecurityGroup 16 | FromPort: 5432 17 | GroupId: !GetAtt AppSecurityGroup.GroupId 18 | IpProtocol: tcp 19 | ToPort: 5432 20 | 21 | DBSecurityGroup: 22 | Type: "AWS::EC2::SecurityGroup" 23 | Properties: 24 | GroupDescription: "RDS Security Group" 25 | SecurityGroupEgress: 26 | - Description: "deny all outbound" 27 | IpProtocol: "-1" 28 | CidrIp: "127.0.0.1/32" 29 | SecurityGroupIngress: 30 | - Description: "permit PostgreSQL (5432) from AppSecurityGroup" 31 | FromPort: 5432 32 | IpProtocol: tcp 33 | SourceSecurityGroupId: !GetAtt AppSecurityGroup.GroupId 34 | ToPort: 5432 35 | Tags: 36 | - Key: Name 37 | Value: !Join ["-", [!Ref "AWS::StackName", rds]] 38 | VpcId: !Ref VPC 39 | 40 | DBCluster: 41 | Type: "AWS::RDS::DBCluster" 42 | DeletionPolicy: Snapshot 43 | UpdateReplacePolicy: Snapshot 44 | Properties: 45 | DatabaseName: ${self:custom.databaseName} 46 | DBClusterIdentifier: "${self:service}-${self:provider.stage}" 47 | DBClusterParameterGroupName: !Ref DBClusterParameterGroup 48 | DBSubnetGroupName: !Ref RDSSubnetGroup 49 | EnableHttpEndpoint: true 50 | Engine: aurora-postgresql 51 | EngineMode: serverless 52 | EngineVersion: "11.16" # Data API only supports PostgreSQL 11.16 https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Concepts.Aurora_Fea_Regions_DB-eng.Feature.Data_API.html#Concepts.Aurora_Fea_Regions_DB-eng.Feature.Data_API.apg 53 | ManageMasterUserPassword: true 54 | MasterUsername: postgres 55 | ScalingConfiguration: 56 | AutoPause: true 57 | MaxCapacity: 2 58 | MinCapacity: 2 59 | SecondsUntilAutoPause: 300 # 5 minutes 60 | StorageEncrypted: true 61 | Tags: 62 | - Key: Name 63 | Value: !Join ["-", [!Ref "AWS::StackName", rds]] 64 | UseLatestRestorableTime: true 65 | VpcSecurityGroupIds: 66 | - !Ref DBSecurityGroup 67 | 68 | Outputs: 69 | DBClusterAddress: 70 | Description: RDS Cluster Address 71 | Value: !GetAtt DBCluster.Endpoint.Address 72 | 73 | DBClusterPort: 74 | Description: RDS Cluster Port 75 | Value: !GetAtt DBCluster.Endpoint.Port 76 | 77 | SecretArn: 78 | Description: Secret ARN 79 | Value: !GetAtt DBCluster.MasterUserSecret.SecretArn 80 | -------------------------------------------------------------------------------- /example/serverless.yml: -------------------------------------------------------------------------------- 1 | service: sls-vpc-example 2 | 3 | provider: 4 | name: aws 5 | runtime: nodejs14.x 6 | stage: ${opt:stage, 'dev'} 7 | region: ${opt:region, 'us-east-1'} 8 | versionFunctions: false 9 | logRetentionInDays: 1 10 | deploymentBucket: 11 | serverSideEncryption: AES256 12 | blockPublicAccess: true 13 | endpointType: regional 14 | environment: 15 | AWS_NODEJS_CONNECTION_REUSE_ENABLED: "1" 16 | NODE_ENV: development 17 | logs: 18 | restApi: 19 | accessLogging: false 20 | executionLogging: false 21 | fullExecutionData: false 22 | roleManagedExternally: true 23 | 24 | plugins: 25 | - serverless-vpc-plugin 26 | - serverless-webpack 27 | 28 | package: 29 | individually: true 30 | excludeDevDependencies: true 31 | 32 | custom: 33 | secretName: "rds-db-credentials/${self:service}/${self:provider.stage}" 34 | databaseName: slsvpcexample 35 | vpcConfig: 36 | cidrBlock: "10.0.0.0/16" 37 | createDbSubnet: true 38 | createNatInstance: true 39 | createParameters: true 40 | zones: 41 | - us-east-1a 42 | - us-east-1b 43 | services: 44 | - secretsmanager 45 | - rds-data 46 | - s3 47 | - elasticfilesystem 48 | subnetGroups: 49 | - rds 50 | # createBastionHost: true 51 | # bastionHostKeyName: bastion-key 52 | webpack: 53 | includeModules: 54 | forceExclude: 55 | - aws-sdk 56 | packager: npm 57 | 58 | functions: 59 | example: 60 | handler: index.handler 61 | fileSystemConfig: 62 | localMountPath: /mnt/efs0 63 | arn: 64 | 'Fn::GetAtt': [ EFSAccessPoint, Arn ] 65 | description: Example Handler 66 | dependsOn: 67 | - EFSMountTarget1 68 | - EFSMountTarget2 69 | role: ExampleLambdaRole 70 | tracing: Active 71 | environment: 72 | SECRET_ARN: 73 | "Fn::GetAtt": [ DBCluster, "MasterUserSecret.SecretArn" ] 74 | RESOURCE_ARN: 75 | "Fn::Join": 76 | - ":" 77 | - - "arn:aws:rds" 78 | - Ref: "AWS::Region" 79 | - Ref: "AWS::AccountId" 80 | - "cluster:${self:service}-${self:provider.stage}" 81 | DATABASE_NAME: ${self:custom.databaseName} 82 | SCHEMA_NAME: public 83 | 84 | resources: 85 | - ${file(resources/iam_cf.yml)} 86 | - ${file(resources/efs_cf.yml)} 87 | - ${file(resources/rds_cf.yml)} 88 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const slsw = require('serverless-webpack'); 2 | const nodeExternals = require('webpack-node-externals'); 3 | 4 | module.exports = { 5 | entry: slsw.lib.entries, 6 | target: 'node', 7 | devtool: slsw.lib.webpack.isLocal ? 'eval-source-map' : false, 8 | externals: [nodeExternals()], 9 | mode: slsw.lib.webpack.isLocal ? 'development' : 'production', 10 | optimization: { 11 | minimize: false, 12 | }, 13 | performance: { 14 | hints: false, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /jest-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "collectCoverage": true, 3 | "testEnvironment": "node", 4 | "verbose": true 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-vpc-plugin", 3 | "version": "1.0.6", 4 | "engines": { 5 | "node": ">=12.0" 6 | }, 7 | "description": "Serverless Plugin to generate a VPC", 8 | "author": "Smoke Turner, LLC ", 9 | "license": "MIT", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/smoketurner/serverless-vpc-plugin" 13 | }, 14 | "keywords": [ 15 | "serverless", 16 | "serverless plugin", 17 | "vpc", 18 | "lambda", 19 | "aws", 20 | "aws lambda", 21 | "amazon web services", 22 | "serverless.com" 23 | ], 24 | "main": "src/index.js", 25 | "scripts": { 26 | "test": "SLS_DEBUG=* jest --coverage" 27 | }, 28 | "dependencies": { 29 | "cidr-split": "0.1.2" 30 | }, 31 | "devDependencies": { 32 | "aws-sdk-mock": "5.8.0", 33 | "eslint": "8.56.0", 34 | "eslint-config-airbnb-base": "15.0.0", 35 | "eslint-config-prettier": "9.1.0", 36 | "eslint-plugin-import": "2.29.1", 37 | "eslint-plugin-jest": "27.2.3", 38 | "eslint-plugin-prettier": "5.0.1", 39 | "jest": "29.6.2", 40 | "nock": "13.3.4", 41 | "prettier": "3.0.1", 42 | "serverless": "3.38.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/az.js: -------------------------------------------------------------------------------- 1 | const { APP_SUBNET, PUBLIC_SUBNET, DB_SUBNET } = require('./constants'); 2 | const { buildEIP, buildNatGateway } = require('./natgw'); 3 | const { buildSubnet } = require('./subnets'); 4 | const { buildRoute, buildRouteTable, buildRouteTableAssociation } = require('./routes'); 5 | 6 | /** 7 | * Builds the Availability Zones for the region. 8 | * 9 | * 1.) Splits the VPC CIDR Block into /20 subnets, one per AZ. 10 | * 2.) Split each AZ /20 CIDR Block into two /21 subnets 11 | * 3.) Use the first /21 subnet for Applications 12 | * 4.) Split the second /21 subnet into two /22 subnets: one Public subnet (for load balancers), 13 | * and one for databases (RDS, ElastiCache, and Redshift) 14 | * 15 | * @param {Map} subnets Map of subnets 16 | * @param {Array} zones Array of availability zones 17 | * @param {Number} numNatGateway Number of NAT gateways (and EIPs) to provision 18 | * @param {Boolean} createDbSubnet Whether to create the DBSubnet or not 19 | * @param {Boolean} createNatInstance Whether to create a NAT instance or not 20 | * @return {Object} 21 | */ 22 | function buildAvailabilityZones( 23 | subnets, 24 | zones = [], 25 | { numNatGateway = 0, createDbSubnet = true, createNatInstance = false } = {}, 26 | ) { 27 | if (!(subnets instanceof Map) || subnets.size < 1) { 28 | return {}; 29 | } 30 | if (!Array.isArray(zones) || zones.length < 1) { 31 | return {}; 32 | } 33 | 34 | const resources = {}; 35 | 36 | if (numNatGateway > 0) { 37 | for (let index = 1; index <= numNatGateway; index += 1) { 38 | Object.assign(resources, buildEIP(index), buildNatGateway(index)); 39 | } 40 | } 41 | 42 | zones.forEach((zone, index) => { 43 | const position = index + 1; 44 | 45 | Object.assign( 46 | resources, 47 | 48 | // App Subnet 49 | buildSubnet(APP_SUBNET, position, zone, subnets.get(zone).get(APP_SUBNET)), 50 | buildRouteTable(APP_SUBNET, position), 51 | buildRouteTableAssociation(APP_SUBNET, position), 52 | // no default route on Application subnet 53 | 54 | // Public Subnet 55 | buildSubnet(PUBLIC_SUBNET, position, zone, subnets.get(zone).get(PUBLIC_SUBNET)), 56 | buildRouteTable(PUBLIC_SUBNET, position), 57 | buildRouteTableAssociation(PUBLIC_SUBNET, position), 58 | buildRoute(PUBLIC_SUBNET, position, { 59 | GatewayId: 'InternetGateway', 60 | }), 61 | ); 62 | 63 | const params = {}; 64 | if (numNatGateway > 0) { 65 | params.NatGatewayId = `NatGateway${(index % numNatGateway) + 1}`; 66 | } else if (createNatInstance) { 67 | params.InstanceId = 'NatInstance'; 68 | } 69 | 70 | // only set default route on Application subnet to a NAT Gateway or NAT Instance 71 | if (Object.keys(params).length > 0) { 72 | Object.assign(resources, buildRoute(APP_SUBNET, position, params)); 73 | } 74 | 75 | if (createDbSubnet) { 76 | // DB Subnet 77 | Object.assign( 78 | resources, 79 | buildSubnet(DB_SUBNET, position, zone, subnets.get(zone).get(DB_SUBNET)), 80 | buildRouteTable(DB_SUBNET, position), 81 | buildRouteTableAssociation(DB_SUBNET, position), 82 | // no default route on DB subnet 83 | ); 84 | } 85 | }); 86 | 87 | return resources; 88 | } 89 | 90 | module.exports = { 91 | buildAvailabilityZones, 92 | }; 93 | -------------------------------------------------------------------------------- /src/bastion.js: -------------------------------------------------------------------------------- 1 | const https = require('https'); 2 | 3 | const { PUBLIC_SUBNET } = require('./constants'); 4 | 5 | /** 6 | * Return the public IP 7 | * 8 | * @return {Promise} 9 | */ 10 | function getPublicIp() { 11 | const options = { 12 | hostname: 'checkip.amazonaws.com', 13 | port: 443, 14 | path: '/', 15 | timeout: 1000, 16 | }; 17 | return new Promise((resolve, reject) => { 18 | const req = https.get(options, (res) => { 19 | const buffers = []; 20 | res.on('data', (data) => buffers.push(data)); 21 | res.once('end', () => resolve(Buffer.concat(buffers).toString())); 22 | res.once('error', reject); 23 | }); 24 | req.once('timeout', () => { 25 | req.destroy(); 26 | }); 27 | req.once('error', reject); 28 | }); 29 | } 30 | 31 | /** 32 | * Build an EIP for the bastion host 33 | * 34 | * @return {Object} 35 | */ 36 | function buildBastionEIP({ name = 'BastionEIP' } = {}) { 37 | return { 38 | [name]: { 39 | Type: 'AWS::EC2::EIP', 40 | Properties: { 41 | Domain: 'vpc', 42 | }, 43 | }, 44 | }; 45 | } 46 | 47 | /** 48 | * Build an IAM role for the bastion host 49 | * 50 | * @param {Object} params 51 | * @return {Object} 52 | */ 53 | function buildBastionIamRole({ name = 'BastionIamRole' } = {}) { 54 | return { 55 | [name]: { 56 | Type: 'AWS::IAM::Role', 57 | Properties: { 58 | AssumeRolePolicyDocument: { 59 | Statement: [ 60 | { 61 | Effect: 'Allow', 62 | Principal: { 63 | Service: 'ec2.amazonaws.com', 64 | }, 65 | Action: 'sts:AssumeRole', 66 | }, 67 | ], 68 | }, 69 | Policies: [ 70 | { 71 | PolicyName: 'AllowEIPAssociation', 72 | PolicyDocument: { 73 | Version: '2012-10-17', 74 | Statement: [ 75 | { 76 | Action: 'ec2:AssociateAddress', 77 | Resource: '*', 78 | Effect: 'Allow', 79 | }, 80 | ], 81 | }, 82 | }, 83 | ], 84 | ManagedPolicyArns: [ 85 | // eslint-disable-next-line no-template-curly-in-string 86 | { 'Fn::Sub': 'arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore' }, 87 | ], 88 | }, 89 | }, 90 | }; 91 | } 92 | 93 | /** 94 | * Build an instance profile for the bastion host 95 | * 96 | * @param {Object} params 97 | * @return {Object} 98 | */ 99 | function buildBastionInstanceProfile({ name = 'BastionInstanceProfile' } = {}) { 100 | return { 101 | [name]: { 102 | Type: 'AWS::IAM::InstanceProfile', 103 | Properties: { 104 | Roles: [ 105 | { 106 | Ref: 'BastionIamRole', 107 | }, 108 | ], 109 | }, 110 | }, 111 | }; 112 | } 113 | 114 | /** 115 | * Build the auto-scaling group launch configuration for the bastion host 116 | * 117 | * @param {String} keyPairName Existing key pair name 118 | * @param {Object} params 119 | * @return {Object} 120 | */ 121 | function buildBastionLaunchConfiguration( 122 | keyPairName, 123 | { name = 'BastionLaunchConfiguration' } = {}, 124 | ) { 125 | return { 126 | [name]: { 127 | Type: 'AWS::AutoScaling::LaunchConfiguration', 128 | Properties: { 129 | AssociatePublicIpAddress: true, 130 | BlockDeviceMappings: [ 131 | { 132 | DeviceName: '/dev/xvda', 133 | Ebs: { 134 | VolumeSize: 10, 135 | VolumeType: 'gp3', 136 | DeleteOnTermination: true, 137 | }, 138 | }, 139 | ], 140 | KeyName: keyPairName, 141 | ImageId: { 142 | Ref: 'LatestAmiId', 143 | }, 144 | InstanceMonitoring: false, 145 | IamInstanceProfile: { 146 | Ref: 'BastionInstanceProfile', 147 | }, 148 | InstanceType: 't2.micro', 149 | SecurityGroups: [ 150 | { 151 | Ref: 'BastionSecurityGroup', 152 | }, 153 | ], 154 | // On-Demand price of t2.micro in us-east-1 (https://aws.amazon.com/ec2/pricing/on-demand/) 155 | SpotPrice: '0.0116', 156 | // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-helper-scripts-reference.html 157 | UserData: { 158 | 'Fn::Base64': { 159 | 'Fn::Join': [ 160 | '', 161 | [ 162 | '#!/bin/bash -xe\n', 163 | '/usr/bin/yum update -y\n', 164 | '/usr/bin/yum install -y aws-cfn-bootstrap\n', 165 | 'EIP_ALLOCATION_ID=', 166 | { 'Fn::GetAtt': ['BastionEIP', 'AllocationId'] }, 167 | '\n', 168 | 'INSTANCE_ID=`/usr/bin/curl -sq http://169.254.169.254/latest/meta-data/instance-id`\n', 169 | // eslint-disable-next-line no-template-curly-in-string 170 | '/usr/bin/aws ec2 associate-address --instance-id ${INSTANCE_ID} --allocation-id ${EIP_ALLOCATION_ID} --region ', 171 | { Ref: 'AWS::Region' }, 172 | '\n', 173 | '/opt/aws/bin/cfn-signal --exit-code 0 --stack ', 174 | { Ref: 'AWS::StackName' }, 175 | ' --resource BastionAutoScalingGroup ', 176 | ' --region ', 177 | { Ref: 'AWS::Region' }, 178 | '\n', 179 | ], 180 | ], 181 | }, 182 | }, 183 | }, 184 | }, 185 | }; 186 | } 187 | 188 | /** 189 | * Build the bastion host auto-scaling group 190 | * 191 | * @param {Number} numZones Number of availability zones 192 | * @param {Object} params 193 | * @return {Object} 194 | */ 195 | function buildBastionAutoScalingGroup(numZones = 0, { name = 'BastionAutoScalingGroup' } = {}) { 196 | if (numZones < 1) { 197 | return {}; 198 | } 199 | 200 | const zones = []; 201 | for (let i = 1; i <= numZones; i += 1) { 202 | zones.push({ Ref: `${PUBLIC_SUBNET}Subnet${i}` }); 203 | } 204 | 205 | return { 206 | [name]: { 207 | Type: 'AWS::AutoScaling::AutoScalingGroup', 208 | CreationPolicy: { 209 | ResourceSignal: { 210 | Count: 1, 211 | Timeout: 'PT10M', 212 | }, 213 | }, 214 | Properties: { 215 | LaunchConfigurationName: { 216 | Ref: 'BastionLaunchConfiguration', 217 | }, 218 | VPCZoneIdentifier: zones, 219 | MinSize: 1, 220 | MaxSize: 1, 221 | Cooldown: '300', 222 | DesiredCapacity: 1, 223 | Tags: [ 224 | { 225 | Key: 'Name', 226 | Value: { 227 | // eslint-disable-next-line no-template-curly-in-string 228 | 'Fn::Sub': '${AWS::StackName}-bastion', 229 | }, 230 | PropagateAtLaunch: true, 231 | }, 232 | ], 233 | }, 234 | }, 235 | }; 236 | } 237 | 238 | /** 239 | * Build a SecurityGroup to be used by the bastion host 240 | * 241 | * @param {String} sourceIp source IP address 242 | * @param {Object} params 243 | * @return {Object} 244 | */ 245 | function buildBastionSecurityGroup(sourceIp = '0.0.0.0/0', { name = 'BastionSecurityGroup' } = {}) { 246 | return { 247 | [name]: { 248 | Type: 'AWS::EC2::SecurityGroup', 249 | Properties: { 250 | GroupDescription: 'Bastion Host', 251 | VpcId: { 252 | Ref: 'VPC', 253 | }, 254 | SecurityGroupIngress: [ 255 | { 256 | Description: 'permit inbound SSH', 257 | IpProtocol: 'tcp', 258 | FromPort: 22, 259 | ToPort: 22, 260 | CidrIp: sourceIp, 261 | }, 262 | { 263 | Description: 'permit inbound ICMP', 264 | IpProtocol: 'icmp', 265 | FromPort: -1, 266 | ToPort: -1, 267 | CidrIp: sourceIp, 268 | }, 269 | ], 270 | Tags: [ 271 | { 272 | Key: 'Name', 273 | Value: { 274 | // eslint-disable-next-line no-template-curly-in-string 275 | 'Fn::Sub': '${AWS::StackName}-bastion', 276 | }, 277 | }, 278 | ], 279 | }, 280 | }, 281 | }; 282 | } 283 | 284 | /** 285 | * Build the bastion host 286 | * 287 | * @param {String} keyPairName Existing key pair name 288 | * @param {Number} numZones Number of availability zones 289 | * @return {Promise} 290 | */ 291 | async function buildBastion(keyPairName, numZones = 0) { 292 | if (numZones < 1) { 293 | return {}; 294 | } 295 | let publicIp = '0.0.0.0/0'; 296 | try { 297 | publicIp = await getPublicIp(); 298 | if (publicIp) { 299 | publicIp = `${publicIp.trim()}/32`; 300 | } 301 | } catch (err) { 302 | console.error('Unable to discover public IP address:', err); 303 | } 304 | 305 | return { 306 | ...buildBastionEIP(), 307 | ...buildBastionIamRole(), 308 | ...buildBastionInstanceProfile(), 309 | ...buildBastionSecurityGroup(publicIp), 310 | ...buildBastionLaunchConfiguration(keyPairName), 311 | ...buildBastionAutoScalingGroup(numZones), 312 | }; 313 | } 314 | 315 | module.exports = { 316 | getPublicIp, 317 | buildBastion, 318 | buildBastionAutoScalingGroup, 319 | buildBastionEIP, 320 | buildBastionIamRole, 321 | buildBastionInstanceProfile, 322 | buildBastionLaunchConfiguration, 323 | buildBastionSecurityGroup, 324 | }; 325 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {Number} Default EIP limit per VPC 3 | * @see https://docs.aws.amazon.com/vpc/latest/userguide/amazon-vpc-limits.html 4 | */ 5 | const DEFAULT_VPC_EIP_LIMIT = 5; 6 | 7 | /** 8 | * @type {String} Application subnet 9 | */ 10 | const APP_SUBNET = 'App'; 11 | 12 | /** 13 | * @type {String} Public subnet 14 | */ 15 | const PUBLIC_SUBNET = 'Public'; 16 | 17 | /** 18 | * @type {String} Database subnet 19 | */ 20 | const DB_SUBNET = 'DB'; 21 | 22 | /** 23 | * @type {Array} Valid subnet groups 24 | */ 25 | const VALID_SUBNET_GROUPS = ['rds', 'redshift', 'elasticache', 'dax']; 26 | 27 | module.exports = { 28 | DEFAULT_VPC_EIP_LIMIT, 29 | APP_SUBNET, 30 | PUBLIC_SUBNET, 31 | DB_SUBNET, 32 | VALID_SUBNET_GROUPS, 33 | }; 34 | -------------------------------------------------------------------------------- /src/flow_logs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Build an S3 bucket for logging 3 | * 4 | * @return {Object} 5 | */ 6 | function buildLogBucket() { 7 | return { 8 | LogBucket: { 9 | Type: 'AWS::S3::Bucket', 10 | DeletionPolicy: 'Retain', 11 | UpdateReplacePolicy: 'Retain', 12 | Properties: { 13 | BucketEncryption: { 14 | ServerSideEncryptionConfiguration: [ 15 | { 16 | ServerSideEncryptionByDefault: { 17 | SSEAlgorithm: 'AES256', 18 | }, 19 | }, 20 | ], 21 | }, 22 | LifecycleConfiguration: { 23 | Rules: [ 24 | { 25 | ExpirationInDays: 365, 26 | Id: 'RetentionRule', 27 | Prefix: 'AWSLogs', 28 | Status: 'Enabled', 29 | Transitions: [ 30 | { 31 | TransitionInDays: 30, 32 | StorageClass: 'STANDARD_IA', 33 | }, 34 | { 35 | TransitionInDays: 90, 36 | StorageClass: 'GLACIER', 37 | }, 38 | ], 39 | }, 40 | ], 41 | }, 42 | OwnershipControls: { 43 | Rules: [ 44 | { 45 | ObjectOwnership: 'BucketOwnerEnforced', 46 | }, 47 | ], 48 | }, 49 | PublicAccessBlockConfiguration: { 50 | BlockPublicAcls: true, 51 | BlockPublicPolicy: true, 52 | IgnorePublicAcls: true, 53 | RestrictPublicBuckets: true, 54 | }, 55 | Tags: [ 56 | { 57 | Key: 'Name', 58 | Value: { 59 | // eslint-disable-next-line no-template-curly-in-string 60 | 'Fn::Sub': '${AWS::StackName} Logs', 61 | }, 62 | }, 63 | ], 64 | VersioningConfiguration: { 65 | Status: 'Enabled', 66 | }, 67 | }, 68 | }, 69 | }; 70 | } 71 | 72 | /** 73 | * Build an S3 Bucket Policy for the logging bucket 74 | * 75 | * @return {Object} 76 | * @see https://docs.aws.amazon.com/vpc/latest/userguide/flow-logs-s3.html#flow-logs-s3-permissions 77 | */ 78 | function buildLogBucketPolicy() { 79 | return { 80 | LogBucketPolicy: { 81 | Type: 'AWS::S3::BucketPolicy', 82 | Properties: { 83 | Bucket: { 84 | Ref: 'LogBucket', 85 | }, 86 | PolicyDocument: { 87 | Version: '2012-10-17', 88 | Statement: [ 89 | { 90 | Sid: 'AWSLogDeliveryAclCheck', 91 | Effect: 'Allow', 92 | Principal: { 93 | Service: 'delivery.logs.amazonaws.com', 94 | }, 95 | Action: ['s3:GetBucketAcl', 's3:ListBucket'], 96 | Resource: { 97 | 'Fn::GetAtt': ['LogBucket', 'Arn'], 98 | }, 99 | Condition: { 100 | StringEquals: { 101 | 'aws:SourceAccount': { 102 | Ref: 'AWS::AccountId', 103 | }, 104 | }, 105 | ArnLike: { 106 | 'aws:SourceArn': { 107 | // eslint-disable-next-line no-template-curly-in-string 108 | 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:*', 109 | }, 110 | }, 111 | }, 112 | }, 113 | { 114 | Sid: 'AWSLogDeliveryWrite', 115 | Effect: 'Allow', 116 | Principal: { 117 | Service: 'delivery.logs.amazonaws.com', 118 | }, 119 | Action: 's3:PutObject', 120 | Resource: { 121 | // eslint-disable-next-line no-template-curly-in-string 122 | 'Fn::Sub': 'arn:${AWS::Partition}:s3:::${LogBucket}/*', 123 | }, 124 | Condition: { 125 | StringEquals: { 126 | 'aws:SourceAccount': { 127 | Ref: 'AWS::AccountId', 128 | }, 129 | 's3:x-amz-acl': 'bucket-owner-full-control', 130 | }, 131 | ArnLike: { 132 | 'aws:SourceArn': { 133 | // eslint-disable-next-line no-template-curly-in-string 134 | 'Fn::Sub': 'arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:*', 135 | }, 136 | }, 137 | }, 138 | }, 139 | { 140 | Sid: 'AllowSSLRequestsOnly', 141 | Effect: 'Deny', 142 | Principal: '*', 143 | Action: 's3:*', 144 | Resource: [ 145 | { 146 | // eslint-disable-next-line no-template-curly-in-string 147 | 'Fn::Sub': '${LogBucket.Arn}/*', 148 | }, 149 | { 150 | 'Fn::GetAtt': 'LogBucket.Arn', 151 | }, 152 | ], 153 | Condition: { 154 | Bool: { 155 | 'aws:SecureTransport': false, 156 | }, 157 | }, 158 | }, 159 | ], 160 | }, 161 | }, 162 | }, 163 | }; 164 | } 165 | 166 | /** 167 | * Build a VPC FlowLog definition that logs to an S3 bucket 168 | * 169 | * @param {Object} params 170 | * @return {Object} 171 | */ 172 | function buildVpcFlowLogs({ name = 'S3FlowLog' } = {}) { 173 | return { 174 | [name]: { 175 | Type: 'AWS::EC2::FlowLog', 176 | DependsOn: 'LogBucketPolicy', 177 | Properties: { 178 | DestinationOptions: { 179 | FileFormat: 'parquet', 180 | HiveCompatiblePartitions: true, 181 | PerHourPartition: true, 182 | }, 183 | LogDestination: { 184 | 'Fn::GetAtt': ['LogBucket', 'Arn'], 185 | }, 186 | LogDestinationType: 's3', 187 | // eslint-disable-next-line no-template-curly-in-string 188 | LogFormat: '${version} ${account-id} ${interface-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${start} ${end} ${action} ${log-status} ${vpc-id} ${subnet-id} ${instance-id} ${tcp-flags} ${type} ${pkt-srcaddr} ${pkt-dstaddr} ${region} ${az-id} ${sublocation-type} ${sublocation-id} ${pkt-src-aws-service} ${pkt-dst-aws-service} ${flow-direction} ${traffic-path}', 189 | MaxAggregationInterval: 60, // seconds 190 | ResourceId: { 191 | Ref: 'VPC', 192 | }, 193 | ResourceType: 'VPC', 194 | TrafficType: 'ALL', 195 | }, 196 | }, 197 | }; 198 | } 199 | 200 | module.exports = { 201 | buildLogBucket, 202 | buildLogBucketPolicy, 203 | buildVpcFlowLogs, 204 | }; 205 | -------------------------------------------------------------------------------- /src/nacl.js: -------------------------------------------------------------------------------- 1 | const { APP_SUBNET, PUBLIC_SUBNET, DB_SUBNET } = require('./constants'); 2 | 3 | /** 4 | * Build a Network Access Control List (ACL) 5 | * 6 | * @param {String} name 7 | * @return {Object} 8 | */ 9 | function buildNetworkAcl(name) { 10 | const cfName = `${name}NetworkAcl`; 11 | 12 | return { 13 | [cfName]: { 14 | Type: 'AWS::EC2::NetworkAcl', 15 | Properties: { 16 | Tags: [ 17 | { 18 | Key: 'Name', 19 | Value: { 20 | 'Fn::Sub': `\${AWS::StackName}-${name.toLowerCase()}`, 21 | }, 22 | }, 23 | ], 24 | VpcId: { 25 | Ref: 'VPC', 26 | }, 27 | }, 28 | }, 29 | }; 30 | } 31 | 32 | /** 33 | * Build a Network ACL entry 34 | * 35 | * @param {String} name 36 | * @param {String} CidrBlock 37 | * @param {Object} params 38 | * @return {Object} 39 | */ 40 | function buildNetworkAclEntry( 41 | name, 42 | CidrBlock, 43 | { Egress = false, Protocol = -1, RuleAction = 'allow', RuleNumber = 100 } = {}, 44 | ) { 45 | const direction = Egress ? 'Egress' : 'Ingress'; 46 | const cfName = `${name}${direction}${RuleNumber}`; 47 | return { 48 | [cfName]: { 49 | Type: 'AWS::EC2::NetworkAclEntry', 50 | Properties: { 51 | CidrBlock, 52 | NetworkAclId: { 53 | Ref: name, 54 | }, 55 | Egress, 56 | Protocol, 57 | RuleAction, 58 | RuleNumber, 59 | }, 60 | }, 61 | }; 62 | } 63 | 64 | /** 65 | * Build a Subnet Network ACL Association 66 | * 67 | * @param {String} name 68 | * @param {Number} position 69 | */ 70 | function buildNetworkAclAssociation(name, position) { 71 | const cfName = `${name}SubnetNetworkAclAssociation${position}`; 72 | return { 73 | [cfName]: { 74 | Type: 'AWS::EC2::SubnetNetworkAclAssociation', 75 | Properties: { 76 | SubnetId: { 77 | Ref: `${name}Subnet${position}`, 78 | }, 79 | NetworkAclId: { 80 | Ref: `${name}NetworkAcl`, 81 | }, 82 | }, 83 | }, 84 | }; 85 | } 86 | 87 | /** 88 | * Build the Public Network ACL 89 | * 90 | * @param {Number} numZones Number of availability zones 91 | */ 92 | function buildPublicNetworkAcl(numZones = 0) { 93 | if (numZones < 1) { 94 | return {}; 95 | } 96 | 97 | const resources = {}; 98 | 99 | Object.assign( 100 | resources, 101 | buildNetworkAcl(PUBLIC_SUBNET), 102 | buildNetworkAclEntry(`${PUBLIC_SUBNET}NetworkAcl`, '0.0.0.0/0'), 103 | buildNetworkAclEntry(`${PUBLIC_SUBNET}NetworkAcl`, '0.0.0.0/0', { 104 | Egress: true, 105 | }), 106 | ); 107 | 108 | for (let i = 1; i <= numZones; i += 1) { 109 | Object.assign(resources, buildNetworkAclAssociation(PUBLIC_SUBNET, i)); 110 | } 111 | 112 | return resources; 113 | } 114 | 115 | /** 116 | * Build the Application Network ACL 117 | * 118 | * @param {Number} numZones Number of availability zones 119 | */ 120 | function buildAppNetworkAcl(numZones = 0) { 121 | if (numZones < 1) { 122 | return {}; 123 | } 124 | 125 | const resources = {}; 126 | 127 | Object.assign( 128 | resources, 129 | buildNetworkAcl(APP_SUBNET), 130 | buildNetworkAclEntry(`${APP_SUBNET}NetworkAcl`, '0.0.0.0/0'), 131 | buildNetworkAclEntry(`${APP_SUBNET}NetworkAcl`, '0.0.0.0/0', { 132 | Egress: true, 133 | }), 134 | ); 135 | 136 | for (let i = 1; i <= numZones; i += 1) { 137 | Object.assign(resources, buildNetworkAclAssociation(APP_SUBNET, i)); 138 | } 139 | 140 | return resources; 141 | } 142 | 143 | /** 144 | * Build the Database Network ACL 145 | * 146 | * @param {Array} appSubnets Array of application subnets 147 | */ 148 | function buildDBNetworkAcl(appSubnets = []) { 149 | if (!Array.isArray(appSubnets) || appSubnets.length < 1) { 150 | return {}; 151 | } 152 | 153 | const resources = buildNetworkAcl(DB_SUBNET); 154 | 155 | appSubnets.forEach((subnet, index) => { 156 | Object.assign( 157 | resources, 158 | buildNetworkAclEntry(`${DB_SUBNET}NetworkAcl`, subnet, { 159 | RuleNumber: 100 + index, 160 | }), 161 | buildNetworkAclEntry(`${DB_SUBNET}NetworkAcl`, subnet, { 162 | RuleNumber: 100 + index, 163 | Egress: true, 164 | }), 165 | buildNetworkAclAssociation(DB_SUBNET, index + 1), 166 | ); 167 | }); 168 | 169 | return resources; 170 | } 171 | 172 | module.exports = { 173 | buildNetworkAcl, 174 | buildNetworkAclEntry, 175 | buildNetworkAclAssociation, 176 | buildPublicNetworkAcl, 177 | buildAppNetworkAcl, 178 | buildDBNetworkAcl, 179 | }; 180 | -------------------------------------------------------------------------------- /src/nat_instance.js: -------------------------------------------------------------------------------- 1 | const { PUBLIC_SUBNET } = require('./constants'); 2 | 3 | /** 4 | * Build a SecurityGroup to be used by the NAT instance 5 | * 6 | * @return {Object} 7 | */ 8 | function buildNatSecurityGroup() { 9 | return { 10 | NatSecurityGroup: { 11 | Type: 'AWS::EC2::SecurityGroup', 12 | Properties: { 13 | GroupDescription: 'NAT Instance', 14 | VpcId: { 15 | Ref: 'VPC', 16 | }, 17 | SecurityGroupEgress: [ 18 | { 19 | Description: 'permit outbound HTTP to the Internet', 20 | IpProtocol: 'tcp', 21 | FromPort: 80, 22 | ToPort: 80, 23 | CidrIp: '0.0.0.0/0', 24 | }, 25 | { 26 | Description: 'permit outbound HTTPS to the Internet', 27 | IpProtocol: 'tcp', 28 | FromPort: 443, 29 | ToPort: 443, 30 | CidrIp: '0.0.0.0/0', 31 | }, 32 | ], 33 | SecurityGroupIngress: [ 34 | { 35 | Description: 'permit inbound HTTP from AppSecurityGroup', 36 | IpProtocol: 'tcp', 37 | FromPort: 80, 38 | ToPort: 80, 39 | SourceSecurityGroupId: { 40 | Ref: 'AppSecurityGroup', 41 | }, 42 | }, 43 | { 44 | Description: 'permit inbound HTTPS from AppSecurityGroup', 45 | IpProtocol: 'tcp', 46 | FromPort: 443, 47 | ToPort: 443, 48 | SourceSecurityGroupId: { 49 | Ref: 'AppSecurityGroup', 50 | }, 51 | }, 52 | ], 53 | Tags: [ 54 | { 55 | Key: 'Name', 56 | Value: { 57 | // eslint-disable-next-line no-template-curly-in-string 58 | 'Fn::Sub': '${AWS::StackName}-nat', 59 | }, 60 | }, 61 | ], 62 | }, 63 | }, 64 | }; 65 | } 66 | 67 | /** 68 | * Build the NAT instance 69 | * 70 | * @param {Object} imageId AMI image ID 71 | * @param {Array} zones Array of availability zones 72 | * @param {Object} params 73 | * @return {Object} 74 | */ 75 | function buildNatInstance(imageId, zones = [], { name = 'NatInstance' } = {}) { 76 | if (!imageId) { 77 | return {}; 78 | } 79 | if (!Array.isArray(zones) || zones.length < 1) { 80 | return {}; 81 | } 82 | 83 | return { 84 | [name]: { 85 | Type: 'AWS::EC2::Instance', 86 | DependsOn: 'InternetGatewayAttachment', 87 | Properties: { 88 | AvailabilityZone: { 89 | 'Fn::Select': ['0', zones], 90 | }, 91 | BlockDeviceMappings: [ 92 | { 93 | DeviceName: '/dev/xvda', 94 | Ebs: { 95 | VolumeSize: 10, 96 | VolumeType: 'gp2', 97 | DeleteOnTermination: true, 98 | }, 99 | }, 100 | ], 101 | ImageId: imageId, // amzn-ami-vpc-nat-hvm-2018.03.0.20181116-x86_64-ebs 102 | InstanceType: 't2.micro', 103 | Monitoring: false, 104 | NetworkInterfaces: [ 105 | { 106 | AssociatePublicIpAddress: true, 107 | DeleteOnTermination: true, 108 | Description: 'eth0', 109 | DeviceIndex: '0', 110 | GroupSet: [ 111 | { 112 | Ref: 'NatSecurityGroup', 113 | }, 114 | ], 115 | SubnetId: { 116 | Ref: `${PUBLIC_SUBNET}Subnet1`, 117 | }, 118 | }, 119 | ], 120 | SourceDestCheck: false, // required for a NAT instance 121 | Tags: [ 122 | { 123 | Key: 'Name', 124 | Value: { 125 | // eslint-disable-next-line no-template-curly-in-string 126 | 'Fn::Sub': '${AWS::StackName}-nat', 127 | }, 128 | }, 129 | ], 130 | }, 131 | }, 132 | }; 133 | } 134 | 135 | module.exports = { 136 | buildNatInstance, 137 | buildNatSecurityGroup, 138 | }; 139 | -------------------------------------------------------------------------------- /src/natgw.js: -------------------------------------------------------------------------------- 1 | const { PUBLIC_SUBNET } = require('./constants'); 2 | 3 | /** 4 | * Build an EIP 5 | * 6 | * @param {Number} position 7 | * @return {Object} 8 | */ 9 | function buildEIP(position) { 10 | const cfName = `EIP${position}`; 11 | return { 12 | [cfName]: { 13 | Type: 'AWS::EC2::EIP', 14 | Properties: { 15 | Domain: 'vpc', 16 | }, 17 | }, 18 | }; 19 | } 20 | 21 | /** 22 | * Build a NatGateway in a given AZ 23 | * 24 | * @param {Number} position 25 | * @return {Object} 26 | */ 27 | function buildNatGateway(position) { 28 | const cfName = `NatGateway${position}`; 29 | const subnet = `${PUBLIC_SUBNET}Subnet${position}`; 30 | return { 31 | [cfName]: { 32 | Type: 'AWS::EC2::NatGateway', 33 | Properties: { 34 | AllocationId: { 35 | 'Fn::GetAtt': [`EIP${position}`, 'AllocationId'], 36 | }, 37 | SubnetId: { 38 | Ref: subnet, 39 | }, 40 | Tags: [ 41 | { 42 | Key: 'Name', 43 | Value: { 44 | 'Fn::Sub': `\${AWS::StackName}-\${${subnet}.AvailabilityZone}`, 45 | }, 46 | }, 47 | { 48 | Key: 'Network', 49 | Value: 'Public', 50 | }, 51 | ], 52 | }, 53 | }, 54 | }; 55 | } 56 | 57 | module.exports = { 58 | buildEIP, 59 | buildNatGateway, 60 | }; 61 | -------------------------------------------------------------------------------- /src/outputs.js: -------------------------------------------------------------------------------- 1 | const { VALID_SUBNET_GROUPS } = require('./constants'); 2 | 3 | /** 4 | * Append subnets to output 5 | * 6 | * @param {Array} subnets 7 | * @param {Object} outputs 8 | */ 9 | function appendSubnets(subnets, outputs) { 10 | const subnetOutputs = subnets.map((subnet) => ({ 11 | [`${subnet.Ref}`]: { 12 | Value: subnet, 13 | }, 14 | })); 15 | 16 | Object.assign(outputs, ...subnetOutputs); 17 | } 18 | 19 | /** 20 | * Append subnet groups to output 21 | * 22 | * @param {Array} subnetGroups 23 | * @param {Object} outputs 24 | */ 25 | function appendSubnetGroups(subnetGroups, outputs) { 26 | const typesToNames = { 27 | rds: 'RDSSubnetGroup', 28 | redshift: 'RedshiftSubnetGroup', 29 | elasticache: 'ElastiCacheSubnetGroup', 30 | dax: 'DAXSubnetGroup', 31 | }; 32 | 33 | const subnetGroupOutputs = subnetGroups.map((subnetGroup) => ({ 34 | [typesToNames[subnetGroup]]: { 35 | Description: `Subnet Group for ${subnetGroup}`, 36 | Value: { 37 | Ref: typesToNames[subnetGroup], 38 | }, 39 | }, 40 | })); 41 | 42 | Object.assign(outputs, ...subnetGroupOutputs); 43 | } 44 | 45 | /** 46 | * Append bastion host to output 47 | * 48 | * @param {Object} outputs 49 | */ 50 | function appendBastionHost(outputs) { 51 | // eslint-disable-next-line no-param-reassign 52 | outputs.BastionSSHUser = { 53 | Description: 'SSH username for the Bastion host', 54 | Value: 'ec2-user', 55 | }; 56 | // eslint-disable-next-line no-param-reassign 57 | outputs.BastionEIP = { 58 | Description: 'Public IP of Bastion host', 59 | Value: { 60 | Ref: 'BastionEIP', 61 | }, 62 | }; 63 | } 64 | 65 | /** 66 | * Append export outputs 67 | * 68 | * @param {Object} outputs 69 | */ 70 | function appendExports(outputs) { 71 | Object.entries(outputs).forEach(([name, value]) => { 72 | // eslint-disable-next-line no-param-reassign 73 | value.Export = { 74 | Name: { 75 | 'Fn::Sub': `\${AWS::StackName}-${name}`, 76 | }, 77 | }; 78 | }); 79 | } 80 | 81 | /** 82 | * Build CloudFormation Outputs on common resources 83 | * 84 | * @param {Boolean} createBastionHost 85 | * @param {Boolean} createDbSubnet 86 | * @param {Array} subnetGroups 87 | * @param {Array} subnets 88 | * @param {Boolean} exportOutputs 89 | * @return {Object} 90 | */ 91 | function buildOutputs({ 92 | createBastionHost = false, 93 | createDbSubnet = true, 94 | subnetGroups = VALID_SUBNET_GROUPS, 95 | subnets = [], 96 | exportOutputs = false, 97 | } = {}) { 98 | const outputs = { 99 | VPC: { 100 | Description: 'VPC logical resource ID', 101 | Value: { 102 | Ref: 'VPC', 103 | }, 104 | }, 105 | LambdaExecutionSecurityGroupId: { 106 | Description: 'DEPRECATED - Please use AppSecurityGroupId instead', 107 | Value: { 108 | Ref: 'AppSecurityGroup', 109 | }, 110 | }, 111 | AppSecurityGroupId: { 112 | Description: 'Security Group ID that the applications use when executing within the VPC', 113 | Value: { 114 | Ref: 'AppSecurityGroup', 115 | }, 116 | }, 117 | }; 118 | 119 | // subnet groups need at least 2 subnets 120 | if (createDbSubnet && Array.isArray(subnets) && subnets.length > 1) { 121 | appendSubnetGroups(subnetGroups, outputs); 122 | } 123 | 124 | if (Array.isArray(subnets) && subnets.length > 0) { 125 | appendSubnets(subnets, outputs); 126 | } 127 | 128 | if (createBastionHost) { 129 | appendBastionHost(outputs); 130 | } 131 | 132 | if (exportOutputs) { 133 | appendExports(outputs); 134 | } 135 | 136 | return outputs; 137 | } 138 | 139 | module.exports = { 140 | buildOutputs, 141 | }; 142 | -------------------------------------------------------------------------------- /src/parameters.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Build an AWS Systems Manager Parmeter 3 | * 4 | * @param {String} LogicalResourceId 5 | * @param {Object} params 6 | * @return {Object} 7 | */ 8 | function buildParameter(LogicalResourceId, { Value = null } = {}) { 9 | let Type = 'String'; 10 | if (Value) { 11 | if (Array.isArray(Value)) { 12 | Type = 'StringList'; 13 | // eslint-disable-next-line no-param-reassign 14 | Value = { 15 | 'Fn::Join': [',', Value], 16 | }; 17 | } 18 | } else { 19 | // eslint-disable-next-line no-param-reassign 20 | Value = { Ref: LogicalResourceId }; 21 | } 22 | 23 | const cfName = `Parameter${LogicalResourceId}`; 24 | return { 25 | [cfName]: { 26 | Type: 'AWS::SSM::Parameter', 27 | Properties: { 28 | Name: { 29 | 'Fn::Sub': `/SLS/\${AWS::StackName}/${LogicalResourceId}`, 30 | }, 31 | Tier: 'Standard', 32 | Type, 33 | Value, 34 | }, 35 | }, 36 | }; 37 | } 38 | 39 | module.exports = { 40 | buildParameter, 41 | }; 42 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | const { PUBLIC_SUBNET } = require('./constants'); 2 | 3 | /** 4 | * Build a RouteTable in a given AZ 5 | * 6 | * @param {String} name 7 | * @param {Number} position 8 | * @return {Object} 9 | */ 10 | function buildRouteTable(name, position) { 11 | const cfName = `${name}RouteTable${position}`; 12 | 13 | const Tags = [ 14 | { 15 | Key: 'Name', 16 | Value: { 17 | 'Fn::Sub': `\${AWS::StackName}-${name.toLowerCase()}-\${${name}Subnet${position}.AvailabilityZone}`, 18 | }, 19 | }, 20 | ]; 21 | 22 | if (name === PUBLIC_SUBNET) { 23 | Tags.push({ Key: 'Network', Value: 'Public' }); 24 | } else { 25 | Tags.push({ Key: 'Network', Value: 'Private' }); 26 | } 27 | 28 | return { 29 | [cfName]: { 30 | Type: 'AWS::EC2::RouteTable', 31 | Properties: { 32 | VpcId: { 33 | Ref: 'VPC', 34 | }, 35 | Tags, 36 | }, 37 | }, 38 | }; 39 | } 40 | 41 | /** 42 | * Build a RouteTableAssociation 43 | * 44 | * @param {String} name 45 | * @param {Number} position 46 | * @return {Object} 47 | */ 48 | function buildRouteTableAssociation(name, position) { 49 | const cfName = `${name}RouteTableAssociation${position}`; 50 | return { 51 | [cfName]: { 52 | Type: 'AWS::EC2::SubnetRouteTableAssociation', 53 | Properties: { 54 | RouteTableId: { 55 | Ref: `${name}RouteTable${position}`, 56 | }, 57 | SubnetId: { 58 | Ref: `${name}Subnet${position}`, 59 | }, 60 | }, 61 | }, 62 | }; 63 | } 64 | 65 | /** 66 | * Build a Route for a NatGateway or InternetGateway 67 | * 68 | * @param {String} name 69 | * @param {Number} position 70 | * @param {Object} params 71 | * @return {Object} 72 | */ 73 | function buildRoute( 74 | name, 75 | position, 76 | { NatGatewayId = null, GatewayId = null, InstanceId = null } = {}, 77 | ) { 78 | const route = { 79 | Type: 'AWS::EC2::Route', 80 | Properties: { 81 | DestinationCidrBlock: '0.0.0.0/0', 82 | RouteTableId: { 83 | Ref: `${name}RouteTable${position}`, 84 | }, 85 | }, 86 | }; 87 | 88 | // fixes "route table rtb-x and network gateway igw-x belong to different networks" 89 | // see https://stackoverflow.com/questions/48865762 90 | if (name === PUBLIC_SUBNET) { 91 | route.DependsOn = ['InternetGatewayAttachment']; 92 | } 93 | if (NatGatewayId) { 94 | route.Properties.NatGatewayId = { 95 | Ref: NatGatewayId, 96 | }; 97 | } else if (GatewayId) { 98 | route.Properties.GatewayId = { 99 | Ref: GatewayId, 100 | }; 101 | } else if (InstanceId) { 102 | route.Properties.InstanceId = { 103 | Ref: InstanceId, 104 | }; 105 | } else { 106 | throw new Error( 107 | 'Unable to create route: either NatGatewayId, GatewayId or InstanceId must be provided', 108 | ); 109 | } 110 | 111 | const cfName = `${name}Route${position}`; 112 | return { 113 | [cfName]: route, 114 | }; 115 | } 116 | 117 | module.exports = { 118 | buildRoute, 119 | buildRouteTable, 120 | buildRouteTableAssociation, 121 | }; 122 | -------------------------------------------------------------------------------- /src/subnet_groups.js: -------------------------------------------------------------------------------- 1 | const { DB_SUBNET } = require('./constants'); 2 | 3 | /** 4 | * Build an RDSubnetGroup for a given number of zones 5 | * 6 | * @param {Number} numZones Number of availability zones 7 | * @return {Object} 8 | */ 9 | function buildRDSSubnetGroup(numZones = 0) { 10 | if (numZones < 1) { 11 | return {}; 12 | } 13 | 14 | const subnetIds = []; 15 | for (let i = 1; i <= numZones; i += 1) { 16 | subnetIds.push({ Ref: `${DB_SUBNET}Subnet${i}` }); 17 | } 18 | 19 | return { 20 | RDSSubnetGroup: { 21 | Type: 'AWS::RDS::DBSubnetGroup', 22 | Properties: { 23 | DBSubnetGroupName: { 24 | Ref: 'AWS::StackName', 25 | }, 26 | DBSubnetGroupDescription: { 27 | Ref: 'AWS::StackName', 28 | }, 29 | SubnetIds: subnetIds, 30 | }, 31 | }, 32 | }; 33 | } 34 | 35 | /** 36 | * Build an ElastiCacheSubnetGroup for a given number of zones 37 | * 38 | * @param {Number} numZones Number of availability zones 39 | * @return {Object} 40 | */ 41 | function buildElastiCacheSubnetGroup(numZones = 0) { 42 | if (numZones < 1) { 43 | return {}; 44 | } 45 | 46 | const subnetIds = []; 47 | for (let i = 1; i <= numZones; i += 1) { 48 | subnetIds.push({ Ref: `${DB_SUBNET}Subnet${i}` }); 49 | } 50 | 51 | return { 52 | ElastiCacheSubnetGroup: { 53 | Type: 'AWS::ElastiCache::SubnetGroup', 54 | Properties: { 55 | CacheSubnetGroupName: { 56 | Ref: 'AWS::StackName', 57 | }, 58 | Description: { 59 | Ref: 'AWS::StackName', 60 | }, 61 | SubnetIds: subnetIds, 62 | }, 63 | }, 64 | }; 65 | } 66 | 67 | /** 68 | * Build an RedshiftSubnetGroup for a given number of zones 69 | * 70 | * @param {Number} numZones Number of availability zones 71 | * @return {Object} 72 | */ 73 | function buildRedshiftSubnetGroup(numZones = 0) { 74 | if (numZones < 1) { 75 | return {}; 76 | } 77 | 78 | const subnetIds = []; 79 | for (let i = 1; i <= numZones; i += 1) { 80 | subnetIds.push({ Ref: `${DB_SUBNET}Subnet${i}` }); 81 | } 82 | 83 | return { 84 | RedshiftSubnetGroup: { 85 | Type: 'AWS::Redshift::ClusterSubnetGroup', 86 | Properties: { 87 | Description: { 88 | Ref: 'AWS::StackName', 89 | }, 90 | SubnetIds: subnetIds, 91 | }, 92 | }, 93 | }; 94 | } 95 | 96 | /** 97 | * Build an DAXSubnetGroup for a given number of zones 98 | * 99 | * @param {Number} numZones Number of availability zones 100 | * @return {Object} 101 | */ 102 | function buildDAXSubnetGroup(numZones = 0) { 103 | if (numZones < 1) { 104 | return {}; 105 | } 106 | 107 | const subnetIds = []; 108 | for (let i = 1; i <= numZones; i += 1) { 109 | subnetIds.push({ Ref: `${DB_SUBNET}Subnet${i}` }); 110 | } 111 | 112 | return { 113 | DAXSubnetGroup: { 114 | Type: 'AWS::DAX::SubnetGroup', 115 | Properties: { 116 | SubnetGroupName: { 117 | Ref: 'AWS::StackName', 118 | }, 119 | Description: { 120 | Ref: 'AWS::StackName', 121 | }, 122 | SubnetIds: subnetIds, 123 | }, 124 | }, 125 | }; 126 | } 127 | 128 | /** 129 | * Build the database subnet groups 130 | * 131 | * @param {Number} numZones Number of availability zones 132 | * @param {Array} subnetGroups options of subnet groups 133 | * @return {Object} 134 | */ 135 | function buildSubnetGroups(numZones = 0, subnetGroups = []) { 136 | if (numZones < 2) { 137 | return {}; 138 | } 139 | if (!Array.isArray(subnetGroups) || subnetGroups.length < 1) { 140 | return {}; 141 | } 142 | 143 | const groupMapping = { 144 | rds: buildRDSSubnetGroup, 145 | redshift: buildRedshiftSubnetGroup, 146 | elasticache: buildElastiCacheSubnetGroup, 147 | dax: buildDAXSubnetGroup, 148 | }; 149 | 150 | return subnetGroups.reduce((acc, cur) => { 151 | return Object.assign(acc, groupMapping[cur.toLowerCase()](numZones)); 152 | }, {}); 153 | } 154 | 155 | module.exports = { 156 | buildDAXSubnetGroup, 157 | buildElastiCacheSubnetGroup, 158 | buildRDSSubnetGroup, 159 | buildRedshiftSubnetGroup, 160 | buildSubnetGroups, 161 | }; 162 | -------------------------------------------------------------------------------- /src/subnets.js: -------------------------------------------------------------------------------- 1 | const CIDR = require('cidr-split'); 2 | 3 | const { APP_SUBNET, PUBLIC_SUBNET, DB_SUBNET } = require('./constants'); 4 | 5 | /** 6 | * Split a /16 CIDR block into /20 CIDR blocks. 7 | * 8 | * @param {String} cidrBlock VPC CIDR block 9 | * @return {Array} 10 | */ 11 | function splitVpc(cidrBlock) { 12 | return CIDR.fromString(cidrBlock) 13 | .split() 14 | .map((cidr) => cidr.split()) 15 | .reduce((all, halves) => all.concat(...halves)) 16 | .map((cidr) => cidr.split()) 17 | .reduce((all, halves) => all.concat(...halves)) 18 | .map((cidr) => cidr.split()) 19 | .reduce((all, halves) => all.concat(...halves)); 20 | } 21 | 22 | /** 23 | * Splits the /16 VPC CIDR block into /20 subnets per AZ: 24 | * 25 | * Application subnet = /21 26 | * Public subnet = /22 27 | * Database subnet = /22 28 | * 29 | * @param {String} cidrBlock VPC CIDR block 30 | * @param {Array} zones Array of availability zones 31 | * @return {Map} 32 | */ 33 | function splitSubnets(cidrBlock, zones = []) { 34 | const mapping = new Map(); 35 | 36 | if (!cidrBlock || !Array.isArray(zones) || zones.length < 1) { 37 | return mapping; 38 | } 39 | 40 | const azCidrBlocks = splitVpc(cidrBlock); // VPC subnet is a /16 41 | 42 | const publicSubnets = []; 43 | const appSubnets = []; 44 | const dbSubnets = []; 45 | 46 | zones.forEach((zone, index) => { 47 | const azCidrBlock = azCidrBlocks[index]; // AZ subnet is a /20 48 | const subnets = []; 49 | 50 | const azSubnets = CIDR.fromString(azCidrBlock) 51 | .split() 52 | .map((cidr) => cidr.toString()); 53 | subnets.push(azSubnets[0]); // Application subnet is a /21 54 | 55 | const smallerSubnets = CIDR.fromString(azSubnets[1]) 56 | .split() 57 | .map((cidr) => cidr.toString()); 58 | subnets.push(...smallerSubnets); // Public and DB subnets are both /22 59 | 60 | const parts = [ 61 | [APP_SUBNET, subnets[0]], 62 | [PUBLIC_SUBNET, subnets[1]], 63 | [DB_SUBNET, subnets[2]], 64 | ]; 65 | 66 | appSubnets.push(subnets[0]); 67 | publicSubnets.push(subnets[1]); 68 | dbSubnets.push(subnets[2]); 69 | 70 | mapping.set(zone, new Map(parts)); 71 | }); 72 | 73 | mapping.set(PUBLIC_SUBNET, publicSubnets); 74 | mapping.set(APP_SUBNET, appSubnets); 75 | mapping.set(DB_SUBNET, dbSubnets); 76 | 77 | return mapping; 78 | } 79 | 80 | /** 81 | * Create a subnet 82 | * 83 | * @param {String} name Name of subnet 84 | * @param {Number} position Subnet position 85 | * @param {String} zone Availability zone 86 | * @param {String} cidrBlock Subnet CIDR block 87 | * @return {Object} 88 | */ 89 | function buildSubnet(name, position, zone, cidrBlock) { 90 | const cfName = `${name}Subnet${position}`; 91 | 92 | const Tags = [ 93 | { 94 | Key: 'Name', 95 | Value: { 96 | 'Fn::Sub': `\${AWS::StackName}-${name.toLowerCase()}-${zone}`, 97 | }, 98 | }, 99 | ]; 100 | 101 | if (name === PUBLIC_SUBNET) { 102 | Tags.push({ Key: 'Network', Value: 'Public' }); 103 | } else { 104 | Tags.push({ Key: 'Network', Value: 'Private' }); 105 | } 106 | 107 | return { 108 | [cfName]: { 109 | Type: 'AWS::EC2::Subnet', 110 | Properties: { 111 | AvailabilityZone: zone, 112 | CidrBlock: cidrBlock, 113 | Tags, 114 | VpcId: { 115 | Ref: 'VPC', 116 | }, 117 | }, 118 | }, 119 | }; 120 | } 121 | 122 | module.exports = { 123 | splitVpc, 124 | splitSubnets, 125 | buildSubnet, 126 | }; 127 | -------------------------------------------------------------------------------- /src/vpc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Build a VPC 3 | * 4 | * @param {String} CidrBlock 5 | * @param {Object} params 6 | * @return {Object} 7 | */ 8 | function buildVpc(CidrBlock = '10.0.0.0/16') { 9 | return { 10 | VPC: { 11 | Type: 'AWS::EC2::VPC', 12 | Properties: { 13 | CidrBlock, 14 | EnableDnsSupport: true, 15 | EnableDnsHostnames: true, 16 | InstanceTenancy: 'default', 17 | Tags: [ 18 | { 19 | Key: 'Name', 20 | Value: { 21 | Ref: 'AWS::StackName', 22 | }, 23 | }, 24 | ], 25 | }, 26 | }, 27 | }; 28 | } 29 | 30 | /** 31 | * Build an InternetGateway and InternetGatewayAttachment 32 | * 33 | * @return {Object} 34 | */ 35 | function buildInternetGateway() { 36 | return { 37 | InternetGateway: { 38 | Type: 'AWS::EC2::InternetGateway', 39 | Properties: { 40 | Tags: [ 41 | { 42 | Key: 'Name', 43 | Value: { 44 | // eslint-disable-next-line no-template-curly-in-string 45 | 'Fn::Sub': '${AWS::StackName}-igw', 46 | }, 47 | }, 48 | { 49 | Key: 'Network', 50 | Value: 'Public', 51 | }, 52 | ], 53 | }, 54 | }, 55 | InternetGatewayAttachment: { 56 | Type: 'AWS::EC2::VPCGatewayAttachment', 57 | Properties: { 58 | InternetGatewayId: { 59 | Ref: 'InternetGateway', 60 | }, 61 | VpcId: { 62 | Ref: 'VPC', 63 | }, 64 | }, 65 | }, 66 | }; 67 | } 68 | 69 | /** 70 | * Build a SecurityGroup to be used by applications 71 | * 72 | * @param {Object} prefixLists AWS-owned managed prefix lists in the region 73 | * @return {Object} 74 | */ 75 | function buildAppSecurityGroup(prefixLists = null) { 76 | const egress = [ 77 | { 78 | Description: 'permit HTTPS outbound', 79 | IpProtocol: 'tcp', 80 | FromPort: 443, 81 | ToPort: 443, 82 | CidrIp: '0.0.0.0/0', 83 | }, 84 | ]; 85 | if (prefixLists) { 86 | egress.push({ 87 | DestinationPrefixListId: prefixLists.s3, 88 | Description: 'permit HTTPS to S3', 89 | IpProtocol: 'tcp', 90 | FromPort: 443, 91 | ToPort: 443, 92 | }); 93 | egress.push({ 94 | DestinationPrefixListId: prefixLists.s3, 95 | Description: 'permit HTTP to S3', 96 | IpProtocol: 'tcp', 97 | FromPort: 80, 98 | ToPort: 80, 99 | }); 100 | egress.push({ 101 | DestinationPrefixListId: prefixLists.dynamodb, 102 | Description: 'permit HTTPS to DynamoDB', 103 | IpProtocol: 'tcp', 104 | FromPort: 443, 105 | ToPort: 443, 106 | }); 107 | } 108 | 109 | return { 110 | DefaultSecurityGroupEgress: { 111 | Type: 'AWS::EC2::SecurityGroupEgress', 112 | Properties: { 113 | IpProtocol: '-1', 114 | DestinationSecurityGroupId: { 115 | 'Fn::GetAtt': ['VPC', 'DefaultSecurityGroup'], 116 | }, 117 | GroupId: { 118 | 'Fn::GetAtt': ['VPC', 'DefaultSecurityGroup'], 119 | }, 120 | }, 121 | }, 122 | AppSecurityGroup: { 123 | Type: 'AWS::EC2::SecurityGroup', 124 | Properties: { 125 | GroupDescription: 'Application Security Group', 126 | SecurityGroupEgress: egress, 127 | SecurityGroupIngress: [ 128 | { 129 | Description: 'permit HTTPS inbound', 130 | IpProtocol: 'tcp', 131 | FromPort: 443, 132 | ToPort: 443, 133 | CidrIp: '0.0.0.0/0', 134 | }, 135 | ], 136 | VpcId: { 137 | Ref: 'VPC', 138 | }, 139 | Tags: [ 140 | { 141 | Key: 'Name', 142 | Value: { 143 | // eslint-disable-next-line no-template-curly-in-string 144 | 'Fn::Sub': '${AWS::StackName}-sg', 145 | }, 146 | }, 147 | ], 148 | }, 149 | }, 150 | }; 151 | } 152 | 153 | /** 154 | * Build a DHCP Option Set 155 | * 156 | * @param {String} region 157 | * @return {Object} 158 | */ 159 | function buildDHCPOptions(region) { 160 | let domainName; 161 | if (region === 'us-east-1') { 162 | domainName = 'ec2.internal'; 163 | } else { 164 | domainName = { 165 | // eslint-disable-next-line no-template-curly-in-string 166 | 'Fn::Sub': '${AWS::Region}.compute.internal', 167 | }; 168 | } 169 | 170 | return { 171 | DHCPOptions: { 172 | Type: 'AWS::EC2::DHCPOptions', 173 | Properties: { 174 | DomainName: domainName, 175 | DomainNameServers: ['AmazonProvidedDNS'], 176 | Tags: [ 177 | { 178 | Key: 'Name', 179 | Value: { 180 | // eslint-disable-next-line no-template-curly-in-string 181 | 'Fn::Sub': '${AWS::StackName}-DHCPOptionsSet', 182 | }, 183 | }, 184 | ], 185 | }, 186 | }, 187 | VPCDHCPOptionsAssociation: { 188 | Type: 'AWS::EC2::VPCDHCPOptionsAssociation', 189 | Properties: { 190 | VpcId: { 191 | Ref: 'VPC', 192 | }, 193 | DhcpOptionsId: { 194 | Ref: 'DHCPOptions', 195 | }, 196 | }, 197 | }, 198 | }; 199 | } 200 | 201 | module.exports = { 202 | buildVpc, 203 | buildInternetGateway, 204 | buildAppSecurityGroup, 205 | buildDHCPOptions, 206 | }; 207 | -------------------------------------------------------------------------------- /src/vpce.js: -------------------------------------------------------------------------------- 1 | const { APP_SUBNET } = require('./constants'); 2 | 3 | /** 4 | * Build a VPCEndpoint 5 | * 6 | * @param {String} service 7 | * @param {Object} params 8 | * @return {Object} 9 | */ 10 | function buildVPCEndpoint(service, { routeTableIds = [], subnetIds = [] } = {}) { 11 | const endpoint = { 12 | Type: 'AWS::EC2::VPCEndpoint', 13 | Properties: { 14 | ServiceName: { 15 | 'Fn::Sub': `com.amazonaws.\${AWS::Region}.${service}`, 16 | }, 17 | VpcId: { 18 | Ref: 'VPC', 19 | }, 20 | }, 21 | }; 22 | 23 | // @see https://docs.aws.amazon.com/vpc/latest/userguide/vpc-endpoints.html 24 | if (service === 's3' || service === 'dynamodb') { 25 | endpoint.Properties.VpcEndpointType = 'Gateway'; 26 | endpoint.Properties.RouteTableIds = routeTableIds; 27 | endpoint.Properties.PolicyDocument = { 28 | Statement: [ 29 | { 30 | Effect: 'Allow', 31 | Principal: '*', 32 | Resource: '*', 33 | }, 34 | ], 35 | }; 36 | if (service === 's3') { 37 | endpoint.Properties.PolicyDocument.Statement[0].Action = 's3:*'; 38 | } else if (service === 'dynamodb') { 39 | endpoint.Properties.PolicyDocument.Statement[0].Action = 'dynamodb:*'; 40 | } 41 | } else { 42 | endpoint.Properties.VpcEndpointType = 'Interface'; 43 | endpoint.Properties.SubnetIds = subnetIds; 44 | endpoint.Properties.PrivateDnsEnabled = true; 45 | endpoint.Properties.SecurityGroupIds = [ 46 | { 47 | Ref: 'AppSecurityGroup', 48 | }, 49 | ]; 50 | } 51 | 52 | const parts = service.split(/[-_.]/g); 53 | parts.push('VPCEndpoint'); 54 | const cfName = parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(''); 55 | 56 | return { 57 | [cfName]: endpoint, 58 | }; 59 | } 60 | 61 | /** 62 | * Build VPC endpoints for a given number of services and zones 63 | * 64 | * @param {Array} services Array of VPC endpoint services 65 | * @param {Number} numZones Number of availability zones 66 | * @return {Object} 67 | */ 68 | function buildEndpointServices(services = [], numZones = 0) { 69 | if (!Array.isArray(services) || services.length < 1) { 70 | return {}; 71 | } 72 | if (numZones < 1) { 73 | return {}; 74 | } 75 | 76 | const subnetIds = []; 77 | const routeTableIds = []; 78 | for (let i = 1; i <= numZones; i += 1) { 79 | subnetIds.push({ Ref: `${APP_SUBNET}Subnet${i}` }); 80 | routeTableIds.push({ Ref: `${APP_SUBNET}RouteTable${i}` }); 81 | } 82 | 83 | const resources = {}; 84 | services.forEach((service) => { 85 | Object.assign(resources, buildVPCEndpoint(service, { routeTableIds, subnetIds })); 86 | }); 87 | 88 | return resources; 89 | } 90 | 91 | module.exports = { 92 | buildEndpointServices, 93 | buildVPCEndpoint, 94 | }; 95 | --------------------------------------------------------------------------------