🤖

본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.

⚠️

본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.

이미지 로딩 중...

ECS 태스크 정의 완벽 가이드 - 슬라이드 1/7
A

AI Generated

2025. 12. 20. · 5 Views

ECS 태스크 정의 완벽 가이드

AWS ECS에서 컨테이너를 실행하기 위한 핵심 설정인 태스크 정의를 초급자도 쉽게 이해할 수 있도록 설명합니다. 실무에서 자주 마주치는 상황을 스토리로 풀어내며, 컨테이너 정의부터 리소스 설정, 환경 변수, 로깅, 볼륨까지 모두 다룹니다.


목차

  1. 태스크_정의란
  2. 컨테이너_정의_작성
  3. CPU_메모리_설정
  4. 환경_변수_설정
  5. 로깅_설정
  6. 볼륨_마운트

1. 태스크 정의란

어느 날 김개발 씨는 팀 리더에게 새로운 미션을 받았습니다. "이번에 우리 서비스를 AWS ECS로 옮기기로 했어요.

먼저 태스크 정의부터 작성해 볼래요?" 김개발 씨는 고개를 끄덕였지만 사실 태스크 정의가 뭔지 잘 몰랐습니다.

태스크 정의는 ECS에서 컨테이너를 어떻게 실행할지 알려주는 설계도입니다. 마치 요리할 때 레시피를 보는 것처럼, 어떤 재료(컨테이너 이미지)를 쓰고 어떻게 조리(실행)할지 정의합니다.

이 설계도가 있어야 ECS가 여러분의 애플리케이션을 정확히 실행할 수 있습니다.

다음 코드를 살펴봅시다.

{
  "family": "my-web-app",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256",
  "memory": "512",
  "containerDefinitions": [
    {
      "name": "web",
      "image": "nginx:latest",
      // 컨테이너 포트 설정
      "portMappings": [
        {
          "containerPort": 80,
          "protocol": "tcp"
        }
      ]
    }
  ]
}

김개발 씨는 입사 6개월 차 백엔드 개발자입니다. 그동안 EC2 인스턴스에 직접 서버를 배포해왔지만, 이번에는 회사에서 컨테이너 기반 아키텍처로 전환하기로 했습니다.

팀 리더 박시니어 씨가 말했습니다. "ECS를 쓰려면 먼저 태스크 정의를 작성해야 해요." 태스크 정의라니, 처음 듣는 단어에 김개발 씨는 막막했습니다.

하지만 박시니어 씨의 설명을 듣고 나니 생각보다 간단했습니다. 태스크 정의란 정확히 무엇일까요?

쉽게 비유하자면, 태스크 정의는 마치 레스토랑의 레시피와 같습니다. 요리사가 요리를 만들 때 레시피를 보고 어떤 재료를 얼마나 쓸지, 어떤 순서로 조리할지 확인하듯이, ECS도 태스크 정의를 보고 컨테이너를 어떻게 실행할지 결정합니다.

레시피 없이는 요리를 만들 수 없고, 태스크 정의 없이는 ECS에서 컨테이너를 실행할 수 없습니다. 태스크 정의가 없던 시절에는 어땠을까요?

사실 태스크 정의는 ECS의 핵심 개념이기 때문에, ECS 없이는 존재하지 않습니다. 하지만 ECS 이전 시대를 생각해봅시다.

개발자들은 EC2 인스턴스에 직접 접속해서 도커 명령어를 입력하고, 환경 변수를 설정하고, 포트를 열어주고, 로그를 확인하는 등 모든 작업을 수동으로 해야 했습니다. 서버가 10대, 20대로 늘어나면 이런 작업을 일일이 반복해야 했죠.

실수하기도 쉽고, 시간도 오래 걸렸습니다. 바로 이런 문제를 해결하기 위해 ECS태스크 정의가 등장했습니다.

태스크 정의를 한 번만 작성해두면, ECS가 알아서 컨테이너를 실행해줍니다. 서버가 100대든 1000대든 상관없이 같은 설정으로 일관되게 배포할 수 있습니다.

또한 버전 관리도 가능해서, 이전 버전으로 롤백하는 것도 쉽습니다. 무엇보다 코드로 인프라를 관리하는 IaC(Infrastructure as Code) 원칙에 딱 맞습니다.

위의 코드를 한 줄씩 살펴보겠습니다. 먼저 family 필드는 태스크 정의의 이름입니다.

같은 family에 속한 태스크 정의는 버전으로 관리됩니다. "my-web-app:1", "my-web-app:2"처럼 말이죠.

다음으로 networkMode는 네트워크 설정입니다. "awsvpc"는 각 태스크가 고유한 ENI(네트워크 인터페이스)를 갖는다는 의미입니다.

requiresCompatibilities 필드는 어디서 실행할지 정합니다. FARGATE는 서버리스 옵션으로, EC2 인스턴스를 관리할 필요가 없습니다.

cpumemory는 컨테이너가 사용할 리소스입니다. 256 CPU 유닛은 0.25 vCPU를 의미합니다.

containerDefinitions 배열에는 실제로 실행할 컨테이너들을 정의합니다. 하나의 태스크에 여러 컨테이너를 넣을 수도 있습니다.

각 컨테이너는 name, image, portMappings 등을 설정할 수 있습니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 전자상거래 쇼핑몰을 운영한다고 가정해봅시다. 평소에는 트래픽이 적지만, 세일 기간에는 트래픽이 10배로 증가합니다.

