Search
👷

파편화된 VPN 인프라, Tailscale로 통합하기

Tags
Infrastructure
Post
Last edited time
2026/01/25 06:57
2 more properties

1. 들어가기 앞서

스타트업에서 개발팀 리드로 일하다 보면 "일단 돌아가면 된다"는 생각으로 넘어가는 것들이 많다. VPN 인프라가 그랬다. 회사에 입사하여 개발팀의 VPN 상황을 파악해보니 Bastion 서버로 DB에 접근하고, AWS Client VPN으로 오픈마켓 API 테스트하고, Tailscale Exit Node로 내부 사이트에 접속하는 식으로 목적에 따라 다른 도구를 사용하고 있었다.
문제는 이것들이 서로 충돌한다는 점이었다. 백엔드 팀원들이 오픈마켓 API를 테스트하려고 AWS Client VPN을 켜면, 외부 인터넷 접속이 막혀서 Slack이나 Google 검색이 안 됐다. Bastion 서버를 통한 DB 접속도 동시에 불가능했다. 결국 "VPN 켜고 API 테스트 → VPN 끄고 다른 작업 → 다시 VPN 켜고..."를 반복해야 했다. 게다가 각 환경별로 Bastion 포트포워딩 설정이 달라서, 로컬 .env 파일에는 DB_PORT=3307, DB_PORT=3308 같은 포트 번호들이 난무했다. 개발 환경 세팅 문서만 봐도 한숨이 나왔다.
어느 날 product-ops 팀에서 "DB 접근이 필요해요"라는 요청이 들어왔을 때, 또다시 Bastion 서버 설정을 하면서 깨달았다. 이건 관리가 아니라 삽질이었다. 생산성이 바닥을 치고 있었다. 그래서 VPN 인프라 정비를 결심했고, 그 과정을 기록으로 남긴다.
Note: 이 글에 등장하는 IP 주소와 CIDR은 보안상 가칭으로 작성되었다.

2. 기본 개념 정리

본격적인 이야기에 앞서, VPN 관련 기본 개념들을 정리해보겠다. 이미 알고 계신 분들은 건너뛰어도 좋다.

2.1. VPN이란?

VPN(Virtual Private Network)은 인터넷을 통해 사설 네트워크에 안전하게 접속할 수 있게 해주는 기술이다. 쉽게 말해, 회사 네트워크에 "가상으로" 연결되어 마치 사무실에 있는 것처럼 내부 리소스에 접근할 수 있게 해준다.
VPN이 필요한 대표적인 상황:
보안 인프라 접근: 내부망에서만 접근 가능한 관리자 페이지
IP 화이트리스트: 특정 IP에서만 접근 허용하는 외부 API
DB 접근: Private Subnet에 있는 RDS 인스턴스

2.2. AWS VPC 기초

AWS VPC(Virtual Private Cloud)는 AWS 내에서 격리된 네트워크 공간이다.
graph TB
    subgraph VPC["VPC (10.0.0.0/16)"]
        subgraph PublicSubnet["Public Subnet<br/>10.0.0.0/24"]
            EC2[EC2]
        end
        subgraph PrivateSubnet["Private Subnet<br/>10.0.10.0/24"]
            RDS[(RDS)]
        end
    end

    Internet((Internet))
    IGW[Internet Gateway]
    NAT[NAT Gateway]

    Internet <--> IGW
    IGW <--> EC2
    EC2 --> NAT
    NAT --> RDS

    style PublicSubnet fill:#e8f4f8,stroke:#0077b6
    style PrivateSubnet fill:#ffecd2,stroke:#ff9500
Mermaid
복사
CIDR 표기법
10.0.0.0/16에서 /16은 앞의 16비트가 네트워크 주소라는 의미다. 즉, 10.0.x.x 범위의 65,536개 IP를 사용할 수 있다.
Public vs Private Subnet
Public Subnet: Internet Gateway를 통해 인터넷과 직접 통신 가능
Private Subnet: NAT Gateway를 통해서만 외부 통신 가능, 외부에서 직접 접근 불가
NAT Gateway
Private Subnet의 리소스가 인터넷에 접속할 때 사용하는 게이트웨이.
나가는 트래픽은 NAT Gateway의 고정 IP로 변환된다.
이 IP가 외부 API 화이트리스트에 등록되는 IP다

