인프라 공방전 스터디를 열게 된 이유?

AI 기술 발전으로 개발 영역이 빠르게 대체되는 시대에, 개발자는 비즈니스 성장과 안정적인 시스템 구축이라는 두 마리 토끼를 모두 잡아야 합니다.
그러기 위해서는 비즈니스에 적절한 시점에 적합한 기술을 도입하고 비즈니스의 변화를 염두해두고 디자인하는 사람이 되어야 되는데 이를 하기 위해서는 경험과 지식이 필요한데 안정적인 서비스 운영은 직접 경험을 통해 배울 수 있기 때문에 이 스터디를 만들게 되었습니다.
SSH 없이 EC2 접속하기 — AWS SSM 터널링 완전 정복
이 글은 실제 세팅 과정에서 마주친 에러들을 하나씩 해결하며 작성한 실전 경험 기반의 글입니다. Private Subnet EC2에 SSM으로 접속하는 전 과정을 정리해보았습니다.
왜 SSM인가?

전통적인 SSH 접속은 몇 가지 문제를 안고 있습니다.
- 인바운드 22번 포트를 열어야 한다
- SSH 키를 관리해야 한다
- Bastion Host가 필요하다
- Private Subnet의 인스턴스에는 직접 접근이 불가능하다
AWS SSM(Systems Manager) Session Manager는 이 모든 문제를 해결합니다. EC2의 SSM Agent가 SSM 서비스로 먼저 아웃바운드 연결을 맺고, 로컬 클라이언트는 그 연결을 통해 세션을 시작하는 방식입니다.
로컬 PC
└─ aws ssm start-session
└─ SSM 엔드포인트 (443 아웃바운드만 필요)
└─ SSM Agent (EC2)
└─ 대상 포트 (DB, Web 등)
인바운드 포트가 전혀 필요 없습니다. 보안 그룹에 아웃바운드 443만 열려 있으면 충분합니다.
사전 준비
1. AWS Credentials 설정
SSM 명령어를 실행하기 전에 반드시 로컬에 AWS 인증 정보를 설정해야 합니다. AccessKey와 시크릿 키를 만드는 건 아래의 문서를 참고해주세요.
aws configure --profile my-profile
AWS Access Key ID: AKIA...
AWS Secret Access Key: xxxxxxxx
Default region name: ap-northeast-2
Default output format: json
설정 후 인증 확인:
aws sts get-caller-identity --profile my-profile
정상 출력되면 준비 완료입니다.
주의: region 입력 시 오타에 유의한다. ap-northeaset-2 (X) → ap-northeast-2 (O) 오타가 있으면 Could not connect to the endpoint URL 에러가 발생한다.
2. Session Manager Plugin 설치
aws ssm start-session 명령어는 내부적으로 Session Manager Plugin을 호출합니다. AWS CLI와 별개로 로컬에 설치해야 하는 플러그인이기 때문에 cli를 설치하셨더라도 설치를 권장드립니다.
# macOS
brew install --cask session-manager-plugin
# 설치 확인
session-manager-plugin --version
이 과정을 빠뜨리면 아래 에러가 발생합니다.
SessionManagerPlugin is not found.
3. EC2에 IAM Role 부여
EC2 인스턴스에 AmazonSSMManagedInstanceCore 정책이 포함된 IAM Role을 부여해야 합니다.
EC2 → 인스턴스 선택 → 작업 → 보안 → IAM 역할 수정
로컬 CLI 사용자에게도 아래 권한이 필요합니다.
{
"Effect": "Allow",
"Action": [
"ssm:StartSession",
"ssm:TerminateSession",
"ssm:DescribeSessions",
"ssm:DescribeInstanceInformation"
],
"Resource": "*"
}
트러블슈팅 1 — TargetNotConnected
증상
인스턴스를 ssm연결을 호출 했을 때 대상이 존재하지 않는다는 메세지가 발생하면서 접속되지 않은 현상입니다. 원인은 크게 두가지로 볼 수 있었습니다.
- 접근하려는 EC2가 ssm 에이전트가 제대로 설치되거나 연동되지 않은 경우
- IAM Role이 제대로 부여되지 않은 경우
An error occurred (TargetNotConnected) when calling the StartSession operation:
i-xxxxxxxxxxxxxxxxx is not connected.
진단
먼저 인스턴스가 SSM에 등록됐는지 확인합니다. AWS 대시보드 화면에서 보려면 EC2 클릭 후 작업 -> 모니터링 및 문제 해결 -> 시스템 로그 조회에서 보실 수 있습니다.
aws ssm describe-instance-information --profile my-profile
# → InstanceInformationList: [] (빈 결과면 문제)
EC2에 직접 접속해서 IAM 정보를 확인하면 IAM 권한이 있는지 확인할 수 있습니다.
curl http://169.254.169.254/latest/meta-data/iam/info
# → 아무것도 출력되지 않음
이게 핵심 단서였습니다. IAM Role이 전파 중인 것이 아니라 애초에 EC2에 IAM Role이 붙어있지 않은 상황이였습니다.
원인
SSM Agent가 SSM 서비스에 자신을 등록하려면 AmazonSSMManagedInstanceCore 권한이 필요한데, Role 자체가 없으니 등록이 불가능했던 것입니다.
그 후 Role 부여 후에도 바로 연결되지 않는 이유는 SSM Agent가 SSM 서비스에 재등록하는 시간이 필요하기 때문에 조금 시간이 지난 뒤 재시도해보니 PingStatus: Online이 확인할 수 있습니다. 그 뒤에는 세션 연결이 가능합니다.
aws ssm describe-instance-information --profile my-profile
# → PingStatus: "Online" 이 뜨면 연결 가능
교훈
TargetNotConnected는 "연결이 끊겼다"가 아니라 "한 번도 연결된 적 없다"일 수 있다. 에이전트가 연결 되었는지 확인하고 EC2 메타데이터 API를 통해 IAM Role 유무를 통해 확인할 수 있다.
트러블슈팅 2 — ssm-user로 접속되어 ec2-user 디렉토리 접근 불가
증상
SSM 접속 후 /home/ec2-user로 이동하려 하면 다음과 같이 접근 권한 문제가 뜹니다.
sh-5.2$ cd ec2-user/
sh: cd: ec2-user/: Permission denied
원인
SSM Session Manager는 기본적으로 ssm-user 계정으로 세션을 시작합니다. ssm-user는 ec2-user의 홈 디렉토리에 접근 권한이 없습니다.
잘못된 시도
sudo cd가 동작하지 않는 이유는 cd가 shell builtin이기 때문이다. 만약 윈도우에서 guest의 메모장을 켰을 때 이 메모장을 root 계정의 메모장으로 만들 수 없듯이 나 자신의 상태를 바꾸는 건 불가능하다.
sudo cd ec2-user/ # 아무 반응 없음
Shell Builtin이란?
리눅스에서 명령어는 두 종류로 나뉩니다.
| 종류 | 예시 | 특징 |
| 외부 명령어 | ls, cp, mv | 파일시스템에 실행 파일로 존재 (/bin/ls) |
| Shell Builtin | cd, export, alias | Shell 자체에 내장, 별도 파일 없음 |
sudo는 새로운 프로세스를 다른 권한으로 실행하는 명령어입니다. cd는 현재 shell 프로세스의 작업 디렉토리 상태를 바꾸는 것이므로 sudo로 실행할 수 있는 대상 자체가 아닙니다.
type cd # cd is a shell builtin
type ls # ls is /bin/ls
올바른 해결
sudo su - ec2-user
sudo su - ec2-user를 통해 ec2-user 권한으로 새로운 shell 프로세스를 띄우는 것입니다.
SSM 세션 시작
└── sh 프로세스 (ssm-user 권한)
└── sudo su - ec2-user 실행
└── bash 프로세스 (ec2-user 권한) ← 여기서 작업
└── exit → 다시 ssm-user의 sh로 돌아옴
Private Subnet EC2에 SSM 연결하기
Private Subnet의 EC2는 인터넷망이 연결되어있지 않기 때문에 SSM 서비스에 접근할 수 없습니다. 해결 방법은 두 가지 입니다.
| 방법 | 장점 | 단점 |
| NAT Instance | 비용 저렴 (EC2 비용만) | 직접 관리 필요 |
| NAT Gateway | AWS 완전 관리, 고가용성 | 시간당 요금 + 데이터 요금 |
인프라 규모나 서비스 규모가 작다면 NAT Instance가 비용 면에서 유리합니다.
NAT Instance 구성
1. Public Subnet에 EC2 생성

