지난 글에서 '컨테이너 오케스트레이션 툴'이란 무엇인지 확인했다.

요약하자면 우리가 띄운 여러개의 컨테이너를 관리해주고 조율하는 툴이었다.

이번에는 컨테이너의 오케스트레이션 툴 중의 하나인 ECS(Elastic Container Service)

그리고 그 방법 중 하나인 fargate방식을 이용하여 오케스트레이션 툴을 쓰는 방법을 확인해보자.

 

일단 가장 앞서서, 우리가 무엇을 하는 것인지를 확인해보자.

앞으로 할 일이 너무 길어서, 꼭 우리가 뭘 하는지를 알고 실행해야 한다.

1. AWS ECR Repository :  레지스트리에 우리가 도커파일로 만들어 줄 이미지를 퍼블리싱한다.

2. Task Definition : 태스크 정의를 작성해서 태스크가 어떤 이미지를 기반으로 어떻게 사용될것인지를 정의한다.

  • JSON 파일 형식이지만, 우리가 AWS의 UI에 맞게 설정하면 AWS가 자동으로 JSON으로 번역한다.

3. Task : Task Definition을 기반으로 만들어진 인스턴스로, 태스크는 클러스터에 속한 컨테이너/EC2 인스턴스나 fargate에 배포한다.

4. Service : 클러스터에서 지정된 수의 작업을 동시에 실행하고 관리할 수 있게 해주는 구성으로, 한 개 이상의 태스크가 서비스에 포함되어 있고, 태스크와 관련된 Auto-Scaling과 Load Balancing을 관리한다.

5. Cluster : Task나 Service의 논리적 그룹으로, 클러스터를 실행하여 작업을 실행한다.

 

결국 우리는 클러스터를 생성해서 관리할 것이므로, 시나리오는 다음과 같다.

5번 클러스터 생성 -> 1번 ECR 레지스트리에 이미지 등록 -> 2번 Task Definition 정의 -> 4번 Service 설정

 

5. Cluster 생성

> AWS > ECS > 클러스터 > 클러스터 생성

클러스터 생성 메뉴에서는 일단 클러스터 이름만 정해주도록 하자.

나는 현재 진행중인 프로젝트 이름으로, docker-pro-wanted로 설정했다.

VPC는 웬만하면 자동으로 잡혀 있을 것이고, 사용하고 있는 VPC가 아닌 새로 VPC를 만들어야 할 경우 새 vPC 생성을 해주면 된다.

 

인프라 탭에서는 fargate 방식을 사용할 것이므로 다른 체크박스를 건드리지 말도록 하자.

그리고 생성 버튼을 눌러주면 된다.

혹시 

다음과 같은 오류가 발생하는 경우, 그냥 무시하고 다시 생성해주면 정상적으로 생성된다.

 

1. AWS ECR Repository

이제 클러스터를 생성했으니, 리포지토리를 생성하고 이미지를 업로드해보자.

>AWS ECR > 리포지토리 > 리포지토리 생성

리포지토리에서도 똑같이 이름만 적어보자. 

나는 choincnp/new-httpd로 작성했다.

이 레포지토리는 프라이빗으로 작성하였으므로, AWS IAM을 업데이트 해야 한다. 

조금 전 사진을 보면 퍼블릭 / 프라이빗 리포지토리를 설정하는 과정에서

프라이빗 리포지토리를 보면 설명에 액세스는 IAM 및 리포지토리 정책 과정에 의해 관리됩니다. 가 있다.

 

그럼 이제 이 클러스터를 관리할 수 있는 IAM을 만들고, CLI에 이 IAM을 연동시켜보자.

IAM 설정은 이 글에서 다루지 않고, 참고 링크를 남긴다.

https://jhlee-developer.tistory.com/entry/AWS-AWS-CLI-%EC%84%A4%EC%B9%98-%EB%B0%8F-V2-%EC%97%85%EB%8D%B0%EC%9D%B4%ED%8A%B8

 

[AWS] IAM 사용자 생성하는법 - 액세스 키 발급

IAM 사용자 생성 AWS 계정에 대한 액세스 키를 생성하면 AWS CLI 또는 API에 액세스할 수 있다. 서비스 메뉴에서 IAM(AWS 리소스에 대한 액세스 관리) → 사용자 탭에서 사용자 추가를 클릭한다. 사용자

jhlee-developer.tistory.com

IAM에서는 ECS를 관리하기 위해서 다음 두 정책을 연결해줘야 한다.

그래야 ECS/ECR 관련한 리소스에 접근할 수 있다.

  • AmazonECS FullAccess
  • AmazonEC2ContainerRegistryFullAccess

 

IAM을 생성하고 리포지토리 이름을 누르면 우측 상단에

'푸시 명령 보기'가 있다.

설명도 잘 써져 있으니 읽어보면 된다.

shell을 열고, 이대로만 설정하면 바로 이미지가 올라가는 것을 확인할 수 있다.

나는 MacOS에서 하지않고 Windows 환경으로 했기에 windows를 선택해서 명령어를 쳤는데 되지 않았고,

aws cli가 떠있었기 때문에 powershell을 열어서 linux용 명령어를 집어넣었더니 됐다.

혹시나 shell에서

PS C:\Windows\system32> aws --version
aws-cli/2.13.8 Python/3.11.4 Windows/10 exe/AMD64 prompt/off

aws --version을 쳤을 때 명령이 실행되지 않는다면 aws cli를 설치하면 된다.

AWS CLI를 설치하거나, ver 1에서 2로 업그레이드하는 방법에 대해서는 블로그를 첨부한다.

https://jhlee-developer.tistory.com/entry/AWS-ECS-%EC%82%AC%EC%9A%A9

 

[AWS] AWS CLI 설치, 업데이트 방법(V1 → V2)

AWS Command Line Interface (AWS CLI) AWS 서비스를 관리하는 통합 도구 여러 개의 AWS 서비스를 명령줄에서 제어하고 스크립트를 통해 자동화 가능 AWS CLI 설치 Linux $ curl "https://awscli.amazonaws.com/awscli-exe-linux-

jhlee-developer.tistory.com

 

혹시나,

Unable to locate credentials.
You can configure credential by running “aws configure”.

라는 문구가 뜨면, AWS CLI 초기 세팅을 해주면 된다.

보통 aws --configure에서 아이디와 비밀번호를 설정해주면 된다.

https://kimjingo.tistory.com/209

위의 블로그를 참고하자 :-)

 

이미지를 푸시하고, 이 화면까지 왔다면 이제 다음으로는 태스크 정의(Task Definition)을 설정하자.

 

2. Task Definition

> ECS > 태스크 정의 > 새 태스크 정의 생성

과거에 ECS를 썼던 사람이나, 새 ECS환경이 너무 복잡하다고 생각하는 사람은

좌측 상단의

새로운 ECS환경을 on/off로 해서 본인에게 편리한 설정으로 자유롭게 이동할 수 있다. 물론 옵션은 똑같고 UI의 변경만 있다.

태스크 정의 이름 / OS는 설정할 수 있지만, 태스크 역할이 비어있는 것을 볼 수있다.

IAM에서 설정을 해주자.

 

>IAM > 역할 > 역할 만들기

ECS TASK를 설정하고 넘어가자.

권한에 ECSTaskExecutionRole을 설정하고 넘어간다.

이름도 바꿔주자.

 

이제 다시 돌아와서 새로고침을 누르면 태스크 역할에 만든 role이 보인다.

 

메모리와 vCPU는 가장 작게 설정해준다.

이제, 컨테이너 추가를 해보자.

컨테이너의 이미지는 베이스로 할 이미지를 선택해야 한다. 이 이미지를 베이스로 컨테이너를 띄우겠다는 뜻.

아까 레지스트리에서 push해준 이미지의 URI를 집어넣으면 된다. 못 찾겠으면 새 창을 띄우고

> ECR > 리포지토리 > 이미지 > 상세 이미지

의 정보에서 이미지 URI를 복사해서 붙여넣기 하면 된다.

 

포트 매핑도 80, 8080, 3000 등 본인이 원하는 포트로 매핑을 하자.

다른 설정은 건드리지 않고 생성을 누르면

task-definition이 생성되었다. 이 작업 정의로 태스크만 만들수도 있고, 서비스도 만들 수 있다.

우리는 서버에서 서비스를 돌려볼 것이므로 서비스 생성을 누른다.

4. Service

시작 유형은 아까와 같이 fargate, 

이름과 작업 개수를 설정해주고, 다음을 누른다.

아까 생성했던 클러스터와 서브넷까지 설정해주고, 보안 그룹도 편집해준다.

보안그룹에서 중요한 것은

포트 매핑이 제대로 되지 않아서 나는 오류가 많으므로, 포트 매핑을 신경써서 해주자.

보통 기본적으로 잘 되어있다. 

로드 밸런서에는 다양한 로드 밸런서가 있으나, 우리는 이것저것 관리를 다 해주는 ALB를 선택할 것이다.

로드 밸런서를 만들러 가보자.

ELB(Elastic Load Balancer)

>EC2 > 로드 밸런서

아까와 같은 로드 밸런서 타입이 있는데, 우리는 ALB를 고른다.

 

아까 선택했던 VPC와 매핑까지 해주자.

이제, 대상 그룹을 지정해야 한다.

ALB는 로드 밸런서를 만들어서 그 대상에 대해 조정해야 하는데,

우리는 사실 아직 컨테이너를 띄우지 않았으므로 가짜 대상 그룹을 만들어서 넣어주고 삭제한 뒤 클린한 로드 밸런서를 만들어서 그 로드 밸런서를 ECS 서비스에서 ALB로 지정할 것이다.

콘솔의 맹점이라고도 볼 수 있지만, 일단 대상 그룹을 설정하지 않으면 진행이 불가능하므로

대상 그룹 생성을 해 준다. 대신 옵션은 그냥 아무거나 넣어준다.

> EC2 > 대상 그룹 > 대상 그룹 생성

대상 그룹 이름을 적어주고, 다음으로 넘어간다.

ip주소도 아무거나 써주고 생성한다.

그러면 이렇게 대상 그룹이 생성됨을 확인할 수 있다.

 

다시 ECS 설정으로 넘어가자.

새로고침 버튼을 누르면, 우리가 만든 ALB를 넣을 수 있다.

이제 로드 밸런서 생성 버튼을 눌러보자.

이제 파란 링크를 통해서 아까 만들어준 가짜 리스너와 타겟 그룹을 지워주자.

리스너를 지웠으면, 타겟도 지워주자.

이제 클린한 상태의 ALB를 만들었다. 이제 이 ALB를 서비스에 붙이면서, 타겟 그룹을 다시 우리 컨테이너로 연결한다.

 

