grep

Engineering

개인화된 Airflow 테스트 환경 구축 및 운영 경험

카카오

2026년 8월 7일

원문에서 보기 ↗

안녕하세요! 데이터서비스 조직의 비오, 소나, 테라입니다.

데이터서비스 조직은 다양한 기술로 데이터 생태계를 만들어 가며 모든 크루가 데이터로 문제를 해결할 수 있도록 돕고 있습니다. 그 과정에서 수많은 데이터 파이프라인을 다루고 있으며 이를 위해 수천 개의 Airflow DAG를 여러 하둡별 에어플로우 클러스터 위에서 운영하고 있습니다.

이렇게 여러 파이프라인을 다루다 보니 새로운 파이프라인을 개발하거나 기존 파이프라인을 개선하는 일이 잦습니다. 하지만 파이프라인을 테스트하는 과정에는 불필요하고 비효율적인 작업이 반복되고 있었고, 더 쉽고 빠르게 테스트할 수 있는 환경이 있으면 좋겠다는 사용자 요청도 있었습니다.

이런 고민에서 출발해 만든 것이 바로 AirZone입니다. 테스트 환경을 준비하는 부담을 0에 가깝게 만들자는 뜻을 담아, Airflow Zero Zone에서 이름을 따 AirZone이라 지었습니다.

이 글에서는 기존에 파이프라인을 테스트할 때 어떤 불필요한 작업들이 반복됐는지 살펴보고, 이를 해결하기 위해 AirZone을 어떻게 설계하고 구축했는지 또 운영하며 마주한 문제들은 어떻게 풀어왔는지 소개하겠습니다.

1. 기존 테스트 방식의 한계

먼저 팀 Airflow가 어떻게 구성되어 있는지 살펴보면 다음과 같습니다.

[1] airflow-api : 팀 파이프라인은 프로젝트별 저장소 수십 개로 나뉘어 있지만 Airflow가 읽는 DAG는 공통 저장소 하나입니다. 사용자가 프로젝트 저장소에 push하면 GitHub webhook을 받아, 공통 저장소의 해당 서브 모듈 포인터를 최신 커밋으로 갱신합니다.

[2] git-sync : Scheduler Pod 안에 sidecar 컨테이너로 함께 뜹니다. 공통 저장소를 주기적으로 pull해 Pod 안 볼륨에 반영하고, Scheduler는 그 볼륨을 마운트해 DAG를 인식합니다. Worker Pod는 태스크 실행 시점에 별도로 sync를 수행해 실행 시점의 최신 DAG를 가져옵니다.

[3] Hadoop Task Pod : Worker Pod가 직접 태스크를 실행하지 않고, AAKubernetesPodOperator가 별도 Pod를 띄워 그 안에서 실행합니다. 이 Pod는 팀에서 자체 빌드한 하둡 이미지를 사용하며, init container로 kinit을 수행해 Kerberos 인증을 마친 뒤 Spark·Hive 태스크를 실행합니다.

이 인프라 위에서 사용자분들이 DAG를 어떻게 테스트해왔는지, 그 과정에서 어떤 한계가 있었는지 살펴보겠습니다. 기존에 DAG를 테스트하는 방법은 대표적으로 아래 세 가지였습니다.

로컬 Airflow

내 PC에 테스트 환경을 직접 구축하는 방식입니다. Airflow만 띄우면 되는 게 아니라 하둡 인증과 연결 설정, 도커 환경까지 로컬에 맞춰야 합니다. 한 번 만들어두면 편하지만 초기 구축 비용이 크고 실제 환경과 조금이라도 다르면 운영 환경에서는 실패할 수 있다는 단점이 있습니다.

개발용 Airflow

개발용 하둡과 연동해 운영에 반영하기 전 DAG가 정상 동작하는지 확인하는 환경입니다. git에 올린 코드를 동기화해 실행하기 때문에 여기서 테스트하려면 코드를 고칠 때마다 커밋·푸시하고 반영을 기다려야 했습니다. 이때 submodule update부터 DAG file processing까지 걸리는 시간이 길어 DAG를 개발하는 단계에서는 이 과정을 매번 기다려야 하는 게 부담이었습니다.

테스트용 Airflow

