[Airflow] 안녕, 에어플로우! with BashOperator

Part 1. 실습으로 익히는 에어플로우 기본

1. 목적

이번 포스트에서는 하루에 한번씩 “안녕, 에어플로우!” 라고 인사하는 단순한 워크플로우부터 시작해서 데브옵스(DevOps) 환경에 응용할 수 있는 실용적인 예제까지 연결해서 살펴보도록 하겠습니다.

에어플로우(Airflow)는 워크플로우 엔진의 하나입니다. 에어플로우는 워크플로우를 하나의 DAG로 정의합니다. 또한 하나의 DAG은 한 개 이상의 작업들(tasks)로 이루어집니다.

DAG의 특징의 자세한 이야기는 뒤에 Part 3에서 하도록 하고 여기서는 간단하게 용어설명 정도 하고 넘어가겠습니다. DAG 이란 Directed Acyclic Graph의 약자로, “방향성 비순환 그래프”로 번역합니다. 다시 풀어 얘기하면;

  • 그래프(graph)는 노드(node, 꼭지점)와 엣지(edge, 변)으로 구성되는 구조를 말합니다. 아래 그림에서는 “Task 1”이 하나의 노드, “Task 1”과 “Task 3”을 연결하는 선이 엣지입니다.
  • 방향성(directed) 노드와 노드간의 엣지가 단방향으로 연결됩니다. “Task 1”과 “Task 3”을 연결하는 선이 화살표로 표시되어있습니다.
  • 비순환(acyclic): 한번 지나간 노드는 다시 지나가지 않는다. 노드를 연결한 엣지를 따라가다보면, 어디서 시작하던지 간에 더 이상 연결된 엣지가 없는 노드에 다다르게 됩니다.

즉, 작업간의 의존성 혹은 선후 관계가 생긴다고 할 수 있습니다.

그래프를 구성하는 각각의 노드를 작업(task)이라고 생각할 수 있습니다. 에어플로우는 여러 형태의 작업을 지원하는데, 우리는 가장 기본이 되는 BashOperator로 시작해보죠.

2. BashOperator

2.1 환경변수

시작하기전에, 앞으로 자주 언급할 환경변수를 정의하겠습니다. 각자 사용하시는 쉘에 맞게 .bashrc 또는 .zshrc 파일 마지막에 다음 줄을 추가합니다.

export AIRFLOW_HOME=$HOME/airflow
export PYTHONPATH=$AIRFLOW_HOME/config:$PYTHONPATH

추가한 내용이 적용되도록 source ~/.bashrc 또는 source ~/.zshrc 명령을 실행합니다.

2.2 DAG을 위한 디렉토리

우선 AIRFLOW_HOME 디렉토리 아래에 dags 디렉토리를 만듭니다. 이 곳이 앞으로 만들 모든 DAG의 소스코드를 저장하는 위치입니다.

$ cd $AIRFLOW_HOME
$ mkdir dags
$ ls
airflow.cfg
airflow.db
dags
logs
unittests.cfg

dags 디렉토리에 우리의 첫 번째 DAG을 만들겠습니다. 즐겨쓰는 편집기를 이용해서 아래 내용을 담은 dags/hello_airflow.py 파일을 만드세요.

from airflow.operators.bash_operator import BashOperator
from airflow.models import DAG
from datetime import datetime, timedelta

args = {
    'owner': 'airflow',
    'start_date': datetime(2018, 11, 1)
}

dag = DAG(
    dag_id='hello_airflow',
    default_args=args,
    schedule_interval="@once")

# Bash Operator
cmd = 'echo "Hello, Airflow"'
BashOperator(task_id='t1', bash_command=cmd, dag=dag)

가장 먼저 중요한 DAG과 작업(task)부터 살펴보겠습니다. 우선 DAG 입니다. 위의 소스코드 10행에서 매개변수는 세 개를 이용해서 DAG 클래스의 인스턴스 만듭니다.

  • dag_id: DAG를 구별하는 유일한 이름. 여기서는 hello_airflow
  • default_args: DAG의 각종 설정
  • schedule_interval: DAG 실행 간격. 예를들어 1시간, 하루, 일주일 등.