다시 ECS 설정으로 되돌아가자. 이 때 즈음 나는 내가 어느 뎁스까지 기어들어간건지도 헷갈리기 시작했다.

>ECS 

이 컨테이너를 로드밸런싱 할 것이므로, 로드 밸런서에 추가 버튼을 눌러준다.

리스너 포트를 설정해주고, 다음을 누른다.

이제 거의 다 왔다.

오토스케일링에서 최소 태스크의 개수, 최대 태스크의 개수 등을 설정할 수 있다.

다음을 누르면 

서비스가 만들어지고, 

 

이제 이렇게 우리의 클러스터가 만들어졌다. 박수! :)

클러스터를 호출도 해보고, 다양한 테스트를 만들어보자.

이제 도커를 잘 쓰고 활용할 수 있게 되었다.

그런데, 개발자가 해야 하는 일 중 도커 컨테이너를 관리하는 것은 극히 일부분일 뿐이다.

만약에, 컨테이너를 돌릴 때 컨테이너에 깔려있는 특정 어플리케이션의 버전이 바뀌었다거나, 컨테이너에 트래픽이 몰린다거나, 버그가 발생한다면

개발자가 일일히 컨테이너를 돌면서 버전을 조사하고, 로드 밸런싱을 하고, 컨테이너를 증설하고, 버그 때문에 컨테이너를 내렸다가 올리기에는 너무나 많은 인적 소모가 있다.

 

그래서 나온 것이 '컨테이너 오케스트레이션 툴'이다.

오케스트레이션이란 그 말 뜻 그대로 여러 개의 컴퓨터 시스템, 앱, 서비스 등을 조율하고 관리하는 것으로, 여러개의 작업을 함께 연결하여 방대한 work-flow나 프로세스를 실행한다.

이름을 보고 '오케스트라'가 떠올랐다면, '여러 가지 악기를 조율해서 공연을 만들어 가는' 그 느낌 그대로 이해하면 된다.

 

컨테이너 오케스트레이션 툴과 제공하는 업체를 크게 살펴보자면,

  • GCP(Google Cloud Flatform)에서 제공하는
    • GKE(Google Kubernetes Engine)
  • AWS(Amazon Web Service)에서 제공하는
    • EKS(Elastic Kubernetes Service)
    • ECS(Elastic Container Service)

등이 있다.

 

우리가 컨테이너를 제어하고, 만들어내기 위해 여태껏 이미지도 만들고, 레지스트리에 등록도 하고 해 보았는데,

이것을 프로덕션 환경에서 컨테이너들을 구성해서 실제 서비스를 할 때,

'컨테이너를 제어하는데 필요한 여러가지 요소들'을 컨테이너 오케스트레이션이라고 한다.

그럼, 컨테이너 오케스트레이션은 무슨 일을 할까?

  • 컨테이너 클러스터링(Clustering)
    • 여러 대의 노드(node)를 하나의 클러스터(cluster)로 묶어서, 어플리케이션을 분산하고 실행하고, 자원을 효율적으로 이용하는 기능
    • 여러 대의 물리적이거나 가상의 서버를 하나의 시스템처럼 동작하게 만드는 기술
    • 컨테이너를 실행하는 호스트의 자원을 효율적으로 분배, 컨테이너가 안정적으로 실행 되도록 함.
    • 여러 대의 컨테이너를 묶어 하나의 서버처럼 사용 할 수 있도록 지원
  • 서비스 디스커버리(Service Discovery)
    • 컨테이너를 '자동으로 발견'하고, 서비스 이름과 IP 주소 등을 관리하여, 애플리케이션 간의 연결을 관리하는 기능
    • 클라우드 환경에서의 컨테이너 생성, 배치, 이동에 따른 IP, Port 정보 업데이트 및 관리
    • 결국 컨테이너를 알아서 찾은 다음, 그 컨테이너에 관련된 잡다한 관리들을 전부 다 해준다.
  • 자동 스케일링(Autoscaling) - 중요
    • 애플리케이션의 트래픽 양에 따라 자동으로 컨테이너 수를 조절하여, 자원 사용량을 최적화하고, 가용성을 보장하는 기능
    • 트래픽을 감당할수 있도록 계속 제어해줌. 컨테이너를 띄우기도 하고 죽이기도 하면서 수 조절
  • 로드 밸런싱(Load Balancing)
    • 여러 대의 노드에서 실행 중인 컨테이너들을 조절하여, 트래픽을 균등하게 분배하여, 애플리케이션의 성능을 최적화하는 기능
  • 롤아웃과 롤백(Rollout and Rollback)
    • 새로운 버전의 애플리케이션을 롤아웃하고, 이전 버전으로 롤백하는 기능
    • 컨테이너에 버그가 있거나, 컨테이너가 장애가 나서 죽었을때 빠르게 복구해준다.
  • 자동 복구(Automatic Recovery)
    • 컨테이너나 노드의 장애 시 자동으로 복구하는 기능
  • 모니터링과 로깅(Monitoring and Logging)
    • 컨테이너나 노드의 상태를 모니터링하고, 로그를 수집하여, 애플리케이션의 성능과 문제점을 분석하는 기능
  • 보안과 네트워크 관리(Security and Network Management)
    • 컨테이너와 노드의 보안을 관리하고, 네트워크 설정을 관리하는 기능

이것들을 비롯한 '관리'와 관련된 많은 일을 하는 것이 컨테이너 오케스트레이션이다.

 

그럼, 대표적인 컨테이너 오케스트레이션 툴에는 뭐가 있을까?

1. Docker swarm

  • Docker Inc.이 개발한 도커 컨테이너 오케스트레이션 도구
  • Docker Swarm은 2015년 Docker 1.12 버전에서 처음 발표됨
  • 이전에는 Docker Swarm Classic이라는 이름으로, Swarm 모드가 추가되기 전에 도커 엔진에 통합되기 전에 독립적으로 배포
  • Docker Swarm은 쿠버네티스가 등장하기 전까지 가장 대중적인 컨테이너 오케스트레이션 도구 중 하나
  • 간단하게 작동하고, 설정이 쉬움

2. Kubernetes

  • 배경
    • 구글은 Gmail, Youtube, 검색 등 다양한 웹 서비스가 있고, 이 서비스에 대한 대용량 트래픽을 감당하기 위해 쿠버네티스를 만들었고, 실제 서비스에 적용함
    • 기존 서비스를 유지하며 계속 업데이트를 통해서 고도화할 수 있음.
  • 오픈 소스 기반, 구글에서 설계, 현재 리눅스 재단에 의해 관리.
  • 쿠버네티스 v1.0은 2015년 7월 21일에 출시됨
  • 대규모에 적합함
    • 스케일링 기능 강화 (Replication Controller: 컨테이너 수를 동적으로 조절)
    • 서비스 디스커버리 기능 강화 (DNS 기반)
  • 가장 기능이 풍부하고 널리 사용되는 컨테이너 오케스트레이션 프레임워크
  • 베어 메탈, VM환경, 퍼블릿 클라우드 등의 다양한 환경에서 작동하도록 설계되어 있음.
  • https://kubernetes.io/ko/docs/home/

 

3. GKE(Google Kubernetes Engine)

  • Google Cloud Platform(GCP)에서 제공하는 Kubernetes 기반의 관리형 컨테이너 오케스트레이션 서비스
  • 따라서 GKE는 Kubernetes를 기반으로 하고, Kubernetes의 기능을 모두 제공

4. EKS (Amazon Elastic Kubernetes Service)

  • AWS 제공하는 관리형 Kubernetes 서비스
  • EKS는 Kubernetes 기반으로 구축되어 있음
  • 사용자는 Kubernetes API를 사용하여 EKS 클러스터를 관리할 수 있다.

5. ECS (Amazon Elastic Container Service)

  • AWS에서 제공하는 관리형 컨테이너 오케스트레이션 서비스
  • Docker 컨테이너를 실행하기 위한 기능을 제공
  • 사용자는 ECS를 사용하여 컨테이너를 배포, 관리, 스케일링
  • 쿠버네티스의 복잡한 기능/세팅을 간소화하여 개발자들이 인프라에 대한 신경을 덜게끔 함
  • https://docs.aws.amazon.com/ko_kr/ecs/index.html

등, 정말 많은 오케스트레이션 툴이 있다.

다음 글에서는 쿠버네티스를, 이번 글에서는 ECS에 대해 다룰 예정이다.

 

ECS(Amazon Elastic Container Service)

AWS ECS에는 세 가지 계층이 있다.

  • 용량 : 컨테이너가 실행되는 인프라(컴퓨팅 자원)
  • 컨트롤러 : 컨테이너에서 실행되는 app을 배포하고 관리함
  • 프로비저닝 : 스케줄러와 인터페이스하여 애플리케이션과 컨테이너를 배포하고 관리하는데 사용하는 도구

이 컨트롤러를 통해 Capacity options(=컴퓨팅 자원)을 가지고, 이 자원 위에 어플리케이션들을 올린다.

이 Capacity options를 이용하는 방법이 3가지가 있는데,

  1. EC2 instance를 이용하는 방법
  2. Fargate방식을 이용하는 방법( = EC2를 조금 더 추상화 하였으며, EC2 관리도 귀찮다, 싫다 할때 사용)
  3. 온프레미스(외부) 컴퓨팅 자원을 이용하는 방법(효율적이지 않음)

조금 더 자세하게 살펴보자.

Task가 무엇인지는 조금 있다가 알아보도록 하자.

EC2방식?

초기 컴퓨팅 자원을 EC2 인스턴스에 할당한 뒤, Task에 필요한만큼을 할당한다.

용량공급자(Capacity Providers)를 통해 EC2 Auto-Scaling Group을 연결하고,

위의 그림을 보면 Auto-Scaling Group이 EC2 인스턴스에 vCPU 4개를 인스턴스에 할당한 뒤, 2개만 Task에 할당하는 구조이다. 그래서 실제로 도커 컨테이너가 실행되는 곳은 EC2 Instance 내부이다.

또한, ECS에서 제공하는 관리형 지표인 "Capacity Provider Reservation"에 따라 임의로 EC2 용량을 추가/제거 할 수 있으며, 컨테이너 개수의 증가/감소에 따라 EC2도 함께 증가/감소하게 된다.

- 과금 : 호스트로 사용하는 EC2 요금만 부과된다.

Fargate방식?

공식 문서에 따르면,