테스트 Airflow에 연결된 SSH 컨테이너에 접속해 사용하는 방식입니다. 로컬에서 작성한 코드를 컨테이너 안으로 복사하면 Airflow가 읽어 DAG를 실행합니다. git을 거치지 않고 바로 테스트할 수 있지만 수정할 때마다 파일을 복사하는 과정을 거쳐야 했습니다. 또한 실제 데이터와 하둡에 접근해 테스트하려면 컨테이너가 prod 망 안에 있어야 했는데, 그래서 접속할 때마다 prod VPN을 연결해야 하는 불편함이 있었습니다.

세 가지 외에도, production Airflow에서 테스트용 DAG를 실행하기도 했습니다. 모델링같이 실제 하둡 데이터가 필요한 예외적인 경우도 있기 때문입니다. 하지만 스케줄러와 워커는 Airflow의 모든 DAG와 태스크가 공유하는 자원입니다. 무거운 테스트 DAG는 다른 DAG의 스케줄링을 밀리게 했고 워커 슬롯을 점유하면 다른 프로젝트의 태스크가 실행을 기다려야 했습니다. 심한 경우 테스트가 리소스를 과도하게 사용해 Node에 장애가 발생하면, 같은 Node에서 돌던 다른 태스크까지 함께 중단되기도 했습니다. 따라서 각자의 테스트가 다른 프로젝트에 영향을 주지 않으려면 서로 격리된 환경이 필요했습니다.

2. AirZone 요구사항

AirZone이 갖춰야 할 요구사항은 두 가지였습니다. 준비 없이 쉽게 테스트할 수 있는 환경, 그리고 다른 작업과 격리된 환경입니다. 이 두 가지를 갖춘 Beta 환경을 여는 것을 1차 목표로 삼았습니다.

쉽게 테스트할 수 있는 환경

사용자가 Kubernetes나 Helm을 몰라도 Airflow 환경을 만들고 지울 수 있어야 합니다. 인프라 구성은 AirZone이 맡고, 사용자는 DAG 검증에만 집중할 수 있습니다. 로컬 환경을 따로 세팅하지 않아도 브라우저에서 코드를 수정할 수 있도록 Jupyter Notebook도 함께 제공합니다. 또한 테스트가 의미를 가지려면 실행되는 DAG가 prod 환경과 유사해야 하고, 하둡 인증도 prod와 동일한 방식으로 이뤄져야 합니다.

격리된 환경

PR 단위로 독립된 Airflow를 생성합니다. 사용자마다 환경이 분리되므로 테스트가 다른 작업에 영향을 주지 않습니다.

3. AirZone 설계

PR 코멘트를 사용자 접점으로 정한 뒤, 나머지 구조는 세 가지 결정으로 이어졌습니다.

요청과 배포는 분리한다

환경 하나를 만드는 데는 시크릿 배포, Helm 설치, 헬스체크까지 이어져 수 분이 걸립니다. API 프로세스가 이 과정을 끝까지 붙잡으면 응답이 늦어지고, 실패 시 재시도가 어렵고, 배포 로그가 API 로그에 섞여 원인 추적도 힘들어집니다.

그래서 요청 처리와 배포 실행을 분리했습니다. airzone-api는 요청의 유효성(PR 존재 및 open 여부, 네임스페이스 중복)만 검증하고 바로 응답합니다. 실제 배포는 이 단계에서 만든 Kubernetes Job이 컨테이너 안에서 helm install부터 헬스체크까지 처리합니다.

이렇게 나누면 실패를 다루기 쉬워집니다. Job마다 독립된 로그와 상태가 남아 어느 요청의 어느 단계에서 실패했는지 바로 좁힐 수 있고, 이전 실패 Job은 다음 요청에서 지우고 새로 만들어 재시도할 수 있습니다.

PR 단위로 네임스페이스를 분리한다

레포명-PR번호를 네임스페이스 이름으로 삼으면, Airflow 웹 서버·스케줄러·PostgreSQL·Jupyter·DAG 볼륨·로그가 PR 단위로 격리됩니다. 다른 PR의 테스트가 스케줄러나 워커 자원을 침범할 일이 없고, 한 사람이 여러 PR을 동시에 열어두고 각각 테스트하는 것도 자연스럽게 가능해집니다.

또한 PR이라는 단위는 리뷰 관점에서도 잘 맞습니다. 리뷰어가 PR 페이지의 링크로 리뷰용 환경에 접속해 실제 동작을 확인할 수 있고, 어떤 코드 상태가 배포돼 있는지도 PR 하나로 특정됩니다. 삭제할 때도 네임스페이스를 기준으로 정리하면 되기 때문에 수명 주기 관리가 단순해집니다.

