[Docker in Action] Running Software in containers (2)

2022. 4. 10. 16:30DevOps/Docker & Kubernetes

 

 

 

 

Eliminating metaconflicts: Building a website farm

이전 포스팅에서 도커가 Process Namespace를 통해 프로세스를 격리시켜 컨테이너 안에서 동작하는 소프트웨어 간의 의도치 않은 충돌(conflicts)을 방지한다는 것을 살펴보았습니다. 하지만 컨테이너 간의 프로세스가 잘 격리되더라도 컨테이너의 이름과 같은 "metadata"끼리 충돌이 발생할 가능성이 여전히 존재하며, 이를 "metaconflicts" 라고 합니다. 

 

 

예를 들어, 이전 포스팅에서 살펴보았던 Web, Mailer, Watcher(Agent) 구조에서 Web과 Watch를 Scale up 하는 경우, 각각의 컨테이너는 동일한 이름을 가질 수 없습니다. 컨테이너를 관리하기 위해서는 특정 컨테이너를 유일한 식별자로 구분할 수 있어야 하기 때문입니다.

 

 

Flexible container identification

위의 상황을 조금 더 구체적으로 살펴보도록 하겠습니다. "webid"라고 이름붙인 컨테이너를 하나 생성한 뒤에, 동일한 이름의 컨테이너를 하나 더 생성하려고 하면 ("docker run" 커맨드를 통해) 다음과 같이 conflict가 납니다. 컨테이너를 생성할 때, 이렇게 명시적으로 semantic 한 이름을 부여하는 것은 가독성을 높이고 이해하기 쉽다는 장점이 있지만, 동일한 컨테이너를 여러 개 생성해야 할 때는 이렇게 conflicts를 생성할 수 있습니다.

 

 

 

"--name" flag를 사용하지 않는다면 docker는 컨테이너를 생성할때 아래와 같이 임의의 이름을 가진 컨테이너를 생성합니다. (--name flag는 이를 오버라이드 하는 옵션입니다.)

 

 

"--rename" flag를 사용해서 다음과 같이 docker container의 이름을 변경할 수도 있습니다. 다만 변경하려는 이름이 이미 존재하는 컨테이너의 이름과 동일한 경우(Exit상태의 컨테이너라도) 마찬가지로 conflicts를 내면서 변경에 실패합니다.

 

 

"name"을 사용하지 않더라도 도커는 컨테이너를 생성할때 hex-encoded 된 1024bit의 식별자를 생성합니다. 이를 Container ID라고 합니다. Container ID는 Container name과는 다르게 해당 컨테이너를 "유일하게" 식별할 수 있는 식별자이기 때문에 이 값을 사용하면 여러개의여러 개의 컨테이너들을 각각 유일하게 식별할 수 있습니다. 이를 환경변수에 저장해서 스크립트화해서 관리하는 방식을 사용하면 여러 개의 컨테이너들을 conflict 없이 잘 관리하는데 도움을 줍니다.

 

 

Container state and dependencies

name대신에 container id를 사용해서 이전 포스팅의 예제인 web, mailer, watcher 구조를 스크립트화 해보면 다음과 같습니다.

MAILER_CID=$(docker run -d dockerinaction/ch2_mailer)
WEB_CID=$(docker create nginx)
AGENT_CID=$(docker create --link $WEB_CID:insideweb \
	--link $MAILER_CID:insidemailer \
    dockerinaction/ch2_agent)

docker create를 사용하면 "RUN" 상태가 아닌 "CREATED" 상태의 컨테이너가 생성됩니다. 따라서 위 스크립트를 실행하면 mailer 컨테이너가 실행되고, web과 agent 컨테이너는 CREATED 상태가 됩니다. ("docker ps -a" 명령어를 사용하면 Running 상태뿐만 아니라 Created, Exited와 같은 모든 상태의 컨테이너를 조회할 수 있습니다.)

 

 

위의 스크립트를 실행하면 다음과 같이 2개의 컨테이너가 "Created" 상태, 1개의 컨테이너가 "Up", 즉 "Running" 상태인 것을 확인할 수 있습니다. "Created" 상태의 컨테이너를 실행하기 위해 "docker start" 명령어를 사용할 수 있는데, 이 경우 "dependency"에 맞는 순서로 컨테이너를 실행시켜야 한다는 점에 주목해야 합니다.

 

 

