Kubenetes는, 프로그램(애플리케이션)을 Container 로 배포, 확장 및 관리할 때 사용하는 시스템

Docker Container 를 배포하고 orchestration 하는 표준 시스템임

Kube 는 여러 머신을 위한 docker-compose 라고 생각할 수 있음

docker-compose 는 하나의 머신 위에 여러 Container 를 띄우고 관리하고

Kube 는 더 나아가 여러 머신 위에 여러 Container 를 띄우고 모두 관리(배포, 모니터링 등)함

 


 

 

 

Kube 없이 수동으로 Container 를 배포하면, Container 가 떠야 할 서버를 직접 구성 및 관리해야하며

Container 관리, 즉 보안, 구성, Container 간 충돌, Container down 등의 이슈도 직접 처리해야 함

Container down 이 발생 할 때마다 직접 모니터링하고 이슈를 해결하고

수동으로 재배포해줘야 하는 번거로움이 있음 (근데 이건 Kube 써도 마찬가지 아닌가)

Container 로 들어오는 트래픽이 양이 변할 때 Container Scaling 을 수동으로 처리해야 하며

트래픽을 모든 Container 에 고르게 분산시키기 위해 추가 작업을 해야 함

 

Kube 를 사용하면 자동 배포, Scaling, 로드 밸런싱, Container 관리 등을 손쉽게 할 수 있음

 

 


 

 

 

Kube 는 (EC2 처럼 CPU, RAM 이 장착된 Physical/Virtual 머신인) 하나 이상의 Worker Nodes 로 이루어져 있으며 

Worker Nodes 는 (실제 Container 와 동일한) 하나 이상의 Pods (=Containers) 와

(Pods 의 네트워크 트래픽 제어를 위한) Proxy/Config 로 이루어져 있음

Prox/Config 는 Pods 가 외부 인터넷에 연결 가능한지 여부와

외부 인터넷에서 (Pods 내부에서 실행되는) Container 로 어떻게 접근할 수 있는지를 제어함

 

Kube 를 사용하여 Container 및 Pod 를 동적으로 추가/제거하는 경우

Kube 가 사용 가능한 Worker nodes 에 Pods 가 자동으로 배포됨

따라서 서로 다른 여러 Worker Nodes 위에서, 여러 Containers 를 실행하여 워크로드를 고르게 분배 가능함

 

Worker Nodes 에 Pods 를 새로 띄우거나, (실패하거나 필요하지 않은 경우) Pods 를 교체하거나 종료하는 역할은

Master Node 에 의해 진행됨

Master Node 는 Worker Nodes 처럼 하나의 머신임

Master Node 위에서 실행되는 Control Plane 이 위 작업을 수행하게 됨

Control Plane 은 Master Node 에서 실행되는 다양한 서비스의 다양한 도구 모음이며,

Worker Nodes 를 제어하는 컨트롤 센터같은 거라고 생각하자.

우리가 Kube 를 사용할 때, Worker Nodes, Pods 와 직접 상호작용하지 않음(일반적으로 하지 않음)

Kube 와 Control Plane 이 우리 대신 Worker Nodes, Pods 와 상호작용 함

 

Master Node 와 Worker Nodes 가 하나의 Cluster 안에 포함되며

Cluster 내 Nodes 들은 하나의 네트워크를 형성하여 서로 통신이 가능함

 

EC2 위에 띄운 Master Node 는 Cloud Provider(예를 들어 AWS) 의 API 에 다음과 같은 명령을 보내 Cluster 를 구축함

"내가 지금부터 너의 서비스들을 이용하여 Kube 를 실행할 Cluster 를 만들꺼야.

Worker Nodes 만들 EC2 3대를 생성하고, 로드 밸런싱을 위한 로드 밸런서, ... 등을 실행해 줘"

그리고 Master Node 에서 Kube 와 Kube 툴을 실행하고

AWS 로부터 받은 리소스(EC2, 로드 밸런서 등)를 이용하여 Pods 를 띄우는 등의 작업을 진행함

 

 


 

 

 

Kube 사용자가 (Cloud Provider 도움 없이) Kube 를 사용하기 위해 해야 할 일들이 있음

Kube 는 infrastructure 에 대해 전혀 신경쓰지 않기 때문에

사용자가 infra 를 미리 구축해주어야 함

 

Nodes 로 사용될 머신들 (AWS 의 경우 EC2) 을 준비해야하며

준비된 모든 Nodes 에 Kube (API Server, kubelet 등의 Kube services, softwares) 를 설치해야 함

필요하다면, cloud provider 로부터 얻을 수 있는 로드 밸런서 등의 리소스를 미리 준비

 

사용자가 위와 같은 infra 리소스들(Nodes, 로드 밸런서 등)을 준비해두면

Kube 는 이 리소스들을 알아서 운용함

 


 

 

Worker Node 에 대해 자세히 알아보자.

 

Worker Node 는 (위에서 설명한 것처럼) 하나의 머신이며

Master Node 에 의해 관리됨

 

 

Worker Node 안에서 하나 이상의 Pods 가 있고

이 Pods 또한 Master Node 에 의해 관리됨

(따라서 Worker Node 가 Pod 를 삭제할 수 없음)

Worker Node 에 꼭 설치되어 있어야하는 것들은 다음과 같음

- Docker : Pods 내 Container 실행에 사용됨

- kubelet : Master Node 와 통신할 때 사용됨

- kube-proxy : (Pods 내 Container 에 의해) 해당 node 로 들어오고 나가는 네트워크 트래픽을 처리할 때 사용됨

AWS 를 사용하는 경우, 위에서 언급한 필요한 머신(instance) 및 소프트웨어들을 AWS 가 알아서 설치해준다고 함

 

Pods 는 하나 이상의 Containers 를 실행하고 있음

일반적으로 pod 하나에 Container 하나 실행

Pods 는 Container 외에 Volumes 같은 리소스도 실행 가능(..!)

이 Volume 은 Container 가 사용 가능한 저장소임

 

Pods 는 Containers(와 Volumes) 를 묶는 논리적인 단위 같은 거라고 이해하면 편할 듯

 

 


 

kubelet 은 Worker Node 에 존재하며,

다음과 같이 노드 및 파드 운영에 큰 역할을 담당하고 있음

 

- 파드 관리 : Master Node 로부터 할당된 파드를 Worker Node 에 배치하고 실행

  파드 상태 주기적으로 모니터링하고 문제 발생시 다시 시작

- 컨테이너 실행 : 파드에 정의된 Container 를 실행하고 관리

  Container 시작/중지 및 리소스 할당

- 리소스 모니터링 및 보고 : Worker Node 의 리소스 사용량 모니터링하여 클러스터 상태 파악

  모니터링 정보는 Master 에 보고되고, 클러스터 스케줄링할 때 사용됨

- 상태 모니터링 및 보고 : Worker Node 및 파드 상태를 Master 에 보고함

  이를 통해 Master 는 전체 클러스터 상태 파악이 가능

- Worker Node 자동 복구 : Worker Node 가 비정상적으로 종료되면 자동으로 복구 시도

  이를 통해 Worker Node 의 가용성을 유지하고 안정성을 보장

 

참고 : https://velog.io/@gun_123/Kubernetes-Kubelet#:~:text=%EC%9D%84%20%EC%A7%80%EC%9B%90%ED%95%A9%EB%8B%88%EB%8B%A4.-,1.%20Kubelet%EC%9D%B4%EB%9E%80%20%EB%AC%B4%EC%97%87%EC%9D%B8%EA%B0%80%EC%9A%94%3F,%EB%A5%BC%20%EC%8B%A4%ED%96%89%20%EB%B0%8F%20%EA%B4%80%EB%A6%AC%ED%95%A9%EB%8B%88%EB%8B%A4.

 


 

 

Master Node 에 대해 자세히 알아보자.

 

Master Node 는 (위에서 설명한 것처럼) 하나의 머신이며

Worker Node 를 관리함

 

 

Master Node 에 꼭 설치되어 있어야하는 것들은 다음과 같음

- API Server : Worker Node 의 kubelet 과 통신할 때 사용

- Scheduler : Pods 들을 관찰하고, Pod 를 생성할 Worker Node 를 선택하는 일을 담당

  (Pod 다운되었거나, 비정상적이거나, 스케일링으로 새로 Pod 를 생성하는 경우) 

  Scheduler 가 API Server 에게 "Pods 2 대 새로 생성해" 같은 요청 전달하면

  API Server 가 그걸 받고 실제 작업을 하나 봄

- Kube-Controller-Manager : Worker Node 전체를 감시하고 제어, Pods 가 의도된 수만큼 떠있는지 확인하는 역할

  그래서 API Server, Scheduler 와 긴밀하게 연동된다고 함

- Cloud-Controller-Manager : Cloud-Controller-Manager 는 Cloud Provider 에게 무슨 일을 해야하는지 알려줌

  Cloud Provider 에 의해 달라진다고 하는데 구체적으로 뭐가 다른지는 아직 모르겠음

 

 


 

 

Pods 를 묶는 논리적인 그룹을 Service 라고 함

(위에서 언급된) Poxy 와 연관이 있음

Service 는 특정 Pod 를 외부 세계에 노출하여

특정 IP 혹은 Domain 으로 특정 Pod 에 연결할 수 있도록 하는 용어임...(???)

그럼 Pod 끼리 (Service 에 의해 제공된) IP 로 통신한다는 말이 되나?

 


 

Kube 사용자는 kubectl 을 통해 사용자가 원하는 명령을 kube(정확히는 Master Node) 에 대신 전달하도록 할 수 있음

kubectl 는, 새로운 deployment 생성, deployment 삭제, 변경 같은 명령을

kube Cluster 에 보내는 데 사용되는 도구임

 

 


 

 

minikube 를 통해 local 컴퓨터에 kube 를 설치해서 테스트베드로 사용할 수 있음

minikube 를 통해 kube 가 구체적으로 어떻게 동작하는지 알아보자

 

kube 는 몇 가지 Objects 들이 제 역할을 함으로써 동작함

여기서 Objects 는 언어의 객체 이런 게 아니라, kube 의 핵심 파트 한 부분 부분을 의미한다고 보면 됨

kube 의 Objects 는 Pods, Services, Deployments, Volume..... 등임

사용자가 명령적 방식 혹은 선언적 방식 두 방식 중 하나를 통해 

kube 에 "이러저러한 Objects 를 사용하는 이 코드를 실행시켜줘" 라고 명령함

그럼 kube 는 그 코드를 그대로 실행하고, 사용자가 원하는 것이 실행됨

 

 

 


 

 

 

Pod object 를 알아보자

 

Pod 는 kube 에 의해 상호 작용하는 가장 작은 유닛. kube 는 pods 와 pods 내 containers 를 관리함

Pods 는 기본적으로 cluster 내부 IP 를 갖고 있음 (사용자가 ip 수정 가능)

Pods 내에 여러 containers 가 있는 경우, 이 containers 은 localhost 를 통해 서로 통신한다고 함

사용자가 코드를 통해 kube 에게 Pods 를 생성하라고 명령을 보내면

kube 는 worker nodes 중 하나를 선택하고 그 위에 pods 를 생성 및 실행함

pods 는 container 처럼 일시적으로 올라가는 것임. pods 가 내려가면 그 동안의 모든 데이터가 손실

pods 에 이상이 생기면, kube 가 알아서 pods 를 재실행함

 

 


 

 

deployment object 를 알아보자

 

deployment 는 사용자의 desired state 를 담고 있는, 일종의 명령서같은 것

deployment 는 kube 로 작업할 때 만지게 될 주요 object 중 하나임.

왜냐하면 deployment 를 통해 kube 에 다양한 실행 명령을 내릴 수 있기 때문 

deployment 를 통해 하나 이상의 pods 를 제어할 수 있고, 내부적으로 컨트롤러 객체를 생성할 수 있음

사용자는 deployment 를 통해 pods 를 띄우거나 함 (사용자가 deployment 없이 pods 를 직접 띄우지는 않는 듯)

deployment 를 일시중지 하거나, 삭제하거나, 롤백할 수 있음

만약 새로 배포한 deployment에 이상이 생겼다면 바로 롤백해서 그 전 deployment 상태로 되돌릴 수 있다는 말임

이런 기능을 시스템적으로 제공함

deployment 는 다이나믹하게 그리고 자동으로 scaling 이 가능