AirZone 전용 Helm 차트를 만든다

기존 Airflow 차트를 그대로 쓰면 빠르게 시작할 수는 있지만, PR 브랜치 기반 작업 공간, 브라우저 IDE, 키탭 주입, 테스트용 로그 링크, 삭제 가능한 네임스페이스라는 AirZone의 요구사항을 한 배포 단위로 다루기 어렵습니다. 반대로 테스트 환경에서 필요하지 않은 구성도 있었습니다. PGBouncer나 외부 DB 연결처럼 운영 환경에서는 필요하지만 개인 테스트 환경에는 과한 요소는 걷어냈습니다.

세 가지 결정을 구성 요소로 정리하면 다음과 같습니다.

구성 요소역할설계 이유
GitHub PR 코멘트AirZone 생성·삭제 링크를 안내하고, 생성 결과를 다시 PR에 남깁니다.사용자가 이미 테스트를 시작하는 지점이 PR이었기 때문에 별도 플랫폼이나 UI 접근 없이 진입할 수 있습니다.
airzone-api요청 파라미터와 PR 상태를 검증하고, 생성·삭제 Job을 만듭니다.API 서버는 빠르게 응답하고, 오래 걸리는 설치 작업과 책임을 분리합니다.
Kubernetes Job · CronJob생성·삭제 요청은 Job으로 실행하고, PR 종료 후 방치된 환경은 CronJob이 매일 자동 회수합니다.요청 단위 실행 격리와 자동 정리를 같은 Kubernetes 기본 기능으로 일관되게 처리할 수 있습니다.
AirZone Helm 차트Airflow, PostgreSQL, DAG PVC, Jupyter 등을 하나의 환경으로 묶어 배포합니다.같은 입력값으로 재현 가능한 개인 Airflow 환경을 만들고 삭제할 수 있습니다.
카카오워크 알림요청 수신 시 1차 알림을 보내고, 배포 완료 후 2차 알림으로 결과를 전달합니다. Jupyter 토큰과 Kubernetes 네임스페이스 토큰은 PR 코멘트가 아닌 카카오워크로만 전달합니다. 운영 오류 발생 시에도 담당자에게 알립니다.PR은 공개 범위가 넓기 때문에 민감한 인증 정보는 채널을 분리해 전달합니다.

덕분에 AirZone은 사용자의 입장에서는 “PR 코멘트의 링크를 누르는 기능”처럼 보이지만, 운영자의 입장에서는 생성 요청, 설치 Job, 헬스체크, 결과 알림, 정리 배치를 각각 추적할 수 있는 구조가 되었습니다.

4. AirZone 구축 과정

설계에서 정한 흐름을 실제로 연결하는 작업이 구축의 핵심이었습니다. GitHub 훅 처리, 요청 검증, Kubernetes Job 실행, Helm 차트 배포, DAG 동기화, 서비스 헬스체크, 결과 알림이 하나의 흐름으로 이어지고, 어느 한 단계가 끊기면 사용자는 “링크를 눌렀는데 아무 일도 일어나지 않는” 상황을 마주하게 됩니다.

PR 생성 훅을 사용자 인터페이스로 쓰기

AirZone의 기본 사용자 인터페이스는 별도 웹 화면이 아니라 GitHub PR 코멘트입니다. PR이 열리는 이벤트를 airflow-api가 훅으로 받고, PR의 base/head repository, PR 번호, 요청자, 브랜치를 읽어 생성 링크를 만듭니다. 사용자가 테스트하려는 하둡 환경을 고를 수 있도록 하둡별 링크도 함께 남깁니다.

HADOOP_CLUSTER = ["hadoopA", "hadoopB", "hadoopC"]

# PR opened 이벤트를 받아 AirZone 생성 링크를 댓글로 남긴다
def request_airzone_on_pr_opened(req_json):
    base_repository = req_json["pull_request"]["base"]["repo"]["full_name"]
    head_repository = req_json["pull_request"]["head"]["repo"]["full_name"]
    pr_number = req_json["number"]
    requester = req_json["pull_request"]["user"]["login"].replace("-", ".")
    branch = github_utils.get_branch_in_pull_request(base_repository, pr_number)

    create_url = make_airzone_create_url(
        base_repository=base_repository,
        head_repository=head_repository,
        pr_number=pr_number,
        user=requester,
        branch=branch,
    )
    urls = {h: f"{create_url}&hadoop={h}" for h in HADOOP_CLUSTER}
    issue.create_comment(make_airzone_guide_message(urls))

