A Story about Building Container Images
최근 들어 Docker Container에 대해 깊은 이해가 필요하여 이 참에 어렴풋이 알고 있던 Container에 대해 제대로 함 알아보자는 요량으로 관련 서적을 뒤지다가 가장 최근의 책이라고 여겨지는 “Docker Deep Dive 2024 edition” 을 골라 습독을 했다.
대략 Docker Container에 대한 내용은 익히 알고 있던터라 개념적 이해는 쉽게 지나갔지만 실제 이 기술을 이용하고 싶을 때 막히지 않을 정도로 몸에 익혀보자는 생각으로 책에 나온 샘플 코드를 이리 저리 내가 활용해 보고 싶은 시나리오로 변형하고 적용하다 보니 계획했던 2주 보다 시간이 좀 더 걸렸다. 이 참에 다시 한 번 느끼지만 자전거 타는 방법을 공부하는 것과 자전거를 탈 줄 안다는 것에는 큰 간극이 있음을 확인했다.
이 글에서 Docker Container에 대한 배경 설명은 생락한다. 왜나면 이런 개념 설명은 인터넷을 조금만 찾아보면 늘려 있기 때문이다. 여기서는 Docker Deep Dive 책을 보고 이 책에 소개된 기술을 내가 생각한 시나리오에 어떻게 적용 했는지 그리고 그 적용 과정에서 알게된 Know-How을 기록하기로 한다.
Container에서 핵심이 되는 기술들은 여럿 있지만 중급 개발자들이 많이 활용하는 또는 Container 환경에서 자주 부딪히는 기술들은 아마도 Volume과 Network일 것이다. 여기서 한 발 더 나아간다면 Container Clustering(Docker Swarm)정도가 되지 않을까 한다. Container Clustering은 또 시스템 운영 자동화의 핵심인 Kubernetes 와 닿기 때문에 다른 주제로 이 기술을 얘기를 해야 할것 같고 여기서는 Container 이미지를 만들 때 Volume과 Network 기술을 활용하는 시나리오로 얘기를 풀어보고자 한다.
시나리오
예전에 경로탐색 알고리즘으로 Dijkstra, Bidirectoinal Dijkstra 그리고 Contraction Hierarchy에 대해 설명한 적이 있다. 이 과정에서 작성했던 코드들을 동작 가능한 형태의 컨테이너 이미지로 만들어 배포하면 보다 많은 사람들이 쉽게 경험하지 않을까라는 생각으로 이어졌다. 왜나하면 이런 코드를 실행하려면 기본적으로 도로네트워크 데이터가 있어야 하지만 이 부분을 제외하더라도 데이터베이스와 WAS(Tomcat)의 설치와 설정이 필요하고 여기에 웹어플리케이션 배포 뿐만 아니라 필요한 환경 설정도 해야해서 시간이 제법 소요되는 일이기 때문이다.
여기에 CI(Continuous Integration) 개념을 적용하여 Source Repository(Gitlab)에서 실시간으로 Stable한 소스코드를 가져와 WebApp Binary로 빌드하고 이렇게 만들어진 WebApp을 WAS에 올려 최종 실행 가능한 이미지로 만들면 향후 Cloud환경에서 CI/CD환경 구성에 있어서도 활용도가 높겠다는 생각을 하게 되었다.
자 그럼 배경설명은 이정도로 하고 전체 시나리오와 구성하고자 하는 어플리케이션 구조에 대한 얘기로 이어가보자.
경로탐색 알고리즘의 실행 결과로 생성된 경로가 어떤 형태인지 지도상에서 시각적으로 확인할 수 있도록 할 뿐만 아니라 알고리즘 종류를 바꿔가면어 그 결과가 어떻게 달라지는지 확인할 수 있도록 하는 WebApp을 만들고자 한다.
당연한 이야기지만 경로탐색 알고리즘 실행에 필요한 모델인 도로네트워크 그래프(Graph)를 구성하려면 도로네트워크 데이터가 필요하다. 이 데이터의 로딩과 WebApp 동작에 필요한 부가 데이터를 제공하기 위해서는 데이터베이스 설치와 구성이 필수적이다.
이렇게 보면 어플리케이션 구조는 일반적인 Web-WAS-DB형태의 3-Tier가 적합하며 각 Tier의 역할은 아래와 같이 요약할 수 있다.
- WEB – 사용자가 알고리즘 종류를 선택하며 지도를 시각화하고 경로탐색 알고리즘의 실행 결과를 지도상에 표현한다.
- WAS – 경로탐색 알고리즘을 실행하고 그 결과를 Client로 전달한다. 뿐만 아니라 도로네트워크 데이터의 속성정보를 데이터베이스에서 읽어 Graph Model을 구성하고 클라이언트 UI에서 필요로 하는 도로 속성 데이터를 Client로 전달하는 기능도 병행한다.
- DB – 도로네트워크 데이터를 저장하고 WAS에서 필요로 하는 데이터를 제공한다.
이런 구조를 Container로 구성하려면 간단하게 데이터베이스와 WAS를 Container화 하면 된다. 데이터베이스는 상대적으로 독립적으로 동작하는 형태여서 Docker Hub와 같은 Container Registry에서 받아 바로 사용하면 된다. 하지만 경로탐색과 같은 어플리케이션은 Tomcat과 같은 기존의 표준 WAS Container 이미지 위에 웹앱을 추가하고 이 추가된 부분을 포함한 새로운 Container이미지로 빌드해야 한다.
뿐만 아니라 Container는 삭제되면 실행 동안 생성했던 모든 데이터들도 같이 사라지게 되므로 Container 삭제 후에도 데이터베이스의 데이터가 유지될 수 있도록 Container외부의 저장공간과 매핑된 Volume을 이용해야 한다. WAS와 데이터베이스간의 연동도 필요하므로 이들 간의 통신을 위해 네트워크 구성도 같이 고려해야 한다.
이제 시나리오 구현을 위해 데이터베이스, WAS, 네트워크 구성, Volume을 설정하는 일련의 과정을 설명하도록 한다. 최종적으로는 Docker Compose를 이용하여 이들 개별 과정을 하나로 묶어 처리하는 방법으로 설명을 이어가보도록 하겠다.
Network 구성
Container의 네트워크는 Namespace로 분리되어 있고 Sandbox 형태로 구성되어 Container 독립적인 네트워크 구성이 가능하다. Container가 어떤 환경에서 사용되느냐에 따라 거기에 적합한 네트워크구성을 선택할 수 있지만 여기서는 하나의 호스트 내에서 Container간의 통신즉, WAS Container와 DB Container간의 통신이 편리하도록 Bridge 네트워크 Driver를 활용하도록 한다.
참고로, 컴퓨터 네트워크는 역사가 오래되고 그 간의 다양한 노하우를 축적해 가며 발전해 온 상태라 개발자들이 사용하기에는 매우 편리하게 되어 있지만 그 내부 기술들을 하나씩 파악하기에는 광범위하고 복잡도도 증가하여 여기서는 개별 기술들에 대한 깊이 있는 설명은 건너 뛰도록 한다.
Docker에서 Default로 제공하는 bridge 네트워크는 “bridge“라고 불리며 호스트 커널에 있는 “docker0” 이름의 “Linux birdge“와 매핑된다. 이 부분을 그림으로 표현하면 아래와 같다.

