Kubernetes - 쿠버네티스로 웹 서비스 배포(1): 인그레스를 활용한 Django & Nginx 배포
in DEV on DevOps, Kubernetes, K8s, Docker, Django, Nginx, Ingress, Sidecar-pattern, Deployment, Service, Docker-hub, Container-orchestration, Python, Web-server
여러 도메인을 라우팅해야 하거나, SSL/TLS 인증서를 통합 관리하고, 로드 밸런싱을 더 효율적으로 처리해야 할 때 인그레스(Ingress)를 사용한다.
이번 포스트에서는 Django(웹 프레임워크)와 Nginx(웹 서버)를 쿠버네티스 환경에 배포해보며, 인그레스를 활용하여 외부 트래픽을 안정적으로 처리하는 전체 과정을 실습해본다.
도커 이미지를 빌드하는 것부터 디플로이먼트, 서비스, 그리고 최종적으로 인그레스를 설정하는 단계까지 포함한다.
목차
개발 환경
- Guest OS: Ubuntu 24.04.2 LTS
- Host OS: Mac Apple M3 Max
- Memory: 48 GB
1. 사전 준비 사항
본격적인 실습에 앞서 몇 가지 준비가 필요하다.
이번 포스트에서는 우리가 만든 컨테이너 이미지를 Docker Hub에 직접 업로드하고, 쿠버네티스 클러스터가 이를 내려받아 실행하는 구조를 가진다.
따라서 Docker Hub 계정이 없다면 먼저 가입을 진행해야 한다.
또한, Django 애플리케이션이 데이터를 저장할 데이터베이스인 PostgreSQL이 로컬 서버(Host)에서 정상적으로 실행 중인지 확인한다.
assu@myserver01:~/work/ch09/ex15$ sudo systemctl status postgresql.service
● postgresql.service - PostgreSQL RDBMS
Loaded: loaded (/usr/lib/systemd/system/postgresql.service; enabled; preset: enabled)
Active: active (exited) since Mon 2025-12-15 06:27:35 UTC; 2 weeks 3 days ago
Main PID: 1459 (code=exited, status=0/SUCCESS)
CPU: 1ms
이와 같이 상태가 active인 것을 확인했다면 준비가 완료된 것이다.
PostgreSQL이 설치되어 있지 않다면, 2.2. 도커 스토리지의 필요성을 참고하세요.
2. 인그레스(Ingress)를 활용한 django 실행
이제 인그레스를 활용하여 Django 애플리케이션을 외부로 서비스해본다. 전체적인 흐름은 다음과 같다.
- Django & Nginx 이미지 빌드(Docker Hub에 업로드)
- 디플로이먼트(Deployment) 생성(Pod 실행)
- 서비스(Service) 생성(내부 네트워크 구성)
- 인그레스(Ingress) 생성(외부 트래픽 라우팅)
2.1. 디렉터리 정리
먼저 디렉터리를 생성하고 정리한다.
기존에 사용했던 Django 관련 파일을 재사용할 것이므로, 2.2. Django 이미지 빌드(Host IP 연결) 에서 사용했던 ch05/ex08 디렉터리를 복사하여 가져온다.
assu@myserver01:~/work$ ls
app ch04 ch05 ch06 ch09
# 이번 실습을 위한 디렉터리 생성
assu@myserver01:~/work$ mkdir ch10
assu@myserver01:~/work$ cd ch10
assu@myserver01:~/work/ch10$ mkdir ex01
assu@myserver01:~/work/ch10$ cd ex01
assu@myserver01:~/work/ch10/ex01$ pwd
/home/assu/work/ch10/ex01
assu@myserver01:~/work/ch05/ex08$ pwd
/home/assu/work/ch05/ex08
assu@myserver01:~/work/ch05/ex08$ ls
myDajngo04 myNginx04
# 기존 소스 복사
assu@myserver01:~/work/ch05/ex08$ cp -r ./ /home/assu/work/ch10/ex01
assu@myserver01:~/work/ch05/ex08$ cd /home/assu/work/ch10/ex01
assu@myserver01:~/work/ch10/ex01$ ls
myDajngo04 myNginx04
2.2. Django 이미지 빌드
Django 컨테이너가 로컬 호스트(Node)에 설치된 PostgreSQL 데이터베이스를 바라볼 수 있도록 설정을 변경하고, 새로운 이미지를 빌드한다.
먼저 settings.py 파일이 있는 위치로 이동한다.
assu@myserver01:~/work/ch10/ex01$ cd myDajngo04/myapp/myapp
assu@myserver01:~/work/ch10/ex01/myDajngo04/myapp/myapp$ ls
asgi.py __init__.py __pycache__ settings.py urls.py wsgi.py
assu@myserver01:~/work/ch10/ex01/myDajngo04/myapp/myapp$ vim settings.py
DATABASES 항목의 HOST 정보를 수정한다.
수정 전(Docker 환경)
...
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'postgres',
'USER': 'postgres',
'PASSWORD': 'mysecretpassword',
'HOST': '172.17.0.1', # Docker Bridge Gateway IP
'PORT': 5432,
}
}
...
수정 후(Kubernetes 환경)
...
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'postgres',
'USER': 'postgres',
'PASSWORD': 'mysecretpassword',
'HOST': '10.0.2.4', # PostgreSQL이 설치된 Node의 실제 IP
'PORT': 5432,
}
}
...
기존 Docker 환경에서는 172.17.0.1을 사용했지만, 쿠버네티스 환경(특히 Minikube나 VM 환경)에서는 노드의 실제 IP를 명시해주어야 파드에서 호스트의 DB로 접근이 원활하다. 여기서는 myserver01의 IP인 10.0.2.4를 입력했다.
Docker 환경과 k8s 환경에서의 네트워크 차이에 대한 내용은 Docker vs k8s 네트워크 차이: Django에서 로컬 DB 연결 시 Host IP의 차이를 참고하세요.
이제 설정 변경을 완료했으므로 Docker 이미지를 빌드한다.
assu@myserver01:~/work/ch10/ex01/myDajngo04/myapp/myapp$ cd ../..
assu@myserver01:~/work/ch10/ex01/myDajngo04$ ls
Dockerfile myapp requirements.txt
# 이미지 빌드(Tag: 0.2)
assu@myserver01:~/work/ch10/ex01/myDajngo04$ docker image build . -t mydjango_ch10:0.2
[+] Building 9.7s (11/11) FINISHED docker:default
...
1 warning found (use docker --debug to expand):
- JSONArgsRecommended: JSON arguments recommended for CMD to prevent unintended behavior related to OS signals (line 11)
assu@myserver01:~/work/ch10/ex01/myDajngo04$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
mydjango_ch10 0.2 a5b8ae94299f 8 seconds ago 1.19GB
이미지가 정상적으로 생성되었다. 이제 이 이미지를 쿠버네티스 클러스터가 가져갈 수 있도록 Docker Hub에 업로드한다.
먼저 웹 브라우저로 Docker Hub에 접속하여 mydjango_ch10 이라는 이름의 리포지토리를 생성한다.

