본문 바로가기

Tech/Airflow

Airflow 멀티테넌시 환경 구축하기

나 같은 쭈니어 데이터엔지니어가 Airflow 멀티테넌시 환경을 구축하게 될 때,

도움이 되고자 구축했던 경험을 글로 남겨 공유한다.!

 


 

회사에서 신규 프로젝트로 Airflow를 처음으로 사용하게 되었다.

공식 Airflow Helm Chart를 사용하여 AWS EKS 위에, Kubernetes Executor로 구축하였다.

구축한 Airflow에서 DAG 파일을 관리하기 위해, GitSync(GitLab)를 사용하게 되었는데,

이부분에서 여러명의 데이터 엔지니어가 DAG 파일을 생성, 작업 하기엔 conflict issue 등 여러 불편함이 생겼다.

이 불편함을 해소하기 위해서, 여러 회사들의 방법을 벤치 마킹 했고,
그 과정에서 발견한 대표적인 방법들은 아래와 같았다.

 

  - Line:
https://engineering.linecorp.com/ko/blog/multi-tenancy-airflow-2

  - NFT Bank:
https://blog.nftbank.ai/nftbank에서-airflow-데이터-파이프라인을-안전하게-빠르게-개발과-테스트를-할-수-있는-이유-653aa18b683e

 

두 회사의 공통점은, github에서 개발자가 테스트 브랜치를 생성할 때, 브랜치명을 기반으로 Airflow Base Url을 변경하여 새로운 Airflow Cluster를 자동 생성해주는 방법이였다. (아래 사진 예시)

[사진 출처] https://engineering.linecorp.com/ko/blog/multi-tenancy-airflow-2

 

이 컨셉을 기반으로 멀티테넌시 방식을 구축하였다.

이 글에서는 내가 구축했던 과정과 삽질 했던 부분에 대해서 자세히 설명한다.

 

[사전 준비 된 부분]

회사 내 EKS, ArgoCD, Jenkins 설치 및 연동 같은 경우는 이미 인프라가 구성되어있는 상태에서 진행했다.


 

먼저 설명에 들어가기 앞서, 벤치마킹 했던 회사들과 인프라 구성이 다른 부분이 하나 있었다.

다들 GitHub을 사용하였는데, 우리 회사의 경우는 GitLab을 사용한다.

(나중에 Jenkins ↔ Gitlab 간 webhook 설정에서 이슈가 발생)

 

내가 멀티테넌시를 구축한 구조는 간단히 아래와 같다.

위 구조를 바탕으로 구분지어 하나하나 설명하겠다.

 

1. GitLab Jenkins

첫번째로 설명할 부분은 GitLab → Jenkins 서버로 webhook 설정 및 이벤트 구분 했던 방법이다.

 

[이 부분에서 내가 하고 싶었던 것]

1. 개발자가 새로운 이슈를 생성하고 test/issue01 형태의 브랜치를 생성하게 되면, 이 브랜치가 새로 생성된 브랜치인지 체크

2. 개발자가 개발을 마무리하고 test/issue01 형태의 브랜치를 main 혹은 develop 브랜치로 머지 할 경우, 머지 이벤트의 state가 merged인지 체크

 

[어떻게 구성했나?]

먼저, Jenkins와 Gitlab 간의 webhook Event를 받기 위한 세팅이 필요했다.

 

Jenkins에서는 아래와 같이 세팅한다.

세팅 하기 위해 Jenkins에서 파이프라인(아이템)을 생성 해야한다.

파이프라인을 아래 2가지로 구성했는데, 이유는 다음과 같다.

  • Create 관련 PipeLine
  • Delete 관련 PipeLine

첫번째 삽질.

Gitlab에서 webhook event 생성 시, Push Event와 Merge Event 두개를 전부다 체크하여 생성 하면,

Push Event는 잘 동작하는데, Merge Event는 발생해도 Jenkins Gitlab Plugin에서 webhook을 수신하지 못하는 에러가 있었다.
(Jenkins에서 Gitlab webhook Event를 Catch하고 event payload를 사용하기 위해 Gitlab Plugin을 사용했다.)

관련 해결 스택오버플로우:
https://stackoverflow.com/questions/73112676/build-a-jenkins-job-on-a-merge-request-to-master

 

그래서, 위 문제를 해결하기 위해,

Push Event Webhook(이하. Create 관련 PipeLine),

Merge Event Webhook(이하. Delete 관련 PipeLine)

