Docker 환경 변수 마스터하기: 설정과 비밀 정보

환경 변수를 마스터하여 안전하고 유연한 Docker 배포를 구현하세요. 이 포괄적인 가이드는 일반 애플리케이션 설정을 위한 환경 변수 사용과 API 키, 비밀번호 같은 민감한 데이터의 안전한 관리 사이의 중요한 차이점을 명확히 설명합니다. 민감하지 않은 설정을 전달하는 실용적인 방법을 배우고, 환경 변수를 통해 비밀 정보를 노출할 때의 심각한 위험을 이해하며, Docker Secrets와 Compose를 활용한 강력하고 암호화된 비밀 관리 방법을 알아보세요. Docker 지식을 향상시키고 애플리케이션을 보호하세요.

Docker 환경 변수 마스터하기: 설정과 비밀 정보

환경 변수는 Docker에서 편리합니다. 동일한 이미지를 개발, 스테이징, 프로덕션 환경에서 서로 다른 설정으로 실행할 수 있기 때문입니다. 하지만 팀이 비밀번호, 서명 키, API 토큰을 로그 레벨이나 포트 번호와 같은 곳에 함께 넣으면 그 편리함이 위험해집니다.

명확한 개념 모델은 간단합니다. 환경 변수는 민감하지 않은 런타임 설정에 적합합니다. 비밀 정보는 비밀 저장소나 마운트된 비밀 파일에서 가져와야 하며, 접근이 제한되고 교체 계획이 있어야 합니다.

설정을 위한 환경 변수 이해하기

환경 변수는 Docker 컨테이너에서 실행되는 애플리케이션을 포함하여 런타임 설정을 전달하는 간단하고 널리 채택된 방법입니다. 이를 통해 Docker 이미지를 다시 빌드하지 않고도 애플리케이션의 동작을 수정할 수 있어 컨테이너를 더 유연하고 이식 가능하게 만듭니다. 이는 애플리케이션 포트 번호, 디버그 플래그, 타사 서비스 URL과 같은 민감하지 않은 동적 설정에 이상적입니다.

설정 변수 전달 방법

Docker는 컨테이너에 환경 변수를 정의하고 주입하는 여러 방법을 제공합니다.

1. Dockerfile의 ENV 명령어

ENV 명령어는 컨테이너가 실행될 때 내부에서 사용할 수 있는 기본 환경 변수를 설정합니다. 이는 변경될 가능성이 낮거나 애플리케이션에 적절한 기본값을 제공하는 변수에 적합합니다.

FROM alpine:latest

ENV APP_PORT=8080
ENV DEBUG_MODE=false

COPY ./app /app
WORKDIR /app
CMD ["/app/start.sh"]

팁: ENV는 기본값을 설정하지만, 런타임에 재정의될 수 있습니다.

2. docker run과 함께 -e 또는 --env 플래그 사용

단일 컨테이너를 실행할 때 -e 또는 --env 플래그를 사용하여 환경 변수를 직접 전달할 수 있습니다. 이는 임시 테스트나 Dockerfile 기본값과 다른 특정 설정을 제공할 때 일반적입니다.

docker run -d -p 80:8080 --name my_app_instance \
  -e APP_PORT=80 \
  -e DEBUG_MODE=true \
  my_app_image:latest

3. Docker Compose의 env_file

특히 docker-compose.yml 파일에 정의된 여러 서비스에 걸쳐 여러 환경 변수를 관리할 때 env_file 옵션은 매우 편리합니다. 하나 이상의 .env 파일에서 변수를 로드하여 docker-compose.yml을 더 깔끔하게 유지할 수 있습니다.

docker-compose.yml:

version: '3.8'
services:
  webapp:
    image: my_app_image:latest
    ports:
      - "80:8080"
    env_file:
      - ./config/app.env

./config/app.env:

APP_PORT=8080
DEBUG_MODE=false
API_ENDPOINT=https://api.example.com/v1

4. Docker Compose의 environment

