Tailscale 하나로 맥북·러너·홈페이지 서버를 통합 관리하기
시리즈 CI·테스트 인프라 삽질 현재 위치 15 / 15
- CI 테스트 5분+ → 46초: MySQL 튜닝부터 paratest 병렬화까지
- 자체 호스팅 러너가 컨테이너일 때 CI가 깨지는 6가지
- GitHub Actions로 AI가 만든 코드를 검증하는 파이프라인
- GitHub CI 환경 전환 사례: Mac → Linux 러너 이전과 문제 해결
- GitHub 조직 이름을 바꿨더니 저장소 다섯 개의 워크플로를 고쳐야 했다
- CI 연속 실패 일지: #129‑#144 PR을 고치며 배운 15가지 교훈
- 테스트가 서로를 망가뜨릴 때: 주문 테스트 고아 행으로 찾은 스위트 오염
- "회원탈퇴가 없네?"에서 시작해 1221개 테스트까지
- AI 개발 컨설팅 첫 상담에서 실제로 묻는 질문들
- "클론 333, 방문자 2" — 퍼블릭 저장소 트래픽 숫자의 진실
- 개별로는 통과하는데 전체에서만 죽는다 — AIFid 테스트 스위트가 composer test를 멈춘 날들
- 게으름이 개발자를 움직이게 만든다
- Claude Code로 구현한 이슈‑배포 자동화 플로우
- PHP·Node·TS·Flutter 4언어 CI를 self-hosted 러너에 올리며 밟은 함정들
- Tailscale 하나로 맥북·러너·홈페이지 서버를 통합 관리하기
요약: 맥북프로, 셀프호스티드 러너(우분투), 홈페이지용 서버 3대를 Tailscale로 묶었다. 같은 네트워크에 있든 외부에 있든 장비 위치와 상관없이 하나의 사설망(tailnet)으로 접속할 수 있고, 포트포워딩 없이 MagicDNS 이름으로 연결된다. 설치 방법, 연결 확인 방법, 얻은 이점을 정리한다.
왜 Tailscale인가
운영 중인 장비가 세 대다.
- 개발용 맥북프로
- GitHub Actions 셀프호스티드 러너 우분투 서버
- 홈페이지를 서비스하는 서버
장비마다 접속 경로가 달랐다. SSH 포트를 공인으로 열거나, 공인 IP·DDNS를 관리하거나, 배포용 호스트는 Cloudflare 프록시를 피해 직접 접속 주소를 따로 둬야 했다. 장비가 늘수록 방화벽 규칙과 접속 주소 관리가 번거로워진다.
Tailscale은 WireGuard 기반 메시 VPN이다. 각 장비에 에이전트를 설치하고 같은 계정으로 로그인하면 100.x.x.x 사설 IP와 이름(MagicDNS)이 붙은 하나의 사설망이 만들어진다. 중앙 VPN 서버를 세울 필요가 없고, 장비끼리 가능하면 직접(P2P) 통신한다.

설치 방법
우분투 서버 (러너 서버, 홈페이지 서버 공통)
공식 설치 스크립트를 쓴다. 두 서버 모두 같은 절차다.
curl -fsSL https://tailscale.com/install.sh | sh
설치 후 로그인한다. 명령을 실행하면 인증 URL이 출력되고, 브라우저에서 같은 계정으로 승인하면 장비가 tailnet에 등록된다.
sudo tailscale up
서버는 재부팅 후에도 자동으로 붙어야 하므로 데몬 활성화 상태를 확인한다.
sudo systemctl enable --now tailscaled
tailscale ip -4
별도 옵션 없이 기본값 그대로 설치했다. Tailscale SSH(--ssh)는 켜지 않았고 기존 OpenSSH를 그대로 쓴다. 키 만료도 기본 설정 그대로다.
맥북프로
맥은 제작사(Tailscale) 홈페이지에서 직접 내려받아 설치했다. App Store 버전이나 Homebrew가 아니라 공식 사이트의 macOS 설치 파일이다. 설치 후 메뉴 막대 앱에서 로그인하면 끝이다.
CLI는 tailscale 명령으로 쓴다.
tailscale version
tailscale status
연결 확인
세 대에 모두 로그인하면 어느 장비에서든 tailscale status로 전체 목록을 볼 수 있다. 실제 출력은 아래 형태였다(IP·계정은 마스킹).
100.x.x.x <맥북프로> <계정> macOS -
100.x.x.x <러너 서버> <계정> linux idle
100.x.x.x <홈페이지 서버> <계정> linux active; direct 172.30.x.x:41641
direct 172.30.x.x:41641은 중계 서버(DERP)를 거치지 않고 같은 LAN 안에서 장비끼리 직접 연결됐다는 뜻이다. 직접 연결이 안 되면 relay "tok"처럼 중계 지점이 표시된다.
네트워크 상태는 tailscale netcheck로 확인한다. 이 장비에서는 UDP: true가 나와 직접 연결에 문제가 없었다.
접속은 사설 IP 또는 MagicDNS 이름으로 한다.
ssh <사용자>@<러너-서버-호스트명>
ssh <사용자>@100.x.x.x
연결이 안 될 때는 순서대로 확인한다.
- 양쪽 모두
tailscale status에Online으로 보이는가 - 서버에서
tailscaled가 실행 중인가 (systemctl status tailscaled) - 서버 방화벽이
tailscale0인터페이스의 SSH를 막고 있지 않은가 - ACL 정책이 해당 접근을 허용하는가
얻은 이점

