AWS 콘솔을 처음 열면 서비스 목록은 길고, 무엇부터 만들어야 하는지는 알려 주지 않는다. EC2부터 띄우고 싶어도 EC2를 넣을 서브넷이 없고, 서브넷을 만들어도 라우팅 테이블이 없으면 밖으로 나가지 못한다.
책 한 권을 따라 VPC부터 S3까지 웹 서비스 하나가 돌아갈 만한 인프라를 콘솔로 직접 만들어 봤다. 이 글에서는 만든 순서대로 설정값을 정리하고, 중간중간 왜 그렇게 나누는지를 짧게 적는다.
완성된 구성은 이렇다.
flowchart TB
U["사용자"] --> IGW["Internet Gateway"]
Admin["관리자 (SSH)"] --> IGW
subgraph VPC["VPC 10.0.0.0/16"]
subgraph PUB["퍼블릭 서브넷 1, 2"]
ALB["ALB (HTTP 80)"]
BAS["점프서버"]
NAT1["NAT Gateway 1"]
NAT2["NAT Gateway 2"]
end
subgraph PRI["프라이빗 서브넷 1, 2"]
WEB1["web01 :3000"]
WEB2["web02 :3000"]
RDS["RDS MySQL"]
end
end
IGW --> ALB
IGW --> BAS
ALB --> WEB1
ALB --> WEB2
BAS -. SSH .-> WEB1
BAS -. SSH .-> WEB2
WEB1 --> RDS
WEB2 --> RDS
WEB1 -. IAM 역할 .-> S3[("S3 버킷 (VPC 밖)")]네트워크: VPC, 서브넷, 게이트웨이
VPC의 CIDR은 10.0.0.0/16으로 잡았다.
서브넷은 VPC의 IP 주소 범위를 나누는 단위다. 나누는 이유는 두 가지다. 외부에 공개하는 리소스와 그렇지 않은 리소스를 가르는 역할 분리, 그리고 AWS 안에서 물리적으로 이중화하는 기기 분리다. 퍼블릭과 프라이빗을 두 개씩 만든 것도 이 때문이다.
| 서브넷 | CIDR |
|---|---|
| public1 | 10.0.0.0/20 |
| public2 | 10.0.16.0/20 |
| private1 | 10.0.64.0/20 |
| private2 | 10.0.80.0/20 |
Internet Gateway를 만들어 VPC에 연결한다. NAT Gateway는 프라이빗 서브넷 두 개에 하나씩 대응하도록 두 개를 만들어 퍼블릭 서브넷에 하나씩 두고, NAT Gateway마다 EIP를 할당한다.
퍼블릭과 프라이빗을 가르는 건 라우팅 테이블이다
서브넷 이름에 public, private을 붙였다고 성격이 정해지지 않는다. 서브넷 밖으로 나가는 경로가 Internet Gateway인지 NAT Gateway인지가 그 서브넷의 성격을 정한다. 그 경로를 적는 곳이 라우팅 테이블이다.
퍼블릭 라우팅 테이블 1개
라우팅:
0.0.0.0/0→ Internet Gateway서브넷 연결: 퍼블릭 서브넷 2개 모두
프라이빗 라우팅 테이블 2개
라우팅: 각각
0.0.0.0/0→ NAT Gateway서브넷 연결: 프라이빗 서브넷을 1개씩 나눠 맡는다
라우팅 테이블과 서브넷은 1:n이라서 여러 서브넷이 라우팅 테이블 하나를 같이 쓸 수 있다. 퍼블릭은 나가는 곳이 Internet Gateway 하나뿐이라 테이블 하나로 충분하고, 프라이빗은 서브넷마다 NAT Gateway가 달라서 테이블을 두 개 만든다.
라우팅 정보에 넣는 대상의 순서는 상관없다. AWS 라우팅 테이블은 목적지 CIDR 블록과 일치하는 경로 중 가장 구체적인 것을 찾아 쓰기 때문이다.
일반적인 네트워크 설계에서는 이런 설정을 라우터(router)라는 기기에 하지만, AWS 콘솔에서는 라우터를 따로 만들 필요가 없다. 라우팅 테이블을 만들면 라우터에 해당하는 것도 함께 생긴다.
보안그룹 두 개와 점프서버
보안그룹은 두 개를 만들었다. 둘 다 인바운드만 설정한다.
| 보안그룹 | 인바운드 |
|---|---|
| 점프서버(bastion host)용 | SSH(22) / Anywhere-IPv4(0.0.0.0/0) |
| 로드밸런서용 | HTTP(80), HTTPS(443) / Anywhere-IPv4(0.0.0.0/0) |
보안그룹은 EC2, 로드밸런서, RDS 같은 리소스 단위로 거는 설정이고, 네트워크 ACL은 서브넷 단위로 걸려 그 안의 리소스 전부에 적용된다. 책의 저자는 설정 누락을 인프라 설계서나 CloudFormation 같은 IaC로 막고, 네트워크 ACL은 잘 쓰지 않는다고 한다.
점프서버를 두는 이유는 이렇다. 외부에서 리소스로 들어오는 접속은 제한된 관리자만 할 수 있어야 하는데, 그 설정을 모든 리소스에 하나하나 하기는 어렵고 빠뜨리기도 쉽다. 그래서 입구를 하나 두고 그 서버를 거쳐야만 각 리소스에 들어갈 수 있게 한다. 목적한 리소스로의 통신 외에는 쓸 일이 없으니 성능이 낮아도 되고 OS도 가리지 않는다.
실습이라 SSH를
0.0.0.0/0으로 열었지만, 실제로 쓸 서버라면 소스를 관리자 IP로 좁히는 편이 낫다.
키페어(pem)를 하나 만들고 점프서버를 띄운다.
AMI: Amazon Linux 2 AMI(HVM) - Kernel 5.10, SSD Volume Type
키페어: 앞에서 만든 키페어
네트워크: 퍼블릭 서브넷 01(이 EC2가 퍼블릭 서브넷 01 안에 생긴다), 퍼블릭 IP 자동 할당 활성화
보안그룹:
default,sg-bastion두 개
접속은 pem 키가 있는 위치에서 한다. 처음에는 키 권한이 너무 열려 있다며 거부당한다.
# pem 키가 있는 위치에서
ssh -i {pem키 이름} ec2-user@{Bastion Public IP}
# 키 권한이 너무 열려 있다는 에러가 난다(보호되지 않은 키).
# 다른 사용자가 읽을 수 없게 권한을 줄인다
chmod 400 {pem키 이름}
# 다시 접속하면 성공
ssh -i {pem키 이름} ec2-user@{Bastion Public IP}
# 로그아웃은 logout, exit, Ctrl+D 중 하나
키페어가 GitHub 같은 저장소에 올라가는 걸 막아 주는 git-secrets라는 도구도 있다. 나중에 마이크로서비스 쪽에 관심이 생기면 AWS의 서버리스 컴퓨팅 유스케이스도 읽어 볼 생각이다.
웹 서버 두 대와 로드밸런서
웹 서버는 점프서버와 같은 AMI, 같은 키페어로 만들되 네트워크 설정이 다르다.
서브넷: 프라이빗 서브넷 01
퍼블릭 IP 자동 할당: 비활성화
보안그룹:
default하나
같은 설정으로 한 대를 더 만들고 서브넷만 프라이빗 서브넷 02로 바꾼다. 이제 점프서버에는 default + SSH, 웹 서버에는 default만 붙어 있다.
웹 서버에 SSH 규칙이 없는데도 점프서버에서 들어갈 수 있는 건 default 보안그룹 덕분이다. default 보안그룹의 기본 인바운드 규칙은 같은 보안그룹이 붙은 리소스끼리의 트래픽을 허용한다. 점프서버, 웹 서버, 로드밸런서가 모두 default를 달고 있으니 서로 통한다.
로드밸런서가 하는 일
로드밸런서의 주요 역할은 세 가지다.
요청 분산
SSL(Secure Socket Layer) 처리. 외부에서 들어온 HTTPS 요청 패킷을 복호화해 웹 서버에 넘기고, 나가는 응답 패킷은 암호화한다. 웹 서버가 암복호화를 다 하면 부하 때문에 비효율적이다.
부정 요청 대응
여기에 요청 라우팅(Request Routing)이 붙는다. 외부에 공개한 프로토콜·포트 조합으로 들어온 요청을, 내부 웹 서버가 받는 프로토콜·포트로 바꿔 주는 기능이다. 하나의 로드밸런서에 대상 그룹을 여러 개 지정할 수도 있다.
로드밸런서는 인터넷 게이트웨이로 가는 경로가 있는 서브넷, 여기서는 퍼블릭 서브넷에 두어야 한다. 서브넷을 잘못 고르면 외부에서 웹 서버에 도달하지 못한다.
리스너를 만들다가 대상 그룹을 만든다
ALB 설정은 크게 두 덩어리다. 로드밸런서 쪽은 어떤 프로토콜(HTTP, HTTPS)로 받을지처럼 인터넷에서 로드밸런서로 들어올 때의 설정이고, 클라이언트 요청을 받는 “리스너"가 여기 있다. 대상 그룹은 그 요청을 넘겨받을 서버 쪽 설정이다.
로드밸런서 설정:
Network mapping: 퍼블릭 서브넷 2개
보안그룹:
default, 앞에서 만든sj-sg-elbListeners and routing: HTTP, 80 포트를 설정하고 “Create target group"을 누른다

