본문 바로가기
SK Planet ASAC

Docker

Docker는 왜 사용할까? 

Docker는 일관성 보장(Consistency)과 다중 컨테이너 지원(Multiple Containers)을 위해 사용한다. 

 

일관성 보장(Consistency) 

어플리케이션이 어떤 머신에서도 정상 및 동일 동작하기 위해서는 어플리케이션이 구동되는데 필요한 모든 것들이 환경과 버전 등이 맞아야 한다. 이를 일관성이라 한다. 

이렇게 Docker를 통해 일관성이 보장된 어플리케이션은 어딜가도 자신이 만족하는 딱 그 환경에서 동작이 가능하다. 모든 일관성이 보장되어 있는 음식이 든 밀키트라고 생각하면 된다...

 

예를 들어 Application 파일, 환경 변수, 런타임 환경과 버전, 라이브러리 등... 어플리케이션마다 필요로하는 라이브러리가 전부 각각 다르고 이 모든 것을 합쳐 하나의 컨테이너로 사용한다면 어떤 컴퓨터에 설치를 하더라도 (OS가 설치되어 있기 때문에) 구동이 가능할 것이다!

 

정리하자면 이렇게 Docker를 통해 일관성이 보장된 어플리케이션은 어딜가도 자신이 만족하는 환경에서 동작이 가능하다.

 

다중 컨테이너 지원(Multiple Containers)

하나의 서버임에도 다중 컨테이너를 동작하면 다양한 어플리케이션들을 조합하여 원하는 서버 구동이 가능하다. 

예컨대, 하나의 서버(머신)에서 Spring Boot와 MySQL을 각각 따로 동작할 때, 하나의 서버에서 Spring Boot에서 생성하는 로그를 Logstash로 읽어서 다른곳에 전송할 때와 같이 여러개의 컨테이너를 동작할 수 있다.

 

Docker가 뭔데?

다시 돌아와서 Docker가 무엇인지 다시 생각해본다. 앞서 [일관성 보장]과 [다중 컨테이너 지원]이라고 언급했는데, 이는 [격리 구동을 위한 가상화 기술], 즉 [격리된 공간에서 프로세스가 동작하는 기술]이라고 볼 수 있다. 

 

이때 격리와 가상화를 위한 단위는 두가지로 나뉜다. 

https://wnsgml972.github.io/setting/2020/07/20/docker/

VM(Virtual Machine)

VM은 하나의 서버 HOST OS(하드웨어를 구동하는 데 필요한 API 집합) 위에 Hypervisor가 실행되어 각 Guest OS들을 매니징한다. 각 Guest OS가 독립적이기 때문에 리소스를 많이 사용하고, 이로 인해 무거워져 느려진다. 

 

Docker의 Container

HOST OS가 할당해 준 메모리를 사용해서 Docker Engine을 구동한다. HOST OS의 메모리와 엔진을 별도의 매니저 없이 그대로 사용하기 때문에 경량화 되어있다. 또한 Guest OS가 필요하지 않다. 가볍다...!

Hypervisor

가상화 구조에서는 하드웨어와 가상 머신이 직접 연결되지 않는다(하드웨어나 가상 머신들이 자신이 가상화 되었다는 것을 모를수도 있음). 하드웨어 위에서 가상 머신을 생성하고, 필요한만큼 자원을 할당해준 후에 가상 머신들의 요청을 처리해주는 등 가상화를 도와줄 매니저가 필요하다. 그 역할을 수행하는 것이 바로 하이퍼바이저.
때문에 하드웨어 리소스를 가상 머신에 할당하고, 가상 머신들의 리소스 사용 스케줄링, 가상 머신과 하드웨어 간의 I/O 명령 처리 등 모든 것을 하이버파이저가 담당한다. 

 