dags 디렉토리의 같은 dag_id의 DAG 이 여러 개가 있다면, 에어플로우는 그 중 마지막에 읽은 단 하나 밖에 인식하지 못하기 때문에 주의가 필요합니다

default_args는 매우 많은 설정을 가지고 있습니다. 뒤에서 설정파일과 함께 설명하겠습니다. 마지막으로 schedule_interval="@once" 는 DAG을 한 번만 사용하겠다는 의미입니다. 일회성 DAG이나 테스트 할 때 주로 사용하는 설정입니다. 좀 더 실용적인 사용 예는 뒤에서 소개하겠습니다.

위의 소스코드에서 마지막 줄의 BashOperator 가 바로 작업입니다. 이름처럼 bash 명령을 실행하는 operator 입니다. 에어플로우가 동작하는 환경에서 실행가능한 모든 리눅스 명령을 실행할 수 있다고 생각하시면 됩니다. 리눅스 명령의 예로는 ls, echo, mkdir, awk, df, jq 같은 명령들이 있습니다.

우선 BashOperator의 매개변수부터 설명하겠습니다.

  • task_id: 작업을 구별하는 유일한 이름. 여기서는 t1
  • bash_command: 실행할 bash 명령어. 문자열 형태.
  • dag: 작업이 속하는 DAG

16행의 문자열을 명령행에서 실행하면 echo 명령어가 뒤에 나오는 문자열을 그대로 화면에 출력함을 확인할 수 있습니다.

$ echo "Hello, Airflow"
Hello, Airflow

마지막으로 라이브러리 import 문을 살펴보면, 1행은 BashOperator를 위해서, 2행은 DAG 클래스를 위해서, 3행은 DAGdefault_args에 들어가는 datetime 클래스를 사용하기 위해서 정의합니다.

이처럼 hello_airflow DAG은 하나의 작업(task)으로 구성되는 가장 단순한 DAG입니다.

그럼 이제 실행해보겠습니다.

$ airflow test hello_airflow t1 20181101
...
[2018-11-21 15:28:12,319] {bash_operator.py:106} INFO - Output:
[2018-11-21 15:28:12,319] {bash_operator.py:110} INFO - Hello! Airflow
[2018-11-21 15:28:12,319] {bash_operator.py:114} INFO - Command exited with return code 0

테스트 명령을 조금 구체적으로 살펴보겠습니다. test 하위명령은 특정한 작업을 실행하는 명령이며, 뒤에서 다룰 의존성에 관계없이 무조건 실행하기 때문에 개발하며 동작을 확인할 때 편리한 명령입니다. 매개변수 없이 airflow test 만 입력하면, dag_idtask_id, execution_date가 꼭 필요한 매개변수임을 알 수 있습니다.

$ airflow test
[2018-11-21 15:18:38,017] {__init__.py:51} INFO - Using executor SequentialExecutor
usage: airflow test [-h] [-sd SUBDIR] [-dr] [-tp TASK_PARAMS]
                    dag_id task_id execution_date

실행 방법이나 결과는 airflow testairflow run 이 동일합니다. 하지만, airflow run 은 실행결과를 데이터베이스에 남기고, airflow test는 남기지 않는 차이가 있습니다.

위에서 설명한 것 처럼 우리의 예제에서 dag_idhello_airflow, task_idt1 입니다. 그럼 execution_date는 어떤 값을 넣어야 할까요? execution_date는 다양한 날짜-시간 형태를 받아들입니다. 나중에 자세히 언급할 start_date 항목과 깊은 관계가 있지만, 지금은 형태에만 주목하도록 하겠습니다.