'AWS Fargate Fargate는 Amazon EC2 인스턴스의 서버나 클러스터를 관리할 필요 없이 컨테이너를 실행하기 위해 Amazon ECS에 사용할 수 있는 기술입니다. Fargate를 사용하면 더 이상 컨테이너를 실행하기 위해 가상 머신의 클러스터를 프로비저닝, 구성 또는 조정할 필요가 없습니다. 따라서 서버 유형을 선택하거나, 클러스터를 조정할 시점을 결정하거나, 클러스터 패킹을 최적화할 필요가 없습니다.' 

라고 적혀있다.

서버리스란?
EC2 인스턴스등 호스트 컴퓨팅을 사용하지 않고 컴퓨팅 자원을 제공해주는 서비스

결국, 이용자는 컨테이너가 어디서 운영되는지 관리할 필요가 없다. 그저 ECS에서 알아서 컴퓨팅 자원을 Task에 배분해준다. 사진을 보면 EC2 인스턴스를 사용하지 않고도 알아서 Task에 vCPU 2대가 할당되어있는 것을 볼 수 있다. 

Fargate 방식은 효율적으로 자원을 분배하기 위해 EC2 인스턴스조차도 추상화해놓았기 때문에,

극단적인 예를 들면 1/2vCPU, 1/4vCPU를 Task에 분배하는 것조차 가능하다.

- 과금 : 시간당 vCPU, Storage 용량 비용이 부과된다.

 

개인적으로는 이렇게 이해했다. 물론 완벽한 비유는 아니다.

- 스프링(리액트)의 오만 떼만 설정방식때문에 세팅하는데에 시간이 너무 길어지고, 그 시간을 조금 더 개발 시간에 투자하기 위해 스프링 부트(create-react-app / start.spring.io)가 등장했듯, 쿠버네티스의 초기 세팅이나 튜닝, 또 컨테이너 관리에 걸리는 시간을 조금 더 단축하고 ECS의 fargate 방식이 등장했다.

- 물론, 상세한 튜닝이나 설정이 필요한 경우 스프링 부트 대신 스프링을 사용해야 하듯, ECS보다 EKS를 사용한다.

External방식?

AWS 컴퓨팅 자원을 이용하지 않고, 외부 리소스를 컨테이너에 할당하는 유형이나 많이 이용되지는 않는다.

호스트나 컨테이너 등의 실제 서비스는 물리적으로 AWS 밖에서 동작하며, 이것을 모니터링하거나 관리할 때에는 AWS 콘솔을 이용한다.

그럼 효율적인 자원 관리를 해주는 Fargate 방식이 EC2보다 더 비싸겠네요?

꼭 그런건 아니다. 상황에 따라 다르다.

EC2방식은 컨테이너를 띄우기 위해서는 EC2 인스턴스가 계속 켜져 있어야 한다. 그래서 계속 비용이 발생하고, Fargate방식은 효율적 자원 분배를 할 수 있게 해주기 때문에 트래픽이 적을 때에는 Fargate방식이 오히려 저렴하고, 트래픽이 많을 때에도 상황에 따라 Fargate방식이 더 저렴한 경우도 있다.

 

ECS 구성

이제 실제적으로 ECS가 어떻게 구성되어있는지를 살펴보자.

AWS Cloud라는 가상 공간 안에 VPC를 만들고, VPC 안에 ECS 클러스터가 존재한다.

1. Task Definition

Task definition이란 컨테이너를 설명하는 json파일로, 다음의 정보를 포함한다.

  • 컨테이너 이미지: 어떤 이미지를 기반으로 컨테이너를 만들 것인지
  • 컨테이너 vCPU / Memory 자원 할당
  • 포트 구성
  • 컨테이너 환경변수
  • 컨테이너 로그
  • 이 외에 도커 컨테이너를 정의하는 모든 설정 파일

어떻게 보면 docker-compose와 닮았고, 실제로 Task definition은 docker-compose를 지원한다.

2. Task

쿠버네티스 클러스터 안의 'pod'와 유사한 개념이다. ECS에서 배포할 수 있는 가장 작은 단위로, 도커 컨테이너의 작업 단위이다.

3. Service

쿠버네티스 클러스터 안의 'node'와 유사한 개념이다.

특정 작업 정의에 기반한 작업 집합을 실행하고 유지,관리하는 역할

  • 특정 작업의 인스턴스를 지정된 수로 유지하거나, 부하 분산, 서비스 발견, 롤링 업데이트 등과 같은 추가 기능을 제공
  • Task를 몇개나 유지할 것인지 등을 Service로 묶어서 관리함

주요 기능 및 목적

  • Desired Count
    • 사용자가 지정한 수의 작업 인스턴스를 지속적으로 실행하도록 보장
    • 만약 작업 인스턴스가 실패하거나 중지되면 ECS는 자동으로 새 작업을 시작하여 원하는 수의 작업 을 유지
  • Load Balancing
    • Application Load Balancer, Network Load Balancer 또는 Classic Load Balancer와 통합 될 수 있음
    • 트래픽이 ECS 작업에 균일하게 분산될 수 있도록 함
  • Service Discovery
    • 동적 IP 주소(EIP)를 사용하여 서비스를 검색하고 연결
  • Rolling Updates
    • 애플리케이션 업데이트
    • 작업 정의(서비스의 개수, 관리 방법 등)를 업데이트하면서 서비스를 지속적으로 실행할 수 있도록 함
  • Scaling
    • 요구 사항 / 정책에 따라 자동으로 확장 및 축소 시킴

지난 글에 build를 통해 도커파일(금형 설계서)를 가지고 이미지(금형)를 만들어서 레지스트리에 공유하는 작업까지 같이 해 보았다.

 

그런데, 이미지 생성 방식에는 dockerfile만을가지고 build하는 방법 외에도, 기존 사용하고 있는 컨테이너를 이미지로 만드는 방법또한 존재한다.

바로 commit라는 기능이다.

 

도커파일로 빌드하면 되는데 커밋 왜 씀?이라는 물음에 대답하자면,

물론 build와 commit의 공통점은

'나만의 커스텀 이미지를 만들 수 있다'는 것이지만,

도커파일로 build하는것이 아무것도 없는 백지상태에서 이미지를 새로 창조하는 것이라면,

commit은 기존에 쓰고 있는 컨테이너를 보통 '백업 용도'로 이미지를 생성하는 것이 차이점이다.

 

조금 더 자세한 예를 들어보자.

우리가 ubuntu라는 이미지를 pull하고, run해서 container를 만들었다고 치자.

팀 모두는 git을 이용해 형상관리를 해야 하고,

A팀은 python3를,

B팀은 php를,

C팀은 Nodejs를 이용해서 개발을 한다.

그럼 결국 3개의 컨테이너는 다음과 같은 구상을 띄어야 한다.

  • A container : ubuntu + git + python3
  • B container : ubuntu + git + php
  • C container : ubuntu + git + nodeJs

이 때 이용하는것이 commit이다.

ubuntu가 설치되어 있는 컨테이너에 git을 설치하고, commit을 해서 ubuntu+git이 설치되어 있는 컨테이너를 만들고,

그 컨테이너에서 이미지를 추출해서 A/B/C팀에 전달하고 각 팀은 자신이 필요한 개발 툴만 새로 설치해서 개발을 진행하면 된다.

도식도로 확인하면 다음과 같은 시나리오가 완성된다.

물론, dockerfile로도 가능은 하다.

 

그럼 위의 케이스 대로 한번 실행을 해보자.

먼저, ubuntu를 설치한다.

$ docker pull ubuntu
Using default tag: latest
latest: Pulling from library/ubuntu
Digest: sha256:0bced47fffa3361afa981854fcabcd4577cd43cebbb808cea2b1f33a3dd7f508
Status: Downloaded newer image for ubuntu:latest
docker.io/library/ubuntu:latest

이제는 컨테이너를 띄워보자. 컨테이너 안에 들어가서 작업할 것이기 때문에 -it 명령어를 준다.

$ winpty docker run -it --name my-ubuntu ubuntu bash
root@304d6eb32b3f:/#

winpty 명령어는 내가 윈도우의 bash를 설치할 때 기본 쉘로 설정하지 않아서 tty 설정때문에 준 옵션이고,

컨테이너의 이름을 잘 알아보기 위해 my-ubuntu라는 이름을 주었다.

그리고 ubuntu 이미지를 기반으로 하는 컨테이너를 띄우고 bash shell을 띄우기 위해 위의 명령어들을 주었다.

root@304d6eb32b3f:/# apt update
root@304d6eb32b3f:/# apt install git

주의할 점은, 이렇게 설치한 git은 이 컨테이너에서만 깔려있지, 새로운 컨테이너에서는 깔려 있지 않다는 점이다.

이제 이 컨테이너에서 나와서, 이미지를 만들어보자.

$ docker commit my-ubuntu choincnp:ubuntu-git
sha256:117b6e2b4de30fcbdf08983bd98d4bfde6a410fd63f54bf25c3e137bbd6006f3

$ docker images
REPOSITORY               TAG          IMAGE ID       CREATED         SIZE
choincnp                 ubuntu-git   117b6e2b4de3   5 seconds ago   196MB

docker commit [IMAGE NAME] [REPOSITORY] : [TAG]

의 syntax로 작성하면 된다. 

https://choincnp.tistory.com/72

에서 기술했던 도커 허브에 이미지를 업로드할때와 똑같은 구조이다.

 

이제 이 이미지를 기반으로 다시 컨테이너를 만들고,

$ docker run -it --name nodejs choincnp:ubuntu-git bash

# apt update
# apt install nodejs

이렇게, C팀의 컨테이너를 만들 수 있다. 이런식으로 A팀과 B팀의 컨테이너도 만들어서 돌릴 수 있다.

물론, 이를 도커파일로도 만들어 제공할 수 있다.

dockerfile

FROM ubuntu:latest
RUN apt update && apt install -y git

도커파일에서 RUN을 돌릴때마다 레이어가 하나씩 추가되므로 가급적이면 한 번에 쓸 명령어는 한 줄에 위치시키는 것이 좋다. 설치할 때 INSTALL을 돌리면 아까 봤듯 설치할거냐고 묻는 ‘Y/N’ 이 뜨는데, 이게 뜨면 도커는 설치를 하지 않는다. 그래서 Y를 선택하게 하는 -Y 옵션도 준다. 

도커파일의 다른 옵션들은 이전 글에 설명하였으므로 따로 기술하지 않겠다.


도커파일은 이런 일련의 명령어들을 한 번에 스크립트로 작성해서 보여주므로,

이 이미지가 어떻게 생성되었는지에 대해 굉장히 명료하다.

 

그래서 도커파일로 build하는 방법과 commit하는 방법, 두 가지에 대해서 명확하게 알고

