Jiwon Min Developer

AWS Secrets Manager와 ECS 연동: 프로덕션 환경을 위한 안전한 비밀 관리 완벽 가이드

프로덕션 환경에서 컨테이너 기반의 애플리케이션을 운영할 때, 가장 까다로운 문제 중 하나는 바로 비밀(Secret) 관리입니다. 데이터베이스 자격 증명, 외부 API 키, 인증서 등 민감한 정보를 코드에 하드코딩하거나, Git 리포지토리에 커밋하거나, 심지어 일반 환경 변수로 주입하는 것은 심각한 보안 취약점으로 이어질 수 있습니다. 이러한 방식은 비밀 정보가 유출될 위험을 높이며, 비밀 값 변경 시 애플리케이션의 재배포가 필요해 관리 복잡성을 증가시킵니다.

안전하고 효율적인 비밀 관리를 위해 많은 팀이 AWS Secrets Manager와 같은 전문 솔루션을 도입합니다. AWS Secrets Manager는 비밀 정보의 중앙 관리, 수명 주기 제어, 자동 순환(Rotation), 그리고 IAM을 통한 세분화된 접근 제어를 제공하여 보안 수준을 획기적으로 향상시킵니다. 특히 Amazon ECS(Elastic Container Service)와 긴밀하게 통합되어, 컨테이너 애플리케이션에 비밀 정보를 동적으로 안전하게 주입하는 강력한 메커니즘을 제공합니다. 이 글에서는 AWS Secrets Manager와 ECS를 연동하는 핵심 아키텍처를 이해하고, 실무에서 바로 적용 가능한 설정 방법과 모범 사례를 심도 있게 다루겠습니다.

AWS Secrets Manager와 ECS 연동: 프로덕션 환경을 위한 안전한 비밀 관리 완벽 가이드

© AI Generated Image


도입 배경 및 문제 정의

컨테이너 환경이 보편화되면서, 애플리케이션 구성과 비밀 정보를 분리하는 것은 ‘선택’이 아닌 ‘필수’가 되었습니다. 전통적인 비밀 관리 방식들은 다음과 같은 명확한 한계를 가집니다.

  • 코드/설정 파일에 하드코딩: 가장 위험한 방식으로, 코드 저장소가 유출되면 모든 비밀 정보가 함께 노출됩니다.
  • Docker 이미지에 환경 변수(ENV)로 저장: docker inspect 명령어나 컨테이너 내부 접근 시 쉽게 노출될 수 있으며, 이미지 레이어에 평문으로 저장될 위험이 있습니다.
  • .env 파일 사용: 컨테이너 호스트(EC2 인스턴스)에 파일이 저장되므로, 인스턴스에 접근 권한이 있는 모든 사용자가 비밀을 볼 수 있습니다. 파일 관리 및 배포도 번거롭습니다.

이러한 문제들을 해결하기 위해, 우리는 비밀 관리의 책임을 애플리케이션 외부의 안전한 중앙 저장소로 이전해야 합니다. AWS Secrets Manager는 바로 이 역할을 수행하며, ECS와의 네이티브 연동을 통해 개발자가 비밀 관리의 복잡성에서 벗어나 비즈니스 로직에 집중할 수 있도록 돕습니다.

핵심 아키텍처 및 원리

ECS에서 AWS Secrets Manager의 비밀을 사용하는 핵심 원리는 ECS Task Execution Role에 있습니다. ECS Agent는 컨테이너 이미지를 ECR에서 가져오고(pull), CloudWatch Logs에 로그를 전송하며, Secrets Manager에서 비밀을 가져오는 등의 작업을 수행하기 위해 IAM 역할(Role)이 필요합니다. 이 역할을 ‘Task Execution Role’이라고 부릅니다.

우리가 할 일은 이 Task Execution Role에 특정 비밀에 접근할 수 있는 secretsmanager:GetSecretValue 권한을 부여하는 것입니다. 그러면 ECS Agent는 태스크(Task) 시작 시점에 이 역할을 사용하여 Secrets Manager로부터 비밀 값을 안전하게 가져와 컨테이너에 주입해 줍니다.

