setup / 3 · consumer

Consumer

Work in the consumer account. The consumer VPC and private subnets already exist — create the subscriber EC2, attach the interface endpoint to the provider service, and install measure tools. Validate after each step (fictitious sample output below — replace IDs with yours). Measuring both paths is the next page.

Which account

   
Account role Consumer spoke (subscriber host for Measure)
AWS CLI profile consumer
Already exists Consumer VPC, private subnets (workloads + interface ENIs)
What you create Subscriber EC2 (+ SG), VPCE SG, interface VPC endpoint
From provider $SVC_NAME, $RELAY_PRIVATE_IP

Confirm you are in the right account, then set network IDs and provider outputs:

export AWS_PROFILE=consumer
export AWS_REGION=ap-southeast-6   # same Region as the provider

aws sts get-caller-identity

# Existing consumer spoke (example IDs — use yours)
export CONSUMER_VPC_ID=vpc-0c0c0c0c0c0c0c0c0

# private — subscriber EC2 (one AZ)
export CONSUMER_PRIVATE_SUBNET_ID=subnet-0ccc1111dddd2222

# private — interface endpoint ENIs (2+ AZs)
export CONSUMER_VPCE_SUBNET_IDS=subnet-0ccc1111dddd2222,subnet-0ccc3333eeee4444,subnet-0ccc5555ffff6666

# From Provider (fictitious — use yours)
export SVC_NAME=com.amazonaws.vpce.ap-southeast-6.vpce-svc-0aaaa1111bbbb2222
export RELAY_PRIVATE_IP=10.50.1.10

Validate — a different account ID than shared-services (fictitious):

{
    "UserId": "AROAEXAMPLEID:you@example.com",
    "Account": "444455556666",
    "Arn": "arn:aws:sts::444455556666:assumed-role/AWSReservedSSO_AdministratorAccess_example/you@example.com"
}

Interface endpoint hours accrue per AZ while the VPCE exists. Subscriber EC2 is billable too. Tag with demo=pl-vs-inspected-tgw so Teardown is easy to find.

Subscriber EC2

Still with AWS_PROFILE=consumer. Launch in a private subnet with SSM (no public IP). NAT is only needed if the host must reach the public internet; the measure step itself talks to PrivateLink ENIs and the provider private IP via TGW.

SSM interface endpoints

Same rule as provider: need ssm and ssmmessages available in this VPC. ec2messages may not exist in the Region.

aws ec2 describe-vpc-endpoints \
  --profile consumer --region "$AWS_REGION" \
  --filters "Name=vpc-id,Values=$CONSUMER_VPC_ID" \
  --query 'VpcEndpoints[?contains(ServiceName, `ssm`)].{ServiceName:ServiceName,State:State}' \
  --output table

Validate:

---------------------------------------------------------------
|                   DescribeVpcEndpoints                      |
+------------------------------------------------+------------+
|  com.amazonaws.ap-southeast-6.ssm              |  available |
|  com.amazonaws.ap-southeast-6.ssmmessages      |  available |
+------------------------------------------------+------------+

If ssm is missing, create an interface endpoint tagged for this lab (reuse your existing endpoint security group for HTTPS to AWS APIs; do not weaken it):

aws ec2 create-vpc-endpoint \
  --profile consumer --region "$AWS_REGION" \
  --vpc-id "$CONSUMER_VPC_ID" \
  --vpc-endpoint-type Interface \
  --service-name "com.amazonaws.${AWS_REGION}.ssm" \
  --subnet-ids $(echo "$CONSUMER_VPCE_SUBNET_IDS" | tr ',' ' ') \
  --security-group-ids "$EXISTING_ENDPOINT_SG_ID" \
  --private-dns-enabled \
  --tag-specifications 'ResourceType=vpc-endpoint,Tags=[{Key=demo,Value=pl-vs-inspected-tgw},{Key=Name,Value=plvtgw-ssm}]'

Poll until State=available — do not rely on aws ec2 wait vpc-endpoint-available (that waiter is not always present). Teardown removes only Name=plvtgw-ssm.

Security groups

Consumer security groups Create plvtgw-subscriber for the EC2 host and plvtgw-vpce for the interface endpoint. Allow TCP 9000 from the subscriber SG into the endpoint SG only. Consumer — security groups create · TCP 9000 · SG to SG Consumer VPC private (exists) plvtgw-subscriber instance SG plvtgw-vpce endpoint SG :9000

Dashed boundaries = security groups. Endpoint SG allows TCP 9000 from the subscriber SG only.

Create two harness security groups: one for the subscriber instance, one for the PrivateLink interface endpoint.

CONSUMER_INSTANCE_SG_ID=$(aws ec2 create-security-group \
  --profile consumer --region "$AWS_REGION" \
  --group-name plvtgw-subscriber \
  --description "pl-vs-inspected-tgw consumer subscriber EC2" \
  --vpc-id "$CONSUMER_VPC_ID" \
  --tag-specifications 'ResourceType=security-group,Tags=[{Key=demo,Value=pl-vs-inspected-tgw},{Key=Name,Value=plvtgw-subscriber}]' \
  --query 'GroupId' --output text)