- 포트포워딩이 필요 없다. 가장 편한 점이다. 공유기 관리 페이지에 들어가 포트를 열고, 공인 IP가 바뀔 때마다 DDNS를 챙기고, 장비가 늘 때마다 규칙을 추가하던 일이 사라진다. 각 장비가 바깥쪽으로 먼저 연결을 맺고 NAT 통과는 Tailscale이 처리하므로, 공유기 뒤에 있거나 다른 네트워크에 있어도 에이전트 설치와 로그인만으로 접속된다.
- 공용 인터넷에 SSH를 열지 않아도 된다. 포트포워딩이 없으니 서버의 22번 포트를 인터넷에 노출할 이유도 없다. 인바운드를 막고 tailnet 안에서만 접속하도록 줄일 수 있다.
- 이름으로 접속한다. MagicDNS 덕에 IP를 외우지 않아도 호스트명으로 접속한다.
- 같은 LAN에서는 직접 연결된다. 위
direct표시처럼 중계 없이 통신해 지연이 낮다. - Cloudflare 프록시 문제를 피한다. 프록시(주황 구름) 뒤의 호스트명은 SSH 포트가 통과하지 못해
i/o timeout이 난다. 배포 대상 서버를 tailnet 주소로 지정하면 이 제약이 사라진다. 단, 이 경우 SSH를 호출하는 쪽(러너)도 tailnet에 들어 있어야 한다. GitHub 호스티드 러너는 tailnet 밖이므로, 이 구성은 셀프호스티드 러너가 tailnet에 있을 때 성립한다. - 접근 제어를 중앙에서 한다. ACL로 "맥북 → 서버", "러너 → 홈페이지 서버"처럼 장비 간 허용 범위를 정할 수 있다.
참고로 러너 서버는 Tailscale 설치 외에 따로 건드린 것이 없다. 러너 설정과 워크플로는 그대로이고, 배포 경로도 기존 방식을 유지한다. 위 Cloudflare 이야기는 배포 호스트를 tailnet 주소로 바꿀 때 쓸 수 있는 선택지이고, 이번에 적용한 것은 아니다.
주의할 점
- 키 만료에 주의한다. 기본값은 만료가 켜져 있어서(기본 180일) 서버도 시간이 지나면 tailnet에서 빠져 원격 접속이 끊긴다. 나는 기본값 그대로 뒀지만, 서버로 오래 쓸 장비는 관리 콘솔(Machines)에서 Disable key expiry를 켜 두는 편이 안전하다.
- tailnet에 들어온 장비는 기본적으로 서로 접근 가능하다. 서버가 늘면 ACL로 최소 권한을 정의한다.
- 러너 서버는 외부 코드(PR 등)를 실행할 수 있으므로, tailnet 안의 다른 장비로 가는 경로를 ACL로 필요한 곳만 열어 둔다.
- Tailscale 계정(로그인 IdP)이 곧 사설망의 열쇠다. 해당 계정에 2단계 인증을 켠다.
정리
세 대에 에이전트를 설치하고 같은 계정으로 로그인하는 것만으로 맥북, 러너, 홈페이지 서버가 하나의 사설망이 됐다. 서버마다 SSH 포트를 열고 주소를 관리하던 부담이 줄었고, tailscale status 한 번으로 연결 상태를 확인할 수 있다.

댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.