사용자는 deployment 에 n 개의 pods 를 띄워달라고 설정할 수 있고 그에 맞게 scaling 이 됨

혹은 traffic 이 많아지면 pods 개수를 늘리는 등의 scaling 설정도 가능함

 

 


 

 

 


service object 를 알아보자

Service 는 Pods 혹은 Pods 내의 Container 에 접근하기 위해 필요한 object 임

Service 는 Pods 의 ip 를 노출시켜, 다른 Pods 가 ip 가 노출된 Pods 에 접근 가능하도록 만들거나

혹은 외부에서 ip 가 노출된 Pods 에 접근 가능하도록 만듦

각 Pods 는 기본적으로 cluster 내부 IP 를 갖고있는데, 외부에서 이 ip 를 통해 Pods 에 접근이 불가능함

또한 Pods 의 내부 ip 는 Pods 가 교체 될 때마다 변경됨

이런 특징들은 Pods 를 사용할 때 꾸준히 문제가 되는데, Service 가 이를 해결해줄 수 있음

Service 는 Pods 를 그룹화하고 고정된 공유 IP 주소를 제공함

하나의 Service 내에 여러 pods 가 포함되어있고, Service 는 이 pods 에 접근 가능한 하나의 고정된 ip 를 제공하는 것임.

service 가 제공하는 ip 는 다른 Pods 혹은 외부에서 접근 가능함

 

흠.. service 가 제공하는 ip 가 하나뿐이라면,

내부에 각 pods 는 어떻게 구분해서 접근 가능한거지? port number 로 구분하나..?

 

 

 


 

 

 

kube 에서 객체들을 띄우기 위한 방법을 두 가지 제공함

명령적 접근방식 vs 선언적 접근방식

 

명령적 접근 방식은 deployment, service 등을 띄우는 명령어를 직접 입력하여 하나씩 띄우는 것

마치 docker run 을 여러번 하여 containers 를 띄우는 것과 같음

 

선언적 접근 방식은 띄우고 싶은 객체들의 정보를 입력해 둔 config 파일(yaml) 을 만들고 이 yaml 을 이용하여 한 번에 띄우는 것

마치 docker-compose 를 통해 한 번에 여러 container 를 띄우는 것과 같음

예를 들어 다음과 같이 config 파일인 yaml 파일을 만들 수 있음

 


apiVersion: apps/v1
kind: Deployment
metadata:
  name: first-app
spec:
  selector:
    matchLabels:
      app: first-dummy
    replicas: 3
    template:
      metadata:
        labels:
          app: first-dummy
      spec:
        containers:
        - name: first-node
          image: "first-app"

 

config 파일을 이용하려면 아래처럼 kubectl apply 라는 명령어 사용

 

kubectl apply -f first-app.yaml

 

kube 는 yaml 에 설정된 것을 유지하기 위해 노력함

예를 들어 pods 가 하나 죽어서 live pods 가 3개에서 2개가 되었다면

yaml 의 상태(pods 의 replicas 는 3)를 맞추기 위해, 하나를 새로 띄워서 3개를 맞춤

사용자가 yaml 을 업데이트하면, kube 는 그 업데이트 된 사항을 확인하고 그대로 환경을 구성함

 

 


 

 

Deployment 를 생성하는 yaml 파일을 만들어봄.

공식 문서를 계속 참고 및 확인할 것

https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#deployment-v1-apps

 

apiVersion: 현재 kube 에 맞는 apiVersion 을 찾아서 넣음

  선언적 접근 방식을 사용한다면 반드시 넣어야 함

  "kubenetes deployment apiversion" 등으로 검색해서 나온 최신 버전 yaml 샘플 등에서 발췌하여 넣으면 됨

  예를 들면 아래 스샷과 같이 "apps/v1"

 


kind: Deployment Kube 에서 생성하고 싶은 Object 를 넣음

  예를 들어 Deployment, Service, Job 등

 

metadata : 생성하는 Object 의 이름 등의 정보를 넣음

  예를 들어 kind 에 Deployment 가 추가되었다면, 아래처럼 Deployment 의 이름을 first-app 으로 명명

  name: first-app

 

spec : 생성하는 Object 의 사양(spec) 정보를 넣음

  예를 들어 위 kind 에 Deployment 가 추가되었다면, (아래처럼) 구성하고 싶은 Deployment 의 사양을 입력
  replicas: 3 pods 의 개수를 지정. default 는 1이며 (처음에 pod 를 띄우고 싶지 않다면) 0으로도 지정 가능

  selector: deployment 가 계속 관리해야 할 pod 의 라벨을 넣음. pod 의 라벨은 아래 다시 설명됨

    matchLabels:

      app: first-dummy

      deploy: second-dummy

      tier: third-dummy
      
여기서 pod 의 라벨이 3개 추가되었음. 이 말 뜻은, 3개 라벨이 충족하지 않거나, 다른 라벨을 갖는 pod 는
      해당 deployment 에 의해 관리되지 않는다는 말임.

      deployment 에 속한 pods 를 deployment 에게 알려준다고 할 수 있음...

  template: pod 에서 동작할 Container Image 를 정의하는 부분

    위 kind 가 Deployment 이기 때문에, 여기서의 template 은 PodTemplateSpec 이 되며 [문서]

    PodTemplateSpec 에는 metadata 와 spec 두 가지를 추가할 수 있음 [문서]

    (kind: Pod) 파드Object 만들기 위해 추가.

      위 kind 가 Deployment 라면 여기 kind 는 자동으로 Pod 가 되어

      여기 kind (kind: Pod) 생략 가능
    metadata: pod 는 kube 세계의 새로운 Object 이기 때문에 metadata 를 한 번 더 중첩시켜 넣어줌 
      labels: deployment 에 의해 관리될 pod 의 라벨 설정. 사용자가 원하는 labels 을 여럿 추가할 수 있음
        app: first-dummy

        deploy: second-dummy

        tier: third-dummy

    spec: Pod 의 사양을 정의. 위 Spec 은 Deployment 의 사양이고, 여기 Spec 은 Pod 의 사양
      containers: Pod 안에 올라갈 Container 리스트를 정의. 예를 들어 두 가지 Container 를 띄운다면
        - name: first-node
          image: mydockerhub/first-app

        - name: second-node
          image: mydockerhub/second-app:4

 

이렇게 만들어진 yaml 파일 이름이 mykubecluster.yaml 이라고 하자

이 yaml 을 기반으로 kube 에 클러스터를 띄우려면 kubectl apply 명령어 사용

 

kubectl apply -f=mykubecluster.yaml

 

deployment 를 실행한 이후에, 아래 명령어들을 이용하여 Objects 가 잘 떴는지 확인

 

kubectl get deployments

kubectl get pods

 

 

 


 

 

위 deployment 를 위한 yaml 외에, service 를 위한 yaml 을 추가해봄

만들어지는 yaml 파일 이름은 사용자가 정하면 됨. 이를테면 myservice.yaml 등

 

[service v1 core Documentation]


apiVersion: v1
kind: Service
metadata:
  name: backend
spec:
  selector:
    app: first-dummy
    deploy: second-dummy
    tier: third-dummy
  port:
    - protocol: 'TCP'
      port: 80  #외부 (로 노출되는) 포트
      targetPort: 8080  #(외부 포트와 연결되는) 내부 포트
    - protocol: 'TCP'
      port: 443
      targetPort: 443
  type: ClusterIP
  


 

service 의 selector 는 deployment 의 selector 와 마찬가지로,

해당 service 에게 제어되거나, 연결되어야 하는 다른 리소스를 식별하는 데 사용함

deployment 에 의해 생성된 pods 를 service 의 selector 에 추가하여 설정 가능

만약 두 개의 deployments A, B 가 있고

A deployment 는 x:x, y:y 라벨을 갖는 pods 를 갖고

B deployment 는 x:x 라벨만 갖는 pods 를 갖는다고 하자

Service select 에 x:x 만 설정해두면 A deployment 의 pod 와 B deployment 의 pod 모두를 대상으로 설정하여

해당 Service 의 그룹에 포함시켜 Service 에 의해 제어되도록 할 수 있음

 

참고로 Service 의 selector 는 matchLabels 밖에 없음

 

이렇게 Service 의 selector 로 pods 그룹을 설정한다고 해도

pods 그룹만 알 뿐이지 구체적으로 어떤 pods 가 (Service 에 의한 port 에) 노출되어야 하는지는 아직 모름

 

port 를 통해 pods 가 외부에서 오는 어떤 port 를 받을 수 있게 할건지(port: 80)

외부에서 오는 요청이 내부 시스템의 어떤 port 로 연결되게 할 건지(targetPort: 8080) 설정할 수 있음

 

type 에는 여러 값을 넣을 수 있음

- ClusterIP : default. 내부적으로 노출된 IP. 클러스터 내부에서만 접근 가능하며, 클러스터 내부 pod 들끼리 통신 가능 

    해당 타입의 Service 에 속한 pod 에 들어오는 요청을 자동으로 모든 pod 로 분산

- NodePort : 기본적으로 실행되는 worker node 의 ip 와 port 에 노출

- LoadBalancer : 외부 세계에서 pod 로 접근을 원하는 경우 사용. 가장 일반적으로 사용됨

    외부에서 사용 가능한 IP 주소를 생성하고

    실행되는 (pod 가 어떤 노드에 떠 있느냐에 관계없이) 모든 pod 에 들어오는 요청을 자동으로 분산하고

    생성된 고정 IP주소는 Pod 가 실행되는 노드와 독립적임

 

이렇게 만들어진 myservice.yaml 을 kube 에 올릴때는 아래 명령어 사용

 

kubectl apply -f myservice.yaml

 

service 를 실행한 이후에, 아래 명령어를 이용하여 service 가 잘 떴는지 확인

 

kubectl get services

 


 

 

kube 에 올라간 deployments 나 services 의 설정을 업데이트하고 싶다면

yaml 파일을 업데이트하고 다시 실행(kubectl apply -f ...)하면 됨

그러면 업데이트 된 내용이 kube 클러스터에 자동으로 반영됨

 


 

 

kube 에 올라간 리소스(deployments, services...) 를 삭제하고 싶을 때는

delete 명령어에 삭제할 리소스 이름(metadata.name)을 넣어서 사용

 

kubectl delete deployment first-app

kubectl delete service backend

 

혹은 아래처럼 yaml 파일 이름 자체를 넣어, yaml 에서 생성한 모든 리소스들 삭제 가능

 

kubectl delete -f=mykubecluster.yaml

kubectl delete -f=myservice.yaml

kubectl delete -f=mykubecluster.yaml, myservice.yaml

kubectl delete -f=mykubecluster.yaml -f=myservice.yaml

 


 

 

위에 예제에선 두 개의 yaml 을 사용하여 리소스를 띄웠고

두 yaml 의 리소스들이 서로 협력하여 일 하도록 만들었음

어짜피 같이 일 하게 될꺼라면 하나의 yaml 에 넣어도 되지 않을까? 가능함.

두 개의 yaml 내용을 하나의 yaml 에 합치려면 아래와 같이 진행하면 됨

 

< myservice.yaml >

apiVersion: v1
kind: Service
metadata:
  name: backend
....
< mykubecluster.yaml >

apiVersion: apps/v1
kind: Deployment
metadata:
  name: first-app
....
< total.yaml >
apiVersion: v1
kind: Service
metadata:
  name: backend
....
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: first-app
....

 

 

두 yaml 내용 사이에 "---" 를 꼭 넣어줘야 함

"---" 는 yaml 에서 Object 를 구분하는 기호라고 함

 

위와 같이, 여러 yaml 을 묶을 때는 Service Object 를 위한 yaml 을 먼저 배치하는 게 좋다고 함

리소스는 위에서 아래로 내려오면서 차례대로 생성되어 Service 가 먼저 생성됨

Service 는 이후 생성되는 pods 등의 라벨을 보면서, Service 에 지정된 라벨이 생성되는지 확인함

지정된 라벨의 pods 가 생성되면 동적으로 Service 에 추가됨 

 


 

 

위 예제의 deployments 설정 yaml 에서 selector 로 matchLabels 을 사용했는데

matchLabels 말고 matchExpressions 를 사용할 수 있음

 

https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#labelselector-v1-meta

https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#labelselectorrequirement-v1-meta

 

예를 들어 아래와 같은 matchLabels selector 를 matchExpressions selector 로 바꾼다고 하자

< matchLabels selector >