이제 Default로 제공되는 Bridge네트워크가 아닌 Docker 명령어를 이용하여 Singe-host Bridge Network을 생성해보자. 명령어는 간단하다. 아래와 같이 docker network create 명령어를 실행하면 간단하게 “localnet”이라는이름의 bridge 네트워크를 생성할 수 있다. (-d 옵션은 driver의 종류를 지정한다)
skanto@skanto:~$ docker network create -d bridge localnet
9bd8b61bd94c452c87878ae4abb83a56a9a0a535f9d78a979d13c24ce49e4ec9
정상적으로 생성되었는지는 아래와 같이 확인할 수 있다.
skanto@skanto:~$ docker network ls
NETWORK ID NAME DRIVER SCOPE
7bdaed379afc bridge bridge local
61aafbfc8573 host host local
9bd8b61bd94c localnet bridge local
e173d1a8fcbb none null local
방금 생성한 localnet의 상세정보를 확인하려면 inspect 명령어를 이용하면 되고 아래와 같이 여러가지 속성을 확인할 수 있다.
skanto@skanto:~$ docker network inspect localnet
[
{
"Name": "localnet",
"Id": "9bd8b61bd94c452c87878ae4abb83a56a9a0a535f9d78a979d13c24ce49e4ec9",
"Created": "2024-12-22T04:55:59.024670592Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.19.0.0/16",
"Gateway": "172.19.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {},
"Options": {},
"Labels": {}
}
]
Volume 생성
Container환경 하에서는 Volume을 사용하는 것이 필수적이라 할 수 있는데 여기에는 몇가지 이유가 있다.
- Volume은 Container의 lifecycle과 독립적으로 동작하는 객체이다.(Container가 삭제되더라도 Volume은 지워지지 않는다)
- NAS와 같이 특별한 외부 저장매체에 Volume을 매핑할 수 있다.
- 서로 다른 Host에서 동작하는 여러 Container가 동일한 데이터를 공유하기 위해 Volume을 이용할 수 있다.

