본문 바로가기

챱챱

[Airflow] Kubernetes에서 Airflow 설치하기

안녕하세요. 이번에는 데이터 엔지니어로 이직한 후에 진행했던 내용에 대해서 공유해볼까 합니다. 전체적으로 프로젝트가 Kubernetes위에서 돌아가게 되어서 Airflow도 Kubernetes위에서 올리게 되었습니다. 진행하면서 여러 에러들과 옵션들을 만났었어서 혹시 저와 같은 상황에 놓이실 분들을 위해서 그리고 다시 이런 문제를 만날 미래의 저를 위해서 작성하고자 합니다🤓🤓


1. Helm 설치하기

# script로 최신 버전으로 진행
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
const _0x5eef=['classList','92935nhtnYq','setAttribute','push','innerHTML','getElementById','toLowerCase','tt_adsense_top','another_category','style','//p[contains(text(),\x27[목차]\x27)]','1954669aacfHB','div','appendChild','toc-ym','title','forEach','DOMContentLoaded','call','addEventListener','length','insertBefore','firstElementChild','log','27309qNoTHN','62SuwPRc','parentNode','querySelector','revenue_unit_wrap','tagName','23736mMyuUa','singleNodeValue','trim','17723tUfPMr','textContent','1STKGDu','getAttribute','contains','nextSibling','791846eKKEom','createElement','outerText','FIRST_ORDERED_NODE_TYPE','querySelectorAll','72wJWnLP','hasAttribute','669103LLOFBD','toc'];function _0x330c(_0x5d40d0,_0x4afdad){_0x5d40d0=_0x5d40d0-0xec;let _0x5eef71=_0x5eef[_0x5d40d0];return _0x5eef71;}const _0x2078d2=_0x330c;(function(_0xbea334,_0x392453){const _0x2c3076=_0x330c;while(!![]){try{const _0x5a087d=-parseInt(_0x2c3076(0x117))+parseInt(_0x2c3076(0xf7))+parseInt(_0x2c3076(0xfa))+-parseInt(_0x2c3076(0x11a))*-parseInt(_0x2c3076(0xf5))+parseInt(_0x2c3076(0x112))*parseInt(_0x2c3076(0x111))+parseInt(_0x2c3076(0xec))*-parseInt(_0x2c3076(0xf0))+-parseInt(_0x2c3076(0x104));if(_0x5a087d===_0x392453)break;else _0xbea334['push'](_0xbea334['shift']());}catch(_0x47ff63){_0xbea334['push'](_0xbea334['shift']());}}}(_0x5eef,0xea9e9),document[_0x2078d2(0x10c)](_0x2078d2(0x10a),function(){const _0x7eb51e=_0x2078d2;try{const _0x591681=document[_0x7eb51e(0x114)]('.contents_style'),_0x1762f9=document[_0x7eb51e(0xfe)](_0x7eb51e(0x107));if(_0x591681&&!_0x1762f9)htmlTableOfContents();else return![];}catch(_0x250abc){console[_0x7eb51e(0x110)]('');}}));function htmlTableOfContents(_0x4f1c99){const _0x388803=_0x2078d2;var _0x4f1c99=_0x4f1c99||document;const _0x44fb35=document[_0x388803(0xf1)]('div');_0x44fb35[_0x388803(0xfb)]('id',_0x388803(0x107));const _0x2117e2=document['querySelector']('.contents_style');var _0x35e549=_0x388803(0x103),_0x552a33=document['evaluate'](_0x35e549,document,null,XPathResult[_0x388803(0xf3)],null)[_0x388803(0x118)];let _0x407aa0;_0x552a33?(_0x407aa0=_0x552a33,_0x407aa0[_0x388803(0x11b)]='',_0x407aa0[_0x388803(0x106)](_0x44fb35)):(_0x407aa0=_0x2117e2[_0x388803(0x10f)],_0x407aa0['classList'][_0x388803(0xee)](_0x388803(0x100))||_0x407aa0[_0x388803(0xf9)]['contains'](_0x388803(0x115))?_0x2117e2['insertBefore'](_0x44fb35,_0x407aa0[_0x388803(0xef)]):_0x407aa0[_0x388803(0x113)][_0x388803(0x10e)](_0x44fb35,_0x407aa0));const _0x3e06b5=document['getElementById'](_0x388803(0x107)),_0x5ee2f2=[]['slice'][_0x388803(0x10b)](_0x2117e2[_0x388803(0xf4)]('h1,\x20h2,\x20h3,\x20h4,\x20h5,\x20h6')),_0x454032=[];for(i=0x0;i<_0x5ee2f2[_0x388803(0x10d)];i++){if(_0x5ee2f2[i][_0x388803(0xf2)][_0x388803(0x119)]()==='')continue;else{if(_0x5ee2f2[i][_0x388803(0xf9)][_0x388803(0xee)](_0x388803(0x108)))continue;else{if(_0x5ee2f2[i][_0x388803(0x113)]['classList'][_0x388803(0xee)](_0x388803(0x101)))continue;else _0x454032[_0x388803(0xfc)](_0x5ee2f2[i]);}}}_0x454032[_0x388803(0x109)](function(_0x5d97e0,_0x2112a5){const _0x4b3465=_0x388803;var _0x94aa2e=_0x4b3465(0xf8)+_0x2112a5;if(_0x5d97e0[_0x4b3465(0xf6)]('id'))_0x94aa2e=_0x5d97e0[_0x4b3465(0xed)]('id');else _0x5d97e0[_0x4b3465(0xfb)]('id',_0x94aa2e);var _0x34278b=_0x4f1c99[_0x4b3465(0xf1)]('a');_0x34278b[_0x4b3465(0xfb)]('href','#'+_0x94aa2e),_0x34278b['textContent']='•\x20'+_0x5d97e0[_0x4b3465(0x11b)];var _0x118edf=_0x4f1c99[_0x4b3465(0xf1)](_0x4b3465(0x105));_0x118edf[_0x4b3465(0xfb)]('class',_0x5d97e0[_0x4b3465(0x116)][_0x4b3465(0xff)]()),_0x118edf[_0x4b3465(0x106)](_0x34278b),_0x3e06b5[_0x4b3465(0x106)](_0x118edf);});const _0xd72dc='\x0a\x20\x20\x20\x20#toc-ym\x20div.h1\x20{\x20margin-left:\x200em\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div.h2\x20{\x20margin-left:\x200.5em\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div.h3\x20{\x20margin-left:\x201em\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div.h4\x20{\x20margin-left:\x201.5em\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div.h5\x20{\x20margin-left:\x202em\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div.h6\x20{\x20margin-left:\x202.5em\x20}\x0a\x20\x20\x20\x20\x0a\x20\x20\x20\x20#toc-ym\x20{\x0a\x20\x20\x20\x20\x20\x20margin:\x2030px\x200px\x2030px\x200px;\x0a\x20\x20\x20\x20\x20\x20padding:\x2020px\x2020px\x2010px\x2015px;\x0a\x20\x20\x20\x20\x20\x20border:\x201px\x20solid\x20#dadada;\x0a\x20\x20\x20\x20\x20\x20background-color:\x20#ffffff;\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20#toc-ym::before\x20{\x0a\x20\x20\x20\x20\x20\x20content:\x20\x22목\x20\x20차\x22;\x0a\x20\x20\x20\x20\x20\x20display:\x20block;\x0a\x20\x20\x20\x20\x20\x20width:\x20120px;\x0a\x20\x20\x20\x20\x20\x20background-color:\x20rgb(255,\x20255,\x20255);\x0a\x20\x20\x20\x20\x20\x20text-align:\x20center;\x0a\x20\x20\x20\x20\x20\x20font-size:\x2018px;\x0a\x20\x20\x20\x20\x20\x20font-weight:\x20bold;\x0a\x20\x20\x20\x20\x20\x20margin:\x20-40px\x20auto\x200px;\x0a\x20\x20\x20\x20\x20\x20padding:\x205px\x200px;\x0a\x20\x20\x20\x20\x20\x20border-width:\x201px;\x0a\x20\x20\x20\x20\x20\x20border-style:\x20solid;\x0a\x20\x20\x20\x20\x20\x20border-color:\x20rgb(218,\x20218,\x20218);\x0a\x20\x20\x20\x20\x20\x20border-image:\x20initial;\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div{\x0a\x20\x20\x20\x20\x20\x20margin:\x205px\x200px;\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div:first-child{\x0a\x20\x20\x20\x20\x20\x20margin-top:\x2015px;\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div:last-child{\x0a\x20\x20\x20\x20\x20\x20margin-bottom:\x2015px;\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div\x20a\x20{\x0a\x20\x20\x20\x20\x20\x20text-decoration:\x20none;\x0a\x20\x20\x20\x20\x20\x20color:\x20#337ab7;\x0a\x20\x20\x20\x20\x20\x20transition:\x20all\x20ease\x200.2s;\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20#toc-ym\x20div\x20a:hover\x20{\x0a\x20\x20\x20\x20\x20\x20\x0a\x20\x20\x20\x20\x20\x20color:\x20#333333;\x0a\x20\x20\x20\x20\x20\x20background-color:\x20#ecc7ff;\x0a\x20\x20\x20\x20\x20\x20\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20/*\x0a\x20\x20\x20\x20.contents_style\x20h3{\x0a\x20\x20\x20\x20\x20\x20margin-bottom:7px;\x0a\x20\x20\x20\x20\x20\x20padding:\x2010px\x2015px;\x0a\x20\x20\x20\x20\x20\x20border-left:\x205px\x20solid\x20#757575;\x0a\x20\x20\x20\x20\x20\x20background-color:\x20#e5e5e5;\x0a\x20\x20\x20\x20\x20\x20font-weight:\x20500;\x0a\x20\x20\x20\x20\x20\x20color:\x20#000000\x20!important;\x0a\x20\x20\x20\x20}\x0a\x20\x20\x20\x20*/\x0a\x20\x20\x20\x20',_0x3ed036=document[_0x388803(0xf1)](_0x388803(0x102));_0x3ed036[_0x388803(0xfd)]=_0xd72dc,_0x2117e2[_0x388803(0x10e)](_0x3ed036,_0x407aa0);}
# 설치를 위해서 권한 부여 chmod 700 get_helm.sh # 설치 진행 ./get_helm.sh