이를테면, agent 컨테이너를 먼저 실행시키는 경우 다음과 같은 에러가 발생하게 됩니다. 즉 "agent" 컨테이너는 "web" 컨테이너에 의존성을 가지고 있기 때문에 "web" 컨테이너가 "Running" 상태일 때만 실행이 가능하다는 것입니다.  따라서 "docker start $WEB_CID" 명령어를 통해 web 컨테이너를 먼저 실행시켜야 하며, 그 뒤에 agent 컨테이너를 실행시켜야 합니다. 이러한 종속성으로 인해 도커 컨테이너들은 구조적으로 "circular dependency"를 생성할 수 없습니다.

 

 

Building environment-agnostic systems

관리 포인트를 최소화한 "low-maintenance" 시스템을 만들기 위해서는 "global scoped dependencies", "hardcoded deployment architectures", "data locality" 등의 "environment specific"한 사항들을 최소화시켜야 합니다. 도커에서는 이를 지원하기 위해 다음 3가지 기능을 제공하며 이중 Volumes는 이후 포스팅 때 다루려 합니다.

 

  • Read only FileSystems
  • Environment variable injection
  • Volumes

Wordpress를 동작시키는 컨테이너를 생성하여 이를 자세히 알아보려 합니다. 아래에서 살펴볼 Wordpress 컨테이너는 read-only filesystem을 사용하며, 데이터는 오로지 MySQL Database에만 기록(write) 됩니다.

 

Read-only Filesystems