2.3. VPC Peering

서로 다른 VPC끼리 연결하는 방법이다. VPC Peering을 설정하면 마치 같은 네트워크에 있는 것처럼 Private IP로 통신할 수 있다.
graph LR
    subgraph InfraVPC["Infra VPC<br/>10.1.0.0/16"]
        IR[Resources]
    end
    subgraph BetaVPC["Beta VPC<br/>10.2.0.0/16"]
        BR[Resources]
    end

    InfraVPC <-->|VPC Peering| BetaVPC

    style InfraVPC fill:#e8f4f8,stroke:#0077b6
    style BetaVPC fill:#fff3cd,stroke:#ffc107
Mermaid
복사
비용: VPC Peering을 통한 데이터 전송은 $0.01/GB가 부과된다. 같은 AZ 내에서는 무료.

2.4. Tailscale 핵심 개념

Tailscale은 WireGuard 기반의 Mesh VPN 솔루션이다. 전통적인 VPN과 달리 중앙 서버 없이 Peer-to-Peer로 연결된다.
Subnet Router
VPC 내부망 전체를 Tailscale 네트워크에 노출시키는 역할
예: 10.0.0.0/16 라우트를 광고하면, Tailscale에 연결된 사용자가 해당 VPC 내부 리소스에 접근 가능
Exit Node
모든 인터넷 트래픽을 특정 노드를 통해 나가게 하는 기능
사용자의 공인 IP가 Exit Node의 IP로 바뀜
IP 화이트리스트 서비스 접근에 유용
ACL (Access Control List)
그룹별로 어떤 리소스에 접근할 수 있는지 제어
예: ops 그룹은 Exit Node만, backend-developers는 모든 VPC 접근 허용

3. 기존 현황: VPN 인프라의 파편화

3.1. 사내 VPC 구조

우리 회사의 AWS VPC는 환경별로 분리되어 있다.
VPC
CIDR
용도
prod
10.0.0.0/16
실서비스 운영 환경
infra
10.1.0.0/16
공통 인프라 (Jenkins, ML 등)
beta
10.2.0.0/16
QA 테스트 환경
alpha
10.3.0.0/16
개발 테스트 환경
VPC Peering으로 infra beta, infra alpha가 연결되어 있어서, infra VPC에서 다른 테스트 환경으로 접근이 가능한 구조다.

3.2. VPN 접근 방식의 파편화

문제는 상황에 따라 다른 방식으로 접속하고 있었다는 점이다.
상황
기존 방식
문제점
재택 시 내부 사이트 접근
Tailscale Exit Node
Public Subnet의 단일 EC2에 의존
각 환경 DB 접근
Bastion 서버 + SSH 터널
매번 포트포워딩 설정 필요
오픈마켓 API 테스트
AWS Client VPN
연결하면 인터넷이 완전히 끊김

3.3. 구체적인 문제들

3.3.1. AWS Client VPN의 인터넷 차단 문제

AWS Client VPN을 연결하면 외부 인터넷이 완전히 끊겼다. Split Tunnel을 설정하지 않아서 모든 트래픽이 VPN으로 가버리는 구조였다. 그래서 오픈마켓 API 테스트할 때는 다른 작업을 못 하고, VPN 연결/해제를 반복해야 했다.

3.3.2. Bastion 관리의 번거로움

DB에 접속하려면 매번 이런 과정을 거쳐야 했다:
# 1. Bastion 서버로 SSH 터널 설정 ssh -L 3306:rds-instance.xxx.ap-northeast-2.rds.amazonaws.com:3306 bastion-prod # 2. 다른 터미널에서 localhost로 DB 접속 mysql -h 127.0.0.1 -P 3306 -u admin -p
Bash
복사
새로운 팀원이 오면 매번 Bastion 접근 권한을 설정해야 했고, product-ops 팀에서 DB 접근 요청이 올 때마다 IAM 설정과 SSH 키 배포를 반복했다.

3.3.3. 비용 문제

항목
월 비용
AWS Client VPN Association (2개)
$144.00
Bastion EC2 (3개, t4g.nano)
~$15.00
합계
~$159/월
AWS Client VPN은 Endpoint당 $72/월 + 연결 시간당 $0.05가 부과된다. Association만 유지해도 고정 비용이 발생하는 구조라서, 거의 안 쓰면서도 돈이 나가고 있었다.

