Platform

AWS 기반 웹 서비스 인프라 구성과 운영 관점

Route 53, IAM, EC2, S3, CloudFront, ELB, Auto Scaling, CloudWatch를 하나의 웹 서비스 요청 흐름으로 연결해 정리합니다.

AWS 기반 웹 서비스 인프라 구성과 운영 관점

AWS를 처음 공부할 때는 EC2, S3, IAM, Route 53처럼 서비스 단위로 정리하기 쉽다. 그런데 실제 웹 서비스를 구성하려고 하면 서비스 이름보다 먼저 요청의 흐름이 보인다. 사용자는 도메인으로 접속하고, DNS는 트래픽을 어느 엔드포인트로 보낼지 결정한다. 정적 파일은 S3에 둘 수 있지만, 사용자에게 직접 S3 URL을 열어줄지 CloudFront를 앞에 둘지에 따라 보안과 캐시 전략이 달라진다. 백엔드 서버는 EC2에 직접 노출할 수도 있지만, 보통은 ALB 뒤에 두고 헬스체크와 스케일링 기준을 함께 잡아야 한다.

이 글은 Route 53, IAM, EC2, EBS, S3, CloudFront, ELB, Auto Scaling, CloudWatch를 하나의 웹 서비스 인프라 흐름으로 다시 정리한 기록이다. 각각의 기능을 외우는 것보다, 요청이 지나가는 경로와 운영 중 확인해야 할 지점을 연결하는 데 초점을 맞췄다.

1. DNS에서 시작되는 요청 흐름

도메인은 사람이 기억하는 이름이고, DNS는 그 이름을 실제 접근 가능한 주소로 바꿔준다. Route 53은 AWS에서 제공하는 DNS 서비스이며 Hosted Zone 안에 여러 Record를 만든다. 기본적으로 A 레코드는 IPv4 주소를, AAAA 레코드는 IPv6 주소를, CNAME은 다른 도메인 이름을 가리킨다. Route 53은 EC2, ELB, S3, CloudFront와 연결되기 때문에 AWS 기반 웹 서비스의 첫 진입점이 된다.

정적 웹 사이트라면 도메인을 CloudFront 배포 도메인으로 연결하고, 백엔드 API라면 ALB DNS 이름을 가리키게 만들 수 있다. 운영 환경에서는 단순 A 레코드 하나보다 라우팅 정책도 중요하다. Latency based routing은 사용자와 가까운 리전에 요청을 보내고, Weighted routing은 특정 비율로 트래픽을 나누며, Geo routing은 사용자의 위치에 따라 다른 엔드포인트를 선택한다.

실습에서는 Hosted Zone을 만들고, example.com 같은 도메인에 A 레코드를 추가해 특정 IP로 트래픽을 보냈다. 이때 Hosted Zone은 레코드의 컨테이너이고, Record는 특정 이름을 어디로 보낼지 정하는 규칙이라는 점이 핵심이었다.

2. IAM은 기능보다 먼저 정리해야 하는 경계

AWS 리소스를 만들기 시작하면 IAM을 피할 수 없다. IAM은 User, Group, Role, Policy로 구성된다. User는 실제 사람이나 애플리케이션을 나타내고, Group은 여러 사용자에게 같은 권한을 주기 위한 묶음이다. Role은 특정 주체가 임시로 맡을 수 있는 권한 집합이고, Policy는 어떤 리소스에 어떤 작업을 허용하거나 거부할지 정의하는 JSON 문서다.

IAM 실습에서는 test-iam-user를 만들고 AmazonEC2ReadOnlyAccess만 부여한 뒤 EC2 인스턴스 생성을 시도했다. 결과는 “권한 없음”이었다. 이후 Group을 만들고 AmazonEC2FullAccess를 연결한 뒤 같은 사용자를 Group에 추가하자 EC2 생성이 가능해졌다. 이 실습에서 중요한 부분은 “권한이 있으면 된다”가 아니라, 어떤 작업이 어떤 권한에 의해 허용되는지 확인하는 흐름이었다.

CLI나 SDK를 사용할 때는 Access Key를 만들고 로컬의 .aws/credentials에 저장할 수 있다.

[default]
region = ap-northeast-2
aws_access_key_id = YOUR_AWS_ACCESS_KEY
aws_secret_access_key = YOUR_AWS_SECRET_KEY

다만 장기 Access Key는 편하지만 위험하다. 로컬 실습에서는 사용할 수 있지만, 운영 환경에서는 Role 기반 접근, 최소 권한, 키 회전, Secret 노출 방지를 먼저 고려해야 한다.

3. EC2, 보안 그룹, EBS