태스크 정의를 사용하면 Auto Scaling을 설정해서 자동으로 컨테이너 수를 늘릴 수 있습니다. 또한 블루-그린 배포를 통해 무중단 배포도 가능합니다.

많은 스타트업과 대기업들이 이런 방식으로 안정적인 서비스를 운영하고 있습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수 중 하나는 태스크 정의를 너무 복잡하게 만드는 것입니다. 처음부터 모든 옵션을 다 설정하려고 하면 오히려 혼란스럽습니다.

먼저 기본적인 설정만으로 시작하고, 필요할 때마다 하나씩 추가하는 것이 좋습니다. 또한 cpumemory 설정을 너무 낮게 잡으면 컨테이너가 제대로 동작하지 않을 수 있으니 주의해야 합니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 설명을 들은 김개발 씨는 고개를 끄덕였습니다.

"아, 레시피처럼 한 번만 작성해두면 ECS가 알아서 실행해주는 거군요!" 태스크 정의를 제대로 이해하면 AWS ECS를 훨씬 효과적으로 사용할 수 있습니다. 수동 배포의 번거로움에서 벗어나 자동화된 인프라를 구축할 수 있습니다.

여러분도 오늘 배운 내용을 실제 프로젝트에 적용해 보세요.

실전 팁

💡 - 처음에는 최소한의 설정만 넣고 시작하세요

  • family 이름은 서비스 이름과 일치시키면 관리가 편합니다
  • FARGATE를 사용하면 서버 관리 부담을 크게 줄일 수 있습니다

2. 컨테이너 정의 작성

김개발 씨가 첫 번째 태스크 정의를 작성하고 나서 박시니어 씨에게 보여주었습니다. "좋아요!

그런데 컨테이너 정의 부분을 좀 더 자세히 작성해야 해요." 컨테이너 정의가 태스크 정의의 핵심이라는 것을 김개발 씨는 곧 알게 되었습니다.

컨테이너 정의는 태스크 내에서 실행될 각각의 컨테이너 설정을 담습니다. 마치 오케스트라의 악보처럼, 각 악기(컨테이너)가 어떤 역할을 하고 어떻게 연주할지 정의합니다.

이미지 주소, 포트 매핑, 환경 변수 등 컨테이너 실행에 필요한 모든 정보가 여기에 들어갑니다.

다음 코드를 살펴봅시다.

{
  "containerDefinitions": [
    {
      "name": "nginx-web",
      // ECR에서 가져올 이미지
      "image": "123456789.dkr.ecr.ap-northeast-2.amazonaws.com/my-app:latest",
      "essential": true,
      "portMappings": [
        {
          "containerPort": 80,
          "hostPort": 80,
          "protocol": "tcp"
        }
      ],
      // 헬스체크 설정
      "healthCheck": {
        "command": ["CMD-SHELL", "curl -f http://localhost/ || exit 1"],
        "interval": 30,
        "timeout": 5,
        "retries": 3
      }
    }
  ]
}

김개발 씨는 기본 태스크 정의를 작성했지만, 아직 완성도가 떨어진다는 피드백을 받았습니다. "컨테이너 정의를 더 구체적으로 작성해야 해요." 박시니어 씨가 자신의 화면을 보여주며 설명을 시작했습니다.

컨테이너 정의는 태스크 정의의 심장과도 같습니다. 아무리 태스크 정의를 잘 작성해도, 컨테이너 정의가 부실하면 애플리케이션이 제대로 동작하지 않습니다.

컨테이너 정의란 정확히 무엇일까요? 쉽게 비유하자면, 컨테이너 정의는 마치 회사 조직도의 팀원 프로필과 같습니다.

각 팀원(컨테이너)의 이름, 역할, 필요한 자원, 연락처(포트) 등이 명시되어 있습니다. 프로젝트를 시작할 때 이 프로필을 보고 누구에게 무슨 일을 맡길지 결정하듯이, ECS도 컨테이너 정의를 보고 각 컨테이너를 어떻게 실행할지 결정합니다.

컨테이너 정의를 제대로 작성하지 않으면 어떤 일이 벌어질까요? 실제로 한 스타트업에서 있었던 일입니다.

개발자가 급하게 컨테이너를 배포하면서 essential 플래그를 제대로 설정하지 않았습니다. 그 결과 핵심 컨테이너가 실패해도 태스크가 계속 실행되어, 사용자들은 오류 페이지만 보게 되었습니다.

모니터링 알람도 울리지 않아서 한참 뒤에야 문제를 발견했습니다. 또 다른 경우에는 healthCheck를 설정하지 않아서, 컨테이너가 실제로는 죽어있는데도 ECS는 정상이라고 판단하는 문제가 있었습니다.

바로 이런 문제를 방지하기 위해 컨테이너 정의를 꼼꼼하게 작성해야 합니다. name 필드는 컨테이너의 고유한 이름입니다.

같은 태스크 내에서 여러 컨테이너가 서로를 참조할 때 이 이름을 사용합니다. image 필드는 가장 중요합니다.

어떤 도커 이미지를 사용할지 정의하는데, 보통 ECR(Elastic Container Registry)의 전체 경로를 입력합니다. essential 플래그는 매우 중요합니다.

true로 설정하면 이 컨테이너가 실패할 경우 전체 태스크가 중단됩니다. 핵심 애플리케이션 컨테이너는 반드시 essential: true로 설정해야 합니다.