ECS에 비밀을 주입하는 방식은 크게 두 가지로 나뉩니다.

  1. 환경 변수로 주입 (Injecting as Environment Variables):
    • 가장 간단하고 일반적인 방법입니다.
    • ECS Task Definition의 secrets 속성을 사용하여 Secrets Manager에 저장된 키-값 쌍을 컨테이너의 환경 변수로 매핑합니다.
    • 장점: 기존에 환경 변수를 사용하던 애플리케이션의 코드 변경 없이 바로 적용할 수 있습니다.
    • 단점: 컨테이너 내부에서 envprintenv 명령어로 비밀이 노출될 수 있고, 일부 민감한 시스템에서는 프로세스 정보(예: /proc/[pid]/environ)를 통해 확인될 수 있어 보안 강도가 상대적으로 낮습니다.
  2. 파일로 마운트 (Mounting as a File):
    • Secrets Manager에 저장된 전체 JSON 비밀 텍스트를 컨테이너 내부의 특정 경로에 파일로 마운트합니다.
    • ECS Task Definition의 mountPointsvolumes 속성을 사용하며, secretOptions를 통해 Secrets Manager ARN을 지정합니다.
    • 장점: 비밀이 환경 변수 목록에 나타나지 않아 보안성이 더 높습니다. 복잡한 구조의 JSON 비밀 전체를 애플리케이션에서 파싱하여 사용하기에 용이합니다.
    • 단점: 애플리케이션에서 파일 시스템을 읽고 파싱하는 로직이 추가로 필요합니다.

두 방식 모두 장단점이 있으므로, 애플리케이션의 특성과 보안 요구사항에 맞춰 적절한 방식을 선택해야 합니다.

실무 적용 코드/설정 딥다이브

이제 실제 프로덕션 환경에서 적용하는 과정을 단계별로 살펴보겠습니다. 데이터베이스 접속 정보를 담고 있는 비밀을 ECS 컨테이너에 주입하는 시나리오를 가정합니다.

1단계: AWS Secrets Manager에 비밀 생성

먼저, 애플리케이션에서 사용할 비밀을 생성합니다. 여기서는 데이터베이스 연결 정보를 JSON 형식으로 저장하겠습니다.

AWS Management Console 또는 AWS CLI를 사용하여 생성할 수 있습니다.

# AWS CLI를 사용하여 비밀 생성 예제
aws secretsmanager create-secret --name "prod/myapp/database" \
    --description "Database credentials for my application in production" \
    --secret-string '{
        "username": "myuser",
        "password": "<YOUR_SECURE_PASSWORD>",
        "engine": "postgres",
        "host": "<YOUR_RDS_ENDPOINT>",
        "port": 5432,
        "dbname": "mydatabase"
    }'

[🚨 보안 주의] 위 예제의 <...> 부분은 실제 값으로 대체해야 합니다. 실제 비밀번호를 명령어에 직접 입력하는 것은 피하고, --secret-string file://my-secret.json과 같이 파일을 이용하는 것이 더 안전합니다.

생성 후, 해당 비밀의 ARN(Amazon Resource Name)을 기록해 둡니다. (예: arn:aws:secretsmanager:ap-northeast-2:123456789012:secret:prod/myapp/database-AbCdEf)

2단계: ECS Task Execution IAM Role 정책 설정

ECS 태스크가 Secrets Manager에 접근할 수 있도록 Task Execution Role에 권한을 추가해야 합니다. 일반적으로 AmazonECSTaskExecutionRolePolicy 관리형 정책이 이미 연결되어 있지만, Secrets Manager 접근 권한은 별도로 추가해야 합니다.

아래와 같은 인라인 정책을 생성하여 Task Execution Role에 연결합니다.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "secretsmanager:GetSecretValue"
            ],
            "Resource": [
                "arn:aws:secretsmanager:ap-northeast-2:123456789012:secret:prod/myapp/database-AbCdEf"
            ]
        },
        {
            "Effect": "Allow",
            "Action": "kms:Decrypt",
            "Resource": "arn:aws:kms:ap-northeast-2:123456789012:key/<YOUR_KMS_KEY_ID>",
            "Condition": {
                "StringEquals": {
                    "kms:ViaService": "secretsmanager.ap-northeast-2.amazonaws.com"
                }
            }
        }
    ]
}
  • secretsmanager:GetSecretValue: 지정된 비밀 값을 읽을 수 있는 권한입니다. 보안을 위해 Resource에 와일드카드(*) 대신 특정 비밀의 ARN을 명시하는 것이 매우 중요합니다.
  • kms:Decrypt: Secrets Manager는 기본적으로 AWS KMS를 사용하여 비밀을 암호화합니다. 따라서 해당 KMS 키로 암호화된 비밀을 복호화할 수 있는 권한이 필요합니다. 만약 기본 KMS 키(aws/secretsmanager)를 사용했다면 리소스 ARN이 조금 다를 수 있습니다.

