Docker secrets로 민감한 데이터 관리하기

Docker secrets로 민감한 데이터 관리하기

Docker Swarm 서비스에서 비밀번호, SSH 키, TLS 인증서처럼 민감한 데이터를 안전하게 다루는 방법을 알려드릴게요. Docker secrets를 사용하면 이런 데이터를 네트워크로 전송하거나 저장할 때 암호화하고, 필요한 컨테이너에만 골라서 전달할 수 있어요. 지금부터 secrets의 개념부터 실제 사용 예제까지 차근차근 살펴볼게요.

출처: 공식문서

본문

secrets에 대해

Docker Swarm 서비스에서 secret은 비밀번호, SSH 개인 키, SSL 인증서처럼 네트워크를 통해 전송되거나 Dockerfile 또는 애플리케이션 소스 코드에 평문으로 저장되면 안 되는 데이터 덩어리를 말해요. Docker secrets를 사용하면 이런 데이터를 중앙에서 관리하고, 접근이 필요한 컨테이너에만 안전하게 전달할 수 있어요. secrets는 전송 중에도, Docker swarm에 저장된 상태에서도 암호화돼요. 특정 secret은 명시적으로 접근 권한을 부여받은 서비스만 사용할 수 있고, 그 서비스의 태스크가 실행되는 동안에만 접근할 수 있어요.

컨테이너가 런타임에 필요하지만 이미지나 소스 코드 저장소에 저장하고 싶지 않은 민감한 데이터라면 무엇이든 secrets로 관리할 수 있어요. 예를 들면:

  • 사용자 이름과 비밀번호
  • TLS 인증서와 키
  • SSH 키
  • 데이터베이스 이름이나 내부 서버 이름 같은 중요한 데이터
  • 최대 500kb 크기의 일반 문자열 또는 바이너리 콘텐츠

참고

Docker secrets는 swarm 서비스에서만 사용할 수 있고, 독립 실행형(standalone) 컨테이너에서는 사용할 수 없어요. 이 기능을 사용하려면 컨테이너를 서비스로 실행하도록 조정해보세요. 상태를 가지는 컨테이너는 보통 코드 변경 없이 scale 1로 실행할 수 있어요.

secrets를 사용하는 또 다른 이유는 컨테이너와 자격 증명 사이에 추상화 계층을 제공하기 위함이에요. 애플리케이션에 개발, 테스트, 프로덕션 환경이 각각 따로 있다고 생각해볼게요. 각 환경마다 다른 자격 증명을 사용하지만, 개발/테스트/프로덕션 swarm에 같은 이름의 secret으로 저장해둘 수 있어요. 그러면 컨테이너는 세 환경 모두에서 secret의 이름만 알면 동작할 수 있어요.

secrets는 설정 파일처럼 민감하지 않은 데이터를 관리하는 데에도 사용할 수 있어요. 하지만 Docker는 민감하지 않은 데이터를 저장할 때 configs 사용을 지원해요. Configs는 RAM 디스크를 사용하지 않고 컨테이너의 파일시스템에 직접 마운트돼요.

Windows 지원

Docker는 Windows 컨테이너에서도 secrets를 지원해요. 구현상 차이가 있는 부분은 아래 예제에서 따로 설명할게요. 다음 주요 차이점을 기억해두세요.

  • Microsoft Windows에는 RAM 디스크를 관리하는 내장 드라이버가 없어서, 실행 중인 Windows 컨테이너 내부에서 secrets는 컨테이너의 루트 디스크에 평문으로 저장돼요. 하지만 컨테이너가 중지되면 secrets는 명시적으로 제거돼요. 또한 Windows는 docker commit 같은 명령으로 실행 중인 컨테이너를 이미지로 저장하는 것을 지원하지 않아요.
  • Windows에서는 호스트 머신의 Docker 루트 디렉터리가 있는 볼륨에 BitLocker를 활성화해서 실행 중인 컨테이너의 secrets가 저장 시 암호화되도록 권장해요.
  • Windows는 디렉터리가 아닌 파일의 bind-mount를 지원하지 않기 때문에, 사용자 지정 target을 가진 secret 파일은 Windows 컨테이너에 직접 bind-mount되지 않아요. 대신 컨테이너의 모든 secrets는 C:\ProgramData\Docker\internal\secrets에 마운트돼요. 이 경로는 구현 세부사항이므로 애플리케이션이 의존하면 안 돼요. 그 위치에서 컨테이너 안의 원하는 target으로 가리키는 심볼릭 링크가 생성돼요. 기본 target은 C:\ProgramData\Docker\secrets예요.
  • Windows 컨테이너를 사용하는 서비스를 만들 때 secrets에 대해 UID, GID, mode 옵션은 지원되지 않아요. 현재 secrets는 컨테이너 내에서 관리자와 system 접근 권한이 있는 사용자만 접근할 수 있어요.

Docker가 secrets를 관리하는 방법

swarm에 secret을 추가하면 Docker는 상호 TLS 연결을 통해 secret을 swarm 매니저로 보내요. secret은 암호화된 Raft 로그에 저장돼요. Raft 로그 전체는 다른 매니저들에도 복제되므로, 다른 swarm 관리 데이터와 동일한 고가용성 보장을 secrets에도 제공해요.