또는 docker-compose.yml의 서비스 environment 섹션 내에서 직접 환경 변수를 정의할 수 있습니다. 이는 변수 수가 적거나 단일 서비스에 특화된 변수에 선호됩니다.

version: '3.8'
services:
  webapp:
    image: my_app_image:latest
    ports:
      - "80:8080"
    environment:
      APP_PORT: 8080
      DEBUG_MODE: false

비밀 정보에 환경 변수 사용의 함정

환경 변수는 설정에 탁월하지만, 민감한 데이터(비밀번호, API 키, 개인 SSH 키 등)를 관리하는 데는 근본적으로 안전하지 않습니다. 이는 특히 개발 환경에서 자주 간과되는 중요한 보안 취약점입니다.

환경 변수가 비밀 정보에 안전하지 않은 이유:

  1. docker inspect를 통한 가시성: Docker 호스트에 접근 권한이 있는 사람은 누구나 docker inspect <container_id>를 사용하여 실행 중인 컨테이너의 환경 변수를 쉽게 볼 수 있습니다. 즉, 비밀 정보가 일반 텍스트로 명확하게 표시됩니다.

    # 비밀 정보 노출 예시 (프로덕션에서는 절대 이렇게 하지 마세요)
    docker run -d -e DB_PASSWORD=mysecretpassword --name insecure_app nginx:latest
    
    # 누구나 비밀번호를 볼 수 있습니다
    docker inspect insecure_app | grep DB_PASSWORD
    
  2. 프로세스 염탐: 컨테이너 내부에서 다른 프로세스나 사용자(여러 사용자가 있는 경우)가 환경 변수를 읽을 수 있습니다. 특히 애플리케이션이 루트로 실행되거나 상승된 권한이 있는 경우 더욱 그렇습니다.

  3. 로깅 및 기록: 환경 변수가 의도치 않게 로그, CI/CD 파이프라인 기록 또는 셸 기록에 포함되어 우발적으로 노출될 수 있습니다.

  4. 이미지 레이어: Dockerfile에서 ENV를 사용하여 비밀 정보를 설정하면 해당 비밀 정보가 이미지 레이어에 포함되어 이후 레이어에서 unset을 시도해도 남아 있습니다. 이렇게 하면 이미지 자체에서 비밀 정보를 검색할 수 있습니다.

  5. 우발적 공유: 비밀 정보가 포함된 .env 파일이나 docker-compose.yml 파일이 버전 관리 시스템에 커밋되거나 부적절하게 공유되어 광범위한 노출로 이어지는 경우가 많습니다.

경고: 민감한 정보를 일반 환경 변수로 취급하는 것은 일반적인 보안 실수입니다. 항상 환경 변수는 호스트와 컨테이너 내에서 공개적으로 볼 수 있다고 가정하십시오.

Docker에서 비밀 정보 안전하게 관리하기

민감한 데이터에 대한 환경 변수의 보안 문제를 해결하기 위해 Docker는 전용 비밀 관리 기능을 제공합니다. 주로 Docker Secrets(Docker Swarm용)와 Docker Composesecrets 기능( Docker Swarm secrets을 활용하거나 파일을 마운트할 수 있음)을 통해 제공됩니다.

Docker Secrets (Docker Swarm 모드)

Docker Secrets는 Docker Swarm 모드에 통합된 기능으로, 서비스에 대한 민감한 데이터를 안전하게 전송하고 저장하는 방법을 제공합니다. Secrets는:

  • Swarm 관리자의 Raft 로그에 저장 시 암호화됩니다.
  • 권한이 부여된 서비스 작업에 안전하게 전송됩니다.
  • 환경 변수로 노출되지 않고 일반적으로 /run/secrets/<secret_name>에 있는 컨테이너의 파일 시스템에 인메모리 파일로 마운트됩니다.
  • 명시적으로 액세스 권한이 부여된 서비스만 접근할 수 있습니다.

Docker Secrets 사용 방법 (Swarm 모드)

  1. Swarm 초기화 (아직 안 된 경우):
    
    

