AWS 공식 문서가 아닙니다. 개인 실습 기록이며 AWS의 입장을 대변하지 않습니다(기준일 2026-10-25). 정확한 정보는 AWS 공식 문서를 확인하세요. 기능 설명은 기능 정리, 실행 결과는 테스트 결과를 참고하세요. 이 문서의 절차와 기대 결과는 2026-10-25 실습 결과를 반영했습니다. T14(비용 확인)는 아직 결과가 없습니다.
Client VPN 엔드포인트를 Transit Gateway에 직접 연결했을 때 클라이언트 IP가 SNAT 없이 보존되는지 확인하고, 문서에 적힌 동작과 제약을 실제 환경에서 검증합니다.
| 범위 | 항목 |
|---|---|
| 포함 | IPv4, 상호 인증서 인증, 단일 계정, 서울 리전, 소스 IP·포트 보존, VPC 연결 방식과의 비교, 보안 그룹, 분할/전체 터널, 권한 부여 규칙, 다중 VPC, TGW 복귀 경로, 흐름 로그·연결 로그, DNS, 제약 사항(음성 테스트) |
| 제외 | IPv6·듀얼 스택, Active Directory·SAML 그룹 권한, RAM 교차 계정, 온프레미스 연결, AZ 추가·제거와 같은 운영·복원력 테스트 |