리포지토리 생성이 완료되었다면, 터미널에서 로그인을 진행한다.
assu@myserver01:~/work/ch10/ex01/myDajngo04$ docker login
USING WEB-BASED LOGIN
...
WARNING! Your credentials are stored unencrypted in '/home/assu/.docker/config.json'.
Configure a credential helper to remove this warning. See
https://docs.docker.com/go/credential-store/
Login Succeeded
로그인 성공 후, 생성한 이미지를 Docker Hub 계정 경로에 맞게 태그를 변경하고 Push 한다.
(assu 부분은 본인의 Docker Hub ID로 변경해야 합니다.)
# 도커 이미지 태그 생성(형식: <DockerHub_ID>/<Repo_Name>:<Tag>)
assu@myserver01:~/work/ch10/ex01/myDajngo04$ docker tag mydjango_ch10:0.2 assu/mydjango_ch10:0.2
# 도커 허브에 업로드
assu@myserver01:~/work/ch10/ex01/myDajngo04$ docker push assu/mydjango_ch10:0.2
The push refers to repository [docker.io/assu/mydjango_ch10]
5f70bf18a086: Pushed
...
0.2: digest: sha256:c6f8562dc49e19e246e40bb0f57d31b40dce153a8fccc035bffa2675ffc99e76 size: 2840
docker tag 명령은 기존 로컬 이미지를 가리키는 새로운 Alias를 만드는 과정이며, docker push를 통해 원격 저장소로 업로드된다.