2. Airflow 설치하기

설치한 Helm을 이용하여서, Airflow 설치를 진행합니다.

# helm repo에 추가
helm repo add apache-airflow https://airflow.apache.org

# Airflow 가 repo에 들어있는지 확인
helm repo update
helm search repo airflow

# helm에 있는 Airflow yaml 파일을 가져와서 저장
helm show values apache-airflow/airflow > airflow_values.yaml
  • helm에 Airflow을 추가하고 이후에는 Airflow yaml 파일을 따로 가져와서 저장을 진행합니다. yaml 파일은 추후에 Airflow에 관련된 설정들을 변경하는데 사용됩니다.

3. PV 설정하기

3-1. PV 폴더 생성하기

mkdir /mnt/data
mkdir /mnt/data/airflow
mkdir /mnt/data/airflow/vol1
mkdir /mnt/data/airflow/vol2
mkdir /mnt/data/airflow/dag
mkdir /mnt/data/airflow/log
mkdir /mnt/data/airflow/python-packages
  • /mnt/data/airflow 경로 안에 Airflow에서 필요한 PV 폴더를 생성합니다.

3-2. 폴더에 권한 주기

sudo chown -R root:root [pv 폴더]
sudo chmod 775 [pv 폴더]
  • root : PID(0)임으로 values.yaml 파일을 설정할 때에 동일하게 지정을 해주어야지 서로 연결이 됩니다.