이 방식의 장점은 사용자가 새로운 도구를 배우지 않아도 된다는 점입니다. PR을 열면 테스트 환경을 만들 수 있는 선택지가 PR 코멘트로 나타나고, 생성 결과도 다시 PR 코멘트로 남습니다. 반대로 Jupyter 토큰처럼 노출되면 안 되는 정보는 PR이 아니라 카카오워크로 전달해 공개 범위를 분리했습니다.

요청 검증과 Job 생성

링크를 누르면 요청은 airzone-api로 들어옵니다. 여기서는 실제 Airflow를 설치하지 않고, 요청이 유효한지만 확인합니다. 브랜치 정보가 있는지, PR이 존재하고 열려 있는지, 같은 PR로 이미 만들어진 네임스페이스가 있는지를 먼저 검사합니다.

# 요청을 검증하고, 실제 구성은 K8s Job에 맡긴다
def create_airzone(k8s, input_data, pr):
    if not input_data["branch"]:
        return "[ERROR] 요청 필드 수가 부족합니다.", HTTPStatus.BAD_REQUEST

    if pr is None or pr.state != "open":
        return "[ERROR] 열린 PR이 존재하지 않습니다.", HTTPStatus.BAD_REQUEST

    namespace = create_namespace_name(
        input_data["base_repository"],
        input_data["pr_number"],
    ).lower()

    if check_namespace_exists(k8s, namespace):     # 한 PR = 하나의 AirZone
        return "[ERROR] 한 PR 에서는 하나의 AirZone을 생성할 수 있습니다.", HTTPStatus.CONFLICT

    deploy_job(input_data, namespace, action="create")
    return "[SUCCESS] AirZone 생성 요청 완료.", HTTPStatus.OK

검증이 끝나면 실제 작업은 Kubernetes Job으로 넘깁니다. Job 이름은 create-airzone-{namespace}, delete-airzone-{namespace}처럼 요청 종류와 네임스페이스를 포함하도록 만들었습니다. 이미 같은 이름의 Job이 남아 있으면 먼저 지우고 새 Job을 생성해, 이전 실패 흔적 때문에 새 요청이 막히지 않도록 했습니다.

# API 서버는 Job만 만들고, 실제 배포는 Job 컨테이너 안에서 수행한다
def deploy_job(input_data, namespace, action="create"):
    job_name = f"{action}-airzone-{namespace}"
    job_namespace = "airzone"

    delete_if_job_exists(job_name, job_namespace)
    batch_v1.create_namespaced_job(
        namespace=job_namespace,
        body=create_job_object(input_data, job_name, namespace, action=action),
    )

이 구조는 장애를 다루기에도 좋았습니다. API 요청은 빠르게 응답하고, 시간이 오래 걸리는 설치 과정은 독립된 Job 로그와 상태로 추적할 수 있습니다. 생성에 실패해도 API 서버가 영향을 받지 않고, 실패한 Job만 보고 원인을 좁힐 수 있습니다.

Helm 차트를 통한 환경 구성

Job 컨테이너 안에서는 Airflow 한 벌을 Helm 차트로 배포합니다. 여기서 “한 벌”은 Airflow 웹 서버와 스케줄러만 뜻하지 않습니다. PostgreSQL, DAG 저장 PVC, git 초기화 컨테이너, Jupyter, 키탭 시크릿, TLS, 로그 설정까지 테스트에 필요한 요소를 함께 묶었습니다.

구성 요소역할
Git 정보PR의 head repository와 branch를 받아 해당 코드만 동기화합니다.
DAG PVCscheduler, Jupyter이 같은 작업 디렉터리를 보도록 합니다.
Airflow 설정KubernetesExecutor, DAG scan interval, 로그, 하둡 변수 등을 테스트 환경에 맞게 설정합니다.
인증 정보사용자 principal, 공용 principal, 키탭, Jupyter 토큰, dkos에서 제공하는 TLS 인증서를 환경에 주입합니다.
노드와 스토리지여유 있는 노드 그룹을 고르고, 같은 리전의 storageClass를 사용하도록 지정합니다.
로그 연동Airflow 로그를 Elasticsearch·Kibana에서 볼 수 있도록 연결 정보를 주입합니다.
# 여유 있는 노드 그룹을 고른 뒤, 해당 네임스페이스에 Helm 설치
node_group = get_allocatable_nodegroup(core_v1_api, node_group_key, exclude_groups)
node_selector = get_node_selector(node_group_key, node_group)
storage_class = get_storage_class(node_group)