docker swarm init ```

  1. Secret 생성: Secrets는 파일 또는 표준 입력에서 생성됩니다.
    
    

echo "my_secure_db_password" | docker secret create db_password_secret - echo "SG.your_api_key_here" | docker secret create sendgrid_api_key - ```

  1. Secret으로 서비스 배포: 서비스는 이름으로 secrets을 참조합니다. Docker는 secret을 컨테이너에 마운트합니다.
    
    

docker service create --name my-webapp
--secret db_password_secret
--secret sendgrid_api_key
my_app_image:latest ```

  1. 컨테이너에서 Secrets에 접근: 애플리케이션은 마운트된 파일 경로에서 secret을 읽습니다.
    
    

Python 애플리케이션 코드에서 (다른 언어도 유사)

with open('/run/secrets/db_password_secret', 'r') as f: db_password = f.read().strip()

with open('/run/secrets/sendgrid_api_key', 'r') as f: sendgrid_key = f.read().strip() ```

Docker Compose와 Secrets (단일 호스트 또는 Swarm용)

Docker Compose 버전 3.1+는 secrets 섹션을 도입하여 docker-compose.yml 내에서 secrets을 정의하고 참조할 수 있게 합니다. Swarm 모드에서 실행할 때 Compose는 Docker Swarm의 기본 secrets을 활용합니다. Swarm 모드 없이 단일 호스트에서 실행할 때도 Compose는 호스트의 파일을 컨테이너에 안전하게 마운트하여 secrets을 지원하지만, Swarm이 제공하는 저장 시 암호화는 없습니다.

docker-compose.yml에서 secrets 사용하기

  1. Secrets 정의: 외부 파일을 참조하거나 외부 secret(미리 생성된 Swarm secret)으로 만들어 secrets을 정의할 수 있습니다.

    # docker-compose.yml
    version: '3.8'
    
    services:
      webapp:
        image: my_app_image:latest
        ports:
          - "80:8080"
        secrets:
          - db_password
          - sendgrid_api_key
    
    secrets:
      db_password:
        file: ./secrets/db_password.txt # 비밀번호가 포함된 호스트의 파일 경로
      sendgrid_api_key:
        external: true                   # 'sendgrid_api_key'라는 이름의 기존 Docker Swarm secret을 참조
    
  2. 로컬 Secret 파일 생성 (file 사용 시):

    
    