서브넷: Public Subnet ← 반드시 퍼블릭 서브넷에 생성
퍼블릭 IP 자동 할당: 활성화
보안 그룹 인바운드:
- All Traffic ← Private Subnet CIDR (예: 10.0.1.0/24)
보안 그룹 아웃바운드:
- All Traffic ← 0.0.0.0/0
2. 소스/대상 확인 비활성화
EC2 → NAT Instance → 작업 → 네트워킹 → 소스/대상 확인 변경 → 중지 체크
NAT Instance는 트래픽을 받아 전달해주는 역활이기 때문에 자신이 출발지/목적지가 아닌 트래픽을 전달해야 합니다. 위 설정을 반드시 비활성화해야 합니다. 이걸 빠뜨리면 아무리 다른 설정을 해도 NAT가 동작하지 않습니다.
3. IP 포워딩 및 iptables 설정
IPtable란?
iptables는 Linux 커널에서 netfilter 프레임워크를 제어하는 도구입니다. 패킷이 커널을 통과할 때 어떻게 처리할 지 정의하는 도구라고 생각하면 됩니다.
4. 테이블과 체인 구조
iptables는 테이블 → 체인 → 규칙 3단계 구조로 이루어져 있습니다. 패킷을 택배, iptable을 물류 창고라고 생각하면 테이블은 택배의 종류입니다. 일반 택배인지 퀵 배송인지 신선 제품인지를 구분짓는 것이고 체인은 창고의 위치입니다.
입고 전 창고와 출고 직전 창고가 다르듯이 패킷의 상태에 따라 위치하게 되는 체인이 달라집니다. 그 다음은 규칙은 택배를 반송 시킬 건지 폐기 시킬 건지에 대한 기준이라고 생각하면 편합니다.
테이블 종류
| 테이블 | 역활 |
| filter | 패킷 허용/차단 (기본값) |
| nat | 주소 변환 (NAT) |
| mangle | 패킷 헤더 수정 |
| raw | 연결 추적 제외 설정 |
패킷 흐름과 체인

