목차
Application 배포
이전 글에서 이미지와 Docker 네트워크를 만들었다면, 이번에는 RabbitMQ와 메시지를 처리하는 애플리케이션을 같은 사용자 정의 네트워크에 배포해 본다. RabbitMQ 컨테이너만 실행하면 앱 배포가 완료된 것은 아니다. 앱 이미지를 만들고, 브로커 주소를 전달하고, 두 컨테이너의 통신과 메시지 처리까지 확인한다.
예제는 로컬 실습용이다. Python 소비자 앱은 메시지 길이를 기록하고 확인 응답(ack)을 보내는 최소 구현이다. 실제 주문 처리 코드는 비즈니스 작업이 끝난 뒤 ack를 보내도록 바꿔야 한다.
flowchart LR
P[테스트 발행자] -->|AMQP 5672| R[(RabbitMQ)]
R -->|orders 큐| W[소비자 앱]
H[호스트 브라우저] -->|127.0.0.1:15672| M[관리 화면]
M --> R
R --> V[(이름 붙은 볼륨)]
발행자와 소비자 앱은 Docker 네트워크 안에서 rabbitmq:5672로 접속한다. AMQP 포트를 호스트에 공개하지 않아도 된다. 관리 화면만 호스트의 loopback 주소에 연결하고, 브로커 데이터는 볼륨에 보관한다.
1. 네트워크와 자격 증명 준비
Docker Engine과 OpenSSL이 설치된 로컬 실습 환경을 가정한다. 먼저 네트워크와 볼륨을 만든다.
docker network create ecommerce-network
docker volume create ecommerce-rabbitmq-data
기존 글은 --subnet 172.18.0.0/16을 지정했지만, 출력 예시에는 172.21.0.0/16이 표시돼 있었다. 이 실습에서는 Docker가 충돌하지 않는 대역을 선택하게 한다. 컨테이너는 사용자 정의 bridge 네트워크의 DNS로 서로의 이름을 찾을 수 있으므로 IP를 코드에 넣지 않는다.
guest/guest를 사용하지 않고 처음 부팅할 때 쓸 암호를 생성한다. 저장소에서 작업한다면 .env.rabbitmq를 .gitignore에 추가한다. 아래 파일은 개인 실습용으로 권한을 제한한 로컬 파일이며, 운영 환경에서는 비밀 관리 시스템을 사용한다.
umask 077
printf 'RABBITMQ_DEFAULT_USER=lab_user\nRABBITMQ_DEFAULT_PASS=%s\n' \
"$(openssl rand -hex 24)" > .env.rabbitmq
이 파일을 다른 사람에게 보내거나 저장소에 커밋하지 않는다. --env-file로 전달한 값은 Docker 접근 권한이 있는 사용자에게 보일 수 있다. RabbitMQ 접근 제어 문서는 기본 guest 계정을 운영에 사용하지 말고 별도 자격 증명을 만들 것을 권한다.
2. RabbitMQ 실행
Docker 공식 RabbitMQ 이미지의 4.3.6-management 태그를 예제로 사용한다. latest처럼 큰 버전이 자동으로 바뀌는 태그는 사용하지 않는다. 실제 재현성이 필요하면 검증한 이미지 digest까지 고정하고 정기적으로 업데이트한다.
docker pull rabbitmq:4.3.6-management
docker run -d \
--name rabbitmq \
--hostname rabbitmq \
--network ecommerce-network \
--restart unless-stopped \
--env-file .env.rabbitmq \
--mount source=ecommerce-rabbitmq-data,target=/var/lib/rabbitmq \
-p 127.0.0.1:15672:15672 \
rabbitmq:4.3.6-management
5672는 브로커의 AMQP 포트이고 15672는 관리 화면 포트다. 앱과 브로커가 같은 Docker 네트워크에 있으므로 AMQP 포트를 -p로 공개하지 않는다. 127.0.0.1:15672:15672는 로컬 호스트에서만 관리 화면에 접근하게 한다. Docker의 포트 공개 문서에 따르면 호스트 IP를 생략한 -p 15672:15672는 기본적으로 모든 호스트 인터페이스에 바인딩된다.
docker ps --filter name=rabbitmq
docker exec rabbitmq rabbitmq-diagnostics -q ping
docker logs --tail 30 rabbitmq
관리 화면은 로컬 브라우저의 http://127.0.0.1:15672에서 열고, .env.rabbitmq에 생성된 계정으로 로그인한다. 로그와 진단 명령이 오류를 내면 앱을 실행하기 전에 브로커를 먼저 확인한다. 이름 붙은 볼륨을 다시 사용할 때 RABBITMQ_DEFAULT_USER와 RABBITMQ_DEFAULT_PASS를 바꿔도 이미 초기화된 계정이 자동으로 변경되지는 않는다. 비밀번호 변경은 RabbitMQ 사용자 관리 절차를 따로 수행한다.
3. 소비자 애플리케이션 이미지 만들기
다음 파일을 demo-worker 디렉터리에 만든다. Pika는 Python의 AMQP 클라이언트다. 이 예제는 pika==1.4.4를 고정한다.
[requirements.txt]
pika==1.4.4
[worker.py]
import os
import time
import pika
def connect():
return pika.BlockingConnection(
pika.ConnectionParameters(
host=os.environ["RABBITMQ_HOST"],
credentials=pika.PlainCredentials(
os.environ["RABBITMQ_DEFAULT_USER"],
os.environ["RABBITMQ_DEFAULT_PASS"],
),
heartbeat=60,
blocked_connection_timeout=30,
)
)
def handle_message(channel, delivery, properties, body):
# 실제 서비스에서는 비즈니스 작업이 성공한 후 ack를 보낸다.
print(f"processed message ({len(body)} bytes)", flush=True)
channel.basic_ack(delivery_tag=delivery.delivery_tag)
while True:
try:
connection = connect()
channel = connection.channel()
channel.queue_declare(queue="orders", durable=True)
channel.basic_qos(prefetch_count=1)
channel.basic_consume(
queue="orders", on_message_callback=handle_message, auto_ack=False
)
print("worker ready", flush=True)
channel.start_consuming()
except pika.exceptions.AMQPConnectionError:
print("broker connection lost; retrying", flush=True)
time.sleep(5)
브로커 시작이 늦거나 연결이 끊기면 소비자가 다시 연결한다. 메시지를 받은 뒤 처리 전에 연결이 끊어졌을 때 재전달될 수 있으므로, 실제 작업은 중복 실행해도 결과가 안전한 방식으로 구현해야 한다. Pika의 BlockingConnection 예제와 연결 복구 설명을 참고한다.
동작 검증용 발행자도 같은 이미지에 넣는다.
[publish.py]
import os
import pika
connection = pika.BlockingConnection(
pika.ConnectionParameters(
host=os.environ["RABBITMQ_HOST"],
credentials=pika.PlainCredentials(
os.environ["RABBITMQ_DEFAULT_USER"],
os.environ["RABBITMQ_DEFAULT_PASS"],
),
)
)
channel = connection.channel()
channel.queue_declare(queue="orders", durable=True)
channel.confirm_delivery()
channel.basic_publish(
exchange="",
routing_key="orders",
body=b"sample-order-1",
properties=pika.BasicProperties(delivery_mode=2),
)
connection.close()
print("sample message published")
큐의 durable=True와 메시지의 delivery_mode=2는 재시작 시 데이터 보존을 위한 설정이다. 이 설정만으로 모든 장애 상황의 무손실을 보장하지는 않으므로, 업무 시스템에서는 발행 확인·중복 처리·백업·복구 절차도 검토한다.
[Dockerfile]
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY worker.py publish.py ./
USER 10001:10001
CMD ["python", "worker.py"]
앱은 비root 사용자로 실행되고, 소스와 의존성을 이미지에 넣는다. 실제 배포에서는 이미지의 기반 OS와 Python 패키지 보안 업데이트를 주기적으로 반영한다(Docker 이미지 빌드 권장사항).
4. 앱 실행과 끝까지 확인
demo-worker 디렉터리에서 이미지를 빌드한다. .env.rabbitmq 파일은 앞에서 만든 위치를 기준으로 경로를 적는다. 아래 예시는 이 파일을 demo-worker의 상위 디렉터리에 둔 경우다.
docker build -t ecommerce-worker:1.0.0 .
docker run -d \
--name ecommerce-worker \
--network ecommerce-network \
--restart unless-stopped \
--env-file ../.env.rabbitmq \
-e RABBITMQ_HOST=rabbitmq \
ecommerce-worker:1.0.0
docker logs --tail 20 ecommerce-worker
로그에서 worker ready를 확인한 뒤 발행자 컨테이너를 한 번 실행한다. 앱은 브로커를 rabbitmq라는 컨테이너 이름으로 찾는다. 호스트의 localhost를 넣으면 소비자 컨테이너 자기 자신을 가리키므로 연결할 수 없다.
docker run --rm \
--network ecommerce-network \
--env-file ../.env.rabbitmq \
-e RABBITMQ_HOST=rabbitmq \
ecommerce-worker:1.0.0 python publish.py
docker logs --tail 20 ecommerce-worker
docker network inspect ecommerce-network \
--format '{{range .Containers}}{{println .Name}}{{end}}'
발행자는 sample message published, 소비자는 processed message (14 bytes)를 기록해야 한다. 네트워크 조회에는 rabbitmq와 ecommerce-worker가 표시된다. 실제 IP 대역과 컨테이너 ID는 실행할 때마다 달라질 수 있으므로 고정된 docker network inspect 출력 예시를 정답으로 비교하지 않는다.
5. 실패할 때 확인할 순서
| 증상 | 확인할 것 |
|---|---|
| 관리 화면에 접속할 수 없음 | docker ps의 포트와 docker logs rabbitmq 확인. 호스트에서 127.0.0.1:15672로 접속했는가? |
| 앱이 브로커를 찾지 못함 | 두 컨테이너가 ecommerce-network에 있는가? RABBITMQ_HOST=rabbitmq인가? |
| 로그인이 거부됨 | guest를 사용하고 있지 않은가? 볼륨이 기존 계정으로 초기화되어 있지 않은가? |
| 발행 후 처리 로그가 없음 | 큐 이름이 양쪽 모두 orders인가? 소비자 로그와 브로커 상태는 정상인가? |
| 재시작 뒤 데이터가 없음 | RabbitMQ가 같은 이름 붙은 볼륨을 사용하고 있는가? 메시지를 ack하기 전에 실제 처리가 끝났는가? |
실습을 끝내고 컨테이너만 정리하려면 docker stop ecommerce-worker rabbitmq와 docker rm ecommerce-worker rabbitmq를 실행한다. 볼륨은 별도 삭제 전까지 남아 있으므로, 다시 실행할 때는 기존 계정과 데이터가 유지된다. 데이터를 지우고 처음부터 시작해야 할 때만 볼륨 삭제 여부를 결정한다.