자기가 잘 사용할 수 있는 방법에 대해 유연하게 대처하는 것이

도커를 다루는 능력의 핵심이다.

지난 글까지는 컨테이너를 다루는 것에 대해서만 배웠다.

이번 글에서는 도커 이미지를 업로드하는것을 배워보자.

 

다시 한 번 도커파일과 이미지의 관계에 대해서 정리해보자.

결국 도커 이미지를 만든다는 것은,

금형 설계서로 '제작'이라는 행위를 통해 금형을 만드는 것과 마찬가지로

Dockerfile을  'build'해서 제작 틀을 만들어서 추후 컨테이너 인스턴스를 뽑아내는 것과 유사하다.

 

실제 실무에서 사용하는 dockerfile 예제를 보고 참고해보자.

FROM golang:latest

RUN mkdir /appname
WORKDIR /appname
ADD bin/appname bin/appname
ADD conf conf
ADD public public
ARG BUILD_PORT
ENV PORT $BUILD_PORT
EXPOSE $BUILD_PORT
ENTRYPOINT ["bin/appname"]

https://docs.docker.com/build/guide/intro/

도커파일에 대한 공식 문서를 보면 나와있듯, syntax에 맞춰서 조건을 적으면 된다.

syntax는 여러가지가 있지만, 대표적으로 쓰이는 문법들은 다음과 같다.

  • FROM: 베이스 이미지 선정
  • INSTALL: 명령어를 기반으로 파일을 설치
    • shell에서 bash를 실행해 install을 돌리게 되면 진짜 설치할거냐고 묻는 ‘Y/N’ 이 뜨는데, 이게 뜨면 도커는 설치를 하지 않는다. 그래서 Y를 선택하게 하는 -Y 옵션도 주는 경우가 있다.
  • WORKDIR: work directory 선정
    • 사용자의 요청이 들어왔을 때, 그 요청을 어떤 디렉토리에서 찾게 하고 싶은가에 대해 선정하는 것
    • WORKDIR /var/www/html을 작성하면 shell에서는 mkdir -p /var/www/html을 하는 것과 같다.
    • 또한 추가적으로 현재 위치를 이동시키기까지 한다.
  • COPY: 복사할 파일 선정 (예: 작업한 서비스 파일들)
    • COPY [HOST] [DEST] 구조로 되어 있으며, host에 있는 파일을 DEST까지 복사한다.
  • RUN: 실행할 명령어
  • CMD: 실행할 명령어
    • RUN과 CMD는 실행할 명령어라는 공통점이 있지만,
    • RUN은 빌드가 실행되는 시점에서의 명령어로 "이미지에 반영"되고,
    • CMD는 빌드 후 컨테이너가 run될 때 실행되는 명령어로 "컨테이너에 반영"된다.
    • CMD는 조금 더 특이하게, PWD 명령어로도 오버라이딩 할 수 있다.
  • ENTRYPOINT: 컨테이너가 시작할 때 실행할 명령어 (예: 서버 실행)
  • EXPOSE: 컨테이너 외부에 노출할 포트 지정
  • ENV: 환경 변수 지정

자세한 예시를 덧붙여 더 많은 syntax를 작성한 글이 있다.

https://rampart81.github.io/post/dockerfile_instructions/#run

추후 도커파일을 만들 시에 참고하자.

 

이렇게 도커파일을 만들었으면, 이미지를 만들기 위해 build를 해보자.

빌드 신택스는 다음과 같다.

docker build [OPTIONS] PATH | URL | -

$ docker build -t web-server-build .

-t 옵션으로 태그(이미지 이름)를 달아주었다.

또, 현재 디렉토리에 도커파일이 있고, 이미지에 사용하는 파일들을 현재 디렉토리에 위치시킬 것이기 때문에 .(온점)을 달아준다.

 

한번 빌드 했으면, 나중에 태그를 바꿔 줄 때에는 

$ docker image tag [image name]

이 명령어를 주면 된다. 지금은 이미지 이름이 필요 없어도, 추후 태그가 필요한 상황이 꼭 오니 알아두자.

 

이렇게 만든 이미지를, docker hub라는 레지스트리에 올려서 많은 사람들이 내 이미지를 써보게 할 수도 있다.

이 이미지를 docker hub에 올려보자.

도커 허브에 이미지를 올릴 때에는 주의사항이 있다.

바로 '[Docker Hub 사용자명]/이미지명:[태그명]'이 있어야 한다.

따라서 아까 image tag 명령어를 이용해서 적절하게 이미지명이나 태그명을 변경해주자.

먼저, shell에서 docker hub에 로그인을 해야 한다. 로그아웃할때는 login 대신 logout을 치면 된다.

$ docker login
Username: choincnp
Password:

Authenticating with existing credentials...
Login Succeeded

Logging in with your password grants your terminal complete access to your account.
For better security, log in with a limited-privilege personal access token. Learn more at https://docs.docker.com/go/access-tokens/

성공적으로 로그인 했으면, 내가 만든 이미지를

이미지 태그를 바꿔서 그냥 push하기만 하면 된다.

$ docker image tag choincnp:ubuntu-git choincnp/test:latest
#                    이미지 이름     사용자명 /레포지토리명 : 태그
$ docker images
REPOSITORY               TAG          IMAGE ID       CREATED             SIZE
choincnp/test            latest       4999083f3cd2   2 hours ago         196MB
choincnp                 ubuntu-git   4999083f3cd2   2 hours ago         196MB

$ docker push choincnp/test:latest
The push refers to repository [docker.io/choincnp/test]
2439ff1afd0d: Pushed
59c56aee1fb4: Pushed
latest: digest: sha256:3954063a2c7d19a2dece00a8b8469efcdcf7b88dc5e43245e84cea301af68ef9 size: 741

 

 

도커허브에서 레포지토리에 들어가보면 내 이미지가 성공적으로 push되었다는 것을 알 수 있다.

이제 이 이미지 디렉토리인 choincnp/test/general을 통해, 사람들은

docker pull choincnp/test/general

라는 명령어로 내가 만든 이미지를 제각기 다른 환경에서 사용할 수 있다.

 

github처럼 immutable한 버전컨트롤인지는 다시 확인 해 봐야겠지만, 일단 성공했으므로 박수를 먼저 쳐보자.

여러 사람들로 구성된 팀 프로젝트에서 나 혼자만 docker를 알고 있고, 다른 사람에게도 도커 사용을 권한다면

다른 사람들은 다음과 같은 코드를 shell에 작성하면서 내가 대체 뭘 저지르고 있는가에 대해 한참 생각할 것이다.

docker \
run \
    --name "db" \
    -v "$(pwd)/db_data:/var/lib/mysql" \
    -e "MYSQL_ROOT_PASSWORD=root_pass" \
    -e "MYSQL_DATABASE=wordpress" \
    -e "MYSQL_USER=docker_pro" \
    -e "MYSQL_PASSWORD=docker_pro_pass" \
    --network wordpress_net \
mysql:latest

docker \
    run \
    --name app \
    -v "$(pwd)/app_data:/var/www/html" \
    -e "WORDPRESS_DB_HOST=db" \
    -e "WORDPRESS_DB_NAME=wordpress" \
    -e "WORDPRESS_DB_USER=docker_pro" \
    -e "WORDPRESS_DB_PASSWORD=docker_pro_pass" \
    -e "WORDPRESS_DEBUG=1" \
    -p 8000:80 \
    --network wordpress_net \
wordpress:latest

우리는 이제 도커를 어떻게 사용하는 지 알았으니 위의 스크립트를 해석할 수 있지만, 다른 팀원들은 '도대체 이게 뭔데?'라고 생각할 수도 있을 것이다.

그리고, 위와 같은 스크립트를 shell 환경에서 돌린다면 최소한 3개 이상의 shell이 필요하기도 하고,

위의 예제인 wordpress같은 경우에는 mysql 컨테이너를 필수적으로 띄운 상황에서 띄워야 하는데 저 스크립트만 가지고는 '그냥 명령어네, 아무거나 먼저 띄워야지'라고 생각하고 run을 한 경우에는 불상사가 발생할수도 있다.

 

그래서 다음과 같은, 자연어와 비슷하고, yaml 형식으로 제작된 docker-compose가 등장했다.

version: "3.0"

services:
  db:
    image: mysql:latest
    volumes:
      - ./db_data:/var/lib/mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root_pass
      MYSQL_DATABASE: wordpress
      MYSQL_USER: docker_pro
      MYSQL_PASSWORD: docker_pro_pass
  
  app:
    depends_on: 
      - db
    image: wordpress:latest
    volumes:
      - ./app_data:/var/www/html
    ports:
      - "8000:80"
    restart: always
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_USER: docker_pro
      WORDPRESS_DB_PASSWORD: docker_pro_pass

이와 같은 문서를 작성한 후, 쉘을 딱 하나만 켜서

$ docker-compose up

이라는 명령어만 실행하면 모든 컨테이너가 알아서 실행되는, 그런 마법같은 일을 docker-compose를 통해 할 수 있다.

 

예제를 통해 바로 시작해보자.

우리는 Wordpress라는, 설치형 블로그 어플을 사용하고 싶다.

그런데 Wordpress를 띄우려면, MySQL 컨테이너를 선행해서 띄워야 한다.

일단 쉘을 켜서 다음 명령어를 쳐보자.

$ docker network create wordpress_net

$ docker network ls
PS docker-pro-2308\lecture\2nd> docker network ls
NETWORK ID     NAME            DRIVER    SCOPE
440dbeb16442   2nd_default     bridge    local
7fb999d3d64f   bridge          bridge    local
483de980f145   host            host      local
3f40074674cc   none            null      local
24113129f7c6   wordpress_net   bridge    local

bridge / host / scope 등에 대해서는 이따가 다시 확인해보자.

다른 shell을 열어서, 이제는 아까 봤던 bash 명령어를 쳐보자.

docker \
run \
    --name "db" \
    -v "$(pwd)/db_data:/var/lib/mysql" \
    -e "MYSQL_ROOT_PASSWORD=root_pass" \
    -e "MYSQL_DATABASE=wordpress" \
    -e "MYSQL_USER=docker_pro" \
    -e "MYSQL_PASSWORD=docker_pro_pass" \
    --network wordpress_net \
mysql:latest

디렉토리로 확인해도 되고, vsc등의 idea에서 확인해보면 db_data라는 폴더가 생성되었을 것이다.

이것은 ‘앞으로 컨테이너를 껐다 켜도 db가 유지됨’을 의미한다.

다음 명령어도 작성해보자.