만일
docker push시denied: requested access to the resource is denied오류가 발생한다면, 로그인이 제대로 되지 않았거나 리포지토리 권한 문제일 수 있습니다. 리포지토리 권한 문제일 경우 TroubleShooting -docker push시denied: requested access to the resource is denied을 참고하세요.
2.3. Nginx 이미지 빌드
Django 애플리케이션 앞단에서 리버스 프록시(Reverse Proxy) 역할을 수행할 Nginx 이미지를 빌드한다.
먼저 Nginx 관련 파일이 있는 디렉터리로 이동한다.
assu@myserver01:~/work/ch10/ex01/myNginx04$ pwd
/home/assu/work/ch10/ex01/myNginx04
assu@myserver01:~/work/ch10/ex01/myNginx04$ ls
default.conf Dockerfile
쿠버네티스 파드 환경에 맞게 default.conf 설정 파일을 수정한다.
assu@myserver01:~/work/ch10/ex01/myNginx04$ vim default.conf
수정 전(Docker Compose 환경)
upstream myweb{
server djangotest:8000;
}
server{
listen 80;
server_name localhost;
location /{
proxy_pass http://myweb;
}
}
수정 후(Kubernetes Sidecar 패턴 환경)
server{
listen 80;
server_name localhost;
location /{
proxy_pass http://127.0.0.1:8000;
}
}
중요 변경 사항
기존의 Docker Compose 환경에서는 컨테이너 간 통신 시 서비스 이름(예: djangotest)을 호스트명으로 사용했었다.
하지만 이번 쿠버네티스 환경에서는 하나의 파드 안에 Nginx 컨테이너와 Django 컨테이너를 함께 배치할 예정이다.
동일한 파드 내의 컨테이너들은 네트워크 네임스페이스를 공유하므로 localhost를 통해 서로 통신할 수 있다.
따라서 upstream 설정 없이 바로 http://127.0.0.1:8000 으로 패킷을 전달하도록 수정한다.
이제 수정된 설정을 바탕으로 Nginx 도커 이미지를 빌드한다.
assu@myserver01:~/work/ch10/ex01/myNginx04$ docker image build . -t mynginxd_ch10:0.3
[+] Building 2.1s (9/9) FINISHED docker:default
=> [internal] load build definition from Dockerfile 0.0s
...
이미지가 정상적으로 생성되었는지 확인한다.
assu@myserver01:~/work/ch10/ex01/myNginx04$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
mynginxd_ch10 0.3 abb07383ce25 3 minutes ago 192MB
생성된 이미지를 Docker Hub에 올리기 위해 웹 브라우저에서 mynginxd_ch10 리포지토리를 생성한다.

Django 이미지와 동일한 방식으로 태그를 생성하고 Push를 진행한다.
# 태그 생성
assu@myserver01:~/work/ch10/ex01/myNginx04$ docker tag mynginxd_ch10:0.3 assu/mynginxd_ch10:0.3
# 도커 허브 업로드
assu@myserver01:~/work/ch10/ex01/myNginx04$ docker push assu/mynginxd_ch10:0.3
The push refers to repository [docker.io/assu/mynginxd_ch10]
98888136d0ec: Pushed
...
0.3: digest: sha256:de72045142edf66d03dcf150732ad9acad026fb278371e20d0a3923d2dfe4e4a size: 2192

