Octoping의 블로그
article thumbnail

들어가기 앞서

해당 글은 다음 글을 번역한 글입니다. 도커를 사용할 때 피해야 할 안 좋은 습관들에 대해 소개합니다. 어느 정도의 의역이 있을 수 있습니다.

https://dwdraju.medium.com/how-it-works-in-my-machine-turns-it-works-in-my-container-1b9a340ca43d

 

How “It works in my machine” turns “It works in my container”?

You had things running with occasional glitches and there was always an excuse “It works in my machine” as there is rarely identical…

dwdraju.medium.com

 

 


"내 컴퓨터에선 작동합니다"가 어떻게 "내 컨테이너에서는 작동합니다"로 바뀌었을까요?

머신(작동 환경)과 운영체제가 모두 동일한 경우는 굉장히 드뭅니다. 따라서 가끔 결함이 발생할 때에는 "내 컴퓨터에선 작동한다구요"라는 변명이 늘 따라왔습니다. 그러므로 코드가 중단될 때에 똑같은 변명을 더는 하지 않으려면 해결책이 필요합니다. 그리고 누군가가 모든 종속성이 패키지화되어 있고, 어떤 머신에서도 작동하며, 개발 환경과 프로덕션 환경의 패리티를 유지하고, 충돌이 전혀 없는 "컨테이너"라는 유망한 솔루션에 대해 이야기했습니다.

 

그리고 당신은 컨테이너 이미지 빌드를 시작하고, Dockerfile을 작성하고, 포트를 매핑하고, 패키지 설치 명령어, 이미지 크기 최적화, 모범 사례 등의 개념을 익혔습니다.

 

점차 다른 개발자들도 컨테이너를 사용하기 시작했고, 언젠가부터 당신은 프로덕션 환경에서 docker를 사용하기 시작했습니다. 아주 최고죠!!

 

하지만 몇 달 후..

사람들은 컨테이너 문제를 해결하는 데 매일 몇 시간씩 소비하기 시작합니다. 그리고는 이런 이야기가 나오기 시작하죠. "내 컨테이너에서 작동한다구요"

 

 

그래서, 이게 무슨 일이죠?

컨테이너 기술을 도입하기에는 시기상조였던걸까요? 도커를 도입하기 전에 전문가가 필요했던걸까요? 애플리케이션이나 컨테이너의 결함으로 인한 문제였을까요? 물론 아닙니다.

 

"내 컨테이너에서는 작동합니다"라는 상황이 발생하는 이유에 대해 자세히 알아봅시다.

 

latest 이미지 태그를 사용한다

항상 명심해야 할 가장 중요한 사항입니다. 우리는 도커를 학습할 때 모든 이미지에 latest 태그를 사용했습니다. 하지만 이는 자기 발등에 도끼를 꽂는 것과 같습니다.

From node:latest

 

docker를 사용하기 시작할 당시에는 latest 태그가 NodeJS 버전 10을 의미했지만, 한 달 후 누군가 노트북을 포맷하거나 새로운 사람이 합류하는 시점에 latest 태그는 이제 버전 12를 가리키게 되었습니다. 하지만 애플리케이션은 이전 버전에 가장 적합합니다. 이게 바로 모든 사람들이 동일한 Dockerfile을 사용하고 있지만 그럼에도 "내 컨테이너에서는 작동한다구요"라고 말하게 되는 이유입니다.

 

그러므로 항상 버전이 적힌 태그를 사용해야 합니다. ubuntu:latestnode:alpine같은 태그 대신 ubuntu:16.04, node:12-alpine을 사용하세요.

 

컨테이너 엔진과 다른 환경의 버전

Docker는 하위 호환성을 염두에 두고 릴리스를 진행하려고 노력하며, 기능을 제거하기 전에는 릴리스 3번 전에 공지합니다. 엔진을 오래 업그레이드하지 않은 경우가 이 이유일 수도 있습니다.

 

만약 Docker Compose를 사용하는 경우, yml 파일의 변경 및 버전 관리가 매우 중요합니다. docker-compose 파일을 버전 관리할 때에는 항상 마이너 릴리스를 지정하는 것이 좋습니다. 예를 들어, version: "3"이 아니라 version: "3.7"과 같이 사용하는 것이 좋습니다. 기본적으로 전자는 version: "3.0"을 의미하며, 업그레이드된 버전에서만 지원되는 docker-compose의 각 릴리스에는 많은 업데이트가 있기 때문입니다. 이는 핑을 피하는 방법입니다. 버전 작성 및 호환성 매트릭스 가이드를 참고하세요.

 

변수 다루기

일반적으로 애플리케이션의 변수와 시크릿은 config.json이나 .env와 같은 설정 파일을 통해 읽어옵니다. 하지만 docker에서는 런타임 및 빌드 타임 환경 변수를 전달하는 데에 여러 가지 방법이 있습니다. 다음은 그 예시들입니다.

 

docker run -it -e KEY=VALUE --name nginx nginx:1.15-alpine /bin/sh -c "env | grep KEY"
web:
  environment:
    - KEY=VALUE
web:
  env_file:
    - web-variables.env
web:
  image: "nginx:${NGINX_VERSION}"

 

파일과 환경 변수에서 직접 읽기의 가장 큰 차이점은 파일의 경우 볼륨을 공유하면 파일의 변경 사항이 도커에 그대로 반영되지만, 도커 환경 변수를 사용하는 경우 도커를 재시작해야 한다는 점입니다.

 