Network 을 생성한 것과 마찬가지로 Volume 생성도 Docker volume 명령어로 간단하게 생성할 수 있다.
skanto@skanto:~$ docker volume create myvol
myvol
특별한 옵션을 지정하지 않으면 default로 local driver 를 적용한다. 만약 SAN, NAS와 같이 다른 driver를 적용하려면 -d 옵션을 이용하여 지정하면 된다.
정상적으로 Volume이 생성되었는지 확인하려면 아래와 같이 실행한다.
skanto@skanto:~$ docker volume ls
DRIVER VOLUME NAME
local buildx_buildkit_container0_state
local myvol
Network에서 확인했던 것처럼 상세 내용을 확인하려면 아래와 같이 inspect 명령을 활용한다.
skanto@skanto:~$ docker volume inspect myvol
[
{
"CreatedAt": "2024-12-22T06:46:56Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/myvol/_data",
"Name": "myvol",
"Options": null,
"Scope": "local"
}
]
지금까지 비교적 간단하게 Network과 Volume을 생성했다. 이렇게 생성한 Network과 Volume이 어떻게 활용되는지 궁금할 것이다. 지금은 단순 생성한 것에 불과하고 이렇게 생성한 리소스들을 실제 활용하려면 Container 인스턴스를 실행해야 한다. 그럼 이제부터 데이터베이스와 WAS를 각각 컨테이너로 실행해 보도록 하자.
PostGIS Database 및 데이터 로딩
경로탐색 알고리즘 동작에서 가장 기본이 되는 도로네트워크 데이터를 활용하려면 PostGIS 데이터베이스를 활용하는 것이 이상적이다. 왜냐하면 오픈소스이기도 하지만 Spatial 데이터를 처리하기에 필요한 다양한 함수들을 지원하기 때문인다. 대개 Postgre 데이터베이스를 설치하고 PostGIS Extension을 설치하는 것이 일반적이지만 이 과정을 Container 이미지로 만들어 놓은 것이 있기 때문에 이 이미지를 활용하면 매우 편리하다.
다음과 같이 Docker Registry로부터 PostGIS 데이터베이스 이미지를 가져온다.
skanto@skanto:~$ docker pull postgis/postgis:latest
latest: Pulling from postgis/postgis
69fb10dc82f9: Download complete
6e960f1d1cb2: Download complete
76ff61fca8ac: Download complete
e38f3c7dfafc: Download complete
4f4fb700ef54: Already exists
5a4c03fb0645: Download complete
7c47d8691482: Download complete
895fbad56208: Download complete
d09aa25aff9b: Download complete
94b0cc5cc920: Download complete
54dee37b05f4: Download complete
922ff8346899: Download complete
a90f29aea359: Download complete
20349e765a80: Download complete
c001797204e2: Download complete
150ea94b3a83: Download complete
24d10937fb39: Download complete
2c3d13839904: Download complete
Digest: sha256:b57926d4206ce59474e0041e33290ac17f12f4f586c651eb38f2424cc1f69314
Status: Downloaded newer image for postgis/postgis:latest
docker.io/postgis/postgis:latest
Container 이미지를 정상적으로 가져왔는지 아래와 같이 실행해 보면 확인이 가능하다.
skanto@skanto:~$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
postgis/postgis latest b57926d4206c 6 days ago 885MB
moby/buildkit buildx-stable-1 91b72a6d6963 4 weeks ago 299MB
이제 가져온 데이터베이스 Container 이미지를 실행할 차례이다. Container 이미지를 실행할 때 아래와 같이 앞에서 생성했던 Volume과 Network을 지정한다.
skanto@skanto:~$ docker run --name postgis-1 -v myvol:/var/lib/postgresql/data --network localnet -p 6432:5432 -e POSTGRES_PASSWORD=postgres -d postgis/postgis
14b1af28b73b0e64cc0df7e604f35e41c93210b6ede704a98d386ee73eca07b0
-v 옵션을 이용하여 Volume을 지정하며 이때 콜론(:) 앞 부분은 앞서 생성한 Volume 이름이 되고 뒷 부분은 Mount될 PostGIS 컨테이너의 내부 Path가 된다. 일반적으로 PostgreSQL 데이터베이스가 데이터를 저장하는 Path는 “/var/lib/postgresql/data” 이므로 이 위치를 Volume으로 지정하면 Container가 삭제되더라도 다시 실행 했을 때 기존에 있던 데이터를 그대로 유지할 수 있다. 확인을 해보고자 한다면 아래와 같이 Container에 접속한 수 해당 디렉터리의 내용을 확인해 보면 된다.
skanto@skanto:~$ docker exec -it postgis-1 bash
root@14b1af28b73b:/# cd /var/lib/postgresql/data
root@14b1af28b73b:/var/lib/postgresql/data# ls
base pg_commit_ts pg_hba.conf pg_logical pg_notify pg_serial pg_stat pg_subtrans pg_twophase pg_wal postgresql.auto.conf postmaster.opts
global pg_dynshmem pg_ident.conf pg_multixact pg_replslot pg_snapshots pg_stat_tmp pg_tblspc PG_VERSION pg_xact postgresql.conf postmaster.pid
그리고 –network 옵션을 이용하여 앞서 생성했던 Network의 이름(localnet)을 지정한다. 그리고 PostgreSQL의 접속 port는 일반적으로 5432이지만 여기서는 6432로 지정하여 사용한다.(-p 6432:5432) 그 이외의 옵션들은 PostGIS의 Docker Registry의 PostGIS 설명을 참고하기 바란다.
이제 기동된 데이터베이스로 도로네트워크 데이터를 로딩해 보자. 로딩할 데이터는 기존에 설치된 데이터베이스로부터 백업(network_data.backup)을 받았다고 가정하자. 이렇게 백업 받은 데이터를 컨테이너 데이터베이스에 로딩하는 방법은 아래의 명령어와 같이 비교적 쉽게 처리할 수 있다.
우선 Contaienr 데이터베이스로 접속하여 database를 생성한다. 이때 데이터베이스 이름을 “network”으로 지정한다.
skanto@skanto:~/docker/march$ psql -h localhost -p 6432 -U postgres
Password for user postgres:
psql (17.2 (Ubuntu 17.2-1.pgdg20.04+1))
Type "help" for help.
postgres=# create database network;
CREATE DATABASE
postgres=# \l
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules | Access privileges
------------------+----------+----------+-----------------+------------+------------+--------+-----------+-----------------------
network | postgres | UTF8 | libc | en_US.utf8 | en_US.utf8 | | |
postgres | postgres | UTF8 | libc | en_US.utf8 | en_US.utf8 | | |
template0 | postgres | UTF8 | libc | en_US.utf8 | en_US.utf8 | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | libc | en_US.utf8 | en_US.utf8 | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
template_postgis | postgres | UTF8 | libc | en_US.utf8 | en_US.utf8 | | |
(5 rows)
postgres-# \q
skanto@skanto:~/docker/march$
도로네트워크 데이터를 로딩할 데이터베이스를 생성했다면 이제 백업 받았던 데이터를 Restore한다.
skanto@skanto:~/docker/march$ pg_restore --verbose --clean --no-acl --no-owner --host localhost -p 6432 -U postgres --dbname network network_data.backup
pg_restore: connecting to database for restore
Password:
pg_restore: dropping INDEX net_turn_p_geom_idx
... <omitted>
... <omitted>
이제 데이터가 정상적으로 Restore되었는지 확인하기 위해 network 데이터에 접속하여 생성된 table들을 확인해 보도록 하자.
skanto@skanto:~/docker/march$ psql -h localhost -p 6432 -U postgres
Password for user postgres:
psql (17.2 (Ubuntu 17.2-1.pgdg20.04+1))
Type "help" for help.
postgres=# \c network
You are now connected to database "network" as user "postgres".
network=# \dt v_20240118.*
List of relations
Schema | Name | Type | Owner
------------+-----------------+-------+----------
v_20240118 | net_cninfo | table | postgres
v_20240118 | net_dir | table | postgres
v_20240118 | net_fare | table | postgres
v_20240118 | net_fare_ev | table | postgres
v_20240118 | net_fare_ev_hi | table | postgres
v_20240118 | net_fare_hi | table | postgres
v_20240118 | net_ferry_info | table | postgres
v_20240118 | net_ferry_link | table | postgres
v_20240118 | net_guidept_p | table | postgres
v_20240118 | net_image | table | postgres
v_20240118 | net_lane | table | postgres
v_20240118 | net_link_l | table | postgres
v_20240118 | net_linkin | table | postgres
v_20240118 | net_node_p | table | postgres
v_20240118 | net_oil_link | table | postgres
v_20240118 | net_safety_l | table | postgres
v_20240118 | net_safety_p | table | postgres
v_20240118 | net_safety_pair | table | postgres
v_20240118 | net_scenic_info | table | postgres
v_20240118 | net_scenic_link | table | postgres
v_20240118 | net_svcarea | table | postgres
v_20240118 | net_td_link | table | postgres
v_20240118 | net_td_pass | table | postgres
v_20240118 | net_toll | table | postgres
v_20240118 | net_tolllink | table | postgres
v_20240118 | net_turn_p | table | postgres
(26 rows)
위의 출력 결과와 같이 “network” 데이터베이스 “v_20240118” 스키마에 테이블들이 정상적으로 생성되었음을 확인할 수 있다.
이렇게 비교적 간단한 방법으로 PostGIS 데이터베이스를 기동하고 앞에서 생성했던 Volume과 Network을 지정하였다. 이제 생성했던 Network이 어떻게 동작하는지 확인하기 위해 WAS Container이미지를 가져와 실행해 보도록 하자.
Tomcat Container 실행
먼저 아래와 같이 Docker Registry로부터 Tomcat Image를 가져온다.
skanto@skanto:~/docker/march$ docker pull tomcat:latest
latest: Pulling from library/tomcat
Digest: sha256:935ff51abecc8dc793cb19c229ac7a988c8899e5fcba5e69ae96530fa76c4d56
Status: Downloaded newer image for tomcat:latest
docker.io/library/tomcat:latest
이제 앞서 생성했던 Network을 이용하여 Tomcat 컨테이너를 실행해 보자.
skanto@skanto:~/docker/march$ docker run --name tomcat --network localnet -d -p 8080:8080 tomcat
c593239c8da20b867f93ea56088c6f1a67677525355c0e5f2d9db37e44272519
실행한 Tomcat이 정상적으로 기동되었는지는 아래와 같이 확인할 수 있다.
skanto@skanto:~/docker/march$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c593239c8da2 tomcat "catalina.sh run" 36 seconds ago Up 35 seconds 0.0.0.0:8080->8080/tcp tomcat
14b1af28b73b postgis/postgis "docker-entrypoint.s…" 2 hours ago Up 2 hours 0.0.0.0:6432->5432/tcp postgis-1
1963f757e8fc moby/buildkit:buildx-stable-1 "buildkitd --allow-i…" 3 weeks ago Up 3 days buildx_buildkit_container0
그리고 웹 브라우저를 열어 http://localhost:8080 으로 접속하면 아래 화면과 같이 출력됨을 확인할 수 있다. 참고로, 404오류 페이지가 출력되지만 Tomcat 서버에는 실행되는 WebApp이 없는 상태여서 오류페이지가 출력되는 것이므로 정상 동작이다.