리스너를 HTTP:80으로 두고 Create target group으로 넘어간다.
대상 그룹 설정:
Choose a target type: Instances
Protocol: HTTP, Port: 3000. 외부에서 인터넷으로 들어올 때는 HTTP 80으로 받지만, 로드밸런서가 웹 서버로 넘길 때는 HTTP 3000을 쓴다.
Register targets: 웹 서버 2대를 고르고 “Include as pending below”

웹 서버 두 대를 3000 포트로 골라 pending 목록에 넣는다.
대상 그룹을 만든 뒤 로드밸런서 화면으로 돌아와 Listeners and routing의 Default action에 방금 만든 대상 그룹을 넣는다.

Default action이 새 대상 그룹으로 요청을 넘기도록 지정한다.
헬스체크가 통과해야 요청이 흐른다
ssh web01, ssh web02로 각 웹 서버에 들어가 같은 작업을 한다.
먼저 index.html 파일을 만든다.
<html><body>hello world</body></html>
그다음 Amazon Linux에 기본으로 깔린 파이썬으로 HTTP 서버를 띄운다.
python -m SimpleHTTPServer 3000
index.html이 있는 디렉터리에서 실행한다. 제대로 떴다면 로드밸런서가 헬스체크를 할 때마다 액세스 로그가 주기적으로 찍힌다.