subprocess.run([
    "helm", "install", "airflow", "./chart",
    "--namespace", namespace,
    "--values", "./chart/values.yaml",
    "--set", f"git.url={git_url}/{head_repo}.git",
    "--set", f"git.branch={branch}",              # PR 브랜치를 그대로 sync
    "--set", f"principals={{{common_principal},{principal}}}",
    "--set", f"variables.hadoop={hadoop}",        # 대상 하둡 클러스터
    "--set", f"airflow.jupyter.token={secret_token}",
    "--set", "airflow.nodeSelector." + node_selector,
    "--set", f"storageClass={storage_class}",
    "--set", "airflow.elasticsearch.enabled=true",
], check=True, capture_output=True)

AirZone 차트를 별도로 만든 이유도 여기에 있습니다. 기존 Airflow 차트를 그대로 쓰면 빠르게 시작할 수는 있지만, PR 브랜치 기반 작업 공간, 브라우저 IDE, 키탭 주입, 테스트용 로그 링크, 삭제 가능한 네임스페이스라는 AirZone의 요구사항을 한 배포 단위로 다루기 어렵습니다. 차트를 분리하면서 “개인 테스트 환경 하나”를 반복 가능하게 만들 수 있었습니다.

DAG 동기화와 실행 시점 복사

가장 고민이 많았던 부분은 DAG 파일을 어떻게 다룰지였습니다. 사용자는 Jupyter에서 파일을 고치고, Airflow 스케줄러는 그 변경을 바로 봐야 합니다. 반면 KubernetesExecutor가 만든 Worker Pod는 실행 시점마다 새로 뜨기 때문에, 같은 파일을 안정적으로 전달하는 방법이 필요했습니다.

처음에는 사내 공유 스토리지를 작업 디렉터리로 쓰는 방법도 검토했지만, 파일 수가 많을 때 느려지고 권한이 바뀌는 문제가 있었습니다. 그래서 PR 브랜치는 Scheduler Pod의 init container가 한 번 clone하고, Scheduler·Jupyter는 같은 PVC를 공유하도록 했습니다. Worker Pod는 task 실행 시 Scheduler Pod에서 DAG 디렉터리를 복사해 실행합니다.

# scheduler: PR 브랜치를 PVC에 clone하고, IDE 컨테이너도 같은 볼륨을 본다
scheduler:
  extraInitContainers:
    - name: git-init
      image: alpine/git:v2.45.2
      args: ["/scripts/git-init.sh"]
      volumeMounts:
        - mountPath: /git
          name: dags
  extraContainers:
    - name: jupyter-notebook
      volumeMounts:
        - mountPath: /home/airzone
          name: dags
    - name: code-server
      volumeMounts:
        - mountPath: /home/airzone
          name: dags

# worker: task 시작 시 scheduler의 DAG 디렉터리를 복사해서 실행한다
workers:
  extraInitContainers:
    - name: dags
      command:
        - sh
        - -c
        - kubectl cp $(kubectl get pods | awk '($0 ~ /scheduler/) { print $1 }'):/opt/airflow/dags /dags

이 방식은 운영 Airflow의 DAG 배포 방식과 완전히 같지는 않습니다. 하지만 AirZone은 임시 테스트 환경이기 때문에, 사용자가 파일을 고치고 곧바로 실행해보는 경험이 더 중요했습니다. 대신 환경 자체를 PR 단위로 격리하고, 삭제 시 네임스페이스를 함께 지워 테스트 흔적이 오래 남지 않도록 했습니다.

노드 그룹 스케줄링

DAG를 어디에 두어 실행할지가 정리되면, 다음 문제는 이 Pod들을 실제로 어느 노드에 띄울지입니다. 프로덕션 클러스터는 세 리전에 걸쳐 노드 그룹이 나뉘어 있고 리전마다 별도의 Cinder 기반 storageClass가 있습니다. Cinder 볼륨은 생성된 리전 안에서만 노드에 붙일 수 있기 때문에, Pod와 PVC는 반드시 같은 리전에 있어야 정상 동작합니다. 즉, Pod를 어느 노드 그룹에 배치할지 결정하면 PVC의 storageClass도 자동으로 그에 맞춰 정해져야 합니다.