반면 로그 수집기처럼 부가적인 컨테이너는 false로 설정할 수 있습니다. portMappings는 컨테이너 포트를 호스트 포트에 매핑합니다.

containerPort는 컨테이너 내부에서 애플리케이션이 리스닝하는 포트이고, hostPort는 외부에서 접근할 때 사용하는 포트입니다. awsvpc 네트워크 모드에서는 두 값이 같아야 합니다.

healthCheck는 컨테이너가 정상적으로 동작하는지 주기적으로 확인합니다. 위 예제에서는 curl 명령어로 localhost에 HTTP 요청을 보내고, 실패하면 비정상으로 판단합니다.

interval은 체크 간격(초), timeout은 응답 대기 시간, retries는 몇 번 실패해야 비정상으로 볼지 정합니다. 실제 현업에서는 어떻게 활용할까요?

예를 들어 마이크로서비스 아키텍처를 구축한다고 해봅시다. 하나의 태스크에 여러 컨테이너를 함께 배치할 수 있습니다.

메인 애플리케이션 컨테이너, 사이드카 패턴의 로그 수집 컨테이너, 메트릭 수집 컨테이너 등을 한 태스크에 넣으면 네트워크 지연이 거의 없고 관리도 편합니다. Netflix, Airbnb 같은 기업들이 이런 패턴을 적극 활용하고 있습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수는 모든 컨테이너를 essential: true로 설정하는 것입니다.

부가적인 컨테이너까지 필수로 설정하면, 로그 수집기가 잠깐 문제가 생겼을 뿐인데 전체 서비스가 중단될 수 있습니다. 또한 healthCheck를 너무 짧은 간격으로 설정하면 불필요한 부하가 발생합니다.

보통 30초 정도가 적당합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 설명을 듣고 나서 김개발 씨는 자신의 컨테이너 정의를 다시 작성했습니다. "이제 훨씬 명확해졌어요!" 컨테이너 정의를 제대로 작성하면 안정적이고 모니터링하기 쉬운 시스템을 만들 수 있습니다.

여러분도 essential 플래그와 healthCheck를 꼭 설정하는 습관을 들이세요.

실전 팁

💡 - 핵심 컨테이너는 essential: true로 설정하세요

  • healthCheck를 반드시 추가해서 장애를 빠르게 감지하세요
  • 이미지 태그로 latest 대신 구체적인 버전을 사용하면 더 안전합니다

3. CPU 메모리 설정

김개발 씨가 처음 배포한 컨테이너가 자꾸 죽는 문제가 발생했습니다. "왜 계속 재시작되는 거죠?" 박시니어 씨가 로그를 보더니 한숨을 쉬었습니다.

"CPU와 메모리를 너무 적게 할당했네요. 리소스 설정을 제대로 해야 해요."

CPU와 메모리 설정은 컨테이너가 사용할 수 있는 리소스의 양을 정의합니다. 마치 자동차를 운전할 때 엔진 출력과 연료 탱크 크기를 정하는 것처럼, 애플리케이션이 원활히 동작하려면 적절한 리소스가 필요합니다.

너무 적으면 느려지거나 죽고, 너무 많으면 비용이 낭비됩니다.

다음 코드를 살펴봅시다.

{
  "family": "my-web-app",
  "requiresCompatibilities": ["FARGATE"],
  // 태스크 레벨 CPU (256 = 0.25 vCPU)
  "cpu": "1024",
  // 태스크 레벨 메모리 (MB 단위)
  "memory": "2048",
  "containerDefinitions": [
    {
      "name": "web",
      "image": "my-app:latest",
      // 컨테이너 레벨 리소스 제한
      "cpu": 512,
      "memory": 1024,
      "memoryReservation": 512
    }
  ]
}

김개발 씨는 자신이 만든 애플리케이션을 드디어 ECS에 배포했습니다. 하지만 기쁨도 잠시, 컨테이너가 계속 재시작되는 문제가 발생했습니다.

CloudWatch 로그를 보니 "Out of Memory" 에러가 가득했습니다. 박시니어 씨가 태스크 정의를 열어보더니 한숨을 쉬었습니다.

"256 CPU에 512MB 메모리로는 Node.js 애플리케이션을 돌리기엔 부족해요." CPU와 메모리 설정이란 정확히 무엇일까요? 쉽게 비유하자면, CPU와 메모리는 마치 식당의 주방 크기와 조리사 수와 같습니다.

손님이 많은 인기 식당인데 주방이 좁고 조리사가 한 명뿐이라면 어떻게 될까요? 주문이 밀리고, 음식이 늦게 나오고, 결국 손님들이 화를 낼 것입니다.

반대로 손님이 적은데 주방이 너무 크고 조리사가 10명이라면 낭비입니다. 적절한 균형이 필요합니다.

리소스를 제대로 설정하지 않으면 어떤 문제가 생길까요? 실제로 한 회사에서 있었던 사례입니다.

비용을 아끼려고 모든 컨테이너에 최소 리소스를 할당했습니다. 평소에는 괜찮았지만, 트래픽이 조금만 늘어나도 컨테이너가 OOM(Out of Memory)으로 죽었습니다.

심지어 컨테이너가 죽으면서 데이터가 손실되는 심각한 문제까지 발생했습니다. 결국 한밤중에 긴급 배포를 해야 했고, 서비스 중단으로 고객들의 불만이 쏟아졌습니다.