2.4. 디플로이먼트(Deployment) 실행
이제 앞서 만든 두 개의 이미지(Django, Nginx)를 사용하여 실제 애플리케이션을 구동할 디플로이먼트를 정의한다.
assu@myserver01:~/work/ch10/ex01$ ls
myDajngo04 myNginx04
assu@myserver01:~/work/ch10/ex01$ vim django-deploy.yml
apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy-django
spec:
replicas: 3
selector:
matchLabels:
app.kubernetes.io/name: web-deploy
template:
metadata:
labels:
app.kubernetes.io/name: web-deploy
spec:
containers:
# Nginx 컨테이너 (Reverse Proxy)
- name: nginx-d
image: assu/mynginxd_ch10:0.3 # Docker Hub에 업로드한 이미지
ports:
- containerPort: 80
# Django 컨테이너 (App)
- name: django-web
image: assu/mydjango_ch10:0.2
ports:
- containerPort: 8000
위 YAML 파일의 핵심은 spec.containers 항목에 두 개의 컨테이너가 정의되어 있다는 점이다.
이를 통해 멀티 컨테이너 파드가 생성되며, 두 컨테이너는 항상 함께 스케줄링되고 동일한 IP와 포트 공간을 공유한다.
이제 디플로이먼트를 실행한다.
assu@myserver01:~/work/ch10/ex01$ kubectl apply -f django-deploy.yml
deployment.apps/deploy-django created
assu@myserver01:~/work/ch10/ex01$ kubectl get pods
NAME READY STATUS RESTARTS AGE
deploy-django-77675fcbcf-4rbhn 2/2 Running 0 72s
deploy-django-77675fcbcf-6f5gf 2/2 Running 0 72s
deploy-django-77675fcbcf-p5ptn 2/2 Running 0 72s
READY 열이 2/2로 표시되는 것은 파드 내의 두 컨테이너(Django, Nginx)가 모두 정상적으로 실행 중임을 의미한다.
도커 허브에서 이미지를 다운로드해야 하기 때문에 파드가 실행되기까지 다소 시간이 소요된다.
2.5. 서비스(Service) 실행
파드들이 정상적으로 실행되었으므로, 이들에 접근할 수 있는 단일 진입점인 서비스(Service)를 생성한다.
외부 노출을 인그레스가 담당할 예정이므로, 서비스 타입은 내부 통신용인 ClusterIP을 사용한다.
apiVersion: v1
kind: Service
metadata:
name: django-service
spec:
selector:
app.kubernetes.io/name: web-deploy # 서비스와 연결한 파드 설정, 디플로이먼트의 라벨과 일치해야 함
type: ClusterIP
ports:
- protocol: TCP
port: 80 # [서비스용] 클러스터 내부 다른 리소스(Ingress 등)가 접근할 포트
targetPort: 80 # [파드용] 파드 내 Nginx 컨테이너의 포트
서비스를 생성하고 상태를 확인한다.
assu@myserver01:~/work/ch10/ex01$ kubectl apply -f django-service.yml
service/django-service created
assu@myserver01:~/work/ch10/ex01$ kubectl get pod,svc
NAME READY STATUS RESTARTS AGE
pod/deploy-django-77675fcbcf-4rbhn 2/2 Running 0 2m40s
pod/deploy-django-77675fcbcf-6f5gf 2/2 Running 0 2m40s
pod/deploy-django-77675fcbcf-p5ptn 2/2 Running 0 2m40s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/django-service ClusterIP 10.98.94.163 <none> 80/TCP 5s
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 7d1h
service/django-service 서비스가 ClusterIP 타입으로 생성되었으며, 클러스터 IP 10.98.94.163 을 할당받은 것을 확인할 수 있다.
이제 클러스터 내부에서는 이 IP를 통해 Django 앱(정확히는 Nginx)에 접근할 수 있게 되었다.
2.6. 인그레스(Ingress) 실행
이제 클러스터 외부에서 내부의 서비스로 접근할 수 있도록 인그레스를 설정한다.
인그레스는 외부 요청(HTTP/HTTPS)을 클러스터 내부의 서비스(Service)로 라우팅해주는 규칙의 모음이다.
assu@myserver01:~/work/ch10/ex01$ vim django-ingress.yml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: django-ingress
annotations:
# Nginx Ingress Controller가 /test01 경로를 제거하고 백엔드로 전달하도록 설정 (Rewrite)
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
ingressClassName: nginx
rules:
- http:
paths:
# 정규표현식을 사용하여 /test01 뒤에 오는 모든 경로를 포착
- path: /test01(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: django-service # 인그레스와 연동할 서비스 이름, 앞서 생성한 서비스 이름
port:
number: 80
인그레스 설정값에 대한 좀 더 상세한 설명은 6. 인그레스로 하나의 서비스 배포 를 참고하세요.
이제 인그레스 리소스를 생성하고 상태를 확인한다.
assu@myserver01:~/work/ch10/ex01$ kubectl apply -f django-ingress.yml
ingress.networking.k8s.io/django-ingress created
assu@myserver01:~/work/ch10/ex01$ kubectl get pod,svc,ingress
NAME READY STATUS RESTARTS AGE
pod/deploy-django-77675fcbcf-4rbhn 2/2 Running 0 8m49s
pod/deploy-django-77675fcbcf-6f5gf 2/2 Running 0 8m49s
pod/deploy-django-77675fcbcf-p5ptn 2/2 Running 0 8m49s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/django-service ClusterIP 10.98.94.163 <none> 80/TCP 6m14s
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 7d1h
NAME CLASS HOSTS ADDRESS PORTS AGE
ingress.networking.k8s.io/django-ingress nginx * 80 28s
django-ingress가 정상적으로 생성된 것을 확인할 수 있다.
이제 웹 브라우저를 통해 접속 테스트를 진행한다.
- 접속 URL: http://127.0.0.1:2000/test01
브라우저에서 Django 환영 페이지가 보인다면 외부 요청이 Ingress → Service → Pod(Nginx → Django) 순서로 정상적으로 도달한 것이다.
2.6.1. 파드 내부 접속 및 상태 확인
정말 설정한 대로 파일들이 배치되어 있고, DB 연결이 잘 되는지 확인하기 위해 실행 중인 파드 내부로 직접 들어가본다.
1) Nginx 컨테이너 접속
assu@myserver01:~/work/ch10/ex01$ kubectl exec -it pod/deploy-django-77675fcbcf-p5ptn -- /bin/bash
Defaulted container "nginx-d" out of: nginx-d, django-web
root@deploy-django-77675fcbcf-p5ptn:/etc/nginx/conf.d# cat default.conf
server{
listen 80;
server_name localhost;
location /{
proxy_pass http://127.0.0.1:8000;
}
}
root@deploy-django-77675fcbcf-p5ptn:/etc/nginx/conf.d# exit
exit
kubectl exec 명령어 실행 시 별도의 컨테이너 옵션을 주지 않으면, 쿠버네티스는 첫 번째 컨테이너(nginx-d)로 접속시킨다.
default.conf 파일을 확인해보니 우리가 빌드한 대로 proxy_pass 설정이 잘 들어가있다.
2) Django 컨테이너 접속(-c 옵션 사용)
이번에는 같은 파드 내에 있는 Django 컨테이너로 접속해본다.
멀티 컨테이너 파드에서는 -c(container) 옵션을 사용하여 접속할 대상을 명시해야 한다.
assu@myserver01:~/work/ch10/ex01$ kubectl exec -it pod/deploy-django-77675fcbcf-4rbhn -c django-web -- /bin/bash
root@deploy-django-77675fcbcf-4rbhn:/usr/src/app/myapp# ls
db.sqlite3 manage.py myapp
# 데이터베이스 연결 확인
root@deploy-django-77675fcbcf-4rbhn:/usr/src/app/myapp# python manage.py inspectdb
...
from django.db import models
root@deploy-django-77675fcbcf-4rbhn:/usr/src/app/myapp# exit
exit
inspect 명령어가 에러 없이 실행되었다면, Django 컨테이너가 호스트(Node)에 설치된 PostgreSQL 데이터베이스와 정상적으로 통신하고 있음을 의미한다.
2.6.2. 전체 아키텍처 및 리소스 정리
지금까지 진행한 내용의 전체 네트워크 흐름은 아래 그림과 같다.