또한 특정 노드 그룹만 계속 쓰면 그 리전의 자원이 먼저 소진되고 다른 리전은 놀게 됩니다. 그래서 배포할 때마다 여유가 가장 많은 노드 그룹을 골라 그 안에 Pod를 띄우도록 했습니다. 방식 자체는 단순합니다. 배포 직전에 전체 노드 그룹의 CPU와 메모리 사용률을 합산해 그 값이 가장 낮은 그룹을 선택하고, 이 값을 nodeSelector(Airflow Pod 배치)와 storageClass(DAG PVC, PostgreSQL PVC)에 동시에 넘겨줍니다. Airflow 컴포넌트와 저장 볼륨이 항상 같은 리전 안에 자리 잡게 되어, 리전을 넘나드는 볼륨 접근으로 mount가 실패하거나 지연되는 상황을 피할 수 있습니다.

서비스 상태 확인과 결과 알림

Helm 설치가 완료되었다고 해서 곧바로 접속할 수 있는 것은 아닙니다. 설치가 끝난 뒤에도 서비스가 기동을 마치기까지는 시간이 걸리기 때문입니다. 그래서 Web·Jupyter·Scheduler가 실제로 응답할 때까지 기다린 뒤 성공으로 판단했습니다. 하나라도 끝내 응답하지 않으면 어떤 서비스가 문제인지 함께 담아 담당자에게 알립니다.

# 각 서비스가 실제로 응답할 때까지 폴링으로 확인
services_status = {
    "Airflow":   web_sensor(airzone_url, 600, 30),
    "Jupyter":   web_sensor(f"{airzone_url}/jupyter", 300, 30),
    "Scheduler": scheduler_sensor(namespace, "airflow-scheduler", 300, 30),
}
if all(services_status.values()):
    create_keytab_secret(...)        # 사용자 키탭을 넣어야 하둡에 붙을 수 있다
    namespace_token = get_namespace_token(namespace)
else:
    failed = [n for n, up in services_status.items() if not up]
    detail = f"[실패] 다음 서비스에 접속할 수 없습니다: {failed}"

개인 환경이라도 하둡에 실제로 접속해 데이터를 다루려면 인증이 필요합니다. AirZone은 요청한 사용자의 개인 키탭을 찾아 주입하고, 없을 경우 공용 키탭으로 대신 채운 뒤 그 사실을 카카오워크로 안내합니다. 덕분에 개인 환경에서도 실제 하둡 인증이 필요한 작업까지 검증할 수 있습니다.

생성·삭제 결과는 모니터링 로그와 알림으로 남깁니다. 성공 시에는 접속 주소와 삭제 링크를 PR과 카카오워크에 전달하고, 실패 시에는 관리자에게 원인을 함께 보냅니다. 이 흐름까지 포함해야 사용자는 “링크를 눌렀는데 지금 무슨 일이 일어나는지 모르는 상태”에 빠지지 않고, 운영자는 실패한 배포를 추적할 수 있습니다.

최종 아키텍처

구축 과정을 하나의 그림으로 정리하면 다음과 같습니다. 사용자는 PR 코멘트의 링크만 보지만, 내부에서는 API가 요청을 검증하고 Kubernetes Job이 Helm 차트를 실행해 PR 전용 네임스페이스를 만듭니다. 이후 헬스체크, 결과 알림, 생성·삭제 로그, 정리 배치가 환경의 수명 주기를 관리합니다.

5. AirZone 사용 예시

AirZone은 GitHub PR을 중심으로 동작합니다. 생성부터 삭제까지 흐름은 다음과 같습니다.

5.1. 생성

DAG를 수정한 브랜치로 PR을 생성합니다. PR에 생성 링크가 담긴 코멘트가 달리고, 링크를 누르면 잠시 후 접속 주소를 받습니다.

5.2. 사용

안내받은 AirZone URL로 접속하면 실제 Airflow와 동일한 환경에서 DAG를 테스트할 수 있습니다.

또한 Jupyter로 접속하면 커밋·푸시 없이 코드를 바로 수정하고, 이어서 테스트를 계속할 수 있습니다.

5.3. 삭제

생성 완료에 있었던 삭제 링크를 클릭하여 삭제합니다. 삭제 링크를 클릭하지 않아도 PR이 닫히면 자동으로 삭제됩니다.