반대의 경우도 있습니다. 한 스타트업은 "넉넉하게 주는 게 안전하겠지"라고 생각해서 모든 컨테이너에 4 vCPU와 8GB 메모리를 할당했습니다.

실제로는 0.5 vCPU와 1GB면 충분한 애플리케이션이었는데 말이죠. 한 달 뒤 AWS 청구서를 보고 깜짝 놀랐습니다.

예상보다 10배나 많은 비용이 나왔던 것입니다. 바로 이런 문제를 방지하기 위해 적절한 리소스 설정이 필요합니다.

ECS에서는 두 가지 레벨로 리소스를 설정할 수 있습니다. 태스크 레벨컨테이너 레벨입니다.

태스크 레벨 설정은 전체 태스크가 사용할 수 있는 총 리소스입니다. 위 예제에서 "cpu": "1024"는 1 vCPU를 의미하고, "memory": "2048"은 2GB를 의미합니다.

FARGATE를 사용할 때는 반드시 태스크 레벨에서 CPU와 메모리를 설정해야 합니다. 중요한 점은 FARGATE에서는 정해진 조합만 사용할 수 있다는 것입니다.

예를 들어 CPU가 1024(1 vCPU)이면 메모리는 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB 중 하나를 선택할 수 있습니다. 임의의 값을 넣으면 에러가 발생합니다.

컨테이너 레벨 설정은 각 컨테이너가 사용할 수 있는 리소스입니다. cpu 필드는 CPU 유닛 수이고(1024 = 1 vCPU), memory는 하드 리미트입니다.

컨테이너가 이 값을 초과하면 강제 종료됩니다. memoryReservation은 소프트 리미트로, 최소한 보장받는 메모리입니다.

실제 현업에서는 어떻게 결정할까요? 가장 좋은 방법은 프로파일링입니다.

먼저 넉넉한 리소스로 시작해서 실제 사용량을 모니터링합니다. CloudWatch Container Insights를 활성화하면 CPU와 메모리 사용률을 그래프로 볼 수 있습니다.

일주일 정도 모니터링한 뒤, 피크 시간대의 사용량에 20-30% 여유를 더한 값으로 설정하는 것이 좋습니다. 예를 들어 Node.js API 서버라면 보통 0.5 vCPU에 1GB 메모리면 충분합니다.

Java Spring Boot 애플리케이션은 JVM 특성상 최소 1 vCPU에 2GB는 필요합니다. Python Flask 같은 경량 프레임워크는 0.25 vCPU에 512MB로도 가능합니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수는 개발 환경과 프로덕션 환경에 같은 리소스를 할당하는 것입니다.

개발 환경은 트래픽이 거의 없으니 최소 리소스로도 충분합니다. 하지만 프로덕션에는 넉넉하게 할당해야 합니다.

또한 메모리 누수가 있는 애플리케이션은 아무리 메모리를 늘려도 결국 죽습니다. 리소스를 늘리기 전에 코드를 먼저 점검하세요.

다시 김개발 씨의 이야기로 돌아가 봅시다. 박시니어 씨의 조언을 듣고 CPU를 1024로, 메모리를 2048로 늘렸습니다.

컨테이너는 안정적으로 동작했고, 더 이상 재시작되지 않았습니다. 리소스 설정을 제대로 하면 안정성과 비용 효율성을 동시에 얻을 수 있습니다.

여러분도 모니터링을 통해 최적의 값을 찾아보세요.

실전 팁

💡 - FARGATE의 CPU/메모리 조합표를 꼭 확인하세요

  • CloudWatch Container Insights로 실사용량을 모니터링하세요
  • 피크 시간대 사용량의 120-130% 정도로 설정하는 것이 안전합니다

4. 환경 변수 설정

김개발 씨는 개발 환경과 프로덕션 환경에서 다른 데이터베이스를 사용해야 했습니다. "매번 코드를 수정해서 배포해야 하나요?" 박시니어 씨가 웃으며 말했습니다.

"환경 변수를 사용하면 코드 수정 없이 설정을 바꿀 수 있어요."

환경 변수는 애플리케이션의 동작을 외부에서 제어할 수 있게 해주는 설정값입니다. 마치 리모컨으로 TV 채널을 바꾸듯이, 코드를 수정하지 않고도 데이터베이스 주소, API 키, 로그 레벨 등을 변경할 수 있습니다.

특히 민감한 정보는 AWS Secrets Manager를 통해 안전하게 관리할 수 있습니다.

다음 코드를 살펴봅시다.

{
  "containerDefinitions": [
    {
      "name": "web",
      "image": "my-app:latest",
      // 일반 환경 변수
      "environment": [
        {
          "name": "NODE_ENV",
          "value": "production"
        },
        {
          "name": "LOG_LEVEL",
          "value": "info"
        }
      ],
      // Secrets Manager에서 가져오기
      "secrets": [
        {
          "name": "DB_PASSWORD",
          "valueFrom": "arn:aws:secretsmanager:ap-northeast-2:123456789:secret:db-password"
        }
      ]
    }
  ]
}

김개발 씨는 드디어 애플리케이션을 배포했지만, 곧 새로운 문제에 부딪혔습니다. 개발 환경에서는 로컬 데이터베이스를, 프로덕션에서는 RDS를 사용해야 했습니다.