EC2는 클라우드에서 가상 서버를 제공하는 서비스다. 인스턴스를 만들 때 AMI, 인스턴스 타입, 키 페어, 보안 그룹, 스토리지 설정을 함께 정한다. 예를 들어 Ubuntu Server 22.04 LTS AMI를 선택하고, 프리티어 실습용으로 t2.micro를 고르고, SSH 접속을 위한 키 페어를 만든다.

Linux/macOS에서는 키 파일 권한을 제한한 뒤 SSH로 접속한다.

chmod 600 my-key.pem
ssh -i my-key.pem ubuntu@<public-ip>

보안 그룹은 EC2 앞의 가상 방화벽처럼 동작한다. 인바운드 규칙은 외부에서 인스턴스로 들어오는 트래픽을, 아웃바운드 규칙은 인스턴스에서 외부로 나가는 트래픽을 제어한다. 실습에서는 SSH 22번 포트를 열었고, 테스트용으로 8000번 포트를 0.0.0.0/0에 열어보기도 했다. 하지만 운영 환경이라면 SSH를 전체 공개로 열어두지 않고, 접근 IP를 제한하거나 Bastion/VPN/SSM 같은 관리 경로를 검토해야 한다.

EBS는 EC2에 연결해서 사용하는 블록 스토리지다. 하나의 EBS 볼륨은 동시에 여러 EC2에 붙일 수 없지만, 하나의 EC2에는 여러 EBS 볼륨을 연결할 수 있다. EBS는 같은 Availability Zone 안에서 EC2와 연결해야 하며, 특정 시점의 백업으로 Snapshot을 만들 수 있다. Snapshot은 S3 기반으로 저장되고 여러 AZ에 복제된다. 이 구조 때문에 서버를 새로 만들 때 AMI나 Snapshot을 기준으로 복원 전략을 세울 수 있다.

4. S3 정적 호스팅과 객체 스토리지

S3는 객체 스토리지다. 파일 시스템처럼 일부만 수정하는 구조가 아니라 객체 단위로 저장하고 읽는다. Bucket은 객체를 담는 최상위 컨테이너이고, Object는 실제 파일이다. Metadata는 객체에 대한 정보이며, Bucket Policy는 접근 권한을 정의한다.