Docker Container는 linux 기반으로 작동해야 하기 때문에 window의 경우 VM을 사용할 수밖에 없다. 이렇게 OS의 호환성 문제가 있는 경우 VM 사용을 피할 수 없지만, Docker가 없던 시절에는 VM을 사용할 수밖에 없었다. 

 

 

용어

Docker 격리 단위

Docker Image, Bins/Libs와 App 집합이다. App에는 구동시킬 앱이, Bins/Libs에는 구동시킬 때 필요한 라이브러리와 실행 파일, 환경 변수 등이 있다. 

 

Docker 격리 정의

Dockerfile로 정의한다. 어떤 Bins/Libs와 어떤 App을 포장할지 정의한다.

 

Docker 격리 동작

Docker Container. 실행중인 이미지를 이야기하며 이때 이미지는 정적, 컨테이너는 동적으로 실행중인 이미지를 가리킨다. (메모리와 CPU 할당을 받은 상태) 

 

 

Docker를 통한 어플리케이션 관리

그렇다면 이런 Docker를 통해 어플리케이션을 어떻게 관리할까. 하나의 OS 위 다양한 컨테이너를 조합하여 어플리케이션 구동이 가능하다. 

이때 단일 컨테이너인지 다중 컨테이너인지 정의할 수 있다. 여기서 컨테이너란 격리된 공간에서 프로세스가 동작하는 기술을 의미한다. 

왼쪽 그림과 같이 A 컨테이너에는 어플리케이션(Spring boot)을, B 컨테이너에는 DB를 띄워놓을 수 있다. 이를 다중 컨테이너 어플리케이션 관리라고 하는데, Docker-Compose를 통한 다수 이미지를 구성하고 구동할 수 있다. 

Docker Compose

Docker Compose와 Dockerfile 모두 선언형 툴, 최종적으로 어떻게 연결이 되어야 한다는 상태만을 명시한다. 
Dockerfile로 정의한 개별 Container들을 한번에 띄워 이들을 하나의 완벽한 Application으로 서비스 되게 한다. 

선언형 Declarative: What을 의미, 무엇이 되어야 하는가(상태성)를 의미한다. 일반적 프로그래밍 언어인 명령형과는 차이가 있다. 명령형은 "어떻게" 해결하는지에 대해 초점을 맞춰 어떤 단계를 거처 목표를 달성하는지 명시적으로 기술한다. 선언형은 그와 반대로 결과 도출을 위한 명시적 표현 방법이다. 

 

오른쪽 그림처럼 동일한 컨테이너를 띄울 수도 있는데, 분산을 위해 하나의 호스트가 여러개의 Spring boot를 띄운 것이 그 예이다. 이런 경우 여러 인스턴스가 동일한 호스트에서 실행되어 호스트의 CPU, 메모리와 같은 리소스가 여러 인스턴스 간에 공유된다. 이는 각 인스턴스가 필요 이상으로 많은 리소스를 사용하지 않도록 해 불필요한 자원 낭비를 줄일 수 있다.

또한, 하나의 서비스가 다운(?)되었을 때 동일한 어플리케이션이 남은 서비스를 마저 이용할 수 있도록 해준다. 이를 Kubernetes에서 지원해주는데 여튼 되게 좋다!

 

 

Docker Compose 내 다중 컨테이너와 호스트 사이에서의 포트 포워딩

포트 포워딩(Port Forwarding)

외부 네트워크에서 내부 네트워크의 특정 장치에 접근할 수 있게 만들어주는 행위. 예를 들어 가정에서 인터넷을 통해 게임 서버를 운영하고 싶을 때, 외부에서 접근할 수 있도록 특정 포트를 내부 네트워크의 게임 서버로 포워딩할 수 있다. 
내부 네트워크는 일반적으로 공개 IP 주소를 가지지 않는다. 그렇기 때문에 외부에서 내부 네트워크로 직접 접근할 수 없는 것이다. 

 