4. 대응 방향: Tailscale로 통합

4.1. 왜 Tailscale인가?

여러 VPN 솔루션을 검토한 끝에 Tailscale을 선택했다. 주요 이유는 다음과 같다.
1. WireGuard 기반으로 빠르고 가벼움
OpenVPN 대비 훨씬 빠른 연결 속도
CPU 사용량도 낮음
2. Zero-config Mesh 네트워크
중앙 VPN 서버 없이 P2P 연결
NAT 뒤에 있어도 대부분 직접 연결됨 (STUN/DERP 활용)
3. SSO 연동
Google Workspace와 연동하여 회사 계정으로 로그인
퇴사자 계정 비활성화 시 VPN 접근도 자동 차단
4. ACL 기반 세분화된 접근 제어
그룹별로 어떤 리소스에 접근할 수 있는지 세밀하게 제어 가능
JSON 기반 Policy로 관리
5. 비용 효율성
Starter Plan: $6/user/month (10명 기준 $60/월)
AWS Client VPN의 절반도 안 되는 비용

4.2. 아키텍처 설계

핵심은 Router 2개로 모든 VPC를 커버하는 것이다. Router 구성은 다음과 같다.
Router
VPC
역할
광고 라우트
prod-router
Prod VPC
Subnet Router + Exit Node
10.0.0.0/16
infra-router
Infra VPC
Subnet Router
10.1.0.0/16, 10.2.0.0/16, 10.3.0.0/16
prod-router는 Prod VPC 전용이고, infra-router는 VPC Peering을 통해 Infra + Beta + Alpha 3개 VPC를 커버한다.
graph TB
    Developer["개발자<br/>Tailscale Client"]

    subgraph TailscaleNet["Tailscale Network"]
        subgraph InfraRouter["infra-router"]
            IR["Subnet Router<br/>광고: 10.1.0.0/16, 10.2.0.0/16, 10.3.0.0/16"]
        end

        subgraph ProdRouter["prod-router"]
            PR["Subnet Router + Exit Node<br/>광고: 10.0.0.0/16"]
        end
    end

    subgraph AWS["AWS"]
        subgraph InfraVPC["Infra VPC"]
            Jenkins[Jenkins]
        end
        subgraph BetaVPC["Beta VPC"]
            BetaRDS[(Beta RDS)]
        end
        subgraph AlphaVPC["Alpha VPC"]
            AlphaRDS[(Alpha RDS)]
        end
        subgraph ProdVPC["Prod VPC"]
            ProdRDS[(Prod RDS)]
            NAT[NAT Gateway]
        end
    end

    OpenMarket[오픈마켓 API]

    Developer --> IR
    Developer --> PR

    IR --> Jenkins
    IR -.->|Peering| BetaRDS
    IR -.->|Peering| AlphaRDS

    PR --> ProdRDS
    PR --> NAT
    NAT --> OpenMarket

    style TailscaleNet fill:#d4edda,stroke:#28a745
    style ProdVPC fill:#ffecd2,stroke:#ff9500
    style InfraVPC fill:#e8f4f8,stroke:#0077b6
Mermaid
복사

4.3. 그룹별 접근 제어

Tailscale ACL로 그룹별 권한을 차별화했다.
그룹
Prod VPC
Infra/Beta/Alpha
Exit Node
ops
X
X
O
product-ops
특정 DB만
X
O
backend-developers
O
O
O
ops 그룹
운영팀은 보안 사이트(IP 화이트리스트로 보호되는 내부 사이트)만 접근하면 된다.
Exit Node를 통해 허용된 IP로 나가서 접속.
product-ops 그룹
특정 Read DB에만 접근 허용. 전체 VPC 접근은 불필요.
backend-developers 그룹
개발에 필요한 모든 리소스 접근 가능.

4.4. IaC로 관리

Tailscale Router EC2 인스턴스는 Terraform 모듈로 관리한다. 상세 코드는 생략하지만, 핵심은 다음과 같다.
EC2 인스턴스 생성 (t3.small)
User Data로 Tailscale 설치 및 설정
Security Group 설정 (최소 권한 원칙)
IAM Role로 필요한 권한 부여
코드로 관리하면 동일한 구성을 쉽게 재현할 수 있고, 변경 이력도 Git으로 추적된다.