새로 생성했거나 실행 중인 서비스에 secret 접근 권한을 부여하면, 복호화된 secret은 인메모리 파일시스템에 마운트돼요. 컨테이너 내 마운트 지점의 기본 위치는 Linux 컨테이너에서 /run/secrets/<secret_name>, Windows 컨테이너에서 C:\ProgramData\Docker\secrets예요. 사용자 지정 위치도 지정할 수 있어요.

언제든지 서비스를 업데이트해서 추가 secret에 대한 접근 권한을 부여하거나 특정 secret에 대한 접근 권한을 해제할 수 있어요.

노드는 swarm 매니저이거나 secret 접근 권한이 부여된 서비스 태스크를 실행 중인 경우에만 (암호화된) secrets에 접근할 수 있어요. 컨테이너 태스크가 실행을 중지하면, 해당 컨테이너에 공유되었던 복호화된 secrets는 인메모리 파일시스템에서 마운트 해제되고 노드의 메모리에서 플러시돼요.

secret에 접근할 수 있는 태스크 컨테이너를 실행하는 동안 노드가 swarm과의 연결을 잃으면, 태스크 컨테이너는 여전히 자신의 secrets에 접근할 수 있지만 노드가 swarm에 다시 연결될 때까지 업데이트를 받을 수 없어요.

개별 secret은 언제든지 추가하거나 조회할 수 있고, 모든 secrets 목록도 볼 수 있어요. 실행 중인 서비스가 사용 중인 secret은 제거할 수 없어요. 실행 중인 서비스를 중단하지 않고 secret을 제거하는 방법은 Rotate a secret을 참고하세요.

secrets를 더 쉽게 업데이트하거나 롤백하려면 secret 이름에 버전 번호나 날짜를 추가하는 것을 고려해보세요. 컨테이너 내 secret의 마운트 지점을 제어할 수 있기 때문에 이 작업이 더 수월해져요.

docker secret 명령어 자세히 보기

특정 명령어에 대한 자세한 내용은 아래 링크를 확인하거나, 서비스에서 secrets 사용 예제로 계속 읽어보세요.

Compose 파일에서 secrets 정의하고 사용하기

Docker Swarm과 함께 Compose 파일에서 secrets를 사용하는 것은 docker secret create로 secret을 정의하는 것만큼 간단해요.

$ printf "my-secret" | docker secret create some_secret -

compose.yml 파일에서는 최상위 secrets 속성에서 secret을 external로 선언할 수 있어요.

services:
 api:
   image: my-web-app
   environment:
     MY_ENV: /run/secrets/some_secret
   secrets:
     - some_secret

secrets:
  some_secret:
    external: true

docker composedocker stack 명령어 모두 Compose 파일에서 secrets를 정의하는 것을 지원해요. 자세한 내용은 Compose 파일 참조를 확인하세요.

예제

이번 섹션에서는 Docker secrets 사용법을 보여주는 세 가지 단계별 예제를 준비했어요. 예제에 사용된 이미지는 Docker secrets를 더 쉽게 사용할 수 있도록 업데이트된 것들이에요. 자신의 이미지를 비슷하게 수정하는 방법은 이미지에 Docker Secrets 지원 내장하기를 참고하세요.

참고

이 예제들은 단순함을 위해 단일 엔진 swarm과 확장하지 않은 서비스를 사용해요. Linux 컨테이너를 사용하지만 Windows 컨테이너도 secrets를 지원해요. Windows 지원을 참고하세요.

간단한 예제: secrets 시작하기

이 간단한 예제는 몇 가지 명령어만으로 secrets가 어떻게 동작하는지 보여줘요. 실제 사례를 보려면 중급 예제: Nginx 서비스와 secrets 사용으로 계속 읽어보세요.

  1. Docker에 secret을 추가해요. docker secret create 명령어는 마지막 인자가 secret을 읽을 파일 경로를 나타내는데, 여기서는 -로 설정되어 표준 입력을 읽어요.
$ printf "This is a secret" | docker secret create my_secret_data -
  1. redis 서비스를 만들고 secret 접근 권한을 부여해요. 기본적으로 컨테이너는 /run/secrets/<secret_name>에서 secret에 접근할 수 있지만, target 옵션으로 컨테이너 안의 파일 이름을 바꿀 수 있어요.
$ docker service create --name redis --secret my_secret_data redis:alpine
  1. docker service ps로 태스크가 문제없이 실행되는지 확인해요. 정상적으로 동작하면 출력이 다음과 비슷할 거예요.
$ docker service ps redis

ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE            ERROR               PORTS
bkna6bpn8r1a        redis.1             redis:alpine        ip-172-31-46-109    Running             Running 8 seconds ago

만약 오류가 있어서 태스크가 실패하고 계속 재시작된다면 다음과 같은 출력을 보게 될 거예요.

$ docker service ps redis

NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE            ERROR                         PORTS
redis.1.siftice35gla   redis:alpine   moby                Running             Running 4 seconds ago
 \_ redis.1.whum5b7gu13e   redis:alpine   moby                Shutdown            Failed 20 seconds ago       "task: non-zero exit (1)"
 \_ redis.1.2s6yorvd9zow   redis:alpine   moby                Shutdown            Failed 56 seconds ago       "task: non-zero exit (1)"
 \_ redis.1.ulfzrcyaf6pg   redis:alpine   moby                Shutdown            Failed about a minute ago   "task: non-zero exit (1)"
 \_ redis.1.wrny5v4xyps6   redis:alpine   moby                Shutdown            Failed 2 minutes ago        "task: non-zero exit (1)"
  1. docker psredis 서비스 태스크 컨테이너의 ID를 확인하고, docker container exec로 컨테이너에 접속해서 secret 데이터 파일의 내용을 읽어봐요. 기본적으로 secret 파일은 모든 사용자가 읽을 수 있고 secret 이름과 같은 이름을 가져요. 아래 첫 번째 명령어는 컨테이너 ID를 찾는 방법이고, 두 번째와 세 번째 명령어는 셸 완성 기능을 이용해 자동으로 처리하는 방법이에요.