체인별 역활
INPUT
로컬 프로세스로 들어오는 패킷
# SSH 접속 허용
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
OUTPUT
로컬 프로세스에서 나가는 패킷.
# 외부로 나가는 HTTP 허용
iptables -A OUTPUT -p tcp --dport 80 -j ACCEPT
FORWARD
라우터/NAT처럼 통과시키는 패킷. 오늘 NAT Instance에서 이게 REJECT였기 때문에 패킷이 전달되지 않았습니다.
# 전달 허용
iptables -A FORWARD -j ACCEPT
PREROUTING
패킷이 라우팅 결정되기 전에 처리. (주로 DNAT에 사용)
# 외부 80포트 → 내부 8080으로 전달
iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 10.0.1.100:8080
POSTROUTING
패킷이 나가기 직전에 처리. (주로 SNAT/MASQUERADE에 사용)
# 오늘 설정한 바로 이것
iptables -t nat -A POSTROUTING -o ens5 -j MASQUERADE
5. 실제 실행 명령어
# ============================================
# NAT Instance 설정 스크립트
# ============================================
# [1] IP 포워딩 즉시 활성화
# - 리눅스는 기본적으로 자신이 목적지가 아닌 패킷을 버린다
# - ip_forward=1 로 설정하면 다른 호스트 간 패킷을 전달(라우팅)할 수 있게 된다
# - sysctl -w 는 커널 파라미터를 런타임에 즉시 변경 (재부팅 시 초기화됨)
sudo sysctl -w net.ipv4.ip_forward=1
# [2] IP 포워딩 영구 적용
# - /etc/sysctl.conf 에 설정을 추가해 재부팅 후에도 유지되도록 한다
# - tee -a 는 파일에 내용을 추가(append) 한다 (덮어쓰기 아님)
echo "net.ipv4.ip_forward = 1" | sudo tee -a /etc/sysctl.conf
# [3] NAT 규칙 추가 (MASQUERADE)
# - -t nat : nat 테이블에 규칙 추가
# - -A POSTROUTING : 패킷이 외부로 나가기 직전 시점(POSTROUTING 체인)에 적용
# - -o ens5 : 아웃바운드 인터페이스가 ens5 인 패킷에만 적용
# - -j MASQUERADE : 출발지 IP를 ens5 인터페이스의 현재 IP로 자동 변환
# (SNAT와 달리 IP를 명시하지 않아도 되므로 EC2처럼 IP가 바뀌는 환경에 적합)
sudo iptables -t nat -A POSTROUTING -o ens5 -j MASQUERADE
# [4] FORWARD 체인 허용 ← 오늘 문제의 핵심
# - NAT Instance는 Private EC2의 패킷을 대신 전달하는 역할을 한다
# - 이때 패킷은 FORWARD 체인을 통과하는데, 기본값이 REJECT 인 경우 패킷이 버려진다
# - -I FORWARD : FORWARD 체인의 맨 앞(1번)에 규칙을 삽입 (-A 는 맨 뒤 추가)
# 맨 앞에 넣는 이유: 기존 REJECT 규칙보다 먼저 매칭되어 ACCEPT 처리되도록 하기 위함
# - -j ACCEPT : 매칭된 패킷을 허용
sudo iptables -I FORWARD -j ACCEPT
# [5] iptables 규칙 영구 저장
# - iptables 규칙은 메모리에만 존재하므로 재부팅 시 초기화된다
# - iptables-save : 현재 메모리의 모든 iptables 규칙을 텍스트 형식으로 출력
# - tee : 출력 내용을 파일로 저장 (표준출력에도 동시 출력)
# - /etc/sysconfig/iptables : 부팅 시 iptables-restore 가 읽는 규칙 파일 경로
# (systemctl enable iptables 가 활성화된 경우 부팅 시 자동 복원됨)
sudo iptables-save | sudo tee /etc/sysconfig/iptables
주의: iptables의 FORWARD 체인이 기본적으로 REJECT로 설정된 경우가 있다. 이 경우 패킷이 NAT Instance까지 도달해도 전달되지 않는다. 반드시 FORWARD 체인 상태를 확인해야 한다.
# FORWARD 체인 확인
sudo iptables -L FORWARD -n -v
# REJECT 규칙이 있으면 삭제
sudo iptables -D FORWARD -j REJECT --reject-with icmp-host-prohibited
6. Private Subnet 라우팅 테이블 설정
VPC → 라우팅 테이블 → Private Subnet 라우팅 테이블 → 라우팅 편집
대상(Destination) 타깃(Target)
0.0.0.0/0 NAT Instance ID
10.0.0.0/16 local
인스턴스를 타깃으로 지정하면 AWS가 자동으로 ENI로 변환한다. 정상 동작이므로 걱정하지 않아도 됩니다.
직렬 콘솔 설정 (Private EC2 초기 접근 시)
NAT Instance 구성 전 Private EC2에 접근해야 할 때 직렬 콘솔을 사용할 수 있습니다. 직렬 콘솔은 VPC 네트워크와 독립적으로 동작하므로 네트워크 문제와 무관하게 접속 가능하기 때문에 네트워크 문제를 추적하기에 용이합니다.
직렬 콘솔이 안 됐던 3가지 원인
원인 1 — AWS 계정 레벨 비활성화
직렬 콘솔은 기본적으로 계정 레벨에서 비활성화되어 있습니다.
# 활성화 상태 확인
aws ec2 get-serial-console-access-status
# 활성화
aws ec2 enable-serial-console-access
또는 콘솔에서:
EC2 → 대시보드 → 계정 속성 → EC2 직렬 콘솔 → 관리 → 허용
원인 2 — OS 비밀번호 미설정
Amazon Linux 2023은 기본적으로 비밀번호 로그인이 비활성화되어 있다. 직렬 콘솔 화면은 뜨지만 어떤 비밀번호를 입력해도 Login incorrect가 발생합니다. User Data를 통해 비밀번호를 설정해야 한다.
원인 3 — cloud-init 기본 동작으로 인해 User Data가 재실행되지 않음
EC2의 Linux 인스턴스에서 일반적인 User Data 스크립트는 cloud-init에 의해 기본적으로 once-per-instance로 실행됩니다. 따라서 이미 초기 실행이 끝난 인스턴스에서 User Data를 수정하더라도, 인스턴스를 다시 시작한다고 자동으로 재실행되지는 않습니다.
해결 — MIME 멀티파트 방식
MIME(Multipurpose Internet Mail Extensions)은 여러 종류의 콘텐츠를 하나의 메시지에 담기 위한 표입니다. 원래 이메일에서 본문과 첨부파일을 함께 전송하기 위해 만들어졌으며, cloud-init도 같은 방식을 차용했습니다.
always 옵션을 적용하려면 cloud-config와 shell script를 MIME 멀티파트로 분리해야 합니다.
Content-Type: multipart/mixed; boundary="//"
MIME-Version: 1.0
--//
Content-Type: text/cloud-config; charset="us-ascii"
MIME-Version: 1.0
#cloud-config
cloud_final_modules:
- [scripts-user, always] ← 매번 강제 실행
--//
Content-Type: text/x-shellscript; charset="us-ascii"
MIME-Version: 1.0
#!/bin/bash
echo "password" | passwd --stdin ec2-user
passwd -u ec2-user
echo "password" | passwd --stdin root
passwd -u root
sed -i 's/^auth\s*required\s*pam_securetty.so/#auth required pam_securetty.so/' /etc/pam.d/login
echo "done $(date)" >> /var/log/userdata-result.log
--//
성공하면 시스템 로그에서 아래 메시지를 확인할 수 있습니다.
cloud-init: Changing password for user ec2-user.
cloud-init: passwd: all authentication tokens updated successfully.
전체 트러블슈팅 요약
| 에러 | 증상 | 실제 원인 | 해결 방법 |
| TargetNotConnected | SSM 연결 안 됨 | IAM Role 미부여 | EC2에 Role 부여 후 대기 |
| SessionManagerPlugin not found | CLI 명령 실패 | 플러그인 미설치 | brew install --cask session-manager-plugin |
| Permission denied on ec2-user | 홈 디렉토리 접근 불가 | SSM 기본 계정이 ssm-user | sudo su - ec2-user |
| 직렬 콘솔 화면 안 뜸 | 콘솔 접속 불가 | 계정 레벨 비활성화 | AWS 콘솔에서 직렬 콘솔 허용 |
| Login incorrect | OS 로그인 실패 | 비밀번호 미설정 | User Data MIME 멀티파트로 설정 |
| curl 응답 없음 | 인터넷 연결 안 됨 | iptables FORWARD REJECT | sudo iptables -I FORWARD -j ACCEPT |
| NAT 동작 안 함 | 패킷 전달 안 됨 | 소스/대상 확인 활성화 | 소스/대상 확인 비활성화 |
빠른 진단 체크리스트
SSM 연결 안 될 때:
1. IAM Role 있음?
curl http://169.254.169.254/latest/meta-data/iam/info
→ 빈 값이면 Role 없음
2. SSM에 인스턴스 보임?
aws ssm describe-instance-information
→ 빈 리스트면 미등록 상태
3. PingStatus: Online?
→ Online이면 start-session 가능
4. Session Manager Plugin 설치됨?
session-manager-plugin --version
5. Private Subnet이면 NAT 있음?
curl https://checkip.amazonaws.com
→ 응답 없으면 NAT 설정 확인
NAT Instance 체크리스트:
□ Public Subnet에 있는지
□ 소스/대상 확인 비활성화
□ IP 포워딩: cat /proc/sys/net/ipv4/ip_forward → 1
□ iptables FORWARD 체인: ACCEPT 상태인지
□ iptables MASQUERADE 규칙 있는지
□ Private Subnet 라우팅 테이블: 0.0.0.0/0 → NAT Instance
마치며
SSM 세팅 과정은 단순해 보이지만 실제로는 여러 레이어의 설정이 맞물려 있습니다.
- AWS 레벨: IAM Role, 계정 설정
- 네트워크 레벨: 라우팅 테이블, 보안 그룹, NAT
- OS 레벨: SSM Agent, iptables, 비밀번호
어느 하나라도 빠지면 에러가 발생하고, 에러 메시지만으로는 어느 레이어의 문제인지 바로 파악하기 어려웠던 것 같습니다. 그래서 한번에 세팅하여 에러 원인을 찾기보단 레이어 별로 설정한 뒤 테스트 해보는 것을 권장드립니다.