컨테이너, 이미지에 담을 것과 밖에 둘 것
컨테이너는 프로그램과 실행에 필요한 파일을 이미지로 묶어 어느 서버에서나 같은 조합으로 실행한다.
이미지와 컨테이너의 차이, 가상 머신과 다른 점, 설정과 데이터를 이미지 밖에 두는 이유를 기초부터 설명한다.
이 글의 목차”내 컴퓨터에서는 되는데”가 생기는 이유7
배포 안내를 따라가다 보면 “Docker 이미지를 빌드해서 컨테이너로 실행하세요” 같은 문장을 자주 만난다.
AWS에서 컨테이너를 실행해 주는 ECS도 컨테이너 이미지를 받아서 서비스를 띄운다.
그런데 이미지와 컨테이너는 무엇이 다르고, 왜 굳이 이렇게 묶어서 배포할까.
컨테이너가 풀려는 문제부터 정리한다.
”내 컴퓨터에서는 되는데”가 생기는 이유
프로그램은 혼자 돌지 않는다.
언어 런타임, 라이브러리, 운영체제 패키지에 기대서 돌아간다.
런타임은 Node.js나 Python처럼 코드를 실행해 주는 환경이고, 라이브러리는 가져다 쓰는 다른 사람의 코드 묶음이다.
같은 코드라도 서버마다 이것들의 버전이 조금씩 다르면 결과가 달라질 수 있다.
개발자 노트북에서 테스트를 다 통과한 코드가 운영 서버에서 깨지는 흔한 이유다.
서버마다 런타임과 라이브러리를 따로 설치하면 설치할 때마다 조합이 조금씩 어긋나기 쉽다.
컨테이너는 이 조합을 한 번 묶어 두고, 묶은 그대로 옮겨서 실행하는 방식이다.
이미지와 컨테이너의 차이
이렇게 묶어 둔 것을 이미지라고 한다.
이미지에는 컨테이너 실행에 필요한 파일, 바이너리, 라이브러리, 설정이 모두 들어 있다.
바이너리는 컴퓨터가 바로 실행할 수 있는 형태의 프로그램 파일이다.
이미지는 한 번 만들면 고칠 수 없다.
내용을 바꾸려면 새 이미지를 만든다.
이미지 안은 여러 층(layer)으로 쌓여 있고, 층마다 파일을 더하거나 고친 내용이 담긴다.
컨테이너는 이 이미지를 실제로 실행한 것이다.
이미지 하나로 컨테이너를 여러 개 띄울 수 있고, 한 컨테이너가 실행 중에 파일을 만들거나 고쳐도 다른 컨테이너에는 영향이 없다.
컨테이너를 띄우면 이미지의 층 위에 그 컨테이너만 쓰는 쓰기 층이 하나 얹힌다.
실행 중에 만든 파일은 이 층에 쓰이고, 이미지에 있던 파일을 고치면 그 파일을 이 층으로 복사한 뒤 고친다.
아래의 이미지 층은 그대로 남는다.
컨테이너는 작은 가상 머신일까
컨테이너를 작은 가상 머신쯤으로 생각하기 쉬운데, 둘은 구조가 다르다.
가상 머신은 물리 서버 위에서 컴퓨터를 통째로 흉내 내고, 그 위에 운영체제를 따로 하나씩 올린다.
EC2 인스턴스도 이렇게 만든 가상 서버다.
그래서 가상 머신 하나하나가 자체 커널과 드라이버까지 갖춘 운영체제 전체다.
컨테이너는 실행에 필요한 파일을 가진 격리된 프로세스이고, 여러 컨테이너가 같은 커널을 함께 쓴다.
커널은 운영체제의 중심에서 메모리, 프로세스, 파일, 네트워크를 관리하는 부분이다.
프로세스는 실행 중인 프로그램 하나를 말한다.
커널을 함께 쓰면서도 서로 떨어져 있을 수 있는 것은 리눅스의 두 기능 덕분이다.
이름공간(namespace)은 프로세스마다 보이는 파일, 프로세스 목록, 네트워크 같은 자원을 따로 떼어 보여 준다.
리눅스 2.6.24에 처음 들어온 cgroups는 프로세스 묶음이 쓰는 CPU와 메모리 같은 자원을 제한하고 기록한다.
제한은 정해 줘야 걸린다.
Docker도 기본값으로는 컨테이너에 자원 제한을 두지 않는다.
제한을 정하지 않은 컨테이너 하나가 메모리를 다 쓰면, 커널이 메모리를 확보하려고 같은 서버의 다른 프로세스까지 종료할 수 있다.
커널을 함께 쓰니 운영체제를 통째로 띄우는 가상 머신보다 하나에 드는 자원이 적고, 같은 서버에 더 많이 올라간다.
대신 커널에 문제가 생기면 그 위의 컨테이너가 모두 영향을 받는다.
직접 만든 프로그램들을 나눠 실행하기에는 이 정도 격리로 충분하다.
하지만 믿을 수 없는 남의 코드를 나란히 돌리는 경계로 쓰기에는 약한 편이다.
격리 기능은 Docker보다 먼저 있었다
프로세스가 볼 수 있는 범위를 좁히는 기능은 유닉스에 오래전부터 있었다.
프로세스가 보는 파일 시스템의 시작점을 바꾸는 chroot는 Version 7 AT&T UNIX에 처음 들어왔다.
하지만 파일을 찾는 출발점만 바꿀 뿐, 다른 프로세스나 네트워크까지 떼어 놓지는 못했다.
리눅스의 이름공간과 cgroups가 그 범위를 프로세스 목록, 네트워크, 자원 사용량까지 넓혔다.
그 위에 쓰기 쉬운 도구를 얹은 것이 Docker다.
2013년에 처음 공개된 Docker가 풀려던 문제는 코드를 서버로 내보내는 일이 어렵다는 것이었다.
Docker는 격리 기능에 더해, 프로그램과 필요한 파일을 이미지 한 단위로 묶어 어느 서버로든 그대로 옮기는 방식을 내놓았다.
이미지에 넣을 것과 밖에 둘 것
컨테이너는 언제든 지우고 같은 이미지로 새로 띄울 수 있다.
그래서 무엇을 이미지에 넣을지는 두 가지를 보고 정한다.
환경마다 달라지는가, 그리고 컨테이너와 함께 사라져도 되는가.
| 무엇 | 어디에 두나 | 잘못 두면 |
|---|---|---|
| 프로그램과 라이브러리 | 이미지 안 | 밖에 두면 서버마다 조합이 달라진다 |
| 환경별 설정과 비밀 값 | 실행할 때 넘김 | 환경마다 이미지를 다시 빌드하고, 이미지를 받은 사람이 비밀 값을 꺼내 볼 수 있다 |
| 데이터와 업로드 파일 | 이미지 밖 저장소 | 컨테이너를 지울 때 함께 사라진다 |
설정은 데이터베이스 주소나 로그를 얼마나 자세히 남길지처럼 개발, 테스트, 운영 환경마다 달라지는 값이다.
클라우드 앱의 설계 원칙을 정리한 Twelve-Factor App은 설정을 코드와 엄격히 나눠 환경 변수에 두라고 권한다.
환경 변수는 프로그램을 실행할 때 운영체제가 넘겨주는 이름과 값의 쌍이다.
같은 이미지를 환경 변수만 바꿔 실행하면, 테스트에서 검증한 이미지를 그대로 운영에 올릴 수 있다.
설정을 이미지에 구워 넣으면 운영용 이미지를 따로 빌드해야 한다.
그러면 테스트를 통과한 이미지와 운영에 올라가는 이미지가 서로 다른 물건이 된다.
비밀번호나 API 키는 더 조심해야 한다.
이미지는 레지스트리라는 저장소에 올려 두고 여러 서버와 사람이 받아 가는 파일이다.
Docker도 이미지를 빌드할 때 빌드 인자나 환경 변수로 넘긴 비밀 값은 최종 이미지에 남는다고 경고한다.
컨테이너를 지우면 파일은 어떻게 되나
컨테이너가 실행 중에 만들거나 고친 파일은 그 컨테이너의 쓰기 층에 있어서, 컨테이너를 삭제할 때 함께 지워진다.
멈췄다가 다시 시작할 때는 남아 있지만, 지우면 사라진다.
운영에서는 컨테이너를 지우는 일이 아주 흔하다.
새 버전을 배포할 때마다 옛 컨테이너를 내리고 새 이미지로 컨테이너를 다시 띄우기 때문이다.
그래서 남아야 하는 파일은 컨테이너 밖에 둔다.
볼륨(volume)은 컨테이너와 따로 관리되는 저장 공간이다.
컨테이너를 지우고 새로 만들어도 같은 볼륨을 연결하면, 거기에 쓴 파일이 그대로 읽힌다.
실행 중에 저장한 사진 한 장을 두 위치에서 비교한다.
컨테이너 안
교체하면 삭제
별도 볼륨
교체해도 유지
컨테이너를 지우면 안에 새로 쓴 파일도 사라진다. 같은 볼륨에 연결한 새 컨테이너는 보관된 파일을 다시 읽는다.
두 곳에 사진.jpg를 저장했다. 컨테이너를 교체하기 전이다.
실제 파일을 다루지 않는 예시다. 볼륨은 함께 삭제하지 않는다.
ECS에서는 컨테이너가 수시로 바뀐다
AWS에서 컨테이너를 운영하면 이 성질이 더 분명해진다.
ECS 서비스는 정해 둔 개수만큼 태스크를 띄워 두고, 하나가 실패하거나 멈추면 새 태스크를 띄워 그 자리를 채운다.
태스크는 ECS가 함께 실행하는 컨테이너 묶음이다.
새로 뜬 태스크에는 이전 컨테이너 안에 쓴 파일이 없다.
그래서 데이터베이스는 AWS가 운영을 맡는 RDS에, 사용자가 올린 파일은 파일 저장 서비스인 S3에 두는 경우가 많다.
비밀 값도 이미지 밖에 둔다.
AWS의 ECS 비밀 값 전달 문서는 API 키나 데이터베이스 자격 증명을 Secrets Manager나 Parameter Store에 보관해 두고, 컨테이너가 실행될 때 넘겨주라고 권한다.
ECS에서 어떤 이미지를 어떤 설정으로 실행할지 적는 문서를 태스크 정의라고 한다.
여기에 비밀 값 대신 그 값이 보관된 위치를 적어 두면, 컨테이너가 시작될 때 값이 환경 변수로 들어간다.
이미지에도 태스크 정의에도 비밀번호가 남지 않는다.