AWS Networking: Hands-on Runbook
AWS Networking: Hands-on Runbook
Build a production-shaped VPC from the CLI and prove each part works: a two-AZ VPC with public, app and data subnets, an internet gateway, a NAT gateway (regional or zonal), gateway and interface endpoints, chained security groups and tight network ACLs, private access with Session Manager and EC2 Instance Connect Endpoint, flow logs, Reachability Analyzer and Block Public Access, then peering, a transit gateway, a Site-to-Site VPN, IPv6, the Direct Connect calls, and a clean teardown. Tick as you go. Concepts and the twenty-one diagrams live in bubim-awsnet.
Prepare
0 / 0Use a sandbox account: NAT gateways, endpoints, the transit gateway and the VPN bill by the hour, and Block Public Access in phase 8 applies to the whole Region. Keep one shell open; later steps reuse the variables.
aws --version session-manager-plugin --version jq --version
export AWS_REGION=eu-central-1 read AZ_A AZ_B <<< "$(aws ec2 describe-availability-zones --filters Name=state,Values=available \ --query 'AvailabilityZones[0:2].ZoneName' --output text)" export AZ_A AZ_B ACCOUNT=$(aws sts get-caller-identity --query Account --output text) echo $ACCOUNT $AZ_A $AZ_B
tags(){ echo "ResourceType=$1,Tags=[{Key=Name,Value=$2},{Key=project,Value=netlab}]"; }
tags vpc lab-vpcVPC and subnets
0 / 010.0.0.0/16 with three tiers in two AZs. The plan leaves 10.0.2.0 to 10.0.9.255 and the /28 blocks at 10.0.30.0 free for later.
fig 1 · the main route table keeps only the local route, so a subnet nobody associated stays private
| Subnet | AZ a | AZ b | Route table |
|---|---|---|---|
| public | 10.0.0.0/24 | 10.0.1.0/24 | rt-public: IGW |
| app (private) | 10.0.10.0/24 | 10.0.11.0/24 | rt-app: NAT gateway |
| data (isolated) | 10.0.20.0/24 | 10.0.21.0/24 | rt-data: local only |
| transit gateway | 10.0.30.0/28 | 10.0.30.16/28 | main: local only |
export VPC=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16 --tag-specifications "$(tags vpc lab-vpc)" \
--query Vpc.VpcId --output text)
aws ec2 modify-vpc-attribute --vpc-id $VPC --enable-dns-hostnames '{"Value":true}'
echo $VPCsub(){ aws ec2 create-subnet --vpc-id $VPC --cidr-block $1 --availability-zone $2 \
--tag-specifications "$(tags subnet $3)" --query Subnet.SubnetId --output text; }
export PUB_A=$(sub 10.0.0.0/24 $AZ_A pub-a) PUB_B=$(sub 10.0.1.0/24 $AZ_B pub-b)
export APP_A=$(sub 10.0.10.0/24 $AZ_A app-a) APP_B=$(sub 10.0.11.0/24 $AZ_B app-b)
export DB_A=$(sub 10.0.20.0/24 $AZ_A data-a) DB_B=$(sub 10.0.21.0/24 $AZ_B data-b)aws ec2 describe-subnets --filters Name=vpc-id,Values=$VPC \ --query 'Subnets[].[Tags[?Key==`Name`]|[0].Value,CidrBlock,AvailabilityZone,AvailableIpAddressCount]' --output table
DEF_SG=$(aws ec2 describe-security-groups --filters Name=vpc-id,Values=$VPC Name=group-name,Values=default \
--query 'SecurityGroups[0].GroupId' --output text)
rules(){ aws ec2 describe-security-group-rules --filters Name=group-id,Values=$1 \
--query "SecurityGroupRules[?IsEgress==\`$2\`].SecurityGroupRuleId" --output text; }
aws ec2 revoke-security-group-ingress --group-id $DEF_SG --security-group-rule-ids $(rules $DEF_SG false)
aws ec2 revoke-security-group-egress --group-id $DEF_SG --security-group-rule-ids $(rules $DEF_SG true)Internet gateway and the public route table
0 / 0A subnet becomes public by its route table, not by its name. The main table stays local-only, so a forgotten subnet is private.
export IGW=$(aws ec2 create-internet-gateway --tag-specifications "$(tags internet-gateway lab-igw)" \ --query InternetGateway.InternetGatewayId --output text) aws ec2 attach-internet-gateway --internet-gateway-id $IGW --vpc-id $VPC
export RT_PUB=$(aws ec2 create-route-table --vpc-id $VPC --tag-specifications "$(tags route-table rt-public)" \ --query RouteTable.RouteTableId --output text) aws ec2 create-route --route-table-id $RT_PUB --destination-cidr-block 0.0.0.0/0 --gateway-id $IGW for s in $PUB_A $PUB_B; do aws ec2 associate-route-table --route-table-id $RT_PUB --subnet-id $s; done
aws ec2 describe-route-tables --filters Name=vpc-id,Values=$VPC Name=association.main,Values=true \ --query 'RouteTables[0].Routes[].[DestinationCidrBlock,GatewayId,State]' --output table
NAT gateway and private routing
0 / 0Do 3.1 and 3.2 for a regional NAT gateway, or 3.3 instead for the classic zonal pair. Data subnets get no internet route at all.
export NAT=$(aws ec2 create-nat-gateway --availability-mode regional --vpc-id $VPC \ --tag-specifications "$(tags natgateway lab-nat)" --query NatGateway.NatGatewayId --output text) aws ec2 wait nat-gateway-available --nat-gateway-ids $NAT
export RT_APP=$(aws ec2 create-route-table --vpc-id $VPC --tag-specifications "$(tags route-table rt-app)" \ --query RouteTable.RouteTableId --output text) aws ec2 create-route --route-table-id $RT_APP --destination-cidr-block 0.0.0.0/0 --nat-gateway-id $NAT for s in $APP_A $APP_B; do aws ec2 associate-route-table --route-table-id $RT_APP --subnet-id $s; done
nat_az(){
eip=$(aws ec2 allocate-address --domain vpc --tag-specifications "$(tags elastic-ip $3-eip)" \
--query AllocationId --output text)
nat=$(aws ec2 create-nat-gateway --subnet-id $1 --allocation-id $eip \
--tag-specifications "$(tags natgateway $3-nat)" --query NatGateway.NatGatewayId --output text)
aws ec2 wait nat-gateway-available --nat-gateway-ids $nat
rt=$(aws ec2 create-route-table --vpc-id $VPC --tag-specifications "$(tags route-table $3)" \
--query RouteTable.RouteTableId --output text)
aws ec2 create-route --route-table-id $rt --destination-cidr-block 0.0.0.0/0 --nat-gateway-id $nat >/dev/null
aws ec2 associate-route-table --route-table-id $rt --subnet-id $2 >/dev/null
echo $rt
}
export RT_APP=$(nat_az $PUB_A $APP_A rt-app-a) RT_APP_B=$(nat_az $PUB_B $APP_B rt-app-b)export RT_DB=$(aws ec2 create-route-table --vpc-id $VPC --tag-specifications "$(tags route-table rt-data)" \ --query RouteTable.RouteTableId --output text) for s in $DB_A $DB_B; do aws ec2 associate-route-table --route-table-id $RT_DB --subnet-id $s; done
VPC endpoints
0 / 0S3 through a free gateway endpoint (no NAT charge per GB), and Session Manager through interface endpoints, so management traffic stays private and keeps working without the NAT gateway.
export VPCE_S3=$(aws ec2 create-vpc-endpoint --vpc-id $VPC --vpc-endpoint-type Gateway \ --service-name com.amazonaws.$AWS_REGION.s3 --route-table-ids $RT_APP $RT_APP_B $RT_DB \ --query VpcEndpoint.VpcEndpointId --output text) aws ec2 describe-route-tables --route-table-ids $RT_APP \ --query 'RouteTables[0].Routes[].[DestinationCidrBlock,DestinationPrefixListId,GatewayId,NatGatewayId]' --output table
export EP_SG=$(aws ec2 create-security-group --group-name lab-endpoints --description "Interface endpoints" \ --vpc-id $VPC --query GroupId --output text) aws ec2 authorize-security-group-ingress --group-id $EP_SG --protocol tcp --port 443 --cidr 10.0.0.0/16
for svc in ssm ssmmessages; do
aws ec2 create-vpc-endpoint --vpc-id $VPC --vpc-endpoint-type Interface \
--service-name com.amazonaws.$AWS_REGION.$svc --subnet-ids $APP_A $APP_B \
--security-group-ids $EP_SG --private-dns-enabled --query VpcEndpoint.VpcEndpointId --output text
doneChained security groups
0 / 0Load balancer from anywhere on 443, app only from the load balancer’s group, database only from the app’s group.
sg(){ aws ec2 create-security-group --group-name $1 --description "$2" --vpc-id $VPC --query GroupId --output text; }
export ALB_SG=$(sg lab-alb "Public load balancer")
export APP_SG=$(sg lab-app "Application tier")
export DB_SG=$(sg lab-db "Database tier")aws ec2 authorize-security-group-ingress --group-id $ALB_SG --protocol tcp --port 443 --cidr 0.0.0.0/0 aws ec2 authorize-security-group-ingress --group-id $APP_SG --protocol tcp --port 8080 --source-group $ALB_SG aws ec2 authorize-security-group-ingress --group-id $DB_SG --protocol tcp --port 5432 --source-group $APP_SG
aws ec2 describe-security-group-rules --filters Name=group-id,Values=$ALB_SG,$APP_SG,$DB_SG \ --query 'SecurityGroupRules[?!IsEgress].[GroupId,IpProtocol,FromPort,CidrIpv4,ReferencedGroupInfo.GroupId]' --output table
Network ACLs
0 / 0A coarse guard on the data subnets: PostgreSQL in from the app subnets only, replies out to them only. NACLs are stateless, so both directions are spelled out.
export NACL_DB=$(aws ec2 create-network-acl --vpc-id $VPC --tag-specifications "$(tags network-acl nacl-data)" \ --query NetworkAcl.NetworkAclId --output text)
e(){ aws ec2 create-network-acl-entry --network-acl-id $NACL_DB "$@"; }
e --ingress --rule-number 100 --protocol 6 --port-range From=5432,To=5432 --cidr-block 10.0.10.0/23 --rule-action allow
e --egress --rule-number 100 --protocol 6 --port-range From=1024,To=65535 --cidr-block 10.0.10.0/23 --rule-action allow
aws ec2 describe-network-acls --network-acl-ids $NACL_DB \
--query 'NetworkAcls[0].Entries[].[Egress,RuleNumber,Protocol,PortRange.From,PortRange.To,CidrBlock,RuleAction]' --output tablefor s in $DB_A $DB_B; do
assoc=$(aws ec2 describe-network-acls --filters Name=association.subnet-id,Values=$s \
--query "NetworkAcls[0].Associations[?SubnetId=='$s'].NetworkAclAssociationId" --output text)
aws ec2 replace-network-acl-association --association-id $assoc --network-acl-id $NACL_DB
doneA private instance and two ways in
0 / 0No public IP, no inbound port from the internet, no bastion.
trust-ec2.json
{
"Version": "2012-10-17",
"Statement": [{"Effect": "Allow", "Principal": {"Service": "ec2.amazonaws.com"}, "Action": "sts:AssumeRole"}]
}aws iam create-role --role-name netlab-ssm --assume-role-policy-document file://trust-ec2.json aws iam attach-role-policy --role-name netlab-ssm --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore aws iam create-instance-profile --instance-profile-name netlab-ssm aws iam add-role-to-instance-profile --instance-profile-name netlab-ssm --role-name netlab-ssm
export I=$(aws ec2 run-instances --image-id resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 \ --instance-type t3.micro --subnet-id $APP_A --security-group-ids $APP_SG \ --iam-instance-profile Name=netlab-ssm --metadata-options HttpTokens=required \ --tag-specifications "$(tags instance lab-app-1)" --query 'Instances[0].InstanceId' --output text) aws ec2 wait instance-status-ok --instance-ids $I
aws ssm start-session --target $I
# inside the session
curl -sI https://aws.amazon.com | head -1
curl -sI https://s3.eu-central-1.amazonaws.com | head -1export EICE_SG=$(sg lab-eice "Instance Connect Endpoint") aws ec2 revoke-security-group-egress --group-id $EICE_SG --protocol all --port all --cidr 0.0.0.0/0 aws ec2 authorize-security-group-egress --group-id $EICE_SG --protocol tcp --port 22 --source-group $APP_SG aws ec2 authorize-security-group-ingress --group-id $APP_SG --protocol tcp --port 22 --source-group $EICE_SG export EICE=$(aws ec2 create-instance-connect-endpoint --subnet-id $APP_A --security-group-ids $EICE_SG \ --query InstanceConnectEndpoint.InstanceConnectEndpointId --output text)
aws ec2 describe-instance-connect-endpoints --instance-connect-endpoint-ids $EICE \ --query 'InstanceConnectEndpoints[0].State' --output text aws ec2-instance-connect ssh --instance-id $I --os-user ec2-user --connection-type eice
~/.ssh/config
Host bastion HostName 203.0.113.25 User ec2-user IdentityFile ~/.ssh/lab.pem Host 10.0.* User ec2-user IdentityFile ~/.ssh/lab.pem ProxyJump bastion
Flow logs, Reachability Analyzer, Block Public Access
0 / 0Visibility before more connectivity: a record of every flow, a configuration check that costs ten cents a run, and a Region-wide guard against accidental public exposure.
trust-flowlogs.json
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "vpc-flow-logs.amazonaws.com"},
"Action": "sts:AssumeRole",
"Condition": {"StringEquals": {"aws:SourceAccount": "REPLACE_WITH_ACCOUNT_ID"}}
}]
}flowlogs-policy.json
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["logs:CreateLogStream", "logs:PutLogEvents", "logs:DescribeLogGroups", "logs:DescribeLogStreams"],
"Resource": "arn:aws:logs:REPLACE_WITH_REGION:REPLACE_WITH_ACCOUNT_ID:log-group:/vpc/netlab/*"
}]
}sed -i "s/REPLACE_WITH_ACCOUNT_ID/$ACCOUNT/g; s/REPLACE_WITH_REGION/$AWS_REGION/g" trust-flowlogs.json flowlogs-policy.json aws logs create-log-group --log-group-name /vpc/netlab/flow aws logs put-retention-policy --log-group-name /vpc/netlab/flow --retention-in-days 14 export FL_ROLE=$(aws iam create-role --role-name netlab-flowlogs \ --assume-role-policy-document file://trust-flowlogs.json --query Role.Arn --output text) aws iam put-role-policy --role-name netlab-flowlogs --policy-name deliver --policy-document file://flowlogs-policy.json
export FL=$(aws ec2 create-flow-logs --resource-type VPC --resource-ids $VPC --traffic-type ALL \ --log-destination-type cloud-watch-logs --log-group-name /vpc/netlab/flow \ --deliver-logs-permission-arn $FL_ROLE --max-aggregation-interval 60 \ --query 'FlowLogIds[0]' --output text)
Q=$(aws logs start-query --log-group-name /vpc/netlab/flow \ --start-time $(date -d '-30 min' +%s) --end-time $(date +%s) \ --query-string 'filter action = "REJECT" | stats count(*) as drops by srcAddr, dstAddr, dstPort | sort drops desc | limit 20' \ --query queryId --output text) sleep 10; aws logs get-query-results --query-id $Q --query results --output text
reach(){
p=$(aws ec2 create-network-insights-path --source $1 --destination $2 --protocol tcp --destination-port $3 \
--query NetworkInsightsPath.NetworkInsightsPathId --output text)
a=$(aws ec2 start-network-insights-analysis --network-insights-path-id $p \
--query NetworkInsightsAnalysis.NetworkInsightsAnalysisId --output text)
sleep 40
aws ec2 describe-network-insights-analyses --network-insights-analysis-ids $a \
--query 'NetworkInsightsAnalyses[0].[Status,NetworkPathFound,Explanations[0].ExplanationCode]' --output text
}
reach $IGW $I 22aws ec2 modify-vpc-block-public-access-options --internet-gateway-block-mode block-ingress
x(){ aws ec2 create-vpc-block-public-access-exclusion --subnet-id $1 --internet-gateway-exclusion-mode allow-bidirectional \
--query VpcBlockPublicAccessExclusion.ExclusionId --output text; }
export BPA_X1=$(x $PUB_A) BPA_X2=$(x $PUB_B)
aws ec2 describe-vpc-block-public-access-options \
--query 'VpcBlockPublicAccessOptions.[InternetGatewayBlockMode,State]' --output textVPC peering
0 / 0A second VPC, 10.1.0.0/16, joined by a peering connection. Phases 9 to 13 build the picture below.
fig 2 · peering for one close neighbour, the transit gateway as the hub for everything else; each VPN has two tunnels, so keep both up
export VPC_B=$(aws ec2 create-vpc --cidr-block 10.1.0.0/16 --tag-specifications "$(tags vpc lab-vpc-b)" \
--query Vpc.VpcId --output text)
aws ec2 modify-vpc-attribute --vpc-id $VPC_B --enable-dns-hostnames '{"Value":true}'
export SUB_B=$(aws ec2 create-subnet --vpc-id $VPC_B --cidr-block 10.1.0.0/24 --availability-zone $AZ_A \
--tag-specifications "$(tags subnet b-a)" --query Subnet.SubnetId --output text)
export RT_B=$(aws ec2 create-route-table --vpc-id $VPC_B --tag-specifications "$(tags route-table rt-b)" \
--query RouteTable.RouteTableId --output text)
aws ec2 associate-route-table --route-table-id $RT_B --subnet-id $SUB_Bexport PCX=$(aws ec2 create-vpc-peering-connection --vpc-id $VPC --peer-vpc-id $VPC_B \ --tag-specifications "$(tags vpc-peering-connection lab-a-b)" \ --query VpcPeeringConnection.VpcPeeringConnectionId --output text) aws ec2 wait vpc-peering-connection-exists --vpc-peering-connection-ids $PCX aws ec2 accept-vpc-peering-connection --vpc-peering-connection-id $PCX aws ec2 describe-vpc-peering-connections --vpc-peering-connection-ids $PCX \ --query 'VpcPeeringConnections[0].Status.Code' --output text
for rt in $RT_APP $RT_APP_B; do aws ec2 create-route --route-table-id $rt --destination-cidr-block 10.1.0.0/16 --vpc-peering-connection-id $PCX done aws ec2 create-route --route-table-id $RT_B --destination-cidr-block 10.0.10.0/23 --vpc-peering-connection-id $PCX
aws ec2 modify-vpc-peering-connection-options --vpc-peering-connection-id $PCX \ --requester-peering-connection-options AllowDnsResolutionFromRemoteVpc=true \ --accepter-peering-connection-options AllowDnsResolutionFromRemoteVpc=true
export B_SG=$(aws ec2 create-security-group --group-name lab-b --description "VPC B test" --vpc-id $VPC_B \ --query GroupId --output text) aws ec2 authorize-security-group-ingress --group-id $B_SG --protocol tcp --port 443 --source-group $APP_SG export ENI_B=$(aws ec2 create-network-interface --subnet-id $SUB_B --groups $B_SG --description "reachability target" \ --query NetworkInterface.NetworkInterfaceId --output text)
reach $I $ENI_B 443 reach $I $ENI_B 22
Transit gateway
0 / 0The regional hub for everything that is not a single close neighbour. Here it carries the VPN of phase 11; VPC B stays on its peering.
export TGW=$(aws ec2 create-transit-gateway --description "netlab hub" \ --options AmazonSideAsn=64512,DefaultRouteTableAssociation=enable,DefaultRouteTablePropagation=enable,DnsSupport=enable,VpnEcmpSupport=enable \ --tag-specifications "$(tags transit-gateway lab-tgw)" --query TransitGateway.TransitGatewayId --output text) until [ "$(aws ec2 describe-transit-gateways --transit-gateway-ids $TGW \ --query 'TransitGateways[0].State' --output text)" = available ]; do sleep 15; done
export TGW_A=$(sub 10.0.30.0/28 $AZ_A tgw-a) TGW_B=$(sub 10.0.30.16/28 $AZ_B tgw-b) export TGW_ATT=$(aws ec2 create-transit-gateway-vpc-attachment --transit-gateway-id $TGW --vpc-id $VPC \ --subnet-ids $TGW_A $TGW_B --options DnsSupport=enable,ApplianceModeSupport=disable \ --tag-specifications "$(tags transit-gateway-attachment lab-vpc-att)" \ --query TransitGatewayVpcAttachment.TransitGatewayAttachmentId --output text) until [ "$(aws ec2 describe-transit-gateway-vpc-attachments --transit-gateway-attachment-ids $TGW_ATT \ --query 'TransitGatewayVpcAttachments[0].State' --output text)" = available ]; do sleep 15; done
for rt in $RT_APP $RT_APP_B; do aws ec2 create-route --route-table-id $rt --destination-cidr-block 192.168.0.0/16 --transit-gateway-id $TGW done
export TGW_RT=$(aws ec2 describe-transit-gateways --transit-gateway-ids $TGW \ --query 'TransitGateways[0].Options.AssociationDefaultRouteTableId' --output text) aws ec2 search-transit-gateway-routes --transit-gateway-route-table-id $TGW_RT --filters Name=state,Values=active \ --query 'Routes[].[DestinationCidrBlock,Type,TransitGatewayAttachments[0].ResourceType]' --output table
Moving VPC B to the hub Attach it as in 10.2, then replace the pcx- routes with tgw- routes on both sides. Peering and the hub can serve the same VPC for different prefixes; the longest prefix decides.
Site-to-Site VPN on the transit gateway
0 / 0Two IPsec tunnels with BGP to an on-premises network, 192.168.0.0/16. Without a real device the tunnels stay down, which is still enough to practise this phase and PB3. Each connection bills per hour.
export CGW_IP=REPLACE_WITH_ON_PREM_PUBLIC_IP export CGW=$(aws ec2 create-customer-gateway --type ipsec.1 --bgp-asn 65010 --ip-address $CGW_IP \ --tag-specifications "$(tags customer-gateway lab-onprem)" --query CustomerGateway.CustomerGatewayId --output text)
vpn-options.json
{
"StaticRoutesOnly": false,
"TunnelBandwidth": "standard",
"TunnelOptions": [
{
"TunnelInsideCidr": "169.254.10.0/30",
"StartupAction": "start",
"DPDTimeoutAction": "restart",
"IKEVersions": [{"Value": "ikev2"}],
"Phase1EncryptionAlgorithms": [{"Value": "AES256-GCM-16"}],
"Phase1IntegrityAlgorithms": [{"Value": "SHA2-384"}],
"Phase1DHGroupNumbers": [{"Value": 20}],
"Phase2EncryptionAlgorithms": [{"Value": "AES256-GCM-16"}],
"Phase2IntegrityAlgorithms": [{"Value": "SHA2-384"}],
"Phase2DHGroupNumbers": [{"Value": 20}],
"LogOptions": {"CloudWatchLogOptions": {"LogEnabled": true, "BgpLogEnabled": true, "LogOutputFormat": "json",
"LogGroupArn": "arn:aws:logs:REPLACE_WITH_REGION:REPLACE_WITH_ACCOUNT_ID:log-group:/vpn/netlab",
"BgpLogGroupArn": "arn:aws:logs:REPLACE_WITH_REGION:REPLACE_WITH_ACCOUNT_ID:log-group:/vpn/netlab"}}
},
{
"TunnelInsideCidr": "169.254.10.4/30",
"StartupAction": "start",
"DPDTimeoutAction": "restart",
"IKEVersions": [{"Value": "ikev2"}],
"Phase1EncryptionAlgorithms": [{"Value": "AES256-GCM-16"}],
"Phase1IntegrityAlgorithms": [{"Value": "SHA2-384"}],
"Phase1DHGroupNumbers": [{"Value": 20}],
"Phase2EncryptionAlgorithms": [{"Value": "AES256-GCM-16"}],
"Phase2IntegrityAlgorithms": [{"Value": "SHA2-384"}],
"Phase2DHGroupNumbers": [{"Value": 20}],
"LogOptions": {"CloudWatchLogOptions": {"LogEnabled": true, "BgpLogEnabled": true, "LogOutputFormat": "json",
"LogGroupArn": "arn:aws:logs:REPLACE_WITH_REGION:REPLACE_WITH_ACCOUNT_ID:log-group:/vpn/netlab",
"BgpLogGroupArn": "arn:aws:logs:REPLACE_WITH_REGION:REPLACE_WITH_ACCOUNT_ID:log-group:/vpn/netlab"}}
}
]
}sed -i "s/REPLACE_WITH_ACCOUNT_ID/$ACCOUNT/g; s/REPLACE_WITH_REGION/$AWS_REGION/g" vpn-options.json aws logs create-log-group --log-group-name /vpn/netlab export VPN=$(aws ec2 create-vpn-connection --type ipsec.1 --customer-gateway-id $CGW --transit-gateway-id $TGW \ --options file://vpn-options.json --pre-shared-key-storage SecretsManager \ --tag-specifications "$(tags vpn-connection lab-vpn)" --query VpnConnection.VpnConnectionId --output text) aws ec2 wait vpn-connection-available --vpn-connection-ids $VPN
aws ec2 get-vpn-connection-device-types \ --query 'VpnConnectionDeviceTypes[].[VpnConnectionDeviceTypeId,Vendor,Platform,Software]' --output table aws ec2 get-vpn-connection-device-sample-configuration --vpn-connection-id $VPN \ --vpn-connection-device-type-id REPLACE_WITH_DEVICE_TYPE_ID --internet-key-exchange-version ikev2 \ --query VpnConnectionDeviceSampleConfiguration --output text > vpn-device.txt
aws ec2 describe-vpn-connections --vpn-connection-ids $VPN \ --query 'VpnConnections[0].VgwTelemetry[].[OutsideIpAddress,Status,StatusMessage,AcceptedRouteCount]' --output table
export TOPIC=$(aws sns create-topic --name netlab-alerts --query TopicArn --output text) aws sns subscribe --topic-arn $TOPIC --protocol email --notification-endpoint REPLACE_WITH_EMAIL aws cloudwatch put-metric-alarm --alarm-name netlab-vpn-tunnel-down --namespace AWS/VPN --metric-name TunnelState \ --dimensions Name=VpnId,Value=$VPN --statistic Minimum --period 300 --evaluation-periods 2 \ --threshold 1 --comparison-operator LessThanThreshold --treat-missing-data breaching --alarm-actions $TOPIC
IPv6 dual stack
0 / 0Public and app tiers get IPv6 next to IPv4. IPv6 addresses are free, which matters now that every public IPv4 address bills by the hour.
aws ec2 associate-vpc-cidr-block --vpc-id $VPC --amazon-provided-ipv6-cidr-block sleep 10 export V6=$(aws ec2 describe-vpcs --vpc-ids $VPC \ --query 'Vpcs[0].Ipv6CidrBlockAssociationSet[0].Ipv6CidrBlock' --output text) echo $V6
v6sub(){
python3 -c 'import ipaddress, sys
net = ipaddress.ip_network(sys.argv[1])
print(list(net.subnets(new_prefix=64))[int(sys.argv[2])])' $V6 $1
}
aws ec2 associate-subnet-cidr-block --subnet-id $PUB_A --ipv6-cidr-block $(v6sub 0)
aws ec2 associate-subnet-cidr-block --subnet-id $PUB_B --ipv6-cidr-block $(v6sub 1)
aws ec2 associate-subnet-cidr-block --subnet-id $APP_A --ipv6-cidr-block $(v6sub 10)
aws ec2 associate-subnet-cidr-block --subnet-id $APP_B --ipv6-cidr-block $(v6sub 11)
for s in $APP_A $APP_B; do aws ec2 modify-subnet-attribute --subnet-id $s --assign-ipv6-address-on-creation; doneexport EIGW=$(aws ec2 create-egress-only-internet-gateway --vpc-id $VPC \ --tag-specifications "$(tags egress-only-internet-gateway lab-eigw)" \ --query EgressOnlyInternetGateway.EgressOnlyInternetGatewayId --output text) aws ec2 create-route --route-table-id $RT_PUB --destination-ipv6-cidr-block ::/0 --gateway-id $IGW for rt in $RT_APP $RT_APP_B; do aws ec2 create-route --route-table-id $rt --destination-ipv6-cidr-block ::/0 --egress-only-internet-gateway-id $EIGW done
aws ec2 authorize-security-group-ingress --group-id $ALB_SG \
--ip-permissions 'IpProtocol=tcp,FromPort=443,ToPort=443,Ipv6Ranges=[{CidrIpv6=::/0}]'
ENI=$(aws ec2 describe-instances --instance-ids $I \
--query 'Reservations[0].Instances[0].NetworkInterfaces[0].NetworkInterfaceId' --output text)
aws ec2 assign-ipv6-addresses --network-interface-id $ENI --ipv6-address-count 1
# inside a session on the instance
curl -6 -sI https://www.google.com | head -1Direct Connect
0 / 0A connection takes weeks and bills per port hour, so in a lab run only 13.1 and 13.2, which cost nothing. 13.3 and 13.4 show the shape of the real thing.
aws directconnect describe-locations \ --query 'locations[].[locationCode,locationName,join(`,`, availablePortSpeeds),join(`,`, availableMacSecPortSpeeds)]' \ --output table
export DXGW=$(aws directconnect create-direct-connect-gateway --direct-connect-gateway-name netlab-dxgw \ --amazon-side-asn 64513 --query directConnectGateway.directConnectGatewayId --output text)
aws directconnect create-direct-connect-gateway-association --direct-connect-gateway-id $DXGW \ --gateway-id $TGW --add-allowed-prefixes-to-direct-connect-gateway cidr=10.0.0.0/16
aws directconnect create-connection --location REPLACE_WITH_LOCATION_CODE --bandwidth 10Gbps \ --connection-name netlab-dx --request-mac-sec aws directconnect create-transit-virtual-interface --connection-id REPLACE_WITH_DXCON_ID \ --new-transit-virtual-interface "virtualInterfaceName=netlab-tvif,vlan=101,asn=65010,mtu=8500,directConnectGatewayId=$DXGW"
Clean up
0 / 0Reverse order, because most deletes fail while something still depends on the resource. Network interfaces of endpoints and NAT gateways take minutes to go; if a delete reports a dependency, wait and run it again.
aws ec2 delete-vpn-connection --vpn-connection-id $VPN aws ec2 wait vpn-connection-deleted --vpn-connection-ids $VPN aws ec2 delete-customer-gateway --customer-gateway-id $CGW aws cloudwatch delete-alarms --alarm-names netlab-vpn-tunnel-down aws sns delete-topic --topic-arn $TOPIC aws logs delete-log-group --log-group-name /vpn/netlab
ASSOC=$(aws directconnect describe-direct-connect-gateway-associations --direct-connect-gateway-id $DXGW \ --query 'directConnectGatewayAssociations[0].associationId' --output text) [ "$ASSOC" != None ] && aws directconnect delete-direct-connect-gateway-association --association-id $ASSOC aws directconnect delete-direct-connect-gateway --direct-connect-gateway-id $DXGW
for rt in $RT_APP $RT_APP_B; do aws ec2 delete-route --route-table-id $rt --destination-cidr-block 192.168.0.0/16; done aws ec2 delete-transit-gateway-vpc-attachment --transit-gateway-attachment-id $TGW_ATT while aws ec2 describe-transit-gateway-vpc-attachments --transit-gateway-attachment-ids $TGW_ATT \ --query 'TransitGatewayVpcAttachments[0].State' --output text | grep -Eq 'available|deleting'; do sleep 15; done aws ec2 delete-transit-gateway --transit-gateway-id $TGW
aws ec2 delete-network-interface --network-interface-id $ENI_B aws ec2 delete-vpc-peering-connection --vpc-peering-connection-id $PCX aws ec2 delete-security-group --group-id $B_SG aws ec2 delete-subnet --subnet-id $SUB_B aws ec2 delete-route-table --route-table-id $RT_B aws ec2 delete-vpc --vpc-id $VPC_B
aws ec2 terminate-instances --instance-ids $I aws ec2 delete-instance-connect-endpoint --instance-connect-endpoint-id $EICE aws ec2 delete-vpc-endpoints --vpc-endpoint-ids $(aws ec2 describe-vpc-endpoints --filters Name=vpc-id,Values=$VPC \ --query 'VpcEndpoints[].VpcEndpointId' --output text) aws ec2 wait instance-terminated --instance-ids $I
NATS=$(aws ec2 describe-nat-gateways --filter Name=vpc-id,Values=$VPC Name=state,Values=available \ --query 'NatGateways[].NatGatewayId' --output text) for n in $NATS; do aws ec2 delete-nat-gateway --nat-gateway-id $n; done aws ec2 wait nat-gateway-deleted --nat-gateway-ids $NATS for a in $(aws ec2 describe-addresses --filters Name=tag:project,Values=netlab --query 'Addresses[].AllocationId' --output text); do aws ec2 release-address --allocation-id $a done
aws ec2 delete-flow-logs --flow-log-ids $FL aws logs delete-log-group --log-group-name /vpc/netlab/flow aws iam delete-role-policy --role-name netlab-flowlogs --policy-name deliver aws iam delete-role --role-name netlab-flowlogs aws ec2 modify-vpc-block-public-access-options --internet-gateway-block-mode off for x in $BPA_X1 $BPA_X2; do aws ec2 delete-vpc-block-public-access-exclusion --exclusion-id $x; done
aws ec2 detach-internet-gateway --internet-gateway-id $IGW --vpc-id $VPC aws ec2 delete-internet-gateway --internet-gateway-id $IGW aws ec2 delete-egress-only-internet-gateway --egress-only-internet-gateway-id $EIGW for s in $PUB_A $PUB_B $APP_A $APP_B $DB_A $DB_B $TGW_A $TGW_B; do aws ec2 delete-subnet --subnet-id $s; done for rt in $RT_PUB $RT_APP $RT_APP_B $RT_DB; do aws ec2 delete-route-table --route-table-id $rt; done aws ec2 delete-network-acl --network-acl-id $NACL_DB
for g in $ALB_SG $APP_SG $DB_SG $EICE_SG $EP_SG; do aws ec2 revoke-security-group-ingress --group-id $g --security-group-rule-ids $(rules $g false) 2>/dev/null aws ec2 revoke-security-group-egress --group-id $g --security-group-rule-ids $(rules $g true) 2>/dev/null done for g in $ALB_SG $APP_SG $DB_SG $EICE_SG $EP_SG; do aws ec2 delete-security-group --group-id $g; done aws ec2 delete-vpc --vpc-id $VPC
aws iam remove-role-from-instance-profile --instance-profile-name netlab-ssm --role-name netlab-ssm aws iam delete-instance-profile --instance-profile-name netlab-ssm aws iam detach-role-policy --role-name netlab-ssm --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore aws iam delete-role --role-name netlab-ssm aws resourcegroupstaggingapi get-resources --tag-filters Key=project,Values=netlab \ --query 'ResourceTagMappingList[].ResourceARN' --output text
Playbooks
For the day something goes wrong. Ticks here do not count toward the runbook progress.
PB1 No internet from a private instance
when package updates or API calls time out from the app tier
S=REPLACE_WITH_SUBNET_ID aws ec2 describe-route-tables --filters Name=association.subnet-id,Values=$S \ --query 'RouteTables[0].Routes[].[DestinationCidrBlock,NatGatewayId,GatewayId,State]' --output table
aws ec2 describe-nat-gateways --filter Name=vpc-id,Values=$VPC \ --query 'NatGateways[].[NatGatewayId,State,AvailabilityMode,SubnetId,FailureMessage]' --output table aws ec2 describe-vpc-block-public-access-options --query 'VpcBlockPublicAccessOptions.InternetGatewayBlockMode'
PB2 No traffic across a peering
when the peering is active but connections time out
aws ec2 describe-vpc-peering-connections --vpc-peering-connection-ids $PCX --query 'VpcPeeringConnections[0].Status' aws ec2 describe-route-tables --filters Name=route.vpc-peering-connection-id,Values=$PCX \ --query 'RouteTables[].[VpcId,RouteTableId]' --output table
reach REPLACE_WITH_SOURCE_ENI REPLACE_WITH_DEST_ENI 443
PB3 VPN tunnel down
when the alarm from 11.5 fires or a tunnel shows DOWN
aws ec2 describe-vpn-connections --vpn-connection-ids $VPN \ --query 'VpnConnections[0].VgwTelemetry[].[OutsideIpAddress,Status,StatusMessage,AcceptedRouteCount]' --output table
aws logs tail /vpn/netlab --since 30m --format short
aws ec2 replace-vpn-tunnel --vpn-connection-id $VPN --vpn-tunnel-outside-ip-address REPLACE_WITH_TUNNEL_IP
PB4 Timeout or refused?
when a connection to something in the VPC fails
nc -vz -w 5 10.0.20.15 5432
Q=$(aws logs start-query --log-group-name /vpc/netlab/flow \ --start-time $(date -d '-15 min' +%s) --end-time $(date +%s) \ --query-string 'filter dstAddr = "10.0.20.15" or srcAddr = "10.0.20.15" | fields @timestamp, srcAddr, srcPort, dstAddr, dstPort, action | sort @timestamp desc | limit 50' \ --query queryId --output text) sleep 10; aws logs get-query-results --query-id $Q --query results --output text
PB5 Subnet out of addresses
when launches fail with InsufficientFreeAddressesInSubnet, or pods stay pending
aws ec2 describe-subnets --subnet-ids $S --query 'Subnets[0].[CidrBlock,AvailableIpAddressCount]' --output text aws ec2 describe-network-interfaces --filters Name=subnet-id,Values=$S \ --query 'NetworkInterfaces[].[InterfaceType,Status]' --output text | sort | uniq -c | sort -rn
aws ec2 associate-vpc-cidr-block --vpc-id $VPC --cidr-block 100.64.0.0/16
Troubleshooting
| Symptom | Cause | Fix | Step |
|---|---|---|---|
| Subnet creation fails with an invalid or conflicting CIDR | block outside the VPC range, or overlapping another subnet | pick a free block inside the VPC range | 1.2 |
| Instance in a public subnet unreachable from the internet | no public address, no 0.0.0.0/0 to the IGW, or Block Public Access in ingress mode | add an Elastic IP or a load balancer; check the route; exclude the subnet | 2.2, 8.5 |
| NAT gateway creation fails | no internet gateway attached, or (zonal) no Elastic IP left | attach the IGW; release unused Elastic IPs | 2.1, 3.1 |
| Private instance offline in Session Manager | ssm or ssmmessages endpoint missing, private DNS off, or the endpoint group without 443 | create both endpoints with private DNS; allow 443 from the VPC | 4.2, 4.3 |
| S3 traffic still billed through the NAT gateway | gateway endpoint not on that subnet’s table, or the bucket is in another Region | add the route table to the endpoint; cross-Region buckets need an interface endpoint or NAT | 4.1 |
| App cannot reach the database though the group allows it | data NACL without the reply rule, or the app outside 10.0.10.0/23 | egress 1024-65535 to the app range | 6.2 |
| Instance Connect Endpoint times out | app group without 22 from the endpoint’s group, or the endpoint still creating | the two rules from 7.4; wait for create-complete | 7.4, 7.5 |
| Flow log created but no log streams | role trust or policy wrong, or no traffic yet | trust vpc-flow-logs.amazonaws.com; wait one aggregation interval | 8.1 |
| Peering expired | not accepted within seven days | request again and accept in the other account or Region | 9.2 |
| Rule naming the peer’s group rejected | peering across Regions, or not yet active | use the peer’s CIDR | 9.5 |
| Hub has no route to the VPC | propagation off, or the attachment not available | enable propagation or add a static route | 10.4 |
| Traffic reaches the VPC through the hub but replies are lost | the subnet’s table has no route back for that prefix | add the prefix with target tgw- | 10.3 |
| Tunnels down after editing options | proposal mismatch with the device | match IKE version, cipher and DH group on the device | 11.2 |
| Tunnel up, no routes learned | wrong BGP ASN or neighbour address, or nothing advertised | compare AcceptedRouteCount with the device’s BGP setup | 11.4 |
| Instance has no IPv6 address in the OS | no address assigned, or DHCPv6 off in the OS | assign one; enable DHCPv6 | 12.4 |
| Clean-up fails with DependencyViolation | interfaces of endpoints, NAT or the Instance Connect Endpoint still releasing | wait a few minutes and run the delete again | 14.8, 14.9 |

0 comments