이제 Tomcat Container에 접속하여 PostGIS Container로 네트워크 접속이 가능한지 확인해 보자. 여기서는 ping 명령어를 이용하여 서로 연결이 되는지 확인해 보도록 한다.
실행 중인 Tomcat Container에 접속하여 ping을 실행한다. 이때 PostGIS의 IP주소를 사용하지 않고 Container의 이름을 이용하여 접속한다. 참고로, Container Bridge Network은 자체적으로 Name Service를 유지하고 있어 컨테이너의 이름만으로도 해당 컨테이너로 쉽게 접속할 수 있다.
skanto@skanto:~/docker/march$ docker exec -it tomcat bash
root@c593239c8da2:/usr/local/tomcat# ping postgis-1
bash: ping: command not found
하지만, 위에서와 같이 ping 명령어를 찾을 수 없다는 오류가 출력될 것이다. 그럼 apt-get update를 이용하여 package를 업데이트 한 다음 아래와 같이 ping utility를 설치해 준다.
root@c593239c8da2:/usr/local/tomcat# apt-get update
Get:1 http://archive.ubuntu.com/ubuntu noble InRelease [256 kB]
Get:2 http://security.ubuntu.com/ubuntu noble-security InRelease [126 kB]
... <omitted>
... <omitted>
root@c593239c8da2:/usr/local/tomcat# apt-get install -y iputils-ping
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be installed:
libcap2-bin libpam-cap
The following NEW packages will be installed:
iputils-ping libcap2-bin libpam-cap
... <omitted>
... <omitted>
이제 다시 ping 명령어를 이용하여 PostGIS 컨테이너와 통신이 가능한지 확인해 보면 정상 동작됨을 확인할 수 있을 것이다.
root@c593239c8da2:/usr/local/tomcat# ping postgis-1
PING postgis-1 (172.19.0.2) 56(84) bytes of data.
64 bytes from postgis-1.localnet (172.19.0.2): icmp_seq=1 ttl=64 time=0.367 ms
64 bytes from postgis-1.localnet (172.19.0.2): icmp_seq=2 ttl=64 time=0.105 ms
64 bytes from postgis-1.localnet (172.19.0.2): icmp_seq=3 ttl=64 time=0.101 ms
64 bytes from postgis-1.localnet (172.19.0.2): icmp_seq=4 ttl=64 time=0.103 ms
64 bytes from postgis-1.localnet (172.19.0.2): icmp_seq=5 ttl=64 time=0.105 ms
...
이제 시나리오 구현을 위한 서버(WAS, DB) 인프라 구성은 완료되었으므로 다음으로 WebApp을 포함하는 WAS 컨테이너 이미지를 제작해 보도록 하자.
WebApp Container 이미지 제작
앞에서 Docker Registry에서 받은 Tomcat 이미지 위에 경로탐색 실행을 위한 RESTful API Endpoint와 그 결과를 시각화하는 WebApp을 빌드하고 이를 Container이미지로 Build한다.
WebApp은 Gitlab에 있는 소스코드를 Clone해서 Gradle을 이용하여 실시간으로 빌드한다. Gitlab 프로젝트는 아래와 같은 구조로 구성되어 있다.