제가 실행할 때 사용한 20181101 값을 보시면, yyyyMMdd 형태인 것을 쉽게 알아차리실 수 있을 것입니다. 그 밖에도 2018-11-01 도 사용할 수 있습니다. 또한 시간도 필요할때가 있는데, 그럴 때는 20181101-1430 또는 2018-11-01T14:30 형태를 사용할 수 있습니다. 시간이 생략된 경우 기본 값은 0시 입니다.

그 밖에도 airflow 명령은 많은 하위명령(subcommand)이 있습니다. airflow 만 입력하면 전체 하위명령을 확인할 수 있습니다.

$ airflow
usage: airflow [-h]
               {backfill,list_tasks,clear,pause,unpause,trigger_dag,delete_dag,pool,variables,kerberos,render,run,initdb,list_dags,dag_state,task_failed_deps,task_state,serve_logs,test,webserver,resetdb,upgradedb,scheduler,worker,flower,version,connections,create_user}

3. 마크로 (macro)

에어플로우의 강력한 기능 중 하나가 마크로 입니다. 이번에도 간단한 소스코드와 함께 알아보시죠.

from airflow.operators.bash_operator import BashOperator
from airflow.models import DAG
from datetime import datetime, timedelta

args = {
    'owner': 'airflow',
    'start_date': datetime(2018, 11, 1)
}

dag = DAG(
    dag_id='hello_today',
    default_args=args,
    schedule_interval="@once")

# Bash Operator
cmd = 'echo "Hello! Today is {{ ds }}"'
BashOperator(task_id='t1', bash_command=cmd, dag=dag)

첫 번째 예제와 다른 부분을 찾으셨습니까? 그렇습니다. 11행에서 dag_idhello_today로 바뀌었고, 16행의 명령에 {{ ds }} 라는 생소한 문자열이 생겼습니다. 실행해보고 결과부터 확인해죠.

(batch) $ airflow test hello_today t1 20181101
[2018-11-21 15:31:26,361] {bash_operator.py:97} INFO - Running command: echo "Hello! Today is 2018-11-01"
[2018-11-21 15:31:26,370] {bash_operator.py:106} INFO - Output:
[2018-11-21 15:31:26,374] {bash_operator.py:110} INFO - Hello! Today is 2018-11-01
[2018-11-21 15:31:26,374] {bash_operator.py:114} INFO - Command exited with return code 0

위에 보시는 바와 같이 {{ ds }} 가 있던 자리에 2018-11-01 이라는 문자열이 출력됨을 확인하실 수 있습니다. 에어플로우의 매크로(macro)는 마치 C언어의 매크로처럼 원하는 명령을 실행하기 전에 내용을 치환해서 실행합니다. 여기서는 파이선의 datetime 객체를 문자열 yyyy-MM-dd 포맷으로 바꿔서 {{, }} 안의 내용을 치환했습니다.

혹시, “하지만 오늘은 2018-11-01 이 아니야”라고 생각하시고 계신 분이 계신가요? 여기에 에어플로우의 작업을 실행할 때 항상 “시간”이 필요한 이유가 있습니다. 에어플로우는 항상 특정 시점을 기준으로 동작합니다. 우리가 2018-11-01오늘 이라고 알려줬기 때문에 Hello! Today is 2018-11-01 라고 출력한 것입니다. 마찬가지로 다른 시간을 주면, 다른 시간을 오늘 로 인식합니다.

(batch) $ airflow test hello_today t1 20181105
...
[2018-11-21 15:31:26,374] {bash_operator.py:110} INFO - Hello! Today is 2018-11-05
...

다만, 에어플로우는 과거 만 다룹니다. 미래 시간은 사용할 수 없습니다.