selector:
  matchLables:
    app: second-app
    tier: backend
< matchExpressions selector >

selector:
  matchExpressions:
    - {key: app, operator: In, values: [first-app, second-app]}

 

key 는 Label 의 key 값

values 는 Label 의 value 값

operator 에 In, NotIn, Exists, DoesNotExist 등이 들어갈 수 있음

  위에서 사용된 In 은 values 리스트에 포함되어있으면 match 됨

  예를 들어 어떤 pod 의 Label 이 app:second-app 이라면, 위 matchExpressions selector 에 의해 match 됨

  만약 NotIn 을 선택했다면, app:second-app Label 을 갖는 pods 는 match 에서 제외될 것임

 

 


 

 

kube 에서 실행된 Container 가 우리 의도에 따라 정상적으로 실행 중인지 검사하는 방법을 직접 정의할 수 있음

예를 들어 위 예제에서 deployment yaml 의 pods spec.containers 를 보자.

 

apiVersion: apps/v1
kind: Deployment
metadata:
  name: first-app
spec:
  selector:
    matchLabels:
      app: first-dummy
    replicas: 3
    template:
      metadata:
        labels:
          app: first-dummy
      spec:
        containers:
        - name: first-node
          image: "first-app"
          livenessProbe:
            httpGet:
              path: /
              port: 8080
            periodSeconds: 10
            initialDelaySeconds: 5

 

 

livenessProbe : Container 가 구동된 이후 잘 실행중인지 검사하기 위해 설정하는 값들

httpGet : http 의 get 요청이 pods 에서 실행중인 application 으로 전달되어야 함을 의미

periodSeconds : 작업을 수행 빈도(초단위). 10을 넣으면 10초 

initialDelaySeconds : kube 가 처음으로 상태를 확인할 때까지 기다려야 하는 시간(초단위) 5를 넣으면 5초

 

kube 자체적으로 Container 가 죽으면 다시 띄워주긴 하지만

Container 내부 app 이 이상한 방향으로 동작하거나

kube 가 알아차리지 못하는 방식으로 죽어버리는 상황을 대비하기 위해

사용자가 직접 "/:8080 으로 10초간 ping 을 보내서 제대로 동작하는지 확인해봐" 라고 검사 설정을 해둘 수 있음

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Docker Compose 는 다수의 "docker build" 와 다수의 "docker run" 명령어를 대체하는 도구(orchestration 명령 모음)

Docker compose 툴로 다수의 container 들을 한 번에 손쉽게 관리할 수 있음

하나의 Docker compose 를 사용하여 모든 container 들을 빌드, 실행, 및 중지/중단 할 수 있음 

 

Docker compose 는 Dockerfile 을 대체하는 용도가 아니며,

역시 image 혹은 container 대체하는 용도가 아님 

Docker compose 는 여러 host 위에서 동작하는 다중 컨테이너들을 관리하는 데 적합하지 않음

단지 하나의 동일한 host 위에서 동작하는 다중 컨테이너들을 관리할 수 있음


 

Docker compose 에서 관리하는 Container 들은 기본적으로 -d --rm 옵션을 갖게 됨

Docker compose 에서 관리하는 Container 들은 모두 하나의 네트워크로 묶이게 됨

즉, 하나의 동일한 Docker compose 파일에 정의된 모든 서비스(Containers)는

(별다른 네트워크 설정을 하지 않아도) 동일한 네트워크의 일부가 되어 서로 통신이 가능함

 


 

 

Docker compose 구성 내에서 Container 는 "Service" 라는 이름으로 불림

이 Service 에 어떤 port 를 열것인지 어떤 네트워크를 붙일 것인지

볼륨을 추가 할 것인지 환경변수를 추가할 것인지 등등을 docker compose 내에 정의할 수 있음

 


 

 

Docker compose 구성 파일은 "docker-compose.yml" 혹은 "docker-compose.yaml" 라는 이름으로 만듦

docker-compose.yml 의 포맷은 다음 예제(왼쪽)와 같음

< docker-compose.yml >

version: "3.8"
services: 
  mongodb_container:
    image: 'mongo'
    volumes:
      - data:/data/db
    environment:
      - MONGO_INITDB_ROOT_USERNAME=max
      - MONGO_INITDB_ROOT_PASSWORD=secret
    networks:
      - goals-net
  container2:
    ...
  container3:
    ...
volumes:
  data:
< 동일한 역할을 하는 기존 docker 명령어 >

docker run \
 --name mongodb_container \
 -v data:/data/db \
 --network goals-net \
 -e MONGO_INITDB_ROOT_USERNAME=max \
 -- MONGO_INITDB_ROOT_PASSWORD=secret \
 -d \
 --rm \
 mongo

 

- version : Docker comose 사양의 버전을 지정

  (앱이나 docker-compose.yml 파일의 버전이 아님) 

  version 은 해당 docker-compose yml 파일에서 사용할 수 있는 기능에 영향을 미친다고 함 

  계속 업데이트되는 Docker compose 중에서 어느 기능을 사용할 지 version 으로 고정시켜주는 거라고 함

  (근데 최근 문서를 보니 version 은 안 쓰는 것 같기도..? 일단 강의 내용만 따라가보자)

 

- services : Container 를 지정

  위 예제에서는 총 세 개의 containers 를 생성함

  mongodb_container, container2, container3

  각 container 가 어떤 이미지를 기반으로 생성될지, 어떤 네트워크에 붙을지 등을 정의하면 됨

 

- image : 생성될 container 의 기반으로 사용할 이미지

 

- volumes : container 볼륨 설정

  volume 여러개 추가 가능

  ro 등의 옵션을 추가할 수 있음 (예를 들어 -v data:/data/db:ro)

 

- environment : container 내에서 사용할 환경 변수 지정

  위 예제처럼 

      environment:
      - MONGO_INITDB_ROOT_USERNAME=max

  이렇게 지정해도 되고 아래처럼 지정해도 됨

      environment:
        MONGO_INITDB_ROOT_USERNAME: max

  혹은 environment 대신 env_file 을 사용하여, 환경변수 파일(mongo.env)을 넣어 지정할 수 있음

      env_file:
        - ./env/mongo.env

  환경변수 파일(mongo.env) 내용은 다음과 같아야 함

MONGO_INITDB_ROOT_USERNAME=max
MONGO_INITDB_ROOT_PASSWORD=secret

 

- networks : container 에 추가 network 를 붙임

  위에서 설명한 것처럼, Docker compose 가 관리하는 모든 서비스(Containers) 는

  자동으로 하나의 기본 네트워크에 묶이게 되지만

  networks 명령어를 추가하면 해당 Container 에는 추가 네트워크가 붙게됨

  위 예제에서 mongodb_container 는 기본 네트워크와 goals-net 네트워크 두 군데에 붙게 됨

 

- (마지막줄에 있는) volumes : container 에 적용한 volume 의 이름을 넣어주는 곳

  위의 예제에서 mongodb_container 에 volume 이름이 "data" 이므로

  마지막 volumes 명령어에 "data" 를 넣어주었음.

  이렇게 따로 사용되는 volume 이름을 넣어줌으로써, Docker 가 명명된 volume 이름을 인식할 수 있다고 함

  (익명 volume 이나 path 를 직접 지정한 bind mount volume 은 마지막 volumes 명령어로 넣어주지 않아도 됨)

  추가로, 다른 서비스에서 동일한 volume 이름을 사용하면, 그 volume 이 공유된다고 함

  (서로 다른 Container 들이 host 머신 상의 동일한 volume 을 동시에 사용 가능)

 

 


 

 

docker-compose.yml 파일을 생성한 후, 동일한 위치에서 아래 명령어 실행

docker-compose up -d

 

그러면 docker-compose.yml 에 설정한 모든 서비스가 시작됨

(-d 옵션이 없으면 Docker compose 자체가 attached 모드로 시작됨)

 


 

 

docker-compose.yml 실행을 중지하려면 아래 명령어 실행

docker-compose down

 

위 명령어로 volume 은 제거되지 않는데, 

만약 제거하고 싶다면 -v 옵션을 붙임 (docker-compose down -v)

 

 


 

 

docker-compose.yml 의 다른 예제도 확인해봄

< docker-compose.yml >

version: "3.8"
services:
  backend_container:
    # build: ./backend
    build:
      context: ./backend
      dockerfile: Dockerfile
      args:
        default_port: 80
    ports:
      - '80:80'
      - '123:443'
    volumes:
      - ./backend:/app
    stdin_open: true
    tty: true
    depends_on:
      - mongodb_container
      - A_container
< 동일한 역할을 하는 기존 docker 명령어 >

docker run \
--name backend_container \
--rm \
-d \
--build-arg default_port=80 \
-p 80:80 \
-p 123:443 \
-v my/path/backend:/app \
-i \
-t \
...

 

 

- build : service 를 생성할 이미지를 직접 빌드해서 사용함

  단순히 build: ./backend 이런 식으로 짧게 쓸 수 있고 (이 경우, Dockerfile 이름은 반드시 "Dockerfile" 이어야 함)

  context: 와 dockerfile: 등 을 추가하여 좀 더 자세하게 명시할 수 있음

  context: 는 Dockerfile 이 존재하는 위치이며, build 했을 때 기준이 되는 위치임

  위의 예제에서 ./backend 위치를 기준으로 build 를 진행하며 COPY 등의 기준 위치가 됨

  dockerfile: 은 Dockerfile의 이름임

  (위의 예제에서 Dockerfile 의 이름은 default 인 "Dockerfile" 이지만, 내 맘대로 지은 "eyeballsfile" 도 될 수 있겠지)

  args: 를 통해 Dockerfile 내부에서만 사용 가능한 상수값을 지정할 수 있음

  Dockerfile 생성할 때 넣은 ARG 명령 사용한 것과 동일한 부분임

 

- ports : service 에서 노출하고 싶은 포트 지정 가능

  위 예제의 '80:80' 은 service 포트(앞 80)와 이미지 내부 포트(뒤 80)가 연결됨

  '123:443' 은 service 포트(앞 123)와 이미지 내부 포트(뒤 443)가 연결됨

  service 사용자는 80 혹은 123 포트를 사용하여 이미지 내부 80 혹은 443 으로 접근 가능

 

- volumes : 위 예제와 같이 상대 경로에 해당하는 위치를 volume 으로 mount 가능

  이렇게 경로를 이용하여 mount 한 경우, 마지막줄에 volume 의 이름을 넣어주지 않아도 괜찮음

 

- stdin_open tty : 사용자가 해당 service 와의 상호작용(interactive)을 필요로 하는 경우 true

 

- depends_on : docker compose 를 통해 동시에 여러 container(service) 들을 생성할 때

  이미 실행되고 있는 다른 container 에 의존성을 설정할 수 있음

  예를 들어 A container 가 먼저 실행되고 그 후에 B container 를 실행할 수 있도록 순서를 정할 수 있다는 의미

  위의 예제에선, 'mongodb_container' 와 'A_container' 두 개의 container 가

  먼저 실행 된 이후에 'backend_container' container 가 나중에 실행될 것임

 


 

 

 

docker-compose.yml 을 통해 생성한 container 들을 docker ps 를 통해 확인하면

우리가 지정한 이름과 조금 다른 것을 확인할 수 있음

 

위에 docker-compose.yml 예제를 통해 mongodb_container, backend_container 두 가지 container 를 생성 후

docker ps 명령어를 확인해보면 아래와 같은 이름이 보임

- docker-practice_backend_container_1

- docker-practice_mongodb_container_1

 

container 이름은 service 이름에 접두사와 접미사가 붙은 형태이며

접두사 "docker-practice" 는 프로젝트 폴더 이름,

접미사 "1" 은 증가하는 숫자임

 

위와 같은 이름 대신, 직접 container 이름을 지정하려면 container_name 명령어를 사용하면 됨

이를테면

services:
  backend_container:
    container_name: backend_container
docker run \
--name backend_container \
...

 

container 이름에 접두사, 접미사가 붙긴 했지만,

docker compose 가 생성한 기본 network 에서는 우리가 지정한 service 의 이름으로 container 끼리 통신이 가능함

즉, backend_container 에서 "mongodb_container" 이름으로 mongodb_container 에 접근 가능하다는 것임

 


 

 

docker container 의 기반이 되는 각 이미지는

docker run 명령어 실행시 실행되는 기본 프로세스(pid 1번 프로세스)가 있음