docker \
    run \
    --name app \
    -v "$(pwd)/app_data:/var/www/html" \
    -e "WORDPRESS_DB_HOST=db" \
    -e "WORDPRESS_DB_NAME=wordpress" \
    -e "WORDPRESS_DB_USER=docker_pro" \
    -e "WORDPRESS_DB_PASSWORD=docker_pro_pass" \
    -e "WORDPRESS_DEBUG=1" \
    -p 8000:80 \
    --network wordpress_net \
wordpress:latest

우리가 -p옵션으로 wordpress 컨테이너를 호스트의 8000번 포트에 연결했으니 들어가보도록 하자.

> localhost:8000

잘 실행되는것을 볼 수 있다.

그런데 맨날 이렇게 명령어를 한번에 쳐서 띄우기는 쉽지 않으니, docker-compose.yml을 띄워서 이 컨테이너들을 관리해보자.

먼저 다음의 명령어로 지금 우리가 만들었던 워드프레스 컨테이너를 삭제해보자. 이름은 db와 app을 주었던 것을 기억하자.

$ docker rm --force app
$ docker rm --force db
$ docker network rm --force wordpress_net
$ docker ps -a
CONTAINER ID   IMAGE                    COMMAND                   CREATED        STATUS                      PORTS     NAMES
$ docker network ls
NETWORK ID     NAME          DRIVER    SCOPE
440dbeb16442   2nd_default   bridge    local
7fb999d3d64f   bridge        bridge    local
483de980f145   host          host      local
3f40074674cc   none          null      local

깔끔하게 지웠으면, 다음 명령어를 다시 입력해보자.

$ docker-compose up

그러면 뭐가 좌르륵 실행되면서 아까와 같은 db_data, app_data 폴더가 다시 생기는것을 확인할 수 있고,

브라우저에서 localhost:8000로 접속해 보면 아까와 같이 워드프레스도 잘 뜬다.

$ docker ps -a
CONTAINER ID   IMAGE                    COMMAND                   CREATED         STATUS                      PORTS                  NAMES   
e9966757bc11   wordpress:latest         "docker-entrypoint.s…"   2 minutes ago   Up 2 minutes                0.0.0.0:8000->80/tcp   2nd-app-1
986b8530bd9a   mysql:latest             "docker-entrypoint.s…"   2 minutes ago   Up 2 minutes                3306/tcp, 33060/tcp    2nd-db-1

컨테이너도 두개 다 잘 뜬것을 볼 수 있다.

반대로, 이것을 한번에 끄고 싶을때도 간편한 명령어도 쉽게 끌 수 있다.

$ docker-compose down

 

 

분석

이제, 우리가 여태 사용해왔던 shell 방식과, docker-compose yml파일을 실행시킨 것을 비교해보자.

  1. mySql
  2. 워드프레스

mysql은 기본적으로 호스트의 3306번 포트에 위치한다.

또한, /var/lib/mysql에 데이터가 저장된다.

먼저, mySql을 돌리기 위해서는 shell에 다음과 같은 명령어가 있어야 한다.

name은 db라고 지어주자.

$ docker \
run \
 --name "db"\
 mysql:latest

\(백슬래시)는, shell에게 ‘아직 안끝났어’라는 말과 같다.

docker-compose로 가보자.

version: "3.0"

services:
 db:
  images:mysql:latest

compose 파일을 작성하기 위해서는 처음에 version에 대한 명시가 필요한데, 보통 가장 처음에 3.0으로 설정해두는것이 ‘국룰’이라고 한다.

그리고 services 안에 내가 넣고 싶은 컨테이너를 넣는데,

services 바로 밑에 오는 저 db가 shell에서 --name 옵션을 주는 것과 동일하다.

 

그러나 이렇게만 만들면 지난 시간에 배웠듯, 컨테이너를 껐다 켜면 데이터가 유실되므로

호스트의 파일 시스템과 컨테이너의 파일 시스템을 연결해준다.(볼륨 마운트)

$ docker \
run \
 --name "db"\
 -v "$(pwd)/db_data:/var/lib/mysql" \
 mysql:latest

docker-compose.yml을 확인해보자.

version: "3.0"

services:
  db:
    image: mysql:latest
    volumes:
      - ./db_data:/var/lib/mysql

이렇게 호스트의 db_data라는 폴더와 컨테이너의 /var/lib/mysql이 연동되었다.

그 외에 id나 password 지정같은 또다른 옵션들을 줄 수 있다.

docker \
run \
    --name "db" \
    -v "$(pwd)/db_data:/var/lib/mysql" \
    -e "MYSQL_ROOT_PASSWORD=root_pass" \
    -e "MYSQL_DATABASE=wordpress" \
    -e "MYSQL_USER=docker_pro" \
    -e "MYSQL_PASSWORD=docker_pro_pass" \
    --network wordpress_net \
mysql:latest

결국 shell에서는 이와같은 스크립트가 탄생하게 되는 것이고,

docker-compose에서는 위의 옵션을

services:
  db:
    image: mysql:latest
    volumes:
      - ./db_data:/var/lib/mysql
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root_pass
      MYSQL_DATABASE: wordpress
      MYSQL_USER: docker_pro
      MYSQL_PASSWORD: docker_pro_pass

이런 yaml 파일 형식으로 줄 수 있는 것이다.

이 yml 파일을 조금 더 뜯어보자.

https://docs.docker.com/compose/compose-file/03-compose-file/

공식 문서를 보면, 다음과 같은 옵션들이 도커 컴포즈 파일에 들어갈 수 있다.

  • Version(선택적)
  • Services(필수)
  • Networks
  • Volumes
  • Configs
  • Secrets

등과 같은, 다양한 옵션들을 줄 수 있다.

여기서 필수 항목인 Services를 살펴보자.

Services란?

  • 실행하려는 컨테이너들을 정의하는 역할
  • 이름, 이미지, 포트 매핑, 환경 변수, 볼륨등을 포함
  • 해당 정보를 토대로 컨테이너를 생성하고 관리

이 services에 들어갈 수 있는 옵션들에는 어떤 것이 있을까?

  • image : 컨테이너를 생성할 때 쓰일 이미지 지정
  • build : 정의된 도커파일에서 이미지를 빌드해 서비스의 컨테이너를 생성하도록 설정
  • environment : 환경 변수 설정, docker run 명령어의 --env, -e 옵션과 동일
  • command : 컨테이너가 실행될 때 수행할 명령어, docker run 명령어의 마지막에 붙 는 커맨드와 동일
  • depends_on : 컨테이너 간의 의존성 주입, 명시된 컨테이너가 먼저 생성되고 실행
  • ports : 개방할 포트 지정, docker run 명령어의 -p와 동일
  • expose: 링크로 연계된 컨테이너에게만 공개할 포트 설정
  • volumes: 컨테이너에 볼룸을 마운트함
  • restart: 컨테이너가 종료될 때 재시작 정책
    • no: 재시작 되지 않음
    • always: 외부에 영향에 의해 종료 되었을 때 항상 재시작 (수동으로 끄기 전까지)
    • on-failure: 오류가 있을 시에 재시작

 

이 정보를 가지고, 워드프레스 앱 컨테이너를 만들어 보자.

 

워드프레스 컨테이너는 다음과 같이 웹 서버, php, 그리고 파일 시스템으로 /var/www/html이 있다.

웹서버가 있으므로 컨테이너에서는 80번 포트에서 명령을 대기하고 있다.

 

그런데, 여기서 중요한 것은 워드프레스는 db를 이용해야 하므로, 늘 먼저 mySql 컨테이너가 선행되어서 띄워져야 한다.

이 옵션을 docker-compose에서는 depends_on 옵션을 이용해서 설정해준다.

app:
    depends_on: 
      - db
    image: wordpress:latest
    volumes:
      - ./app_data:/var/www/html
    ports:
      - "8000:80"
    restart: always
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_USER: docker_pro
      WORDPRESS_DB_PASSWORD: docker_pro_pass

 

 

 

 

 

생각해보면, app 컨테이너는 외부에서 접근할 수 있어야 한다.

그러나 db 컨테이너는 외부에서 접근하면 큰일난다. 오로지 app 컨테이너에서만 접근할 수 있어야 한다.

그러므로 app 컨테이너만 ports 옵션을 주고, db 컨테이너는 ports의 옵션을 제외한다.

그리고 나머지 환경변수들을 입력해주면 된다.

 

이제, 마지막으로 network에 대해서 알아보자.

wordpress 입장에서 생각해보자.

워드프레스는 처음 어플리케이션을 실행하는데 db를 들고와야 한다.

그래서 db에 접속할 때 host에 db를 적는데, 매번 이렇게 접속할때마다 호스트의 db 전체주소를 넣기에는 굉장히 불편할 것이다.

그래서 컨테이너의 이름을 적으면 접속이 가능해지도록 하면 너무 편할 것 같다.

 

그래서 shell에서는 네트워크를 만들어서 편하게 접속할 수 있도록 각각 network를 넣어 주었다.

아까 도커 네트워크 리스트를 가져오는 명령어를 쳤던 것을 다시 보자.

$ docker network ls
NETWORK ID     NAME          DRIVER    SCOPE
440dbeb16442   2nd_default   bridge    local
7fb999d3d64f   bridge        bridge    local
483de980f145   host          host      local
3f40074674cc   none          null      local

네트워크 드라이버란, 이런 도커 컨테이너를 이어주는 역할을 한다.

 

도커에서는 몇 가지 자체제공하는 기본 드라이버들이 있는데, 확인해보자.

  • 브릿지(bridge) : 기본 네트워크 드라이버로, 동일한 도커 호스트에서 컨테이너의 역할을 도와준다.
  • 호스트(host) : 호스트의 네트워크를 직접 사용한다.
  • 오버레이(overlay) : 서로 다른 도커 호스트의 컨테이너간 통신을 도와준다.

이것 외에도 여러가지 옵션들이 많고, 서드파티 플러그인도 사용할 수 있다.

https://docs.docker.com/network/drivers/

조금 더 찾아보려면, 위의 사이트에서 더 확인해보자.

 

도커 네트워크에 컨테이너들이 물려있는지 확인하려면, 다음과 같은 명령어로 확인할 수 있다.

$ docker network inspect 2nd_default