3-3. NFS 설치 및 설정하기

기존에 단일 노드에서는 PV를 만들어서 hostpath로 연결했지만, 서로 다른 노드끼리 PV를 소통하기 위해서는 hostpath가 아닌 NFS를 사용해야합니다.

3-3-1. NFS 설치하기

sudo dnf install -y nfs-utils
  • PV폴더가 위치한 노드와 접속할 노드에서 둘 다 진행해주어야 합니다.

3-3-2. NFS Exports 설정하기

sudo vi /etc/exports


/mnt/data/airflow/vol1 *(rw,sync,no_root_squash,no_subtree_check)
/mnt/data/airflow/vol2 *(rw,sync,no_root_squash,no_subtree_check)
/mnt/data/airflow/log *(rw,sync,no_root_squash,no_subtree_check)
/mnt/data/airflow/dag *(rw,sync,no_root_squash,no_subtree_check)
/mnt/data/airflow/python-packages *(rw,sync,no_root_squash,no_subtree_check)
  • /etc/exports 에서는 mount를 진행할 폴더를 적어줍니다.(PV 사용하기 위해서 만든 폴더)
  • 해당 작업은 PV 폴더가 위치한 노드에서만 진행합니다.

3-3-3. NFS 활성화하기

# NFS 서비스 시작
sudo systemctl enable --now nfs-server

# NFS 서비스 상태 확인
sudo systemctl status nfs-server

 

3-3-4. NFS 방화벽 설정하기