- User: /test01 경로로 요청
- Ingress: 요청을 받아 django-service로 라우팅(경로는
/로 Rewrite) - Service: 적절한 파드를 찾아 트래픽 분산
- Pod(Nginx): 80 포트로 요청을 받아 로컬호스트의 8000 포트로 전달
- Pod(Django): 요청을 처리하고 필요한 경우 노드(Host)의 DB에서 데이터를 조회
이제 생성한 리소스를 정리한다.
assu@myserver01:~/work/ch10/ex01$ kubectl delete -f django-ingress.yml
ingress.networking.k8s.io "django-ingress" deleted
assu@myserver01:~/work/ch10/ex01$ kubectl delete -f django-service.yml
service "django-service" deleted
assu@myserver01:~/work/ch10/ex01$ kubectl delete -f django-deploy.yml
deployment.apps "deploy-django" deleted
assu@myserver01:~/work/ch10/ex01$ kubectl get pod,svc,ingress
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 7d2h
정리하며..
이번 포스트의 핵심 내용은 아래와 같다.
- Docker Hub 활용
- 로컬 이미지가 아닌 Docker Hub에 업로드된 이미지를 사용하여 클러스터가 이미지를 받아오도록 구성하였다.
- Sidecar 패턴
- 하나의 파드 안에 Django와 Nginx 컨테이너를 함께 배치하여 localhost 통신이 가능하도록 설계하였다.
- Ingress 활용
- 복잡한 외부 접속 설정을 Ingress 리소스 하나로 통합 관리하여 유연한 라우팅을 구현하였다.
이러한 구조는 실제 환경에서도 매우 자주 사용되는 패턴이므로, 각 구성 요소(Pod, Service, Ingress)의 역할과 연결 고리를 확실히 이해해두는 것이 중요하다.
참고 사이트 & 함께 보면 좋은 사이트
본 포스트는 장철원 저자의 한 권으로 배우는 도커&쿠버네티스를 기반으로 스터디하며 정리한 내용들입니다.