[
    {
        "Name": "2nd_default",
        "Id": "20451de16bbc38d536447367679ebb8ae1bdcffd02f93417555fe7d6724c3a7f",
        "Created": "2023-08-07T17:48:53.788668019Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "22339f66dde5c3df5d0916118800940f7b453dc222de5846d328393bb05e53bb": {
                "Name": "2nd-db-1",
                "EndpointID": "d0e3890ec7c4875280851e5a9b07a3a66d6f78c0e04f043595dadab4b68254e4",
                "MacAddress": "02:42:ac:12:00:02",
                "IPv4Address": "172.18.0.2/16",
                "IPv6Address": ""
            },
            "47ba81760d077e720177d870ad00ba0984fd9a37501f2eb65cc16fe3fee4d0be": {
                "Name": "2nd-app-1",
                "EndpointID": "4354f2a5c84a6eb73346e23f37419bed8e84fbbe76898d8c684265712948a3cb",
                "MacAddress": "02:42:ac:12:00:03",
                "IPv4Address": "172.18.0.3/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {
            "com.docker.compose.network": "default",
            "com.docker.compose.project": "2nd",
            "com.docker.compose.version": "2.19.1"
        }
    }
]

여기에서 "containers" 항목을 보면, 우리가 실행한 두 개의 컨테이너가 2nd_default라는 네트워크에 물려 있음을 확인할 수 있다.

 

또한, docker compose도 여러 옵션을 주고 실행할 수 있는데, 예를 들어 다음과 같다.

  • up: 도커 컴포즈 파일로, 컨테이너를 생성하기
  • -f: 도커 컴포즈 파일 지정하기
  • -d: 백그라운드에서 실행하기

이게 도커-컴포즈의 끝이다.

 

마지막으로, 이 docker-compose로 띄웠던 컨테이너들이 유기적으로 만드는 사이클을 살펴보고, 다른 예제도 한번 살펴보고 글을 마치자.

docker-compose를 실행하고, 브라우저에서 localhost:8080에 접속하면 다음과 같은 일이 순차적으로 일어난다.

  • 호스트의 8080번 포트와 연결된 app 컨테이너의 웹 서버가 리스닝을 하고 있다가 연결을 받는다.
  • 웹 서버는 php에게 request를 넘긴다.
  • php는 /var/www/html의 디렉토리에 있는 php application을 읽어서 그대로 실행한다.
  • -v로 마운트했으므로 호스트의 파일 시스템인 app_data에 있는 정보를 읽어온다.
  • php는 app_data에 MySQL db에 접속해서 db를 꺼내오라는 정보를 가지고 같은 네트워크의 3306번 포트에 있는 MySQL 서버에 접속한다.
  • 이 3306번 포트의 MySQL 서버는 /var/lib/mysql이라는 디렉토리에 있는 db를 꺼내와야 한다.
  • -v로 마운트했으므로 호스트의 파일 시스템인 db_data에 있는 정보를 읽어온다.
  • MySQL 서버는 php에 이 정보를 전달한다.
  • php는 이 정보를 가지고 웹 서버에 response를 전달한다.
  • 그리고 마지막으로 웹 서버가 response를 가지고 렌더링을 해서 우리에게 정보를 보여준다.

이 일련의 과정들을 설명할 수 있어야 한다.

마지막으로, 다른 docker-compose 파일을 보면서 어떻게 동작할 지 예상해 보자.

version: '3.0'

services:
  mariadb10:
    image: mariadb:10
    ports:
     - "3310:3306/tcp"
    environment:
      - MYSQL_ROOT_PASSWORD=my_db_passward
      - MYSQL_USER=docker_pro
      - MYSQL_PASSWORD=docker_pro_pass
      - MYSQL_DATABASE=docker_pro
  redis:
    image: redis
    command: redis-server --port 6379
    restart: always
    ports:
      - 6379:6379
  rabbitmq:
    image: rabbitmq:3-management-alpine
    container_name: 'rabbitmq'
    ports:
        - 5672:5672
        - 15672:15672
    volumes:
        - ~/.docker-conf/rabbitmq/data/:/var/lib/rabbitmq/
        - ~/.docker-conf/rabbitmq/log/:/var/log/rabbitmq
    networks:
        - rabbitmq_go_net

networks:
  rabbitmq_go_net:
    driver: bridge

 

지난 글에서 도커의 내부 구조와 그 이유에 대해 설명했다.

 

도커 설치는 많은 글에서 나와 있으므로 건너뛰고, 바로 실행해보도록 하자.

도커도 git과 마찬가지로 데스크톱 어플리케이션을 이용해 gui를 쓸 수도 있고, shell로 명령을 줄 수도 있지만

cli를 사용하는 것이 도커를 가장 잘 이용할 수 있으니, 웬만하면 cli로 쓸 수 있도록 하자.

 

백문이 불여일타, 아파치 서버를 띄워보면서 사용법을 익히자.

Docker official image는 도커허브에서 지원하는 공식 이미지이다.

아파치 서버는 httpd라는 이름의 이미지로 도커 허브에서 제공되고 있다.

들어가보면, 제목 옆에 회색 박스로 docker pull httpd라고 써져 있다.

그걸 그대로 cli에 붙여넣으면 된다.

error during connect: this error may indicate that the docker daemon is not running: 
Post "http://%2F%2F.%2Fpipe%2Fdocker_engine/v1.24/images/create?fromImage=httpd&tag=latest": 
open //./pipe/docker_engine: The system cannot find the file specified.

혹시 cli에서 이 문구가 뜬다면, Docker desktop을 실행하지 않아서 나는 오류니 docker desktop을 실행해서

꼭 우측 하단에 이런 고래 모양이 나오는지 확인하고 실행하도록 하자.

$ docker pull httpd
Using default tag: latest
latest: Pulling from library/httpd
648e0aadf75a: Already exists
c76ba39af630: Already exists
b9819ffb14ec: Already exists
37baa60548e6: Already exists
6dbce5de7542: Already exists
Digest: sha256:d7262c0f29a26349d6af45199b2770d499c74d45cee5c47995a1ebb336093088
Status: Downloaded newer image for httpd:latest
docker.io/library/httpd:latest

What's Next?
  View summary of image vulnerabilities and recommendations → docker scout quickview httpd

이런 문구가 나오면 정상적으로 이미지를 pull한 것이다.

Docker desktop에 들어가면, 이미지가 정상적으로 풀 되었다는 것을 알 수 있다.

cli로 docker images 명령어를 쳐도 똑같이 나온다.

$ docker images
REPOSITORY   TAG       IMAGE ID       CREATED      SIZE
httpd        latest    96a2d0570deb   6 days ago   168MB

다시 Docker desktop에 와서 Actions 탭 밑의 ▶버튼을 누르면, 다음과 같은 창을 확인할 수 있다.

Docker run

run을 누르면 docker desktop의 containers 탭에서 인스턴스가 잘 구동되고 있음을 알 수 있다.

중지와 삭제도 할 수 있다. 

cli에서도 똑같이 할 수 있다.

Docker run (cli)

docs.docker.com > reference > command and reference에 가면 cli에서 실행할 수 있는 명령어에 대한 설명을 볼 수 있다.

기본적으로, 우리가 위에서 했던 실행은 명령어를 입력하면 된다.

$ docker run httpd
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 172.17.0.2. Set the 'ServerName' directive globally to suppress this message
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 172.17.0.2. Set the 'ServerName' directive globally to suppress this message
[Thu Aug 03 19:35:47.361498 2023] [mpm_event:notice] [pid 1:tid 139700299224960] AH00489: Apache/2.4.57 (Unix) configured -- resuming normal operations
[Thu Aug 03 19:35:47.361621 2023] [core:notice] [pid 1:tid 139700299224960] AH00094: Command line: 'httpd -D FOREGROUND'

 

이제 컨테이너가 정상적으로 실행되는지를 확인해보자.

$ docker ps -a
CONTAINER ID   IMAGE     COMMAND              CREATED          STATUS                      PORTS     NAMES
96837dea1d96   httpd     "httpd-foreground"   44 seconds ago   Exited (0) 36 seconds ago             stoic_lehmann

docker ps 명령어를 치면 현재 실행되고 있는 컨테이너, docker ps - a 옵션을 주면 중지된 컨테이너까지 다 볼 수 있다.

 

주의할 점은 아까 App store와 Docker를 비교할 때 도커도 한 프로그램에서 여러 프로세스를 띄울 수 있는 것처럼, 한 이미지에서 여러 컨테이너를 띄울 수 있다고 했다. 잘 켜졌는지 확인하려고 계속 docker run httpd를 치면 인스턴스가 계속 생성된다.

$ docker stop 96837dea1d96
96837dea1d96

$ docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

$ docker ps -a
CONTAINER ID   IMAGE     COMMAND              CREATED         STATUS                      PORTS     NAMES
96837dea1d96   httpd     "httpd-foreground"   9 minutes ago   Exited (0) 24 seconds ago             stoic_lehmann

$ docker rm 96837dea1d96
96837dea1d96

$ docker ps -a
CONTAINER ID   IMAGE     COMMAND              CREATED         STATUS                      PORTS     NAMES

docker stop CID를 입력하면 컨테이너 실행을 중지할 수 있고, docker ps에는 나오지 않지만 -a 옵션을 주면 나오게 된다.

그럴 때는 docker rm(remove) CID를 줘서 삭제하면 -a옵션을 줘도 말끔하게 삭제되는 것을 볼 수 있다.

Docker desktop과는 다르게 cid는 컨테이너가 실행중일 때는 삭제할 수 없고, 중지 후 삭제를 해야한다.

귀찮으면 --force 옵션을 주면 중지를 건너뛰고 삭제할 수 있다.

$ docker rm --force 96837dea1d96
96837dea1d96

이미지를 삭제하고 싶을 때는 rmi(remove image)를 해주면 된다.

$ docker images
REPOSITORY   TAG       IMAGE ID       CREATED      SIZE
httpd        latest    96a2d0570deb   6 days ago   168MB

$ docker rmi httpd
Untagged: httpd:latest
Untagged: httpd@sha256:d7262c0f29a26349d6af45199b2770d499c74d45cee5c47995a1ebb336093088
Deleted: sha256:96a2d0570deb75d5b01df09165d903f7647da37952e1fd9692d791b8e3a6f0cd

$ docker images
REPOSITORY   TAG       IMAGE ID   CREATED   SIZE

다시 docker images 명령어를 입력하면 잘 삭제되었음을 볼 수 있다.

잘 삭제되었는지 gui를 이용해 보고 싶으면, docker desktop을 다시 열어 확인해보자.

 

지금까지 우리가 쳤던 명령어를 다시 정리해보자. 위에서 치지 않았던 명령어도 한 번씩 다시 쳐보도록 하자.

모든 cli 명령어는 docker desktop에서도 gui로 확인할 수 있다.