"매번 코드를 수정해서 다시 빌드하고 배포해야 하나요?" 김개발 씨가 한숨을 쉬었습니다. 박시니어 씨가 옆에서 지켜보다가 웃으며 말했습니다.

"환경 변수를 사용하면 되죠. 코드는 그대로 두고 설정만 바꾸면 돼요." 환경 변수란 정확히 무엇일까요?

쉽게 비유하자면, 환경 변수는 마치 자동차의 내비게이션 설정과 같습니다. 같은 자동차지만 목적지를 다르게 설정하면 다른 경로로 갑니다.

애플리케이션도 마찬가지입니다. 같은 코드지만 환경 변수로 데이터베이스 주소를 다르게 설정하면 다른 데이터베이스에 연결됩니다.

코드를 수정할 필요가 없습니다. 환경 변수 없이 개발하면 어떤 문제가 생길까요?

과거에는 개발자들이 설정값을 코드에 직접 하드코딩했습니다. 개발 환경에서는 주석으로 막고 프로덕션 설정을 활성화하는 식이었죠.

하지만 이런 방식은 매우 위험합니다. 실수로 개발 설정을 프로덕션에 배포하면 큰 사고가 납니다.

실제로 한 회사에서는 데이터베이스 비밀번호를 코드에 하드코딩했다가 GitHub에 공개되어 해킹당한 사례도 있습니다. 또한 설정을 바꿀 때마다 코드를 수정하고 빌드하고 배포하는 과정이 필요했습니다.

시간도 오래 걸리고, 실수할 여지도 많았습니다. 로그 레벨만 바꾸려고 해도 전체 배포 프로세스를 거쳐야 했습니다.

바로 이런 문제를 해결하기 위해 환경 변수가 등장했습니다. ECS에서는 두 가지 방식으로 환경 변수를 설정할 수 있습니다.

environmentsecrets입니다. environment는 일반적인 설정값을 넣습니다.

NODE_ENV, LOG_LEVEL, API_ENDPOINT처럼 민감하지 않은 정보들입니다. 이 값들은 태스크 정의에 평문으로 저장되므로, 누구나 볼 수 있습니다.

따라서 비밀번호나 API 키 같은 민감한 정보는 절대 넣으면 안 됩니다. secrets는 민감한 정보를 안전하게 관리합니다.

AWS Secrets Manager나 Systems Manager Parameter Store에 저장된 값을 참조합니다. valueFrom에는 ARN(Amazon Resource Name)을 입력합니다.

ECS가 컨테이너를 시작할 때 자동으로 가져와서 환경 변수로 주입합니다. 위 예제를 보면 DB_PASSWORD는 Secrets Manager에서 가져옵니다.

이렇게 하면 여러 장점이 있습니다. 첫째, 비밀번호가 태스크 정의에 노출되지 않습니다.

둘째, Secrets Manager에서 비밀번호를 변경하면 다음 배포부터 자동으로 적용됩니다. 셋째, 접근 권한을 IAM으로 세밀하게 제어할 수 있습니다.

실제 현업에서는 어떻게 활용할까요? 대부분의 회사에서는 환경별로 다른 태스크 정의를 관리합니다.

dev-task-definition, staging-task-definition, prod-task-definition처럼 말이죠. 각 태스크 정의는 같은 이미지를 사용하지만 환경 변수만 다릅니다.

개발 환경에는 LOG_LEVEL=debug, 프로덕션에는 LOG_LEVEL=error를 설정하는 식입니다. 또한 Terraform이나 CloudFormation 같은 IaC 도구를 사용하면 환경 변수 관리가 더 쉬워집니다.

변수를 외부 파일로 분리해서 관리할 수 있고, Git으로 버전 관리도 가능합니다. Netflix, Spotify 같은 글로벌 기업들은 이런 방식으로 수백 개의 마이크로서비스를 관리하고 있습니다.

하지만 주의할 점도 있습니다. 초보 개발자들이 흔히 하는 실수는 모든 설정을 환경 변수로 만드는 것입니다.

환경 변수가 너무 많으면 오히려 관리가 어렵습니다. 진짜 환경에 따라 달라지는 값만 환경 변수로 만들고, 나머지는 코드나 설정 파일에 넣는 것이 좋습니다.

또한 Secrets Manager는 비용이 발생합니다. 시크릿 하나당 월 0.40달러, API 호출 1만 건당 0.05달러입니다.

개발 환경에서는 Parameter Store의 무료 티어를 활용하는 것도 좋은 방법입니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 도움으로 환경 변수를 설정한 김개발 씨는 이제 코드 수정 없이 환경을 전환할 수 있게 되었습니다. "정말 편리하네요!" 환경 변수를 제대로 사용하면 유연하고 안전한 애플리케이션을 만들 수 있습니다.

여러분도 민감한 정보는 반드시 Secrets Manager를 사용하세요.

실전 팁

💡 - 민감한 정보는 절대 environment에 넣지 말고 secrets를 사용하세요

  • 환경 변수 이름은 대문자와 언더스코어로 작성하는 것이 관례입니다
  • Secrets Manager의 비용이 부담된다면 Parameter Store를 고려하세요

5. 로깅 설정

김개발 씨의 애플리케이션에서 에러가 발생했습니다. "도대체 무슨 일이 일어난 거죠?" 하지만 로그를 볼 방법이 없었습니다.

박시니어 씨가 말했습니다. "로깅 설정을 하지 않았네요.

CloudWatch Logs로 보내야 해요."

