📕 목차
1. Docker Compose file 구조
2. 여러 Container로 구성된 Application 실행
3. Docker Container 간의 통신
4. Application 설정값 지정
5. 한계
6. 연습 문제
1. Docker Compose file 구조
📌 As-is
- Dockerfile로 인해 배포가 용이해지긴 했지만, Application의 한 부분을 패키징하는 수단에 불과하다.
- 가장 흔히 사용되는 3-Tier Architecture of Web Application만 해도 최소 3개의 Container가 필요하다.
- Presentation Tier : 일반 사용자가 직접 access 할 수 있는 layer
- Logic Tier : business logic layer
- Data Tier : data storage layer
- 참고로 Tier는 Component의 물리적 분리, Layer는 논리적인 분리다.
- 직접 순서대로 각각의 Container들을 명령어로 옵션을 조절해가며 실행하는 것은 비효율적이다.
📌 To-be
- Docker compose script : Application의 원하는 상태, 즉 모든 Component가 실행 중일 때 어떤 상태여야 하는가?
- docker container run 명령으로 Container 실행할 때 지정하는 모든 옵션을 모아놓은 파일
- Docker compose script 작성
- Docker compose 도구로 Application 실행 (docker-compose up)
- Docker compose가 작성된 script를 기반으로 필요한 Docker Object를 만들도록 Docker API에 명령
- Docker compose를 구성하면 하나의 3-Tier Application도 하나의 Network에서 쉽게 관리할 수 있다.
📌 Docker Compose Script
version: '3.7'
services:
todo-web:
image: diamol/ch06-todo-list
ports:
- "8020:80"
networks:
- app-net
networks:
app-net:
external:
name: nat
사실상 다음 명령어와 동일하다.
docker container run -p 8020:80 --name todo-web --network nat diamol/ch06-todo-list
- Version: 해당 파일에 사용된 Docker compose file 형식의 버전
- Services : Application을 구성하는 모든 Component을 열거한다. (Docker compose에서 단위)
- service_name
- Service의 이름이자 Container의 이름
- Docker Network 상에서 다른 Container들이 해당 Container를 식별하기 위한 DNS 이름
- Kubernetes의 tag같은 느낌인 것 같다.
- image
- 해당 Service에서 사용할 Docker Image 이름
- Docker Hub, Private Registry, 혹은 로컬에 저장되어 있을 수도 있다.
- image를 사용하면 Dockerfile을 사용하여 빌드할 필요가 없다.
- container_name
- service_name과 같이 사용할 수 있다.
- 정의하지 않으면 Docker Compose가 자동으로 생성한다.
- ports
- Container가 사용할 port와 Host에서 사용할 port
- ex. "8000:80"
- environment : Service에서 사용할 환경 변수
- volumes : Container와 Host 간 data 공유를 위한 volume 정의
- depends_on : 다른 Service에 의존하는 Service 정의. (해당 Service를 먼저 실행하려 시도한다.)
- build : Dockerfile을 사용하여 Image를 build한다.
- service_name
- Networks : Service Container가 연결될 모든 Docker Network를 열거하는 부분. (동일한 Network에 연결된 다른 Container와 소통할 수 있다.)
- driver
- Docker Engine에서 제공하는 Network Driver 지정
- Container를 연결하기 위한 다양한 Network type을 지원한다.
- driver_opts : Network Driver에 대한 옵션 정의
- ipam
- IP 주소 관리 방법 정의
- IPAM(IP Address Management)은 IP 주소 할당과 관리를 자동화하는 도구
- attachable : 다른 Service에서 해당 Network 접근 가능 여부 지정
- external : 이미 존재하는 Network를 참조하는 경우, 새롭게 생성하지 않는다.
- driver
- Volumes : Application에서 사용하는 Data volume
- driver
- Volume Driver 지정
- Writeable-Layer가 아니라 Docker volume이나 mount 기능 적용 가능
- name : Docker Volume 이름 지정. (정의 안 하면 랜덤 지정)
- external : 이미 존재하는 volume을 참조하는 경우, 새롭게 생성하지 않는다.
- driver
- Environments : Service에서 사용할 환경 변수
- Configuration : Service에서 사용할 환경 설정 파일
- Deployments : Service 배포를 위한 설정 파일
- replicas : Service instance 개수 지정
- placement : Service instance를 배치할 Node를 선택하는 규칙 지정
- update_config : Service update를 제어하는 옵션 지정
- resources : Service instance가 사용할 CPU 및 Memory 등의 resource 지정
- networks : Service가 연결할 Network 지정
✒️ Docker Network
Service를 "app-net"이라는 Docker Network에 연결했는데, 이걸 또 "nat"이라는 Docker Network에 연결했다는 말이 이해가 안 가서 찾아보니 둘은 조금 다른 개념이었다.
"app-net"이 여러 Container들을 묶어주는 Container Network 같은 개념이라면, "nat"은 외부 Network에 연결된 Docker Network다.
📌 실행 명령 : up
docker network create nat
docker-compose up
- Compose Script의 external 필드에 정의된 Network는 Application 실행 전에 생성되어 있어야 한다.
- Container의 log에 표준 출력을 연결하여 Application의 log를 보여주고 있다.
📌 장점
- Application의 소스 코드, Dockerfile과 함께 형상 관리 도구로 관리된다.
- 하나의 파일에 Application의 모든 실행 옵션이 기술되므로 README 파일에 Image 이름, 공개해야 할 port 번호를 문서화할 필요가 전혀 없다.
- Docker-compose Script만으로 간접적으로 문서화하는 효과를 얻을 수 있다.
2. 여러 Container로 구성된 Application 실행
📌 Multi-stage build → Docker compose
version: '3.7'
services:
accesslog:
image: diamol/ch04-access-log
networks:
- app-net
iotd:
image: diamol/ch04-image-of-the-day
ports:
- "80"
networks:
- app-net
image-gallery:
image: diamol/ch04-image-gallery
ports:
- "8010:80"
depends_on:
- accesslog
- iotd
networks:
- app-net
networks:
app-net:
external:
name: nat
- accesslog Service는 Image 이름만 명시했다.
- iotd Service는 REST API고, Image 이름과 80번 포트를 Host PC의 무작위 port를 통해 공개했다.
- image-gallery Service는 Image 이름, port 번호 뿐 아니라 depends_on 항목을 추가했다.
📌 Docker compose로 실행해보기
docker-compose up -d
- Docker compose는 의존 관계를 준수하여 세 개의 Container를 생성한다.
- image-gallery Service는 accesslog와 iotd Service가 실행된 다음에 실행된다.
📌 Scale Out
docker-compose up -d --scale iotd=3
docker-compose logs --tail=1 iotd
- --scale : Scale 지정
- --tail = 1 : 각 iotd Container의 마지막 로그 출력
- API Service는 State가 없으므로 Container를 늘리는 방법으로 Scale Out이 가능하다.
- Web Container가 API에 데이터를 요청하면 Docker가 여러 개의 API Container에 요청을 고르게 분배한다.
- 실제 웹 사이트를 들락날락 거려보면 Container가 각각 마지막 로그가 바뀌는 것을 확인할 수 있다.
✒️ Scale Up & In & Out
• Scale Up : 서버 자원 부족으로 스펙을 상승시키는 것. AWS에선 더 좋은 인스턴스 타입 교체를 의미
• Scale In : 작업이 완료되어 더 이상 필요없는 Scale Out으로 늘렸던 컴퓨팅 수를 줄이는 것
• Scale Out : 서버 자원 스펙 상승으로 한계가 있고 효율이 떨어지는 시점에 컴퓨팅 수를 늘리는 것
사진 출처: https://velog.io/@msung99/Docker-Docker-Swarm-%EB%8F%84%EC%BB%A4-Cluster-%ED%99%98%EA%B2%BD%EC%9D%84-%EA%B5%AC%EC%B6%95%ED%95%98%EB%8A%94-%EB%82%B4%EB%B6%80-%EB%A9%94%EC%BB%A4%EB%8B%88%EC%A6%98%EC%97%90-%EB%8C%80%ED%95%B4
📌 Container 관리
docker-compose stop
docker-compose start
docker container ls
- Docker compose가 Container들을 대신 관리해준다.
- 하지만 Docker compose로 여전히 직접 전체 Application을 관리할 수도 있다.
- 컴퓨팅 자원 절약을 위해 모든 Container 중지
- 혹은 다시 Container를 실행하여 Application 재가동 등
- 일반적으로 Docker 명령행을 쓰지만, Container를 관리하는 Docker compose 또한 Docker API를 이용한다.
- Docker engine 자체는 Container를 실행할 뿐이다. (여러 Container가 하나의 Application으로 동작하는지 여부는 모른다.)
- Docker compose만이 YAML 문서를 기반으로 Application 구조를 이해하고 있다.
- 따라서 Docker compose로 실행한 Container도 Docker 명령행으로 관리할 수 있다.
📌 Scale In
docker-compose down # 애플리케이션과 모든 컨테이너 제거
docker-compose up -d
docker container ls
- external 자원은 Compose 관리 대상이 아니므로 삭제되지 않는다. (volume, network, ...)
- Scale out에 대한 정의가 없으므로 재실행하면 Container 수가 하나로 돌아간다. (Scale down)
💡 Docker compose는 YAML 파일의 Application 정의에 의존하는 클라이언트 측 도구임을 명심하라.
3. Docker Container 간의 통신
📌 DNS
- Docker Container는 자체 가상 IP 주소를 가지는데, Container가 교체되면 IP주소도 바뀌게 된다.
- 따라서 Container IP 주소 대신 Docker에 내장된 DNS 서비스로 디스커버리 기능을 제공한다.
- Container 이름을 Domain 삼아 조회하면 해당 Container의 IP 주소를 찾아준다.
- 만약 Domain이 가리키는 대상이 Container가 아니면, Docker engine을 실행 중인 PC에 요청을 보내 Host PC가 속한 Network나 Internel IP 주소를 조회한다.
📌 Web Container에서 DNS 조회 명령 실행
docker-compose up -d --scale iotd=3
docker container exec -it image-of-the-day-image-gallery-1 sh
nslookup accesslog
- nslookup
- Application Container의 Base Image에 들어 있던 Utility
- 명령 인자로 Domain 지정 시, 해당 Domain을 DNS 서비스에서 조회하고 결과 출력
- accesslog Container IP 주소는 172.18.0.2라는 것을 알 수 있다.
- DNS 조회를 사용하면 Container가 교체돼 IP 주소가 변경되더라도 항상 새로 만들어진 Container에 접근 가능하다.
📌 Container 삭제 후 다시 실행하고 DNS 확인
docker container rm -f image-of-the-day-accesslog-1
docker-compose up -d --scale iotd=3
docker container exec -it image-of-the-day-image-gallery-1 sh
nslookup accesslog
nslookup iotd
- 새로 만들어지거나 삭제된 Container가 없었으므로 같은 IP 주소가 할당되었다.
- iotd 각각의 IP 주소도 비슷한 Network 범위의 IP 주소를 가진다.
- Docker compose는 이 점을 활용하여 간단한 로드 밸런싱을 구현할 수 있다.
- DNS 시스템이 Container 조회 결과 순서를 매번 변화시킨다.
✒️ 로드 밸런싱(Load Balancing)
쉽게 말해 부하분산을 의미한다.
요청에 대한 업무 수행을 적절하게 분배시켜서 서버의 부하 분산을 막는 용도로 사용한다.
Scale up은 하면 기존 서버의 성능을 높이는 대신 비용도 같이 올라간다.
Scale out을 하면 여러 대의 서버를 두어 트래픽을 분산시킬 수 있다. 그러나 이를 위해 반드시 해야 하는 일이 로드 밸런싱이다.
4. Application 설정값 지정
📌 외부 Database
- 소규모 프로젝트가 아닌 이상 SQLite로 유지하긴 한계가 있다.
- 원격 Container에서 동작하는 PostgreSQL Database는 오픈 소스 RDBMS다.
- Docker에서도 잘 동작하므로 Database Container를 따로 실행하여 분산 Application을 구동할 수 있다.
📌 PostgreSQL DB Service
version: "3.7"
services:
todo-db:
image: diamol/postgres:11.5
ports:
- "5433:5432"
networks:
- app-net
todo-web:
image: diamol/ch06-todo-list
ports:
- "8030:80"
environment:
- Database:Provider=Postgres
depends_on:
- todo-db
networks:
- app-net
secrets: # secret 주입
- source: postgres-connection
target: /app/config/secrets.json
networks:
app-net:
secrets:
postgres-connection:
file: ./config/secrets.json
# secrets.json
{
"ConnectionStrings": {
"ToDoDb": "Server=todo-db;Database=todo;User Id=postgres;Password=postgres;"
}
}
- secrets : 실행 시 Container 내부 파일에 기록될 비밀값 정의
- 해당 앱은 실행되면 '/app/config/secrets.json' 파일이 생긴다.
- 해당 파일에는 postgres=connection이라는 이름의 비밀값이 기록된다.
- 비밀값은 Host PC의 './config/secrets.json'에서 읽어 온다.
- 추후 Cluster 환경에서 암호화된 진짜 비밀값으로 이전할 여지를 남겨 두었다.
- 비밀값은 주로 Cluster 환경에서 Kubernetes나 Docker Swarm 같은 Container Platform을 통해 제공된다.
- 평소에 Cluster Database에 암호화되어 있기 때문에 DB의 패스워드, 인증서, API 키 등의 민감 정보로 구성된 설정값 전달에 적합하다.
- Docker를 단일 컴퓨터에서 실행하는 상황이면 비밀값을 보관하는 Cluster에 Database가 없을 것이므로 파일을 통해 전달해도 괜찮다.
- 개발 환경과 테스트 환경 각각의 Application 설정값을 Compose 파일에 정의하여, 공개하는 port를 환경에 따라 달리하거나 Application 기능을 선택적 활성화할 수 있다.
📌 실행해보기
docker-compose up -d
docker-compose ps
- 이전과는 달리 데이터가 PostgreSQL에 저장되고 있다.
- Sqlectron같은 Database Client를 이용해 localhost:5433으로 접근하면 데이터를 확인할 수 있다.
이렇게 모든 환경에서 동일한 Image로 개발 환경과 테스트 환경에서 모든 검증을 마친 Image를 그대로 운영 환경에 투입할 수 있게 된다.
5. 한계
Kubernetes를 공부하다가 Docker Compose를 다시 보니 느낀 점이 있다면,
나름 노력은 했지만 결국 Docker의 Container 명령어를 한 번에 처리하는 느낌에 더 가깝다.
필요한 Docker resource를 파악하고, 없으면 Docker API를 통해 요청해서 수정하거나 생성해낸다.
딱 여기까지다. Docker swarm이나 Kubernetes처럼 완전한 Container Platform을 구현하지는 못한다.
Application이 지속적으로 정의된 상태를 유지하도록 하는 기능도 없고, 일부 Container가 오류가 발생하여 강제 종료되어도 docker-compose up 명령을 다시 실행할 때까지 Application 상태를 원래대로 되돌릴 수가 없다.
하지만 Container Cluster를 운영할 계획이 없다면 Docker compose만으로도 충분하다.
6. 연습 문제
- Host PC가 재부팅되거나 Docker engine이 재시작되면 Application Container도 재시작되도록 하라
- Database Container는 bind-mount에 파일을 저장해 Application을 재시작하더라도 데이터를 유지하라
- Test를 위해 Web Application을 80번 port를 주시하라
- Docker compose 상세 규격을 살펴보라.
어렵네,, 답을 많이 참고했다.
🟡 Application을 위한 docker-compose-test.yaml
version: "3.7"
services:
todo-db:
image: diamol/postgres:11.5
restart: unless-stopped # Docker engine 재시작시 실행
environment:
- PGDATA=/var/lib/postgresql/data # 해당 경로에 DB Container 유지
volumes:
- type: bind # bind-mount 방식 저장
source: /data/postgres
target: /var/lib/postgresql/data
networks:
- app-net
todo-web:
image: diamol/ch06-todo-list
restart: unless-stopped
ports:
- "8050:80"
environment:
- Database:Provider=Postgres
depends_on:
- todo-db
networks:
- app-net
secrets:
- source: postgres-connection
target: /app/config/secrets.json
networks:
app-net:
external:
name: nat
secrets:
postgres-connection:
file: postgres-connection.json
docker-compose -f docker-compose-test.yml up -d
1트 실패.
app-net 어쩌구 하는 것도 거슬리긴 하지만, 주요 error는 Host PC의 "/data/postgres"로 bind 하려는데 그런 경로가 없단다.
C:\data\postgres
Powershell을 써서 Linux 경로를 잡으면 어지간히 되긴 하는데, 어쨌든 Local OS가 Window라서 경로 지정방식이 잘못되었기 때문이라 판단했다.
Window 경로 방식으로 명시했더니 일단 이 단계는 넘어갔다.
8050 포트 번호가 충돌했다. 8000으로 바꿨는데도 실패했다.
저 얼토당토 않는 포트를 모두 점유하고 있다고...? 그럴리가.
netsh interface ipv4 show excludedportrange protocol=tcp
엥? 막히지도 않았다.
놀랍게도 db는 실행 중이고 todo-web은 만들어져 있단다.
? 만들어지긴 뭐가 만들어져 있어. 화나네?
에러 문구를 다시 좀 차분하게 읽어보자. "Error response from daemon: Ports are not available: exposing port TCP 0.0.0.0:8050 -> 0.0.0.0:0"
대체 어따가 연결하고 있는 거야? :(
컴퓨터 재부팅 했더니 그냥 된다 ㅡㅡ..................................하, 내 아까운 시간.......