레지스트리에서 이미지를 가져오기
• docker pull [OPTIONS] NAME[:TAG|@DIGEST]
도커 이미지 확인
• docker images [OPTIONS] [REPOSITORY[:TAG]]
도커 이미지에서 컨테이너를 생성하고 실행
• docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
컨테이너 확인 (전부 확인 : -a)
• docker ps 
컨테이너 중지
• docker stop [OPTIONS] CONTAINER [CONTAINER...]
컨테이너 로그 확인 (계속 확인 : -f)
• docker logs [OPTIONS] CONTAINER
컨테이너 삭제 (중지 안하고 삭제 : --force)
• docker rm [OPTIONS] CONTAINER [CONTAINER...]
이미지 삭제
• docker rmi [OPTIONS] IMAGE [IMAGE...]

더 많은 명령어는 아까 말했듯 docs.docker.com에서 확인할 수 있다.

 

그런데 아까 gui에서 컨테이너를 run할 때 보면 optional settings라는 메뉴가 있는 것을 볼 수 있다.

이것들은 대체 무엇일까? 일단 제일 위부터 확인해보자.

Container name

컨테이너에 이름을 부여할 수 있다. 나중에 컨테이너를 사용할 때 이름 또는 CID로 제어해야 하므로, 알아볼 수 있는 이름을 붙여주도록 하자. 

나는 httpd의 이미지를 가지고 gui에서는 ws2로, cli에서는 docker run --name ws3 httpd라는 명령어를 주었다.

둘 다 잘 실행됨을 알 수 있다.

아까는 docker stop 7806cef04db6을 했던 것을 이제는 docker stop ws2로만 옵션을 줘도 중지가 가능하다.

그리고 image 탭에 보면 httpd:latest와 httpd가 다름을 알 수 있다. 이건 나중에 확인하도록 하자.

Port, Volumes, Environment variable

사실 도커를 이해하기 위해서는 네트워크 지식이 어느정도는 필요하다.

 

먼저, 도커 없이 웹 서버를 사용하는 방법에 대해 알아보자.

웹 서버를 사용하기 위해서는 두 대의 컴퓨터가 필요하다.

한 대의 컴퓨터에는 웹 브라우저(client)가 깔려 있어야 하며, 다른 컴퓨터에는 웹 서버(server)가 깔려 있어야 하고, 웹 파일을 특정 디렉토리(file server)에 위치시켜야 한다.

그래야 client가 server에 어떤 파일을 달라고 요청을 보냈을때, server가 file server에 가서 정확한 파일을 가지고 올 수 있기 때문이다.

예를 들어 서버가 /usr/local/apache2/htdocs/라는 디렉토리에 index.html을 위치시켰다고 가정해보자.

그리고 웹서버에게 ‘웹 서버야, 누가 index.html을 달라고 할 때는 이 디렉토리에서 찾아줘!’라고 각인시켰다고 생각해보자.

그런데, 클라이언트와 서버는 어디에서 만나서 이 파일을 달라고 하고 꺼내주는 작업을 해야 할까?

바로 포트가 이 만남을 주선하는 창구 역할을 한다.

우리 컴퓨터에는 65535개의 포트가 컴퓨터에 설치되어 있는 여러 소프트웨어를 네트워크적으로 구분해준다. 그리고 기본적으로 80번 포트에서 접속을 대기하고 있다가 요청이 오면 마중을 나간다.

 

이번에는 도커를 이용해서 웹서버를 사용하는 방법을 알아보자.

도커를 이용하면, 웹 서버가 컨테이너에 설치된다.(docker run httpd)

그리고 이 컨테이너가 설치된 OS를 ‘docker host’라고 부른다.

하나의 docker host에는 여러개의 컨테이너가 있으며

이 도커 호스트와 여러개의 컨테이너는 서로 독립된 실행구조를 따르기 때문에,

우리가 전에 했던것처럼 80포트에 연결하면 우리의 웹서버가 마중을 나가지 못한다.

그래서 도커 호스트와 컨테이너의 포트를 서로 연결해주어야 한다.

이럴 때 작성하는 명령어는 다음처럼 작성한다.

docker run -p 80:80 httpd

docker hub에 따르면 p옵션은 '컨테이너의 포트를 호스트에게 알린다(연결시킨다)' 라는 뜻이다.

 

따라서 이 명령어를 자연어로 번역하면 다음과 같다.

'도커에서 httpd라는 이름의 이미지를 컨테이너로 띄우는데, 호스트의 80번 포트와 컨테이너의 80번 포트로 이어줘!'

이 작업을 포트포워딩(port forwarding)이라고 한다.

그림으로 확인해보자.

다시 돌아가서 포트포워딩을 실행해보고, localhost:80으로 들어가보자.

It works!가 뜨면 성공적으로 연결된 것이다.

스프링에서의 whitelabel error page가 뜨는것과 같다. ws라는 컨테이너의 index.html이 렌더링된 것이다.

 

명령어 실행

docker desktop 안에 들어가서 cli를 띄우고 pwd(print working directory)를 실행해보자.

나는 실험적으로 호스트의 8888번 포트와 컨테이너의 80포트를 연결했다.

이 pwd는 위의 그림에서 host의 pwd일까, container의 pwd일까, 아니면 둘 다의 pwd일까?

당연히 host와 컨테이너는 독립적으로 동작한다고 하였으므로, 이 cli에서 실행하는 명령어들은 호스트에서 pwd를 하는것이 아니라, 그 컨테이너 안으로 들어가서 pwd를 한 것이다.

ls -al또한 마찬가지다.

그럼, 매번 내가 컨테이너에 들어가서 이걸 실행할 수도 없고, 호스트 cli에서 실행할 수 있는 방법은 없을까?

 

있다. 바로 exec(execute) 명령어다.

docker exec [name] /bin/sh

이 명령어를 치게 되면 본쉘(Bourne shell)이 실행되게 된다.

쉘이라는 프로그램의 본질은 사용자가 입력한 명령어를 쉘 프로그램이 받아서 그것을 운영체제에게 전달해주는 일종의 창구다.

 

호스트 cli에서 docker exec ws3 /bin/sh를 입력하면, 그냥 넘어가게 된다.

리눅스가 사람이었다면 아마 ‘쉘 켰습니다. 근데 어쩌라고요?’라는 말을 했을 것이다.

그래서 다음과 같은 명령어를 준다.

docker exec -it ws /bin/sh

-it 옵션은 컨테이너를 종료하지 않은 상태로 터미널의 입력을 컨테이너 내부로 전달하기 위해 사용된다. 

 

그러면 이제 화면이 달라지고, 여기서 치는 명령어로 계속 소통을 할 수 있게 된다.

exit를 치면, 다시 호스트로 돌아갈 수 있다.

참고로, /bin/bash를 치면 bash shell이 실행된다.

 

이걸 왜 했냐? 바로 컨테이너 내부의 index.html을 변경하기 위해서이다.

hub.docker.com/_httpd에서는 httpd의 index.html이 어디에 있는지 상세하게 설명해준다.

/usr/local/apache2/htdocs에 있다고 한다.

docker exec -it ws3 /bin/bash 명령어를 치고 cd로 /usr/local/apache2/htdocs/에 가보자.

컨테이너는 기본적으로 적은 용량을 구비하는것이 미덕이기때문에 vim이나 nano등은 구비하지 않았다.

나노 에디터 설치부터 index.html을 변경하는 방법은 다음 주소에서 확인할 수 있다.

https://youtu.be/P0ZFyB4iQd0?t=262

에디터로 이 index.html을 수정하고

다시 host의 포트에서 접속을 해보면, index.html이 바뀌어 있는 것을 알 수 있다.

이렇게 열심히 index.html을 수정했다.

거의 애플 공식 홈페이지 저리가라 할 정도로 멋있게 헬로 머현 페이지를 꾸며놨는데,

그런데 갑자기 내가 키우던 고양이가 멋대로 내 컴퓨터에 올라와서는 컨테이너를 지웠다.

나는 부랴부랴 기도하며 다시 컨테이너를 켰지만, 나를 반기는건

전혀 work가 되어있지 않은데 '작동하니 안심하라고!'라는 페이지였다.

이 불상사는 어떻게 해야 좋을까? 고양이를 두들겨 패서는 해답이 나오지 않는다.

또, 내가 대현이를 환영하기 위해 만든 300개의 똑같은 Hello Meohyun 페이지를 만드려면

모든 컨테이너에 나노 에디터를 깔고 똑같이 수정해야할까?

생각만 해도 괘씸하고 눈물이 나고.... 아무튼 그렇다.

 

우리가 컨테이너를 사용하는 이유는 필요할 때 언제든지 생성하고, 필요가 없을때는 지울 수 있는게 가장 큰데,

우리가 호스트의 파일 시스템에서 수정한 내용이 컨테이너의 파일 시스템과 연결될 수 있다면?

그러면 컨테이너가 날라가도 소스코드를 그대로 들고 있을 수 있다.

터미널을 열어서 다음과 같은 명령어를 쳐보자.

docker run -p 8888:80 -v ~/Desktop/htdocs:/usr/local/apache2/htdocs httpd

아까 p 옵션에 대해 배웠으므로, 이제는 v 옵션에 대해 배워보자.

volume의 약자인 v는, 호스트의 파일 시스템에 있는 파일과 컨테이너의 파일 시스템에 있는 파일을 연결시켜준다.

따라서 저 위의 명령어를 자연어로 해석하면

'httpd의 이미지를 run할건데, 내 ~/Desktop/htdocs에 있는 index.html을 컨테이너의 /usr/local/apache2/htdocs에 마운트 시켜줘' 라는 말과 같다.

 

~/Desktop/htdocs는 각 호스트 OS에 있는 파일 경로마다 다르니, 꼭 자신의 파일이 있는 곳으로 해서 실습해보자.

 

주의점

만약 내가 내 OS의 index.html에

<html>
 <body>
  <h1> Hello, Meohyun !</h1>
 </body>
</html>

을 작성했는데, docker run ~ 명령어를 쳐놓고 It works!가 뜬다면,

마운트가 잘 되지 않은 것이다.

이를 대비해서 디렉토리 루트 설정을 되도록이면 소문자 + '-'를 최소화하는 방향으로 사용하자.

 

마지막으로 , Container의 라이프사이클을 보며 다시 한번 복습하고 글을 마친다.

 

지난 글에서 작성한 도커의 특징을 다시 복습하고 가자.

 

도커는 무엇인가?

  • 컨테이너 기반의 가상화 도구
    • 리눅스 컨테이너 기술인 LXC(Linux Containers) 기반
  • 애플리케이션을 컨테이너 단위로 실행하고 배포하는 기술
  • 다양한 운영체제에서 사용할 수 있고, 컨테이너화된 애플리케이션을 손쉽게 빌드, 배포, 관리할 수 있는 다양한 기능을 제공함
  • 위 기능들을 통해 어플리케이션을 빠르게 개발하고, 효율적으로 배포 및 관리할 수 있음.

 