로깅 설정은 컨테이너에서 출력되는 로그를 어디로 보낼지 정의합니다. 마치 블랙박스처럼, 애플리케이션에서 무슨 일이 일어나는지 기록해둡니다.

CloudWatch Logs로 보내면 로그를 검색하고, 필터링하고, 알람을 설정할 수 있습니다. 문제가 생겼을 때 원인을 파악하는 가장 중요한 도구입니다.

다음 코드를 살펴봅시다.

{
  "containerDefinitions": [
    {
      "name": "web",
      "image": "my-app:latest",
      // awslogs 드라이버 사용
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/my-web-app",
          "awslogs-region": "ap-northeast-2",
          "awslogs-stream-prefix": "web",
          // 로그 그룹이 없으면 자동 생성
          "awslogs-create-group": "true"
        }
      }
    }
  ]
}

어느 날 새벽, 김개발 씨에게 알람이 울렸습니다. 서비스에서 500 에러가 발생하고 있다는 것이었습니다.

급히 노트북을 켜고 확인해보려 했지만, 로그를 어디서 봐야 할지 몰랐습니다. EC2 시절처럼 SSH로 접속할 수도 없었습니다.

당황한 김개발 씨는 박시니어 씨에게 전화를 걸었습니다. "로깅 설정을 했나요?" 박시니어 씨의 첫 질문이었습니다.

"아니요, 그게 뭔가요?" 김개발 씨는 자신의 실수를 깨달았습니다. 로깅이란 정확히 무엇일까요?

쉽게 비유하자면, 로깅은 마치 자동차의 블랙박스와 같습니다. 사고가 나면 블랙박스 영상을 보고 무슨 일이 있었는지 확인합니다.

애플리케이션도 마찬가지입니다. 에러가 발생하면 로그를 보고 원인을 파악합니다.

로그가 없으면 완전히 막막합니다. 무엇이 잘못되었는지 알 수 없으니까요.

로깅을 설정하지 않으면 어떤 일이 벌어질까요? 실제로 한 스타트업에서 있었던 일입니다.

개발자들이 급하게 서비스를 출시하면서 로깅 설정을 건너뛰었습니다. 평소에는 괜찮았지만, 어느 날 갑자기 사용자들이 로그인을 할 수 없다는 문의가 쏟아졌습니다.

문제는 원인을 전혀 알 수 없다는 것이었습니다. 로그가 없으니 추측만 할 뿐이었습니다.

결국 개발자들은 밤새워 코드를 하나하나 검토하고, 로컬에서 재현을 시도했습니다. 다음 날 아침이 되어서야 데이터베이스 연결 풀이 고갈된 것이 원인임을 발견했습니다.

로그만 있었다면 10분 만에 찾을 수 있었던 문제에 10시간이 걸린 것입니다. 바로 이런 문제를 방지하기 위해 로깅 설정이 필수입니다.

ECS에서는 logConfiguration 필드로 로깅을 설정합니다. 가장 많이 사용하는 드라이버는 awslogs입니다.

이름에서 알 수 있듯이 AWS CloudWatch Logs로 로그를 전송합니다. awslogs-group은 CloudWatch Logs의 로그 그룹 이름입니다.

보통 /ecs/서비스이름 형식을 사용합니다. 같은 애플리케이션의 로그를 하나의 그룹으로 모으는 것이죠.

awslogs-region은 로그를 저장할 리전입니다. 태스크가 실행되는 리전과 같은 곳을 지정하는 것이 일반적입니다.

awslogs-stream-prefix는 로그 스트림 이름의 접두사입니다. 각 컨테이너는 별도의 로그 스트림을 갖습니다.

예를 들어 prefix가 "web"이면 로그 스트림 이름은 "web/web/태스크ID" 같은 형식이 됩니다. 이렇게 하면 여러 태스크의 로그를 구분해서 볼 수 있습니다.

awslogs-create-group을 true로 설정하면 로그 그룹이 없을 때 자동으로 생성합니다. 수동으로 미리 만들지 않아도 되니 편리합니다.

하지만 IaC 도구로 인프라를 관리한다면 명시적으로 로그 그룹을 생성하는 것이 더 좋습니다. 실제 현업에서는 어떻게 활용할까요?

CloudWatch Logs에 로그가 쌓이면 다양한 분석이 가능합니다. 먼저 Logs Insights로 SQL 같은 쿼리 언어로 로그를 검색할 수 있습니다.

"ERROR"가 포함된 로그만 추출하거나, 특정 시간대의 로그만 볼 수 있습니다. 또한 Metric Filters를 설정해서 특정 패턴이 나타나면 알람을 받을 수 있습니다.

예를 들어 "Database connection failed" 로그가 1분에 10번 이상 나타나면 SNS로 알람을 보내는 식입니다. 문제를 조기에 감지할 수 있습니다.

큰 규모의 서비스에서는 CloudWatch Logs를 S3로 export해서 장기 보관하기도 합니다. CloudWatch Logs는 스토리지 비용이 비싸기 때문에, 오래된 로그는 저렴한 S3로 옮기는 것입니다.

필요할 때 Athena로 쿼리할 수 있습니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수는 너무 많은 로그를 남기는 것입니다. DEBUG 레벨로 모든 것을 로깅하면 로그 양이 폭발적으로 늘어나고, CloudWatch Logs 비용도 함께 증가합니다.