$ docker ps --filter name=redis -q

5cb1c2348a59

$ docker container exec $(docker ps --filter name=redis -q) ls -l /run/secrets

total 4
-r--r--r-- 1 root root 17 Dec 13 22:48 my_secret_data

$ docker container exec $(docker ps --filter name=redis -q) cat /run/secrets/my_secret_data

This is a secret
  1. 컨테이너를 커밋하면 secret이 포함되지 않는지 확인해봐요.
$ docker commit $(docker ps --filter name=redis -q) committed_redis

$ docker run --rm -it committed_redis cat /run/secrets/my_secret_data

cat: can't open '/run/secrets/my_secret_data': No such file or directory
  1. secret을 제거해보세요. redis 서비스가 실행 중이고 secret에 접근하고 있기 때문에 제거가 실패할 거예요.
$ docker secret ls

ID                  NAME                CREATED             UPDATED
wwwrxza8sxy025bas86593fqs   my_secret_data   4 hours ago         4 hours ago

$ docker secret rm my_secret_data

Error response from daemon: rpc error: code = 3 desc = secret 'my_secret_data' is in use by the following service: redis
  1. 서비스를 업데이트해서 실행 중인 redis 서비스에서 secret 접근 권한을 제거해요.
$ docker service update --secret-rm my_secret_data redis
  1. 3단계와 4단계를 다시 반복해서 서비스가 더 이상 secret에 접근할 수 없는지 확인해요. service update 명령어가 서비스를 재배포하기 때문에 컨테이너 ID는 달라져요.
$ docker container exec -it $(docker ps --filter name=redis -q) cat /run/secrets/my_secret_data

cat: can't open '/run/secrets/my_secret_data': No such file or directory
  1. 서비스를 중지하고 제거한 다음, Docker에서 secret을 제거해요.
$ docker service rm redis

$ docker secret rm my_secret_data

간단한 예제: Windows 서비스에서 secrets 사용하기

Microsoft Windows 10에서 Windows 컨테이너를 실행하는 Docker for Windows의 Microsoft IIS 서비스에서 secrets를 사용하는 아주 간단한 예제예요. 웹 페이지를 secret에 저장하는 단순한 예제입니다.

이 예제는 PowerShell이 설치되어 있다고 가정해요.

  1. 다음 내용을 새 파일 index.html로 저장해요.
<html lang="en">
  <head><title>Hello Docker</title></head>
  <body>
    <p>Hello Docker! You have deployed a HTML page.</p>
  </body>
</html>
  1. 아직 swarm을 초기화하지 않았다면 초기화하거나 조인해요.
> docker swarm init
  1. index.html 파일을 homepage라는 이름의 swarm secret으로 저장해요.
> docker secret create homepage index.html
  1. IIS 서비스를 만들고 homepage secret에 접근 권한을 부여해요.
> docker service create `
 --name my-iis `
 --publish published=8000,target=8000 `
 --secret src=homepage,target="\inetpub\wwwroot\index.html" `
 microsoft/iis:nanoserver

참고

이 예제에 secrets를 사용할 기술적 이유는 없어요. configs가 더 적합해요. 이 예제는 설명을 위한 것일 뿐이에요.

  1. http://localhost:8000/에서 IIS 서비스에 접근해요. 첫 번째 단계의 HTML 콘텐츠를 제공할 거예요.

  2. 서비스와 secret을 제거해요.

> docker service rm my-iis
> docker secret rm homepage
> docker image remove secret-test

중급 예제: Nginx 서비스와 secrets 사용하기

이 예제는 두 부분으로 나뉘어요. 첫 번째 부분은 사이트 인증서를 생성하는 것으로 Docker secrets와 직접적인 관련은 없지만, 두 번째 부분에서 사이트 인증서와 Nginx 설정을 secrets로 저장하고 사용할 수 있게 준비하는 과정이에요.

사이트 인증서 생성하기

사이트용 루트 CA와 TLS 인증서 및 키를 생성해요. 프로덕션 사이트에서는 Let's Encrypt 같은 서비스를 사용해서 TLS 인증서와 키를 생성하는 것이 좋을 수 있지만, 이 예제에서는 명령줄 도구를 사용해요. 이 단계는 좀 복잡하지만 Docker secret으로 저장할 무언가를 만들기 위한 준비 단계일 뿐이에요. 이 하위 단계를 건너뛰고 싶다면 Let's Encrypt를 사용해서 사이트 키와 인증서를 생성하고, 파일 이름을 site.keysite.crt로 지정한 다음 Nginx 컨테이너 설정하기로 건너뛰어도 돼요.

  1. 루트 키를 생성해요.
$ openssl genrsa -out "root-ca.key" 4096
  1. 루트 키를 사용해 CSR을 생성해요.