Read-only FileSystem을 사용하면 다음 2가지의 장점을 얻게 됩니다.

  1. 컨테이너가 파일의 변화로부터 영향을 받지 않는다.
    (the container won't be specialized from changes to the files it contains)
  2. 악의적인 Attacker가 컨테이너의 파일을 변화시킬 수 없다.
    (you have increased confidence that an attacker can't compromise files in the container)

 

다음과 같이 "--readonly" flag를 제공하여 컨테이너를 실행시킬 수 있습니다.

docker run -d --name wp --read-only \
	wordpress:latest

 

하지만 실제로 컨테이너를 실행시켜보면 에러가 발생하면서 컨테이너 시작에 실패하는데, 이를 확인하기 위해 로그를 확인해보면 다음과 같은 에러가 발생하고 있음을 확인할 수 있습니다.

 

 

에러 로그를 살펴보면 "create lock file"에 실패했음을 확인할 수 있습니다. 즉, Apache Web Server가 동작하기 위해서 lock file을 생성해야 하는데 readonly로 컨테이너를 생성하였으므로 생성에 실패하였고, 따라서 서버가 정상적으로 동작하지 못하고 종료된 것입니다. 따라서 해당 파일을 생성할 수 있도록 특정 디렉토리에 예외(exception)을 적용해주어야 합니다.

 

 

다음과 같이 readonly 옵션을 주지 않은 컨테이너를 생성한 후에, 정상적으로 컨테이너가 동작한 후, 어떤 디렉토리에 변화가 생겼는지(디렉토리의 변화는 성공적으로 파일을 생성했음을 의미합니다.)를 "docker diff" 커맨드로 확인해보겠습니다.

 

 

 

"/run/apache2/apache2.pid" 파일을 생성한 것을 알 수 있으므로 다음과 같이 해당 디렉토리에 Writable Volume을 마운트 하면, 해당 디렉토리만 read-only의 예외 케이스로 만드는 것이 가능합니다. 아래와 같이 정상적으로 실행되는 것을 확인할 수 있습니다.

docker run -d --name wp2 \
	--read-only \
    -v /run/apache2/ \
    --tmpfs /tmp \
    wordpress:latest

 

 

 

Environment variable injection

Environment Variable(이하 환경변수)은 key - value pair입니다. 도커는 이 환경변수를 사용해서 컨테이너에 정보를 제공합니다.
'--env' 명령어를 사용하면 환경변수를 새로운 컨테이너에 주입(inject)할 수 있으며 다음과 같이 사용합니다. 만약 주입된 환경변수가 Dockerfile에 의해 이미 선언된 적이 있는 변수라면, 이를 오버라이드 합니다.

docker run --env MY_ENVIRONMENT_VAR="this is a test" \
	busybox:latest

 

 

이를 사용해서 다음과 같이 Wordpress 컨테이너에 제공해야 하는 환경변수들을 주입할 수 있습니다. 

 

Building Durable Containers

도커 컨테이너는 다음과 같이 항상 6개의 상태 중 하나의 상태를 갖습니다. Container Failure에 대한 기본적인 전략은 "automatically restarting a process"이지만, 이를 제어할 수 있는 몇 가지 옵션들을 제공합니다.

 

docker의 컨테이너 상태

 

Automatically restarting containers

도커는 다음 4개의 restart policy를 제공하며, "--restart" flag를 사용하여 이를 명시할 수 있습니다.

  • Never restart (default)
  • Attempt to restart whan a failure is detected
  • Attempt for some predetermined time to restart when failure is detected
  • Always restart the container regardless of condition

 

도커는 컨테이너를 재시작할 때 기본적으로 "exponential backoff strategy"를 사용합니다. 즉 실패할 때마다 바로 컨테이너를 재시작하는 것이 아니라 이전에 기다린 시간의 2배씩 더 텀을 두어 기다리는 방식을 사용하는 것입니다. 예를 들어 첫 번째 재시작할 때까지 2초를 기다렸다면 다음 재시작까지는 4초를 기다리고, 그다음 재시작까지는 8초를 기다리는 식입니다. 실제로 "date" 명령어를 통해 컨테이너가 재시작된 시간을 확인해보면 점차 시간이 늘어나고 있는 것을 확인할 수 있습니다.

 

 

 

Using PID 1 and init systems

An init system is a program that’s used to launch and maintain the state of other programs. Any process with PID 1 is treated like an init process by the Linux kernel (even if it is not technically an init system)

 

다음의 명령어를 통해 도커가 프로세스를 관리하는 방법에 대해 살펴보겠습니다. lamp란 (Linux, Apache, MySQL, PHP)를 모두 제공하는 도커 이미지를 의미합니다. 해당 컨테이너를 시작한 후, "docker top" 명령어를 통해 해당 컨테이너 안에서 동작하는 running process를 살펴보도록 하겠습니다.

docker run -d -p 80:80 --name lamp-test tutum/lamp

 

 

해당 컨테이너가 apache, mysql 등의 프로세스를 실행하고 있는 것을 확인할 수 있습니다. 하지만 이는 도커 컨테이너의 process namespace에 대한 정보를 주지 않고 있기 때문에 실제로 아래 정보를 사용해서 도커 컨테이너 안에서 동작하고 있는 apache process를 kill 하고, 해당 프로세스를 process namespace의 PID=1인 systemd 프로세스가 재시작하는 것을 살펴볼 수는 없습니다.

 

 

따라서 실제로 도커 컨테이너 안으로 들어가서 해당 컨테이너의 process list를 확인해 보아야 합니다. 아래와 같이 "docker exec lamp-test ps" 명령어를 사용하면 "lamp-test" 컨테이너의 process namespace 안에서 동작하는 여러 프로세스들을 확인할 수 있습니다. 이 컨테이너의 process namespace 안에서 apache2 프로세스는 PID=435를 할당받아 동작하고 있습니다. 따라서 이를 "docker exec kill 435" 명령어를 통해 지운 뒤에, "docker log" 커맨드를 통해 로그를 확인해보겠습니다.

 

 

 

아래와 같이 apache2 프로세스가 제거되었고, 이를 systemd 프로세스가 다시 살려(spawn) 낸 것을 확인할 수 있습니다. 이렇게 해당 컨테이너의 process namespace에서 PID=1인 init system은 전체적인 프로세스를 관리하는 역할을 합니다. 이는 추후 entrypoint 명령을 통해 오버라이드 할 수 있게 됩니다. (entrypoint 명령어에 대해서는 이후 포스팅에서 자세하게 다룰 예정입니다.)

 

 

반응형