ap-northeast-2cvpn-lab-*과 스크립트로 새로 만들고 Project=cvpn-lab 태그를 붙입니다. 기본 VPC도 쓰지 않습니다.| 대상 | CIDR | 비고 |
|---|---|---|
| cvpn-tgw 클라이언트 CIDR | 10.100.0.0/22 | 검증 대상(TGW 연결형, AZ 2a·2c) |
| cvpn-legacy 클라이언트 CIDR | 10.104.0.0/22 | 비교 대상(VPC 연결형) |
| Transit Gateway CIDR 블록 | 10.255.0.0/24 | TGW 연결형 엔드포인트에 필수 |
| VPC-A(workload) | 10.10.0.0/16 | app-a 10.10.1.10 |
| VPC-B(workload) | 10.20.0.0/16 | app-b 10.20.1.10 |
| Egress VPC | 10.30.0.0/16 | NAT 게이트웨이(전체 터널, 패키지 설치, SSM) |
| Legacy hosting VPC | 10.40.0.0/16 | cvpn-legacy ENI 서브넷 10.40.1.0/24 |
| Client-sim VPC | 192.168.100.0/24 | TGW 미연결, 인터넷으로 엔드포인트 접속 |
| 스택/스크립트 | 만드는 자원 |
|---|---|
scripts/10-certs.sh |
실습용 CA, 서버 인증서, client1·client2 인증서(로컬 pki/), ACM에 서버 인증서 가져오기 |
cvpn-lab-network (infra/network.yaml) |
TGW(기본 연결·전파 끔), TGW 라우팅 테이블 4개, VPC 5개, TGW VPC 연결 4개, NAT 게이트웨이, EC2 Instance Connect Endpoint(EICE) 3개, 보안 그룹, IAM 역할(SSM, 흐름 로그), TGW·VPC-A 흐름 로그, EC2 4대 |
cvpn-lab-endpoint-tgw (infra/endpoint-tgw.yaml) + scripts/30-endpoint-tgw.sh |
TGW 연결형 엔드포인트, 연결 로그, 권한 부여(VPC-A), 라우트(VPC-A·B). client-vpn 연결을 rt-cvpn에 연결하고 rt-spoke·rt-egress에 전파 |
cvpn-lab-endpoint-legacy (infra/endpoint-legacy.yaml) |
VPC 연결형 엔드포인트(2a 서브넷 1개), 엔드포인트 SG, 연결 로그 |
| 라우팅 테이블 | 연결(association) | 전파(propagation) | 정적 라우트 |
|---|---|---|---|
| rt-cvpn | client-vpn 연결 | VPC-A, VPC-B | 0.0.0.0/0 → Egress, 10.0.0.0/8 블랙홀 |
| rt-spoke | VPC-A, VPC-B | client-vpn, Legacy | 0.0.0.0/0 → Egress, 10.0.0.0/8 블랙홀 |
| rt-legacy | Legacy | VPC-A | 10.0.0.0/8 블랙홀 |
| rt-egress | Egress | client-vpn, VPC-A, VPC-B, Legacy | - |
client-vpn 연결은 10.100.0.0/22와 함께 /25 5개를 전파합니다(실측).| 호스트 | 역할 | 확인 수단 |
|---|---|---|
| app-a, app-b (AL2023) | 목적지 서버 | nginx가 seen_src=<IP>:<port>로 응답하고 /var/log/nginx/cvpn.log에 기록, tcpdump |
| client1, client2 (Ubuntu 24.04) | VPN 클라이언트 | OpenVPN(scripts/40-vpn.sh), curl, dig |
scripts/ssm.sh <host> '<cmd>')로 합니다.scripts/eice.sh <host> '<cmd>'). EICE는 VPC 안에서 TCP 22로 접속하므로 VPN이 가져간 기본 경로의 영향을 받지 않고, 추가 요금도 없습니다.| 항목 | 수량 | 시간당 |
|---|---|---|
| Client VPN 엔드포인트 연결 | cvpn-tgw 2 AZ + cvpn-legacy 1 | $0.30 |
| Client VPN 연결 | 최대 2 | $0.10 |
| TGW VPC 연결 | 4 | $0.28 |
| TGW client-vpn 연결 | 1 | 최대 $0.07(과금 여부 미확인, T14) |
| NAT 게이트웨이 | 1 | $0.059 |
| EC2 t4g.small | 4 | $0.083 |
| 퍼블릭 IPv4 주소 | 3(NAT EIP, client1·client2) | $0.015 |
| EICE | 3 | 무료 |
| 합계 | 약 $0.84~0.91 |
CloudWatch Logs 수집·저장 요금(흐름 로그, 연결 로그)과 TGW 데이터 처리 요금은 실습 트래픽 규모에서 미미해 합계에서 뺐습니다.
사전 조건: AWS CLI v2(2.36 이상), jq, openssl, cfn-lint, 실습 계정의 CLI 프로필. 스크립트는 AWS_PROFILE_NAME(기본 default)과 EXPECTED_ACCOUNT(배포할 계정 ID, 필수) 환경 변수를 씁니다. 배포 스크립트는 커밋·push되지 않은 변경이 있으면 멈춥니다.
| 단계 | 명령 | 완료 기준 | 실측 소요 |
|---|---|---|---|
| 0 | cfn-lint infra/*.yaml |
오류 없음 | - |
| 1 | scripts/10-certs.sh |
pki/server-cert-arn 생성 |
1분 |
| 2 | scripts/20-network.sh |
스택 CREATE_COMPLETE, EC2 4대 SSM 온라인 |
10분 |
| 3 | scripts/30-endpoint-tgw.sh |
엔드포인트 available, routing ready 출력 |
44분(엔드포인트 22분, 라우트 누락 복구 포함) |
| 4 | scripts/35-endpoint-legacy.sh |
엔드포인트 available |
3단계와 병렬 |
| 5 | scripts/ssm.sh app-a 'curl -s localhost' |
server=app-a ... 응답 |
- |
실습에서 확인한 주의 사항
available이 되기까지 약 20분 걸립니다(상태 코드는 pending). 라우트와 권한 부여 규칙도 하나에 1.5~8분 걸립니다.endpoint-tgw.yaml에서 RouteVpcB가 RouteVpcA에 DependsOn하도록 순서를 정했습니다. 이 수정의 효과는 아직 검증하지 않았습니다. 3단계가 라우트를 기다리며 멈추면 describe-client-vpn-routes로 확인하고, 빠진 라우트를 CLI로 다시 만듭니다.40-vpn.sh는 클라이언트 인증서 키를 SSM Run Command 매개변수로 보냅니다. 계정 안 명령 기록에 남지만 30일짜리 실습 전용 CA라 허용합니다.명령은 저장소 루트에서 source scripts/env.sh 뒤에 실행합니다.
EP=$(stack_out endpoint-tgw EndpointId); TGW=$(stack_out network TransitGatewayId)
ATT=$(aws_ ec2 describe-transit-gateway-attachments --filters Name=resource-id,Values=$EP \
--query 'TransitGatewayAttachments[0].TransitGatewayAttachmentId' --output text)
Project=cvpn-lab 태그와 cvpn-lab- 이름을 붙입니다. scripts/90-cleanup.sh는 이 태그와 이름으로 찾아 지웁니다.ip -4 -br addr show tun0으로 다시 확인합니다.| ID | 테스트 | 절차 | 기대 결과(실측 기준) |
|---|---|---|---|
| T01 | 엔드포인트 생성과 client-vpn 연결 | 구축 3단계 동안 describe-client-vpn-endpoints, describe-transit-gateway-attachments --filters Name=resource-type,Values=client-vpn, describe-network-interfaces --filters Name=description,Values="*$EP*", search-transit-gateway-routes(rt-spoke) |
pending → available. VpcId·SecurityGroupIds 비어 있음. TransitGatewayConfiguration에 TGW ID, 연결 ID, AZ. ResourceType=client-vpn 연결 자동 생성. 고객 VPC에 엔드포인트 ENI 없음. rt-spoke에 클라이언트 CIDR 전파 |
| T02 | CLI 매개변수와 AZ 기본값 | ① 문서 예시 --transit-gateway-id ② --transit-gateway-configuration TransitGatewayId=$TGW(AZ 미지정)로 생성, AZ 확인 후 삭제 |
① Unknown options(문서 예시의 --availability-zone-list도 실제 CLI에 없음) ② AZ 2개 자동 선택(AvailabilityZoneIds로 표시) |
| T03 | 소스 IP 보존 | 40-vpn.sh client1 tgw up → app-a에서 tcpdump -nni ens5 "net 10.100.0.0/22" → client1에서 curl http://10.10.1.10/ → nginx 로그 |
응답 seen_src, nginx 로그, tcpdump 소스가 모두 client1의 tun IP |
| T04 | VPC 연결 방식 비교 | 40-vpn.sh client2 legacy up → curl http://10.10.1.10/ |
seen_src가 cvpn-legacy ENI IP(10.40.1.x) |
| T05 | 동시 접속과 소스 포트 | 두 클라이언트에서 curl --local-port 40000 http://10.10.1.10/를 tgw·legacy로 각각 |
tgw: 클라이언트별 IP와 포트 40000 보존. legacy: ENI IP로 변환. 문서상 동시 사용자가 같은 대상에 접속하면 PAT가 일어날 수 있음 [D2] |
| T06 | 보안 그룹 | ① sg-app-a HTTP 규칙을 client1 IP/32로 좁힘 → 두 클라이언트 curl → 원복 ② create-client-vpn-endpoint에 TGW 설정과 --vpc-id/--security-group-ids를 함께 지정 |
① client1만 성공 ② 거부(실측은 검증 오류가 아닌 InternalError) |
| T07 | 분할·전체 터널 | ① 라우트 10.20.0.0/16 삭제 → 재접속 → 클라이언트 라우트 확인 → 대상 서브넷 없이 재생성 ② --no-split-tunnel + 권한 규칙 0.0.0.0/0 + 라우트 0.0.0.0/0 → 재접속 → curl https://checkip.amazonaws.com → 원복. 전체 터널에서는 SSM이 끊길 수 있으므로 EICE로 실행하거나, 연결·확인·해제를 한 명령으로 묶어 실행 |
① 삭제 후 클라이언트 라우트에서 빠짐 ② 출구 IP = NAT EIP. 0.0.0.0/0 라우트가 없으면 인터넷 불가 |
| T08 | 권한 부여와 다중 VPC | ① 미승인 VPC-B로 curl http://10.20.1.10/ ② 10.20.0.0/16 승인 → app-a·app-b 호출 |
① 시간 초과 ② 두 서버 모두 같은 client IP로 응답 |
| T09 | TGW 복귀 경로·분할 | ① rt-spoke에서 $ATT 전파 해제 → curl → 재전파 ② rt-cvpn에서 VPC-B 전파 해제 → app-b 호출 → 재전파 ③ TGW 흐름 로그 packets-lost-blackhole |
① 실패 후 즉시 복구 ② 실패 후 복구 ③ ①은 app-a 응답이 VPC-A 연결에서, ②는 요청이 client-vpn 연결에서 블랙홀 손실 |
| T10 | 흐름 로그·연결 로그 | TGW 흐름 로그(/cvpn-lab/tgw-flow), VPC-A 흐름 로그(/cvpn-lab/vpc-a-flow), 연결 로그(/cvpn-lab/cvpn-tgw-connection), describe-client-vpn-connections |
흐름 로그 srcaddr가 client IP, 연결 로그·연결 목록의 common-name과 IP·시각으로 사용자 특정. 연결 로그는 약 17~23분 늦게 들어옴 |
| T11 | DNS | client에서 dig @10.10.0.2 amazon.com, dig @10.10.0.2 ip-10-10-1-10.ap-northeast-2.compute.internal(10.10.0.0/16 승인 상태) |
실측: VPC 리졸버가 응답하고 사설 이름도 해석(블로그 설명과 다름) |
| T12 | 제약 음성 테스트 | a TGW 엔드포인트에 서브넷 연결 / b 클라이언트 CIDR이 VPC-A와 겹침 / c TGW CIDR과 겹침 / d CIDR 없는 TGW / e /23 클라이언트 CIDR / f 다른 리전 TGW | a·d·e·f는 명확한 오류로 거부. b·c는 거부되지 않고 약 20분 뒤 available(확인 후 삭제) |
| T13 | TGW IP 예약 관찰 | 10.255.0.0/24 주소를 쓰는 ENI, 흐름 로그 기록 조회 | 관찰 불가(설계 시 TGW CIDR을 넉넉히) |
| T14 | 비용 확인 | 실습 다음 날 Cost Explorer에서 사용 유형 APN2-TransitGateway-Hours를 작업(operation)별로 묶어 확인. VPC 연결 4개 × 실습 시간을 넘는 사용량이 있으면 client-vpn 연결도 과금 |
client-vpn 연결의 TGW 시간 요금 여부 확정 |
실습이 끝나면 다음 순서로 자원을 지웁니다.
scripts/40-vpn.sh <client> <tgw|legacy> down)scripts/90-cleanup.sh: 태그가 붙은 CLI 생성 엔드포인트·TGW, 스택 3개, ACM 인증서 삭제