mkdir secrets echo "my_local_db_password" > ./secrets/db_password.txt ```

  1. Compose로 배포: docker compose up -d는 서비스를 배포하고 컨테이너 내부의 /run/secrets/<secret_name>에서 secrets을 사용할 수 있게 합니다.

    # 컨테이너 내부에서 ./secrets/db_password.txt의 내용은 다음 위치에 있습니다:
    # /run/secrets/db_password
    

올바른 도구 선택: 설정 vs. 비밀 정보

설정에 환경 변수를 사용할지 전용 비밀 관리 솔루션을 사용할지 결정하는 것은 하나의 주요 질문으로 귀결됩니다.

데이터가 민감한가요?

  • 예(민감한 데이터): Docker Secrets(Swarm 사용) 또는 유사한 비밀 관리 시스템(예: Kubernetes Secrets, HashiCorp Vault)을 사용하십시오. 단일 호스트 Compose 설정의 경우 secrets 섹션을 사용하여 파일을 안전하게 마운트하십시오.
  • 아니요(민감하지 않은 설정): 환경 변수(Dockerfile의 ENV, -e 플래그, env_file 또는 Compose의 environment를 통해)를 사용하십시오.
기능 환경 변수 (설정용) Docker Secrets (민감한 데이터용)
목적 민감하지 않은 애플리케이션 설정 비밀번호, API 키와 같은 민감한 데이터
가시성 docker inspect 및 종종 프로세스 검사를 통해 표시됨 파일로 마운트됨; 일반 환경 변수 값으로 표시되지 않음
보안 민감한 데이터에 적합하지 않음 더 강력한 처리; Swarm secrets는 저장 시 암호화되며, 로컬 Compose 파일 secrets는 호스트 파일 보호에 의존
앱 접근 방식 os.environ 또는 유사한 방식에서 읽음 /run/secrets/<secret_name> 파일에서 읽음
관리 주체 Docker 런타임, Docker Compose Docker Swarm, Docker Compose 또는 외부 비밀 관리자
사용 사례 포트 번호, 디버그 플래그, 민감하지 않은 URL 데이터베이스 비밀번호, API 토큰, 개인 키

둘 다에 대한 모범 사례

설정(환경 변수)의 경우:

  • Dockerfile에서 ENV를 사용하여 합리적인 기본값을 제공하십시오. 이렇게 하면 이미지를 즉시 실행할 수 있고 예상 변수를 명확하게 문서화할 수 있습니다.
  • 가능하면 설정을 외부화하십시오. 대규모 배포의 경우 docker compose와 함께 .env 파일을 사용하거나 외부 설정 서비스를 사용하십시오.
  • 모든 설정 옵션과 예상 값을 문서화하십시오(예: README.md 또는 애플리케이션 문서).
  • 환경(개발, 스테이징, 프로덕션) 간에 변경될 수 있는 값을 하드코딩하지 마십시오.

비밀 정보(Docker Secrets 등)의 경우:

  • 비밀 정보(예: 비밀 정보가 포함된 .env 파일, db_password.txt)를 Git과 같은 버전 관리 시스템에 절대 커밋하지 마십시오.
  • 정기적으로 비밀 정보를 교체하십시오. 이렇게 하면 비밀 정보가 손상되었을 때 노출 기간을 최소화할 수 있습니다.
  • 최소 권한을 부여하십시오. 서비스에 꼭 필요한 비밀 정보에만 접근 권한을 부여하십시오.
  • 비밀 정보 값 로깅을 피하십시오. 애플리케이션 및 인프라 로깅이 비밀 정보 내용을 출력하지 않는지 확인하십시오.
  • 대규모 엔터프라이즈급 배포의 경우 HashiCorp Vault, AWS Secrets Manager 또는 Azure Key Vault와 같은 전용 비밀 관리 솔루션을 고려하십시오. 이러한 솔루션은 감사, 동적 비밀 생성, ID 및 액세스 관리(IAM) 통합과 같은 고급 기능을 제공합니다.

무엇을 어디에 넣을지 결정하는 실용적인 규칙

environment에 값을 추가하기 전에 팀원이 그 값을 지원 티켓에 붙여넣으면 어떻게 될지 물어보십시오. 대답이 "심각한 일은 없다"이면 아마 설정일 것입니다. 대답이 "자격 증명을 교체해야 한다"이면 비밀 정보입니다.

좋은 환경 변수:

APP_ENV=production
LOG_LEVEL=info
PUBLIC_BASE_URL=https://example.com
FEATURE_SIGNUP_ENABLED=false
REDIS_HOST=redis

나쁜 환경 변수:

DATABASE_PASSWORD=...
STRIPE_SECRET_KEY=...
JWT_SIGNING_KEY=...
AWS_SECRET_ACCESS_KEY=...
PRIVATE_SSH_KEY=...

회색 영역이 있습니다. 데이터베이스 호스트 이름은 일반적으로 설정입니다. 사용자 이름과 비밀번호를 포함하는 전체 데이터베이스 URL은 비밀 정보입니다. 공개 분석 키는 브라우저 애플리케이션에 안전할 수 있지만, 동일한 공급업체의 개인 API 토큰은 그렇지 않습니다. 의심스러우면 값이 민감하다고 간주하고 그 반대를 증명할 때까지 그렇게 취급하십시오.

Compose .env 파일은 오해하기 쉽습니다

Docker Compose는 사람들이 자주 혼동하는 두 가지 다른 방식으로 .env를 사용합니다.

첫째, Compose는 compose.yml 내부의 변수 대체를 위해 프로젝트 수준의 .env 파일을 읽습니다:

services:
  web:
    image: "${APP_IMAGE}"
    ports:
      - "${HOST_PORT}:8080"

둘째, env_file은 변수를 컨테이너에 전달합니다:

services:
  web:
    image: my-app
    env_file:
      - ./app.env

이러한 파일은 비슷해 보일 수 있지만 목적이 다릅니다. 첫 번째는 Compose가 구성을 렌더링하는 데 도움이 됩니다. 두 번째는 컨테이너 내부의 런타임 환경이 됩니다. 프로젝트 .env의 값이 명시적으로 전달하지 않는 한 컨테이너 내부에 자동으로 나타난다고 가정하지 마십시오.

로컬 개발의 경우 체크인된 예제 파일이 유용합니다:

# .env.example
APP_ENV=development
LOG_LEVEL=debug
PUBLIC_BASE_URL=http://localhost:3000

그런 다음 실제 .env 파일을 Git에서 제외하십시오:

.env
*.env.local
secrets/

예제 파일은 개인 값을 노출하지 않고 앱이 기대하는 것을 문서화합니다.

애플리케이션에서 파일 기반 비밀 정보 읽기

많은 애플리케이션이 이미 환경 변수에 비밀 정보가 있을 것으로 예상합니다. 파일 기반 비밀 정보로 전환하는 것은 두 패턴을 한동안 모두 지원하면 더 쉽습니다.

예를 들어, Node.js 헬퍼:

import fs from "node:fs";

function readSecret(name) {
  const filePath = process.env[`${name}_FILE`];
  if (filePath) {
    return fs.readFileSync(filePath, "utf8").trim();
  }
  return process.env[name];
}

const databasePassword = readSecret("DATABASE_PASSWORD");

그런 다음 Compose 파일이 마운트된 비밀 파일을 가리킬 수 있습니다:

services:
  web:
    image: my-app
    environment:
      DATABASE_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

이 패턴은 프로덕션을 파일 마운트 비밀 정보로 이동하는 동안 애플리케이션이 이전 환경에서 계속 실행될 수 있기 때문에 잘 작동합니다. 많은 공식 이미지가 이러한 이유로 _FILE로 끝나는 변수를 이미 지원합니다.

빌드 인수에도 비밀 정보를 넣지 마십시오

환경 변수만이 함정이 아닙니다. 개인 패키지를 가져오거나 리포지토리를 복제하는 데 사용하는 경우 빌드 인수도 누출될 수 있습니다:

ARG NPM_TOKEN
RUN npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN

최종 컨테이너에 NPM_TOKEN이 표시되지 않더라도 빌드 기록과 중간 레이어는 예상보다 더 많은 정보를 노출할 수 있습니다. BuildKit을 사용하는 경우 빌드 타임 비밀 정보에 시크릿 마운트를 사용하십시오:

# syntax=docker/dockerfile:1.7
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=secret,id=npm_token \
  NPM_TOKEN="$(cat /run/secrets/npm_token)" npm ci

다음과 같이 빌드하십시오:

docker build \
  --secret id=npm_token,src=.npm-token \
  -t my-app .

이렇게 하면 토큰이 Dockerfile 밖에 유지되고 일반 레이어에 포함되는 것을 방지할 수 있습니다. 여전히 로컬 .npm-token 파일과 CI 시크릿 저장소를 보호해야 합니다.

Kubernetes, 클라우드 비밀 관리자 및 Docker

Docker Secrets는 Swarm에서 유용하고 Compose secrets는 로컬 또는 단일 호스트 설정에 유용합니다. Kubernetes에서는 일반적으로 Kubernetes Secrets, 외부 시크릿 연산자 또는 클라우드 비밀 관리자 통합을 사용합니다. AWS에서는 팀이 AWS Secrets Manager 또는 Systems Manager Parameter Store를 자주 사용합니다. Azure에서는 Azure Key Vault가 일반적입니다. Google Cloud에서는 Secret Manager가 동일한 역할을 합니다.

원칙은 플랫폼 전반에 걸쳐 동일합니다:

  • 민감한 값은 비밀 정보용으로 설계된 시스템에 저장하십시오.
  • 런타임 ID에 필요한 비밀 정보에만 액세스 권한을 부여하십시오.
  • 런타임에 비밀 정보를 마운트하거나 주입하십시오.
  • 이미지를 다시 빌드하지 않고 비밀 정보를 교체하십시오.
  • 비밀 정보를 소스 제어, 이미지 레이어, 로그 및 대시보드에서 제외하십시오.

Kubernetes Secrets는 기본적으로 인코딩되며 모든 클러스터 구성에서 자동으로 암호화되지는 않습니다. 많은 관리형 클러스터가 저장 시 암호화를 지원하지만, 가정하지 말고 실제 클러스터 설정을 확인하십시오. 위험이 높은 자격 증명의 경우 감사 로그 및 교체 지원이 있는 클라우드 비밀 관리자 또는 전용 도구를 사용하십시오.

교체는 설계의 일부입니다

교체할 수 없는 비밀 전략은 미완성입니다. 프로덕션 전에 다음 질문을 하십시오:

  • 이미지를 다시 빌드하지 않고 데이터베이스 비밀번호를 변경할 수 있습니까?
  • 롤아웃 중에 두 개의 유효한 자격 증명이 겹칠 수 있습니까?
  • 애플리케이션이 비밀 정보를 다시 읽습니까, 아니면 다시 시작해야 합니까?
  • 이전 자격 증명이 어디에 기록, 캐시 또는 저장되어 있습니까?
  • 비밀 정보가 변경되면 누가 알림을 받습니까?

데이터베이스의 경우 교체는 종종 두 번째 자격 증명을 만들고, 새 자격 증명으로 애플리케이션을 배포하고, 트래픽을 확인한 다음 이전 자격 증명을 취소하는 것을 의미합니다. API 키의 경우 공급자에 따라 다릅니다. 일부 서비스는 여러 활성 키를 허용하고 다른 서비스는 전환을 강제합니다. 가장 덜 유연한 종속성을 중심으로 배포 프로세스를 설계하십시오.

우발적 노출 정리

비밀 정보가 이미 Git에 커밋되었거나 이미지에 포함된 경우 줄을 삭제하는 것만으로는 충분하지 않습니다. 노출된 것으로 취급하십시오.

일반적인 대응은 다음과 같습니다:

  1. 자격 증명을 취소하거나 교체하십시오.
  2. 현재 코드 또는 이미지에서 제거하십시오.
  3. CI 로그, 이미지 레지스트리, 이슈 트래커 및 채팅 메시지에서 복사본을 확인하십시오.
  4. 조직이 조정을 처리할 준비가 된 경우에만 Git 기록을 다시 작성하십시오. 교체는 여전히 필요합니다.
  5. 반복 실수를 줄이기 위해 스캐닝 또는 사전 커밋 검사를 추가하십시오.

도구가 도움이 될 수 있지만 습관을 대체하지는 않습니다. 비밀 파일의 이름을 명확하게 지정하고, Git에서 무시하고, 시작 시 구성 개체를 전체적으로 인쇄하지 마십시오.

작동 패턴

환경 변수는 앱이 이 환경에서 어떻게 실행되어야 하는지 설명하는 값(포트, 로그 레벨, 기능 플래그, 서비스 호스트 이름, 민감하지 않은 URL)에 사용하십시오. 비밀 정보는 신원을 증명하거나 액세스 권한을 부여하는 값(비밀번호, 토큰, 서명 키, 개인 키, 공급자 자격 증명)에 사용하십시오.

깨끗한 Docker 이미지는 환경 전반에 걸쳐 동일합니다. 개발, 스테이징 및 프로덕션은 런타임에 동작을 변경합니다. 설정은 환경 변수로 전달될 수 있습니다. 비밀 정보는 제한된 액세스 권한이 있는 비밀 저장소 또는 마운트된 비밀 파일에서 가져와야 합니다. 이러한 분리는 모든 컨테이너 검사, 로그 줄 또는 이미지 레이어가 자격 증명 누출로 변하지 않도록 하면서 배포를 유연하게 유지합니다.