이 기본 프로세스를 이미지 변경 없이(즉, Dockerfile 의 CMD, ENTRYPOINT 추가를 통한 업데이트 없이) 바꿀 수 있음

 

예를 들어 다음과 같이 실행한 경우

- docker run -it node

node 라는 이미지는 기본 프로세스가 "node" 이기 때문에 node 가 실행됨

하지만 다음과 같이 실행한 경우

- docker run -it node npm init

"node" 대신 "npm init" 이라는 명령에 의한 프로세스가 기본 프로세스로 실행됨

따라서 npm init 명령에 의한 프로세스가 해당 컨테이너의 생명주기에 관여함

 


 

CMD 가 존재하는 Dockerfile 로 빌드한 이미지를 docker run 으로 실행할 때

뒤에 추가 명령어를 붙여서, 기본 프로세스를 덮어쓰게되면

CMD 로 지정한 실행명령어가 사라지고 run 뒤에 붙인 추가 명령어가 대신 실행됨

 

예를 들어보자

 

< Dockerfile >
FROM ubuntu:latest
CMD ["echo", "hello eyeballs!"]

 

< docker run 명령어 >
docker run --rm myimage echo "bye"

 

 

docker run 을 실행하면 CMD 에 의해 "hello eyeballs!" 가 출력되어야 하지만

run 뒤에 추가한 echo "bye" 명령어에 의해 "bye" 가 대신 출력됨

 


 

CMD 와 비슷한 역할을 하는 ENTRYPOINT 는 조금 다르게 동작함

 

ENTRYPOINT 가 존재하는 Dockerfile 로 빌드한 이미지를 docker run 으로 실행할 때 뒤에 추가 명령어를 붙이면

ENTRYPOINT 로 지정한 실행명령어의 뒤에 추가한 명령어가 붙어 같이 실행됨

 

예를 들어보자

 

< Dockerfile >
FROM ubuntu:latest
ENTRYPOINT ["echo", "hello"]

 

< docker run 명령어 >
docker run --rm myimage "eyeballs"

 

docker run 명령어 뒤에 "eyeballs" 를 추가해 줌

docker run 을 실행하면 ENTRYPOINT 에 의해 실행되는 echo hello 뒤에 eyeballs 가 붙어서

echo hello eyeballs 라는 명령어가 기본 프로세스가 되었음

따라서 "hello eyeballs" 가 출력됨

 

비슷한 논리로, Docker Compose 를 통해 기본 프로세스를 덮어쓸 수 있음

다음과 같은 Dockerfile 과 docker-compose.yml 이 존재한다고 하자

 

< docker-compose.yml > < Dockerfile >
version: "3.8"
services:
  my_service:
    build: ./
FROM ubuntu:latest
ENTRYPOINT ["echo", "hello"]

 

< docker compose 명령어 >
docker compose run [service 이름] [추가 명령어]

docker compose run my_service
docker compose run my_service eyeballs

 

 

 

위와 같이 docker compose run 명령어 뒤에

service 이름과 추가할 명령어를 넣으면

기본 프로세스에 영향을 미칠 수 있음

 

위 예제에서는 Dockerfile 이 ENTRYPOINT 명령어를 실행하기 때문에

명령어가 뒤에 추가되어 hello eyeballs 가 출력되었음

 

docker compose run --rm my_service 처럼 --rm 옵션을 추가해주면

docker compose run 명령어를 통해 실행된 컨테이너가 stop 될 때 컨테이너 자체를 삭제할 수 있음

 

 

 


 

 

 

docker-compose.yml 에 설정된 services 중 내가 원하는 service 만 실행할 수 있음

아래와 같은 docker-compose.yml 에서 bb_svc, cc_svc 만 실행하고 싶다고 하자

< docker-compose.yml >
version: "3.8"
services:
  aa_svc:
    ...
  bb_svc:
    ...
  cc_svc:
    ...
  dd_svc:
    ..

 

아래와 같이 docker compose up 명령어 뒤에 service 이름을 넣어주면

지명된 services 만 실행됨 

docker compose up -d bb_svc cc_svc

 

 

 

 

 

 

 

 

Dockerfile 에서 RUN 을 통해 실행되는 모든 명령은

도커 컨테이너 및 이미지의 작업 디렉토리에서 실행되는데

여기서 말하는 작업 디렉토리는 컨테이너 파일 시스템의 루트 폴더

예를 들어 아래와 같은 Dockerfile 이 존재한다면

FROM node
RUN npm install

node 컨테이너의 / 위치에서 npm install 명령을 실행하게 됨

 

특정 위치에서 RUN 명령어를 실행하기 위해서는 WORKDIR 를 사용

이를 통해 작업 디렉토리를 바꿀 수 있음

예를 들어

FROM node
WORKDIR /app
COPY . ./
RUN npm install

 

COPY . ./ 는 "현재 Dockefile 이 위치해있는 곳의 모든 파일,디렉터리들" 을

"컨테이너 내 작업 디렉터리" 에 복사하는 의미의 명령어

 

RUN : Dockefile 을 통해 이미지를 생성할 때 수행 할 작업을 지시

ENTRYPOINT, CMD : Dockerfile 을 통해 이미지 생성할 때 수행되지 않고,

이미지가 만들어진 후 그 이미지로 컨테이너가 처음 시작될 때 컨테이너 내부에서 수행 할 작업을 지시

 

CMD 명령어는 특이하게 명령을 배열에 담아줘야 함

예를 들어 echo hello 라는 명령을 넣고싶다면

CMD ["echo", "hello"]

 

EXPOSE 명령어를 통해, 도커 컨테이너 실행시 -p 옵션을 통해 연결 가능한 (컨테이너 내) 포트를 문서화 할 수 있음

EXPOSE 80, 443

 

Dockerfile 을 만든 후 이미지로 만드는 방법은 다음과 같음

docker build -t [이미지 이름] [Dockerfile 위치] 
예) docker build -t eyeballs:latest .

 

ENTRYPOINT 와 CMD 를 자세히 알아봄.

 

위에 기술했듯이 ENTRYPOINT, CMD 둘 다

컨테이너가 처음 시작될 때(docker run 명령어로 실행시) 컨테이너 내부에서 수행 할 작업을 지시하는 명령어임

 

CMD 하나만 존재하는 Dockerfile 을 확인해보자

 

 

< Dockerfile >
FROM ubuntu:latest
CMD ["echo", "hello eyeballs!"]

위와 같은 Dockerfile 로 이미지를 만들어서

docker run 명령어로 실행하면

CMD 에 넣은 명령어가 실행되는 것을 볼 수 있음

 

이렇게 CMD 로 실행된 프로세스(echo)는 pid 1 번으로 실행되어 컨테이너의 생명 주기에 관여함

1번 프로세스가 살아있어야 컨테이너가 계속 살아있게 되고

만약 1번 프로세스가 죽게되면 컨테이너도 같이 stop 됨

 

실제로 docker stop 명령어가 컨테이너를 stop 하는 방법은,

pid 1번 프로세스에 SIG TERM 시그널(프로세스 종료 신호)을 보내는 거라고 함

위 Dockerfile 로 만든 컨테이너도 run 명령어를 날리자마자 echo 출력하고 곧바로 stop 되었다.

 

반대로,ENTRYPOINT 가 사용된 Dockerfile 은 어떻게 동작하는지 보자

 

< Dockerfile >
FROM ubuntu:latest
ENTRYPOINT ["echo", "hello eyeballs!"]

ENTRYPOINT 역시 docker run 명령어로 실행을 통해 곧바로 실행되는 것을 볼 수 있음

CMD 와 마찬가지로 ENTRYPOINT로 실행된 프로세스(echo)는 pid 1 번으로 실행되어 컨테이너의 생명 주기에 관여하게 됨

(docker run 명령어 이후 echo 출력하고 곧바로 stop 됨)

 

그럼 CMD 와 ENTRYPOINT 차이는?

ENTRYPOINT 명령어가 먼저 오고, CMD 가 ENTRYPOINT 명령의 파라미터를 넣어주는 역할을 한다고 함

docker run 을 통해 실행되는, 생명주기에 관여하게되는 pid 1번 프로세스는 ENTRYPOINT 의 명령어로 실행되고

CMD 명령어는 ENTRYPOINT 명령어에 이어 붙이는 파라미터, arguments 들을 넣어주는 역할

 

예를 들어보자

< Dockerfile >
FROM ubuntu:latest
ENTRYPOINT  ["top"]
CMD ["-b", "-c"]

docker run 을 실행하면

top -b -c 명령어가 실행됨

 

docker inspect 를 통해 확인해보면 다음과 같이 CMD, ENTRYPOINT 명령어가 들어있는 것을 볼 수 있음

sudo docker inspect myimage 
......
            "Cmd": [
                "-b",
                "-c"
            ],
.....
            "Entrypoint": [
                "top"
            ],
.....

 

추가) docker run 실행시 --entrypoint 옵션으로 pid 1번 프로세스 실행 변경이 가능함

 

참고 : https://asung123456.tistory.com/51


 

docker run 명령어는 attatched 모드로 컨테이너를 실행하고

docker start 혹은 restart 명령어는 detached 모드로 컨테이너를 실행

attatched 모드는 포그라운드에서 동작하면서 컨테이너 내의 출력 결과가 계속 출력되는 모드

detached 모드는 백그라우드에서 동작하며 사용자와 인터랙션이 없는 모드

 

docker run 명령어로 detached 모드를 사용하려면 -d 옵션을 붙이면 됨

docker run -d eyeballs

반대로 docker start 명령어로 attached 모드를 사용하려면 -a 옵션을 붙이면 됨

docker start -a eyeballs

detached 모드로 실행중인 컨테이너에 attached 모드로 붙으려면 docker attach 명령어 사용하면 됨

docker attach eyeballs

(docker run 실행했을 때 처럼 attached 모드로 붙을 수 있음)

 


 

docker container 의 로그를 지속적으로 보고 싶다면 -f 옵션 추가

docker logs -f eyeballs

 


 

도커가 run 되면 echo program(사용자 입력을 받고 그대로 출력하는 ) 파이썬 스크립트를 실행하는 이미지(echo-python)가 있다고 하자

이 이미지로 run 하면

docker run --name echo-python echo-python

사용자 입력을 받아야하는데 받지 못해서 에러가 남

 

사용자 입력을 받을 수 있도록 하려면 -it 옵션을 추가해야 함

docker run -it --name echo-python echo-python

 

  -i, --interactive                    Keep STDIN open even if not attached
  -t, --tty                            Allocate a pseudo-TTY (teletypewriter)

 

내가 흔히 사용하는, docker run -d image bash 와 docker exec -it container bash 두 단계를 거치지 않아도

docker 이미지 내 CMD 명령어를 통해 처음 실행하는 프로그램을 곧바로 사용 가능

 

이런 컨테이너가 stop 된 이후에 start 하게 되면

기본적으로 detached 모드가 되니 상호작용이 불가능

따라서 start 할 때 -a 옵션을 줘서 attached 모드로 실행하고, -i 옵션을 줘서 interactive 활성화 해야 함

docker stop echo-python
docker start -ai echo-python

 


 

현재 컨테이너로 사용되지 않는, 모든 이미지를 지우는 명령어

docker image --prune

  prune       Remove unused images


현재 stop 상태인 모든 container 를 지우는 명령어

docker container --prune

  prune       Remove all stopped containers

 


 

다운받은 Docker Image 의 상세 정보, 예를 들어

어떤 port 를 오픈할 수 있는지, 어떤 이미지를 base 로 삼고 있는지,

어떤 CMD 명령어를 실행하는지, entrypoint가 무엇인지 등을 알고 싶다면 inspect 명령어 사용

docker inspect [image]

 


 

Docker 컨테이너가 삭제되어도, 동일한 이미지로 다시 컨테이너를 띄웠을 때

데이터를 유지할 수 있는 방법은 volume 옵션을 사용하는 것

 

내가 알고있는 volume 은 host 의 path 와 연결하는 것인데,

docker 자체에서 알아서 path 를 정해서 host 어딘가에 데이터를 저장하는 방법을 제공할 수도 있음

 

예를 들어

docker run -v /home/eye/docker/:/app

위와 같은 volume 옵션을 준다면,

내 local host 머신의 /home/eye/docker 내부의 모든 파일, 디렉터리들이

docker container 내부의 /app 위치에 동기화 될 것이며

내가 host 머신에서 해당 파일, 디렉터리들에 직접 접근이 가능함

 

