Handling inbound and outbound traffic

Once we have the ACL created and associated with public subnets, we can allow all inbound and outbound traffic for that:

"OutboundPublicNetworkAclEntry": { 
  "Type": "AWS::EC2::NetworkAclEntry", 
  "Properties": { 
    "NetworkAclId": { 
      "Ref": "PublicNetworkAcl" 
    }, 
    "RuleNumber": "100", 
    "Protocol": "6", 
    "RuleAction": "allow", 
    "Egress": "true", 
    "CidrBlock": "0.0.0.0/0", 
    "PortRange": { 
      "From": "0", 
      "To": "65535" 
    } 
  } 
}, 
"InboundPublicNetworkAclEntry": { 
  "Type": "AWS::EC2::NetworkAclEntry", 
  "Properties": { 
    "NetworkAclId": { 
      "Ref": "PublicNetworkAcl" 
    }, 
    "RuleNumber": "100", 
    "Protocol": "6", 
    "RuleAction": "allow", 
    "Egress": "false", 
    "CidrBlock": "0.0.0.0/0", 
    "PortRange": { 
      "From": "0", 
      "To": "65535" 
    } 
  } 
}, 

You can get more information about this resource's syntax at http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ec2-network-acl-entry.html. Especially for the Protocol property you might need to check the documentation. Just to be clear, 6 means TCP traffic here, as defined at http://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml. Alternatively, can use -1 for all protocols.

In VPC, ACLs are stateless unlike security groups. Security groups are stateful. If you add an inbound rule for port 80 for a security group, the traffic is automatically allowed out, meaning an outbound rule for that particular port need not be explicitly added. But with ACLs you need to provide explicit inbound and outbound rules. That's why we also defined an inbound rule.

Now, we have to create an Internet Gateway and route all outgoing traffic from the public subnet to that Internet Gateway:

"InternetGateway": { 
  "Type": "AWS::EC2::InternetGateway", 
  "Properties": { 
    "Tags": [ 
      { 
        "Key": "Name", 
        "Value": { 
          "Fn::Sub": "${AWS::StackName}: Internet Gateway" 
        } 
      } 
    ] 
  } 
}, 
"GatewayToInternet": { 
  "Type": "AWS::EC2::VPCGatewayAttachment", 
  "Properties": { 
    "VpcId": { 
      "Ref": "VPC" 
    }, 
    "InternetGatewayId": { 
      "Ref": "InternetGateway" 
    } 
  } 
}, 
"PublicRoute": { 
  "Type": "AWS::EC2::Route", 
  "DependsOn": "GatewayToInternet", 
  "Properties": { 
    "RouteTableId": { 
      "Ref": "PublicRouteTable" 
    }, 
    "DestinationCidrBlock": "0.0.0.0/0", 
    "GatewayId": { 
      "Ref": "InternetGateway" 
    } 
  } 
} 

Right now, we have ensured that any device with an IP at public subnet will have internet access because all traffic is routed to Internet Gateway. Now we create Elastic IP addresses (Internet-accessible IPs) and create NAT Gateways. We will attach the Elastic IP with the NAT Gateway, and then we will create a routing rule that routes all private subnet to NAT Gateways. Note that the first private subnet will be routed to the first NAT Gateway and the second one to the second NAT Gateway:

"EIP1": { 
  "Type": "AWS::EC2::EIP", 
  "Properties": { 
    "Domain": "vpc" 
  } 
}, 
"NAT1": { 
  "DependsOn": "GatewayToInternet", 
  "Type": "AWS::EC2::NatGateway", 
  "Properties": { 
    "AllocationId": { 
      "Fn::GetAtt": [ 
        "EIP1", 
        "AllocationId" 
      ] 
    }, 
    "SubnetId": { 
      "Ref": "PublicSubnet1" 
    } 
  } 
}, 
"Nat1Route": { 
  "Type": "AWS::EC2::Route", 
  "Properties": { 
    "RouteTableId": { 
      "Ref": "PrivateRouteTable1" 
    }, 
    "DestinationCidrBlock": "0.0.0.0/0", 
    "NatGatewayId": { 
      "Ref": "NAT1" 
    } 
  } 
} 

And now, let's create the second NAT Gateway in pretty much the same way:

"Nat2Route": { 
  "Type": "AWS::EC2::Route", 
  "Properties": { 
    "RouteTableId": { 
      "Ref": "PrivateRouteTable2" 
    }, 
    "DestinationCidrBlock": "0.0.0.0/0", 
    "NatGatewayId": { 
      "Ref": "NAT2" 
    } 
  } 
}, 
"EIP2": { 
  "Type": "AWS::EC2::EIP", 
  "Properties": { 
    "Domain": "vpc" 
  } 
}, 
"NAT2": { 
  "DependsOn": "GatewayToInternet", 
  "Type": "AWS::EC2::NatGateway", 
  "Properties": { 
    "AllocationId": { 
      "Fn::GetAtt": [ 
        "EIP2", 
        "AllocationId" 
      ] 
    }, 
    "SubnetId": { 
      "Ref": "PublicSubnet2" 
    } 
  } 
}, 
"EIP2": { 
  "Type": "AWS::EC2::EIP", 
  "Properties": { 
    "Domain": "vpc" 
  } 
}, 
"Nat1Route": { 
  "Type": "AWS::EC2::Route", 
  "Properties": { 
    "RouteTableId": { 
      "Ref": "PrivateRouteTable1" 
    }, 
    "DestinationCidrBlock": "0.0.0.0/0", 
    "NatGatewayId": { 
      "Ref": "NAT1" 
    } 
  } 
} 

When you deploy the stack, it will create the VPC and everything else for you.