2개로 나누어서 파이프라인을 구성했다.

 

각 Jenkins 파이프라인(아이템)의 주요 설정은 아래와 같다.

- Create 관련 PipeLine 주요 설정 (Gitlab Trigger)

 

- Delete 관련 PipeLine 주요 설정 (Gitlab Trigger)

 

GitLab에서는 아래와 같이 webhook을 생성한다.

- Create 관련 PipeLine에 사용할 Gitlab webhook 설정 (Push Event)

 

- Delete 관련 PipeLine에 사용할 Gitlab webhook 설정 (Merge Event)

이렇게 위 처럼 설정을 하게되면,

Push Event 발생시, Merge Event 발생시 webhook을 받아 각 Jenkins PipeLine이 동작하게 될 것이다.

 

그럼 이제, 각 이벤트 webhook을 전달 받은 후, jenkinsfile에서 추가적으로 어떤 Stage(필터) 작업이 있는지 설명한다.

 

[Push Event 발생 시 추가 필터 작업]

Push Event를 webhook으로 설정한 이유는 다음과 같았다.

GitLab webhook 트리거 종류에는 branch create와 관련된 웹훅이 따로 존재하지 않다.

새로운 브랜치가 생성되게 되면 Push Event가 발생하는데, 이 때 Push Event의 Payload에 before 값이 0*40개로 세팅되어 이벤트가 발생되게 된다.

Push Event Payload 정보:
http://mmb.irbbarcelona.org/gitlab/help/web_hooks/web_hooks.md

 

따라서, Push Event Payload의 before값으로 새 브랜치 생성 여부를 체크한다.

before 값을 사용하여 새 브랜치 생성을 구분하는 깃랩 이슈:
https://gitlab.com/gitlab-org/gitlab-foss/-/issues/31723

💡 [참고]
Jenkins에서 GitLab Plugin을 사용하게되면 기 설정된 환경변수로 gitlab event의 값들을 가져올 수 있다.
jenkinsfile에서 이 환경변수들을 활용해 payload 값을 가져와 활용했다.

- webhook Event 값: env.gitlabActionType
- 이벤트가 발생한 gitlab branch 명: env.gitlabBranch
- 푸시 이벤트 시, before 값: env.gitlabBefore
- 머지 이벤트 시, 소스 브랜치 명: env.gitlabSourceBranch
- 머지 이벤트 시, 머지 상태: env.gitlabMergeRequestState

 

[Merge Event 발생 시 추가 필터 작업]

Merge Event가 발생하게 되면 여러개의 state가 존재한다.

그중, Merge Request가 완료되면 state가 merged로 payload에 세팅되어 훅이벤트가 발생하게 된다.

이 값(env.gitlabMergeRequestState)을 기준으로 개발자가 이슈 처리를 완료했다는 것을 판단한다.

 

2-1. Jenkins GitLab

첫번째 단계에서 새 브랜치 생성, 머지 이벤트를 구분하여 이벤트 수신을 할 수 있는 환경을 만들어줬다면,

두번째 단계부터는 Jenkins에서 ArgoCD를 통해 EKS에 App을 배포하기 위한 세팅을 하는 부분이다.

 

두번째 단계에서 사용되는 GitLab은 첫번째 단계에서의 Gitlab과 다른 Gitlab이다.

  - 첫번째 단계의 gitlab은 Airflow Dag 파일들을 관리하는 gitlab repo 이고,

  - 두번째 단계의 gitlab은 helm chart를 관리하는 gitlab repo이다.

helm chart는 공식 apache Airflow helm Chart v1.6를 사용했다.

 

 

[Push Event 발생 시]

test/issue01 형태의 브랜치가 새로 생성 될 때 마다,

개별적인 Airflow 환경을 제공해 주기 위해서, 접근 URL을 구분지어 새로운 Airflow Cluster를 띄우는 형태를 가져야 했다.

그러기 위해서는 helm chart의 values.yaml 파일을 새로 생성되는 브랜치마다 수정할 필요가 있었다.

 

- 공식 helm chart values.yaml 에서 추가, 수정 되어야 할 부분들

ingress:
	web:
		path: "/test/issue01"

...

env:
	...
  - name: "AIRFLOW__WEBSERVER__BASE_URL"
    value: "http://airflow.com/test/issue01"
  - name: "AIRFLOW__CLI__ENDPOINT_URL"
    value: "http://airflow.com/test/issue01"

