diff --git a/src/cloud-formation/README.md b/src/cloud-formation/README.md index 9388a9f75..477d47b8e 100644 --- a/src/cloud-formation/README.md +++ b/src/cloud-formation/README.md @@ -8,8 +8,8 @@ cspell:word pullthroughcache # Indexer on CloudFormation The indexer can be automatically deployed on [AWS CloudFormation] with [GitSync] -using the [template file] at `indexer.cfn.yaml` and a development-specific -[stack deployment file] at `deploy-*.yaml`. Once a [stack] is configured +using the [template file] at `indexer.cfn.yaml` and an environment-specific +[stack deployment file] at `deploy-indexer-*.yaml`. Once a [stack] is configured accordingly, `git` updates will result in automatic updates. The indexer provides a public REST endpoint and a public WebSocket endpoint @@ -25,7 +25,19 @@ under a root domain you provide, for an environment name of your choosing: `indexer.cfn.yaml` contains assorted [parameters] of the form `Deploy*` that can be used to [conditionally][conditions] provision and de-provision [resources]. For a concise list of such parameters, see a [stack deployment file] at -`deploy-*.yaml`. See the template [rules] section for associated dependencies. +`deploy-indexer-*.yaml`. See the template [rules] section for associated +dependencies. + +Note that the `Environment` parameter should be unique across stacks, as it is +used for endpoint subdomain assignment. + +## VPC stack abstraction + +The indexer depends on an abstracted [VPC stack](#vpc-stack) with common +resources that can be shared across indexer deployments. Once you've deployed +a single VPC stack based on the [template file] at `vpc.cfn.yaml` and +the [stack deployment file] at `deploy-vpc.yaml`, you can re-use it across +indexer deployments. ## Setup @@ -190,18 +202,30 @@ For a concise list of such parameters, see a [stack deployment file] at -1. Create a [stack deployment file] (see `deploy-*.yml`) with appropriate - [template parameters](#template-parameters). +1. If you haven't already deployed a VPC stack, create a [stack deployment file] + based on `deploy-vpc.yml`. + +1. Create a [stack deployment file] (see `deploy-indexer-*.yml`) with + appropriate [template parameters](#template-parameters) for the indexer (in + particular the `Environment` parameter should be + [unique across stacks](#template-parameters)). -1. [Create the stack with GitSync], then monitor [GitSync events][gitsync event] - in the [GitSync status dashboard]. +1. If you haven't already deployed a VPC stack, [create the stack with GitSync], + then monitor [GitSync events][gitsync event] in the + [GitSync status dashboard] or with [`rain`]. After it has deployed, repeat + for the indexer. + + > Use concise stack names like `emoji-vpc` and `emoji-production` since + > excessively long names may result in resource creation failure due to + > character count limits, for example the + > [load balancer target group name 32 character limit]. ## Querying endpoints ### Public endpoints -Once you have [deployed a stack](#setup), query the public endpoint for your -deployment environment: +Once you have [deployed an indexer stack](#setup), query the public endpoint for +your deployment environment: 1. Set your stack name: @@ -279,12 +303,12 @@ deployment environment: ### Bastion host connections -Before you try connecting to the bastion host, verify that the -`DeployBastionHost` [condition][conditions] evaluates to `true`. Note too that -if you have been provisioning and de-provisioning other resources, you might -want to de-provision then provision the bastion host before running the below -commands, in order to refresh the bastion host [user data] that stores the URLs -of other resources in the stack. +Before you try connecting to the [bastion host][connect to a bastion host], +verify that the `DeployBastionHost` [condition][conditions] evaluates to `true`. +Note too that if you have been provisioning and de-provisioning other resources, +you might want to de-provision then provision the bastion host before running +the below commands, in order to refresh the bastion host [user data] that stores +the URLs of other resources in the stack. 1. Install the [EC2 Instance Connect CLI]: @@ -367,6 +391,45 @@ of other resources in the stack. ## Design notes +### VPC stack + +The indexer uses +[NAT gateways to provide internet access for private instances], with +[a NAT gateway in each Availability Zone][az-specific nat gateways] to ensure +high resilience. To avoid [VPC quota] exhaustion for multiple indexer +deployments, networking resources associated with the indexer are thus +abstracted into a [VPC]-specific [stack template][template file] at +`vpc.cfn.yaml`, whose resources can be re-used across multiple indexer +deployments via [cross-stack references]. + +Similarly, the VPC stack contains a single [EC2 Instance Connect Endpoint] that +can be used to [connect to a bastion host] for an indexer deployment inside the +VPC. This approach avoids [EC2 Instance Connect Endpoint quota] exhaustion. + +Though not strictly necessary to prevent quota exhaustion, the following +common network resources are additionally abstracted: + +1. A [DB subnet group]. +1. A [private DNS namespace]. + +The VPC stack contains a [private and public subnet] for each +[Availability Zone] (AZ), with each public subnet sharing a common +[public internet route] inside a [custom route table]. Per +[AZ-specific NAT gateway best practices][az-specific nat gateways], each private +subnet has its own custom route table with a [NAT gateway route]. No additional +routing is required to enable communication across subnets, because as per the +[AWS routes docs]: + +> Every route table contains a local route for communication within the VPC. +> This route is added by default to all route tables. If your VPC has more than +> one IPv4 CIDR block, your route tables contain a local route for each IPv4 +> CIDR block. + +The VPC uses an [AWS-recommended VPC CIDR block] of `10.0.0.0/16` corresponding +to 65,536 IP addresses, with each non-overlapping [subnet CIDR block] using a +`/19` netmask for up to 8,192 IP addresses per subnet (excluding the +[5 default reserved addresses per subnet]). + ### Database The indexer database uses [Aurora PostgreSQL] on a @@ -378,10 +441,6 @@ The indexer database uses [Aurora PostgreSQL] on a [high availability][high availability for aurora] with [fault tolerant replica promotion] and [autoscaling][aurora autoscaling]. -### NAT gateway redundancy - -The indexer uses [a NAT gateway in each availability zone] for high resilience. - ### Permissions The `ContainerRole` [ECS task execution IAM role] provides @@ -420,7 +479,7 @@ instance is live and at idle. This design ensures that at least one server container is always live for both REST and WebSocket endpoints. -[a nat gateway in each availability zone]: https://docs.aws.amazon.com/vpc/latest/userguide/nat-gateway-basics.html +[5 default reserved addresses per subnet]: https://docs.aws.amazon.com/vpc/latest/userguide/subnet-sizing.html [amazonec2containerserviceautoscalerole]: https://docs.aws.amazon.com/autoscaling/application/userguide/security-iam-awsmanpol.html#ecs-policy [application autoscaling iam access]: https://docs.aws.amazon.com/autoscaling/application/userguide/security_iam_service-with-iam.html [aptos labs grpc endpoint]: https://aptos.dev/en/build/indexer/txn-stream/aptos-hosted-txn-stream#endpoints @@ -430,14 +489,23 @@ REST and WebSocket endpoints. [aurora clusters]: https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.Overview.html [aurora postgresql]: https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Aurora.AuroraPostgreSQL.html [auto-selection of aurora az]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbinstance.html#cfn-rds-dbinstance-availabilityzone +[availability zone]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-availability-zones [aws cloudformation]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/Welcome.html +[aws routes docs]: https://docs.aws.amazon.com/vpc/latest/userguide/subnet-route-tables.html#route-table-routes +[aws-recommended vpc cidr block]: https://docs.aws.amazon.com/vpc/latest/userguide/vpc-cidr-blocks.html +[az-specific nat gateways]: https://docs.aws.amazon.com/vpc/latest/userguide/nat-gateway-basics.html [cloudformation service role]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-servicerole.html [conditions]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/conditions-section-structure.html +[connect to a bastion host]: https://docs.aws.amazon.com/prescriptive-guidance/latest/patterns/access-a-bastion-host-by-using-session-manager-and-amazon-ec2-instance-connect.html [container autoscaling]: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/service-auto-scaling.html [container logging permissions]: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/using_awslogs.html#ec2-considerations [create the stack with gitsync]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/git-sync-walkthrough.html +[cross-stack references]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/walkthrough-crossstackref.html +[custom route table]: https://docs.aws.amazon.com/vpc/latest/userguide/subnet-route-tables.html#custom-route-tables +[db subnet group]: https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_VPC.WorkingWithRDSInstanceinaVPC.html#USER_VPC.Subnets [ec2 instance connect cli]: https://github.com/aws/aws-ec2-instance-connect-cli [ec2 instance connect endpoint]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/connect-using-eice.html +[ec2 instance connect endpoint quota]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/eice-quotas.html [ecr pull through cache permissions]: https://docs.aws.amazon.com/AmazonECR/latest/userguide/pull-through-cache-iam.html [ecr pull through cache rule creation docs]: https://docs.aws.amazon.com/AmazonECR/latest/userguide/pull-through-cache-creating-rule.html [ecs task execution iam role]: https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_execution_IAM_role.html @@ -452,12 +520,18 @@ REST and WebSocket endpoints. [iam roles]: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html [inline policy]: https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_managed-vs-inline.html#inline-policies [least-privilege permissions]: https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege +[load balancer target group name 32 character limit]: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/create-target-group.html#:~:text=For%20Target%20group%20name%2C%20type,the%20default%20values%20as%20needed. [make route 53 the dns service for a domain you own]: https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/migrate-dns-domain-in-use.html [managed rules]: https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups-list.html [multi-az aurora serverless v2 cluster]: https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-serverless-v2.how-it-works.html#aurora-serverless.ha +[nat gateway route]: https://docs.aws.amazon.com/vpc/latest/userguide/route-table-options.html#route-tables-nat +[nat gateways to provide internet access for private instances]: https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html [parameter naming constraints]: https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-su-create.html [parameters]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html [poweruseraccess]: https://docs.aws.amazon.com/aws-managed-policy/latest/reference/PowerUserAccess.html +[private and public subnet]: https://docs.aws.amazon.com/vpc/latest/userguide/configure-subnets.html#subnet-types +[private dns namespace]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-servicediscovery-privatednsnamespace.html +[public internet route]: https://docs.aws.amazon.com/vpc/latest/userguide/route-table-options.html#route-tables-internet-gateway [resources]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resources-section-structure.html [role passing]: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_passrole.html [rule actions]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-wafv2-webacl-ruleaction.html @@ -467,6 +541,7 @@ REST and WebSocket endpoints. [stack deployment file]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/git-sync-concepts-terms.html#git-sync-concepts-terms-depoyment-file [step scale cloudwatch alarm]: https://docs.aws.amazon.com/autoscaling/application/userguide/step-scaling-policy-overview.html#step-scaling-how-it-works [step scaling]: https://docs.aws.amazon.com/autoscaling/application/userguide/application-auto-scaling-step-scaling-policies.html +[subnet cidr block]: https://docs.aws.amazon.com/vpc/latest/userguide/subnet-sizing.html [systems manager parameters]: https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html [target tracking]: https://docs.aws.amazon.com/autoscaling/application/userguide/application-auto-scaling-target-tracking.html [template file]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/gettingstarted.templatebasics.html @@ -474,6 +549,9 @@ REST and WebSocket endpoints. [the upstream repository credentials docs]: https://docs.aws.amazon.com/AmazonECR/latest/userguide/pull-through-cache-creating-secret.html [transaction stream service endpoint]: https://aptos.dev/en/build/indexer/txn-stream [user data]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html +[vpc]: https://docs.aws.amazon.com/vpc/latest/userguide/what-is-amazon-vpc.html +[vpc quota]: https://docs.aws.amazon.com/vpc/latest/userguide/amazon-vpc-limits.html [web acl traffic overview dashboards]: https://docs.aws.amazon.com/waf/latest/developerguide/web-acl-dashboards.html [web application firewall]: https://docs.aws.amazon.com/waf/latest/developerguide/waf-chapter.html [`ecr::getauthorizationtoken`]: https://docs.aws.amazon.com/AmazonECR/latest/APIReference/API_GetAuthorizationToken.html +[`rain`]: https://github.com/aws-cloudformation/rain diff --git a/src/cloud-formation/deploy-main.yaml b/src/cloud-formation/deploy-indexer-main.yaml similarity index 93% rename from src/cloud-formation/deploy-main.yaml rename to src/cloud-formation/deploy-indexer-main.yaml index 9da665590..6ee698df8 100644 --- a/src/cloud-formation/deploy-main.yaml +++ b/src/cloud-formation/deploy-indexer-main.yaml @@ -13,9 +13,7 @@ parameters: DeployProcessor: 'true' DeployRestApi: 'true' DeployRestApiDnsRecord: 'true' - DeployRouteTables: 'true' DeployStack: 'true' - DeployVpc: 'true' DeployWaf: 'false' EnableWafRulesGeneral: 'false' EnableWafRulesRestApi: 'false' @@ -23,6 +21,7 @@ parameters: Environment: 'main' Network: 'testnet' ProcessorImageVersion: '0.8.1' + VpcStackName: 'emoji-vpc' tags: null template-file-path: 'src/cloud-formation/indexer.cfn.yaml' ... diff --git a/src/cloud-formation/deploy-vpc.yaml b/src/cloud-formation/deploy-vpc.yaml new file mode 100644 index 000000000..d0a6be70b --- /dev/null +++ b/src/cloud-formation/deploy-vpc.yaml @@ -0,0 +1,5 @@ +--- +parameters: null +tags: null +template-file-path: 'src/cloud-formation/vpc.cfn.yaml' +... diff --git a/src/cloud-formation/indexer.cfn.yaml b/src/cloud-formation/indexer.cfn.yaml index d314fe479..495bbc6f0 100644 --- a/src/cloud-formation/indexer.cfn.yaml +++ b/src/cloud-formation/indexer.cfn.yaml @@ -44,12 +44,6 @@ Conditions: DeployRestApiDnsRecord: !Equals - !Ref 'DeployRestApiDnsRecord' - 'true' - DeployRouteTables: !Equals - - !Ref 'DeployRouteTables' - - 'true' - DeployVpc: !Equals - - !Ref 'DeployVpc' - - 'true' DeployWaf: !Equals - !Ref 'DeployWaf' - 'true' @@ -84,31 +78,10 @@ Mappings: PostgrestHealthCheckPort: 3001 PostgrestPort: 3000 ProcessorWebsocketPort: 3008 - VpcCidrBlock: '10.0.0.0/16' Websocat: Build: 'websocat.aarch64-unknown-linux-musl' ReleaseUrlBase: 'https://github.com/vi/websocat/releases/download' Version: 'v1.13.0' - PrivateSubnets: - A: - AvailabilityZone: 0 - CidrBlock: '10.0.1.0/24' - B: - AvailabilityZone: 1 - CidrBlock: '10.0.2.0/24' - C: - AvailabilityZone: 2 - CidrBlock: '10.0.3.0/24' - PublicSubnets: - A: - AvailabilityZone: 0 - CidrBlock: '10.0.4.0/24' - B: - AvailabilityZone: 1 - CidrBlock: '10.0.5.0/24' - C: - AvailabilityZone: 2 - CidrBlock: '10.0.6.0/24' Outputs: BastionHostId: Condition: 'DeployBastionHost' @@ -202,21 +175,11 @@ Parameters: - 'false' - 'true' Type: 'String' - DeployRouteTables: - AllowedValues: - - 'false' - - 'true' - Type: 'String' DeployStack: AllowedValues: - 'false' - 'true' Type: 'String' - DeployVpc: - AllowedValues: - - 'false' - - 'true' - Type: 'String' DeployWaf: AllowedValues: - 'false' @@ -252,6 +215,8 @@ Parameters: Type: 'Number' ProcessorImageVersion: Type: 'String' + VpcStackName: + Type: 'String' Resources: # Public application load balancer. Alb: @@ -268,9 +233,9 @@ Resources: - !Ref 'BrokerWsClientSecurityGroup' - !Ref 'AWS::NoValue' Subnets: - - !Ref 'PublicSubnetA' - - !Ref 'PublicSubnetB' - - !Ref 'PublicSubnetC' + - Fn::ImportValue: !Sub '${VpcStackName}-PublicSubnetIdA' + - Fn::ImportValue: !Sub '${VpcStackName}-PublicSubnetIdB' + - Fn::ImportValue: !Sub '${VpcStackName}-PublicSubnetIdC' Type: 'application' Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer' # DNS certificate for the broker's application load balancer. @@ -338,7 +303,8 @@ Resources: FromPort: 443 IpProtocol: 'tcp' ToPort: 443 - VpcId: !Ref 'Vpc' + VpcId: + Fn::ImportValue: !Sub '${VpcStackName}-VpcId' Type: 'AWS::EC2::SecurityGroup' # Minimal VM for monitoring internal processes. BastionHost: @@ -367,7 +333,8 @@ Resources: - 'DeployProcessor' - !Ref 'ProcessorWsClientSecurityGroup' - !Ref 'AWS::NoValue' - SubnetId: !Ref 'PrivateSubnetA' + SubnetId: + Fn::ImportValue: !Sub '${VpcStackName}-PrivateSubnetIdA' UserData: # Install PostgreSQL client & websocat, set connection strings. Fn::Base64: !Sub @@ -392,7 +359,8 @@ Resources: - - 'export BROKER_WS_URL="ws://' - !GetAtt 'BrokerServiceDiscovery.Name' - '.' - - !Ref 'AWS::StackName' + # The private DNS namespace comes from the VPC stack. + - !Ref 'VpcStackName' - ':' - !FindInMap - 'Constants' @@ -434,7 +402,8 @@ Resources: - - 'export POSTGREST_URL="http://' - !GetAtt 'PostgrestServiceDiscovery.Name' - '.' - - !Ref 'AWS::StackName' + # The private DNS namespace comes from the VPC stack. + - !Ref 'VpcStackName' - ':' - !FindInMap - 'Constants' @@ -449,7 +418,8 @@ Resources: - - 'export PROCESSOR_WS_URL="ws://' - !GetAtt 'ProcessorServiceDiscovery.Name' - '.' - - !Ref 'AWS::StackName' + # The private DNS namespace comes from the VPC stack. + - !Ref 'VpcStackName' - ':' - !FindInMap - 'Constants' @@ -470,42 +440,19 @@ Resources: - 'Websocat' - 'Version' Type: 'AWS::EC2::Instance' - # Connection endpoint allowing access to the bastion host. - BastionHostConnectionEndpoint: - Condition: 'DeployBastionHost' - Properties: - PreserveClientIp: true - SecurityGroupIds: - - !Ref 'BastionHostConnectionEndpointSecurityGroup' - SubnetId: !Ref 'PrivateSubnetA' - Type: 'AWS::EC2::InstanceConnectEndpoint' - # Security group for bastion host connection endpoint. - BastionHostConnectionEndpointSecurityGroup: - Condition: 'DeployBastionHost' - Properties: - GroupDescription: !Ref 'AWS::StackName' - SecurityGroupEgress: - - DestinationSecurityGroupId: !Ref 'BastionHostSecurityGroup' - IpProtocol: -1 - VpcId: !Ref 'Vpc' - Type: 'AWS::EC2::SecurityGroup' # Security group for the bastion host. BastionHostSecurityGroup: Condition: 'DeployBastionHost' Properties: GroupDescription: !Ref 'AWS::StackName' - VpcId: !Ref 'Vpc' + SecurityGroupIngress: + - IpProtocol: -1 + SourceSecurityGroupId: + Fn::ImportValue: !Sub + '${VpcStackName}-InstanceConnectEndpointSecurityGroupId' + VpcId: + Fn::ImportValue: !Sub '${VpcStackName}-VpcId' Type: 'AWS::EC2::SecurityGroup' - # Ingress policy for the bastion host security group, separated to eliminate - # circular dependencies with the security group for the connection endpoint. - BastionHostSecurityGroupIngress: - Condition: 'DeployBastionHost' - Properties: - GroupId: !Ref 'BastionHostSecurityGroup' - IpProtocol: -1 - SourceSecurityGroupId: - !Ref 'BastionHostConnectionEndpointSecurityGroup' - Type: 'AWS::EC2::SecurityGroupIngress' # Target group for application load balancer traffic to the broker. BrokerAlbTargetGroup: Condition: 'DeployAlb' @@ -536,14 +483,16 @@ Resources: Value: 'lb_cookie' TargetType: 'ip' UnhealthyThresholdCount: 2 - VpcId: !Ref 'Vpc' + VpcId: + Fn::ImportValue: !Sub '${VpcStackName}-VpcId' Type: 'AWS::ElasticLoadBalancingV2::TargetGroup' # Security group for the broker's WebSocket server. BrokerPublisherSecurityGroup: Condition: 'DeployBroker' Properties: GroupDescription: !Ref 'AWS::StackName' - VpcId: !Ref 'Vpc' + VpcId: + Fn::ImportValue: !Sub '${VpcStackName}-VpcId' Type: 'AWS::EC2::SecurityGroup' # Ingress policy for the broker's WebSocket server security group. BrokerPublisherSecurityGroupIngress: @@ -559,14 +508,6 @@ Resources: DependsOn: # Wait until processor server is online. - 'ProcessorRunner' - # Wait until there is an outbound route to get the image from the - # pull through cache. - - 'PrivateRouteTableAssociationA' - - 'PrivateRouteTableAssociationB' - - 'PrivateRouteTableAssociationC' - - 'PrivateRouteThroughNatGatewayA' - - 'PrivateRouteThroughNatGatewayB' - - 'PrivateRouteThroughNatGatewayC' # Proxy for a conditional dependency on the application load balancer # listener, which associates the broker target group with the application # load balancer. @@ -598,9 +539,9 @@ Resources: - !Ref 'ContainerSecurityGroup' - !Ref 'ProcessorWsClientSecurityGroup' Subnets: - - !Ref 'PrivateSubnetA' - - !Ref 'PrivateSubnetB' - - !Ref 'PrivateSubnetC' + - Fn::ImportValue: !Sub '${VpcStackName}-PrivateSubnetIdA' + - Fn::ImportValue: !Sub '${VpcStackName}-PrivateSubnetIdB' + - Fn::ImportValue: !Sub '${VpcStackName}-PrivateSubnetIdC' ServiceRegistries: - RegistryArn: !GetAtt 'BrokerServiceDiscovery.Arn' TaskDefinition: !Ref 'BrokerTask' @@ -614,10 +555,9 @@ Resources: - TTL: 10 Type: 'A' RoutingPolicy: 'MULTIVALUE' - HealthCheckCustomConfig: - FailureThreshold: 1 Name: !Sub '${AWS::StackName}-broker' - NamespaceId: !Ref 'PrivateServiceDiscoveryNamespace' + NamespaceId: + Fn::ImportValue: !Sub '${VpcStackName}-PrivateDnsNamespaceId' Type: 'AWS::ServiceDiscovery::Service' # Task definition for the broker. BrokerTask: @@ -632,7 +572,8 @@ Resources: - - 'ws://' - !GetAtt 'ProcessorServiceDiscovery.Name' - '.' - - !Ref 'AWS::StackName' + # The private DNS namespace comes from the VPC stack. + - !Ref 'VpcStackName' - ':' - !FindInMap - 'Constants' @@ -657,10 +598,10 @@ Resources: - 'Constants' - 'Networking' - 'BrokerPort' - Interval: 5 + Interval: 120 Retries: 1 - StartPeriod: 0 - Timeout: 2 + StartPeriod: 10 + Timeout: 5 Image: !Join - '' - - !Ref 'AWS::AccountId' @@ -716,7 +657,8 @@ Resources: SecurityGroupEgress: - DestinationSecurityGroupId: !Ref 'BrokerPublisherSecurityGroup' IpProtocol: -1 - VpcId: !Ref 'Vpc' + VpcId: + Fn::ImportValue: !Sub '${VpcStackName}-VpcId' Type: 'AWS::EC2::SecurityGroup' # Cluster for running ECS containers. ContainerCluster: @@ -840,7 +782,8 @@ Resources: SecurityGroupEgress: - CidrIp: '0.0.0.0/0' IpProtocol: -1 - VpcId: !Ref 'Vpc' + VpcId: + Fn::ImportValue: !Sub '${VpcStackName}-VpcId' Type: 'AWS::EC2::SecurityGroup' # Database cluster. DbCluster: @@ -851,7 +794,8 @@ Resources: ignore_checks: - 'W2501' Properties: - DBSubnetGroupName: !Ref 'DbSubnetGroup' + DBSubnetGroupName: + Fn::ImportValue: !Sub '${VpcStackName}-DbSubnetGroupName' DatabaseName: !FindInMap - 'Constants' - 'DatabaseConfig' @@ -898,7 +842,8 @@ Resources: Condition: 'DeployDb' Properties: GroupDescription: !Ref 'AWS::StackName' - VpcId: !Ref 'Vpc' + VpcId: + Fn::ImportValue: !Sub '${VpcStackName}-VpcId' Type: 'AWS::EC2::SecurityGroup' # Ingress policy for the database's security group, separated to eliminate # circular dependencies with security group for users of the database. @@ -909,16 +854,6 @@ Resources: IpProtocol: -1 SourceSecurityGroupId: !Ref 'DbUserSecurityGroup' Type: 'AWS::EC2::SecurityGroupIngress' - # Database subnet group. - DbSubnetGroup: - Condition: 'DeployDb' - Properties: - DBSubnetGroupDescription: !Ref 'AWS::StackName' - SubnetIds: - - !Ref 'PrivateSubnetA' - - !Ref 'PrivateSubnetB' - - !Ref 'PrivateSubnetC' - Type: 'AWS::RDS::DBSubnetGroup' # Security group for users of the database. DbUserSecurityGroup: Condition: 'DeployDb' @@ -927,7 +862,8 @@ Resources: SecurityGroupEgress: - DestinationSecurityGroupId: !Ref 'DbSecurityGroup' IpProtocol: -1 - VpcId: !Ref 'Vpc' + VpcId: + Fn::ImportValue: !Sub '${VpcStackName}-VpcId' Type: 'AWS::EC2::SecurityGroup' # Autoscaling policy for scaling in broker and PostgREST. Fn::ForEach::AutoScalingPolicyScaleIn: @@ -1056,148 +992,6 @@ Resources: # yamllint disable-line rule:key-ordering Condition: !Sub 'Deploy${Identifier}' Type: 'AWS::CloudWatch::Alarm' - # Network address translation gateway for each public subnet. - Fn::ForEach::NatGateway: - - 'Identifier' - - - 'A' - - 'B' - - 'C' - - NatGateway${Identifier}: - Properties: - AllocationId: !GetAtt - - !Sub 'NatGatewayEip${Identifier}' - - 'AllocationId' - SubnetId: !Ref - Fn::Sub: 'PublicSubnet${Identifier}' - # ForEach transforms require that condition is second key or later. - Condition: 'DeployVpc' # yamllint disable-line rule:key-ordering - Type: 'AWS::EC2::NatGateway' - # Elastic IP address for each network address translation gateway. - Fn::ForEach::NatGatewayEip: - - 'Identifier' - - - 'A' - - 'B' - - 'C' - - NatGatewayEip${Identifier}: - Properties: - Domain: 'vpc' - # ForEach transforms require that condition is second key or later. - Condition: 'DeployVpc' # yamllint disable-line rule:key-ordering - Type: 'AWS::EC2::EIP' - # Route table for each private subnet. - Fn::ForEach::PrivateRouteTable: - - 'Identifier' - - - 'A' - - 'B' - - 'C' - - PrivateRouteTable${Identifier}: - Properties: - VpcId: !Ref 'Vpc' - # ForEach transforms require that condition is second key or later. - Condition: 'DeployRouteTables' # yamllint disable-line rule:key-ordering - Type: 'AWS::EC2::RouteTable' - # Association for each private route table within each private subnet. - Fn::ForEach::PrivateRouteTableAssociation: - - 'Identifier' - - - 'A' - - 'B' - - 'C' - - PrivateRouteTableAssociation${Identifier}: - Properties: - RouteTableId: !Ref - Fn::Sub: 'PrivateRouteTable${Identifier}' - SubnetId: !Ref - Fn::Sub: 'PrivateSubnet${Identifier}' - # ForEach transforms require that condition is second key or later. - Condition: 'DeployRouteTables' # yamllint disable-line rule:key-ordering - Type: 'AWS::EC2::SubnetRouteTableAssociation' - # Route through network address translation gateway for each private subnet. - Fn::ForEach::PrivateRouteThroughNatGateway: - - 'Identifier' - - - 'A' - - 'B' - - 'C' - - PrivateRouteThroughNatGateway${Identifier}: - Properties: - DestinationCidrBlock: '0.0.0.0/0' - NatGatewayId: !Ref - Fn::Sub: 'NatGateway${Identifier}' - RouteTableId: !Ref - Fn::Sub: 'PrivateRouteTable${Identifier}' - # ForEach transforms require that condition is second key or later. - Condition: 'DeployRouteTables' # yamllint disable-line rule:key-ordering - Type: 'AWS::EC2::Route' - # A private subnet for each of the database availability zones. - Fn::ForEach::PrivateSubnet: - - 'Identifier' - - - 'A' - - 'B' - - 'C' - - PrivateSubnet${Identifier}: - Properties: - AvailabilityZone: !Select - - !FindInMap - - 'PrivateSubnets' - - Ref: 'Identifier' - - 'AvailabilityZone' - - Fn::GetAZs: !Ref 'AWS::Region' - CidrBlock: !FindInMap - - 'PrivateSubnets' - - Ref: 'Identifier' - - 'CidrBlock' - MapPublicIpOnLaunch: false - VpcId: !Ref 'Vpc' - # ForEach transforms require that condition is second key or later. - Condition: 'DeployVpc' # yamllint disable-line rule:key-ordering - Type: 'AWS::EC2::Subnet' - # Association for public route table with each public subnet. - Fn::ForEach::PublicRouteTableAssociation: - - 'Identifier' - - - 'A' - - 'B' - - 'C' - - PublicRouteTableAssociation${Identifier}: - Properties: - RouteTableId: !Ref 'PublicRouteTable' - SubnetId: !Ref - Fn::Sub: 'PublicSubnet${Identifier}' - # ForEach transforms require that condition is second key or later. - Condition: 'DeployRouteTables' # yamllint disable-line rule:key-ordering - Type: 'AWS::EC2::SubnetRouteTableAssociation' - # A public subnet for each availability zone. - Fn::ForEach::PublicSubnet: - - 'Identifier' - - - 'A' - - 'B' - - 'C' - - PublicSubnet${Identifier}: - Properties: - AvailabilityZone: !Select - - !FindInMap - - 'PublicSubnets' - - Ref: 'Identifier' - - 'AvailabilityZone' - - Fn::GetAZs: !Ref 'AWS::Region' - CidrBlock: !FindInMap - - 'PublicSubnets' - - Ref: 'Identifier' - - 'CidrBlock' - MapPublicIpOnLaunch: true - VpcId: !Ref 'Vpc' - # ForEach transforms require that condition is second key or later. - Condition: 'DeployVpc' # yamllint disable-line rule:key-ordering - Type: 'AWS::EC2::Subnet' - # Internet gateway for the virtual private cloud. - InternetGateway: - Condition: 'DeployVpc' - Type: 'AWS::EC2::InternetGateway' - # Attachment of internet gateway to the virtual private cloud. - InternetGatewayAttachment: - Condition: 'DeployVpc' - Properties: - InternetGatewayId: !Ref 'InternetGateway' - VpcId: !Ref 'Vpc' - Type: 'AWS::EC2::VPCGatewayAttachment' # Private network load balancer. Nlb: Condition: 'DeployNlb' @@ -1217,9 +1011,9 @@ Resources: - !Ref 'PostgrestClientSecurityGroup' - !Ref 'AWS::NoValue' Subnets: - - !Ref 'PrivateSubnetA' - - !Ref 'PrivateSubnetB' - - !Ref 'PrivateSubnetC' + - Fn::ImportValue: !Sub '${VpcStackName}-PrivateSubnetIdA' + - Fn::ImportValue: !Sub '${VpcStackName}-PrivateSubnetIdB' + - Fn::ImportValue: !Sub '${VpcStackName}-PrivateSubnetIdC' Type: 'network' Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer' # Security group for direct clients of the network load balancer. @@ -1230,14 +1024,16 @@ Resources: SecurityGroupEgress: - DestinationSecurityGroupId: !Ref 'NlbSecurityGroup' IpProtocol: -1 - VpcId: !Ref 'Vpc' + VpcId: + Fn::ImportValue: !Sub '${VpcStackName}-VpcId' Type: 'AWS::EC2::SecurityGroup' # Security group for the network load balancer. NlbSecurityGroup: Condition: 'DeployNlb' Properties: GroupDescription: !Ref 'AWS::StackName' - VpcId: !Ref 'Vpc' + VpcId: + Fn::ImportValue: !Sub '${VpcStackName}-VpcId' Type: 'AWS::EC2::SecurityGroup' # Network load balancer security group ingress policy for PostgREST traffic # over VPC link. @@ -1272,7 +1068,8 @@ Resources: SecurityGroupEgress: - DestinationSecurityGroupId: !Ref 'PostgrestServerSecurityGroup' IpProtocol: -1 - VpcId: !Ref 'Vpc' + VpcId: + Fn::ImportValue: !Sub '${VpcStackName}-VpcId' Type: 'AWS::EC2::SecurityGroup' # Listener for network load balancer traffic to PostgREST. PostgrestNlbListener: @@ -1315,7 +1112,8 @@ Resources: Value: 'false' TargetType: 'ip' UnhealthyThresholdCount: 2 - VpcId: !Ref 'Vpc' + VpcId: + Fn::ImportValue: !Sub '${VpcStackName}-VpcId' Type: 'AWS::ElasticLoadBalancingV2::TargetGroup' # Service for running PostgREST. PostgrestRunner: @@ -1327,14 +1125,6 @@ Resources: # Wait for processor to ensure that migrations have been run, so that the # schema will be available and load balancer health checks will pass. - 'ProcessorRunner' - # Wait until there is an outbound route to get the image from the - # pull through cache. - - 'PrivateRouteTableAssociationA' - - 'PrivateRouteTableAssociationB' - - 'PrivateRouteTableAssociationC' - - 'PrivateRouteThroughNatGatewayA' - - 'PrivateRouteThroughNatGatewayB' - - 'PrivateRouteThroughNatGatewayC' # Proxy for a conditional dependency on the network load balancer listener # for PostgREST, which associates the PostgREST target group with the # network load balancer. @@ -1366,9 +1156,9 @@ Resources: - !Ref 'PostgrestServerSecurityGroup' - !Ref 'ContainerSecurityGroup' Subnets: - - !Ref 'PrivateSubnetA' - - !Ref 'PrivateSubnetB' - - !Ref 'PrivateSubnetC' + - Fn::ImportValue: !Sub '${VpcStackName}-PrivateSubnetIdA' + - Fn::ImportValue: !Sub '${VpcStackName}-PrivateSubnetIdB' + - Fn::ImportValue: !Sub '${VpcStackName}-PrivateSubnetIdC' ServiceRegistries: - RegistryArn: !GetAtt 'PostgrestServiceDiscovery.Arn' TaskDefinition: !Ref 'PostgrestTask' @@ -1378,7 +1168,8 @@ Resources: Condition: 'DeployPostgrest' Properties: GroupDescription: !Ref 'AWS::StackName' - VpcId: !Ref 'Vpc' + VpcId: + Fn::ImportValue: !Sub '${VpcStackName}-VpcId' Type: 'AWS::EC2::SecurityGroup' # Ingress policy for the PostgREST server security group. PostgrestServerSecurityGroupIngress: @@ -1397,10 +1188,9 @@ Resources: - TTL: 10 Type: 'A' RoutingPolicy: 'MULTIVALUE' - HealthCheckCustomConfig: - FailureThreshold: 1 Name: !Sub '${AWS::StackName}-postgrest' - NamespaceId: !Ref 'PrivateServiceDiscoveryNamespace' + NamespaceId: + Fn::ImportValue: !Sub '${VpcStackName}-PrivateDnsNamespaceId' Type: 'AWS::ServiceDiscovery::Service' # Task definition for PostgREST. PostgrestTask: @@ -1498,20 +1288,13 @@ Resources: RequiresCompatibilities: - 'FARGATE' Type: 'AWS::ECS::TaskDefinition' - # Private DNS namespace for internal service discovery. - PrivateServiceDiscoveryNamespace: - Condition: 'DeployVpc' - Properties: - Description: !Ref 'AWS::StackName' - Name: !Ref 'AWS::StackName' - Vpc: !Ref 'Vpc' - Type: 'AWS::ServiceDiscovery::PrivateDnsNamespace' # Security group for the processor's WebSocket server. ProcessorPublisherSecurityGroup: - Condition: 'DeployVpc' + Condition: 'DeployProcessor' Properties: GroupDescription: !Ref 'AWS::StackName' - VpcId: !Ref 'Vpc' + VpcId: + Fn::ImportValue: !Sub '${VpcStackName}-VpcId' Type: 'AWS::EC2::SecurityGroup' # Ingress policy for the processor's WebSocket server security group. ProcessorPublisherSecurityGroupIngress: @@ -1529,14 +1312,6 @@ Resources: # available, so the service will not be able to connect to the database. - 'DbInstancePrimary' - 'DbInstanceReplica' - # Wait until there is an outbound route to get the image from the - # pull through cache. - - 'PrivateRouteTableAssociationA' - - 'PrivateRouteTableAssociationB' - - 'PrivateRouteTableAssociationC' - - 'PrivateRouteThroughNatGatewayA' - - 'PrivateRouteThroughNatGatewayB' - - 'PrivateRouteThroughNatGatewayC' Properties: Cluster: !Ref 'ContainerCluster' DeploymentConfiguration: @@ -1554,9 +1329,9 @@ Resources: - !Ref 'DbUserSecurityGroup' - !Ref 'ProcessorPublisherSecurityGroup' Subnets: - - !Ref 'PrivateSubnetA' - - !Ref 'PrivateSubnetB' - - !Ref 'PrivateSubnetC' + - Fn::ImportValue: !Sub '${VpcStackName}-PrivateSubnetIdA' + - Fn::ImportValue: !Sub '${VpcStackName}-PrivateSubnetIdB' + - Fn::ImportValue: !Sub '${VpcStackName}-PrivateSubnetIdC' ServiceRegistries: - RegistryArn: !GetAtt 'ProcessorServiceDiscovery.Arn' TaskDefinition: !Ref 'ProcessorTask' @@ -1570,10 +1345,9 @@ Resources: - TTL: 10 Type: 'A' RoutingPolicy: 'MULTIVALUE' - HealthCheckCustomConfig: - FailureThreshold: 1 Name: !Sub '${AWS::StackName}-processor' - NamespaceId: !Ref 'PrivateServiceDiscoveryNamespace' + NamespaceId: + Fn::ImportValue: !Sub '${VpcStackName}-PrivateDnsNamespaceId' Type: 'AWS::ServiceDiscovery::Service' # Task definition for processor. ProcessorTask: @@ -1627,10 +1401,10 @@ Resources: - 'Constants' - 'Networking' - 'ProcessorWebsocketPort' - Interval: 5 - Retries: 1 - StartPeriod: 0 - Timeout: 2 + Interval: 60 + Retries: 2 + StartPeriod: 60 + Timeout: 5 Image: !Join - '' - - !Ref 'AWS::AccountId' @@ -1693,22 +1467,9 @@ Resources: SecurityGroupEgress: - DestinationSecurityGroupId: !Ref 'ProcessorPublisherSecurityGroup' IpProtocol: -1 - VpcId: !Ref 'Vpc' + VpcId: + Fn::ImportValue: !Sub '${VpcStackName}-VpcId' Type: 'AWS::EC2::SecurityGroup' - # Route table for public subnets in the virtual private cloud. - PublicRouteTable: - Condition: 'DeployRouteTables' - Properties: - VpcId: !Ref 'Vpc' - Type: 'AWS::EC2::RouteTable' - # Route from public subnets through the internet gateway. - PublicRouteToInternet: - Condition: 'DeployRouteTables' - Properties: - DestinationCidrBlock: '0.0.0.0/0' - GatewayId: !Ref 'InternetGateway' - RouteTableId: !Ref 'PublicRouteTable' - Type: 'AWS::EC2::Route' # REST API endpoint. RestApi: Condition: 'DeployRestApi' @@ -1899,17 +1660,6 @@ Resources: KeyType: 'API_KEY' UsagePlanId: !Ref 'RestApiUsagePlan' Type: 'AWS::ApiGateway::UsagePlanKey' - # Virtual private cloud for internal networking. - Vpc: - Condition: 'DeployVpc' - Properties: - CidrBlock: !FindInMap - - 'Constants' - - 'Networking' - - 'VpcCidrBlock' - EnableDnsHostnames: true - EnableDnsSupport: true - Type: 'AWS::EC2::VPC' # Web application firewall. Waf: Condition: 'DeployWaf' @@ -2051,15 +1801,6 @@ Resources: WebACLArn: !GetAtt 'Waf.Arn' Type: 'AWS::WAFv2::WebACLAssociation' Rules: - DeployAlb: - Assertions: - - Assert: !Or - - !Equals - - !Ref 'DeployVpc' - - 'true' - - !Equals - - !Ref 'DeployAlb' - - 'false' DeployAlbDnsRecord: Assertions: - Assert: !Or @@ -2094,33 +1835,11 @@ Rules: DeployContainers: Assertions: - Assert: !Or - - !And - - !Equals - - !Ref 'DeployDb' - - 'true' - - !Equals - - !Ref 'DeployRouteTables' - - 'true' - - !Equals - - !Ref 'DeployContainers' - - 'false' - DeployDb: - Assertions: - - Assert: !Or - - !Equals - - !Ref 'DeployVpc' - - 'true' - !Equals - !Ref 'DeployDb' - - 'false' - DeployNlb: - Assertions: - - Assert: !Or - - !Equals - - !Ref 'DeployVpc' - 'true' - !Equals - - !Ref 'DeployNlb' + - !Ref 'DeployContainers' - 'false' DeployNlbVpcLink: Assertions: @@ -2175,24 +1894,6 @@ Rules: - !Equals - !Ref 'DeployRestApiDnsRecord' - 'false' - DeployRouteTables: - Assertions: - - Assert: !Or - - !Equals - - !Ref 'DeployVpc' - - 'true' - - !Equals - - !Ref 'DeployRouteTables' - - 'false' - DeployVpc: - Assertions: - - Assert: !Or - - !Equals - - !Ref 'DeployStack' - - 'true' - - !Equals - - !Ref 'DeployVpc' - - 'false' DeployWaf: Assertions: - Assert: !Or diff --git a/src/cloud-formation/vpc.cfn.yaml b/src/cloud-formation/vpc.cfn.yaml new file mode 100644 index 000000000..10efdc1ed --- /dev/null +++ b/src/cloud-formation/vpc.cfn.yaml @@ -0,0 +1,263 @@ +--- +Mappings: + Constants: + Vpc: + CidrBlock: '10.0.0.0/16' + PrivateSubnets: + A: + # Private subnet in first availability zone. + AvailabilityZone: 0 + CidrBlock: '10.0.0.0/19' + B: + # Private subnet in second availability zone. + AvailabilityZone: 1 + CidrBlock: '10.0.32.0/19' + C: + # Private subnet in third availability zone. + AvailabilityZone: 2 + CidrBlock: '10.0.64.0/19' + PublicSubnets: + A: + # Public subnet in first availability zone. + AvailabilityZone: 0 + CidrBlock: '10.0.128.0/19' + B: + # Public subnet in second availability zone. + AvailabilityZone: 1 + CidrBlock: '10.0.160.0/19' + C: + # Public subnet in third availability zone. + AvailabilityZone: 2 + CidrBlock: '10.0.192.0/19' +Outputs: + DbSubnetGroupName: + Export: + Name: !Sub '${AWS::StackName}-DbSubnetGroupName' + Value: !Ref 'DbSubnetGroup' + InstanceConnectEndpointSecurityGroupId: + Export: + Name: !Sub '${AWS::StackName}-InstanceConnectEndpointSecurityGroupId' + Value: !Ref 'InstanceConnectEndpointSecurityGroup' + InternetGatewayId: + Export: + Name: !Sub '${AWS::StackName}-InternetGatewayId' + Value: !Ref 'InternetGateway' + PrivateDnsNamespaceId: + Export: + Name: !Sub '${AWS::StackName}-PrivateDnsNamespaceId' + Value: !Ref 'PrivateDnsNamespace' + PrivateSubnetIdA: + Export: + Name: !Sub '${AWS::StackName}-PrivateSubnetIdA' + Value: !Ref 'PrivateSubnetA' + PrivateSubnetIdB: + Export: + Name: !Sub '${AWS::StackName}-PrivateSubnetIdB' + Value: !Ref 'PrivateSubnetB' + PrivateSubnetIdC: + Export: + Name: !Sub '${AWS::StackName}-PrivateSubnetIdC' + Value: !Ref 'PrivateSubnetC' + PublicSubnetIdA: + Export: + Name: !Sub '${AWS::StackName}-PublicSubnetIdA' + Value: !Ref 'PublicSubnetA' + PublicSubnetIdB: + Export: + Name: !Sub '${AWS::StackName}-PublicSubnetIdB' + Value: !Ref 'PublicSubnetB' + PublicSubnetIdC: + Export: + Name: !Sub '${AWS::StackName}-PublicSubnetIdC' + Value: !Ref 'PublicSubnetC' + VpcId: + Export: + Name: !Sub '${AWS::StackName}-VpcId' + Value: !Ref 'Vpc' +Resources: + # Database subnet group. + DbSubnetGroup: + Properties: + DBSubnetGroupDescription: !Ref 'AWS::StackName' + SubnetIds: + - !Ref 'PrivateSubnetA' + - !Ref 'PrivateSubnetB' + - !Ref 'PrivateSubnetC' + Type: 'AWS::RDS::DBSubnetGroup' + # Network address translation gateway for each public subnet. + Fn::ForEach::NatGateway: + - 'Identifier' + - - 'A' + - 'B' + - 'C' + - NatGateway${Identifier}: + DependsOn: 'InternetGatewayAttachment' + Properties: + AllocationId: !GetAtt + - !Sub 'NatGatewayEip${Identifier}' + - 'AllocationId' + SubnetId: !Ref + Fn::Sub: 'PublicSubnet${Identifier}' + Type: 'AWS::EC2::NatGateway' + # Elastic IP address for each network address translation gateway. + Fn::ForEach::NatGatewayEip: + - 'Identifier' + - - 'A' + - 'B' + - 'C' + - NatGatewayEip${Identifier}: + Properties: + Domain: 'vpc' + Type: 'AWS::EC2::EIP' + # Route table for each private subnet. + Fn::ForEach::PrivateRouteTable: + - 'Identifier' + - - 'A' + - 'B' + - 'C' + - PrivateRouteTable${Identifier}: + Properties: + VpcId: !Ref 'Vpc' + Type: 'AWS::EC2::RouteTable' + # Association for each private route table within each private subnet. + Fn::ForEach::PrivateRouteTableAssociation: + - 'Identifier' + - - 'A' + - 'B' + - 'C' + - PrivateRouteTableAssociation${Identifier}: + Properties: + RouteTableId: !Ref + Fn::Sub: 'PrivateRouteTable${Identifier}' + SubnetId: !Ref + Fn::Sub: 'PrivateSubnet${Identifier}' + Type: 'AWS::EC2::SubnetRouteTableAssociation' + # Route through network address translation gateway for each private subnet. + Fn::ForEach::PrivateRouteThroughNatGateway: + - 'Identifier' + - - 'A' + - 'B' + - 'C' + - PrivateRouteThroughNatGateway${Identifier}: + DependsOn: + - 'PrivateRouteTableAssociationA' + - 'PrivateRouteTableAssociationB' + - 'PrivateRouteTableAssociationC' + Properties: + DestinationCidrBlock: '0.0.0.0/0' + NatGatewayId: !Ref + Fn::Sub: 'NatGateway${Identifier}' + RouteTableId: !Ref + Fn::Sub: 'PrivateRouteTable${Identifier}' + Type: 'AWS::EC2::Route' + # A private subnet for each availability zone. + Fn::ForEach::PrivateSubnet: + - 'Identifier' + - - 'A' + - 'B' + - 'C' + - PrivateSubnet${Identifier}: + Properties: + AvailabilityZone: !Select + - !FindInMap + - 'PrivateSubnets' + - Ref: 'Identifier' + - 'AvailabilityZone' + - Fn::GetAZs: !Ref 'AWS::Region' + CidrBlock: !FindInMap + - 'PrivateSubnets' + - Ref: 'Identifier' + - 'CidrBlock' + MapPublicIpOnLaunch: false + VpcId: !Ref 'Vpc' + Type: 'AWS::EC2::Subnet' + # Association for public route table with each public subnet. + Fn::ForEach::PublicRouteTableAssociation: + - 'Identifier' + - - 'A' + - 'B' + - 'C' + - PublicRouteTableAssociation${Identifier}: + Properties: + RouteTableId: !Ref 'PublicRouteTable' + SubnetId: !Ref + Fn::Sub: 'PublicSubnet${Identifier}' + Type: 'AWS::EC2::SubnetRouteTableAssociation' + # A public subnet for each availability zone. + Fn::ForEach::PublicSubnet: + - 'Identifier' + - - 'A' + - 'B' + - 'C' + - PublicSubnet${Identifier}: + Properties: + AvailabilityZone: !Select + - !FindInMap + - 'PublicSubnets' + - Ref: 'Identifier' + - 'AvailabilityZone' + - Fn::GetAZs: !Ref 'AWS::Region' + CidrBlock: !FindInMap + - 'PublicSubnets' + - Ref: 'Identifier' + - 'CidrBlock' + MapPublicIpOnLaunch: true + VpcId: !Ref 'Vpc' + Type: 'AWS::EC2::Subnet' + # Instance connect endpoint. + InstanceConnectEndpoint: + Properties: + PreserveClientIp: true + SecurityGroupIds: + - !Ref 'InstanceConnectEndpointSecurityGroup' + SubnetId: !Ref 'PrivateSubnetA' + Type: 'AWS::EC2::InstanceConnectEndpoint' + # Security group for instance connect endpoint. + InstanceConnectEndpointSecurityGroup: + Properties: + GroupDescription: !Ref 'AWS::StackName' + VpcId: !Ref 'Vpc' + Type: 'AWS::EC2::SecurityGroup' + # Internet gateway for the virtual private cloud. + InternetGateway: + Type: 'AWS::EC2::InternetGateway' + # Attachment of internet gateway to the virtual private cloud. + InternetGatewayAttachment: + Properties: + InternetGatewayId: !Ref 'InternetGateway' + VpcId: !Ref 'Vpc' + Type: 'AWS::EC2::VPCGatewayAttachment' + # Private DNS namespace for internal service discovery. + PrivateDnsNamespace: + Properties: + Name: !Ref 'AWS::StackName' + Vpc: !Ref 'Vpc' + Type: 'AWS::ServiceDiscovery::PrivateDnsNamespace' + # Route table for public subnets in the virtual private cloud. + PublicRouteTable: + Properties: + VpcId: !Ref 'Vpc' + Type: 'AWS::EC2::RouteTable' + # Route from public subnets through the internet gateway. + PublicRouteToInternet: + DependsOn: + - 'PublicRouteTableAssociationA' + - 'PublicRouteTableAssociationB' + - 'PublicRouteTableAssociationC' + Properties: + DestinationCidrBlock: '0.0.0.0/0' + GatewayId: !Ref 'InternetGateway' + RouteTableId: !Ref 'PublicRouteTable' + Type: 'AWS::EC2::Route' + # Virtual private cloud for internal networking. + Vpc: + Properties: + CidrBlock: !FindInMap + - 'Constants' + - 'Vpc' + - 'CidrBlock' + EnableDnsHostnames: true + EnableDnsSupport: true + Type: 'AWS::EC2::VPC' +Transform: 'AWS::LanguageExtensions' +...