5. 실제 구현 상세

5.1. Router EC2 인스턴스 구성

두 Router 모두 Private Subnet에 배치했다. Public IP가 없어도 Tailscale 연결에는 문제없다 (NAT Gateway를 통해 Control Plane 통신)
prod-router (Prod VPC 전용)
항목
설정값
설명
VPC
Prod VPC (10.0.0.0/16)
실서비스 환경
Subnet
Private Subnet
보안 강화
Instance Type
t3.small
Tailscale + 라우팅 처리
역할
Subnet Router + Exit Node
VPC 내부망 + 외부 IP 우회
외부 IP
NAT Gateway IP
오픈마켓 API 화이트리스트 등록 IP
infra-router (Infra/Beta/Alpha VPC용)
항목
설정값
설명
VPC
Infra VPC (10.1.0.0/16)
개발 인프라 환경
Subnet
Private Subnet
보안 강화
Instance Type
t3.small
Tailscale + 라우팅 처리
역할
Subnet Router
VPC 내부망 접근만
광고 라우트
3개 VPC CIDR
Infra + Beta + Alpha

5.2. VPC Peering으로 3개 VPC 커버

infra-router 하나로 3개 VPC를 커버할 수 있었던 건 VPC Peering 덕분이다.
graph TB
    subgraph InfraVPC["Infra VPC (10.1.0.0/16)"]
        IR["infra-router"]
    end

    subgraph BetaVPC["Beta VPC (10.2.0.0/16)"]
        BetaRDS[(Beta RDS)]
    end

    subgraph AlphaVPC["Alpha VPC (10.3.0.0/16)"]
        AlphaRDS[(Alpha RDS)]
    end

    IR -->|직접 접근| InfraVPC
    IR -.->|"pcx-01721xxx<br/>VPC Peering"| BetaRDS
    IR -.->|"pcx-010acxxx<br/>VPC Peering"| AlphaRDS

    style InfraVPC fill:#e8f4f8,stroke:#0077b6
    style BetaVPC fill:#fff3cd,stroke:#ffc107
    style AlphaVPC fill:#e2e3e5,stroke:#6c757d
Mermaid
복사
Peering 설정 시 주의할 점은 다음과 같다.
Route Table: Infra VPC Route Table에 Beta/Alpha CIDR → Peering Connection 라우트 추가
Security Group: Peering CIDR(10.2.0.0/16, 10.3.0.0/16)에서 오는 트래픽 허용

5.3. Exit Node로 오픈마켓 API 접근

오픈마켓 API는 보안상 IP 화이트리스트로 접근을 제한한다. Prod 환경의 NAT Gateway IP만 허용되어 있어서, 로컬에서 테스트할 때 막히는 문제가 있었다. 기존에는 AWS Client VPN을 연결해서 Prod VPC 내부에서 나가는 것처럼 했는데, 인터넷이 끊기는 문제가 있었다.
이 문제는 Exit Node를 활용하여 문제를 해결했다. prod-router에 Exit Node를 활성화했다. Private Subnet이지만 NAT Gateway를 통해 나가므로, 외부에서 보면 NAT Gateway IP로 보인다.
graph TB
    LocalPC["로컬 PC"]
    ProdRouter["prod-router<br/>(Exit Node)"]
    NAT["NAT Gateway<br/>52.78.xxx.xxx"]
    API["오픈마켓 API"]

    LocalPC -->|"1. Tailscale"| ProdRouter
    ProdRouter -->|"2. Private Subnet"| NAT
    NAT -->|"3. 화이트리스트 IP"| API
    API -->|"4. 접근 허용 ✓"| NAT

    style NAT fill:#d4edda,stroke:#28a745
    style API fill:#d4edda,stroke:#28a745
Mermaid
복사

5. 구현 중 만난 이슈들

VPN을 셋팅하는게 생각보다 순탄하게만 진행된 건 아니었다. 그 중 몇 가지 삽질한 내용을 공유한다.

5.1. VPC Peering 라우팅 누락

infra-router에서 Beta VPC의 리소스에 접근이 안 됐다. Peering 연결은 되어 있는데, 트래픽이 전달이 안 되는 상황.
Route Table에 Peering 라우트를 안 넣었다. VPC Peering은 연결만 한다고 끝이 아니라, 양쪽 Route Table에 상대방 CIDR → Peering Connection 라우트를 추가해야 한다.