...

web:
  baseUrl: "http://airflow.com/test/issue01"

config:
	...
  webserver:
    base_url: '{{ .Values.web.baseUrl }}'
		...

 

유동적이어야 하는 부분

  - ingress.web.path가 브랜치 명마다 유동적으로 변경되어야 함.

  - env의 AIRFLOW__WEBSERVER__BASE_URL, AIRFLOW__CLI__ENDPOINT_URL 값이 브랜치 명마다 유동적으로 변경되어야 함.

  - web.baseUrl의 endpoint가 브랜치명 마다 유동적으로 변경되어야함.

 

 

values.yaml 파일이 유동적으로 매번 바뀌어야 했기 때문에,

기존의 values.yaml 파일은 template 형태로 두고, values-test-*.yaml 파일을 새 브랜치 생성 이벤트가 발생할 때마다 만들어서 관리하였다.

 

- values.yaml template 형태

ingress:
	web:
		path: "/new_branch"

...

env:
	...
  - name: "AIRFLOW__WEBSERVER__BASE_URL"
    value: "http://airflow.com/new_branch"
  - name: "AIRFLOW__CLI__ENDPOINT_URL"
    value: "http://airflow.com/new_branch"

...

web:
  baseUrl: "http://airflow.com/new_branch"

config:
	...
  webserver:
    base_url: '{{ .Values.web.baseUrl }}'
		...

 

jenkinsfile에서는 새로운 브랜치 생성 push Event가 발생할 때마다 상세 동작방식은 아래와 같다.

  1. 새로 생성된 브랜치 명을 가져온다.

  2. 두번째 gitlab을 clone하고, values.yaml (template)을 values-브랜치명.yaml 으로 copy 한다.

  3. values-브랜치명.yaml에서 new_branch 라는 keyword를 {브랜치명}으로 변경(sed)한다.

  4. gitlab에 add, commit, push 한다.

 

두번째 삽질.

values-브랜치명.yaml의 new_branch라는 keyword를 {브랜치명}으로 변경하기 위해 sed 명령어를 사용했다.

그런데, jenkinsfile에서 명령어를 정의하고 사용할 시, single quote가 적용이 안되는 에러?가 있었다.

해결 방법 URL: https://github.com/hyperledger/bevel/blob/main/automation/Jenkinsfile

 

문제사항을 예를 들면 아래와 같았다.

# 원래 사용하려 했던 방식
sed -i -e 's/new_branch/{branch_name}/g' values-{브랜치 명}.yaml

# 위 커맨드를 jenkinsfile에서 정의하고 사용하게 되면 아래처럼 커맨드가 변경되어 실행이 안된다.
sed -i -e s/new_branch/{branch_name}/g values-{브랜치 명}.yaml

# 그래서, 해결한 방법
sed -i -e 's*new_branch*{branch_name}*g' values-{브랜치 명}.yaml

 

[MERGE Event 발생 시]

머지 이벤트가 발생하게 되면 이슈가 끝났다고 판단한다.

더 이상 , 따로 구성한 Airflow 환경은 필요가 없으므로,

머지 이벤트 payload에서 sourceBranch name을 가져오고 values-{브랜치명}.yaml 파일을 삭제한다.

 

merge event 발생 시, 상세 동작 내용을 정리하자면 아래와 같다.

  1. merge event payload에서 source 브랜치 명을 가져온다.

  2. 두번째 gitlab을 clone하고, values-브랜치명.yaml을 삭제한다.

  3. gitlab에 add, commit, push 한다.

 

2-2. Jenkins KeyCloak

Keycloak의 경우는, 회사에 구축된 ArgoCD가 회사 메일 계정과 SSO 로그인을 제공하기 위해 사용되고 있었다.

Jenkins에서 ArgoCD를 통해 Airflow를 자동 배포 하려고 했고, 그 과정에서 ArgoCD CLI를 사용하려 했다.

(ArgoCD CLI를 사용하기 위해서는 계정 로그인 세션이 필요했다.)

 

ArgoCD에서 KeyCloak을 통한 SSO 기능을 사용하는 경우에는,

ArgoCD 로그인 세션을 가지기 위해서 KeyCloak으로 부터 계정에 대한 ACCESS_TOKEN을 발급받아 사용해야 했다.

ArgoCD SSO 로그인 세션 관련 질문 URL:
https://github.com/argoproj/argo-cd/issues/4424#issuecomment-829468294

 