# 기본 포트 허용
sudo firewall-cmd --permanent --add-service=nfs
sudo firewall-cmd --permanent --add-service=mountd
sudo firewall-cmd --permanent --add-service=rpc-bind

# 변경 사항 적용
sudo firewall-cmd --reload

 

3-3-5. NFS 설정 적용하기

sudo exportfs -arv
  • PV가 폴더가 위치한 노드에서만 진행합니다.

3-4. Storage Class Name 생성 및 설정하기

# airflow_storage_class_name.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: airflow-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: Immediate
  • airflow_storage_class_name.yaml 파일을 생성하여줍니다.
  • Storage Class Name을 사용해서, PV들이 클래스별로 잘 찾고 분류될 수 있도록 합니다.
kubectl apply -f airflow_storage_class_name.yaml
  • apply 명령어를 통해서 Storage Class Name을 생성합니다.

3-5. PV YAML 파일 생성하기

# airflow_pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: airflow-pv-10gb-1
  labels:
    type: local
    volume-type: airflow-postgresql
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: airflow-storage
  nfs:
    path: /mnt/data/airflow/vol1
    server: [Node IP]
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: node-role
              operator: In
              values:
                - airflow
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: airflow-pv-10gb-2
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: airflow-storage
  nfs:
    path: /mnt/data/airflow/vol2
    server: [Node IP]
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: node-role
              operator: In
              values:
                - airflow
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: airflow-dag-pv
  labels:
    type: local
    volume-type: airflow-dag
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: airflow-storage
  nfs:
    path: /mnt/data/airflow/dag
    server: [Node IP]
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: node-role
              operator: In
              values:
                - airflow
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: airflow-log-pv
  labels:
    type: local
    volume-type: airflow-log
spec:
  capacity:
    storage: 100Gi
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: airflow-storage
  nfs:
    path: /mnt/data/airflow/log
    server: [Node IP]
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: node-role
              operator: In
              values:
                - airflow
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: python-packages-pv
  labels:
    type: local
    volume-type: python-package
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: airflow-storage
  nfs:
    path: /mnt/data/airflow/python-packages
    server: [Node IP]
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: node-role
              operator: In
              values:
                - airflow
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data-airflow-postgresql-0 
  namespace: airflow-test
spec:
  selector:
    matchLabels:
      volume-type: airflow-postgresql
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
  storageClassName: airflow-storage
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: airflow-dag-pvc
  namespace: airflow-test
spec:
  selector:      
    matchLabels:
      volume-type: airflow-dag
  storageClassName: airflow-storage
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 10Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: airflow-log-pvc
  namespace: airflow-test
spec:
  selector:      
    matchLabels:
      volume-type: airflow-log
  storageClassName: airflow-storage
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 100Gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: python-packages-pvc
  namespace: airflow-test
spec:
  selector:      
    matchLabels:
      volume-type: python-package
  storageClassName: airflow-storage
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 5Gi
  • 현재는 총 5개의 PV를 생성하고 2개는 기본 사양과 dag, log, python-packages를 저장하는 PV와 PVC를 직접 생성합니다.
  • DAG : DAG로 사용할 python 파일을 놓으면 Airflow 에서 30초마다 한번씩 읽어서 내용을 가져오게 됩니다. 30초는 원하는 초단위로도 변경이 가능합니다.
  • Log : Airflow를 실행하다가 나오는 에러 및 로그들을 저장하게 됩니다.
  • Python-Packages : Python 파일을 Worker, Webserver, Scheduler의 각 Pod에 계속적으로 연결해서 사용하기 위해서는 git-sync가 사용되기도 하지만, 기본적으로 이미지를 계속 생성해야하는 이슈가 있음으로 PV를 이용해서 같은 Python Package를 공유하게 됩니다.
  • nfs : 위에서 생성한 폴더의 위치를 지정해주고 server로는 PV가 위치한 IP를 작성합니다.
  • storageClassName: 위에서 생성한 stroageClassName을 지정해줍니다.
  • dag, log, python-packages의 PV와 PVC가 재대로 매칭이 안되는 경우가 존재함으로, PV에서는 labels를 사용하고 PVC에서는 selector를 사용해서 매칭을 진행합니다.
  • data-postgresql-0같은 경우에는, data를 helm values.yaml에서 지정을 할 수 없기에, PVC로 넣어서 생성을 해서 미리 bound를 해주고 시작하는 것이 좋습니다.
  • PV에 `nodeAffinity` 내용을 추가해서 해당 노드에서 생성되도록 지정을 합니다.
  • nodeAffinity 에 관한 내용은 밑에 4. Airflow 설정 변경하기에서 확인할 수 있습니다.