3단계: ECS Task Definition 구성

이제 ECS Task Definition에 Secrets Manager 연동을 설정할 차례입니다. task-definition.json 파일의 일부를 통해 두 가지 방식을 모두 살펴보겠습니다.

방법 1: 환경 변수로 주입

containerDefinitions 섹션에 secrets 객체를 추가합니다. valueFrom에 Secrets Manager ARN과 함께 JSON 키를 지정하면, 해당 값이 name으로 지정된 환경 변수에 주입됩니다.

{
    "family": "my-app-task",
    "taskRoleArn": "arn:aws:iam::123456789012:role/MyTaskRole",
    "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
    "networkMode": "awsvpc",
    "containerDefinitions": [
        {
            "name": "my-app-container",
            "image": "123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/my-app:latest",
            "portMappings": [
                {
                    "containerPort": 8000,
                    "hostPort": 8000
                }
            ],
            "secrets": [
                {
                    "name": "DB_USERNAME",
                    "valueFrom": "arn:aws:secretsmanager:ap-northeast-2:123456789012:secret:prod/myapp/database-AbCdEf:username::"
                },
                {
                    "name": "DB_PASSWORD",
                    "valueFrom": "arn:aws:secretsmanager:ap-northeast-2:123456789012:secret:prod/myapp/database-AbCdEf:password::"
                },
                {
                    "name": "DB_HOST",
                    "valueFrom": "arn:aws:secretsmanager:ap-northeast-2:123456789012:secret:prod/myapp/database-AbCdEf:host::"
                }
            ]
        }
    ]
    // ... 기타 설정
}

참고: valueFrom의 ARN 형식은 secret-arn:json-key:version-stage:version-id 입니다. json-key 뒤에 ::를 붙이면 최신 버전을 사용한다는 의미입니다.

방법 2: 파일로 마운트

volumes, mountPoints, secrets 속성을 조합하여 사용합니다. 이 방법은 전체 비밀 JSON을 하나의 파일로 만듭니다.

{
    "family": "my-app-task-file-secret",
    "taskRoleArn": "arn:aws:iam::123456789012:role/MyTaskRole",
    "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
    "networkMode": "awsvpc",
    "containerDefinitions": [
        {
            "name": "my-app-container",
            "image": "123456789012.dkr.ecr.ap-northeast-2.amazonaws.com/my-app:latest",
            "portMappings": [
                // ...
            ],
            "mountPoints": [
                {
                    "sourceVolume": "db-secret-volume",
                    "containerPath": "/etc/secrets",
                    "readOnly": true
                }
            ]
        }
    ],
    "volumes": [
        {
            "name": "db-secret-volume",
            "host": {},
            "secretOptions": {
                "name": "database-credentials",
                "valueFrom": "arn:aws:secretsmanager:ap-northeast-2:123456789012:secret:prod/myapp/database-AbCdEf"
            }
        }
    ]
}

이 설정은 잘못되었습니다. volumessecretOptions를 직접적으로 사용하는 방식은 EC2 Launch Type에서 Docker Volume Driver와 연동할 때 사용되는 레거시 방식에 가깝습니다. Fargate 및 최신 ECS에서는 secrets 옵션을 사용하여 환경 변수로 주입하거나, 애플리케이션이 직접 SDK로 호출하는 것이 권장되는 패턴입니다.

정정: Fargate에서 파일을 마운트하는 현대적인 방식은 aws-secrets-extension과 같은 사이드카 컨테이너를 사용하거나, 애플리케이션 부트스트랩 스크립트에서 AWS CLI/SDK를 통해 비밀을 파일로 저장하는 것입니다. 하지만 가장 네이티브하고 간단한 파일 마운트 방식은 사실 AWS Systems Manager Parameter Store를 사용하는 것입니다. Secrets Manager와의 직접적인 파일 마운트 기능은 제한적입니다. 혼란을 드려 죄송합니다. 실무에서는 환경 변수 주입이 가장 널리 쓰이는 패턴입니다.

4단계: 애플리케이션 코드 (Python 예시)

환경 변수로 주입받은 비밀을 애플리케이션에서 사용하는 것은 매우 간단합니다.