jenkinsfile에서 keycloak REST API를 활용해 ACCESS_TOKEN을 발급받았다.

keycloak api documentation URL:
https://www.keycloak.org/docs/latest/securing_apps/
keycloak version별 document archive URL:
https://www.keycloak.org/documentation-archive.html

 

 

- ACCESS_TOKEN 관련 API 예시 (curl 호출)

curl \
  -d "client_id=myclient" \
  -d "client_secret=40cc097b-2a57-4c17-b36a-8fdf3fc2d578" \
  -d "username=user" \
  -d "password=password" \
  -d "grant_type=password" \
  "http://localhost:8080/realms/master/protocol/openid-connect/token"

 

세번째 삽질

curl을 통해 ACCESS_TOKEN 발급 시, username argument로 회사 메일 계정을 사용했다.

ex.) -d “username=aaa@aaa.com”

그런데, 비밀번호가 맞음에도 계속 사용자 정보가 틀렸다는 에러가 발생했다.

해당 문제는 자세하게 이유를 파악하지 못해서,

keycloak 자체에서 계정을 새로 생성하여 해결했다.

ex.) -d ”username=aaa”

해결 관련 URL:
https://www.appsdeveloperblog.com/keycloak-rest-api-create-a-new-user/

 

3. Jenkins ArgoCD

이제 PUSH, MERGE 이벤트 발생 시,

배포할 helm chart의 values 파일도 생성했고, ArgoCD에 접근할 ACCESS_TOKEN도 발급받았으니,

ArgoCD를 통해 EKS에 App 배포할 일만 남았다.

APP 배포의 경우 ArgoCD CLI를 통해 배포를 진행했으며, 상세 동작 내용은 아래와 같다.

[참고]
- argocd helm 사용법: https://velog.io/@ockura/Jenkins-Argocd-Pipline-설정5부 
- argocd set option : https://argo-cd.readthedocs.io/en/stable/user-guide/commands/argocd_app_set/
- argocd delete option: https://argo-cd.readthedocs.io/en/stable/user-guide/commands/argocd_app_delete/
- argocd cli jenkinsfile 예시: https://argo-cd.readthedocs.io/en/stable/user-guide/helm/



[Push Event 발생 시]

1. App을 생성한다.

  - app create 예시

argocd app create {app 이름} \
	 --repo {두번째 GitLab Repo URL} \
	 --path {두번째 GitLab Repo에서 values-{브랜치명}.yaml 파일이 위치한 경로} \
	 --project {app project 구분} \
	 --dest-server {https://kubernetes.default.svc} \
	 --dest-namespace {app이 배포될 EKS의 namespace} \
	 --server {ArgoCD URL} \
	 --grpc-web \
	 --auth-token {KeyCloak에서 얻은 ACCESS_TOKEN}

 

 

2. App에 새로 만들었던 values-{브랜치명}.yaml 파일을 set(오버라이드) 해준다.

  - app set 예시

argocd app set {app 이름} \
		--values values-{브랜치명}.yaml \
		--server {ArgoCD URL} \
		--grpc-web \
		--auth-token {KeyCloak에서 얻은 ACCESS_TOKEN}

 

 

3. App을 Sync(실행)한다.

  - app sync 예시

argocd app sync {app 이름} \
		--server {ArgoCD URL} \
		--grpc-web \
		--auth-token {KeyCloak에서 얻은 ACCESS_TOKEN}

 

[Merge Event 발생 시]

 

1. App을 삭제한다.

  - app delete 예시

argocd app delete {app 이름} \
		--server {ArgoCD URL} \
		--grpc-web \
		--auth-token {KeyCloak에서 얻은 ACCESS_TOKEN}

 

네번째 삽질

argocd CLI 예시들을 보면 —auth-token 옵션이 항상 맨뒤에 가있는 것을 볼 수 있을 것이다.

처음에 —auth-token이라는 옵션을 맨처음 옵션으로 주고 명령어를 사용했었는데,

그렇게 할 경우, access_token이 길어서인지? 뒤에 옵션들이 무시되버리는 문제가 있었다.

그래서, 맨뒤로 빼주니 정상적으로 명령이 동작했다.

 

 

이렇게 내가 구축했던 내용들은 끝이다.

마지막으로, 위에 상세하게 썼던 내용들을 간단한 플로우로 정리하자면 아래와 같다.

 

반응형