프로덕션에서는 INFO나 WARN 레벨만 남기고, 필요할 때만 DEBUG를 활성화하는 것이 좋습니다. 또한 개인정보나 민감한 정보를 로그에 남기면 안 됩니다.

신용카드 번호, 비밀번호, 주민등록번호 같은 것들은 로그에서 마스킹하거나 아예 남기지 않아야 합니다. GDPR이나 개인정보보호법 위반으로 큰 문제가 될 수 있습니다.

다시 김개발 씨의 이야기로 돌아가 봅시다. 새벽에 긴급 상황을 겪은 뒤, 김개발 씨는 즉시 로깅 설정을 추가했습니다.

다음부터는 문제가 생기면 CloudWatch Logs를 보고 빠르게 원인을 찾을 수 있었습니다. 로깅은 단순히 기록을 남기는 것이 아닙니다.

시스템을 이해하고, 문제를 해결하고, 서비스를 개선하는 핵심 도구입니다. 여러분도 반드시 로깅 설정을 하세요.

실전 팁

💡 - 프로덕션에서는 로그 레벨을 INFO 이상으로 설정해서 비용을 절감하세요

  • Metric Filters로 중요한 에러에 대한 알람을 설정하세요
  • 민감한 정보는 절대 로그에 남기지 마세요

6. 볼륨 마운트

김개발 씨는 파일 업로드 기능을 구현했는데, 컨테이너가 재시작될 때마다 파일이 사라지는 문제가 있었습니다. "왜 파일이 계속 없어지는 거죠?" 박시니어 씨가 설명했습니다.

"컨테이너는 상태가 없어요. 파일을 보존하려면 볼륨을 사용해야 해요."

볼륨 마운트는 컨테이너 외부의 저장소를 컨테이너 내부에 연결하는 기능입니다. 마치 USB 드라이브를 컴퓨터에 꽂는 것처럼, 외부 스토리지를 컨테이너에서 사용할 수 있게 해줍니다.

EFS를 사용하면 여러 컨테이너가 같은 파일 시스템을 공유할 수 있고, 컨테이너가 재시작되어도 데이터가 보존됩니다.

다음 코드를 살펴봅시다.

{
  "family": "my-web-app",
  // 볼륨 정의
  "volumes": [
    {
      "name": "efs-storage",
      "efsVolumeConfiguration": {
        "fileSystemId": "fs-12345678",
        "transitEncryption": "ENABLED",
        "authorizationConfig": {
          "iam": "ENABLED"
        }
      }
    }
  ],
  "containerDefinitions": [
    {
      "name": "web",
      "image": "my-app:latest",
      // 컨테이너에 볼륨 마운트
      "mountPoints": [
        {
          "sourceVolume": "efs-storage",
          "containerPath": "/var/data",
          "readOnly": false
        }
      ]
    }
  ]
}

김개발 씨는 사용자가 프로필 사진을 업로드하는 기능을 구현했습니다. 로컬에서 테스트할 때는 완벽하게 동작했습니다.

하지만 ECS에 배포하고 나서 이상한 일이 벌어졌습니다. 사용자가 사진을 업로드하면 잘 보이다가, 몇 시간 후에 사라지는 것이었습니다.

김개발 씨는 코드를 아무리 뜯어봐도 문제를 찾을 수 없었습니다. 박시니어 씨에게 물어보니 한숨을 쉬며 말했습니다.

"컨테이너의 파일 시스템은 임시적이에요. 재시작하면 다 사라져요." 볼륨이란 정확히 무엇일까요?

쉽게 비유하자면, 볼륨은 마치 외장 하드디스크와 같습니다. 노트북의 내장 디스크에 파일을 저장하면 포맷할 때 다 사라지지만, 외장 하드디스크에 저장하면 노트북을 바꿔도 데이터가 남아있습니다.

컨테이너도 마찬가지입니다. 컨테이너 내부에 파일을 저장하면 컨테이너가 삭제될 때 함께 사라지지만, 볼륨에 저장하면 영구적으로 보존됩니다.

볼륨 없이 파일을 다루면 어떤 문제가 생길까요? 실제로 한 전자상거래 사이트에서 있었던 일입니다.

고객들이 상품 리뷰에 사진을 첨부할 수 있었는데, 개발자는 컨테이너 내부에 파일을 저장했습니다. 처음에는 잘 동작했지만, Auto Scaling으로 새 컨테이너가 생성되면 기존 파일들이 없었습니다.

사용자가 자기가 올린 사진을 볼 수 없다는 문의가 쏟아졌습니다. 더 큰 문제는 배포할 때였습니다.

새 버전을 배포하면 기존 컨테이너가 종료되고 새 컨테이너가 생성됩니다. 그 순간 모든 사용자 업로드 파일이 사라졌습니다.

고객들의 항의가 빗발쳤고, 회사는 신뢰를 잃었습니다. 바로 이런 문제를 해결하기 위해 볼륨이 필요합니다.

ECS에서는 volumes 배열에 사용할 볼륨을 정의합니다. 가장 많이 사용하는 것은 EFS(Elastic File System)입니다.

EFS는 AWS의 완전 관리형 파일 스토리지로, 여러 컨테이너가 동시에 읽고 쓸 수 있습니다. efsVolumeConfiguration에서 fileSystemId를 지정합니다.

이것은 미리 생성해둔 EFS 파일 시스템의 ID입니다. transitEncryption을 ENABLED로 설정하면 전송 중 암호화가 활성화됩니다.