또한 ARG라는 변수는 빌드 시에만 사용할 수 있으므로 런타임에는 변수를 사용할 수 없지만, ENV는 빌드와 런타임 모두에서 사용할 수 있습니다.

 

이미지 빌드 프로세스

공식 도커 이미지만으로는 항상 충분하지 않습니다. 모든 사람의 시스템에서 각 단계를 수행하려면 오랜 시간과 리소스가 소요되는 커스터마이징과 추가 패키지가 필요합니다. 따라서 기본 이미지를 가져와 커스터마이징을 통해 자체 이미지를 구축합니다. 여기서 다음과 같이 수동으로 패키지를 추가하고 커밋하는 것은 항상 피해야 합니다.

$ docker run -it --name alpine alpine:3.8 /bin/sh
/ # apk add busybox-extras
[CTRL+p CTRL+q]
$ docker commit alpine alpine-custom
$ docker push alpine-custom

 

여기서 상태 추적이 손실되었습니다. 지금 설치한 busybox-extras의 버전은 나중에는 사용할 수 없게 되어버릴 수도 있습니다. 따라서 항상 패키지를 버전관리 하면서 다음처럼 Dockerfile을 함께 사용하세요.

FROM alpine:3.8
RUN apk add busybox-extras-1.28.4-r3

 

파일과 폴더 권한

다음 예시를 살펴봅시다.

# docker-compose.yml
version: "3"
services:
  myapp:
    image: node:11-alpine
    container_name: "myapp"
    volumes:
      - ./:/app
    entrypoint: /bin/sh
    command: -c "sleep 5 && cd /app && yarn && yarn start"

docker-compose up -d를 실행한 후, 파일과 폴더들의 권한을 확인해봅시다.

 

공유 도커 볼륨의 파일 및 폴더 권한

이 폴더와 파일은 docker 내부에 생성되었기 때문에 node_modulesyarn.lock은 root 사용자가 소유하고 있음을 알 수 있습니다. 마찬가지로 root가 소유하는 파일의 업로드가 있는 경우(동일한 문제는 Linux 시스템에서만 발생하고 MacOS에서는 발생하지 않습니다). 호스트 시스템에서 파일을 편집하거나 추가해야 할 때 문제가 발생하고 git이 변경 사항을 감지합니다. 우리는 매번 권한을 변경해줄 여유가 없습니다. 모든 파일 및 폴더의 소유자를 현재 사용자로 가져오는 방법이 있습니다.

# Updated docker-compose.yml
version: "3"
services:
  myapp:
    image: node:11-alpine
    container_name: "myapp"
    volumes:
      - ./:/app
    entrypoint: /bin/sh
    command: -c "sleep 5 && cd /app && yarn && yarn start"
    user: ${CURRENT_UID}

 

현재 사용자와 그룹 id를 변수로 export 합니다.

export CURRENT_UID=$(id -u):$(id -g)

 

그리고 컨테이너를 시작합니다.

CURRENT_UID=$CURRENT_UID docker-compose up -d

이제 현재 호스트 사용자가 소유한 모든 파일과 폴더를 볼 수 있습니다.

 

호스트와 컨테이너 볼륨 간 공유

docker는 '한 번 실행하면 어디서나 실행할 수 있다'는 개념으로 도입되었지만, 볼륨을 사용할 때에 MacOS의 경우에는 차이가 있습니다. Mac용 Docker Desktop 에디션에서는 공유 파일 시스템 솔루션으로 osxfs를 사용합니다. 컨테이너 내부에 호스트 경로를 마운트하는 동안 추가 단계를 수행해야 합니다. 예를 들어보겠습니다.

# docker-compose.yml
version: "3.2"
services:
  myapp:
    container_name: myapp
    image: node:11-alpine
    volumes:
      - ./:/app

 

만약 docker-compose up을 하면, 다음과 같은 에러를 띄웁니다.

ERROR: for myapp  Cannot start service myapp: b'Mounts denied: \r\nThe path /private/tmp/docker/myapp\r\nis not shared from OS X and is not known to Docker.\r\nYou can configure shared paths from Docker -> Preferences... -> File Sharing.\r\nSee https://docs.docker.com/docker-for-mac/osxfs/#namespaces for more info.\r\n.'

 

하지만 같은 경우 리눅스 환경에서는 정상적으로 실행됩니다. Mac의 경우, Docker 환경설정 메뉴 -> 환경설정 -> 파일 공유에서 경로를 추가해야 합니다.

Add bind mount on macOS

또한 하드 코딩된 볼륨 경로가 있는 경우 다른 사용자에게 문제를 일으키는 것이 일반적입니다.

 


추가적으로 다음과 같은 다른 문제가 발생할 수 있습니다.

 

  • 호스트 머신에서 실행 중인 프로세스가 있고 도커가 동일한 포트를 사용하는 경우 포트 충돌
  • 가상 호스트 URL을 기반으로 다른 컨테이너로 트래픽을 전송하는 데 사용되는 프록시 또는 로드밸런서. 업로드 제한, 프록시로 전송되는 추가 또는 수정된 헤더가 있을 수 있습니다.
  • 이중 패키지 사용: 호스트가 설치되어 있고 버전이 다를 수 있는 도커 명령어 내부에서 실행되는 경우.

 

컨테이너에서 애플리케이션을 실행하는 동안 "내 컨테이너에서는 작동합니다"라는 문제가 발생하면 언제든지 댓글로 공유해 주세요.

profile

Octoping의 블로그

@Octoping

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!