5.2. Exit Node에서 Chrome Remote Desktop 연결 실패

우리 회사에는 재택근무 시 사무실 PC에 원격 접속이 필요한 팀이 있다. Chrome Remote Desktop을 사용하는데, prod-router Exit Node를 켜면 연결이 실패했다. 같은 Exit Node에서 일반 웹 브라우징은 문제없는데, WebRTC 기반 서비스만 안 되는 상황이었다.
원인을 분석해보니 Private Subnet + NAT Gateway 조합이 Symmetric NAT로 동작하기 때문인 것을 알게 되었다. NAT 타입에 대해 간단히 설명하면 다음과 같다.
Full Cone NAT: 외부 포트가 고정. 누구나 그 포트로 패킷을 보낼 수 있음
Symmetric NAT: 목적지마다 다른 외부 포트 사용. STUN으로 알아낸 IP:Port가 실제 P2P 연결에 사용할 IP:Port와 다름
graph LR
    subgraph FullCone["Full Cone NAT ✓"]
        FC_Int["내부 10.0.0.5:1234"]
        FC_Ext["외부 52.78.xxx.xxx:5678"]
        FC_STUN["STUN 서버"]
        FC_Peer["P2P 상대방"]

        FC_Int --> FC_Ext
        FC_Ext --> FC_STUN
        FC_Ext --> FC_Peer
    end

    subgraph Symmetric["Symmetric NAT ✗"]
        SN_Int["내부 10.0.0.5:1234"]
        SN_Ext1["외부 :5678"]
        SN_Ext2["외부 :9012"]
        SN_STUN["STUN 서버"]
        SN_Peer["P2P 상대방"]

        SN_Int --> SN_Ext1
        SN_Int --> SN_Ext2
        SN_Ext1 --> SN_STUN
        SN_Ext2 --> SN_Peer
    end

    style FullCone fill:#d4edda,stroke:#28a745
    style Symmetric fill:#f8d7da,stroke:#dc3545
Mermaid
복사
Chrome Remote Desktop은 WebRTC를 사용하는데, Symmetric NAT 환경에서는 P2P 연결이 실패하고 TURN 릴레이로 fallback해야 했다. 해결 할 수 있는 옵션은 다음과 같이 2가지가 존재했다.
1.
기존 dev-router 유지: Public Subnet에 있는 기존 Exit Node를 Chrome RD용으로 유지
2.
STUN/TURN 포트 허용: Security Group에서 TURN 릴레이 포트 오픈
기존 dev-router는 제거 에정이었으므로 2번 옵션으로 진행하였다. 그리고 STUN/TURN 포트를 아웃바운드 설정하여 WebRTC 서비스가 동작할 수 있게 처리하였다.
UDP 전체 아웃바운드(0-65535)를 열면 편하지만, AWS 보안 Best Practice에 맞지 않다. 보안 감사나 컴플라이언스 요구사항을 충족하려면 필요한 포트만 명시적으로 열어야 한다.

6. 결과 및 배운 점

6.1. 정량적 개선

가장 눈에 띄는 변화는 비용이다. 기존에는 AWS Client VPN Association 2개에 월 $144, Bastion EC2 3대에 약 $15가 들어서 합계 월 $159 정도가 나갔다. Tailscale로 전환한 후에는 Tailscale 비용 약 $60, Router EC2 2대 약 $30, VPC Peering 비용 약 $5를 합쳐서 월 $97 정도로 줄었다. 39% 절감이다.
관리 포인트도 대폭 줄었다. 기존에는 Bastion 서버 3대, AWS Client VPN Endpoint 3개, 기존 Tailscale dev-router까지 6개 이상의 인프라를 따로 관리해야 했다. 이제는 prod-router와 infra-router 2개만 관리하면 된다.
무엇보다 체감되는 건 DB 접근 생산성이다. 예전에는 Bastion 서버에 SSH 터널링 클라이언트를 켜서 사용하는게 여간 번거로운 일이 아니었다. 지금은 Tailscale만 켜면 RDS Private IP로 바로 접속할 수 있다. 이게 하루에 몇 번씩 반복되면 생산성 차이가 꽤 크다.

6.2. 정성적 개선