로드밸런서의 헬스체크 요청이 주기적으로 들어온다.
여기까지 해도 로드밸런서는 곧바로 요청을 넘기지 않는다. 헬스체크를 몇 차례 보내 모두 성공한 뒤에야 라우팅을 시작한다. EC2 대시보드 > Target groups > 상세 페이지 > [Targets] 탭의 Health status가 healthy가 되면 준비가 끝난 것이다.
로드밸런서를 눌러 DNS 이름을 확인하고, 그 주소로 브라우저에서 접속하면 hello world가 뜬다. 확인이 끝나면 파이썬 서버를 Ctrl+C로 끄고, 더는 필요 없는 index.html도 지운다.
RDS는 그룹 세 개를 먼저 만든다
데이터베이스 인스턴스를 만들기 전에 그룹 세 개를 만든다.
파라미터 그룹: 데이터베이스 엔진 고유의 설정. 성능 개선, 사용 현황 파악, 기능 추가, 사용하는 언어나 튜닝 설정을 여기서 한다.
옵션 그룹: AWS를 이용한 데이터베이스 모니터링처럼 주로 RDS 고유의 설정을 한다.
서브넷 그룹: 데이터베이스 서버를 여러 가용 영역에 나눠 둘 때 쓰는 설정이다. 데이터베이스도 서버를 여러 대 두면 신뢰성이나 성능을 높일 수 있다.
가용 영역: VPC에서 쓰는 가용 영역
서브넷: 각 가용 영역의 private 서브넷
데이터베이스는 보안을 위해 기본적으로 퍼블릭 서브넷을 서브넷 그룹에 넣지 않는다.
그다음 데이터베이스를 만든다.
| 항목 | 값 |
|---|---|
| 데이터베이스 생성 방식 | 표준 생성 |
| 엔진 옵션 | MySQL |
| 템플릿 | 프리 티어 |
| 마스터 사용자 이름 | admin |
| 마스터 암호 | Masterpw |
| 최초 데이터베이스 이름 | (공란) |
마스터 암호에 특수문자를 넣었더니 마지막 연결 단계에서 Access denied for user로 실패했다. 문자로만 된 암호로 바꾸고 나서야 붙었다.
연결은 웹 서버에서 확인한다. Amazon Linux 2에는 MySQL 명령어가 기본으로 없어서 따로 설치한다.
# 1. 웹 서버 접속
ssh web01
# 2. MySQL 클라이언트 설치
sudo yum -y install mysql
# 3. RDS 대시보드 > 데이터베이스 > 생성한 인스턴스 > '연결 & 보안' 탭에서 엔드포인트와 포트 확인
# 4. 비밀번호까지 입력해 "mysqld is alive"가 나오면 연결 확인 완료
mysqladmin ping -u admin -p -h {데이터베이스의 엔드포인트}
S3는 VPC 밖에 있다
EC2 인스턴스는 SSD나 하드 디스크에 해당하는 스토리지를 EBS(Amazon Elastic Block Store)라는 이름으로 제공한다. EBS를 스토리지로 쓰면 이런 부담이 따른다.
EC2 인스턴스의 OS 자체도 관리해야 한다(보안 대응 등).
예측하지 못한 장애에 대비해야 한다.
EC2 인스턴스를 쓰지 못하게 될 수 있다. SLA(서비스 품질 보증)에 따르면 1년에 약 5분 정도 서비스가 멈출 수 있다.
이 문제를 풀려고 AWS가 제공하는 서비스가 S3다. 책에서는 S3가 내결함성과 비용 모두 EBS보다 압도적으로 낫다고 한다. 대신 어디까지나 외부 스토리지 서비스라서, EBS처럼 C 드라이브, D 드라이브로 쓰거나 리눅스·맥의 파일시스템처럼 마운트할 수는 없다.
지금까지 만든 서비스는 전부 VPC 안에 있었다. S3는 VPC 밖에 생기므로, EC2에서 S3에 닿으려면 네트워크가 아니라 권한을 준비해야 한다. 접근 방법은 인터넷에서 직접 들어가는 것과 VPC에서 들어가는 것 두 가지이고, VPC에서 들어가려면 S3 버킷 접근 권한이 필요하다. 이 권한은 보통 IAM 역할(role)로 준다. S3 버킷에 접근하는 정책을 가진 역할을 만들고, 그 역할을 EC2에 붙인다.
기술적으로는 S3 접근 정책을 가진 사용자를 만들고 그 사용자의 액세스 키로 버킷에 접근할 수도 있다. 다만 역할을 쓰는 쪽이 더 흔하다.
버킷을 만들 때는 “이 버킷의 퍼블릭 액세스 차단 설정"에서 “모든 퍼블릭 액세스 차단”(기본값)을 그대로 둔다. 의도하지 않은 열람이나 변경을 막아 두고, 버킷을 만든 뒤 용도에 맞게 필요한 권한만 여는 게 좋다.
버킷 하나에만 닿는 정책을 만든다
AmazonS3FullAccess 정책을 그대로 써도 되지만, 이번에는 특정 버킷에만 접근할 수 있는 정책을 만들었다. AmazonS3FullAccess의 내용은 이렇다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:*",
"s3-object-lambda:*"
],
"Resource": "*"
}
]
}
여기서 Resource만 원하는 버킷으로 좁힌다. 버킷 자체와 버킷 안의 객체(/*)를 둘 다 적는다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:*",
"s3-object-lambda:*"
],
"Resource": [
"arn:aws:s3:::(bucketname)",
"arn:aws:s3:::(bucketname)/*"
]
}
]
}
이 정책으로 역할을 만든다.
신뢰할 수 있는 엔터티 유형: AWS 서비스 / EC2
권한 추가 > 권한 정책: 방금 만든 정책
그리고 EC2 콘솔 > 웹 서버 인스턴스 > 작업 > 보안 > IAM 역할 수정에서 이 역할을 붙인다.
역할을 붙인 인스턴스에서 업로드해 본다
Amazon Linux 2에는 S3 같은 AWS 리소스를 다루는 명령어가 들어 있다. 웹 서버에서 파일 하나를 올려 봤다.
# 웹 서버 홈 디렉터리에서 test.txt를 만든 뒤
aws s3 cp test.txt s3://{버킷이름}
업로드가 끝났다. 인스턴스에 액세스 키를 넣어 두지 않아도, 붙어 있는 역할이 권한을 준다.
정리
서브넷의 성격은 라우팅 테이블이 정한다.
0.0.0.0/0이 Internet Gateway로 가면 퍼블릭, NAT Gateway로 가면 프라이빗이다.웹 서버와 DB는 프라이빗에 두고, 밖에서는 점프서버(SSH)와 로드밸런서(HTTP)로만 들어온다.
로드밸런서는 80으로 받아 대상 그룹의 3000으로 넘기고, 헬스체크가 통과한 뒤에야 요청을 보낸다.
RDS는 파라미터·옵션·서브넷 그룹을 먼저 만들고, 서브넷 그룹에는 프라이빗 서브넷만 넣는다.
S3는 VPC 밖에 있어서 EC2에 버킷을 좁힌 IAM 역할을 붙여 접근한다.
만든 순서를 다시 보면 네트워크, 입구, 서버, 데이터 순이다. 한 번 손으로 끝까지 만들어 보니 콘솔의 메뉴가 어떤 순서로 이어지는지가 보였다. 여기까지는 로드밸런서의 긴 DNS 이름으로 접속했고, 다음 글에서 도메인을 붙인다.