(batch) $ airflow test hello_today t1 20281105
[2018-11-26 16:18:51,774] {__init__.py:51} INFO - Using executor SequentialExecutor
[2018-11-26 16:18:51,920] {models.py:271} INFO - Filling up the DagBag from /Users/{USER}/airflow/dags
[2018-11-26 16:18:51,977] {models.py:1355} INFO - Dependencies not met for <TaskInstance: hello_today.t1 2028-11-05T00:00:00+00:00 [None]>, dependency 'Execution Date' FAILED: Execution date 2028-11-05T00:00:00+00:00 is in the future (the current date is 2018-11-26T07:18:51.977695+00:00).

ds 이외에도 공식적으로 지원하는 매크로는 아주 많습니다. 공식문서를 한번 살펴보세요. 나중에 예제를 통해 몇 가지 매크로에 대해 더 알아보고, “Part 4”에서 자신이 원하는대로 매크로를 확장하는 방법도 다루도록 하겠습니다.

4. 디스크 공간 알림 예제

작지만 유용한 예제를 하나 소개해드리겠습니다. 디스크 공간 알림 예제인데요, 하는 일은 특정 디렉토리의 크기가 1GB 이상인 경우 알림 메시지를 보내는 것입니다. 본격적인 모니터링 시스템을 연동하기 어려운 early stage이지만 모니터링이 필요한 경우 유용하게 사용하실 수 있는 방법입니다.

4.1 디스크 공간 검사하기

여기서는 차지하고 있는 디스크 공간을 알아내기 위해 아래 리눅스 명령들을 파이프로 연결해서 사용해보겠습니다.

  • 디렉토리의 크기를 알아내는 du 명령
  • 값을 정렬하는 sort 명령
  • 간단한 계산과 원하는 출력을 해주는 awk 명령

저는 맥북을 사용하는데, 맥북에서 어떤 프로그램이 캐시를 많이 사용하는지 알려주는 알림을 받고 싶다고 가정하겠습니다. 간단한 명령 조합으로 경계값보다 많은 캐시 용량을 차지하는 프로그램 목록을 얻을 수 있습니다. 여기서는 경계값(threshold)을 1GB 라고 정했습니다.