$ openssl req \
  -new -key "root-ca.key" \
  -out "root-ca.csr" -sha256 \
  -subj '/C=US/ST=CA/L=San Francisco/O=Docker/CN=Swarm Secret Example CA'
  1. 루트 CA를 설정해요. 새 파일 root-ca.cnf를 만들고 다음 내용을 붙여넣어요. 이 설정은 루트 CA가 리프 인증서에만 서명하고 중간 CA에는 서명하지 못하도록 제한해요.
[root_ca]
basicConstraints = critical,CA:TRUE,pathlen:1
keyUsage = critical, nonRepudiation, cRLSign, keyCertSign
subjectKeyIdentifier=hash
  1. 인증서에 서명해요.
$ openssl x509 -req -days 3650 -in "root-ca.csr" \
  -signkey "root-ca.key" -sha256 -out "root-ca.crt" \
  -extfile "root-ca.cnf" -extensions \
  root_ca
  1. 사이트 키를 생성해요.
$ openssl genrsa -out "site.key" 4096
  1. 사이트 인증서를 생성하고 사이트 키로 서명해요.
$ openssl req -new -key "site.key" -out "site.csr" -sha256 \
  -subj '/C=US/ST=CA/L=San Francisco/O=Docker/CN=localhost'
  1. 사이트 인증서를 설정해요. 새 파일 site.cnf를 만들고 다음 내용을 붙여넣어요. 이 설정은 사이트 인증서가 서버 인증에만 사용되고 인증서 서명에는 사용할 수 없도록 제한해요.
[server]
authorityKeyIdentifier=keyid,issuer
basicConstraints = critical,CA:FALSE
extendedKeyUsage=serverAuth
keyUsage = critical, digitalSignature, keyEncipherment
subjectAltName = DNS:localhost, IP:127.0.0.1
subjectKeyIdentifier=hash
  1. 사이트 인증서에 서명해요.
$ openssl x509 -req -days 750 -in "site.csr" -sha256 \
  -CA "root-ca.crt" -CAkey "root-ca.key" -CAcreateserial \
  -out "site.crt" -extfile "site.cnf" -extensions server
  1. site.csrsite.cnf 파일은 Nginx 서비스에 필요하지 않지만, 새 사이트 인증서를 생성하려면 필요해요. root-ca.key 파일은 안전하게 보호하세요.

Nginx 컨테이너 설정하기

  1. HTTPS로 정적 파일을 제공하는 아주 기본적인 Nginx 설정을 만들어요. TLS 인증서와 키는 Docker secrets로 저장되어 쉽게 교체할 수 있어요.

현재 디렉터리에 새 파일 site.conf를 만들고 다음 내용을 넣어요.

server {
    listen 443 ssl;
    server_name localhost;

    ssl_certificate /run/secrets/site.crt;
    ssl_certificate_key /run/secrets/site.key;

    location / {
        root /usr/share/nginx/html;
        index index.html index.htm;
    }
}
  1. 키, 인증서, site.conf를 나타내는 세 개의 secrets를 만들어요. 500KB보다 작은 파일이라면 무엇이든 secret으로 저장할 수 있어요. 이렇게 하면 키, 인증서, 설정을 그것을 사용하는 서비스로부터 분리할 수 있어요. 각 명령어에서 마지막 인자는 호스트 머신의 파일시스템에서 secret을 읽을 파일 경로를 나타내요. 이 예제에서는 secret 이름과 파일 이름이 같아요.
$ docker secret create site.key site.key

$ docker secret create site.crt site.crt

$ docker secret create site.conf site.conf
$ docker secret ls

ID                  NAME                CREATED             UPDATED
2hvoi9mnnaof7olr3z5g3g7fp   site.key    58 seconds ago      58 seconds ago
aya1dh363719pkiuoldpter4b   site.crt    24 seconds ago      24 seconds ago
zoa5df26f7vpcoz42qf2csth8   site.conf   11 seconds ago      11 seconds ago
  1. Nginx를 실행하고 세 개의 secrets에 접근할 수 있는 서비스를 만들어요. docker service create 명령어의 마지막 부분은 site.conf secret의 위치에서 Nginx가 추가 설정 파일을 찾는 /etc/nginx.conf.d/로 가는 심볼릭 링크를 만들어요. 이 단계는 Nginx가 실제로 시작하기 전에 실행되므로, Nginx 설정을 변경해도 이미지를 다시 빌드할 필요가 없어요.

참고

일반적으로는 site.conf를 복사하는 Dockerfile을 만들고, 이미지를 빌드한 다음, 사용자 지정 이미지로 컨테이너를 실행할 거예요. 이 예제는 사용자 지정 이미지가 필요 없어요. site.conf를 제자리에 두고 컨테이너를 한 번에 실행해요.

secrets는 기본적으로 컨테이너의 /run/secrets/ 디렉터리에 위치해요. 따라서 secret을 다른 경로에서 사용하려면 컨테이너 안에서 추가 단계가 필요할 수 있어요. 아래 예제는 Nginx가 읽을 수 있도록 site.conf 파일의 실제 위치로 가는 심볼릭 링크를 만들어요.

$ docker service create \
  --name nginx \
  --secret site.key \
  --secret site.crt \
  --secret site.conf \
  --publish published=3000,target=443 \
  nginx:latest \
  sh -c "ln -s /run/secrets/site.conf /etc/nginx/conf.d/site.conf && exec nginx -g 'daemon off;'"