하지만 아래처럼 local path 가 아닌, volume 이름만 준다면

docker run -v myvolume:/app

docker 가 내 local host 머신 어딘가에 myvolume 에 해당하는 위치를 지정해두고

docker container 내부의 /app 에 저장되는 파일, 디렉터리들을 myvolume 위치에 동기화 시킬 것임

나는 host 머신에서 myvolume 위치에 접근이 불가능함

 

만약 아래처럼 이름조차 주지 않는다고 하더라도

docker run -v /app

 

docker 가 내 local host 머신 어딘가에 익명의 이름으로 path 를 지정해두고

docker container 내부의 /app 에 저장되는 파일, 디렉터리들을 동기화시킴

그래서 container 가 죽어도 파일/디렉터리들을 유지할 수 있음

 

위와 같이 내가 접근이 불가능한, 이름으로 volume 을 주고 그 목록을 보고자 한다면 아래 명령어 사용

docker volume ls

 

docker volume 을 삭제하려면 아래 명령어 사용

당연한 이야기지만 아래 명령어로 volume 을 삭제하면,

volume path 에 저장한 데이터들도 모두 삭제됨

docker volume rm [VOLUME NAME]

 

 

특정 이름/익명의 이름으로 추가된 volume 은 container 가 관리하게 되지만

내 local 머신의 path 와 container 내 path 를 잇는 volume 은 container 가 관리하지 않아

docker volume ls 명령어에 나타나지 않음

 


 

volume 을 사용할 때 주의할 점은,

Dockerfile RUN 명령어를 통해 실행되고 저장된 어떤 데이터를

Docker volume 으로 덮어써버릴 수 있다는 것임

 

예를 들어

< Dockerfile >
...
WORKDIR /app
RUN npm install
...

위와 같은 Dockerfile 은 npm install 명령어를 통해 /app 에 설치 데이터를 저장함 (정확히는 /app/node_modules 에 저장됨)

아래 명령어로 Docker run 을 하게되면

docker run -v /home/eye/myapps:/app ...

Container 내부 /app 위치에 저장되어있던 npm 설치 데이터들이 모두 덮어쓰여지게 됨

 

이런 경우를 위해, 덮어쓰지 말아야 하는 디테일한 path(/app/node_modules) 는 익명 volume 으로 처리하는 옵션을 추가

docker run -v /home/eye/myapps:/app -v /app/node_modules ...

위와 같이 지정해주면 docker container 내부의 /app/node_modules 가 첫번째 볼륨에 의해 덮어쓰여지지 않게 됨

 

 

 


 

docker volume 을 지정하면,

local 머신과 container 내부가 서로  공유하는 path 가 생기고

이 path 에 존재하는 파일들에 서로 read 와 write 가 가능함

local 머신에서도 파일을 읽고 쓸 수 있고

container 내부에서도 파일을 읽고 쓸 수 있다는 말임

 

그런데 나는 container 내부에서 파일을 읽기만 하고 쓰지는 못하도록 하고 싶음

즉, local 머신에서만 쓸 수 있도록 하고 싶음

그럴 땐 volume 옵션에 read only 성격을 추가하면 됨

 

docker run -v "/home/eye/myapps:/app:ro"

위와 같이 ro 옵션을 주면, container 내부에서는 /app 및 /app 이하 모든 path 에서 write 가 불가능하게 됨

 

volume 옵션이 두 가지 있다면,

좀 더 깊이있는 path 를 갖는 volume 옵션의 우선순위가 더 높음

예를 들어 다음과 같은 옵션이 있을 때

docker run -v "/home/eye/myapps:/app:ro" -v "/home/eye/myapps/modules:/app/modules"

 

/app:ro 보다 /app/modules 가 좀 더 깊이있기 때문에

/app/modules 의 우선순위가 높음

즉, container 에서는 /app path 에 있는 파일에 write 가 불가능하지만

/app/modules path 에 있는 파일에는 write 가 가능하다는 말임


 

docker container 가 관리하는 volume 의 자세한 내용을 보고 싶다면

(언제 생성되었고 이름은 무엇이고 local 머신 어디에 저장되어있는지... 등등) 

docker volume inspect [VOLUME 이름]

위 명령어로 mountpoint 가 나타나며

이는 docker container 의 가상머신 상의 경로라서

local 머신에서 찾을 수 없음

 


 

현재 사용되지 않는 모든 local volume 을 지우는 명령어

docker volume --prune

prune       Remove unused local volumes

 


 

Dockerfile 에서 COPY 명령어로 데이터를 container 내부로 복사할 때

복사하지 말아야 할 리스트인 .dockerignore 를 만들어서 복사하지 않도록 만들 수 있음

< .dockerignore >
Dockerfile
README.md
.git

 

 


 

docker container 내에 환경변수를 넣어줄 수 있음

여기서 말하는 환경변수는 linux 에서 사용하는 환경변수 그거임

 

예를 들어 아래와 같은 Dockerfile 을 docker run 해보면

< Dockerfile >
FROM ubuntu:latest
ENV MYNAME eyeballs

 

 

이렇게 container 내부 환경변수를 미리 지정할 수 있음

환경변수를 지정하는 방법은 총 3가지

 

1. Dockerfile 에 ENV 명령어로 지정

  위 예제대로 넣어줄 수 있음

 

2. docker run 명령어에 --env 혹은 -e 옵션으로 지정

  위 예제처럼 MYNAME 을 넣으려면 아래처럼 옵션 추가

docker run --env MYNAME=eyeballs ...
혹은
docker run -e MYNAME=eyeballs ...

 

3. docker run 명령어에 --env-file 로 환경변수가 저장된 파일를 지정

< .myenvfile >
docke run --env-file ./.myenvfile ...

 

만약 1번 방법과 2번 방법이 동시에 사용된다면?

2번 방법으로 들어간 환경변수가 더 높은 우선순위를 갖게 됨

만약 2번 방법으로 환경변수를 넣어줘야 하는데 넣지 못했다면,

1번 방법으로 들어간 환경변수가 대신 사용됨

즉, 1번 방법으로 들어간 환경변수는 default 값이 되는 것임

 

강의에서 "ENV MYNAME eyeballs" 처럼 설정했다가, 환경 변수가 default 로 적용되지 않아서

"ENV MYNAME=eyeballs" 처럼 등호를 넣어 다시 설정하니 되는 것을 보았음

1번 방법이 default 로써 사용되려면 등호를 넣어서 설정해야 하나 봄

 


 

ARG 명령어는 Dockerfile 내부에서만 사용 가능한 상수값

아래처럼 ARG 에 설정한 값은 ENV 등에서 사용이 가능함

FROM node
ARG DEFAULT_PORT=80
ENV PORT $DEFAULT_PORT

위와 같이 Dockerfile 내부에서 지정할 수 있고,

docker run 명령어에 옵션을 주어 지정도 가능

docker run --build-arg DEFAULT_PORT=8000 ...

ARG 가 지정되고나면 값은 고정되어버림

위와 같이 옵션을 통해 값을 지정하면, 이미지는 동일하나 ARG 값이 다른 여러 container 를 만들 수 있음

 

참고로 ARG 값은 CMD 명령어에서 사용 불가능함

ARG 명령어와 ARG 로 설정한 값을 사용하는 명령어는

Dockerfile 의 마지막에 넣어두는 게 좋음

ARG 값이 바뀌면 이미지를 빌드할 때 (업데이트가 된) ARG 명령어 이후의 모든 명령어를 다시 빌드하게 되기 때문

빌드를 다시 하지 않아도 되는 명령어들까지 빌드하게 되므로 리소스 및 시간이 낭비되기 때문

 


 

Docker container 내부에서, WWW (외부 인터넷) 에 존재하는 웹사이트 접근이나 API 요청하기 위해

따로 설정해야 하는 것은 없음. 그냥 가능함


Docker container 내부에서, container 가 실행되는 host 머신에 접근하려면

(localhost 가 아닌) host.docker.internal 로 접근하면 됨.

예를 들어 host 머신 위에 mongodb 가 실행중이고

Docker container 내부에서 host 머신의 mongodb 에 접근하려면 아래와 같은 방식으로 접근 (수도코드)

mongodb.connect('mongodb://host.docker.internal:27017/info')

 

Docker container 내부에서 host 머신으로 ping 을 날릴때도 이와 비슷함

ping host.docker.internal

 


 

Docker container 간 네트워크 연결이 가능함

예를 들어 AA container 와 BB container 가 존재하고

AA 가 BB 에게 ping 을 날리는 상황을 가정하자

 

AA container 내부에서 ping 을 실행하려면

BB container 의 ip 주소를 알아야 함

BB container 의 ip 주소는 docker container inspect BB 명령어 결과에서 볼 수 있음

결과중에 NetworkSettings.IPAddress 부분을 보면 BB container 의 ip 주소가 나타남

이렇게 얻은 ip 주소를 이용하여 AA container 는 ping 을 날릴 수 있음

 

위와 같이 ip 주소를 직접 얻어 사용하면

ip 주소를 사용하는 코드에서 하드코딩을 하게 되고

또 ip 주소가 바뀌면 하드코딩 또한 바뀌고 이미지 빌드도 다시 해야하고 실행도 다시 해야하기 때문에 아주 불편함

따라서, container 간 통신을 위해 ip 주소를 사용하는 대신,

하나의 network 로 container 들을 묶는 기술을 사용해야 함 (바로 아래서 설명)

 


 

하나의 docker network 를 만들고

그 network 에 container 들을 연결시켜두면

container 들끼리 자기들의 이름으로 통신이 가능하게 됨

 

아래 명령어를 통해 network 를 하나 만듦

network 의 이름은 간단하게 mynetwork 라고 하자

docker network create mynetwork

 

만든 network 는 아래 명령어로 확인이 가능

docker network ls

 

각 container 를 실행할 때 내가 만든 mynetwork 에 연결하여 실행

예를 들어 AA container, BB container 를 실행할 때 아래처럼 --network 옵션을 추가하여 연결

docker run --name AA --network mynetwork ...
docker run --name BB --network mynetwork ...

 

그럼 AA container 내부에서 BB container 의 이름(BB)으로 ping 을 날릴 수 있게 됨

ping BB

 

AA container 에서 Mongodb container 에 연결하려면 docker run 실행시 -p 옵션으로 포트를 열어야할까?

아님. -p 옵션은 host 머신에서 달라붙기 위해 오픈하는 포트 번호를 넣는 옵션일 뿐

container 간 연결에서는 -p 옵션을 넣지 않아도 container 간 포트가 열려있음


 

 

어떤 프로그램, 툴 의 공식 이미지는 '사용 방법'이 다 정해져서 나옴

예를 들어 mongodb 공식 이미지는 "mongo"이며

https://hub.docker.com/_/mongo 에서 확인할 수 있음

 

공식 이미지는, 만들어질 때 내부에서 프로그램을 어떻게 운용할지 정해둠

즉, pid 1번 프로세스는 무엇이며

로그는 어느 dir 에 저장하며

기본 port 는 무엇이며

환경변수에 따라 어떤 설정값이 추가되는지 등

그리고 그 '사용 방법'은 대개 docker image 에 안내되어있음

예를 들어 mongodb 공식 이미지의 경우

mongodb 프로세스가 데이터를 저장하는 dir 위치를 /data/db  로 설정해두었고

 

mongodb 의 root 사용자를 정의하려면

MONGO_INITDB_ROOT_USERNAME,  MONGO_INITDB_ROOT_PASSWORD 라는

두 가지 환경 변수를 사용해야 한다는 내용이 이미 image 내에 포함되어 있음

 

따라서 우리는 공식 이미지를 사용할 때, '사용 방법'에 따라 사용하면 됨

(우리가 '사용 방법'을 직접 추리할 수는 없는 노릇)

 

 


 

 

Docker Container 는 리소스(memory, cpu, io bps) 제한이 없음

호스트의 리소스를 제한 없이 사용함

따라서 Container 에서 리소스를 많이 사용하는 만큼

호스트 머신에서 동작하는 다른 프로세스에 영향이 끼침

심지어 Container 의, Container 에 의해 Out Of Memory 도 발생할 수 있음

 

Container 의 Memory 및 CPU 제한을 확인하려면 docker inspect 명령을 사용

아래 스샷은 리소스 제한이 없는 Container 의 Memory 와 Cpu 상태를 보여줌 (리소스 제한이 없어서 0으로 나타남)

 

 


 

 