$ THRESHOLD_VALUE=$((1024**2))
$ du ~/Library/Caches/* | sort -rn | awk -v TVAL=$THRESHOLD_VALUE '{ if($1==$1+0 && $1>TVAL) {split($2,u,"//"); print $1 /1024**2 " GB " u[1] } }'
4.2501 GB /Users/{USER}/Library/Caches/Homebrew
2.37842 GB /Users/{USER}/Library/Caches/Homebrew/Cask
...
1.16296 GB /Users/{USER}/Library/Caches/Google/Chrome/Default/Cache

이제 매일 오전 9시에 나에게 알림을 주도록 에어플로우 DAG으로 변환해보겠습니다.

from airflow.operators.bash_operator import BashOperator
from airflow.models import DAG
from datetime import datetime, timedelta

args = {
    'owner': 'airflow',
    'start_date': datetime(2018, 11, 1, 9, 0, 0)
}

dag = DAG(
    dag_id='mon_disk',
    default_args=args,
    schedule_interval=timedelta(days=1))

# Bash Operator
cmd = '''
THRESHOLD_VALUE=$((1024**2))
du ~/Library/Caches/* | sort -rn | awk -v TVAL=$THRESHOLD_VALUE '{ if($1==$1+0 && $1>TVAL) {split($2,u,"//"); print $1 /1024**2 " GB " u[1] } }'
'''
BashOperator(task_id='t1', bash_command=cmd, dag=dag)

이전 예제와 다른 부분은 start_date 에 시간(9시)까지 설정한 것과, DAG 초기화 할 때 schedule_interval@once 대신 timedelta(days=1) 값을 넣은 것입니다. 에어플로우 환경 설정이 완성되면, 이 DAG 은 매일 실행되며, 실행 시간은 오전 9시가 됩니다.

앞에서와 마찬가지 방법으로 테스트 해보겠습니다.

(batch) $ airflow test mon_disk t1 20181101
...
Output:
4.2501 GB /Users/{USER}/Library/Caches/Homebrew
2.37842 GB /Users/{USER}/Library/Caches/Homebrew/Cask
...
1.16378 GB /Users/{USER}/Library/Caches/Google/Chrome/Default/Cache
Command exited with return code 0

이제 제 맥북의 디스크 용량을 어떤 프로그램이 많이 사용하고 있는지 쉽게 알 수 있습니다. 그런데, 뭔가 부족하지 않나요? 알림 메시지는 어떻게 보낼까요?

4.2 라인 노티파이(LINE notify) 추가하기

라인 메신져에는 LINE notify라는 API를 제공합니다. Node.js와 Python으로 LINE Notify 사용해보기(1) – 기본 글을 참고하시면 쉽게 기본 개념을 이해하고, 개인 토근도 발급받을 수 있습니다.

위의 블로그 포스트에서는 파이선을 사용했지만, curl 명령을 통해서도 쉽게 메시지를 보낼 수 있습니다. 아래 예제는 TOKEN 값만 여러분의 토근으로 적어주시면 잘 동작합니다.

$ notify() {
    TOKEN="## use your token ##"

    MESSAGE="$1"
    echo ${MESSAGE}
    curl -X POST -H "Authorization: Bearer ${TOKEN}" -F "message=${MESSAGE}" https://notify-api.line.me/api/notify
}
$ MSG="New message"
$ notify "${MSG}"
New message
{"status":200,"message":"ok"}%                                                                                                                                                         (batch)

이를 그대로 에어플로우 작업으로 바꿔보겠습니다.

TOKEN = "## use your token ##"
message = "Hello, World @ {{ ds }}"
cmd_notify = f"""
curl -sX POST -H "Authorization: Bearer {TOKEN}" -F "message='{message}'" https://notify-api.line.me/api/notify
"""

BashOperator(task_id='t2', bash_command=cmd_notify, dag=dag)

매크로를 이용한 위의 코드도 테스트해보면 역시 잘 동작합니다.

$ airflow test mon_disk t2 20181101

이제 코드를 완성해보겠습니다. 지금까지 두 개로 만든 작업을 하나로 합쳐보겠습니다. du, sort, awk 명령들의 결과를 RESULT 환경변수에 담아서 LINE notify에 전달합니다.

from airflow.operators.bash_operator import BashOperator
from airflow.models import DAG
from datetime import datetime, timedelta

args = {
    'owner': 'airflow',
    'start_date': datetime(2018, 11, 1)
}

dag = DAG(
    dag_id='mon_disk',
    default_args=args,
    schedule_interval=timedelta(days=1))

# Disk usage + LINE notify
cmd_notify = """
THRESHOLD_VALUE=$((1024**2))
RESULT=$(du ~/Library/Caches/* | sort -rn | awk -v TVAL=$THRESHOLD_VALUE '{ if($1==$1+0 && $1>TVAL) {split($2,u,"//"); print $1 /1024**2 " GB " u[1] } }')

TOKEN="## use your token ##"
curl -sX POST -H "Authorization: Bearer ${TOKEN}" -F "message='${RESULT}'" https://notify-api.line.me/api/notify
"""
BashOperator(task_id='check_usage', bash_command=cmd_notify, dag=dag)
airflow test mon_disk check_usage 20181101

지금까지 BashOperator를 이용해서 단순하지만, 매우 유용한 디스크 알림예제를 만들어 봤습니다. 이를 응용하여 CPU, 힙메모리 사용률, 로그 파일 확인 등을 주기적으로 살펴보며 알림을 주는 간단한 모니터링 시스템으로 사용할 수도 있을 것입니다.

위의 예제가 잘 동작하기는 하나 배시 스크립트에 익숙하지 않으면 사용하기 어렵겠다는 생각을 하셨을 수도 있겠습니다. 하지만 에어플로우의 강력함은 이제부터 시작입니다. 다음 포스트에서 PythonOperator 를 사용해서 더 구조적이고 더 강력한 DAG을 만들어봅시다.