심볼릭 링크를 만드는 대신, secrets는 target 옵션으로 사용자 지정 위치를 지정할 수 있어요. 아래 예제는 site.conf secret이 심볼릭 링크 없이 컨테이너 내부의 /etc/nginx/conf.d/site.conf에서 사용할 수 있게 만드는 방법을 보여줘요.

$ docker service create \
  --name nginx \
  --secret site.key \
  --secret site.crt \
  --secret source=site.conf,target=/etc/nginx/conf.d/site.conf \
  --publish published=3000,target=443 \
  nginx:latest \
  sh -c "exec nginx -g 'daemon off;'"

site.keysite.crt secrets는 사용자 지정 target 위치 없이 짧은 구문을 사용해요. 짧은 구문은 secrets를 /run/secrets/에 secret과 같은 이름으로 마운트해요. 실행 중인 컨테이너 안에는 이제 다음 세 개의 파일이 존재해요.

  • /run/secrets/site.key
  • /run/secrets/site.crt
  • /etc/nginx/conf.d/site.conf
  1. Nginx 서비스가 실행 중인지 확인해요.
$ docker service ls

ID                  NAME                MODE                REPLICAS            IMAGE
zeskcec62q24        nginx               replicated          1/1                 nginx:latest

$ docker service ps nginx

NAME                  IMAGE               NODE                DESIRED STATE       CURRENT STATE            ERROR               PORTS
nginx.1.9ls3yo9ugcls   nginx:latest        moby                Running             Running 3 minutes ago
  1. 서비스가 정상 동작하는지, Nginx 서버에 접근할 수 있는지, 올바른 TLS 인증서가 사용되는지 확인해요.
$ curl --cacert root-ca.crt https://localhost:3000

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support. refer to
<a href="https://nginx.org">nginx.org</a>.<br/>
Commercial support is available at
<a href="https://www.nginx.com">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
$ openssl s_client -connect localhost:3000 -CAfile root-ca.crt

CONNECTED(00000003)
depth=1 /C=US/ST=CA/L=San Francisco/O=Docker/CN=Swarm Secret Example CA
verify return:1
depth=0 /C=US/ST=CA/L=San Francisco/O=Docker/CN=localhost
verify return:1
---
Certificate chain
 0 s:/C=US/ST=CA/L=San Francisco/O=Docker/CN=localhost
   i:/C=US/ST=CA/L=San Francisco/O=Docker/CN=Swarm Secret Example CA
---
Server certificate
-----BEGIN CERTIFICATE-----
…
-----END CERTIFICATE-----
subject=/C=US/ST=CA/L=San Francisco/O=Docker/CN=localhost
issuer=/C=US/ST=CA/L=San Francisco/O=Docker/CN=Swarm Secret Example CA
---
No client certificate CA names sent
---
SSL handshake has read 1663 bytes and written 712 bytes
---
New, TLSv1/SSLv3, Cipher is AES256-SHA
Server public key is 4096 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
SSL-Session:
    Protocol  : TLSv1
    Cipher    : AES256-SHA
    Session-ID: A1A8BF35549C5715648A12FD7B7E3D861539316B03440187D9DA6C2E48822853
    Session-ID-ctx:
    Master-Key: F39D1B12274BA16D3A906F390A61438221E381952E9E1E05D3DD784F0135FB81353DA38C6D5C021CB926E844DFC49FC4
    Key-Arg   : None
    Start Time: 1481685096
    Timeout   : 300 (sec)
    Verify return code: 0 (ok)
  1. 이 예제 실행 후 정리하려면 nginx 서비스와 저장된 secrets를 제거해요.
$ docker service rm nginx

$ docker secret rm site.crt site.key site.conf

고급 예제: WordPress 서비스와 secrets 사용하기

이 예제에서는 사용자 지정 루트 비밀번호를 가진 단일 노드 MySQL 서비스를 만들고, 자격 증명을 secrets로 추가한 다음, 이 자격 증명을 사용해 MySQL에 연결하는 단일 노드 WordPress 서비스를 만들어요. 다음 예제는 이 예제를 기반으로 MySQL 비밀번호를 교체하고 WordPress 서비스가 여전히 MySQL에 연결할 수 있도록 서비스를 업데이트하는 방법을 보여줘요.

이 예제는 민감한 자격 증명을 이미지에 저장하거나 명령줄에 직접 전달하지 않기 위해 Docker secrets를 사용하는 몇 가지 기법을 보여줘요.

참고

이 예제는 단순함을 위해 단일 엔진 swarm을 사용하고, 단일 노드 MySQL 서비스를 사용해요. 단일 MySQL 서버 인스턴스는 단순히 replicated 서비스로 확장할 수 없고, MySQL 클러스터를 설정하는 것은 이 예제의 범위를 벗어나기 때문이에요.

또한 MySQL 루트 비밀번호를 변경하는 것은 디스크의 파일을 변경하는 것처럼 간단하지 않아요. MySQL에서 비밀번호를 변경하려면 쿼리나 mysqladmin 명령어를 사용해야 해요.

  1. MySQL용 임의의 영숫자 비밀번호를 생성하고 docker secret create 명령어로 mysql_password라는 이름의 Docker secret으로 저장해요. 비밀번호를 더 짧게 또는 더 길게 만들려면 openssl 명령어의 마지막 인자를 조정해요. 비교적 임의의 비밀번호를 만드는 한 가지 방법일 뿐이며, 원하는 다른 명령어를 사용해도 돼요.