Container 에 memory 제한을 주려면 다음과 같은 옵션을 사용

docker run -d -it --memory=1g ubuntu
docker run -d -it --memory=500m ubuntu

 

 

"Memory" 에 숫자가 나타나며, 숫자가 1gb 크기를 보여주는 것을 확인 가능

"MemorySwap" 을 따로 지정해주지 않으면 "Memory" 의 2배 크기만큼 잡힌다고 함

"MemorySwap" 을 따로 지정하려면 아래 옵션 추가

docker run -d -it --memory=1g --memory-swap=1g ubuntu
docker run -d -it --memory=1g --memory-swap=3.1g ubuntu

 

이 외에 --memory-swappiness, --kernel-memory, --oom-kill-disable 등의 옵션이 존재함

 


 

 

Container 에 cpu 제한을 주려면 다음과 같은 옵션을 사용

--cpus : Container 내부에서 제한하고 싶은 만큼의 cpu 점유율을 퍼센트 수치로 지정
host cpu 가 4개 존재하고, --cpus 에 0.5 를 넣으면
Container 에 2개(50%) 만큼의 cpu 성능 제한이 설정됨
docker run -d -it --cpus=0.5 ubuntu

 

--cpuset-cpus : host cpu 중 특정 cpu 만 사용하도록 지정
host 머신에서 htop 으로 볼 수 있는 cpu 의 index 를 넣으면
Container 는 넣은 index 에 해당하는 cpu 만 사용
index 는 0부터 시작 
docker run -d -it --cpuset-cpus=0 ubuntu #0번 index 에 해당하는 cpu 사용
docker run -d -it --cpuset-cpus=0,1 ubuntu #0번, 1번 index 에 해당하는 cpu 사용
docker run -d -it --cpuset-cpus=0-2 ubuntu #0번, 1번, 2번 index 에 해당하는 cpu 사용

 

 

이 외에 --cpu-period, --cpu-quata, --cpu-shares  등의 옵션이 존재함

 


 

리소스 제한 설정을 하지 않은 상태로 이미 올라간 Container 에

리소스를 제한하고 싶다면 update 명령어 사용

docker run -d -it --name eyeballs ubuntu
docker update --memory=1g eyeballs

 

참고로 부하 테스트를 진행할때 사용할 수 있는 stress 라는 명령어가 존재함

cpu 부하, memory 부하를 임의로 일으킬 수 있음

 

 

 

 


 

 

Docker 에 대한 내용이 너무 길어져서

Docker Compose 에 대한 내용은 다른 포스트로 올림

 

https://eyeballs.tistory.com/642

 

[Docker] Udmey Docker & Kubernetes : 실전 가이드 필기 - Docker Compose

Docker Compose 는 다수의 "docker build" 와 다수의 "docker run" 명령어를 대체하는 도구(orchestration 명령 모음) Docker compose 툴로 다수의 container 들을 한 번에 손쉽게 관리할 수 있음 하나의 Docker compose 를 사

eyeballs.tistory.com

 

 

 

 

 

Airflow 테스트를 위해

Docker 로 Airflow 설치하는 방법을 설명함

 

Centos 7 을 사용하였고, Airflow 는 Sequential Executor 를 사용함.

 

 

 

Centos 7 Docker Image : https://eyeballs.tistory.com/543

 

위 Docker Image 를 만들어준 다음, 아래 명령어로 container 하나 띄움

 

$ docker run --name airflow -p 8080:8080 --privileged -d mycentos:7 init

 

아래 명령어로 docker container 들어가서 필요한 것들 설치 진행

$ docker exec -it airflow bash
$ yum upgrade -y
$ yum install python3 wget vim sudo gcc make -y

 

아래 명령어로 sqlite 최신 버전 설치 [참고]

( sqlite 다운로드 페이지 :https://www.sqlite.org/download.html )

만약 sqlite3 --version 이 3.7.17

$ cd /opt
$ wget https://www.sqlite.org/2023/sqlite-autoconf-3430100.tar.gz
$ tar -xzf sqlite-autoconf-3430100.tar.gz 
$ cd sqlite-autoconf-3430100
$ make
$ make install

위처럼 sqlite3 를 최신 버전으로 업그레이드 하는 경우는

pip 로 airflow 설치시 3.8.3 이상의 sqlite 버전을 필요로 하기 때문.

can't find new sqlite version? (SQLite 3.8.3 or later is required (found 3.7.17))

만약 sqlite3 버전이 3.8.3 이상이라면 위와 같이 최신 버전으로 업그레이드 하지 않아도 됨

 

 

airflow 라는 사용자를 만들어서 sudo 권한 부여

$ useradd airflow
$ usermod -aG wheel airflow

 

아래 명령어로 /etc/sudoers 편집 진행

아래 주석 처리되어있는 부분의 주석을 해제

$ visudo
전) #%wheel  ALL=(ALL)       NOPASSWD: ALL
후) %wheel  ALL=(ALL)       NOPASSWD: ALL

 

아래 명령어로 사용자 airflow 로 접근

지금부터 나오는 명령어들은 모두 airflow 계정으로 진행

$ su - airflow

 

아래 명령어로 pip3 업그레이드 진행 및 setuptool 설치

$ sudo -H pip3 install --upgrade --ignore-installed pip setuptools

 

 

아래 명령어로 airflow home 을 만듦

$ mkdir ~/airflow
$ export AIRFLOW_HOME=~/airflow

 

아래 명령어로 sqlite3 가 최신 버전(>3.7.17)으로 업그레이드 되었는지 확인

$ pyhon3
>>> import sqlite3
>>> sqlite3.sqlite_version
'3.7.17'
$ export LD_LIBRARY_PATH="/usr/local/lib"
$ python3
>>> import sqlite3
>>> sqlite3.sqlite_version
'3.43.1'

 

아래 명령어로 airflow 설치

나는 2.2.2를 설치하고 싶어서 직접 2.2.2를 넣었으니, 원하는 버전을 넣으면 됨.

$ AIRFLOW_VERSION=2.2.2
$ PYTHON_VERSION="$(python3 --version | cut -d " " -f 2 | cut -d "." -f 1-2)"
$ CONSTRAINT_URL=" https://raw.githubusercontent.com/apache/airflow/constraints-$ {AIRFLOW_VERSION}/constraints-${PYTHON_VERSION}.txt"
$ pip3 install "apache-airflow==${AIRFLOW_VERSION}" --constraint "${CONSTRAINT_URL}"

 

아래 명령어로 apache airflow 의 admin 유저 생성

$ airflow users  create --role Admin --username airflow --email airflow --firstname airflow --lastname airflow --password airflow

 

아래 명령어로 apache airflow 를 Sequential Executor 모드로 실행

$ airflow standalone

 

잠시 후 웹브라우저에서 localhost:8080 에 접근하여 WebUI 가 뜨는지 확인

id, pw 는 각각 airflow, airflow 로 접근 가능

 

dags 위치는 airflow.cfg 에서 찾아볼 수 있음 ($AIRFLOW_HOME/dags)

$ cat $AIRFLOW_HOME/airflow.cfg | grep dags_folder

 

 

 

 

 

 

 

참고

https://airflow.apache.org/docs/apache-airflow/2.2.2/start/local.html

https://www.webdesignsun.com/insights/upgrading-sqlite-on-centos/

https://musclebear.tistory.com/131

https://sun2day.tistory.com/216

 

"Data Pipellines with Apache Airflow

Apache Airflow 기반의 데이터 파이프라인

에어플로 중심의 워크플로 구축에서 커스텀 컴포넌트 개발 및 배포, 관리까지"

책 내용 공부한 후, 나중에 다시 참고할 내용 필기해 둠

책은 Airflow 2.0.0 버전을 기준으로 쓰여졌지만,

나는 Airflow 2.2.2 버전에서 dag 구현 및 테스트 진행함

 

 

 


 

기본 DummyOperator, BashOperator, PythonOperator 샘플 코드

from datetime import datetime, timedelta

from airflow import DAG
from airflow.operators.bash import BashOperator
from airflow.operators.python import PythonOperator
from airflow.operators.dummy import DummyOperator

def _func():
    print('hello python!')

with DAG(
    dag_id='eyeballs_dag',
    schedule_interval='0 0 * * *',
    start_date=datetime(2023, 6, 25),
    catchup=False,
    dagrun_timeout=timedelta(minutes=60),
    tags=['eyeballs'],
    params={"example_key": "example_value"},
) as dag:

    start = DummyOperator(
        task_id='dummy_operator',
    )

    bash_operator = BashOperator(
        task_id='bash_operator',
        bash_command='echo hello bash!',
    )

    python_operator = PythonOperator(
        task_id='python_operator',
        python_callable=_func,
    )

    start >> bash_operator >> python_operator

 

 


 

dag 에 schedule_interval 은 cron 으로 넣을 수 있지만,

@daily 등으로 넣을 수 있음 [참고]

with DAG(
    dag_id='eyeballs_dag',
    schedule_interval='@daily',
    start_date=datetime(2023, 6, 25),
) as dag:
preset meaning cron
None Don’t schedule, use for exclusively “externally triggered” DAGs  
@once Schedule once and only once  
@hourly Run once an hour at the beginning of the hour 매시간 0 * * * *
@daily Run once a day at midnight 매일 자정 0 0 * * *
@weekly Run once a week at midnight on Sunday morning 매주 일요일 자정 0 0 * * 0
@monthly Run once a month at midnight of the first day of the month 매월 1일 자정 0 0 1 * *
@yearly Run once a year at midnight of January 1 매년 1월 1일 자정 0 0 1 1 *

 

 


 

 

종료 날짜를 지정하면, 종료 날짜까지만 dag 스케줄링을 진행

예를 들어 아래와 같이 1월 5일까지만 지정하면

execution_date 가 1월 5일 값을 갖는 날까지(즉 현실 시간으로 1월 6일) 동작

with DAG(
    dag_id='eyeballs_dag',
    schedule_interval='@daily',
    start_date=datetime(2023, 1, 1),
    end_date=datetime(2023, 1, 5)
) as dag:

 


 

며칠 몇 시에 동작하도록 스케줄링하는 게 아니라

3시간마다 혹은 10분마다 스케줄링하도록 만들려면

아래 코드와 같이 dt.timedelta 를 사용하면 됨

3일마다 스케줄링 : dt.timedelta(days=3)

2시간마다 스케줄링 : dt.timedelta(hours=2)

with DAG(
    dag_id='eyeballs_dag',
    schedule_interval='dt.timedelta(days=3)',
    start_date=datetime(2023, 6, 25),
) as dag:

 


 

jinja template 진자 템플릿을 이용하여

airflow 가 기본적으로 갖고 있는 변수 중 하나인

execution_date 을 bash operator 에서 사용하는 샘플 코드

 

아래 dag 를 실행하는 실제 날짜는 10월 1일인 상황

 

코드 :

    b1 = BashOperator(
        task_id='b1',
        bash_command='''
            echo execution_date : {{execution_date}} ;
            echo ds : {{ds}} ;
            echo ds_nodash : {{ds_nodash}} ;
            echo execution_date with strftime : {{execution_date.strftime("%Y_%m_%d")}}
        '''
    )

결과 :

[2023-10-01, 13:43:54 UTC] {subprocess.py:89} INFO - execution_date : 2023-09-30T00:00:00+00:00
[2023-10-01, 13:43:54 UTC] {subprocess.py:89} INFO - ds : 2023-09-30
[2023-10-01, 13:43:54 UTC] {subprocess.py:89} INFO - ds_nodash : 20230930
[2023-10-01, 13:43:54 UTC] {subprocess.py:89} INFO - execution_date with strftime : 2023_09_30

 

코드 :

    b2 = BashOperator(
        task_id='b2',
        bash_command='''
            echo next_execution_date : {{next_execution_date}} ;
            echo next_ds : {{next_ds}} ;
            echo next_ds_nodash : {{next_ds_nodash}} ;
        '''
    )

결과 :

[2023-10-01, 13:43:58 UTC] {subprocess.py:89} INFO - next_execution_date : 2023-10-01T00:00:00+00:00
[2023-10-01, 13:43:58 UTC] {subprocess.py:89} INFO - next_ds : 2023-10-01
[2023-10-01, 13:43:58 UTC] {subprocess.py:89} INFO - next_ds_nodash : 20231001

 

