제목 : docker 는 어떻게 운용되며 실행할까? (Dockerfile + CLI)
부제 : Spring 프로젝트와 Nginx 를 섞어보자
이 글을 작성하는 이유
docker 는 Virtual Machine 의 선택된 가상 OS 가 무겁기 때문에
"가벼운 컨테이너" 라는 캐치 프레이즈로 널리 쓰이고 있는 프로그램이다.
Singleton 서버에서 마이크로서비스의 시대를 활짝 열어버린 주역이라고 할 수 있다.
특히, 각 기능을 담당하는 특화 서버들 간의 통신 속도,
무거운 Guest OS 를 서버마다 설치 할 필요 없이,
Docker 만의 OS 수준 가상화를 통해 가벼운 개별 실행을 보장한다.
나는 클라우드에 내가 만든 프로젝트 코드를 올려서, 실제 상호작용이 가능한 서버를 만들려고 한다.
그런데, 여기서 제약이 생긴다. 나는 이미 AWS, GCP 에서 의도치 않은 큰 비용이 청구 된 적이 있으며,
또한 이유를 참작 받아 탕감받았다.
또한, 단순 1년 무료라는 이벤트성 웹 서비스를 사용하기에는
생각보다 1년 무료는 그리 긴 시간이 아니다.
따라서, 나는 Oracle 에서 제공하는 평생 무료의 제한된 Computing Resource 를 가지고,
마치 귀중한 식재료를 얇게 발라먹듯 사용 할 것이다.
위의 상황은 어떤 것이냐면,
인스턴스 1 개에 1 개의 프로젝트를 넣을 수 있는 넉넉한 상황이 아니라,
인스턴스 1 개에 허용될 수 있는 적당한 선에서 메모리를 아껴가며
프로젝트들을 책꽂이에 넣듯 사용해야 하는 상황이다.
컴퓨팅 리소스가 아까운 지금의 상황에서, docker 에 대한
기초 이론적인 지식 뿐만 아니라, 실전적인 예제까지 봐 가며
어떻게 한정된 리소스 속에서 캐싱, API 서버, 데이터베이스, 등등을
격리시켜 메모리를 최적화 할 수 있는지 알아보려 한다.
따라서, 이번 글은
- Docker 의 간단한 기초 지식
- Docker 내부의 여러 프로세스의 역할 분석
- 아주 간단한 Spring 서버를 Docker 로 띄우기
- nginx 컨테이너를 띄워 Spring 서버에 요청 가도록 만들기
를 한다.
나머지 단계는 이후 글로 작성할 것인데,
- Cloud Server 에 Docker 설치 및 확인하기
- Cloud Server 의 docker container 로 Spring 띄우고 메모리 확인하기
- 이를 위해 Github Actions 를 사용하기
- Cloud SSH Remote 를 Github Actions 서버에서 실행하기
- 등등 주제가 많다..
Cloud 부터 작성될 글의 양을 고려하여 포스팅을 나눌 것이다.
Docker 란 무엇인가?
위에서 도커에 대한 아주 간략한 정보에 이어,
Docker 를 요약하자면, 현재 컴퓨터 세상에 존재하는 다양한 목적의 프로그램들을
개별 가상 컨테이너로 가볍게 분리 해 주는 프로그램이다.
Infrastructure(인프라스트럭쳐) 라는 단어가 흔히 사용 될 정도로,
개별 인프라들은 특정 서비스나 목적을 위해 계층을 이루거나, 서로를 의존한다.
이제는 단일 프로젝트에서 Spring 만을 사용하는 것이 아니라,
Redis, Kafka, RabbitMQ, Nginx, 등등을 사용한다.
캐싱 뿐만 아니라, 요청 분산, 심지어는 데이터베이스도 가상화 시켜 데이터를 저장 할 수 있다.
이러한 프로그램들을 물론 Local Server 에 직접 설치하여 Port 로 접근 할 수도 있겠지만,
이는 추후 유지보수와 확장성 면에서 심각한 부작용을 보일 수 있다.
바로, 어떠한 형태가 되었든 최신 버전으로의 이전이 굉장히 번거로우며,
현재 사용 진행중인 컴퓨팅 리소스에 대한 확장과 축소 대응이 어렵다.
인프라를 추가하거나 제거함에 따라 추가적인 문제들이 정말 많을텐데,
이러한 문제와 번거로움을 정해진 "설정 파일" 로 "자동화" 시킨 것이 Docker 이다.
개인적으로 느꼈던 생각은,
도커는 인프라 계의 프레임워크라고 불러도 괜찮을 만큼,
설정 파일을 통해 CI/CD 파이프라인을 해결 할 수 있다.
프레임워크라고 불러도 괜찮을 만큼, 배워야 하는 것 또한 비슷하다고 생각한다.
Docker 에 존재하는 다양한 형태의 이미지들
도커 레지스트리(Registry) 에는 내가 필요로 하는 이미지들이 대부분 존재하는데,
예를 들면 우리가 직접 Nginx 를 컨테이너화 시키기 위해
Nginx 파일을 특정 폴더에 생성하고, 커스텀하고... 하는 것이 아니라
이미 존재하는 Nginx 이미지를 docker image 로 다운로드 받고,
Official Nginx 이미지를 컨테이너로 가동시킨 후,
- 컨테이너 내부에 들어가서 직접 파일을 조정한다. OR
- 내 로컬 컴퓨터에 컨테이너 서버와 특정 로컬 위치를 "Mount" 시킨 후 조정한다.
가볍게 경량화 시킨 이미지 내부에 VIM 같은 에디터가 내장 될 리는 없으니,
VI 에디터를 잘 사용한다면 1 번을 통해 조정해도 상관 없다.
이미 최적화 되어 있는 공식 이미지들
나중에 Dockerfile 을 작성하면 알게 되겠지만,
도커 컨테이너 실행에 최적화 되어 있는 이미지들이 준비중이다.
데이터베이스와 같은 이미지 뿐만 아니라,
NodeJS 기반 엔진이라면, node 이미지를,
Java 기반 엔진이라면, java 이미지를 기반으로 생성한다.
물론, C, C++ 와 같이 Binary 수준의 결과물만 나오는 프로그램과는 다르게,
Node 즉, JavaScript 진영은 NodeJS 엔진을 통한 실행을 필요로 하며,
Java 기반의 Spring 프레임워크 서버는 JRE 엔진이 존재하는 이미지를 필요로 한다.
이를 우리가 항상 느끼지 않는 것은, 로컬 컴퓨터에 개발을 위한 편리한 프로그램들이 깔려 있는 것과 더불어,
현대적인 에디터가 실행과 중단을 직접 도맡아 하는 경우가 많기 때문이다.
수많은 공식 이미지들이 존재하는 것을 넘어, 도커는 Customize 이미지를 쉽게 만들 수 있게 지원한다.
위에서 말했듯, Interactive Terminal 을 통한 제어와, Local Mount 를 통한 제어가 있다.
클라우드에서 더욱 빛을 발하는 image
도커 없이도 당연하게 제작한 프로젝트 실행이 가능하다.
그러나, 이 프로젝트 실행을 위해 설정해야 할 환경 조작에 대한 번거로움을 고려 해 보자.
사실 번거로운 수준을 넘어 설치 지옥에 빠질 가능성도 있다.
Spring 을 위한 환경과 MySQL 만을 설치한다고 가정해도, 꽤 귀찮은 시나리오가 떠오를 정도이다.
정말 바이너리 수준의 즉시 실행 가능한 프로그램 수준이 아니라면,
프로그램 실행을 위한 기반을 무조건 마련해야 한다.
우리가 로컬 컴퓨터에서 쉽게 프로젝트를 실행하고 관찰하고, 수정하는 것은
위에서 말했듯 현대적인 에디터의 훌륭한 디버깅 기능 뿐만 아니라,
GUI(Graphic User Interface) 덕분이다.
만약에 가상화, (Docker, or VM) 을 하지 않는다면,
우리가 필요로 하는 모든 기반 의존성 프로그램에 대한 공식 사이트에 들어가서,
일일이 파일을 요청 다운로드(curl) 해야 할 수도 있다.
그러나, Docker 를 사용한다면 그 이야기가 달라진다.
Docker 는 우리가 필요로 하는 기반 프로그램, 혹은 응용 프로그램들이
전부 "이미지" 로 다운로드 할 수 있다.
심지어는 내가 커스텀한 이미지를 다운로드 할 수도 있다.
그렇다면, 우리는 다양한 인프라들을 사용함에 있어 OS 에 직접 설치 할 필요가 없으며,
그저 불러와서 docker 엔진에게 컨테이너를 실행하라고 명령하면 될 뿐이다.
특히 경량화 된 이미지를 사용한다는 점에서, 나에게는 도커가 더 매력적인 선택지이다.
도커의 핵심 엘리먼트를 분석 해 보자. - 도커 아키텍쳐
나는 편한 프로그램에 대한 경계를 해 왔다.
그 이유가, 몇 번 적용해 보았다고 해서 그 방식을 이해했다고 말할 수가 없었기 때문이다.
결국 나는 Docker 를 알지만, 그 기반 원리와 구성 요소를 이해하지 못해 다시 공식문서를 보고 있다.
사실 도커를 사용한다는 것 자체가 편리한 프로그램을 사용한다는 점에서 모순적이지만,
언제나 항상 기술적 타협점은 가지고 있어야 한다고 생각한다.
개발자로서 컴퓨터 프로세스를 "직접 제어" 한다는 것은 큰 일을 해내는 것은 맞다.
그러나, 그 직접 제어로 인해 개발 생산력이 떨어진다면, 그건 올바르지 않은 방향일 것이다.
아집은 옳지 않다.
따라서, 나는 Docker 를 통해 infra ochestration 의 추상적 기반을 마련하려고 한다.
Docker Daemon - 도커 데몬
도커 데몬이란, Docker 의 요소인 이미지, 컨테이너, 네트워크, 볼륨(저장소)
이들을 관리하고 다루는 프로그램이다.
도커 데몬은 도커가 실행중인 클라이언트 PC 의 프로세스라고 생각하면 된다.
이후 설명할 Docker Client 에 의한 사용자 요청을 기다리고 있다.
전체적으로 도커 데몬은 도커를 위해 생성된 다른 데몬을 다루기 위해 소통하는데,
클라이언트 PC 에서 실행되는 Docker 프로세스들을 관리하는 핵심 프로세스(데몬) 이다.
Docker client - 도커 클라이언트
보통은 도커 데스크탑 버전을 PC 에 다운로드 받아서 직접 컨테이너, 이미지, 볼륨과 같은 요소들을
GUI 입력 요소로 제어할 수 있지만, Docker client 는 명령어와 밀접하다.
즉, 우리가
docker run ...docker container list ..docker build -t ....
이러한 "명령어" 들을 인식하고 dockerd 에게 전달해 준다.
즉, Docker 의 명령어 API 이다.
물론 명령어를 통해 도커 클라이언트를 사용하고 싶다면,
위에서 언급한 docker daemon 이 필수적이다.
Docker Registries - 도커 레지스트리
우리가 사용할 Nginx 라던지, 데이터베이스, 등등
원하는 이미지들은 "공식 이미지" 로 OpenSource 로 배포되어 있다.
이들을 Docker Hub 에서 태그와 함께 쉽게 가져올 수 있는데,
docker pull <원하는 이미지>:<원하는 버전>, docker run ...
이러한 방식으로 쉽게 가져올 수 있다.
뿐만 아니라, 개인의 Docker Registry 주소도 존재하여,
내가 만든 Custom Image 를 Docker Registry 에 저장하고,
이를 클라우드 환경에서 끌어다 사용할 수 있다.
이는 매우 핵심적인 기능이다.
Docker File - 도커 파일
도커 파일은 우리가 만든 프로젝트를 기반으로 어떤 환경과 환경 변수,
그리고 어떠한 외부 자원과 실행 명령어를 사용 할 것인지 기술하는 파일이다.
파일 확장자나 이름 필요 없이 그냥 Dockerfile 로 기술한다.
물론, Dockerfile-1 과 같은 이름으로 작성 한 뒤,
docker build -t new-app -f ./Dockerfile-1
과 같은 방식으로 도커파일을 지정 할 수는 있다.
그러나, 만약에 에디터의 하이라이팅 및 예시 기능을 사용하기 위해서는
Dockerfile 이라는 정확한 파일을 기술해야 문법적 지원을 받을 수 있다.
따라서, 주로 도커 파일을 제작하는 Convention(관례) 는,
정확한 목적을 가진 이름의 디렉토리를 만든 뒤, 해당 디렉토리에 Dockerfile 을 생성한다.
Dockerfile 만의 문법이 존재하며,
특히 예약어의 경우 대문자로 이루어진다.
FROM: 어떤 이미지를 기반으로 컨테이너를 실행 할 것인지 지정WORKDIR: 결국 컨테이너는 서버를 의미하므로, 이 컨테이너 서버의 "어디에서" 실행 할 것인지 지정.COPY: Docker 환경 속이 아닌 장소에서, 이 이미지의 특정 경로로 파일을 복사하기 위해 사용된다.RUN: 명령어를 실행하는데 사용되며, 의존성을 설치하거나, 필요 디렉토리를 만드는 중간 단계의 명령어를 의미한다.ENV: 이미지가 컨테이너로 실행되면서 내부에서 사용될 "환경 변수" 를 의미한다.ARG: 현재Dockerfile에서 사용될 변수를 선언한다.EXPOSE: 제작하는 이 이미지가 어느 port 로 "노출" 될 것인지를 지정한다.CMD: 컨테이너가 실행 된 뒤, 기본적으로 실행되야 할 필수 명령어를 넣는다.
이 예약어는 하나만 유효하며, 맨 마지막이 선택된다.
이에 대한 자세한 설명은
dockerdocs - Writing a Dockerfile
사이트에서 참조 할 수 있다.
도커로 스프링 '로컬에서' 띄워보기
스프링 공식 사이트에서 친절하게 Docker 로 띄우는 법을 보여주고 있다.
그런데, 여기서 Gradle, Maven 의 방식이 살짝 다르다.
예시 Dockerfile :
FROM openjdk:8-jdk-alpine
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
이 예제에서 이상한 점을 느꼈다.
- 왜
8버전이지? 현재 spring init 은 최소가 Java17버전인데? - 왜
jdk이지? 컨테이너 내부에서 따로 develop 할 게 아닌데?jre가 들어가야 하지 않나?
따라서, 이에 대한 부분을 AI (Gemini) 에게 물어보니,
옛 공식자료이며, "돌아만 가게" 만들었기 때문에 최적화가 안되어 있다고 한다.
따라서, 현재는 eclipse-temurin:17-jre-focal 이라는 이미지에서 가져온다고 한다.
따라서, 위에서 제시한 "공식문서 예제" 는 이렇게 바뀐다.
FROM eclipse-temurin:17-jre-focal
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
도커파일로 스프링 띄워보기
만약에 스프링을 Gradle 로 띄운다면,
위에서 제시한
ARG JAR_FILE=target/*.jar 은 바뀌어야 한다.
위의 경로는 Maven 을 위한 경로이다.
Gradle 을 사용하여 빌드한다면:
FROM eclipse-temurin:17-jre-focal
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
로 작성하여, 굳이 docker build .. 명령어 실행 시
긴 명령어로 실행하지 않도록 방지하자.
현재 테스트의 기반이 될 폴더의 구조는 이러하다 :
➜ core-5 tree -L 2
.
├── HELP.md
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
├── main
└── test
4 directories, 4 files
폴더 구조를 보면, 아직 Maven 빌드 폴더인 ./target 이 없는 것을 볼 수 있다.
# 내장된 mvnw 프로그램을 통해 Maven 전역 프로그램 설치 없이 메이븐 명령이 가능하다.
$ ./mvnw package
...
...
테스팅
...
완료
$
현재 디렉토리 :
➜ core-5 tree -L 2
.
├── HELP.md
├── mvnw
├── mvnw.cmd
├── pom.xml
├── src
│ ├── main
│ └── test
└── target
├── classes
├── generated-sources
├── generated-test-sources
├── maven-archiver
├── maven-status
├── springcore.test-0.0.1-SNAPSHOT.jar # 이 파일을 사용하게 됩니다.
├── springcore.test-0.0.1-SNAPSHOT.jar.original
├── surefire-reports
└── test-classes
12 directories, 6 files
위의 디렉토리를 보면, Docker 이미지에 넣을 springcore.test-0.0.1-SNAPSHOT.jar
이 package을 통해(gradle 은 build) 생성되었음을 알 수 있다.
이제 위에서 선언한 Dockerfile 중 Maven 용을 프로젝트의 루트에 생성한다.
.
├── Dockerfile # 위에서 보였던 Dockerfile Maven 버전을 작성.
├── ...
├── src
│ ├── main
│ └── test
└── target
├── ...
├── springcore.test-0.0.1-SNAPSHOT.jar
➜ core-5 docker build -t test-spring .
[+] Building 0.9s (7/7) FINISHED docker:desktop-linux
=> [internal] load build definition from Dockerfile 0.0s
....
=> => naming to docker.io/library/test-spring 0.0s
View build details: docker-desktop://dashboard/build/desktop-linux/desktop-linux/xgk2h5plnv5rs97goinmqamtm
What's next:
View a summary of image vulnerabilities and recommendations → docker scout quickview
➜ core-5 docker image list
REPOSITORY TAG IMAGE ID CREATED SIZE
test-spring latest ef97619f4b21 3 minutes ago 280MB
hello-world latest ca9905c726f0 3 months ago 5.2kB
... 등등 이전에 만들었던 나의 이미지들
위에서 test-spring 이라는 TAG 로 Spring 프로젝트가 이미지화 된 것을 볼 수 있다.
➜ core-5 docker run -d -p 8080:8080 test-spring
d3a682bcb5cc963593cbf92f80ef334b22acccb47da49382040487aa515238b4
-d: Detached 모드인데, "현재 터미널" 이 실행된test-spring컨테이너로 곧바로 접속하지 않고, Container ID 만 출력하고 명령이 종료됨.-p:<호스트OS 에 노출될 PORT>:<컨테이너 내부에 연결될 PORT>
스프링 프로젝트에서 따로 환경설정으로 PORT 를 바꾸지는 않았으므로, Default 인 8080 으로 설정.
➜ core-5 docker container list
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d3a682bcb5cc test-spring "java -jar app.jar" 21 seconds ago Up 21 seconds 0.0.0.0:8080->8080/tcp, [::]:8080->8080/tcp stoic_faraday
현재 나의 컴퓨터 로컬 환경에서 8080 으로 접근 할 수 있다.
➜ core-5 curl -X GET "http://localhost:8080"
헬로 월드를 도커 내부에서 실행 - 루트 url
➜ core-5 curl -X GET "http://localhost:8080/result"
This is BeanResultSample
내부에는 JSON 반환체가 아닌, text/plain 형태로 반환되고 있다.
스프링 컨테이너 정지하기
➜ core-5 docker container stop d3a
d3a
현재 실행되고 있는 도커 컨테이너 중, d3a 를 종료했다.
d3a 는 무엇일까?
d3a 는 이미지가 "컨테이너화" 되면서 생기는 고유(Unique) 한 해싱 번호의 "전반부 일부" 이다.
현재 가지고 있는 컨테이너가 "중복되지 않는 선에서" 구별될 수 있는
전반부 일부만 입력해도 종료된다.
➜ core-5 docker container list --all
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d3a682bcb5cc test-spring "java -jar app.jar" 7 minutes ago Exited (143) 49 seconds ago stoic_faraday
일반적인 docker ps, docker container list 는,
현재 "실행중인" 컨테이너 리스트를 나타 내 준다.
그러나, 종료되었으나, 사라지지 않는 컨테이너(대기) 도 존재한다.
우리가 docker container rm ... 를 통해 컨테이너를 "삭제" 한 것은 아니고,
단순히 정지시킨 것이기 때문에, --all 이라는 옵션을 붙여서 볼 수 있다.
Nginx 컨테이너를 이용하여 Spring 서버와 리버스 프록시 체결하기
도커를 이용하여 격리된 컨테이너는 PORT(8080) 과 함께 노출되어 접근이 가능하다.
그러나, 프로덕션에 가까운 완성도를 보이기 위해서는, Reverse Proxy 형태를 만들어야 한다.
왜 그럴까?
하나의 서버에 단순한 하나의 프로젝트에 필요한 인프라를 간단히 넣는다면,
컨테이너 당 하나의 PORT 를 할당하여 노출시킬 수도 있다.
그러나 여기서 문제가 발생한다.
나는 클라우드에서, https 통신이 공식적으로 가능하도록 만들 것이다.
정보를 제공하는 "서버"란,
- 정적 정보(페이지) 를 Serving 하는 Front-End 전용 서버나,
- 동적 정보(비즈니스 데이터) 를 전달 해 주는 Back-End 전용 서버나
동일하게 https 가 적용 되어야 한다.
(그렇지 않다면 전달 데이터는 모두 평문으로 노출되기 때문입니다.)
이 과정에서 https 연결을 위한 ssl 인증서, 도메인 이름을 증명할 공간이 필요하다.
내 상황에 Nginx 가 더더욱 필요 한 이유
나는 클라우드 서버 중 "평생 무료" 인스턴스를 제공하는 Oracle 클라우드 서버에서,
한정된 컴퓨팅 리소스를 발라 먹듯이 촘촘히 프로젝트를 넣을 것이다.
동일한 컴퓨터에 동일한 프레임워크를 사용하여 여러 프로젝트를 생성할 수도 있지만,
그 목적들은 다를 것이다.
그렇다면, 외부에서 오는 모든 요청들에 대한 ssl 인증서와 tcp 체결을,
각 프로젝트에서 실행 할 것인가? 그건 코드를 너무 낭비하는 일이다.
내가 http2.0 버전을 언제 적용할 지도 모르는 일이다.
그렇다면 어떤 언어로 작성될 지도 모를 모든 API 프로젝트의 인증서 요구사항을 바꿔야한다는 말과 동일하다.
따라서, 외부(클라이언트)에서 내부(클라우드 인스턴스) 로 요청이 들어 올 때,
모든 인증을 한번에 해결하고, nginx 가 다시 프로젝트들에 요청하는 방식으로 이루어져야 한다.
또한, Nginx 는 들어온 경로들을 다시 변환하여 내부에 스스로 요청 할 수도 있다.
리버스 프록시는, 클라이언트로부터 들어온 요청들을 각각의 API 서버로 분리하고,
혹은 위에서 말했듯 hyper text protocol secure (https) 를 하나의 장소에서 해결하기 위한
특별한 해결사로 사용할 수도 있다.
또한, 중요한 기능으로 외부에서 요청한 특정한 경로가 있다면,
이를 로컬로 다시 요청을 보내기 직전 원하는 경로로 비틀 수도 있다.
- Client :
https://test-domain.com/api/v1/docker-spring - Nginx 변환 :
https://localhost:8080/nginx-for-spring
외부에서 들어온 요청을 Nginx 가 받는데, 또 Nginx --> Nginx 형태가 될 수도 있는 이유는,
Server Scaling 과정에서 여러개로 분산된 spring 컨테이너의 요청들을 관리하는,
그러한 Nginx 가 존재 할 수도 있기 때문이다.
단순하게 1 개의 Docker Container 로 관리한다면, localhost:8080
이런 식으로 작성 할 수도 있다.
또한, Docker 에서 Nginx 내부 설정 파일을 따로 건들지 않고도,
단순 명령어를 통해 docker network 에 종속시켜 여러 컨테이너를 관리 할 수도 있다.
그러나, 나는 결국 nginx 를 Customize 된 Image 화 시켜 만들게 될 것이다.
위의 내용을 요약 해 보자면,
flowchart TB
Client("Client - 프론트엔드")
subgraph Instance["클라우드 인스턴스"]
subgraph Nginx["Nginx"]
nginx-role1("https 요청에 대한 ssl 인증과 tcp 연결 체결 <br/> 및 요청 분산")
end
Nginx-Spring("Nginx - Spring 이미지들에 대한 요청 분산용 <br/> Spring 이미지들에 대한 주소를 가짐")
subgraph Springs["Spring Images"]
Spring1("Spring 복제1")
Spring2("Spring 복제2")
end
subgraph Other-Projects["다른 프로젝트들"]
another-project("또 다른 용도의 API 프로젝트들..")
end
Nginx --http 프로토콜 및 (경로 변환)--> Nginx-Spring
Nginx --http 프로토콜 및 (경로 변환)--> Other-Projects
Nginx-Spring --http--> Springs
end
Client --https 프로토콜--> Nginx
아직 내부에 DB 나 Redis 와 같은 캐싱 장치는 넣지 않았는데,
이는 그래프가 너무 복잡해지고, 설명에 적합하지 않아 넣지 않았다.
Nginx 를 통해 로컬에서 테스팅 해 보자 - with Docker Network
이 과정을 실행하기 전, Docker Network 에 대한 내용과,
로드 밸런싱을 수행하기 위해 Docker 를 어떻게 활용되어야 하는지 보았다.
만약에, Docker Network 를 생성하지 않고, 단순히 Nginx 라는 컨테이너를 실행하여,
localhost:8080 와 같은 형식으로 요청을 보내도 된다.
그러나, 이러한 경우, "API 접근 시각" 에서 바라보았을 때,
목적에 따른 각기 다른 프로젝트들이 네트워크적으로 격리 될 수 없음을 확인했다.
"그냥 구현하기만 하면 되는 것이 아닌가?"
물론 이도 맞는 말이지만, 나는 한정된 리소스에서 복잡한 프로젝트 체계를 잡아야 한다.
이 과정에서 핵심 기능 network 를 이해하지 못한다면, 추후 클라우드 네트워크 환경이
스파게티처럼 엮여 있을 가능성이 90% 는 될 것이라 생각한다. (확장까지 고려했을 때.)
내가 이제 수행할 과정에서 매우 많은 도커의 Command 가 사용 될 것이지만,
이는 Docker-Compose 를 사용하지 않아 그렇다.
나는 아직 Docker-Compose 를 사용하지 않고, docker 명령어를 이해하기 위해
Dockerfile + docker command + container + network 형식으로 구성 할 것이다.
현재 글에서 Docker network 역할 수준인 bridge, host, none, .. 을 다루면
난잡해 질 것 같다.
지금 만들게 될 도커 네트워크는 bridge 이다.
1. 도커 가상 네트워크 생성 - Docker network create.
# spring-test-network 라는 이름의 도커 네트워크 생성 - bridge
➜ ~ docker network create spring-test-network
6171b5277ed3a9b19ce3fc4bccb0b1fea8df9b6362877c6eb4b85566a8747fcd
# bridge, host, none 은 docker 의 기본 네트워크라서 삭제 불가
➜ ~ docker network ls
NETWORK ID NAME DRIVER SCOPE
d7a1a41c2242 bridge bridge local
3508372af286 host host local
a2587916247a none null local
6171b5277ed3 spring-test-network bridge local
위의 목록을 보면, 내가 생성한 새로운 네트워크 spring-test-network 가 보일 것이다.
아직 어떤 컨테이너도 속하지는 않으며, 이 네트워크는 현재 오로지 IP 로만 접근이 가능하다.
2. 이미 만들어 둔 스프링 컨테이너 삭제
test-spring 은 현재 정지되었으며,
docker run -d -p 8080:8080 test-spring 으로, 생성된 이미지를 기반으로
2 개의 옵션이 적용 된 컨테이너로 탄생되었었다.
그러나, 현재 스프링 서버는 로컬에 노출되어서는 안된다. Nginx 를 통해서 연결되어야 한다.
따라서, 생성된 컨테이너를 지우고, 새로운 옵션들과 함께 test-spring 을 실행한다.
# 정지된 컨테이너도 포함하여 폴 수 있는 명령어 옵션 --all
➜ ~ docker container list --all
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d3a682bcb5cc test-spring "java -jar app.jar" 22 hours ago Exited (143) 22 hours ago stoic_faraday
# 컨테이너 ID "접두어" 일부를 이용하여 쉽게 삭제
➜ ~ docker container rm d3a
d3a
# Detached 모드는 유지하되, 컨테이너의 고유 이름과 네트워크가 선언된 컨테이너가 생성됨.
#
➜ ~ docker run -d --name=spring1 --network=spring-test-network test-spring
0695c883b5f00b5bfa934cfb0129e63eea1df1682e7ab5c2ea940fe302db52b9
# 랜덤한 이름 대신, 확실하게 NAMES 에 spring1 이 박힌 것을 볼 수 있다.
➜ ~ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
0695c883b5f0 test-spring "java -jar app.jar" About a minute ago Up About a minute spring1
--name 옵션은 왜 붙였냐면,
이제 곧 Nginx 이미지를 이용하여 이름과 네트워크, 노출 포트를 설정하는데,
추가적으로 test-spring 이미지 기반 컨테이너에 "연결하기" 위해서,
이 NAMES 라는 항목이 꼭 필요하다.
/etc/nginx/conf.d/default.conf 는 Nginx 에서 꼭 알아야 할 경로인데,
이 경로에 적힌 .conf 파일이, Nginx 가 받은 요청을 "어디로 분산시킬지" 결정하기 때문이다.
즉, 간단히 말해 코드로 작성하지 않는 "설정 파일" 이다.
공식 이미지와 함께 nginx 이미지 & 컨테이너 실행하기
현재 과정에서는 Nginx 에 대한 매우 디테일한 설정은 필요가 없다.
내부 파일 중 /etc/nginx/conf.d/default.conf 파일만 변경하면 되므로,
Nginx 공식 이미지를 기반으로 곧바로 실행하면 된다.
(만약에 이 글을 실제로 실행하는 귀중한 분이 계시다면, 감사합니다.)
(잠깐 참고용으로 사용할 .conf 파일은 따로 예제 디렉토리를 생성하시길 권장드립니다.)
Nginx 문법 참고용 추천 사이트 :
spring-conf.conf 예제 파일:
server {
# 80 포트로 수신한다.
listen 80;
# nginx 가 동작하고 있음을 알기 위한 경로
location / {
return 200 "nginx 게이트웨이는 동작중.";
}
# location 마지막에 '/' 가 붙었는데, 이게 붙지 않으면,
# 예를 들어 "../api/v1/result" 라는 요청이 간다면,
# "http://spring1:8080//result" 가 된다.
# "//result" 는 프로젝트에 해당하는 경로가 없다. --> 404 반환
location /api/v1/ {
# uri 로 /api/v1/* 이 날라왔을 때, 밑의 경로로 치환한다.
# 마지막에 '/' 를 꼭 붙여주어야 하는데, 그렇지 않으면
# http://sprin1:8080/api/v1 으로 요청이 날라간다.
# 현재 스프링 프로젝트는 '/api/v1' 이 아니라, '/' 에 매칭되어 있다.
proxy_pass http://spring1:8080/;
}
}
# '\' 를 통해 명령어를 깔끔하게 여러 줄로 입력 할 수 있음.
# 마지막 줄의 'nginx' 는 공식 nginx 도커 이미지를 가져와서 컨테이너로 구성 한 것을 의미합니다.
# 명령 중간에 또다른 명령어의 결과를 넣고 싶다면, "$(원하는 명령어)" 로 주입 할 수 있습니다.
➜ example docker run -d \
--name=nginx-spring-test \
--network=spring-test-network \
-v "$(pwd)/spring-conf.conf:/etc/nginx/conf.d/default.conf" \
-p 8080:80 nginx
95bc996905c7fa70ea9f03031bb4e11d19275c3a6614e691f44d334120a6d6e5
# 밑의 nginx 컨테이너가 성공적으로 띄워진 것을 볼 수 있습니다.
➜ ~ docker container ls --all
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
95bc996905c7 nginx "/docker-entrypoint.…" 7 seconds ago Up 6 seconds 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp nginx-spring-test
0695c883b5f0 test-spring "java -jar app.jar" 2 hours ago Up 2 hours spring1
Request && Response
# 단순 루트 요청시 nginx location / 반응
➜ example curl -X GET "http://localhost:8080"
nginx 게이트웨이는 동작중.
# location "/api/v1/" 을 통해 스프링 '/' 경로에 접근
➜ example curl -X GET "http://localhost:8080/api/v1/"
헬로 월드를 도커 내부에서 실행 - 루트 url
# /api/v1/result 요청을 통해 스프링 "/result" 경로에 접근
➜ example curl -X GET "http://localhost:8080/api/v1/result"
This is BeanResultSample
위의 상황을 그래프로 요약 해 보자
---
title : Local 컴퓨터 내의 흐름
---
flowchart TB
Client("Client - Local 컴퓨터")
subgraph Docker["Docker"]
subgraph D-Network["도커 네트워크 - 격리 네트워크"]
direction LR:
subgraph Nginx["Nginx - 8080 포트 노출"]
direction LR
route1("경로 - 'localhost:8080' <br/> nginx 가동 확인용")
route2("경로 - 'localhost:8080/api/v1/ <br/> spring 요청 전달용")
route1 ~~~ route2
end
subgraph Spring["Spring 단일 컨테이너"]
direction LR
route1-spring("경로 - /")
route2-spring("경로 - /result")
route1-spring ~~~ route2-spring
end
Nginx -- spring1:8080/result --> Spring
end
end
Client --GET localhost:8080/api/v1/result--> Nginx
위의 그래프를 보다 보면, 굳이? 할 수도 있다.
로컬 Develop 상황에서는 굳이 복잡한 nginx 설정을 구비 할 필요가 없기 때문이다.
그러나, 나는 클라우드 상황에 대비 할 것이기 때문에, 이러한 방향으로 구축을 해 보았다.
마무리
혹시 Nginx 에 대한 전반적인 설명과 문법이 궁금하다면,
Nginx 설정 파일을 따로 판매하는 국내 업체에서 운영하는 사이트를 보면 되는데,
Nginx Store - Nginx Reverse Proxy 구성하기
이 사이트를 참조하면 된다.
내가 GUI 를 적극 이용하지 않은 이유
나는 Graphical User Interface, 즉 macOS 나, Window 와 같은 환경에 설치하는
Docker Desktop 의 편리한 기능을 이용하지 않고,
CLI(Commond Line Interface) 를 이용하여 Docker 를 구축했다.
그 이유는 생각보다 간단한데, GUI 는 편리 한 대신 사용할 수 있는 장소가 매우 한정적이며,
CLI 는 어디서든 사용할 수 있기 때문이다.
나는 클라우드에서 Docker 를 운용해야 하는데,
당연히 GUI 는 존재하지 않는다. CLI 로만 운용해야 한다.
아직 Docker 에 대한 설명이 부족합니다. - Need Docker-Compose
단순 Dockerfile 수준과 명령어만을 이용하여 인프라를 구축한다면,
나중에 네트워크와 더불어, 컨테이너 복제, 확장, 축소 등등의 기능이 매우 어려워 진다.
Docker 의 산출물에서 특정 변화가 필요 할 경우, 위에서 선보인 예제와 비슷하게
수많은 옵션을 따로 CLI 로 적어야 하기 때문이다.
해당 이미지로 실행할 컨테이너가
- 어떤 이름을 가질지
- 어떤 Docker Network 에 속할지
- 어떤 로컬 Volumn 을 참조할지
- 서버는 몇 개로 확장할지
이에 대한 내용을 명령어로 지속적으로 실행한다면, 번거로움이 따라올 수 밖에 없다.
따라서, 이에 대한 내용의 후속편으로 Docker-Compose 를 다루거나,
도커의 이해에 필요한 또 다른 내용으로 작성을 하게 될 것이다.
참조 사이트
IBM - Docker 란?
https://www.ibm.com/kr-ko/think/topics/docker
위키백과 - 도커(소프트웨어)
https://ko.wikipedia.org/wiki/%EB%8F%84%EC%BB%A4_(%EC%86%8C%ED%94%84%ED%8A%B8%EC%9B%A8%EC%96%B4)
Spring 공식문서 - Spring Boot with Docker
https://spring.io/guides/gs/spring-boot-docker
DockerDocs 공식문서 - Writing a Dockerfile
https://docs.docker.com/get-started/docker-concepts/building-images/writing-a-dockerfile/
Docker Hub 레지스트리 - eclipse-temurin:17-jre-focal
Docker Hub 레지스트리 - nginx
https://hub.docker.com/_/nginx
Docker 공식문서 - How to Use the NGINX docker Official Image
https://www.docker.com/blog/how-to-use-the-official-nginx-docker-image/
Nginx Store(국내기업)- Nginx Reverse Proxy 구성하기
https://nginxstore.com/training/nginx-reverse-proxy-%EB%A1%9C-%EC%84%A4%EC%A0%95%ED%95%98%EA%B8%B0/
'Computer Science' 카테고리의 다른 글
| 멀티 스레드의 특성과 C 에서의 사용법 - 1편 (0) | 2025.09.27 |
|---|