앞서 말한 '게임 서버'의 예시와 비슷하게 Docker에서도 해당 개념이 적용된다. 직접적으로 각 컨테이너에 접근할 수 없어 HOST 포트를 한가지 설정해줘야 하는데, 이는 8081:8080과 같이 선언한다. :앞 8081은 컨테이너에 접근하기 위한 Host Port, 8080은 컨테이너에 직접적인 Container Port다. 이 내용은 docker-compose.yml에 정의한다. 

 

그렇다면 컨테이너 간 접근은 어떻게 할까?

외부에서 컨테이너에 접근하는 방법은 알아봤다. 위 그림에서 오른쪽 도식처럼 Spring Boot 컨테이너가 MySQL 컨테이너로 접근하려면 어떻게 해야할까. 2가지의 방법이 있다. 

 

1. User-defined Bridge 방법

docker가 자동 제공해주는 Automatic DNS Resolution을 사용하는 것이다. Bridge 네트워크를 정의하고,

networks:
  x-network:
    driver: bridge

 

해당 Bridge 네트워크를 공유해서 사용하고자하는 컨테이너들에 연결한다. 

version: "3"
services:
  postgres:
    container_name: postgres_host
    image: postgres:15
    ports:
      - 5432:5432
    networks:
      - x-network (여기)
    environment:
      POSTGRES_DB: tutorial
      POSTGRES_USER: "user"
      POSTGRES_PASSWORD: "!@#"

  docker_tutorial:
    build: .
    ports:
      - 8080:8080
    networks:
      - x-network (여기)
    environment:
      # export SPRING_PROFILE=develop
      # SPRING_PROFILE: ${SPRING_PROFILE}
      SPRING_PROFILE: ${SPRING_PROFILE}
    depends_on:
      - postgres

 

2. Hostname 방법 

명시적으로 DNS 내 컨테이너의 hostname을 등록한다. 각 컨테이너마다 hostname을 정의하면 DNS 설정이 된다.

version: "3"
services:
  postgres:
    container_name: postgres_host
    hostname: postgres_host(여기)
    image: postgres:15
    ports:
      - 5432:5432
    environment:
      POSTGRES_DB: tutorial
      POSTGRES_USER: "user"
      POSTGRES_PASSWORD: "!@#"

  docker_tutorial:
    build: .
    ports:
      - 8080:8080
    environment:
      # export SPRING_PROFILE=develop
      # SPRING_PROFILE: ${SPRING_PROFILE}
      SPRING_PROFILE: ${SPRING_PROFILE}
    depends_on:
      - postgres

 

그러면 후에 docker_tutorial에서 아래와 같이 접근할 수 있다. 

String url = "jdbc:postgresql://postgres:5432/tutorial";

 

postgres는 docker_tutorial에서 참조하기 위해 사용하는 호스트 이름, tutorial은 postgres의 환경 변수로 설정된 데이터 베이스 이름이다. 

 

 

호스트와 컨테이너의 관계에 따른 시스템 선택 

호스트와 컨테이너의 관계(몇 대 몇?)에 따라 Docker 또는 Kubernetes를 선택할 수 있다. 

 

Docker

1 호스트 내 1 단일 컨테이너 설정: Dockerfile 적절한 사진을 찾지 못했다... 그리고 싶진 않다.

1 호스트 내 N 다수 컨테이너 설정: Docker Compose(멀티 컨테이너)

https://medium.com/dtevangelist/docker-%EA%B8%B0%EB%B3%B8-3-8-container%EB%8A%94-%EB%AD%98%EA%B9%8C-bf3df8cbaf44

 

위 그림이 1개의 호스트 내 다수 N개의 컨테이너를 관리할 때의 구성이다. A, B, C의 어플리케이션이 든 컨테이너가 각각 하나의 호스트 내 올라가 있는 것을 볼 수 있다. 이는 모두 하나의 YAML 파일로 관리한다. 

 

Kubernetes

M개의 호스트 내 N개의 컨테이너 설정

