"일단 서버에 올렸는데 외부에서 안 들어와요."
AWS에서 VPC로 격리된 네트워크를 직접 구성하고, EC2에 Nginx를 올려 외부에서 접속 가능한 웹 서비스를 만든 기록입니다. 보안 그룹과 IAM 최소 권한을 적용했습니다.
실습 리소스는 2026-10-12에 모두 삭제했습니다. 문서에 나오는 IP(
43.203.180.77)와 도메인(aws.ficklebobo.dev)은 현재 접속되지 않습니다.
- 네트워크: VPC 1개, Public Subnet 1개, Internet Gateway, Route Table(
0.0.0.0/0 → IGW) - 컴퓨트: Public Subnet에 EC2 1대, SSH 접속, Nginx 설치 및 실행
- 접근 제어: Security Group은 필요한 포트만 허용 (HTTP 80은 전체, SSH 22는 내 IP만)
- 권한: 구축·운영은 IAM 사용자 1개로, EC2/VPC 작업 범위로 제한 (AdministratorAccess 미부여)
- 운영: 실습 종료 후 과금 위험 리소스를 순서대로 정리하고 체크리스트로 근거 기록
- 보너스: HTTPS 적용 (도메인 연결, Let's Encrypt), Docker 컨테이너 배포
| 항목 | 값 |
|---|---|
| 클라우드 | Amazon Web Services |
| 리전 | 서울 (ap-northeast-2), 가용 영역 ap-northeast-2a |
| 접근 방식 | AWS 관리 콘솔 (IAM 사용자 생성 후 구축·운영은 IAM 사용자로만 진행) |
| 인스턴스 | t3.micro 1대 |
| 스토리지 | EBS 8 GiB (gp3) |
| 서버 OS | Ubuntu 26.04 LTS |
| 웹 서버 | Nginx 1.28.3 |
| 작업 환경 | macOS 27.0.1, OpenSSH (.pem 키페어) |
| 컨테이너 | Docker 29.1.3 (docker.io), nginx:alpine |
| 도메인·HTTPS | aws.ficklebobo.dev, Let's Encrypt (certbot 4.0.0) |
미션 제약은 프리 티어 범위 내 진행입니다. 이 실습용으로 AWS 계정을 새로 가입했는데, 기존 계정과 연결되어 무료 플랜을 선택할 수 없어 유료(종량제) 플랜으로 진행했습니다. 그래서 프리 티어 한도가 자동으로 비용을 막아 주지 않았고, 아래처럼 직접 비용을 통제했습니다.
- 리소스 사양: 프리 티어 대상 사양(
t3.micro1대, EBS 8 GiB)만 사용하고, 과금 위험이 있는 NAT Gateway, Elastic IP, ELB, RDS는 만들지 않았습니다. - 시간과 비용: 구축부터 검증까지 1시간 남짓 쓰고 같은 날 전부 삭제했습니다. 단가 기준 추정 비용은 0.1달러 미만이며 정리 후 결제 화면에서 0을 확인했습니다(반영 지연 가능). 방치했다면 월 14달러 안팎입니다. (정리 체크리스트)
.
├── README.md # 개요, 아키텍처, 외부 접속 증빙(방식 A), 보너스 기록
├── docs/
│ ├── architecture.png # 아키텍처 다이어그램
│ ├── architecture.svg # 다이어그램 원본 (SVG)
│ ├── troubleshooting.md # 트러블슈팅 보고서
│ └── cleanup-checklist.md # 리소스 정리 체크리스트
└── images/ # 증빙 스크린샷
| 구분 | 이름 | 설정 |
|---|---|---|
| VPC | bobo-vpc |
10.0.0.0/16 |
| Subnet | bobo-subnet |
10.0.1.0/24, ap-northeast-2a, 퍼블릭 IPv4 자동 할당 활성화 |
| Internet Gateway | bobo-igw |
bobo-vpc에 Attached |
| Route Table | bobo-rt |
10.0.0.0/16 → local, 0.0.0.0/0 → bobo-igw, bobo-subnet에 명시적 연결 |
| Security Group | bobo-web-sg |
80, 443, 8080 ← 0.0.0.0/0, 22 ← 내 IP/32 (443·8080은 보너스 추가, 전체 포트 허용 규칙 없음) |
| EC2 | bobo-web |
t3.micro, Ubuntu 26.04 LTS, 8 GiB gp3, 키 페어 bobo-key, 프라이빗 10.0.1.35, 퍼블릭 43.203.180.77 |
| IAM 사용자 | angry-bobo |
AmazonEC2FullAccess, AmazonVPCFullAccess (AdministratorAccess 미부여) |
실습에는 EC2와 VPC(보안 그룹 포함) 작업만 필요하므로 두 관리형 정책만 연결했습니다. S3, RDS 등 무관한 서비스 권한과 관리자 권한은 부여하지 않았고, IAM 자체를 조회·변경하는 권한도 없습니다.
- 처음에는 루트 계정만 있으므로
angry-bobo는 루트로 생성했고, 연결된 정책도 루트로 한 번 조회했습니다. 결제 화면 확인도 루트로 한 번 했습니다. - 네트워크·컴퓨트·보안 그룹 구축과 운영은 모두 IAM 사용자로 진행했습니다. 아래는
angry-bobo로 서울 리전 콘솔에 접속한 화면입니다.
VPC bobo-vpc(10.0.0.0/16)를 만들었습니다.
인터넷 게이트웨이 bobo-igw를 만들어 bobo-vpc에 연결(Attach)했습니다.
라우트 테이블 bobo-rt에 0.0.0.0/0 → bobo-igw 라우트를 추가했습니다.
bobo-subnet을 bobo-rt에 명시적으로 연결했습니다. 인터넷으로 가는 라우트를 가진 라우트 테이블에 연결되어야 Public Subnet이 됩니다.
서브넷 상세 화면입니다. 10.0.1.0/24, 퍼블릭 IPv4 주소 자동 할당 예, 연결된 라우팅 테이블 bobo-rt를 확인할 수 있습니다.
bobo-web-sg 인바운드 규칙입니다. 기본 구성(80, 22)에 보너스에서 8080, 443을 추가한 최종 상태입니다.
- HTTP(80), HTTPS(443), Docker(8080)는 누구나 접근하도록
0.0.0.0/0으로 열었습니다. - SSH(22)는 학습자 개인 IP(
/32)로만 제한했습니다. - 아웃바운드는 기본값(모든 트래픽)입니다.
Ubuntu 26.04 LTS, t3.micro, 키 페어 bobo-key로 인스턴스를 시작했습니다. 네트워크는 bobo-vpc / bobo-subnet, 퍼블릭 IP 자동 할당은 활성화, 보안 그룹은 기존 bobo-web-sg를 선택했고, 스토리지는 8 GiB gp3입니다.
인스턴스가 실행 중입니다. 퍼블릭 IPv4 43.203.180.77, 프라이빗 IPv4 10.0.1.35가 할당됐습니다.
EC2(bobo-web, 퍼블릭 IP 43.203.180.77)에 SSH로 접속해 Nginx를 설치하고 확인한 과정입니다. 시간은 모두 UTC입니다.
키 파일 권한을 400으로 제한한 뒤 접속했습니다. 처음 접속이라 호스트 키 등록 확인이 나옵니다.
$ chmod 400 ~/Downloads/bobo-key.pem$ ssh -i ~/Downloads/bobo-key.pem ubuntu@43.203.180.77
The authenticity of host '43.203.180.77 (43.203.180.77)' can't be established.
ED25519 key fingerprint is: SHA256:<생략>
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '43.203.180.77' (ED25519) to the list of known hosts.
Welcome to Ubuntu 26.04 LTS (GNU/Linux 7.0.0-1006-aws x86_64)
System information as of Mon Oct 12 12:12:56 UTC 2026
System load: 0.05 Temperature: -273.1 C
Usage of /: 30.5% of 6.61GB Processes: 120
Memory usage: 26% Users logged in: 0
Swap usage: 0% IPv4 address for ens5: 10.0.1.35
ubuntu@ip-10-0-1-35:~$배너의 문서 링크, ESM 안내, 라이선스 문구는 생략했습니다.
Public Subnet의 인스턴스에서 인터넷으로 나가는 통신이 되는지 확인했습니다. (Route Table의 0.0.0.0/0 → IGW가 동작한다는 증거)
ubuntu@ip-10-0-1-35:~$ curl -I https://example.com
HTTP/2 200
date: Mon, 12 Oct 2026 12:13:56 GMT
content-type: text/html; charset=utf-8
server: cloudflare
last-modified: Fri, 09 Oct 2026 20:19:47 GMT
allow: GET, HEAD
accept-ranges: bytes
age: 79
cf-cache-status: HIT
cf-ray: a496057eab27ea8b-ICN
alt-svc: h3=":443"; ma=86400ubuntu@ip-10-0-1-35:~$ sudo apt update
Hit:1 http://ap-northeast-2.ec2.archive.ubuntu.com/ubuntu resolute InRelease
Get:2 http://ap-northeast-2.ec2.archive.ubuntu.com/ubuntu resolute-updates InRelease [137 kB]
Get:3 http://ap-northeast-2.ec2.archive.ubuntu.com/ubuntu resolute-backports InRelease [137 kB]
Get:4 http://ap-northeast-2.ec2.archive.ubuntu.com/ubuntu resolute/universe amd64v3 Packages [16.3 MB]
...
Get:50 http://security.ubuntu.com/ubuntu resolute-security/multiverse amd64 c-n-f Metadata [120 B]
Fetched 32.4 MB in 4s (7,933 kB/s)
189 packages can be upgraded. Run 'apt list --upgradable' to see them.
Get:5~Get:49는 같은 형식이라 생략했습니다.
ubuntu@ip-10-0-1-35:~$ sudo apt install -y nginx
Installing:
nginx
Installing dependencies:
nginx-common
Summary:
Upgrading: 0, Installing: 2, Removing: 0, Not Upgrading: 189
Download size: 666 kB
Space needed: 1,880 kB / 4,662 MB available
Get:1 http://ap-northeast-2.ec2.archive.ubuntu.com/ubuntu resolute-updates/main amd64v3 nginx-common all 1.28.3-2ubuntu1.11 [37.9 kB]
Get:2 http://ap-northeast-2.ec2.archive.ubuntu.com/ubuntu resolute-updates/main amd64v3 nginx amd64 1.28.3-2ubuntu1.11 [628 kB]
Fetched 666 kB in 0s (2,702 kB/s)
Setting up nginx-common (1.28.3-2ubuntu1.11)...
Created symlink '/etc/systemd/system/multi-user.target.wants/nginx.service' → '/usr/lib/systemd/system/nginx.service'.
Setting up nginx (1.28.3-2ubuntu1.11)...패키지 압축 해제와 재시작 필요 여부 점검 출력은 생략했습니다. (
No services need to be restarted.등)
enabled이므로 재부팅 후에도 자동 시작됩니다.
ubuntu@ip-10-0-1-35:~$ systemctl status nginx --no-pager
● nginx.service - A high performance web server and a reverse proxy server
Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
Active: active (running) since Mon 2026-10-12 12:15:34 UTC; 23s ago
Invocation: 3ffc6e1729f0456b8e5a88c3a7a672c1
Docs: man:nginx(8)
Main PID: 1715 (nginx)
Tasks: 3 (limit: 627)
Memory: 3M (peak: 7.2M)
CPU: 46ms
CGroup: /system.slice/nginx.service
├─1715 "nginx: master process /usr/sbin/nginx -g daemon on; master_process on;"
├─1718 "nginx: worker process"
└─1719 "nginx: worker process"ubuntu@ip-10-0-1-35:~$ curl -I http://localhost
HTTP/1.1 200 OK
Server: nginx/1.28.3 (Ubuntu)
Date: Mon, 12 Oct 2026 12:16:17 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Mon, 12 Oct 2026 12:15:33 GMT
Connection: keep-alive
ETag: "6acccf65-267"
Accept-Ranges: bytes- 방식: A (브라우저에서
http://<퍼블릭IP>접속) - 접속 정보:
http://43.203.180.77 - 결과: Nginx 기본 페이지(
Welcome to nginx!) 정상 표시
위 화면은 HTTPS 보너스를 적용하기 전에 캡처한 것입니다.
인스턴스 내부에서의 확인(SSH 접속, 아웃바운드 통신, localhost 응답)은 스크린샷 대신 위 서버 작업 로그의 터미널 출력으로 남겼습니다.
같은 EC2에 Docker를 설치하고 Nginx 컨테이너를 띄워 외부에서 접속했습니다. 호스트의 Nginx가 이미 80 포트를 쓰고 있어서 컨테이너는 호스트 8080 → 컨테이너 80으로 매핑했습니다.
| 항목 | 값 |
|---|---|
| Docker | docker.io 29.1.3 (Ubuntu 기본 저장소) |
| 이미지 | nginx:alpine (Docker Hub) |
| 컨테이너 이름 | bobo-nginx |
| 포트 매핑 | 호스트 8080 → 컨테이너 80 |
| 접속 URL | http://43.203.180.77:8080 |
ubuntu@ip-10-0-1-35:~$ sudo apt install -y docker.io
Installing:
docker.io
Installing dependencies:
bridge-utils containerd dns-root-data dnsmasq-base pigz runc ubuntu-fan
Summary:
Upgrading: 0, Installing: 8, Removing: 0, Not Upgrading: 189
Download size: 74.0 MB
Space needed: 281 MB / 4,660 MB available
Fetched 74.0 MB in 1s (86.9 MB/s)
...
Setting up containerd (2.2.2-0ubuntu1.1)...
Setting up docker.io (29.1.3-0ubuntu4.1)...
Created symlink '/etc/systemd/system/multi-user.target.wants/docker.service' → '/usr/lib/systemd/system/docker.service'.
Created symlink '/etc/systemd/system/sockets.target.wants/docker.socket' → '/usr/lib/systemd/system/docker.socket'.
Get,Unpacking,Selecting,Preparing to unpack줄과 재시작 점검 출력은 생략했습니다.
ubuntu@ip-10-0-1-35:~$ docker -v
Docker version 29.1.3, build 29.1.3-0ubuntu4.1컨테이너를 띄우기 전에는 8080을 듣는 프로세스가 없어서 접속이 즉시 실패합니다.
ubuntu@ip-10-0-1-35:~$ curl -I http://localhost:8080
curl: (7) Failed to connect to localhost port 8080 after 1 ms: Could not connect to server이미지는 로컬에 없어서 Docker Hub에서 받아온 뒤 실행됩니다.
ubuntu@ip-10-0-1-35:~$ sudo docker run -d --name bobo-nginx -p 8080:80 nginx:alpine
Unable to find image 'nginx:alpine' locally
alpine: Pulling from library/nginx
e2de96513ba9: Pull complete
...
d54d3939625e: Download complete
Digest: sha256:df221db836e1754089190208cee7eeda94f233197056426eda74a43ab1abeac2
Status: Downloaded newer image for nginx:alpine
4dd77b90f11869570e6a68d66bce5e20f7f47a27ec04ac26514caae40dfa339fSTATUS가 Up이고 PORTS에 0.0.0.0:8080->80/tcp 매핑이 보입니다. 아래 화면은 실행 4분 뒤에 캡처했습니다.
ubuntu@ip-10-0-1-35:~$ curl -I http://localhost:8080
HTTP/1.1 200 OK
Server: nginx/1.31.6
Date: Mon, 12 Oct 2026 12:43:09 GMT
Content-Type: text/html
Content-Length: 896
Last-Modified: Tue, 15 Sep 2026 14:18:52 GMT
Connection: keep-alive
ETag: "6aa953cc-380"
Accept-Ranges: bytesServer: nginx/1.31.6은 컨테이너 안의 Nginx입니다. 호스트에 설치한 Nginx(1.28.3)와 버전이 달라 서로 다른 프로세스가 응답했음을 확인할 수 있습니다.
보안 그룹에 8080을 열기 전에는 같은 URL이 타임아웃됐고, bobo-web-sg에 8080/TCP ← 0.0.0.0/0 규칙을 추가해 해결했습니다. 원인 추적 과정은 트러블슈팅 보고서에 정리했습니다.
규칙을 추가한 뒤에는 http://43.203.180.77:8080에서 컨테이너의 Nginx 화면이 표시됩니다.
보유 도메인의 서브도메인을 EC2에 연결하고, 호스트 Nginx에 Let's Encrypt 인증서를 적용했습니다.
| 항목 | 값 |
|---|---|
| 도메인 | aws.ficklebobo.dev (보유 도메인의 서브도메인) |
| DNS | 가비아 DNS, A 레코드 aws → 43.203.180.77 (TTL 600) |
| 적용 대상 | 호스트 Nginx (/etc/nginx/sites-enabled/default) |
| 인증서 | Let's Encrypt, ECDSA, 만료 2027-01-10 (90일), certbot.timer로 자동 갱신 |
| 접속 URL | https://aws.ficklebobo.dev (HTTP는 301로 HTTPS 리다이렉트) |
블로그에 쓰는 도메인이라 기존 레코드(@, www)는 그대로 두고 서브도메인 A 레코드 한 줄만 추가했습니다.
인증서 발급은 80 포트로 진행하지만, 발급 후 HTTPS 접속을 받으려면 443이 열려 있어야 합니다. bobo-web-sg에 HTTPS / 443 / 0.0.0.0/0 규칙을 추가했습니다. (최종 규칙은 보안 그룹 참고)
certbot의 Nginx 플러그인은 도메인과 일치하는 server_name을 가진 서버 블록에 인증서를 설정합니다. 기본 사이트의 server_name _;을 도메인으로 바꾸고 설정을 검증했습니다.
ubuntu@ip-10-0-1-35:~$ sudo grep -n server_name /etc/nginx/sites-enabled/default
46: server_name _;
83:# server_name example.com;ubuntu@ip-10-0-1-35:~$ sudo sed -i 's/server_name _;/server_name aws.ficklebobo.dev;/' /etc/nginx/sites-enabled/defaultubuntu@ip-10-0-1-35:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successfulubuntu@ip-10-0-1-35:~$ sudo systemctl reload nginxubuntu@ip-10-0-1-35:~$ sudo apt install -y certbot python3-certbot-nginx
Installing:
certbot python3-certbot-nginx
Installing dependencies:
python3-acme python3-certbot python3-configargparse python3-icu python3-josepy python3-parsedatetime python3-pytz python3-rfc3339
Summary:
Upgrading: 0, Installing: 10, Removing: 0, Not Upgrading: 189
Download size: 1,249 kB
Space needed: 6,407 kB / 4,282 MB available
Fetched 1,249 kB in 2s (597 kB/s)
...
Setting up certbot (4.0.0-4)...
Created symlink '/etc/systemd/system/timers.target.wants/certbot.timer' → '/usr/lib/systemd/system/certbot.timer'.
Setting up python3-certbot-nginx (4.0.0-3)...
Get,Unpacking,Selecting,Preparing to unpack줄과 재시작 점검 출력은 생략했습니다.
이메일 등록은 선택 사항이라 건너뛰고 약관에만 동의했습니다.
ubuntu@ip-10-0-1-35:~$ sudo certbot --nginx -d aws.ficklebobo.dev
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Enter email address or hit Enter to skip.
(Enter 'c' to cancel):
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at:
https://letsencrypt.org/documents/LE-SA-v1.8-July-06-2026.pdf
You must agree in order to register with the ACME server. Do you agree?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: y
Account registered.
Requesting a certificate for aws.ficklebobo.dev
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/aws.ficklebobo.dev/fullchain.pem
Key is saved at: /etc/letsencrypt/live/aws.ficklebobo.dev/privkey.pem
This certificate expires on 2027-01-10.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.
Deploying certificate
Successfully deployed certificate for aws.ficklebobo.dev to /etc/nginx/sites-enabled/default
Congratulations! You have successfully enabled HTTPS on https://aws.ficklebobo.dev후원 안내 문구는 생략했습니다.
HTTPS는 200, HTTP는 301로 HTTPS 주소로 이동합니다. 인증서는 VALID: 89 days로 조회됩니다.
ubuntu@ip-10-0-1-35:~$ curl -I https://aws.ficklebobo.dev
HTTP/1.1 200 OK
Server: nginx/1.28.3 (Ubuntu)
Date: Mon, 12 Oct 2026 12:59:06 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Mon, 12 Oct 2026 12:15:33 GMT
Connection: keep-alive
ETag: "6acccf65-267"
Accept-Ranges: bytesubuntu@ip-10-0-1-35:~$ curl -I http://aws.ficklebobo.dev
HTTP/1.1 301 Moved Permanently
Server: nginx/1.28.3 (Ubuntu)
Date: Mon, 12 Oct 2026 12:59:11 GMT
Content-Type: text/html
Content-Length: 178
Connection: keep-alive
Location: https://aws.ficklebobo.dev/ubuntu@ip-10-0-1-35:~$ sudo certbot certificates
Saving debug log to /var/log/letsencrypt/letsencrypt.log
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Found the following certs:
Certificate Name: aws.ficklebobo.dev
Serial Number: 577aa7e5a1c21fa820f0ae488a36dcb3c80
Key Type: ECDSA
Domains: aws.ficklebobo.dev
Expiry Date: 2027-01-10 11:59:40+00:00 (VALID: 89 days)
Certificate Path: /etc/letsencrypt/live/aws.ficklebobo.dev/fullchain.pem
Private Key Path: /etc/letsencrypt/live/aws.ficklebobo.dev/privkey.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -브라우저 접속 결과입니다. 최신 Chrome은 주소창에서 https://를 숨기고 자물쇠를 설정 아이콘으로 표시해서, 아이콘을 눌러 연 연결 상태 팝업을 함께 남겼습니다.
인증서 정보 화면입니다. 발급 기관 Let's Encrypt, 발급 대상 aws.ficklebobo.dev, 만료일 2027-01-10(KST 20:59:40, certbot certificates의 UTC 11:59:40과 동일)을 확인할 수 있습니다.
