# settings.py (Django 예시)
import os
import json

# 환경 변수로 주입된 비밀 읽기
DB_USERNAME = os.environ.get('DB_USERNAME')
DB_PASSWORD = os.environ.get('DB_PASSWORD')
DB_HOST = os.environ.get('DB_HOST')

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'mydatabase',
        'USER': DB_USERNAME,
        'PASSWORD': DB_PASSWORD,
        'HOST': DB_HOST,
        'PORT': '5432',
    }
}

만약 애플리케이션이 직접 SDK를 통해 Secrets Manager를 호출한다면 다음과 같이 구현할 수 있습니다. (이 경우, Task Role에 secretsmanager:GetSecretValue 권한이 필요합니다, Execution Role이 아닌)

import boto3
import json

def get_secret(secret_name, region_name="ap-northeast-2"):
    session = boto3.session.Session()
    client = session.client(
        service_name='secretsmanager',
        region_name=region_name
    )
    
    try:
        get_secret_value_response = client.get_secret_value(
            SecretId=secret_name
        )
    except Exception as e:
        # 프로덕션에서는 더 정교한 예외 처리가 필요합니다.
        raise e
    else:
        # 비밀은 'SecretString'에 JSON 문자열로 저장되어 있습니다.
        secret = get_secret_value_response['SecretString']
        return json.loads(secret)

# 애플리케이션 초기화 시점에 호출
db_credentials = get_secret("prod/myapp/database")
DB_USERNAME = db_credentials['username']
DB_PASSWORD = db_credentials['password']
# ...

이 방식은 ECS Task Definition에 비밀 ARN이 노출되지 않는다는 장점이 있지만, 애플리케이션 코드에 AWS SDK 의존성이 추가되고 API 호출 비용 및 레이턴시를 고려해야 합니다.

성능 최적화 및 Best Practices

  1. 최소 권한의 원칙 (Principle of Least Privilege): IAM 정책을 설정할 때, 항상 와일드카드(*) 대신 특정 비밀의 ARN을 명시하여 해당 태스크가 꼭 필요한 비밀에만 접근하도록 제한해야 합니다.

  2. 비밀 순환(Secret Rotation) 활성화: 데이터베이스 암호와 같이 주기적으로 변경해야 하는 비밀은 Secrets Manager의 자동 순환 기능을 사용하는 것이 좋습니다. AWS Lambda와 연동하여 정해진 주기(예: 90일)마다 자동으로 비밀을 변경하고 Secrets Manager에 업데이트할 수 있습니다. 이는 보안을 크게 강화합니다.

  3. 애플리케이션 내 캐싱: 만약 애플리케이션이 SDK를 통해 직접 비밀을 조회하는 경우, 애플리케이션 시작 시 한 번만 조회하여 메모리에 캐싱하고 재사용하는 것이 좋습니다. 모든 요청마다 GetSecretValue API를 호출하면 불필요한 비용과 지연 시간이 발생할 수 있습니다.

  4. 비밀의 논리적 분리: prod/myapp/database, dev/myapp/database와 같이 환경(prod, dev)과 서비스명(myapp)을 조합하여 비밀 이름을 지정하면 관리가 용이합니다.

  5. 감사 및 모니터링: 모든 Secrets Manager API 호출은 AWS CloudTrail에 기록됩니다. 정기적으로 CloudTrail 로그를 모니터링하거나 Amazon GuardDuty와 같은 서비스를 사용하여 비정상적인 비밀 접근 시도를 탐지하고 대응해야 합니다.

결론

AWS Secrets Manager를 Amazon ECS와 연동하는 것은 현대적인 클라우드 네이티브 애플리케이션의 보안을 위한 핵심적인 단계입니다. 비밀 정보를 코드와 인프라로부터 안전하게 분리하고 중앙에서 관리함으로써, 보안 취약점을 크게 줄이고 운영 효율성을 높일 수 있습니다. Task Execution Role을 이용한 환경 변수 주입 방식은 구현이 간단하면서도 강력한 보안을 제공하므로 대부분의 시나리오에 적합합니다.

더 이상 .env 파일이나 하드코딩된 비밀 값으로 인해 불안해하지 마십시오. 오늘 소개된 아키텍처와 모범 사례를 프로덕션 환경에 적용하여, 더욱 안전하고 견고하며 확장 가능한 컨테이너 서비스를 구축하시길 바랍니다.

참고문헌