6. Beta 운영과 개선

AirZone은 바로 정식 배포하지 않고, 먼저 1차 Beta 버전을 배포했습니다. 앞서 정한 두 요구사항이 실제로 잘 만족되는지 검증하고, 그 외에 사용자 관점에서 불편한 점은 없는지 확인하기 위해서였습니다.

Beta를 운영해보니 두 요구사항은 실제 사용에서도 잘 지켜졌습니다. 사용자들은 VPN이나 로컬 하둡 설정 같은 준비 없이 PR만 열면 개인 Airflow 테스트 환경이 구성되어 바로 테스트가 가능했고, PR 단위로 격리된 환경에서 다른 작업에 영향을 주지 않고 테스트할 수 있었습니다. 여기에 더해 실제 사용에서 나온 피드백을 모아 2차 정식 배포에 반영했습니다.

가장 많이 나온 피드백은 코드 수정에 대한 것이었습니다. Beta에서는 코드 수정 도구로 Jupyter만 제공했는데, Jupyter는 코드를 고치면 체크포인트 파일을 따로 만듭니다. 이 파일을 Airflow가 원본 대신 읽어, 수정한 내용이 Airflow DAG에 제대로 반영되지 않는 경우가 있었습니다. .airflowignore로 해당 파일을 무시하도록 설정할 수도 있지만, 그보다 사용자들이 익숙한 편집 환경을 쓰도록 지원하는 편이 낫다고 판단했습니다. 그래서 VS Code for Web 환경을 지원하고, 서비스 어카운트 토큰 기반으로 로컬 IDE 연결도 지원했습니다. 덕분에 평소 쓰던 IDE와 AI 코딩 도구를 그대로 활용해 DAG를 수정하고 바로 테스트할 수 있게 되었습니다.

한편 AirZone을 상시 실행 환경으로 키우지는 않았습니다. 개인 환경은 테스트가 끝나면 지워지는 것이 전제이기 때문입니다. 그래서 방치된 환경이 자원을 계속 점유하지 않도록, 14일 이상 유지되는 환경은 자동으로 정리하는 로직을 추가했습니다.

다만 임시 환경을 넘어 개인적인 Airflow 환경을 직접 구축해 쓰고 싶다는 요청도 있었습니다. 그래서 AirZone과 같은 구성을 자체 클러스터에 배포할 수 있는 Helm 차트 가이드를 제공했습니다. 덕분에 임시 환경이 아니라 상시로 운영하는 개인 Airflow 환경까지 쉽게 구축할 수 있게 되었고, 데이터 조직뿐 아니라 다른 조직에서도 이 가이드를 활용해 자체 Airflow 환경을 구축하는 사례도 있었습니다.

도입 후 달라진 점

위와 같은 변화 외에도, 기존에 로컬, 개발, 테스트용 Airflow에서 각자 다른 방식으로 테스트하던 크루들이 모두 AirZone으로 전환하여 서로 다르게 준비하던 테스트 환경이 하나의 방식으로 정리된 점도 크게 달라진 점 중 하나입니다.

마치며

지금까지 파이프라인 테스트 과정의 비효율을 줄이기 위해 개인 테스트 환경 AirZone을 어떻게 설계하고 구축했는지, 그리고 운영하며 어떤 요청을 반영해 왔는지 소개했습니다.

AirZone을 도입한 뒤로, 테스트를 위해 반복하던 준비 작업의 상당 부분이 사라졌습니다. 이제는 PR을 여는 것만으로 운영 환경과 동일한 Airflow를 5분 내외에 받아 쓸 수 있습니다. 각 환경은 독립돼 있어 서로 영향을 주지 않고, 작업이 끝나면 자동으로 정리됩니다. 덕분에 사용자는 인프라를 신경 쓰지 않고 파이프라인 자체에 집중할 수 있게 되었습니다.

앞으로도 운영하며 받는 피드백을 바탕으로 AirZone을 꾸준히 개선하려 합니다. 환경 준비의 부담을 0에 가깝게 만들자는 처음의 목표를 이어가며, 누구나 쉽게 테스트를 할 수 있는 환경을 계속 고민하겠습니다. 이번 글은 대표로 몇 명이 모여 정리해 보았지만, 함께 고민하고 개발했던 nathan.kwon, dingo.kim 의 기여에도 감사의 마음을 전합니다. 긴 글 읽어주셔서 감사합니다!