참고

secret을 만든 후에는 업데이트할 수 없어요. 제거하고 다시 만드는 것만 가능하고, 서비스가 사용 중인 secret은 제거할 수 없어요. 하지만 docker service update를 사용하면 실행 중인 서비스의 secret 접근 권한을 부여하거나 해제할 수 있어요. secret을 업데이트해야 한다면 secret 이름에 버전 구성 요소를 추가해서, 나중에 새 버전을 추가하고 서비스가 그것을 사용하도록 업데이트한 다음 이전 버전을 제거하는 방식을 고려해보세요.

마지막 인자는 -로 설정되어 표준 입력에서 입력을 읽는다는 뜻이에요.

$ openssl rand -base64 20 | docker secret create mysql_password -

l1vinzevzhj4goakjap5ya409

반환된 값은 비밀번호가 아니라 secret의 ID예요. 이 튜토리얼의 나머지 부분에서는 ID 출력을 생략할게요.

MySQL root 사용자용 두 번째 secret을 생성해요. 이 secret은 나중에 생성하는 WordPress 서비스와 공유되지 않아요. mysql 서비스를 부트스트랩하는 데만 필요해요.

$ openssl rand -base64 20 | docker secret create mysql_root_password -

docker secret ls로 Docker가 관리하는 secrets 목록을 확인해요.

$ docker secret ls

ID                  NAME                CREATED             UPDATED
l1vinzevzhj4goakjap5ya409   mysql_password      41 seconds ago      41 seconds ago
yvsczlx9votfw3l0nz5rlidig   mysql_root_password 12 seconds ago      12 seconds ago

secrets는 swarm의 암호화된 Raft 로그에 저장돼요.

  1. MySQL과 WordPress 서비스 간 통신에 사용할 사용자 정의 오버레이 네트워크를 만들어요. MySQL 서비스를 외부 호스트나 컨테이너에 노출할 필요는 없어요.
$ docker network create -d overlay mysql_private
  1. MySQL 서비스를 만들어요. MySQL 서비스는 다음과 같은 특징을 가져요.
  • scale이 1로 설정되어 단일 MySQL 태스크만 실행돼요. MySQL 로드 밸런싱은 독자에게 맡길게요. 단순히 서비스 scale을 늘리는 것 이상이 필요해요.
  • mysql_private 네트워크의 다른 컨테이너에서만 접근할 수 있어요.
  • 볼륨 mydata를 사용해 MySQL 데이터를 저장하므로 mysql 서비스가 재시작되어도 데이터가 유지돼요.
  • secrets는 각각 tmpfs 파일시스템의 /run/secrets/mysql_password/run/secrets/mysql_root_password에 마운트돼요. 환경 변수로 노출되지 않고, docker commit 명령어를 실행해도 이미지에 커밋될 수 없어요. mysql_password secret은 권한이 없는 WordPress 컨테이너가 MySQL에 연결할 때 사용하는 거예요.
  • 환경 변수 MYSQL_PASSWORD_FILEMYSQL_ROOT_PASSWORD_FILE/run/secrets/mysql_password/run/secrets/mysql_root_password 파일을 가리키도록 설정해요. mysql 이미지는 시스템 데이터베이스를 처음 초기화할 때 이 파일들에서 비밀번호 문자열을 읽어요. 이후에는 비밀번호가 MySQL 시스템 데이터베이스 자체에 저장돼요.
  • 환경 변수 MYSQL_USERMYSQL_DATABASE를 설정해요. 컨테이너가 시작될 때 wordpress라는 새 데이터베이스가 생성되고, wordpress 사용자는 이 데이터베이스에 대해서만 전체 권한을 가져요. 이 사용자는 데이터베이스를 생성하거나 삭제하거나 MySQL 설정을 변경할 수 없어요.
$ docker service create \
  --name mysql \
  --replicas 1 \
  --network mysql_private \
  --mount type=volume,source=mydata,destination=/var/lib/mysql \
  --secret source=mysql_root_password,target=mysql_root_password \
  --secret source=mysql_password,target=mysql_password \
  -e MYSQL_ROOT_PASSWORD_FILE="/run/secrets/mysql_root_password" \
  -e MYSQL_PASSWORD_FILE="/run/secrets/mysql_password" \
  -e MYSQL_USER="wordpress" \
  -e MYSQL_DATABASE="wordpress" \
  mysql:latest
  1. docker service ls 명령어로 mysql 컨테이너가 실행 중인지 확인해요.
$ docker service ls