CONSUMER_ENDPOINT_SG_ID=$(aws ec2 create-security-group \
  --profile consumer --region "$AWS_REGION" \
  --group-name plvtgw-vpce \
  --description "pl-vs-inspected-tgw consumer interface endpoint :9000" \
  --vpc-id "$CONSUMER_VPC_ID" \
  --tag-specifications 'ResourceType=security-group,Tags=[{Key=demo,Value=pl-vs-inspected-tgw},{Key=Name,Value=plvtgw-vpce}]' \
  --query 'GroupId' --output text)

aws ec2 authorize-security-group-ingress \
  --profile consumer --region "$AWS_REGION" \
  --group-id "$CONSUMER_ENDPOINT_SG_ID" \
  --ip-permissions "IpProtocol=tcp,FromPort=9000,ToPort=9000,UserIdGroupPairs=[{GroupId=${CONSUMER_INSTANCE_SG_ID},Description=from subscriber}]"

echo "CONSUMER_INSTANCE_SG_ID=$CONSUMER_INSTANCE_SG_ID"
echo "CONSUMER_ENDPOINT_SG_ID=$CONSUMER_ENDPOINT_SG_ID"

Validate — endpoint SG allows TCP 9000 from the subscriber SG only:

aws ec2 describe-security-groups \
  --profile consumer --region "$AWS_REGION" \
  --group-ids "$CONSUMER_ENDPOINT_SG_ID" \
  --query 'SecurityGroups[0].{GroupId:GroupId,GroupName:GroupName,Ingress:IpPermissions}' \
  --output json
{
    "GroupId": "sg-0eeee3333ffff4444",
    "GroupName": "plvtgw-vpce",
    "Ingress": [
        {
            "IpProtocol": "tcp",
            "FromPort": 9000,
            "ToPort": 9000,
            "UserIdGroupPairs": [
                {
                    "Description": "from subscriber",
                    "GroupId": "sg-0cccc1111dddd2222"
                }
            ]
        }
    ]
}

IAM instance profile (SSM)

Consumer IAM instance profile for SSM Create an IAM role trusted by ec2.amazonaws.com, attach AmazonSSMManagedInstanceCore, and expose it as an instance profile for the subscriber EC2. Consumer — IAM instance profile SSM Session Manager IAM Role plvtgw-ec2-ssm Systems Manager AmazonSSMManagedInstanceCore EC2 profile instance profile

Role + AmazonSSMManagedInstanceCore → instance profile attached at launch.

Same pattern as provider — create once in the consumer account if missing:

aws iam create-role \
  --profile consumer \
  --role-name plvtgw-ec2-ssm \
  --assume-role-policy-document '{
    "Version":"2012-10-17",
    "Statement":[{
      "Effect":"Allow",
      "Principal":{"Service":"ec2.amazonaws.com"},
      "Action":"sts:AssumeRole"
    }]
  }'

aws iam attach-role-policy \
  --profile consumer \
  --role-name plvtgw-ec2-ssm \
  --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore

aws iam create-instance-profile \
  --profile consumer \
  --instance-profile-name plvtgw-ec2-ssm

aws iam add-role-to-instance-profile \
  --profile consumer \
  --instance-profile-name plvtgw-ec2-ssm \
  --role-name plvtgw-ec2-ssm

aws iam tag-role \
  --profile consumer \
  --role-name plvtgw-ec2-ssm \
  --tags Key=demo,Value=pl-vs-inspected-tgw

sleep 15

Validate:

aws iam get-instance-profile \
  --profile consumer \
  --instance-profile-name plvtgw-ec2-ssm \
  --query 'InstanceProfile.{Name:InstanceProfileName,Roles:Roles[0].RoleName}' \
  --output json
{
    "Name": "plvtgw-ec2-ssm",
    "Roles": "plvtgw-ec2-ssm"
}

Launch the instance

Launch consumer subscriber EC2 Launch Amazon Linux 2023 in a private subnet with the subscriber security group and IAM instance profile, no public IP. Manage the host with SSM Session Manager. Consumer — launch subscriber EC2 private · no public IP · SSM Consumer VPC private plvtgw-subscriber SSM start-session start-session

EC2 in private; SSM for access — no public IP. Measure traffic stays private (VPCE / TGW).

AMI_ID=$(aws ssm get-parameters \
  --profile consumer --region "$AWS_REGION" \
  --names /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 \
  --query 'Parameters[0].Value' --output text)

CONSUMER_INSTANCE_ID=$(aws ec2 run-instances \
  --profile consumer --region "$AWS_REGION" \
  --image-id "$AMI_ID" \
  --instance-type t3.micro \
  --subnet-id "$CONSUMER_PRIVATE_SUBNET_ID" \
  --security-group-ids "$CONSUMER_INSTANCE_SG_ID" \
  --iam-instance-profile Name=plvtgw-ec2-ssm \
  --no-associate-public-ip-address \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=plvtgw-subscriber},{Key=demo,Value=pl-vs-inspected-tgw}]' \
  --query 'Instances[0].InstanceId' --output text)

