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

⏱ 90-120 minDificultad: mediaTask statements: 1.23.44.4

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:

  1. Un gateway endpoint de S3 (gratis) con una endpoint policy que solo permite un bucket.
  2. Un interface endpoint (AWS PrivateLink) para AWS STS con DNS privado.
  3. 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.

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.com resuelve a una IP privada de la VPC A y get-caller-identity funciona.
  • 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).


Volver al módulo