ID                  NAME                MODE                REPLICAS            IMAGE
wvnh0siktqr3        mysql               replicated          1/1                 mysql:latest
  1. 이제 MySQL이 준비되었으니, MySQL 서비스에 연결하는 WordPress 서비스를 만들어요. WordPress 서비스는 다음과 같은 특징을 가져요.
  • scale이 1로 설정되어 단일 WordPress 태스크만 실행돼요. WordPress 세션 데이터를 컨테이너 파일시스템에 저장할 때의 제약 때문에 WordPress 로드 밸런싱은 독자에게 맡길게요.
  • WordPress를 호스트 머신의 포트 30000에 노출해서 외부 호스트에서 접근할 수 있게 해요. 호스트 머신의 80 포트에서 웹 서버를 실행 중이 아니라면 80 포트를 노출해도 돼요.
  • mysql_private 네트워크에 연결되어 mysql 컨테이너와 통신하고, 모든 swarm 노드에서 80 포트를 30000 포트로 게시해요.
  • mysql_password secret에 접근할 수 있지만 컨테이너 안에서 다른 target 파일 이름을 지정해요. WordPress 컨테이너는 /run/secrets/wp_db_password 마운트 지점을 사용해요.
  • 환경 변수 WORDPRESS_DB_PASSWORD_FILE을 secret이 마운트된 파일 경로로 설정해요. WordPress 서비스는 그 파일에서 MySQL 비밀번호 문자열을 읽어 wp-config.php 설정 파일에 추가해요.
  • 사용자 이름 wordpress/run/secrets/wp_db_password의 비밀번호를 사용해 MySQL 컨테이너에 연결하고, wordpress 데이터베이스가 아직 없으면 생성해요.
  • 테마와 플러그인 같은 데이터를 wpdata라는 볼륨에 저장해서 서비스가 재시작되어도 파일이 유지돼요.
$ docker service create \
  --name wordpress \
  --replicas 1 \
  --network mysql_private \
  --publish published=30000,target=80 \
  --mount type=volume,source=wpdata,destination=/var/www/html \
  --secret source=mysql_password,target=wp_db_password \
  -e WORDPRESS_DB_USER="wordpress" \
  -e WORDPRESS_DB_PASSWORD_FILE="/run/secrets/wp_db_password" \
  -e WORDPRESS_DB_HOST="mysql:3306" \
  -e WORDPRESS_DB_NAME="wordpress" \
  wordpress:latest
  1. docker service lsdocker service ps 명령어로 서비스가 실행 중인지 확인해요.
$ docker service ls

ID                  NAME                MODE                REPLICAS            IMAGE
wvnh0siktqr3        mysql               replicated          1/1                 mysql:latest
nzt5xzae4n62        wordpress           replicated          1/1                 wordpress:latest
$ docker service ps wordpress

ID                  NAME                IMAGE               NODE                DESIRED STATE       CURRENT STATE            ERROR               PORTS
aukx6hgs9gwc        wordpress.1         wordpress:latest    moby                Running             Running 52 seconds ago

이 시점에서 실제로 WordPress 서비스의 mysql_password secret 접근 권한을 해제할 수 있어요. WordPress가 secret을 자신의 설정 파일 wp-config.php에 복사했기 때문이에요. 하지만 지금은 그렇게 하지 마세요. 나중에 MySQL 비밀번호를 교체할 때 사용할 거예요.

  1. 아무 swarm 노드에서 http://localhost:30000/에 접속하고 웹 기반 마법사로 WordPress를 설정해요. 모든 설정은 MySQL wordpress 데이터베이스에 저장돼요. WordPress는 WordPress 사용자용 비밀번호를 자동으로 생성하는데, 이는 WordPress가 MySQL에 접근할 때 사용하는 비밀번호와 완전히 달라요. 이 비밀번호는 비밀번호 관리자 같은 곳에 안전하게 저장하세요. secret 회전 후 WordPress에 로그인할 때 필요해요.

블로그 글을 한두 개 작성하고 WordPress 플러그인이나 테마를 설치해서 WordPress가 완전히 작동하고 서비스 재시작 후에도 상태가 저장되는지 확인해보세요.

  1. 다음 예제로 진행할 거라면 서비스나 secrets를 정리하지 마세요. 다음 예제는 MySQL 루트 비밀번호를 교체하는 방법을 보여줘요.

예제: secret 회전하기

이 예제는 이전 예제를 기반으로 해요. 이 시나리오에서는 새 MySQL 비밀번호로 새 secret을 만들고, mysqlwordpress 서비스를 업데이트해서 그것을 사용하게 한 다음, 이전 secret을 제거해요.

참고

MySQL 데이터베이스의 비밀번호를 변경하는 것은 단일 환경 변수나 파일을 변경하는 것과 달리 추가 쿼리나 명령어를 실행해야 해요. 이미지는 데이터베이스가 아직 존재하지 않을 때만 MySQL 비밀번호를 설정하고, MySQL은 기본적으로 비밀번호를 MySQL 데이터베이스 안에 저장하기 때문이에요. 비밀번호나 다른 secrets를 교체할 때는 Docker 외부에서 추가 단계가 필요할 수 있어요.

  1. 새 비밀번호를 생성하고 mysql_password_v2라는 이름의 secret으로 저장해요.
$ openssl rand -base64 20 | docker secret create mysql_password_v2 -
  1. MySQL 서비스를 업데이트해서 이전 secret과 새 secret 모두에 접근할 수 있게 해요. secret은 업데이트하거나 이름을 바꿀 수 없지만, 새 target 파일 이름으로 secret 접근 권한을 해제하고 다시 부여할 수 있다는 점을 기억하세요.
$ docker service update \
  --secret-rm mysql_password mysql

$ docker service update \
  --secret-add source=mysql_password,target=old_mysql_password \
  --secret-add source=mysql_password_v2,target=mysql_password \
  mysql

서비스를 업데이트하면 서비스가 재시작돼요. MySQL 서비스가 두 번째로 재시작되면 이전 secret은 /run/secrets/old_mysql_password에, 새 secret은 /run/secrets/mysql_password에 접근할 수 있어요.

MySQL 서비스가 이제 이전 secret과 새 secret 모두에 접근할 수 있지만, WordPress 사용자의 MySQL 비밀번호는 아직 변경되지 않았어요.

