마이크로서비스를 성공적으로 유지하려면 이식성 문제, 즉 "마이크로서비스를 어떻게 다른 기술 환경에 실행할 수 있을까?"라는 문제를 해결해야 합니다.
❗️ 이식성(portability)은 소프트웨어를 다른 환경에 사용하거나 이동할 수 있는 능력이다.
도커는 컨테이너화 기술을 통해서 애플리케이션의 배포와 실행을 용이하게 해주는 오픈 소스 플랫폼입니다.
도커는 애플리케이션과 그 종속성을 컨테이너라는 표준화된 단위로 패키징하여, 코드가 개발 환경에서부터 테스트, 스테이징, 프로덕션 환경에 이르기까지 일관된 환경에서 실행될 수 있도록 합니다.
도커를 사용하기 이전에는 VM을 통해서 배포를 많이 하였고, 현재도 VM을 이용하여 배포를 하고 있는 기업도 많다고 합니다.
❗️ VM은 한 컴퓨터 내에서 다른 컴퓨터 동작을 에뮬레이션할 수 있는 소프트웨어 환경입니다.
물리 머신을 완벽히 에뮬레이션하는 하이퍼바이저(hypervisor)에 기반을 두며, 이것은 호스트 OS로부터 리소스를 받아서 그것을 게스트 OS에게 할당해주는 역할을 수행합니다. 애플리케이션은 게스트 OS 위에서 동작하게 됩니다.
반면 컨테이너는 격리되고 독립된 환경에서 애플리케이션의 의존성 구성 요소와 함께 애플리케이션을 실행할 수 있는 가상 운영 체제(OS)가 포함된 패키지입니다.
따라서, 별도의 게스트 OS가 필요로 하지 않습니다.
위에서 컨테이너는 가상 운영 체제가 포함된 패키지라고 표현을 해놓았는데, 일단 이부분에 대해서 좀 정리를 하고 넘어가겠습니다.
일단, 도커가 아닌 VM을 이용해서 배포를 한다고 가정을 해봅시다.
우선 1계층으로부터 들어오는 phsical 데이터를 VM이 돌아가는 가상 머신 환경 즉, 게스트 OS에게 건네줘야 됩니다.
즉, Virtual Machine이 돌아가고 있는 환경에서 소프트웨어적으로 구현된 phsical layer가 있어야 되고, 그 위에서 게스트 OS가 동작하고 있을 것입니다.
최종적으로 게스트 OS 위에서 애플리케이션이 동작하는 형태로 진행되는 것인데, 애플리케이션을 동작시키기 위해서 많은 것들을 추가적으로 필요로 하는 것을 볼 수 있습니다.
따라서, 이것이 리소스의 낭비라고 판단되어져 나온 것이 도커이고 도커는 이러한 형태처럼 불필요하게 리소스를 잡아먹는 부분을 줄였습니다.
애플리케이션과 그 종속성(환경)을 같이 묶고, 그것을 동작시키기 위한 웹 서버도 같이 묶습니다. 그리고 이것을 컨테이너화 시켜서 실행시키는 것입니다.
여기서 컨테이너화 시켜서 "실행" 시킨다는 의미가 중요한데, 실행시킨다는 것은 호스트 OS 입장에서 봤을 때는 특정 프로그램을 인스턴스화 시켜 메모리를 할당해 동작하게 하는 것으로 즉, 컨테이너화 시켜서 실행하는 시점부터 하나의 프로세스라고 생각되게 됩니다.
도커 엔진은 컨테이너에 필요한 리소스(메모리, CPU 등)를 할당하고, 호스트 OS로부터 이 리소스를 효율적으로 관리합니다. 컨테이너는 각각 독립된 환경에서 실행되지만, 같은 호스트 OS의 커널을 공유합니다.
이렇다고 해서 VM을 이용한 배포가 나쁘다는 것은 아닙니다.
예를 들어 다양한 운영 체제를 처리하고, 단일 서버에서 여러 애플리케이션을 관리하고, OS 기능이 필요한 애플리케이션을 실행하려고 한다면 VM이 더 나은 솔루션입니다.
다만, 마이크로서비스의 경우 이식성과 확장성이 중요하기 때문에 도커를 사용하는 것이 좋습니다.
마이크로서비스와 함께 컨테이너를 사용한다면 다음과 같은 이점이 있습니다.
DockerFile의 경우, 도커 이미지를 생성하기 위해서 필요한 스크립트 파일입니다.
이미지 생성을 할 때 필요한 설정들뿐만이 아니라 구체적인 설정까지 작성할 수 있습니다.
더불어서, 마이크로서비스 아키텍처에서는 하나의 도커 파일만 필요한 것이 아닌 여러 도커 파일을 동시에 실행 시키면서 전체적인 설정을 요구하는 경우가 많습니다.
이 경우, 도커 컴포즈(Docker Compose)를 이용할 수 있는데, 도커 컴포즈는 서비스 설계와 구축이 용이한 스크립트를 작성하여 도커를 더 쉽게 사용할 수 있도록 해줍니다.
이러한 경우 도커 컴포즈를 이용하여 전체 설정 및 실행을 구성할 수 있습니다.
이후 책에서는 실행에 관련된 내용을 주로 다루고 있는데 그것 말고 좀 더 구체적인 내용에 대해서 따로 다루겠습니다.
도커를 이용하여 여러 컨테이너들을 실행시키면서 이것을 '그룹화'할 수 있는 것은 물론 '네트워크'를 엮어서 사용할 수 있습니다.
이것을 이해하기 위해서는 도커 네트워크에 대해서 이해를 해야합니다.
먼저, 하나의 이미지를 가지고 컨테이너가 구축할 때 어떠한 기본 작업들을 실행하는지 나열해보자면 다음과 같습니다. (DockerFile로 실행하는 경우)
이것이 기본적인 흐름입니다. 좀 더 구체적으로 정리를 해보자면 다음과 같습니다.
1. 도커 데몬(Docker Daemon):
2. CLI(Command Line Interface) 설정:
3. Dockerfile 처리:
4. 애플리케이션 실행:
여기서 네트워크가 어떻게 설정되는지 살펴봅시다.
도커는 컨테이너에 내부 IP를 순차적으로 할당
내부 IP는 컨테이너를 재시작할 때마다 변경될 수 있음
내부 IP는 도커가 설치된 호스트, 즉 내부망에서만 쓸 수 있는 IP이므로 외부와 연결될 필요가 있음
컨테이너를 시작할 때마다 호스트에 veth(Virtual Ethernet) 라는 네트워크 인터페이스를 생성
도커는 각 컨테이너에 외부와의 네트워크를 제공하기 위해 컨테이너마다 가상 네트워크 인터페이스를 호스트에 생성하며 이 인터페이스의 이름은 veth로 시작
veth 인터페이스는 사용자가 직접 생성할 필요는 없으며 컨테이너가 생성될 때 도커 엔진이 자동으로 생성
veth 인터페이스는 호스트가 갖고 있는 eth0, eth1 등과 연결되어 있음
docker0 브리지는 각 veth 인터페이스와 바인딩돼 호스트의 eth0 인터페이스와 이어주는 역할
여기서, 컨테이너 간의 네트워크 방식을 정의하고 구성하는 역할을 하는 것이 네트워크 드라이버로 dafault는 Bride입니다.
정리를 해보자면 다음과 같습니다.
네트워크 드라이버
생성되는 컨테이너에 초점을 맞춰서 설명을 하자면 위와 같았습니다.
그렇다면, 전체적인 플로우에 맞춰서 한번 더 확인해 볼 필요도 있겠죠.
먼저 그림부터 확인을 해봅시다.
하나씩 짚어나가 봅시다.
먼저, eth-0은 현재 도커 데몬이 실행되고 있는 서버의 물리적 인터넷 포트이며, 외부 네트워크(인터넷)와 연결되어 있습니다.
이 인터넷 포트가 iptables를 통해 docker0 또는 다른 사용자가 만든 가상 브릿지(Mybridge)와 통신이 됩니다.
또한 이 가상 브릿지는 각각의 컨테이너와 통신하게 되는데, 이때 가상 브릿지와 컨테이너간 통신은 맥주소로 통신이 이루어집니다.
컨테이너가 생성된다고 가정해봅시다. 여기서 생성되는 컨테이너는 Container0입니다.
컨테이너를 생성하면, 자동으로 컨테이너에 eth0 interface가 생기며(별도의 옵션을 통해 추가 가능) 그 컨테이너의 eth0과 1:1 매칭되는 veth-0 interface가 생성된 것을 볼 수 있습니다. 이 가상 interface는 OSI 참조 모델의 레이어 2인 가상 네트워크 인터페이스로, pair인 eth0(컨테이너)과 터널링 통신을 합니다.
이렇게 pair로 구성된 두개의 interface를 통해 격리된 네트워크 환경(namespace)이 제공되며, MAC 주소와 private ip가 부여됩니다. 마치 직접 다이렉트로 케이블을 연결한 두 대의 PC가 서로 패킷을 주고받는 형태와 같습니다.
default 옵션인 Bridge 드라이버를 이용할 경우 컨테이너의 eth0와 한 쌍인 vth-0은 docker0 가상 브릿지 네트워크에 연결됩니다. 이 가상 네트워크는 컨테이너끼리 통신할 수 있도록 만듭니다.
도커는 1서비스당 1컨테이너를 권고하므로, 한 컨테이너에 여러 서비스를 포함하지 않고 각각 컨테이너로 하나씩 만들고 가상 브릿지 네트워크로 연결되는 것이 권고됩니다.
가상 브릿지 네트워크에 대해서 좀만 더 구체적으로 알아봅시다.
도커 설치 시, default로 docker0라는 가상 브릿지 네트워크가 생성됩니다. docker0 가상 브릿지 네트워크는 소프트웨어적인 스위치방식으로, 일반적인 스위치 개념과는 다르게 DHCP로 연결된 container에게 사전에 정의된 IP pool을 할당합니다. docker0는 default로 172.17.0.0/16 서브넷을 가진 IP 대역을 가집니다.
이 부분에 대해서 좀 더 구체적으로 정리를 해보겠습니다.
일반적인 네트워크 스위치
도커의 docerk0 브릿지 네트워크
docker0
는 가상 브릿지 네트워크로, 소프트웨어적으로 구현된 네트워크 인터페이스입니다. 이는 컨테이너들이 서로 통신하고 호스트 시스템과 통신할 수 있는 가상의 네트워크 환경을 제공합니다.172.17.0.0/16
범위 내에서 이루어집니다.컨테이너가 특별한 옵션 없이 생성된다면 이 docker0 에 기본적으로 연결됩니다. docker0 는 default 브리지 네트워크일 뿐이고, 사용자가 얼마든지 추가로 가상 브리지 네트워크를 생성할 수 있습니다.
❓ veth를 사용하는 이유가 무엇일까?
docker0는 브릿지의 역할을 수행하면서, 내부 컨테이터들의 네트워크와 외부 네트워크를 연결시켜주는 역할을 수행한다고 했습니다. 여기서 컨테이너와 연결할 때 컨테이너의 eth와 연결하는 것이 아닌 가상 네트워크 인터페이스인 veth와 연결되는 것을 확인할 수 있었습니다. 왜 직접 연결하지 않고 이렇게 하는 것인까요?
'veth' 인터페이스를 사용하는 주된 이유는 다음과 같습니다.
1. 격리(Isolation): 컨테이너는 격리된 환경에서 실행되며, 이것은 네트워크 통신에도 적용됩니다. veth 인터페이스 쌍을 사용하면, 컨테이너의 네트워크 트래픽을 호스트 시스템과 격리된 상태로 유지하면서 관리할 수 있습니다.
2. 통신 관리: 'veth' 쌍을 통해 컨테이너와 호스트 간의 통신이 가능해집니다. 컨테이너의 eth0 인터페이스는 'veth'의 한 끝에 연결되고, 다른 한 끝은 도커 호스트의 네트워크 브리지에 연결됩니다. 이 구조를 통해 컨테이너는 호스트 및 다른 컨테이너와의 네트워크 통신을 할 수 있습니다.
3. 네트워크 브릿지 연결: 'veth' 쌍의 한 끝이 네트워크 브리지에 연결되면, 이 브리지는 여러 'veth' 인터페이스와 연결되어 여러 컨테이너 간의 통신을 중계합니다. 이 브리지는 컨테이너 네트워크를 호스트 네트워크와 연결하면서도 서로 다른 컨테이너들이 서로 통신할 수 있게 합니다.
4. 유연성(Flexibility): 'veth' 인터페이스를 사용함으로써, 도커는 컨테이너 네트워킹을 더 유연하게 구성할 수 있습니다. 예를 들어, 다른 네트워크 네임스페이스, 서로 다른 네트워크 브리지, 혹은 심지어는 호스트 시스템 밖의 다른 장치와의 연결 등을 설정할 수 있습니다.정리하자면, 여러 컨테이너 네트워크(외부 네트워크)와 격리를 해주고, 컨테이너 네트워크와 브리지 네트워크를 연결해주며, 브리지 네트워크에게 해당 인터페이스를 통해 접근 및 관리하도록 제공해주며, 최종적으로 외부 네트워크와 통신하게 도와줍니다.
docker0는 도커 설치시 자동으로 만들어지며, 컨테이너 생성시 명시적으로 지정하지 않고 run 하는 경우 이 네트워크로 컨테이너가 시작됩니다.
여기서 문제점은, DNS 기능이 비활성화 되어 있어서 이름식별이 안됩니다. 따라서, IP로만 통신이 가능한 상태입니다.
컨테이너는 일종의 프로세스이므로, 내렸다 올렸다하면 IP가 바뀌는 등의 유동성이 있습니다.
컨테이너끼리 연동을 하려면 IP 기반의 설정은 좋지 않으며, link 옵션을 사용하여 연동해야 합니다. 즉 docker container run 할 때 --link 옵션을 지정해야 합니다.
--link 옵션을 지정한다는 것은 컨테이너 안에 있는 /etc/hosts 파일에 컨테이너명과 컨테이너에 할당된 IP가 등록되고 이를 통해 통신을 가능하게 하는 것입니다.
link가 걸린 상태에서 컨테이너를 재기동하면 IP가 바뀔 수 있는데, 바뀐 IP값이 자동으로 /etc/hosts에 등록되어서 container의 ip 정보가 바뀌어도 서로간 문제없이 통신이 가능합니다.
사용자 정의 네트워크는 도커 데몬에 내장된 내부 DNS 서버에 의해 이름 해결이 이루어집니다.
내부 DNS 서버를 사용하면, link 기능과 같이 /etc/hosts 파일에 의존하지 않고 이름 문제를 해결할 수 있습니다. 따라서 docker0에 연결된 컨테이너들은 ping을 할 때 IP만 가능하지만 사용자 정의 네트워크에 연결된 컨테이너들은 컨테이너명(docker container run --name으로 생성되는 컨테이너명)뿐만 아니라 컨테이너 시작 시 지정한 --net-alias 옵션을 사용한 알리아스명으로도 통신이 가능합니다.
조금 설명이 복잡한데 코드로 간단히 보자면 다음과 같습니다.
다음은 Docker-Compose 파일 예시입니다.
version: '3.7'
services:
licensingservice:
image: ostock/licensing-service:0.0.1-SNAPSHOT
ports:
- "8080":"8080:
environment:
- "SPRING_PROFILES_ACTIVE=dev"
networks:
backend:
aliases:
- "licenseservice"
networks:
backend:
driver: bridge
하나씩 정리를 해보자면,
(내부:외부)
SPRING_PROFILES_ACTIVE
환경 변수를 설정합니다.이 외에도 다양한 옵션들이 존재합니다.
사실, 도커에 관한 설명을 할 때 빠질 수 없는 것이 볼륨(Volume)과 바인드 마운트(Bind Mount)인데, 이것은 MSA를 위한 도커 설명에서 다룰 것이 아닌 도커 자체를 정리하게 되면 거기서 다루도록 하겠습니다.
참고한 사이트