이번 글에서는, 도커의 아키텍쳐에 대해서 알아보자.

도커는 크게 '클라이언트', '도커 호스트', '레지스트리'로 나누어져 있다.

먼저, 도커의 핵심인 도커 호스트를 보자.

Docker Host

도커 호스트에 있는 'Docker daemon'이 도커 호스트의 핵심이다.

Docker daemon을 실행하는것이, 우리가 Cli에서 명령어로 도커를 띄우는 것이다.

도커 데몬은 다음과 같은 순서로 컨테이너를 띄운다.

  1. Cli에서 도커 데몬에 요청을 보낸다.
  2. 도커 데몬에서 이미지를 생성한다.
  3. 그 이미지와 우리가 만든 레이어를 토대로 컨테이너를 띄운다.

점선을 보면, 도커 데몬이 레지스트리에서 이미지를 떠와서 우리의 정보와 함께 이미지를 생성하고, 또 이 이미지를 레지스트리에 등록하기도 한다는 것을 볼 수 있다.

 

그런데, 3번의 '레이어'라는것은 무엇일까?

우리가 컨테이너의 변경점이 있다고 해서 컨테이너를 처음부터 다시 설치하는 일은 굉장히 비효율적일 것이다.

그래서 컨테이너는 R/O인 image layer를 겹겹이 추가하되, 위에 read-write가 가능한 컨테이너 레이어를 둠으로써 현재 실행중인 image에 변경점이 가지 않도록 하였다.

Docker file이라는 베이스 파일의 한 줄, 한 줄이 레이어로, 명령어를 추가해 나갈 때마다 레이어가 한 겹씩 쌓여간다고 생각하면 이해가 쉽다.

위에는 도커 컨테이너가 LXC 기반이라고 했지만, 현재 도커는 layer 아키텍처를 도입함으로써 LXC와는 독립적인 방식을 구현하고 있다. 

 

이제 본격적으로 도커 호스트의 구성요소를 알아보자.

Docker daemon ( = dockerd)

  • 도커 엔진의 핵심 구성 요소
  • 도커 호스트에서 컨테이너를 관리하고 실행하는 역할
  • 컨테이너를 생성, 시작, 중지, 삭제하는 등의 작업을 수행한다.
  • 컨테이너에서 이미지를 관리한다.

 

Docker Client (=cli)

  • Docker와 상호 작용
  • 도커 명령어를 사용하면 Docker daemon으로 보내어 실행

 

Docker Object

다음은, 도커 오브젝트의 구성 요소에 대해 알아보자.

Docker Image

  • 도커 컨테이너를 만들기 위한 읽기 전용 템플릿
  • 도커 파일을 가지고 만들 수 있다.

Docker Container

  • 도커 이미지의 실행 가능한 인스턴스
  • 애플리케이션을 실행하기 위한 모든 파일과 설정 정보를 포함하는 패키지

Docker Registries

  • 도커 이미지(Docker Image)를 관리하고 저장하는 곳
    • Docker hub : 디폴트 레지스트리로, 누구나 접근 가능한 공개형 저장소
  • Docker hub 이외에도 AWS가 지원해주는 이미지나 private한 이미지 허브등이 있다.

 

App store과 Docker를 비교한다면?

App store

  • 우리가 어플리케이션을 실행할 때 App store에 가서
  • 프로그램을 다운받아 설치하고
  • 그 인스턴스인 프로세스를 실행한다

Docker

  • 우리가 어떤 어플리케이션을 실행할 때 Docker hub에 가서
  • 이미지를 pull하고
  • 그 인스턴스인 컨테이너를 run한다

비교해보면 docker pull, docker run이 어떤 의미를 내포하는지 잘 알수 있다.

한 프로그램으로 여러 프로세스를 실행할 수 있듯, 한 이미지로 여러 컨테이너를 띄울수도 있다.

 

다음 글에서는, Docker를 실제로 구동해보자.

2022년 기준 StackOverFlow의 개발 툴에 관한 Survey 중 npm에 육박하는 사용량을 가진 Docker.

Docker는 왜 쓰는지, 어떻게 쓰는지, 어떻게 구성되어 있는지 알아보자.

 

내 컴퓨터에서 어떤 애플리케이션을 만들고자 하면, 우리가 사용하는 OS 위에 여러 소프트웨어를 깔아야 한다.

그 중에는 웹 서버도 있겠고, 데이터베이스도 있겠고, 또 여러 가지 소프트웨어들이 있겠다.

 

늘 그랬지만, 이 소프트웨어들을 설치하고 실행하는 과정은 그리 녹록치 않다.

설치하면서 만나는 오류들에 대해서 때로는 당황스럽고, 때로는 분노를 느낄 때가 참 많다.

내 생각이지만, 개발자의 덕목은 '귀찮은 것을 멀리하고 싶어야' 하는데 있다고 느껴진다.

그래서 개발자는 꿈을 꾼다.

 

웹 서버를 만든 사람이 컴퓨터를 구해서 적당한 OS를 설치해주고, 그 위에 적당히 알아서 웹 서버를 설치해주고, 내가 네트워크 통신으로 그걸 이용할 수 있으면 얼마나 좋을까...

이 방식은 너무나 좋지만 문제가 있었다.

바로 '소프트웨어 하나마다 그 소프트웨어를 굴릴 수 있는 OS를 설치해야 하고, 그 OS를 설치하기 위해선 새로운 컴퓨터가 필요한 것'이다.

돈을 아끼기 위해서 고용된 개발자가 소프트웨어를 설치하지 않으려고 컴퓨터를 사다니. 잘려야 마땅하다.

 

그래서 개발자는 또 다시 꿈을 꾼다.

 

각 소프트웨어마다 OS가 필요해서 컴퓨터를 새로 사야한다면, 그냥 내 컴퓨터 안에 가상화된 컴퓨터를 하나 만들어서, 그 컴퓨터에서 위와 같은 작업을 하면 어떨까?

천만다행이게도, 이 꿈은 이루어질 수 있었다.

바로 vmware, virtualbox같은 친구들이 '가상화(Virtualization)'라는 이름으로 물리적인 컴퓨터 안에 가상 머신(Virtual Machine = computer)을 설치해 주고, 그 각각의 VM 안에 OS를 깔아서 어플리케이션을 설치할 수 있게 만들어 주었다.

그림으로 확인해보자.

OS 위에 하이퍼바이저(VM을 생성하고 구동하는 소프트웨어)를 두고, 이 하이퍼바이저가 다음과 같은 일을 하게 한다.

  • host OS에서 리소스를 할당받아서 각 guest OS에 리소스를 할당하고 조율한다.
  • 각 guest OS들의 요청을 번역해서 하드웨어에 전달하기

여기서 잠깐! Host OS와 Guest OS의 차이점?

Host OS와 Guest OS 모두 자원을 어플리케이션이나 프로세스에 할당할 수 있고, 매니징해주는 OS라는 공통점이 있지만, Host OS와 달리 VM을 구동할 때 실행되는 Guest OS는 직접 자원을 가져올 수 없고 하이퍼바이저를 통해서만 리소스를 할당받을 수 있다.

 

여기까지는 좋았다. 그런데 또 문제가 발생했다.

  • 예를 들어 2개의 소프트웨어밖에 필요하지 않으면, 굳이 하이퍼바이저가 필요한가?
  • 그림에서 보여준 Guest OS가 모두 같은 OS라면, 용량도 크고 설치 시간도 길어지는데다 같은 OS를 계속 설치해 주어야 한다. => 개발자가 가장 싫어하는 '중복'이다.
  • 그러면 1억개의 소프트웨어를 실행하기 위해서는 내 컴퓨터에 1억개의 OS를 설치해야 하는데, 그러면 너무나도 느린 내 컴퓨터를 두고볼 수가 없다. 

결론 : 아무튼 비효율적이다.

 

그래서 개발자는 또 생각했다.

그냥 한 대의 컴퓨터에 있는 host(주인) OS로, 격리된 환경에서 앱을 실행하는데 필요한 라이브러리와 실행 파일(bin)만 가지고 앱을 실행시킬 수는 없을까?

바로 이렇게...

리눅스 운영체제에는 이런 앱 실행방법이 내장되어 있다. 이러한 기술을 '컨테이너'라고 부른다.

컨테이너 기술을 이용해서 이런 일을 쉽게 해주는 제품이 여러 가지 있지만, 가장 대표적인 제품이 바로 '도커'인 것이다.

도커는 우리가 리눅스를 사용하지 않아도 리눅스 OS에서 어플리케이션이 돌아갈 수 있도록, VM 위에 Linux OS를 설치해 어플리케이션을 돌릴 수 있도록 해준다.

도커 공식 사이트(https://docs.docker.com/get-started/overview/)의 문서를 읽어보면, 도커에 대한 설명은 다음과 같다.

컨테이너 기반 가상화 도구이며
애플리케이션을 컨테이너라는 단위로 격리하여 실행하고 배포하는 기술

이제, 어느정도 이 문장을 이해할 수 있을 것이다.

다시 컨테이너란 무엇인지 정리해보자.

  • 컨테이너는 가상화 기술 중의 하나이다.
  • 호스트 운영체제 위에 여러 개의 격리된 환경을 생성
  • 각각의 컨테이너 안에서 애플리케이션을 실행

컨테이너 기반의 특징에는 어떤게 있을까?

  • 리눅스 커널의 기능을 사용해서 만들어졌다. (아까 말했듯이 리눅스의 기술이므로)
    • chroot: 파일 시스템을 격리
    • namespace: 프로세스 격리
    • cgroup: 하드웨어 자원 격리
    • 등의 기능을 가지고 있다.
  • 프로세스 단위의 격리 환경

이제 질릴만큼 컨테이너와 가상화에 대한 이야기를 했으니, 마지막으로 도커를 정리해보자.

도커란?
- 오픈 소스 플랫폼이다.
- 다양한 운영체제에서 사용할 수 있다.
- 어플리케이션을 인프라에서 분리하여, 컨테이너 단위로 격리해서 실행하고 배포하는 기술이다.
- 리눅스 컨테이너 기술(LXC) 기반으로, vmware등의 가상화 소프트웨어보다 신속하다.
- 인프라를 어플리케이션을 관리하는 것 처럼 관리할 수 있다.
- 위 기능들을 통해 어플리케이션이나 코드를 빠르게 개발하고, 효율적으로 배포, 관리할 수 있다.

 

다음 글에서는 도커의 내부 구조를 알아보자.

+ Recent posts