코드 :

    b3 = BashOperator(
        task_id='b3',
        bash_command='''
            echo prev_execution_date : {{prev_execution_date}} ;
            echo prev_ds : {{prev_ds}} ;
            echo prev_ds_nodash : {{prev_ds_nodash}}
        '''
    )

결과 :

[2023-10-01, 13:44:01 UTC] {subprocess.py:89} INFO - prev_execution_date : 2023-09-29 00:00:00+00:00
[2023-10-01, 13:44:01 UTC] {subprocess.py:89} INFO - prev_ds : 2023-09-29
[2023-10-01, 13:44:01 UTC] {subprocess.py:89} INFO - prev_ds_nodash : 20230929

 

airflow 가 갖는 기본 변수 : https://airflow.apache.org/docs/apache-airflow/2.2.2/templates-ref.html

 

airflow 가 갖는 기본 변수는 다음과 같이 코드에서 직접 확인 가능

코드 :

def _func(**kwargs):
    for arg in kwargs:
        print(arg, ':', kwargs[arg])

...


p1 = PythonOperator(
    task_id='p1',
    python_callable=_func,
    dag=dag
)

결과 :

더보기
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - conf : <airflow.configuration.AirflowConfigParser object at 0x7f9bc30a6eb8>
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - dag : <DAG: execution_date>
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - dag_run : <DagRun execution_date @ 2023-10-01 13:23:29.703212+00:00: manual__2023-10-01T13:23:29.703212+00:00, externally triggered: True>
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - data_interval_end : 2023-10-01T00:00:00+00:00
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - data_interval_start : 2023-09-30T00:00:00+00:00
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - ds : 2023-10-01
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - ds_nodash : 20231001
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} WARNING - /home/airflow/.local/lib/python3.6/site-packages/airflow/models/taskinstance.py:1941 DeprecationWarning: Accessing 'execution_date' from the template is deprecated and will be removed in a future version. Please use 'logical_date' or 'data_interval_start' instead.
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - execution_date : 2023-10-01T13:23:29.703212+00:00
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - inlets : []
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - logical_date : 2023-10-01T13:23:29.703212+00:00
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - macros : <module 'airflow.macros' from '/home/airflow/.local/lib/python3.6/site-packages/airflow/macros/__init__.py'>
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} WARNING - /home/airflow/.local/lib/python3.6/site-packages/airflow/models/taskinstance.py:1941 DeprecationWarning: Accessing 'next_ds' from the template is deprecated and will be removed in a future version. Please use 'data_interval_end | ds' instead.
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - next_ds : 2023-10-01
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} WARNING - /home/airflow/.local/lib/python3.6/site-packages/airflow/models/taskinstance.py:1941 DeprecationWarning: Accessing 'next_ds_nodash' from the template is deprecated and will be removed in a future version. Please use 'data_interval_end | ds_nodash' instead.
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - next_ds_nodash : 20231001
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} WARNING - /home/airflow/.local/lib/python3.6/site-packages/airflow/models/taskinstance.py:1941 DeprecationWarning: Accessing 'next_execution_date' from the template is deprecated and will be removed in a future version. Please use 'data_interval_end' instead.
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - next_execution_date : 2023-10-01T13:23:29.703212+00:00
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - outlets : []
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - params : {'example_key': 'example_value'}
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - prev_data_interval_start_success : 2023-09-30T00:00:00+00:00
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - prev_data_interval_end_success : 2023-10-01T00:00:00+00:00
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} WARNING - /home/airflow/.local/lib/python3.6/site-packages/airflow/models/taskinstance.py:1941 DeprecationWarning: Accessing 'prev_ds' from the template is deprecated and will be removed in a future version.
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - prev_ds : 2023-10-01
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} WARNING - /home/airflow/.local/lib/python3.6/site-packages/airflow/models/taskinstance.py:1941 DeprecationWarning: Accessing 'prev_ds_nodash' from the template is deprecated and will be removed in a future version.
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - prev_ds_nodash : 20231001
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} WARNING - /home/airflow/.local/lib/python3.6/site-packages/airflow/models/taskinstance.py:1941 DeprecationWarning: Accessing 'prev_execution_date' from the template is deprecated and will be removed in a future version.
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - prev_execution_date : 2023-10-01T13:23:29.703212+00:00
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} WARNING - /home/airflow/.local/lib/python3.6/site-packages/airflow/models/taskinstance.py:1941 DeprecationWarning: Accessing 'prev_execution_date_success' from the template is deprecated and will be removed in a future version. Please use 'prev_data_interval_start_success' instead.
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - prev_execution_date_success : None
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - prev_start_date_success : 2023-10-01T13:43:50.677389+00:00
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - run_id : manual__2023-10-01T13:23:29.703212+00:00
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - task : <Task(PythonOperator): p1>
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - task_instance : <TaskInstance: execution_date.p1 manual__2023-10-01T13:23:29.703212+00:00 [running]>
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - task_instance_key_str : execution_date__p1__20231001
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - test_mode : False
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - ti : <TaskInstance: execution_date.p1 manual__2023-10-01T13:23:29.703212+00:00 [running]>
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} WARNING - /home/airflow/.local/lib/python3.6/site-packages/airflow/models/taskinstance.py:1941 DeprecationWarning: Accessing 'tomorrow_ds' from the template is deprecated and will be removed in a future version.
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - tomorrow_ds : 2023-10-02
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} WARNING - /home/airflow/.local/lib/python3.6/site-packages/airflow/models/taskinstance.py:1941 DeprecationWarning: Accessing 'tomorrow_ds_nodash' from the template is deprecated and will be removed in a future version.
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - tomorrow_ds_nodash : 20231002
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - ts : 2023-10-01T13:23:29.703212+00:00
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - ts_nodash : 20231001T132329
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - ts_nodash_with_tz : 20231001T132329.703212+0000
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - var : {'json': None, 'value': None}
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - conn : <airflow.models.taskinstance.TaskInstance.get_template_context.<locals>.ConnectionAccessor object at 0x7f9bc06c3748>
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} WARNING - /home/airflow/.local/lib/python3.6/site-packages/airflow/models/taskinstance.py:1941 DeprecationWarning: Accessing 'yesterday_ds' from the template is deprecated and will be removed in a future version.
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - yesterday_ds : 2023-09-30
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} WARNING - /home/airflow/.local/lib/python3.6/site-packages/airflow/models/taskinstance.py:1941 DeprecationWarning: Accessing 'yesterday_ds_nodash' from the template is deprecated and will be removed in a future version.
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - yesterday_ds_nodash : 20230930
[2023-10-01, 14:28:52 UTC] {logging_mixin.py:109} INFO - templates_dict : None

 


 

airflow 2.2 버전부터 헷갈리는 execution_date 대신 data_interval_start 를 사용하도록 업데이트 됨

 

https://blog.bsk.im/2021/03/21/apache-airflow-aip-39/

기존 이름 새로운 이름
execution_date data_interval_start, logical_date
next_execution_date data_interval_end, next_logical_date
prev_execution_date prev_logical_date

 


 

Airflow dag 를 작성할 때 가장 중요하게 고려해야 하는 두 가지는 원자성멱등성

 

원자성 : Airflow 태스크 수행이 성공하려면 완벽하게 성공해야 하고, 실패하려면 완벽하게 실패해야 함

멱등성 : 동일한 입력으로 동일한 태스크를 여러 번 호출해도 결과가 동일해야 함

 

 


 

 

zero-padding : 빈 앞자리를 0으로 채움

예를 들어 1월이면 01, 8일이면 08, 4시면 04시

execution_date 에서 추출한 월, 일, 시간에 zero-padding 을 넣는 방법을 다음과 같음

 

코드 :

    b1 = BashOperator(
        task_id='b1',
        bash_command='''
            echo execution_date : {{execution_date}} ;
            echo execution_date.year: {{execution_date.year}} ;
            echo execution_date.month: {{execution_date.month}} ;
            echo month with zero-padding : {{ '{:02}'.format(execution_date.month) }} ;
            echo execution_date.day: {{execution_date.day}} ;
            echo day with zero-padding : {{ '{:02}'.format(execution_date.day) }} ;
            echo execution_date.hour: {{execution_date.hour}} ;
            echo hour with zero-padding : {{ '{:02}'.format(execution_date.hour) }} ;
        '''
    )

결과 :

execution_date : 2023-10-01T07:23:29.703212+00:00
execution_date.year: 2023
execution_date.month: 10
month with zero-padding : 10
execution_date.day: 1
day with zero-padding : 01
execution_date.hour: 7
hour with zero-padding : 07

 


 

 

PythonOperator 가 실행하는 함수에서 execution_date 를 사용하는 여러가지 방법

def _func1(execution_date):
    print('execution_date : ', execution_date)
    year, month, day, hour, *_ = execution_date.timetuple()
    print('year : ', year)
    print('month : ', month)
    print('day : ', day)
    print('hour : ', hour)

def _func2(**context):
    print('execution_date : ', context['execution_date'])

def _func3(execution_date, **context):
    print('execution_date : ', execution_date)
    print('execution_date' in context) #False

...

    p1 = PythonOperator(
        task_id='p1',
        python_callable=_func1,
    )

    p2 = PythonOperator(
        task_id='p2',
        python_callable=_func2,
    )

    p3 = PythonOperator(
        task_id='p3',
        python_callable=_func3,
    )

 

p3 처럼 execution_date 를 파라미터에 직접 넣었다면

**context 에 execution_date 값은 불포함

 


 

 

 

PythonOperator 가 실행하는 함수에 변수 넣는 방법

def _func1(greeting, name, date):
    print(f"{greeting}! I'm {name}. Today is {date}")

def _func2(param1, param2, param3, **context):
    print(f"{param1}! I'm {param2}. Today is {param3}")

...

    p1 = PythonOperator(
        task_id='p1',
        python_callable=_func1,
        op_kwargs={
            "greeting":"hello",
            "name":"eyeballs",
            "date":"{{execution_date}}"
        }
    )

    p2 = PythonOperator(
        task_id='p2',
        python_callable=_func2,
        op_args=["hello", "eyeballs", "{{execution_date}}"]
    )

func1 결과 :

[2023-10-02, 00:31:57 UTC] {logging_mixin.py:109} INFO - hello! I'm eyeballs. Today is 2023-10-01T00:00:00+00:00

func2 결과 :

[2023-10-02, 00:32:00 UTC] {logging_mixin.py:109} INFO - hello! I'm eyeballs. Today is 2023-10-01T00:00:00+00:00

 

아래와 같이 Rendered 를 누르면 Python Operator 에서 함수로 넘겨준 파라미터들을 볼 수 있음

 

 

 


Airflow Task 간 데이터를 교환/전송할 때 xcom 을 사용할 수 있음

하나의 Task 가 xcom(피클링)을 통해 데이터를 메타스토어에 저장하고,

다른 Task 에서 xcom 이 저장한 데이터를 메타스토어로부터 읽는 것으로 데이터를 교환함

 

피클 Picke 이란, Python 객체를 디스크에 저장할 수 있게 하는 직렬화 프로토콜

피클링된 객체는 메타스토어의 블록 blob 에 저장되기 때문에

일반적으로 문자열 몇 개와 같은 작은 데이터 전송에만 적용 가능

 

Task 간 좀 더 큰 데이터를 교환 할 때는 xcom 이 아닌 외부 데이터베이스를 사용해야 함

 

xcom 샘플 코드

def _push_xcom(**context):
    context["task_instance"].xcom_push(key="name", value="eyeballs")

def _pull_xcom(**context):
    name=context["task_instance"].xcom_pull(task_ids="push_op", key="name")
    print(f"Hello, {name}!")

...

    push_op = PythonOperator(
        task_id='push_op',
        python_callable=_push_xcom,
    )

    pull_op = PythonOperator(
        task_id='pull_op',
        python_callable=_pull_xcom,
    )

    push_op >> pull_op

위 코드에서 사용된 xcom_pull 은 현재 DAG 실행을 통해 게시된 값만 가져옴

다른 날짜에서 실행된 task 가 xcom_push 로 넣은 값을 (키값이 동일해도) 가져오지 않는다는 말임

 

 


 

의존성 fan-out, fan-in

a >> [b,c] >> d

 


트리거 규칙 Trigger Rule 이란, Task 가 트리거되는 기준을 의미함

즉, Task 가 언제 어떤 조건이 되었을 때 실행 될 것인지를 말 함

 

airflow Trigger Rule 문서 :