수백개의 호스트 내 수천개의 컨테이너가 올려진 모습이다. Kubernetes는 Docker를 포함한 수많은 컨테이너의 런타임을 지원한다. 이를 Orchestration이라고 하는데, 다수의 컨테이너들만 관리하는 Docker다. Compose와는 달리 다수의 호스트들도 관리한다. 마찬가지로 하나의 YAML 파일로 관리한다.

 

 

일반적인 Docker 사용 절차

Develop -> Test Java -> Build Java -> Build Docker Image -> Push -> Pull -> Run 

 

1. Develop 

우선, 배포/구동하고자 하는 Spring Boot(예시) 어플리케이션을 하나 만든다.

 

2. Test and Build

앞서 개발한 Java 소스 코드를 테스트 한 뒤 Java 빌드(JAR or WAR 파일 생성)

 

3. Build Docker Image and Push Docker Image to Registry

먼저, Dockerfile(Instruction 집합) 정의를 통해 Docker Image를 생성한다. 이 때 Docker Image에는 우리가 실행할 프로그램(앱)과 앱을 구동할 엔진, 구동을 보조해줄 Bins/Libs가 정의되어있다. 

우리는 Java 파일을 예로 들었으니, 실행할 프로그램은 JAR, 엔진은 JRE or JDK 17, 보조해 줄 Bins/Libs 정의는 SSH 등이 될 것.

Instruction

명령어와 같은 개념이다. Application 파일, Environment Varables(환경 변수), 서드파티 라이브러리, OS 등이 있다.

 

이제 생성한 Image를 Docker Registry(도커 저장소)에 Push한다. Docker Hub에 올려도 되고, AWS를 활용한다면 ECR을 사용하면 된다. 

 

4. Pull Docker Image from Registry and Run Docker Image on HOST

어플리케이션을 실행하려는 서버 내 Docker Registry에서 Docker Image를 다운로드하고, 이를 실행한다.(동적인 컨테이너가 됨!)

 

 

CI / CD

위 Docker 사용 절차에서 나눈 것이 각각 CI, CD가 된다. 

 

CI(Continuous Integration)

테스트 및 자바 빌드 & 이미지 빌드 

- Test and Build Java Application 

- Build Dcoker Image and Push Docker Image to Registry 

 

CD(Continuous Deployment)

앞서 만든 Image를 구동하는 단계다. 

Pull Docker Image from Registry and Run Docker Image on Host

 

 

CI / CD 툴 

배포는 CI와 CD 두 절차로 수행되는데, 이를 한꺼번에 One-Tool로 사용할 수 있다. 하지만 개발자가 CI/CD 툴을 설정할 때 전체 절차를 다 세부적으로 정의해줘야 한다. 

 

Github Action 

CI/CD 툴로 유명한 건 Github Action, Jenkins가 있는데 Github Action은 서버를 공짜로 제공해준다고 한다..

또한, 소스코드 중앙 관리를 위해 Github Repo를 쓰는데 Github Action과 호환성이 좋다. 

때문에 Github Action을 사용하면 자동으로 코드 저장소에서 어떤 이벤트가 발생했을 때 특정 작업이 일어나게 하거나, 주기적으로 어떤 작업들을 반복해서 실행시킬 수도 있다. 

 

  • 테스트 : 예를 들어, 누군가가 코드 저장소에 Pull Request 를 생성하게 되면
    • Github Action 을 통해 해당 코드 변경분에 문제가 없는지 각종 검사를 진행할 수 있고
  • 배포 : 어떤 새로운 코드가 기본 브랜치(master 또는 main)에 유입(push)되면
    • Github Action 을 통해 소프트웨어를 빌드(build)하고 상용 서버에 배포(deploy)할수도 있다.
  • 배치 : 뿐만 아니라 매일 밤 특정 시각에 그날 하루에 대한 통계 데이터를 수집시킬 수도 있다.