AWSTemplateFormatVersion: '2010-09-09'

# $Id: aws-ec2-networking.yaml,v 1.3 2026/02/06 12:18:19 mgoddard Exp mgoddard $

Description: >
  VPC with Public + Private subnets. Public subnet has IGW; Private subnet uses NAT Gateway.
  Optional SSH to bastion in public subnet; private instances are reachable from bastion only.

Parameters:

  VpcCidr:
    Type: String
    Default: 10.0.0.0/16
    Description: CIDR block for the VPC
    AllowedPattern: ^(([0-9]{1,3}\.){3}[0-9]{1,3})\/([0-9]|[1-2][0-9]|3[0-2])$

  PublicSubnetCidr:
    Type: String
    Default: 10.0.1.0/24
    Description: CIDR for the public subnet
    AllowedPattern: ^(([0-9]{1,3}\.){3}[0-9]{1,3})\/([0-9]|[1-2][0-9]|3[0-2])$

  PrivateSubnetCidr:
    Type: String
    Default: 10.0.2.0/24
    Description: CIDR for the private subnet
    AllowedPattern: ^(([0-9]{1,3}\.){3}[0-9]{1,3})\/([0-9]|[1-2][0-9]|3[0-2])$

  AllowPublicSsh:
    Type: String
    Default: "true"
    AllowedValues: ["true", "false"]
    Description: Allow inbound SSH to the bastion SG from SSHFromCidr

  AllowPublic5432:
    Type: String
    Default: "true"
    AllowedValues: ["true", "false"]
    Description: Allow inbound DB client connections to the bastion SG from SSHFromCidr

  SSHFromCidr:
    Type: String
    Default: 0.0.0.0/0
    Description: Source CIDR for SSH to the bastion (e.g., your office IP/32)
    AllowedPattern: ^(([0-9]{1,3}\.){3}[0-9]{1,3})\/([0-9]|[1-2][0-9]|3[0-2])$

Conditions:

  AllowPublicSshCondition: !Equals [ !Ref AllowPublicSsh, "true" ]

  AllowPublic5432Condition: !Equals [ !Ref AllowPublic5432, "true" ]

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCidr
      EnableDnsHostnames: true
      EnableDnsSupport: true
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-vpc'

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-igw'

  VpcIgwAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref PublicSubnetCidr
      MapPublicIpOnLaunch: true
      AvailabilityZone: !Select [ 0, !GetAZs '' ]
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-public-subnet'

  PrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref PrivateSubnetCidr
      MapPublicIpOnLaunch: false
      AvailabilityZone: !Select [ 1, !GetAZs '' ]
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-private-subnet'

  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-public-rt'

  DefaultRouteToInternet:
    Type: AWS::EC2::Route
    DependsOn: VpcIgwAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  PublicSubnetRouteAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet
      RouteTableId: !Ref PublicRouteTable

  NatEip:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-nat-eip'

  NatGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatEip.AllocationId
      SubnetId: !Ref PublicSubnet
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-nat'

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-private-rt'

  DefaultRouteToNat:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway

  PrivateSubnetRouteAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PrivateSubnet
      RouteTableId: !Ref PrivateRouteTable

  # -------- Security Groups --------

  # Bastion SG (attach to instances in the public subnet).
  BastionSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Bastion/public SG (egress open; SSH optional)
      VpcId: !Ref VPC
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-bastion-sg'

  BastionSshIngress:
    Condition: AllowPublicSshCondition
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref BastionSG
      IpProtocol: tcp
      FromPort: 22
      ToPort: 22
      CidrIp: !Ref SSHFromCidr
      Description: SSH to bastion from specified CIDR

  Bastion5432Ingress:
    Condition: AllowPublic5432Condition
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref BastionSG
      IpProtocol: tcp
      FromPort: 5432
      ToPort: 5432
      CidrIp: !Ref SSHFromCidr
      Description: DB client connections to bastion from specified CIDR

  # Private instances SG: allow SSH only from BastionSG
  PrivateInstanceSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Private instances SG (egress open; SSH from bastion only)
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          SourceSecurityGroupId: !Ref BastionSG
          Description: SSH from BastionSG
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-private-sg'

  PrivateInstanceDbIngressFromPublicCidr:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref PrivateInstanceSG
      IpProtocol: tcp
      FromPort: 5432
      ToPort: 5432
      CidrIp: !Ref PublicSubnetCidr
      Description: DB (5432) from public subnet CIDR

Outputs:
  VpcId:
    Description: VPC ID
    Value: !Ref VPC
    Export:
      Name: !Sub '${AWS::StackName}:VpcId'

  PublicSubnetId:
    Description: Public Subnet ID (Internet egress via IGW)
    Value: !Ref PublicSubnet
    Export:
      Name: !Sub '${AWS::StackName}:PublicSubnetId'

  PublicSecurityGroupId:
    Description: Security Group for instances in public subnet
    Value: !Ref BastionSG
    Export:
      Name: !Sub '${AWS::StackName}:BastionSG'

  PrivateSubnetId:
    Description: Private Subnet ID (Internet egress via NAT)
    Value: !Ref PrivateSubnet
    Export:
      Name: !Sub '${AWS::StackName}:PrivateSubnetId'

  PrivateSecurityGroupId:
    Description: Security Group for instances in private subnet (SSH from bastion only)
    Value: !Ref PrivateInstanceSG
    Export:
      Name: !Sub '${AWS::StackName}:PrivateInstanceSG'