aws ec2 wait instance-running \
  --profile consumer --region "$AWS_REGION" \
  --instance-ids "$CONSUMER_INSTANCE_ID"

echo "CONSUMER_INSTANCE_ID=$CONSUMER_INSTANCE_ID"

Validate — instance running, then SSM Online:

aws ec2 describe-instances \
  --profile consumer --region "$AWS_REGION" \
  --instance-ids "$CONSUMER_INSTANCE_ID" \
  --query 'Reservations[0].Instances[0].{InstanceId:InstanceId,State:State.Name,PrivateIp:PrivateIpAddress}' \
  --output json

aws ssm describe-instance-information \
  --profile consumer --region "$AWS_REGION" \
  --filters "Key=InstanceIds,Values=$CONSUMER_INSTANCE_ID" \
  --query 'InstanceInformationList[0].{InstanceId:InstanceId,PingStatus:PingStatus,PlatformName:PlatformName}' \
  --output json
{
    "InstanceId": "i-0cccc1111dddd2222",
    "State": "running",
    "PrivateIp": "10.60.3.20"
}
{
    "InstanceId": "i-0cccc1111dddd2222",
    "PingStatus": "Online",
    "PlatformName": "Amazon Linux"
}
aws ssm start-session \
  --profile consumer --region "$AWS_REGION" \
  --target "$CONSUMER_INSTANCE_ID"

Interface endpoint

Requires $SVC_NAME from Provider.

Consumer interface VPC endpoint In the consumer VPC private subnets, create an interface endpoint for the provider endpoint service. The subscriber reaches TCP 9000 on the local ENI; PrivateLink carries traffic to the provider NLB. Consumer — interface endpoint PrivateLink · TCP 9000 Consumer VPC · private subscriber Interface EP plvtgw-pl :9000 PrivateLink NLB :9000 endpoint service

Subscriber → local interface ENI (:9000) → PrivateLink → provider NLB / endpoint service.

Create the interface endpoint

EP_ID=$(aws ec2 create-vpc-endpoint \
  --profile consumer --region "$AWS_REGION" \
  --vpc-endpoint-type Interface \
  --service-name "$SVC_NAME" \
  --vpc-id "$CONSUMER_VPC_ID" \
  --subnet-ids $(echo "$CONSUMER_VPCE_SUBNET_IDS" | tr ',' ' ') \
  --security-group-ids "$CONSUMER_ENDPOINT_SG_ID" \
  --tag-specifications 'ResourceType=vpc-endpoint,Tags=[{Key=demo,Value=pl-vs-inspected-tgw},{Key=Name,Value=plvtgw-pl}]' \
  --query 'VpcEndpoint.VpcEndpointId' --output text)

echo "EP_ID=$EP_ID"

Wait until available

Poll (do not use the missing vpc-endpoint-available waiter):

for i in $(seq 1 48); do
  st=$(aws ec2 describe-vpc-endpoints \
    --profile consumer --region "$AWS_REGION" \
    --vpc-endpoint-ids "$EP_ID" \
    --query 'VpcEndpoints[0].State' --output text)
  echo "State=$st"
  [[ "$st" == available ]] && break
  sleep 5
done

Validate:

State=pending
…
State=available
PL_DNS=$(aws ec2 describe-vpc-endpoints \
  --profile consumer --region "$AWS_REGION" \
  --vpc-endpoint-ids "$EP_ID" \
  --query 'VpcEndpoints[0].DnsEntries[0].DnsName' --output text)

echo "PL_DNS=$PL_DNS"

Validate (fictitious hostname):

PL_DNS=vpce-0eeee3333ffff4444-abcd1234.vpce-svc-0aaaa1111bbbb2222.ap-southeast-6.vpce.amazonaws.com

Measure tools on the consumer host

Install on the instance (SSM session) — needed for Measure:

sudo dnf install -y tmux nmap-ncat nmap pv
command -v nc tmux pv nping

Validate:

/usr/bin/nc
/usr/bin/tmux
/usr/bin/pv
/usr/bin/nping

Optional: sub.py

Only if you want a Comparison one-liner later. Interactive Measure does not need it.

Copy from the laptop (same base64 paste pattern as Provider relay.py), or:

# laptop
base64 -w0 scripts/sub.py
# on the instance
sudo mkdir -p /opt/plvtgw
printf '%s' '…paste sub.py b64…' | base64 -d | sudo tee /opt/plvtgw/sub.py >/dev/null
sudo chmod 755 /opt/plvtgw/sub.py

Validate:

ls -l /opt/plvtgw/sub.py
python3 -m py_compile /opt/plvtgw/sub.py && echo ok
-rwxr-xr-x. 1 root root … /opt/plvtgw/sub.py
ok

Keep $EP_ID, $PL_DNS, $CONSUMER_INSTANCE_ID, $RELAY_PRIVATE_IP, and the two SGs for Measure and Teardown.