Lab práctico · Semana 5: Redes en AWS (I): VPC, conectividad híbrida y costes de red
VPC endpoints (gateway e interface) y VPC peering
Qué vas a construir
Dos VPC completamente privadas (sin internet gateway ni NAT) y vas a darles acceso a servicios de AWS y entre sí sin salir a internet:
- Un gateway endpoint de S3 (gratis) con una endpoint policy que solo permite un bucket.
- Un interface endpoint (AWS PrivateLink) para AWS STS con DNS privado.
- Un VPC peering entre las dos VPC, comprobando que no es transitivo para los endpoints (la VPC B no puede usar el gateway endpoint de la VPC A).
La base (VPC, subredes, instancias, rol IAM y EC2 Instance Connect Endpoints) se despliega con CloudFormation; los endpoints y el peering los creas tú con la CLI.
flowchart LR
s3[("Amazon S3")]
sts["AWS STS"]
subgraph a["VPC A 10.0.0.0/16"]
ia["Instancia A (privada)"]
gw["Gateway endpoint S3<br/>(ruta en la tabla)"]
ie["Interface endpoint STS<br/>(ENI con IP privada)"]
end
subgraph b["VPC B 10.1.0.0/16"]
ib["Instancia B (privada)"]
end
ia --> gw --> s3
ia --> ie --> sts
ia <-- "VPC peering" --> ib
ib -. "no puede usar el endpoint de A (sin edge-to-edge)" .-> gw
Antes de empezar
- Usuario de IAM Identity Center o IAM con MFA (nunca root), región eu-south-2, AWS CloudShell.
- Haber hecho el lab 09 (se da por sabido qué es una tabla de rutas, un SG y el EC2 Instance Connect Endpoint).
export AWS_REGION=eu-south-2
echo "export AWS_REGION=eu-south-2" > ~/lab10.env
Paso 1: desplegar la base con CloudFormation
Crea el fichero lab10.yaml (en CloudShell: nano lab10.yaml, pega y guarda con Ctrl+O, Ctrl+X):
AWSTemplateFormatVersion: "2010-09-09"
Description: Lab 10 - dos VPC privadas con instancias y EC2 Instance Connect Endpoints
Parameters:
AmiId:
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64
Resources:
VpcA:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsSupport: true
EnableDnsHostnames: true
Tags: [{ Key: Name, Value: lab10-vpc-a }]
SubnetA:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VpcA
CidrBlock: 10.0.1.0/24
AvailabilityZone: !Select [0, !GetAZs ""]
Tags: [{ Key: Name, Value: lab10-privada-a }]
RouteTableA:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VpcA
Tags: [{ Key: Name, Value: lab10-rt-a }]
SubnetARouteTable:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties: { SubnetId: !Ref SubnetA, RouteTableId: !Ref RouteTableA }
VpcB:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.1.0.0/16
EnableDnsSupport: true
EnableDnsHostnames: true
Tags: [{ Key: Name, Value: lab10-vpc-b }]
SubnetB:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VpcB
CidrBlock: 10.1.1.0/24
AvailabilityZone: !Select [0, !GetAZs ""]
Tags: [{ Key: Name, Value: lab10-privada-b }]
RouteTableB:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VpcB
Tags: [{ Key: Name, Value: lab10-rt-b }]
SubnetBRouteTable:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties: { SubnetId: !Ref SubnetB, RouteTableId: !Ref RouteTableB }
EiceSgA:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: EICE VPC A
VpcId: !Ref VpcA
EiceSgB:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: EICE VPC B
VpcId: !Ref VpcB
InstanceSgA:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Instancia A
VpcId: !Ref VpcA
SecurityGroupIngress:
- { IpProtocol: tcp, FromPort: 22, ToPort: 22, SourceSecurityGroupId: !Ref EiceSgA }
- { IpProtocol: icmp, FromPort: -1, ToPort: -1, CidrIp: 10.1.0.0/16 }
InstanceSgB:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Instancia B
VpcId: !Ref VpcB
SecurityGroupIngress:
- { IpProtocol: tcp, FromPort: 22, ToPort: 22, SourceSecurityGroupId: !Ref EiceSgB }
- { IpProtocol: icmp, FromPort: -1, ToPort: -1, CidrIp: 10.0.0.0/16 }
EndpointSg:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: HTTPS hacia interface endpoints desde la VPC A
VpcId: !Ref VpcA
SecurityGroupIngress:
- { IpProtocol: tcp, FromPort: 443, ToPort: 443, CidrIp: 10.0.0.0/16 }
EiceA:
Type: AWS::EC2::InstanceConnectEndpoint
Properties:
SubnetId: !Ref SubnetA
SecurityGroupIds: [!Ref EiceSgA]
EiceB:
Type: AWS::EC2::InstanceConnectEndpoint
Properties:
SubnetId: !Ref SubnetB
SecurityGroupIds: [!Ref EiceSgB]
InstanceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal: { Service: ec2.amazonaws.com }
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Roles: [!Ref InstanceRole]
InstanceA:
Type: AWS::EC2::Instance
Properties:
ImageId: !Ref AmiId
InstanceType: t3.micro
SubnetId: !Ref SubnetA
SecurityGroupIds: [!Ref InstanceSgA]
IamInstanceProfile: !Ref InstanceProfile
Tags: [{ Key: Name, Value: lab10-instancia-a }]
InstanceB:
Type: AWS::EC2::Instance
Properties:
ImageId: !Ref AmiId
InstanceType: t3.micro
SubnetId: !Ref SubnetB
SecurityGroupIds: [!Ref InstanceSgB]
IamInstanceProfile: !Ref InstanceProfile
Tags: [{ Key: Name, Value: lab10-instancia-b }]
Outputs:
VpcA: { Value: !Ref VpcA }
VpcB: { Value: !Ref VpcB }
SubnetA: { Value: !Ref SubnetA }
RouteTableA: { Value: !Ref RouteTableA }
RouteTableB: { Value: !Ref RouteTableB }
EndpointSg: { Value: !Ref EndpointSg }
InstanceA: { Value: !Ref InstanceA }
InstanceB: { Value: !Ref InstanceB }
IpB: { Value: !GetAtt InstanceB.PrivateIp }
Despliega (tarda unos 5 minutos, sobre todo por los EC2 Instance Connect Endpoints):
aws cloudformation deploy --stack-name lab10 --template-file lab10.yaml \
--capabilities CAPABILITY_IAM --tags lab=lab10
Carga las salidas en variables:
out () { aws cloudformation describe-stacks --stack-name lab10 \
--query "Stacks[0].Outputs[?OutputKey=='$1'].OutputValue" --output text; }
for k in VpcA VpcB SubnetA RouteTableA RouteTableB EndpointSg InstanceA InstanceB IpB; do
echo "export $k=$(out $k)" >> ~/lab10.env
done
source ~/lab10.env && cat ~/lab10.env
Crea también un bucket de prueba con un fichero, y un segundo bucket «prohibido»:
CUENTA=$(aws sts get-caller-identity --query Account --output text)
BUCKET_OK=lab10-permitido-$CUENTA
BUCKET_NO=lab10-prohibido-$CUENTA
aws s3 mb s3://$BUCKET_OK && aws s3 mb s3://$BUCKET_NO
echo "hola desde S3 por un endpoint privado" > saludo.txt
aws s3 cp saludo.txt s3://$BUCKET_OK/ && aws s3 cp saludo.txt s3://$BUCKET_NO/
echo "export BUCKET_OK=$BUCKET_OK BUCKET_NO=$BUCKET_NO" >> ~/lab10.env
Paso 2: comprobar que la VPC A no llega a S3
Entra en la instancia A (sin IP pública, a través de su EC2 Instance Connect Endpoint):
aws ec2-instance-connect ssh --instance-id $InstanceA --os-user ec2-user --connection-type eice
Dentro de la instancia (la AWS CLI ya viene en Amazon Linux 2023; las credenciales llegan del rol por el servicio de metadatos, que no necesita internet):
export AWS_REGION=eu-south-2
aws s3 ls --cli-connect-timeout 5 --cli-read-timeout 5 || echo "SIN RUTA HACIA S3"
exit
Falla por tiempo de espera, no por permisos: el rol tiene AmazonS3ReadOnlyAccess, pero la VPC no tiene ninguna ruta hacia el endpoint público de S3.
Paso 3: gateway endpoint de S3
source ~/lab10.env
VPCE_S3=$(aws ec2 create-vpc-endpoint --vpc-id $VpcA \
--service-name com.amazonaws.eu-south-2.s3 --vpc-endpoint-type Gateway \
--route-table-ids $RouteTableA \
--query VpcEndpoint.VpcEndpointId --output text)
echo "export VPCE_S3=$VPCE_S3" >> ~/lab10.env
aws ec2 describe-route-tables --route-table-ids $RouteTableA --query 'RouteTables[0].Routes'
Observa la ruta nueva: destino pl-… (la prefix list de S3 en eu-south-2) y target vpce-…. Repite la prueba desde la instancia A:
aws ec2-instance-connect ssh --instance-id $InstanceA --os-user ec2-user --connection-type eice
Dentro de la instancia (sustituye CUENTA por tu ID de cuenta de 12 dígitos):
export AWS_REGION=eu-south-2
aws s3 ls
aws s3 cp s3://lab10-permitido-CUENTA/saludo.txt -
exit
aws s3 ls y la lectura del objeto funcionan: el tráfico va a S3 por la red de AWS, sin internet gateway ni NAT, y gratis.
Paso 4: endpoint policy que solo permite un bucket
Las endpoint policies son políticas de recurso que limitan qué se puede hacer a través de ese endpoint, aunque el rol IAM permita más. Crea politica-endpoint.json sustituyendo el nombre del bucket:
cat > politica-endpoint.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "SoloBucketPermitido",
"Effect": "Allow",
"Principal": "*",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::$BUCKET_OK",
"arn:aws:s3:::$BUCKET_OK/*"
]
}
]
}
EOF
cat politica-endpoint.json
aws ec2 modify-vpc-endpoint --vpc-endpoint-id $VPCE_S3 --policy-document file://politica-endpoint.json
Prueba desde la instancia A (sustituye CUENTA por tu ID de cuenta):
aws ec2-instance-connect ssh --instance-id $InstanceA --os-user ec2-user --connection-type eice
export AWS_REGION=eu-south-2
aws s3 ls s3://lab10-permitido-CUENTA/
aws s3 ls s3://lab10-prohibido-CUENTA/ || echo "DENEGADO POR LA ENDPOINT POLICY"
aws s3 ls || echo "ListBuckets tambien denegado"
exit
El bucket permitido se lista; el prohibido y ListBuckets dan AccessDenied, aunque el rol IAM tenga lectura de todo S3. Para que el permiso efectivo exista, tanto la política IAM como la endpoint policy (y la bucket policy, si la hay) deben permitirlo.
Paso 5: interface endpoint para AWS STS (PrivateLink)
Ahora un servicio sin gateway endpoint. Creamos un interface endpoint en la subred de la VPC A con DNS privado activado (por eso la plantilla habilitó EnableDnsSupport y EnableDnsHostnames):
source ~/lab10.env
VPCE_STS=$(aws ec2 create-vpc-endpoint --vpc-id $VpcA --vpc-endpoint-type Interface \
--service-name com.amazonaws.eu-south-2.sts \
--subnet-ids $SubnetA --security-group-ids $EndpointSg --private-dns-enabled \
--query VpcEndpoint.VpcEndpointId --output text)
echo "export VPCE_STS=$VPCE_STS" >> ~/lab10.env
until [ "$(aws ec2 describe-vpc-endpoints --vpc-endpoint-ids $VPCE_STS --query 'VpcEndpoints[0].State' --output text)" = "available" ]; do
echo "esperando al endpoint..."; sleep 20
done
date
El endpoint suele tardar un par de minutos en pasar de pending a available. Después, desde la instancia A:
aws ec2-instance-connect ssh --instance-id $InstanceA --os-user ec2-user --connection-type eice
export AWS_REGION=eu-south-2
getent hosts sts.eu-south-2.amazonaws.com
aws sts get-caller-identity
exit
getent devuelve una IP privada 10.0.1.x: con DNS privado, el nombre público del servicio resuelve a la interfaz de red del endpoint dentro de la VPC, sin cambiar nada en la aplicación. Y get-caller-identity ya responde (con el rol de la instancia).
Mira la interfaz de red que ha creado el endpoint:
aws ec2 describe-network-interfaces --filters Name=vpc-id,Values=$VpcA Name=interface-type,Values=vpc_endpoint \
--query 'NetworkInterfaces[].[PrivateIpAddress,AvailabilityZone,Description]' --output table
Paso 6: VPC peering entre A y B
source ~/lab10.env
PCX=$(aws ec2 create-vpc-peering-connection --vpc-id $VpcA --peer-vpc-id $VpcB \
--tag-specifications 'ResourceType=vpc-peering-connection,Tags=[{Key=Name,Value=lab10-peering}]' \
--query VpcPeeringConnection.VpcPeeringConnectionId --output text)
aws ec2 accept-vpc-peering-connection --vpc-peering-connection-id $PCX
echo "export PCX=$PCX" >> ~/lab10.env
aws ec2 create-route --route-table-id $RouteTableA --destination-cidr-block 10.1.0.0/16 --vpc-peering-connection-id $PCX
aws ec2 create-route --route-table-id $RouteTableB --destination-cidr-block 10.0.0.0/16 --vpc-peering-connection-id $PCX
Fíjate en los tres pasos: solicitar, aceptar y enrutar en ambos lados. Aceptar sin rutas no sirve de nada. Los security groups de la plantilla ya permiten ICMP desde la otra VPC.
Prueba desde la instancia A:
echo $IpB
aws ec2-instance-connect ssh --instance-id $InstanceA --os-user ec2-user --connection-type eice
ping -c 3 IP_DE_B
exit
(sustituye IP_DE_B por el valor de $IpB). Responde: las dos VPC se comunican por IP privadas.
Paso 7: el peering no comparte los endpoints (sin edge-to-edge)
La VPC B no tiene endpoint de S3. ¿Puede usar el de la VPC A a través del peering? Entra en la instancia B:
aws ec2-instance-connect ssh --instance-id $InstanceB --os-user ec2-user --connection-type eice
export AWS_REGION=eu-south-2
aws s3 ls --cli-connect-timeout 5 --cli-read-timeout 5 || echo "B NO PUEDE USAR EL GATEWAY ENDPOINT DE A"
exit
No puede. El gateway endpoint funciona mediante rutas en las tablas de la VPC A; el peering no admite enrutamiento de borde a borde: B tampoco podría usar un internet gateway, un NAT, una VPN o un Direct Connect de A. Un interface endpoint, en cambio, es una IP privada: B podría alcanzarlo por el peering si configuraras la resolución DNS (por ejemplo con una zona privada de Route 53 asociada a la VPC B). Es el patrón de «endpoints centralizados» en una VPC de servicios compartidos, normalmente con Transit Gateway.
Comprueba que funciona
- Sin endpoint, la instancia A no llegaba a S3 (tiempo de espera, no permisos).
- Con el gateway endpoint aparece una ruta
pl-… → vpce-…y la instancia A lee de S3. - La endpoint policy permite el bucket «permitido» y deniega el «prohibido» y
ListBuckets. -
sts.eu-south-2.amazonaws.comresuelve a una IP privada de la VPC A yget-caller-identityfunciona. - A hace ping a B por el peering; B no puede usar el gateway endpoint de A.
Limpieza
Borra primero lo que creaste a mano (el peering y los endpoints dependen de la pila) y después la pila:
source ~/lab10.env
aws ec2 delete-route --route-table-id $RouteTableA --destination-cidr-block 10.1.0.0/16
aws ec2 delete-route --route-table-id $RouteTableB --destination-cidr-block 10.0.0.0/16
aws ec2 delete-vpc-peering-connection --vpc-peering-connection-id $PCX
aws ec2 delete-vpc-endpoints --vpc-endpoint-ids $VPCE_S3 $VPCE_STS
Espera a que el interface endpoint desaparezca (su interfaz de red bloquea el borrado del security group y la subred):
aws ec2 describe-vpc-endpoints --filters Name=vpc-id,Values=$VpcA --query 'VpcEndpoints[].[VpcEndpointId,State]'
Cuando la lista esté vacía, vacía y borra los buckets y elimina la pila:
aws s3 rb s3://$BUCKET_OK --force
aws s3 rb s3://$BUCKET_NO --force
aws cloudformation delete-stack --stack-name lab10
aws cloudformation wait stack-delete-complete --stack-name lab10
rm -f ~/lab10.env lab10.yaml politica-endpoint.json saludo.txt
Comprueba que no queda ningún endpoint de interfaz cobrando:
aws ec2 describe-vpc-endpoints --filters Name=vpc-endpoint-type,Values=Interface --query 'VpcEndpoints[].VpcEndpointId'
Preguntas para pensar como arquitecto
1. El centro de datos, conectado por Direct Connect a la VPC A, necesita leer de S3 de forma privada. ¿Sirve el gateway endpoint de este lab?
No. Los gateway endpoints solo sirven para tráfico que se origina dentro de la VPC que los tiene en sus tablas de rutas. Desde on-premises hay que usar un interface endpoint de S3 (IP privadas accesibles por VPN o Direct Connect) o una public VIF de Direct Connect hacia los endpoints públicos de S3.
2. Tienes 20 VPC que necesitan acceso privado a Secrets Manager, KMS y SQS en 3 AZ. ¿Cuántos interface endpoints pagarías si cada VPC tiene los suyos? ¿Qué alternativa hay?
20 VPC × 3 servicios × 3 AZ = 180 horas de endpoint por cada hora de reloj. La alternativa es centralizar los interface endpoints en una VPC de servicios compartidos conectada por Transit Gateway y compartir la resolución DNS con zonas privadas de Route 53 (o Route 53 Resolver). Pagas el TGW (por adjunto y GB), así que conviene hacer números con la AWS Pricing Calculator.
3. ¿Por qué, sin endpoint, el comando fallaba por tiempo de espera y no por «AccessDenied»? ¿Qué te dice eso al diagnosticar en el examen?
Porque el problema era de red (no había ruta), no de permisos. Un timeout apunta a rutas, security groups o NACL; un AccessDenied apunta a IAM, bucket policy, endpoint policy o SCP. En los enunciados, «la conexión se agota» sugiere rutas/SG/NACL/endpoints; «acceso denegado», políticas.
4. Un cliente quiere exponer un servicio interno a 200 cuentas de clientes con rangos IP que se solapan con el suyo. ¿Peering o PrivateLink?
AWS PrivateLink: el proveedor publica el servicio detrás de un Network Load Balancer como endpoint service y cada cliente crea un interface endpoint en su VPC. Funciona con CIDR solapados, el tráfico es solo del consumidor al servicio y no hace falta gestionar 200 peerings (que además no admiten solapamiento).