보안을 위해 권장되는 설정입니다. authorizationConfig에서 iam을 ENABLED로 하면 IAM 역할로 접근 제어를 할 수 있습니다.

특정 태스크만 EFS에 접근하도록 제한할 수 있습니다. 컨테이너 정의에서는 mountPoints 배열로 볼륨을 마운트합니다.

sourceVolume은 위에서 정의한 볼륨 이름이고, containerPath는 컨테이너 내부의 마운트 경로입니다. 애플리케이션은 이 경로에 파일을 읽고 쓸 수 있습니다.

readOnly를 false로 하면 쓰기가 가능하고, true로 하면 읽기만 가능합니다. 실제 현업에서는 어떻게 활용할까요?

EFS는 특히 마이크로서비스 아키텍처에서 유용합니다. 여러 서비스가 같은 파일을 공유해야 할 때 EFS를 사용하면 편리합니다.

예를 들어 사용자가 업로드한 이미지를 웹 서버가 저장하고, 이미지 처리 서버가 읽어서 썸네일을 생성하는 시나리오를 생각해봅시다. 두 서비스가 같은 EFS를 마운트하면 파일을 쉽게 공유할 수 있습니다.

또 다른 사용 사례는 설정 파일 공유입니다. 여러 컨테이너가 같은 설정 파일을 읽어야 할 때 EFS에 파일을 두고 readOnly로 마운트하면 됩니다.

설정이 변경되면 EFS의 파일만 수정하면 모든 컨테이너에 반영됩니다. Netflix는 ECS와 EFS를 조합해서 수백만 명의 사용자에게 스트리밍 서비스를 제공하고 있습니다.

인코딩된 비디오 파일을 EFS에 저장하고, 여러 스트리밍 서버가 이를 읽어서 전송합니다. 하지만 주의할 점도 있습니다.

초보 개발자들이 흔히 하는 실수는 모든 데이터를 EFS에 저장하는 것입니다. EFS는 편리하지만 비용이 비싸고 성능도 로컬 디스크보다 느립니다.

진짜 영구적으로 보존해야 하는 데이터만 EFS에 저장하고, 임시 파일이나 캐시는 컨테이너 내부에 두는 것이 좋습니다. 또한 EFS는 성능 모드와 처리량 모드를 선택할 수 있습니다.

작은 파일을 자주 읽고 쓴다면 범용 성능 모드가 적합하고, 큰 파일을 다룬다면 Max I/O 모드가 좋습니다. 처음에는 버스팅 모드로 시작하고, 필요하면 프로비저닝 모드로 변경하세요.

정말 큰 규모의 파일 저장이 필요하다면 S3를 고려하는 것도 좋습니다. S3는 EFS보다 훨씬 저렴하고, 무한대로 확장 가능합니다.

다만 파일 시스템처럼 사용할 수 없고 API로 접근해야 합니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.

박시니어 씨의 조언을 듣고 EFS를 설정한 김개발 씨는 더 이상 파일이 사라지는 문제를 겪지 않았습니다. "이제 안심할 수 있어요!" 볼륨을 제대로 사용하면 안정적이고 확장 가능한 파일 저장소를 구축할 수 있습니다.

여러분도 영구적으로 보존해야 하는 데이터가 있다면 반드시 볼륨을 사용하세요.

실전 팁

💡 - 모든 데이터를 EFS에 넣지 말고, 진짜 필요한 것만 저장하세요

  • 정적 파일(이미지, 동영상)은 S3가 더 저렴하고 효율적입니다
  • transitEncryption을 반드시 활성화해서 보안을 강화하세요

이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!

#AWS#ECS#TaskDefinition#Container#CloudWatch

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.

함께 보면 좋은 카드 뉴스

쿠버네티스 아키텍처 완벽 가이드

초급 개발자를 위한 쿠버네티스 아키텍처 설명서입니다. 클러스터 구조부터 Control Plane, Worker Node, 파드, 네트워킹까지 실무 관점에서 쉽게 풀어냅니다. 점프 투 자바 스타일로 술술 읽히는 이북 형식으로 작성되었습니다.

Docker로 컨테이너화 완벽 가이드

Spring Boot 애플리케이션을 Docker로 컨테이너화하는 방법을 초급 개발자도 쉽게 이해할 수 있도록 실무 중심으로 설명합니다. Dockerfile 작성부터 멀티스테이지 빌드, 이미지 최적화, Spring Boot의 Buildpacks까지 다룹니다.

보안 아키텍처 구성 완벽 가이드

프로젝트의 보안을 처음부터 설계하는 방법을 배웁니다. AWS 환경에서 VPC부터 WAF, 암호화, 접근 제어까지 실무에서 바로 적용할 수 있는 보안 아키텍처를 단계별로 구성해봅니다.

AWS Organizations 완벽 가이드

여러 AWS 계정을 체계적으로 관리하고 통합 결제와 보안 정책을 적용하는 방법을 실무 스토리로 쉽게 배워봅니다. 초보 개발자도 바로 이해할 수 있는 친절한 설명과 실전 예제를 제공합니다.

AWS KMS 암호화 완벽 가이드

AWS KMS(Key Management Service)를 활용한 클라우드 데이터 암호화 방법을 초급 개발자를 위해 쉽게 설명합니다. CMK 생성부터 S3, EBS 암호화, 봉투 암호화까지 실무에 필요한 모든 내용을 담았습니다.