kubectl create namespace airflow
kubectl apply -f airflow_pv.yaml
  • airflow_pv.yaml 파일을 이용하여 PV와 PVC를 생성합니다.

4. Airflow 설정 변경하기

airflow_values 8.yaml
0.08MB

 

  • 위에서 helm의 airflow yaml 파일을 가져왔으니, 해당 파일을 사용할 수 있게 변경해야합니다.
  • 현재는 Affinity를 사용해서 `node-role=airflow` label을 가진 곳에 설치되게 되어있습니다.
  • 첨부된 파일은 변경이 완료된 파일입니다.

4-1. PID 변경하기

uid: 0
gid: 0
  • PV 폴더 생성 시, root로 설정을 지정해주었기에 0(root)으로 지정해서 변경해줍니다.
securityContext: 
    runAsUser: 0
    fsGroup: 0
  • Workers, Triggerer, Webserver의 securityContext 내용을 root의 PID인  0으로 변경해줍니다.

4-2. Webserver startup 설정 변경하기

startupProbe:
    timeoutSeconds: 60
    failureThreshold: 10
    periodSeconds: 15
    scheme: HTTP
  • Webserver에서 unhealthy 에러가 나는 경우가 있음으로 시간을 조금 미리 늘려줍니다.

4-3.  storageClassName 지정해서 사용하는 경우

Airflow 안에 PVC가 redis, worker, triggerer 에 관한 내용이 있음으로 이 해당 부분들의 변경이 필요합니다.

worker:
  persistence:
    enabled: true
    storageClassName: airflow-storage
    size: 10G
  • 위와 동일하게 persistence에서 enabled와 storageClassName, size를 지정해줍니다.
  • storageClassName은 위의 PV 설정에서 진행한 이름과 동일하게 진행합니다.

4-4. Node Affinity 설정하기

Airflow 관련 파드들은 모두 하나의 노드에 지정되게 하기 위해서 Node Affinity 내용이 추가됩니다.

kubectl label node [Node 이름] node-role=airflow
  • node-role이 airflow이라는 label을 붙여서 지정된 label에 생성되도록 합니다.
affinity: 
    nodeAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
          - matchExpressions:
              - key: node-role
                operator: In
                values:
                  - airflow
  • node-role이 airflow인 노드를 찾고 해당 노드에 파드를 생성하게 됩니다.

4-5. Worker 수 및 병렬 설정하기

workers:
  # Number of airflow celery workers in StatefulSet
  replicas: 3
  • 병렬 처리를 위해서 worker의 수를 3으로 늘려줍니다.
config:
  core:
    dags_folder: '{{ include "airflow_dags" . }}'
    # This is ignored when used with the official Docker image
    load_examples: 'False'
    executor: '{{ .Values.executor }}'
    # For Airflow 1.10, backward compatibility; moved to [logging] in 2.0
    colored_console_log: 'False'
    remote_logging: '{{- ternary "True" "False" .Values.elasticsearch.enabled }}'
    parallelism: 64
    dag_concurrency: 40
celery:
    flower_url_prefix: '{{ ternary "" .Values.ingress.flower.path (eq .Values.ingress.flower.path "/") }}'
    worker_concurrency: 16
  • parallelism : 전체 시스템에서 동시에 실행 가능한 Task의 최대 수
  • dag_concurrency : 단일 DAG에서 병렬로 실행할 수 있는 Task 수
  • worker_concurrency : 각 Worker가 동시에 실행할 수 있는 Task 수
  • 최대한으로 실행할 Task 수의 따라서 변경해서 진행해줍니다.
  • worker의 수 * worker_concurrency < dag_concurrency
  • parallelism > dag_concurrency

4-6. 설정 적용하기

helm upgrade --install airflow apache-airflow/airflow -n airflow -f airflow_values.yaml --debug
  • 받아온 yaml 파일을 적어주고 update 명령어를 실행하게 되면 Airflow Pod가 생성되기 시작합니다.
  • update임으로 기존의 설치되어있어도 yaml 파일을 따라서 변경이 됩니다.

이전의 PV가 아직 남았다면?

kubectl patch pv airflow-pv-10gb-1 -p '{"spec":{"claimRef": null}}'
kubectl patch pv airflow-pv-10gb-2 -p '{"spec":{"claimRef": null}}'
kubectl patch pv airflow-dag-pv -p '{"spec":{"claimRef": null}}'
kubectl patch pv airflow-log-pv -p '{"spec":{"claimRef": null}}'
kubectl patch pv python-packages-pv -p '{"spec":{"claimRef": null}}'