숫자로 드러나지 않는 개선도 있다. Tailscale Admin Console에서 누가 언제 어디에 접속했는지 한눈에 볼 수 있게 됐다. 기존에는 Bastion 접근 로그 따로, VPN 연결 로그 따로 봐야 했는데, 이제는 한 곳에서 모든 접근 현황을 확인할 수 있다.
AWS Client VPN을 쓸 때는 ACM 인증서 관리가 꽤 번거로웠다. 인증서 만료 전에 갱신해야 하고, 갱신 후에는 클라이언트 설정 파일도 다시 배포해야 했다. Tailscale은 이런 인증서 관리가 필요 없다. WireGuard 키 교환을 자체적으로 처리하기 때문이다.
SSO 연동도 편하다. Google Workspace와 연결해뒀더니 회사 계정으로 로그인하면 끝이다. 퇴사자가 생기면 Google 계정만 비활성화하면 VPN 접근도 자동으로 막힌다. 예전에는 퇴사 처리할 때 Bastion SSH 키 삭제, VPN 인증서 폐기 등을 일일이 챙겨야 했다.

6.3. 배운 점

이번 작업을 하면서 몇 가지 깨달은 게 있다. VPN 인프라도 IaC로 관리해야 한다는 점이다. "나중에 정리하자"고 넘어갔던 것들이 쌓이니까 결국 관리 부채가 됐다. 이번에 Terraform으로 코드화하면서 변경 이력 추적이 가능해졌고, 같은 구성을 다른 환경에 재현하거나 문제가 생겼을 때 롤백하는 것도 쉬워졌다.
보안과 편의성 사이에서 균형을 잡는 것도 중요하다. 최소 권한 원칙을 지키겠다고 너무 빡빡하게 막으면 업무에 지장이 생긴다. 필요한 포트는 열되, 왜 열었는지 문서화해두는 게 맞는 것 같다. 나중에 보안 감사를 받거나 인수인계할 때 이 기록이 있으면 훨씬 수월하다.
"일단 돌아가면 된다"는 생각의 함정도 느꼈다. 돌아가는 것과 잘 돌아가는 것은 다르다. 당장 서비스가 멈추는 건 아니니까 미루기 쉬운데, 그 사이에 팀원들의 생산성이 조금씩 갉아먹히고 있었다. 당장은 불편해도 정비할 시간을 내야 한다. 그래야 나중에 더 큰 문제가 안 생긴다.

6.4. 앞으로의 방향

아직 할 일이 남아 있다. AWS Client VPN Endpoint와 Bastion 서버들은 현재 stopped 상태로 두고 있다. 한 달 정도 운영해보고 문제가 없으면 2월 중으로 완전히 삭제할 예정이다. 혹시 모를 롤백을 위해 당장 지우지는 않았다.
현재 구조에서 prod-router나 infra-router가 죽으면 해당 VPC 접근이 불가능해진다. Single Point of Failure다. 트래픽이 많지 않아서 당장은 괜찮지만, 장기적으로는 이중화를 검토해야 한다. Tailscale의 HA 구성 가이드를 보고 있는데, 같은 라우트를 광고하는 노드를 여러 대 두면 자동으로 failover가 된다고 한다.
Device Posture Check 도입도 고려하고 있다. 현재는 Tailscale에 연결만 되면 접근이 가능한데, 접속 기기의 보안 상태(OS 업데이트 여부, 디스크 암호화 등)를 체크해서 조건을 충족하지 않으면 접근을 막는 기능이다. 보안 요구사항이 높아지면 적용할 계획이다.

7. 마치며

VPN 인프라 정비는 "해야지, 해야지" 하면서 미루기 쉬운 작업이다. 당장 서비스가 안 돌아가는 게 아니니까. 하지만 삽질의 시간을 줄이고, 새 팀원의 온보딩을 쉽게 하고, 보안을 강화하려면 결국 해야 하는 일이다.
Tailscale이 유일한 답은 아니다. 조직의 규모, 기존 인프라, 예산에 따라 다른 선택이 더 나을 수도 있다. 중요한 건 현재 상태를 파악하고, 문제를 인식하고, 개선하려는 시도를 하는 것이라 생각한다.
비슷한 고민을 하는 분들께 이 글이 도움이 되길 바란다.

8. 참고 자료