참고로, 각 프로젝트는 아래와 같이 구성된다.
- march 프로젝트
경로탐색 알고리즘(Dijkstra, Bi-Directional Dijkstra, Contraction Hierarchy)을 구현하며 Core Library 성격의 프로젝트이다. 이 프로젝트는 Jar파일로 빌드되며 model, route 프로젝트의 공통 라이브러리로 활용된다. - model 프로젝트
WebApp프로젝트로 데이터 모델을 구성한다. 데이터베이스로부터 도로네트워크 데이터를 읽은 다음 메모리상에 도로네트워크 그래프 모델을 생성한다. - route 프로젝트
WebApp프로젝트로 클라이언트가 요청한 경로탐색 요청을 받아 경로탐색을 수행하고 그 결과를 전달한다. 또한 웹앱 UI도 이 프로젝트에서 서비스 된다.
각 WebApp 프로젝트별 Context Parameter 설정 및 Tomcat 서버 환경 설정은 또 다른 주제여서 여기서는 생략하도록 한다. 관심이 있다면 각 Gitlab의 프로젝트별 Tomcat context 설정파일(model.xml, ROOT.xml)을 참고하기 바란다.
최종 Tomcat 이미지로 빌드하기 위해 Gradle 설치, 소소코드 Clone, JAR파일 빌드, WAR파일 빌드, Tomcat 서버로 Deploy 과정을 거쳐야 하므로 Multi-Stage Build방식으로 진행한다.
먼저 Gradle Container 이미지를 가져온 다음 Gitlab Repository로부터 소스코드를 Clone하고 Gradle을 이용하여 각 프로젝트별 WAR파일을 빌드한다.
이후 Tomcat Container 이미지를 가져오고 이 이미지에 앞서 빌드한 WAR파일을 Deploy하고 최종적으로 WAR파일을 포함하는 Tomcat Container 이미지로 빌드한다.
이런 일련의 과정을 Dockerfile 에 기술할 수 있으며로 아래와 같다.
FROM gradle:jdk18-alpine AS build
# Current workding dir: /home/gradle
RUN apk add --update git
RUN git clone https://10132999:PASSWORD@gitlab.dspace.kt.co.kr/GISRPMM/march.git
WORKDIR /home/gradle/march
RUN gradle war
# Base image
FROM tomcat:latest AS server
# Current workding dir: /usr/local/tomcat
# Copy jars to tomcat lib directory(march-core-1.0.0.jar, postgresql-42.7.2.jar)
COPY --from=build /home/gradle/march/march/build/libs/*.jar lib
COPY --from=build /home/gradle/march/march/lib/*.jar lib
# Copy environment configurations including classpath
#COPY ./setenv.sh bin
COPY --from=build /home/gradle/march/setenv.sh bin
# Copy Web Applicatoin(ROOT)(ROOT.war, model.war)
#COPY ./*.war webapps
COPY --from=build /home/gradle/march/model/build/libs/model.war webapps
COPY --from=build /home/gradle/march/route/build/libs/ROOT.war webapps
# Configure Web Application Contexts(ROOT.xml, model.xml)
RUN mkdir -p conf/Catalina/localhost
COPY --from=build /home/gradle/march/route/ROOT.xml conf/Catalina/localhost
COPY --from=build /home/gradle/march/model/model.xml conf/Catalina/localhost
Dockerfile이 있는 디렉터리에서 docker build 명령어를 수행하면 아래와 같이 Container 이미지가 정상적으로 Build되는 것을 볼 수 있다. 이때 tag는 march:latest로 지정했다.
skanto@skanto:~/docker/march$ docker build -t march:latest .
[+] Building 95.9s (21/21) FINISHED docker:desktop-linux
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 1.53kB 0.0s
=> [internal] load metadata for docker.io/library/tomcat:latest 3.4s
=> [internal] load metadata for docker.io/library/gradle:jdk18-alpine 3.7s
=> [auth] library/gradle:pull token for registry-1.docker.io 0.0s
=> [auth] library/tomcat:pull token for registry-1.docker.io 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [server 1/9] FROM docker.io/library/tomcat:latest@sha256:935ff51abecc8dc793cb19c229ac7a988c8899e5fcba5e69ae96530fa76c4d56 0.3s
=> => resolve docker.io/library/tomcat:latest@sha256:935ff51abecc8dc793cb19c229ac7a988c8899e5fcba5e69ae96530fa76c4d56 0.0s
... <omitted>
=> [server 2/9] COPY --from=build /home/gradle/march/march/build/libs/*.jar lib 0.0s
=> [server 3/9] COPY --from=build /home/gradle/march/march/lib/*.jar lib 0.0s
=> [server 4/9] COPY --from=build /home/gradle/march/setenv.sh bin 0.0s
=> [server 5/9] COPY --from=build /home/gradle/march/model/build/libs/model.war webapps 0.0s
=> [server 6/9] COPY --from=build /home/gradle/march/route/build/libs/ROOT.war webapps 0.0s
=> [server 7/9] RUN mkdir -p conf/Catalina/localhost 0.2s
=> [server 8/9] COPY --from=build /home/gradle/march/route/ROOT.xml conf/Catalina/localhost 0.0s
=> [server 9/9] COPY --from=build /home/gradle/march/model/model.xml conf/Catalina/localhost 0.0s
=> exporting to image 0.6s
=> => exporting layers 0.3s
=> => exporting manifest sha256:e826e99b9f52f3bbbd13c3c96620265d934ff66c757e3a48f2a4fa67a2db7e8a 0.0s
=> => exporting config sha256:fe2b53751c3d83289ab7c89b667a4553c7b0c97b621525874c30f7e2cf6272e9 0.0s
=> => exporting attestation manifest sha256:97beb23724e581e04b889450e30ae54439cebdc422958832c6aa542b6300bc6d 0.0s
=> => exporting manifest list sha256:f0ec7957f95b0585dd3f6187446c820048302eed7729acbe518e816351b7ad07 0.0s
=> => naming to moby-dangling@sha256:f0ec7957f95b0585dd3f6187446c820048302eed7729acbe518e816351b7ad07 0.0s
=> => unpacking to moby-dangling@sha256:f0ec7957f95b0585dd3f6187446c820048302eed7729acbe518e816351b7ad07
생성된 Container 이미지는 아래와 같이 확인할 수 있다.
skanto@skanto:~/docker/march$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
march latest 6541378e7005 2 minutes ago 715MB
postgis/postgis latest b57926d4206c 7 days ago 885MB
tomcat latest 935ff51abecc 13 days ago 710MB
moby/buildkit buildx-stable-1 91b72a6d6963 4 weeks ago 299MB
참고로, 초기 Tomcat 이미지 보다 새로 생성한 Tomcat 이미지(march)가 5MB 더 커진 것을 확인할 수 있다. 또한 이미지가 생성된 과정을 확인하려면 아래와 같이 history명령을 활용하면 된다.
skanto@skanto:~/docker/march$ docker history march
IMAGE CREATED CREATED BY SIZE COMMENT
6541378e7005 8 minutes ago COPY /home/gradle/march/model/model.xml conf… 32.8kB buildkit.dockerfile.v0
<missing> 8 minutes ago COPY /home/gradle/march/route/ROOT.xml conf/… 32.8kB buildkit.dockerfile.v0
<missing> 8 minutes ago RUN /bin/sh -c mkdir -p conf/Catalina/localh… 28.7kB buildkit.dockerfile.v0
<missing> 8 minutes ago COPY /home/gradle/march/route/build/libs/ROO… 233kB buildkit.dockerfile.v0
<missing> 8 minutes ago COPY /home/gradle/march/model/build/libs/mod… 1.24MB buildkit.dockerfile.v0
<missing> 8 minutes ago COPY /home/gradle/march/setenv.sh bin # buil… 24.6kB buildkit.dockerfile.v0
<missing> 8 minutes ago COPY /home/gradle/march/march/lib/*.jar lib … 1.11MB buildkit.dockerfile.v0
<missing> 8 minutes ago COPY /home/gradle/march/march/build/libs/*.j… 127kB buildkit.dockerfile.v0
<missing> 13 days ago CMD ["catalina.sh" "run"] 0B buildkit.dockerfile.v0
... <omitted>
<missing> 2 months ago /bin/sh -c #(nop) ADD file:bcebbf0fddcba5b86… 87.6MB
<missing> 2 months ago /bin/sh -c #(nop) LABEL org.opencontainers.… 0B
<missing> 2 months ago /bin/sh -c #(nop) LABEL org.opencontainers.… 0B
<missing> 2 months ago /bin/sh -c #(nop) ARG LAUNCHPAD_BUILD_ARCH 0B
<missing> 2 months ago /bin/sh -c #(nop) ARG RELEASE 0B
이렇게 하면 경로탐색 시각화 시나리오 동작에 필요한 데이터베이스와 WebApp이 추가된 Tomcat 컨테이너 이미지 빌드가 완성된다. Bridge Driver를 이용한 Network 생성으로 Single Host내에서의 Container간 통신을 가능하도록 하고 Volume을 생성하여 데이터베이스에서 생성한 데이터를 Container 외부 저장공간에 Mount히고 저장함으로써 Container삭제 후 다시 생성하더라도 이전에 데이터를 복구할 수 있는 방법을 확인해 보았다.
마지막으로 한가지 덧붙이면, Container 기술 활용 중 마주할 가능성이 높은 부분으로 로컬 시스템에 있는 파일을 생성한 Volume에 복사하는 것을 생각해 볼 수 있다. 경로탐색 알고리즘은 도로네트워크 데이터를 그래프 모델을 메모리상에 만들고 이를 기반으로 경로탐색을 한다. 따라서 도로네트워크 데이터가 크면 클수록 데이터베이스에서 데이터를 읽어 그래프모델을 구성하데 시간이 많이 소요된다. 그래서 한 번 만들어진 그래프 모델을 파일로 저장한 다음 파일에서 바로 읽어 모델을 복원하면 매 번 모델을 새로 구축하지 않아도 되고 로딩 시간을 많이 단축시킬 수 있다. 이미 생성해 둔 그래프모델 데이터가 있다면 이를 컨테이너 내부에서 활용 할 수 있도록 Volume을 하나 만들고 이 Volume에 데이터를 복사하는 과정을 수행해 본다.
먼저 데이터 저장을 위한 Volume 객체를 아래와 같이 생성한다.
skanto@skanto:~/docker/march$ docker volume create mydata
mydata
다음으로 Ubuntu Container 이미지를 받은 다음 interactive mode(-it)로 실행한다. 이때 앞서 생성한 Volume 객체(mydata)를 Container내부의 /data 디렉터리로 마운트 한다.(-v mydata:/data)
skanto@skanto:~/docker/march$ docker run --name ubuntu -it -v mydata:/data ubuntu
Unable to find image 'ubuntu:latest' locally
latest: Pulling from library/ubuntu
Digest: sha256:80dd3c3b9c6cecb9f1667e9290b3bc61b78c2678c02cbdae5f0fea92cc6734ab
Status: Downloaded newer image for ubuntu:latest
root@87532cf8447d:/#
이제 Host에서 다른 터미널을 하나 열어 로컬의 파일을 Ubuntu Container의 /data 디렉터리로 복사한다.
kanto@skanto:~/docker/march$ docker cp ./graph.dat ubuntu:/data
Successfully copied 1.01GB to ubuntu:/data
cp 명령을 활용하면 로컬에 있는 파일을 명시한 Volume으로 복사할 수 있으며 아래와 같이 Ubuntu Container의 콘솔에서 지정된 Volume에(/data로 마운드됨) 파일이 정상적으로 복사 되었는지 확인할 수 있다. 또한 ubuntu Container를 삭제하고 다시 인스턴스를 실행해도 파일이 계속 유지됨을 확인할 수 있다.
root@87532cf8447d:/# cd /data
root@87532cf8447d:/data# ls -l
total 988760
-rw-rw-r-- 1 ubuntu ubuntu 1012482493 Aug 16 00:54 graph.dat
Docker Compose 를 활용한 Multi Container Build
지금까지는 각각의 Container를 개별적으로 실행하고 빌드했다. 이제 개별적을 수행한 빌드/실행 작업을 하나의 스크립트 파일로 정의할 수 있다. Docker Compose를 이용하면 여러개의 컨테이너 생성 및 빌드 작업을 하나의 순차적 Task로 정의할 수 있다. 쉽게 생각하면 Gradle 파일을 이용하여 여러 개의 Task를 만들고 각각 순차적으로 실행하는 개념과 비슷하다고 보면 된다.
Compose 파일은 크게 services, networks, volumes 구성되며 networks와 volumes는 앞에서 살펴본 것과 동일한 개념으로 생각하면 된다. 그리고 services는 Microservice 를 생성하는 개념으로 개별 서비스 컨테이너를 빌드, 실행하는 역할을 한다. 개별 코드 Block과 활용 방법은 관련 자료를 참고하기 바란다.
굳이 이와 관련된 메타포를 찾는 다면 집을 짓는 것과 비유할 수 있을 것이다. 집 짓기 전에 터를 닥고 인프라 시설을 먼저 갖추어야 한다. 즉 수도(Volume)을 깔고 통신선(Network)을 먼저 끌어와야 한다. 그 다음 집을 짓는데 이 집을 Container로 생각하면 무리가 없을 것이다.
지금까지 설명한 내용을 하나의 Compose파일(compose.yaml)로 만들면 아래와 같이 정의할 수 있다.
networks:
localnet:
driver: bridge
volumes:
postgis-data:
network-data:
services:
tomcat:
build: .
deploy:
replicas: 1
ports:
- target: 8080
published: 8080
networks:
- localnet
volumes:
- type: volume
source: network-data
target: /usr/local/tomcat/network-data
postgis:
image: postgis/postgis:latest
deploy:
replicas: 1
ports:
- target: 5432
published: 6432
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
networks:
localnet:
volumes:
- type: volume
source: postgis-data
target: /var/lib/postgresql/data
자 이렇게 작성된 compose.yaml 파일을 docker compose up 명령어로 실행하면 한 번에 모든 컨테이너 이미지를 빌드하고 실행할 수 있다.
skanto@skanto:~/docker/march$ docker compose up &
[6] 168513
[+] Running 19/19ocker/march$
✔ postgis Pulled 28.8s
✔ 3253e6899ccd Download complete 1.3s
✔ 69fb10dc82f9 Download complete 10.1s
✔ 76ff61fca8ac Download complete 1.2s
✔ e38f3c7dfafc Download complete 1.3s
✔ 895fbad56208 Download complete 2.2s
✔ 5a4c03fb0645 Download complete 1.2s
✔ fa54e3793199 Download complete 20.4s
✔ 922ff8346899 Download complete 1.3s
✔ 54dee37b05f4 Download complete 1.3s
✔ 2c3d13839904 Download complete 1.3s
✔ 4f4fb700ef54 Download complete 1.2s
✔ 150ea94b3a83 Download complete 1.2s
✔ 20349e765a80 Download complete 4.1s
✔ c001797204e2 Download complete 1.3s
✔ 24d10937fb39 Download complete 2.2s
✔ d09aa25aff9b Download complete 1.2s
✔ a90f29aea359 Download complete 21.8s
✔ 658061ad5816 Download complete 1.3s
[+] Building 8.5s (23/23) FINISHED docker-container:container
=> [tomcat internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 1.53kB 0.0s
=> [tomcat internal] load metadata for docker.io/library/tomcat:late 2.1s
=> [tomcat internal] load metadata for docker.io/library/gradle:jdk1 2.2s
=> [tomcat auth] library/tomcat:pull token for registry-1.docker.io 0.0s
=> [tomcat auth] library/gradle:pull token for registry-1.docker.io 0.0s
=> [tomcat internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [tomcat server 1/9] FROM docker.io/library/tomcat:latest@sha256:9 0.0s
=> => resolve docker.io/library/tomcat:latest@sha256:935ff51abecc8dc 0.0s
=> [tomcat build 1/5] FROM docker.io/library/gradle:jdk18-alpine@sha 0.0s
=> => resolve docker.io/library/gradle:jdk18-alpine@sha256:4d3a06b38 0.0s
=> [tomcat build 2/5] RUN apk add --update git 0.0s
=> [tomcat build 3/5] RUN git clone https://10132999:PASSWORD@gitl 110.1s
=> [tomcat build 4/5] WORKDIR /home/gradle/march 0.0s
=> [tomcat build 5/5] RUN gradle war 10.0s
=> [tomcat server 2/9] COPY --from=build /home/gradle/march/march/bu 0.0s
=> [tomcat server 3/9] COPY --from=build /home/gradle/march/march/li 0.0s
=> [tomcat server 4/9] COPY --from=build /home/gradle/march/seten.sh 0.0s
=> [tomcat server 5/9] COPY --from=build /home/gradle/march/model/bu 0.0s
=> [tomcat server 6/9] COPY --from=build /home/gradle/march/route/bu 0.0s
=> [tomcat server 7/9] RUN mkdir -p conf/Catalina/localhost 0.1s
=> [tomcat server 8/9] COPY --from=build /home/gradle/march/route/RO 0.0s
=> [tomcat server 9/9] COPY --from=build /home/gradle/march/model/mo 0.0s
=> [tomcat] exporting to oci image format 6.2s
=> => exporting layers 0.0s
=> => exporting manifest sha256:fa33cca566f87d14995d5e6521121f67ee0a 0.0s
=> => exporting config sha256:6273f4ebe6db3e0cea22d10997dbbcb1c2f5f6 0.0s
=> => sending tarball 6.2s
=> [tomcat] importing to docker 0.0s
=> [tomcat] resolving provenance for metadata file 0.0s
[+] Running 3/3
✔ Network march_localnet Created 0.0s
✔ Container march-tomcat-1 Created 1.1s
✔ Container march-postgis-1 Created 1.1s
이제 다음의 명령어를 이용하여 network, volume과 container들이 정상적으로 생성되었는지 확인해 보자.
skanto@skanto:~/docker/march$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
march-tomcat latest 73b290831ea0 About an hour ago 715MB
postgis/postgis latest 361a43880e74 13 hours ago 885MB
moby/buildkit buildx-stable-1 91b72a6d6963 4 weeks ago 299MB
skanto@skanto:~/docker/march$ docker network ls
NETWORK ID NAME DRIVER SCOPE
7bdaed379afc bridge bridge local
61aafbfc8573 host host local
4aa14067a7b4 march_localnet bridge local
e173d1a8fcbb none null local
skanto@skanto:~/docker/march$ docker volume ls
DRIVER VOLUME NAME
local buildx_buildkit_container0_state
local march_network-data
local march_postgis-data
skanto@skanto:~/docker/march$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f8ab6bd06476 postgis/postgis:latest "docker-entrypoint.s…" About a minute ago Created march-postgis-1
aaa8f08905e9 march-tomcat "catalina.sh run" About a minute ago Created march-tomcat-1
1963f757e8fc moby/buildkit:buildx-stable-1 "buildkitd --allow-i…" 3 weeks ago Up 4 days buildx_buildkit_container0
마지막으로 생성된 각각의 container를 아래와 같이 실행한다.
skanto@skanto:~/docker/march$ docker restart march-postgis-1
march-postgis-1
skanto@skanto:~/docker/march$ docker restart march-tomcat-1
march-tomcat-1
WebApp 로딩이 완료될 때까지 시간이 걸릴 수 있으니 진행되는 상태를 로그로 확인해 보고 싶다면 아래와 같이 docker log 명령어를 이용하여 확인할 수 있다.
skanto@skanto:~/docker/march$ docker logs -f march-tomcat-1
25-Dec-2024 02:16:10.019 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version name: Apache Tomcat/11.0.2
... <omitted>
... <omitted>
Now loading nodes .... done: 3,017,915
Now loading links ....... done: 6,434,167
Now loading connections ............ done: 11,304,641
Graph loading DONE... elpased: 00:00:43
Calculates transient time of every links...
Graph is bound to ServletContext(/model): march.model.graph
Now loads RTree from the loaded Graph model...
RTree loading DONE!
RTree is bound to ServletContext(/model): march.model.rtree
Network link information is loading...
Links loaded: 1,000,000
Links loaded: 2,000,000
Links loaded: 3,000,000
Link loading DONE: 3,698,757
Links are bound to ServletContext(/model): march.model.links
... <omitted>
... <omitted>
25-Dec-2024 02:17:18.377 INFO [main] org.apache.coyote.AbstractProtocol.start Starting ProtocolHandler ["http-nio-8080"]
25-Dec-2024 02:17:18.385 INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [68113] milliseconds
로딩이 완료되었다면 웹브라우저를 열고 http://localhost:8080로 접속한다. 앞에서 Tomcat Container만 실행했을 때 404오류가 출력됐지만 이제는 아래의 화면과 같이 경로탐색 결과를 시각화하는 WebApp이 실행되는 것을 확인할 수 있다.

마치며…
웹 개발의 가장 흔한 Web-WAS-DB 구조에서 데이터베이스와 WebApp을 실행하는 WAS를 컨테이너 이미지로 빌드하고 실행해 보았다. 그 과정에서 인프라 성격에 해당하는 Network과 Volume을 활용하는 방법에 대해서도 살펴보았다. 컨테이너의 장점으로 꼽자면 Microservice를 쉽게 구축할 수 있고 향후 유지보수, 테스트, 배포가 용이하다는 것이다. 나아가 Cloud 환경을 준비하는 경우라면 컨테이너 기술은 필수 요소기술이라 할 수 있을 것이다.
이 글에서 살펴봤듯이 GitHub, Gitlab과 같은 Source Repository에서 Stable한 소스를 실시간으로 가져와 Binary 로 빌드하고 최종 컨테이너 이미지로 제작하였다. 이렇게 제작한 컨테이너는 하나의 Microservice로 간편하게 실행할 수 있다는 것도 확인하였다. 좀 더 나아가 빌드한 컨테이너를 Registry로 등록하면 CI 뿐만 아니라 CD(Continuous Deploy)까지도 원활하게 처리할 수 있을 것이다.
지금까지 클라우드 환경에서 가장 핵심이 되는 Container 기술의 활용방법을 시나리오를 기반으로 살펴 보았다. 높은 관점에서 컨테이너 기술을 활용, 이해하는데 도움이 되었다면 이런 개념을 머리에 넣고 추가적인 세부기술을 접해 나가면 길을 헤메지 않고 원하는 목표에 도달할 것으로 기대한다.