참고

이 예제는 MySQL root 비밀번호는 교체하지 않아요.

  1. 이제 mysqladmin CLI를 사용해 wordpress 사용자의 MySQL 비밀번호를 변경해요. 이 명령어는 /run/secrets의 파일에서 이전 및 새 비밀번호를 읽지만, 명령줄에 노출하거나 셸 히스토리에 저장하지 않아요.

WordPress가 MySQL에 연결할 수 없게 되므로 빠르게 수행하고 다음 단계로 넘어가세요.

먼저 mysql 컨테이너 태스크의 ID를 찾아요.

$ docker ps --filter name=mysql -q

c7705cf6176f

아래 명령어에 ID를 대체하거나, 셸 확장을 사용해 한 번에 처리하는 두 번째 변형을 사용해요.

$ docker container exec <CONTAINER_ID> \
  bash -c 'mysqladmin --user=wordpress --password="$(< /run/secrets/old_mysql_password)" password "$(< /run/secrets/mysql_password)"'

또는:

$ docker container exec $(docker ps --filter name=mysql -q) \
  bash -c 'mysqladmin --user=wordpress --password="$(< /run/secrets/old_mysql_password)" password "$(< /run/secrets/mysql_password)"'
  1. wordpress 서비스를 업데이트해서 새 비밀번호를 사용하고 target 경로는 /run/secrets/wp_db_password로 유지해요. 이렇게 하면 WordPress 서비스의 롤링 재시작이 발생하고 새 secret이 사용돼요.
$ docker service update \
  --secret-rm mysql_password \
  --secret-add source=mysql_password_v2,target=wp_db_password \
  wordpress
  1. 아무 swarm 노드에서 다시 http://localhost:30000/에 접속해서 WordPress가 작동하는지 확인해요. 이전 작업에서 WordPress 마법사를 실행할 때 만든 WordPress 사용자 이름과 비밀번호를 사용해요.

작성한 블로그 글이 여전히 존재하는지 확인하고, 설정 값을 변경했다면 여전히 변경되어 있는지 확인해요.

  1. MySQL 서비스에서 이전 secret에 대한 접근 권한을 해제하고 Docker에서 이전 secret을 제거해요.
$ docker service update \
  --secret-rm mysql_password \
  mysql

$ docker secret rm mysql_password
  1. 다음 명령어를 실행해서 WordPress 서비스, MySQL 컨테이너, mydatawpdata 볼륨, Docker secrets를 제거해요.
$ docker service rm wordpress mysql

$ docker volume rm mydata wpdata

$ docker secret rm mysql_password_v2 mysql_root_password

이미지에 Docker Secrets 지원 내장하기

서비스로 배포할 수 있고 자격 증명 같은 민감한 데이터를 환경 변수로 필요로 하는 컨테이너를 개발한다면, 이미지가 Docker secrets를 활용하도록 조정하는 것을 고려해보세요. 한 가지 방법은 컨테이너를 만들 때 이미지에 전달하는 각 파라미터를 파일에서도 읽을 수 있도록 하는 거예요.

Docker library의 많은 Docker 공식 이미지, 예를 들어 위 예제에서 사용한 wordpress 이미지가 이런 방식으로 업데이트되었어요.

WordPress 컨테이너를 시작할 때 환경 변수로 필요한 파라미터를 제공해요. WordPress 이미지는 WORDPRESS_DB_PASSWORD처럼 WordPress에 중요한 데이터가 포함된 환경 변수에 대해 파일에서 값을 읽을 수 있는 변형(WORDPRESS_DB_PASSWORD_FILE)도 갖도록 업데이트되었어요. 이 전략은 이전 버전과의 호환성을 유지하면서, 컨테이너가 직접 전달받는 대신 Docker가 관리하는 secret에서 정보를 읽을 수 있게 해줘요.

참고

Docker secrets는 환경 변수를 직접 설정하지 않아요. 이는 의도적인 결정이에요. 환경 변수는 컨테이너 간에 의도치 않게 누출될 수 있기 때문이에요(예: --link를 사용하는 경우).

Compose에서 secrets 사용하기

services:
  db:
    image: mysql:latest
    volumes:
      - db_data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_root_password
      - db_password

  wordpress:
    depends_on:
      - db
    image: wordpress:latest
    ports:
      - "8000:80"
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

secrets:
  db_password:
    file: db_password.txt
  db_root_password:
    file: db_root_password.txt

volumes:
  db_data:

이 예제는 Compose 파일에서 두 개의 secrets를 사용해 간단한 WordPress 사이트를 만들어요.

최상위 요소 secretsdb_passworddb_root_password 두 개의 secrets를 정의해요.

배포할 때 Docker는 이 두 secrets를 만들고 Compose 파일에 지정된 파일의 내용으로 채워요.

db 서비스는 두 secrets를 모두 사용하고, wordpress는 하나를 사용해요.

배포하면 Docker는 서비스의 /run/secrets/<secret_name> 아래에 파일을 마운트해요. 이 파일들은 디스크에 영구 저장되지 않고 메모리에서 관리돼요.

각 서비스는 환경 변수를 사용해 해당 secret 데이터를 어디에서 찾을지 지정해요.

secrets의 짧은 구문과 긴 구문에 대한 자세한 내용은 Compose Specification에서 확인할 수 있어요.

더 알아보기