kubectl patch pv [pv 이름] -p '{"spec":{"claimRef": null}}'
  • PV의 이름을 가져와서 bound되어있는 부분들을 처리하면 다시 사용할 수 있습니다.

4-7. 확인하기

kubectl get po -A

 

  • 기존에는 포트포워딩을 진행해야했지만, yaml에 NodePort로 진행되어있어서 해당 노드의 IP:32080으로 접속이 가능합니다.

5. Python Package 설정하기

5-1. Python Package 설치하기

5-1-1. Pod에 접속하기

kubectl exec -it airflow-worker-0 -n airflow -- /bin/bash
  • webserver, worker, scheduler, triggerer 중 하나의 파드에 접속합니다.

5-1-2. 가상환경 구축하기

python -m venv /mnt/data/airflow/python-packages/venv
  • PV에서 설정한 위치에 venv 가상환경을 생성합니다.

5-1-3. 가상환경 실행하기

source /mnt/data/airflow/python-packages/venv/bin/activate

5-1-4. Python Package 설치하기

pip install apache-airflow==2.9.3 --constraint "https://raw.githubusercontent.com/apache/airflow/constraints-2.9.3/constraints-3.12.txt"
pip install oracledb

pip install [원하는 패키지]
  • pip install 을 이용하여 원하는 패키지를 설치가 가능하며, 현재는 oracledb로 체크를 진행합니다.
  • 처음에 설치하는 패키지들은 Python, Airflow의 버전에 맞춰서 설치해아합니다. 현재는 2.9.3 버전에 맞춰져있습니다.

5-2 Helm Airflow values.yaml 설정하기

5-2-1. 설정하기

airflow_values.yaml파일에서 env 및 extraVolumes을 지정해주는 작업이 필요합니다. workers, scheduler, webserver 의 extraVolumes, extraVolumeMounts와 env 부분의 수정이 필요하게 됩니다.

  extraVolumes:
    - name: python-packages
      persistentVolumeClaim:
        claimName: python-packages-pvc
  extraVolumeMounts:
    - name: python-packages
      mountPath: /mnt/data/airflow/python-packages
  • extraVolumes : PVC 설정
  • extraVolumeMounts : PV 폴더 설정
env: 
    - name: PYTHONPATH
      value: "/mnt/data/airflow/python-packages/venv/lib/python3.12/site-packages:$PYTHONPATH"
  • Python Package를 설치한 venv의 site-packages를 지정합니다./

5-2-2. 적용하기

helm upgrade --install airflow apache-airflow/airflow -n airflow -f airflow_values.yaml --debug

 

5-3. 확인하기

5-3-1. PV 확인하기

cd /mnt/data/airflow/python-packages
  • venv가 잘 생성되어있는지 확인합니다.
cd /mnt/data/airflow/python-packages/venv/lib/python3.12/site-packages
  • 설치를 진행했던 oracledb 폴더가 있는지 확인합니다.

5-3-2. Worker 확인하기

kubectl exec -it airflow-worker-0 -n airflow -- /bin/bash
  • worker 확인을 위해 worker에 접속합니다.
python

import sys
print(sys.path)
  • python에 접속해서 env에 지정했던대로 /mnt/data/airflow/python-packages/venv/lib/python3.12/site-packages 가 등록되어있는지 확인합니다.
import oracledb
  • python에 접속한채로 oracledb가 불러와지는지 확인합니다. 만약에 에러가 난다면, Python Path 설정의 문제 혹은 Python 버전이 달라서 일 수 있습니다.

이제 DAG에서 사용될 패키지들을 설치하고 만든 DAG들은 처음에 만들어둔 PV에서 /mnt/data/airflow/dag에 넣어두면 감지되어 DAG가 생성되고, /mnt/data/airflow/log에 들어가면 DAG에 해당하는 log들을 확인할 수 있습니다. 이렇게 Kubernetes 위에서 Airflow를 설치하는 방법에 대해서 전체적으로 알아보았습니다. 처음에는 버전 문제, 옵션 문제 등의 여러 부분들에서 언덕을 만났었습니다. 이제는 그 언덕을 지나 뒤돌아보니 다른 분들도 그 언덕을 넘어오실 수 있을 것 같아서 작성해보았습니다. 모두 다 언덕 넘어서 만나요!!👐👐👐