정적 웹 사이트 실습에서는 S3 버킷을 만들고 index.html을 업로드한 뒤 정적 웹 사이트 호스팅을 켰다. 이때 퍼블릭 접근 차단을 해제하고 s3:GetObject 권한을 주면 외부에서 객체를 읽을 수 있다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::your-bucket-name/*"
    }
  ]
}

이 방식은 실습에는 직관적이지만, 운영에서는 S3를 직접 공개하기보다 CloudFront를 앞에 두는 구성이 더 자연스럽다. 사용자에게 S3 URL을 직접 열어주면 TLS, 캐시, 접근 제어, 도메인 연결을 따로 고민해야 한다. CloudFront를 앞에 두면 사용자 진입점은 CloudFront가 되고, S3는 원본 저장소 역할에 집중할 수 있다.

CLI로도 S3를 다룰 수 있다.

aws s3 ls
aws s3 mb s3://your-unique-bucket-name
echo "Hello, AWS CLI!" > hello.txt
aws s3 cp hello.txt s3://your-unique-bucket-name
aws s3 rb s3://your-unique-bucket-name --force

SDK 실습에서는 JavaScript용 AWS SDK를 설치하고 S3 버킷 생성과 객체 업로드를 코드로 실행했다.

npm init -y
npm install --save @aws-sdk/client-s3
import { S3Client, CreateBucketCommand, PutObjectCommand } from "@aws-sdk/client-s3";

const s3Client = new S3Client({ region: "ap-northeast-2" });
const bucketName = "your-unique-bucket-name-20231010";

await s3Client.send(new CreateBucketCommand({ Bucket: bucketName }));
await s3Client.send(
  new PutObjectCommand({
    Bucket: bucketName,
    Key: "hello.txt",
    Body: "Hello, JavaScript SDK!",
  }),
);

콘솔, CLI, SDK는 서로 다른 도구지만 결국 같은 AWS API를 호출한다. 그래서 권한이 부족하면 콘솔에서도 실패하고, CLI/SDK에서도 실패한다.

5. CloudFront를 앞에 둘 때 달라지는 것

CloudFront는 CDN이다. S3나 EC2, ALB 같은 Origin 앞에 배치되어 사용자와 가까운 Edge Location에서 콘텐츠를 캐싱한다. 정적 파일을 S3에 두고 CloudFront를 연결하면 전송 속도, TLS, 캐시 제어, DDoS 기본 방어, 도메인 연결을 한 지점에서 다룰 수 있다.

CloudFront 실습에서는 S3 버킷을 Origin으로 선택하고, Origin Access Control을 만들어 CloudFront만 S3 객체를 읽을 수 있게 했다. 이때 S3 직접 접근은 Access Denied가 나고, CloudFront 도메인으로 접근하면 파일이 보이는 것이 정상이다.

S3 Bucket Policy는 CloudFront 배포 ARN을 조건으로 제한한다.

{
  "Version": "2012-10-17",
  "Statement": {
    "Sid": "AllowCloudFrontServicePrincipalReadOnly",
    "Effect": "Allow",
    "Principal": {
      "Service": "cloudfront.amazonaws.com"
    },
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::your-bucket-name/*",
    "Condition": {
      "StringEquals": {
        "AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/EXAMPLE"
      }
    }
  }
}

브라우저 개발자 도구의 Network 탭에서 응답 헤더를 보면 캐시 동작도 확인할 수 있다. 처음 요청은 x-cache: Miss from cloudfront로 Origin에서 가져오고, 이후 요청은 x-cache: Hit from cloudfront로 Edge 캐시에서 응답한다. 배포 후 파일을 바꿨는데 내용이 그대로라면 캐시 무효화(invalidation), Cache-Control 헤더, 파일명 버전 전략을 확인해야 한다.

6. ALB와 Auto Scaling으로 서버 계층 다루기

정적 파일은 S3와 CloudFront로 해결할 수 있지만, 서버에서 처리해야 하는 요청은 컴퓨팅 계층이 필요하다. EC2 한 대에 직접 트래픽을 보내면 구조는 단순하지만 장애와 확장에 약하다. ALB(Application Load Balancer)를 앞에 두면 여러 EC2 인스턴스로 트래픽을 분산하고, Target Group의 Health Check로 정상 인스턴스에만 요청을 보낼 수 있다.

실습에서는 WordPress AMI 기반 EC2 두 대를 서로 다른 Availability Zone에 띄우고 ALB Target Group에 등록했다. ALB DNS로 접속한 뒤 새로고침하면 서로 다른 인스턴스로 요청이 분산되는 것을 확인했다. 이때 Health Check 경로가 /로 잡혀 있고, 각 Target이 healthy가 되어야 정상적으로 트래픽을 받는다.

Auto Scaling은 트래픽이나 메트릭에 따라 EC2 인스턴스 수를 조정한다. 기본 구성 요소는 Launch Template, Auto Scaling Group, Scaling Policy다. Launch Template에는 AMI, 인스턴스 타입, 키 페어, 보안 그룹 같은 새 인스턴스 생성 정보가 들어간다. ASG에는 최소/최대/희망 인스턴스 수와 사용할 서브넷, 연결할 Target Group을 지정한다.

부하 테스트는 stress로 진행했다.

ssh -i "my-key.pem" bitnami@<public-ip>
sudo apt-get update
sudo apt-get install stress
stress --cpu 4

CloudWatch에서 CPUUtilization이 80%를 넘으면 Alarm이 발생하고, Target Tracking 정책에 의해 Scale-out 이벤트가 발생한다. ASG Activity 탭에서는 새 인스턴스가 시작되는 기록을 볼 수 있다.

여기서 가장 중요한 발견은 데이터 불일치였다. WordPress를 EC2 두 대에 각각 띄우면 각 인스턴스가 자기 로컬 MySQL을 갖는다. ALB가 요청을 분산하면 어떤 글은 보이고 어떤 글은 안 보이는 현상이 생긴다. 서버 계층을 수평 확장하려면 애플리케이션 인스턴스는 상태를 갖지 않게 만들고, 데이터베이스는 RDS 같은 외부 공용 저장소로 분리해야 한다.

7. 이미지 업로드 서비스에서 상태를 밖으로 빼기

EC2, S3, CloudFront, ALB, Auto Scaling을 한 번에 이해하기 좋은 예시는 이미지 업로드 서비스다. 사용자는 웹 서버에 이미지를 업로드하고, 서버는 파일을 S3에 저장한다. 이후 이미지는 CloudFront를 통해 제공된다. 이때 EC2는 요청 처리와 업로드 중계만 담당하고, 파일의 최종 상태는 S3로 이동한다.

이 구조에서 EC2 인스턴스가 교체되어도 업로드된 파일은 사라지지 않는다. 새 인스턴스는 같은 S3 버킷과 같은 RDS를 바라보면 된다. 반대로 이미지를 EC2 로컬 디스크에 저장하면 Auto Scaling이 만들어낸 새 인스턴스에는 파일이 없고, 종료된 인스턴스의 파일은 함께 사라진다.

Node.js/Express 기반 업로드 API라면 흐름은 다음처럼 나뉜다.

upload request
  -> validate content type and size
  -> generate object key
  -> upload file stream to S3
  -> save metadata to RDS
  -> return image id or CloudFront URL

S3 object key는 사용자 입력 파일명을 그대로 쓰지 않는다. 파일명에는 개인정보가 들어갈 수 있고, 같은 이름이 충돌할 수 있다. 날짜 prefix와 난수 ID를 조합하면 조회와 정리 정책을 세우기 쉽다.

images/2026/07/05/9f2d6a2b-4f20-4e1a-a2fd.jpg

AWS SDK를 쓰는 애플리케이션은 access key를 코드에 넣지 않는 것이 기본이다. EC2에서 실행한다면 instance profile과 IAM role을 사용하고, 필요한 S3 권한만 부여한다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject"
      ],
      "Resource": "arn:aws:s3:::example-image-bucket/images/*"
    }
  ]
}

이 정책은 예시일 뿐이고, 실제 운영에서는 삭제 권한, list 권한, 버킷 전체 권한을 분리해서 검토한다. 업로드만 필요한 서버에 s3:*를 주면 장애 대응은 편해 보이지만 사고 범위가 커진다.

CloudFront를 붙이면 이미지 응답은 빨라지지만 캐시 무효화와 원본 보호를 함께 생각해야 한다. 사용자가 같은 object key로 이미지를 덮어쓰면 엣지 캐시에 이전 이미지가 남아 있을 수 있다. 가능하면 immutable key를 사용하고, 변경된 이미지는 새 key로 저장하는 방식이 단순하다.

cache-friendly
  /images/user-1/profile/2026-07-05T10-00-00Z.jpg

cache-hostile
  /images/user-1/profile.jpg

서버 계층을 stateless하게 만들면 Auto Scaling은 훨씬 안전해진다. 하지만 stateless는 “아무 상태도 없다”가 아니라 상태를 적절한 저장소로 옮긴다는 뜻이다.

application instance
  - request handling
  - validation
  - temporary processing

external state
  - S3: uploaded files
  - RDS: relational metadata
  - ElastiCache: session or cache
  - CloudWatch Logs: runtime logs

이 구분이 있어야 Auto Scaling이 단순 서버 복제 기능을 넘어 장애 복구와 비용 조절 도구가 된다.

8. CloudWatch는 사후 확인 도구가 아니라 설계 일부

CloudWatch는 AWS 리소스와 애플리케이션 상태를 지표와 로그로 확인하는 서비스다. EC2 CPU 사용률, ALB Target 상태, Auto Scaling 이벤트, Alarm 상태를 관찰할 수 있다. 실습에서는 CPUUtilization이 80%를 초과하면 SNS 이메일 알림을 보내도록 Alarm을 만들었다.

운영 중에는 “문제가 생기면 CloudWatch를 본다”보다 “어떤 문제가 생겼을 때 어떤 지표를 봐야 하는지 미리 정한다”가 더 중요하다.

  • 사용자가 접속하지 못한다면 Route 53, CloudFront, ALB, Target Group 상태를 본다.
  • 정적 파일이 안 바뀐다면 CloudFront 캐시와 invalidation을 본다.
  • 서버 응답이 느리다면 ALB latency, EC2 CPU, 애플리케이션 로그를 본다.
  • 스케일링이 안 된다면 CloudWatch Alarm, ASG Activity, Launch Template 오류를 본다.
  • 접근이 거부된다면 IAM Policy, Bucket Policy, Security Group을 본다.

AWS 서비스는 각각 분리되어 있지만 장애는 보통 경로를 따라 나타난다. 그래서 개별 서비스를 외우는 것보다 요청 흐름을 기준으로 확인 지점을 정리하는 편이 훨씬 실용적이었다.

마무리

이번 정리에서 가장 크게 남은 점은 AWS 인프라를 “리소스 목록”이 아니라 “요청과 운영의 경로”로 봐야 한다는 것이다. Route 53은 이름을 엔드포인트로 연결하고, CloudFront는 사용자 가까이에서 콘텐츠를 캐싱하고, S3는 객체 저장소가 되고, ALB는 서버 계층으로 요청을 나누며, Auto Scaling은 인스턴스 수를 조절한다. IAM과 보안 그룹은 그 경로를 누가, 어디서, 어떻게 사용할 수 있는지 제한한다. CloudWatch는 그 경로가 실제로 동작하는지 확인하는 신호를 제공한다.

처음에는 각 서비스를 따로 공부했지만, 웹 서비스 하나를 놓고 보면 모든 구성이 연결된다. DNS, 캐시, 권한, 보안 그룹, 헬스체크, 스케일링, 로그는 따로 존재하지 않는다. 사용자의 요청이 실패했을 때 어느 지점에서 막혔는지 찾을 수 있어야 한다. 그 관점에서 AWS 기본 서비스들은 클라우드 인프라를 이해하는 좋은 출발점이 된다.

이 글의 무단 복제, 재배포, 자동 수집을 허용하지 않습니다. 인용이 필요한 경우 출처와 원문 링크를 함께 남겨주세요.