https://airflow.apache.org/docs/apache-airflow/2.2.2/concepts/dags.html?highlight=all_failed#trigger-rules 

  • all_success (default): All upstream tasks have succeeded
    모든 상위 태스크가 성공하면 트리거됨
  • all_failed: All upstream tasks are in a failed or upstream_failed state
    모든 상위 태스크가 실패하면 트리거됨
    태스크 그룹에서 하나 이상 실패가 예상되는 상황에서 오류 처리 코드를 트리거하는 용도로 사용
  • all_done: All upstream tasks are done with their execution
    성공/실패 상관 없이 모든 상위 태스크가 실행을 마치면 트리거됨
    모든 상위 태스크 실행 완료 후 청소 코드 등을 실행하는 용도로 사용
  • one_failed: At least one upstream task has failed (does not wait for all upstream tasks to be done)
    하나 이상의 상위 태스크가 실패하자마자 트리거됨. 다른 상위 태스크의 실행 완료는 기다리지 않음
    알림 또는 롤백 같은 일부 오류 처리 코드를 빠르게 실행하기 위한 용도로 사용
  • one_success: At least one upstream task has succeeded (does not wait for all upstream tasks to be done)
    하나 이상의 상위 태스크가 성공하자마자 트리거됨. 다른 상위 태스크의 실행 완료는 기다리지 않음
    하나의 결과를 사용할 수 있게 되는 즉시 다운스트림 연산/알람을 빠르게 실행하기 위한 용도로 사용
  • none_failed: All upstream tasks have not failed or upstream_failed - that is, all upstream tasks have succeeded or been skipped
    실패한 상위 태스크는 없지만, 태스크가 성공 또는 건너 뛴 경우 트리거됨
    DAG 에 조건부 브랜치 구조가 있는 경우 사용
  • none_failed_min_one_success: All upstream tasks have not failed or upstream_failed, and at least one upstream task has succeeded.
  • none_skipped: No upstream task is in a skipped state - that is, all upstream tasks are in a success, failed, or upstream_failed state
    건너뛴 상위 태스크는 없지만, 태스크가 성공 또는 실패한 경우 트리거됨
    모든 업스트림 태스크가 실행된 경우, 해당 결과를 무시하고 실행하기 위한 용도로 사용
  • always: No dependencies at all, run this task at any time
    상위 태스크 상태와 상관없이 트리거됨
    테스트 용도로 사용

 


 

센서는 특정 조건이 true 인지 지속적으로 확인하고 true 라면 성공

만약 특정 조건에 false 가 뜨면, 상태가 true 가 될 때 까지 혹은 지정된 timeout 이 날 때까지 계속 확인함

 

PythonSensor 의 경우, PythonSensor가 사용하는 함수의 return 값이 true 인지 false 인지를 계속 확인함

PythonSensor 샘플 코드

from datetime import datetime, timedelta

from airflow import DAG
from airflow.operators.python import PythonOperator
from airflow.sensors.python import PythonSensor
from airflow.operators.dummy import DummyOperator

def _sensor():
    if datetime.now().second == 30 : return True
    else : return False

with DAG(
    dag_id='sensor',
    schedule_interval='0 0 * * *',
    start_date=datetime(2023, 6, 25),
    catchup=False,
) as dag:

    start_op = DummyOperator(
        task_id='start_op',
    )

    sensor_op = PythonSensor(
        task_id='sensor_op',
        python_callable=_sensor,
    )

    stop_op = DummyOperator(
        task_id='stop_op',
    )

    start_op >> sensor_op >> stop_op

 

 


 

 

sensor 가 무한정 기다리는 상황이 닥치게 되면

그 다음 스케줄링 때 다시 sensor 가 실행되고

그 다음 스케줄링 떄 다시 sensor 가 실행되고...

이렇게 sensor 가 끝나지 않고 계속 누적되어 동작하여

DAG 내의 최대 task 수 (default 16) 개에 도달하게 되면

새로운 task 가 실행 자체를 못 하는 경우가 발생함

 

따라서 DAG 내 최대 task 수를 조절하거나

sensor 의 timeout 을 하루 등으로 줄이거나

poke 모드 대신 reschedule 모드를 사용하여 포크 동작을 실행할 때만 task 슬롯을 차지하고

대기 시간 동안은 task 슬롯을 차지하지 않도록 만들어야 함

 


 

Dag 에서 다른 Dag 를 Trigger 할 수 있음

TriggerDagRunOperator 샘플 코드

from datetime import datetime, timedelta

from airflow import DAG
from airflow.operators.dummy import DummyOperator
from airflow.operators.trigger_dagrun import TriggerDagRunOperator

with DAG(
    dag_id='dag1',
    schedule_interval='0 0 * * *',
    start_date=datetime(2023, 6, 25),
) as dag1:
    start_op = DummyOperator(
        task_id='start_op',
    )
    trigger_dag = TriggerDagRunOperator(
        task_id='trigger_dag',
        trigger_dag_id='dag2'
    )
    start_op >> trigger_dag 

with DAG(
    dag_id='dag2',
    schedule_interval=None,
    start_date=datetime(2023,6,25),
) as dag2:
    start_op = DummyOperator(
        task_id='start_op',
    )
    start_op

결과 : dag2 의 Schedule 은 None 임에도 불구하고, dag1 에 의해 호출되어 실행됨

 


 

Dag 간 fan-in 의존성 맞추는 방법은 시스템 상에서 제공되지 않음

예를 들어 dag1과 dag2가 모두 성공해야 dag3 이 트리거되는 방법은 없다는 말임

대신 xcom 등으로 dag1, dag2 의 성공 여부를 저장하고 dag3 에서 센서 등으로 결과값을 확인한다거나

ExternalTaskSensor 를 사용하여 dag1, dag2 의 상태를 꾸준히 확인하는 방법을 사용 가능함

 

Airflow ExternalTaskSensor Documentation : https://airflow.apache.org/docs/apache-airflow/2.2.2/howto/operator/external_task_sensor.html

 

ExternalTaskSensor 를 사용하는 코드 예제 (테스트 진행되지 않았으니, 반드시 테스트 진행 후 사용해야 함)

from datetime import datetime, timedelta

from airflow import DAG
from airflow.operators.dummy import DummyOperator
from airflow.sensors.external_task import ExternalTaskSensor

with DAG(
    dag_id='dag1',
    schedule_interval='0 0 * * *',
    start_date=datetime(2023, 6, 25),
    catchup=False,
) as dag1:
    dag1_op = DummyOperator(
        task_id='dag1_op',
    )

with DAG(
    dag_id='dag2',
    schedule_interval="0 0 * * *",
    start_date=datetime(2023,6,25),
    catchup=False,
) as dag2:
    dag2_op = DummyOperator(
        task_id='dag2_op',
    )

with DAG(
    dag_id='dag3',
    schedule_interval=None,
    start_date=datetime(2023,6,25),
    catchup=False,
) as dag3:
    dag1_sensor = ExternalTaskSensor(
        task_id = "dag1_sensor",
        external_dag_id = "dag1",
        external_task_id = "dag1_op"
    )
    dag2_sensor = ExternalTaskSensor(
        task_id = "dag2_sensor",
        external_dag_id = "dag2",
        external_task_id = "dag2_op"
    )
    start_op = DummyOperator(
        task_id='start_op',
    )
    [dag1_sensor, dag2_sensor] >> start_op

 


 

Task, DAG의 실행이 평소보다 오래 걸리는 상황을 파악하기 위해

SLA(Service-Level Agreement) 을 추가 가능

SLA 로 지정해 둔 제한 시간보다 실행 시간이 오래 걸린다면 경고를 주거나 python 함수 등을 실행하게 만듦

 

SLA는 Dag 및 Task 에 각각 설정 가능함

Task 의 시작 또는 종료 시간이 DAG 시작 시간고 비교하여 SLA 에 지정한 제한 시간을 넘겼는지 확인 할 것임

 

SLA 를 2초로 설정한 코드 예제 (테스트 진행되지 않았으니, 반드시 테스트 진행 후 사용해야 함)

from datetime import datetime, timedelta

from airflow import DAG
from airflow.operators.python import PythonOperator
from airflow.operators.dummy import DummyOperator

default_args={
    "sla":timedelta(seconds=2),
}

def sla_miss_callback(context):
    print("TOO LONG!")

def _func():
    sum = 0
    for i in range(99999999):
        sum = sum + i
    print(sum)

with DAG(
    dag_id='test',
    schedule_interval='0 0 * * *',
    start_date=datetime(2023, 6, 25),
    catchup=False,
    sla_miss_callback=sla_miss_callback,
    default_args=default_args,
) as dag:

    start = DummyOperator(
        task_id='dummy_operator',
    )
    
    python = PythonOperator(
        task_id='python',
        python_callable=_func
    )

    start >> python

 

다음과 같이 Operator 에 적용할 수 있다고 함

    python = PythonOperator(
        task_id='python',
        python_callable=_func,
        sla=timedelta(seconds=2)
    )

 

DAG 의 시작 시간과 Task 의 종료 시간을 계속 비교한다고 함

주의 할 점은 비교하는 시작 시간이 Task 의 시작 시간이 아니라 DAG 의 시작 시간이라는 것

(개별 Task 가 아닌 DAG 시작 시간을 기준으로 SLA 가 정의되었기 때문)

 


sla 뿐만 아니라, Task 가 성공했을 때 혹은 Task 가 실패했을 때 callback 을 호출하도록 만들 수 있음

 

NameDescription
on_success_callback Invoked when the task succeeds
on_failure_callback Invoked when the task fails
sla_miss_callback Invoked when a task misses its defined SLA
on_retry_callback Invoked when the task is up for retry

 

Callback 사용 샘플 코드

from datetime import datetime, timedelta
from airflow import DAG
from airflow.operators.dummy import DummyOperator


def task_failure_alert(context):
    print(f"Task has failed, task_instance_key_str: {context['task_instance_key_str']}")


def dag_success_alert(context):
    print(f"DAG has succeeded, run_id: {context['run_id']}")


with DAG(
    dag_id="example_callback",
    schedule_interval=None,
    start_date=datetime(2021, 1, 1),
    dagrun_timeout=timedelta(minutes=60),
    catchup=False,
    on_success_callback=None,
    on_failure_callback=task_failure_alert,
    tags=["example"],
) as dag:

    task1 = DummyOperator(task_id="task1")
    task2 = DummyOperator(task_id="task2")
    task3 = DummyOperator(task_id="task3", on_success_callback=dag_success_alert)
    task1 >> task2 >> task3

 

 

Task 들을 그룹으로 묶어서

눈으로 보기에 좀 더 정렬되어 보이고 이해하기 쉽도록 만들 수 있음

 

 

Task Group 사용 샘플 코드

from datetime import datetime

from airflow.models.dag import DAG
from airflow.operators.bash import BashOperator
from airflow.operators.dummy import DummyOperator
from airflow.utils.task_group import TaskGroup

# [START howto_task_group]
with DAG(
    dag_id="example_task_group", start_date=datetime(2021, 1, 1), catchup=False, tags=["example"]
) as dag:
    start = DummyOperator(task_id="start")

    # [START howto_task_group_section_1]
    with TaskGroup("section_1", tooltip="Tasks for section_1") as section_1:
        task_1 = DummyOperator(task_id="task_1")
        task_2 = BashOperator(task_id="task_2", bash_command='echo 1')
        task_3 = DummyOperator(task_id="task_3")

        task_1 >> [task_2, task_3]
    # [END howto_task_group_section_1]

    # [START howto_task_group_section_2]
    with TaskGroup("section_2", tooltip="Tasks for section_2") as section_2:
        task_1 = DummyOperator(task_id="task_1")

        # [START howto_task_group_inner_section_2]
        with TaskGroup("inner_section_2", tooltip="Tasks for inner_section2") as inner_section_2:
            task_2 = BashOperator(task_id="task_2", bash_command='echo 1')
            task_3 = DummyOperator(task_id="task_3")
            task_4 = DummyOperator(task_id="task_4")

            [task_2, task_3] >> task_4
        # [END howto_task_group_inner_section_2]

    # [END howto_task_group_section_2]

    end = DummyOperator(task_id='end')

    start >> section_1 >> section_2 >> end

 

Airflow WebUI Graph 에서 보이는 화면

차례대로 Group 을 클릭하지 않았을 때, Group 을 클릭했을 때

 

+ Recent posts