Cloud Composerに入門する

# はじめに
* Cloud Composerに入門した時に確認したことのメモです。（QA方式）

# 確認したこと
## Cloud Composerとは？
* Apache Airflow で構築された、フルマネージドのワークフローオーケストレーションサービス
    * ハイブリッドおよびマルチクラウド環境にまたがるパイプラインを作成、スケジューリング、モニタリング
    * Apache Airflow のオープンソースのプロジェクト上に構築され、Python を使用して運用
    * 特定のベンダーに依存する必要がなくなり、使用も簡単

## 仕組みは？
![スクリーンショット 2021-12-26 10.13.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92368/a4d36fe4-c580-40ed-06a0-6f9cd89ae1a9.png)
[Cloud Composer 環境のアーキテクチャ ](https://cloud.google.com/composer/docs/concepts/architecture?hl=ja)から引用

* 利用者としては、「DAGファイル」と呼ばれる.pyのファイルをGCSの指定のバケット下に置くだけで、ワークフローをどんどん追加して、やりたいタスクを動かしていくことができる
* Airflow Worker(s): 各タスクを実行するワーカー。node_countの設定で増やせる
* Cloud Storage
    * DAGs: ワークフローを記述するファイルを配置する
    * Logs: 各ワーカー上で出力されたログのファイルが配置される
    * plugin: DAGのなかで読み込みたい外部ファイルやモジュールなどを配置する

## シンプルな構築・利用手順は？
### 1. Cloud Composer APIの有効化
* コンソールからCloud Composer APIのENABLEを押下

### 2. Cloud Composer 環境を作成する
* コンソールからComposer
* -> 環境の作成

```
Location: us-central1など
Image Version: composerとairflowのバージョン
Maintenance windows: 週に12h必要
Service account: This service account runs the pods of your environment and performs environment operations such as upgrading your environment to a new version
Environment resources: Scheduler, Web server, WorkerのCPU・Memory・Storageなど
Network configuration: GKEのネットワーク設定（IPレンジなど）
```

* -> GKEやGCSが作成される（GKEのコンソール画面からclustersやServicesが確認できる）

### 3. DAGファイルの作成
* v2の場合の例

```python:example_v2_dag.py
from airflow.decorators import dag, task
from airflow.utils.dates import days_ago

@dag(default_args={'owner': 'airflow'}, schedule_interval=None, start_date=days_ago(2))
def tutorial_taskflow_api_etl():
   @task
   def extract():
       return {"1001": 301.27, "1002": 433.21, "1003": 502.22}

   @task
   def transform(order_data_dict: dict) -> dict:
       total_order_value = 0

       for value in order_data_dict.values():
           total_order_value += value

       return {"total_order_value": total_order_value}

   @task()
   def load(total_order_value: float):

       print("Total order value is: %.2f" % total_order_value)

   order_data = extract()
   order_summary = transform(order_data)
   load(order_summary["total_order_value"])

tutorial_etl_dag = tutorial_taskflow_api_etl()
```

* v1の場合の例

```python:tutorial.py
from datetime import datetime, timedelta
from textwrap import dedent

# The DAG object; we'll need this to instantiate a DAG
from airflow import DAG

# Operators; we need this to operate!
from airflow.operators.bash import BashOperator
# These args will get passed on to each operator
# You can override them on a per-task basis during operator initialization
default_args = {
    'owner': 'airflow',
    'depends_on_past': False,
    'email': ['airflow@example.com'],
    'email_on_failure': False,
    'email_on_retry': False,
    'retries': 1,
    'retry_delay': timedelta(minutes=5),
    # 'queue': 'bash_queue',
    # 'pool': 'backfill',
    # 'priority_weight': 10,
    # 'end_date': datetime(2016, 1, 1),
    # 'wait_for_downstream': False,
    # 'dag': dag,
    # 'sla': timedelta(hours=2),
    # 'execution_timeout': timedelta(seconds=300),
    # 'on_failure_callback': some_function,
    # 'on_success_callback': some_other_function,
    # 'on_retry_callback': another_function,
    # 'sla_miss_callback': yet_another_function,
    # 'trigger_rule': 'all_success'
}
with DAG(
    'tutorial',
    default_args=default_args,
    description='A simple tutorial DAG',
    schedule_interval=timedelta(days=1),
    start_date=datetime(2021, 1, 1),
    catchup=False,
    tags=['example'],
) as dag:

    # t1, t2 and t3 are examples of tasks created by instantiating operators
    t1 = BashOperator(
        task_id='print_date',
        bash_command='date',
    )

    t2 = BashOperator(
        task_id='sleep',
        depends_on_past=False,
        bash_command='sleep 5',
        retries=3,
    )
    t1.doc_md = dedent(
        """\
    #### Task Documentation
    You can document your task using the attributes `doc_md` (markdown),
    `doc` (plain text), `doc_rst`, `doc_json`, `doc_yaml` which gets
    rendered in the UI's Task Instance Details page.
    ![img](http://montcs.bloomu.edu/~bobmon/Semesters/2012-01/491/import%20soul.png)

    """
    )

    dag.doc_md = __doc__  # providing that you have a docstring at the beginning of the DAG
    dag.doc_md = """
    This is a documentation placed anywhere
    """  # otherwise, type it like this
    templated_command = dedent(
        """
    {% for i in range(5) %}
        echo "{{ ds }}"
        echo "{{ macros.ds_add(ds, 7)}}"
        echo "{{ params.my_param }}"
    {% endfor %}
    """
    )

    t3 = BashOperator(
        task_id='templated',
        depends_on_past=False,
        bash_command=templated_command,
        params={'my_param': 'Parameter I passed in'},
    )

    t1 >> [t2, t3]
```

### 4. DAG の Cloud Storage へのアップロード
* DAGファイルをCloud Storageのdagsフォルダにアップロードすると、ファイルがCloud Composerによって解析される

```bash
gsutil cp ./example_v2_dag.py <DAGs_folder_path>
```

* -> （参考）コンソールからDAGを確認

![スクリーンショット 2021-12-26 13.57.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92368/a26f052a-3ac7-bbab-e83b-b360c7e6a0b6.png)


## DAGの書き方は？
### 基本編
上記 `example_v2_dag.py` 参照（Airflow 2.0対応の書き方）

* ①ライブラリのインポート
    * airflow自体のライブラリなどのインポートなどをする

* ②デフォルト引数の定義
    * タスクを実行するために必要となる、DAGのデフォルト引数を定義する
    * DAG内の全オペレータに共通して設定するパラメータを定義する
    * airflow.models.BaseOperator

* ③DAGの作成
    * DAG自体を作成する。DAG本体の名前や説明、どの間隔でタスクを実行するかを定義する
    * airflow.DAGのコンストラクタで設定できる引数の一覧と説明は、airflow.models.dag.DAGのドキュメント参照

* ④タスクの宣言
    * 実際に実行するタスク (ジョブ) を宣言する。実行するすべてのタスクを宣言して、前の手順で作成したDAGの一部とする

```python
t1 = BashOperator(
    task_id='print_date',
    bash_command='date',
    dag=dag,
)
```

* ⑤依存関係の記載

```bash:v1
# ワークフロー　t2タスクの上流にt1をセット
t2.set_upstream(t1)
# ワークフロー　t3タスクの上流にt1をセット
t3.set_upstream(t1)

# ワークフローは下記のような記述もできる
#t1.set_downstream([t1, t3])
#t1 >> [t2, t3]
#[t2, t3] << t1
```

### 応用編
* 外から変数を渡したい時はどうするか
    * ユースケース
        * バッチによってはパラメータ付きで手動実行したい
        * 一時的なデータ投入・更新バッチのために毎回DAG定義を書くのではなく、汎用的なDAGを用意してパラメータで実行するコマンドを動的に切り替えできると便利（日付など）
    * 渡す側
        * 1. CLIからパラメータ付き実行
            * `airflow trigger_dag -c '{"key1":1, "key2":2}' dag_id`
        * 2. GUIからパラメータ付き実行
            * `ヘッダのBrowse -> DAG Runs -> + アイコンクリックで新規DAG Run作成画面に遷移する`
        * 3. REST API
            * `$ curl -X POST \
   http://localhost:8080/api/experimental/dags/<DAG_ID>/dag_runs \
   -H 'Cache-Control: no-cache' \
   -H 'Content-Type: application/json' \
   -d '{"conf":"{\"key\":\"value\"}"}'`

    * -> 渡される側（オペレータ） ①template文字列内で受け取る
 
        ```python
        bash_task = BashOperator(
            task_id="bash_task",
            bash_command='echo "Here is the message: '
                         '{{ dag_run.conf["json_key"] }}" ',
            dag=dag,
        )
        ```

    * -> 渡される側（オペレータ） ②context経由で受け取る
        * Operator内部のcontextという変数にdag_runが入っている
 
        ```python
        def print_value(**context):
            print(context['dag_run'].conf['json_key'])

        PythonOperator(
            python_callable=print_value,
        )
        ```

* スクリプト（bash, SQLなど）ファイルを各オペレータから読み込みたい時はどうするか
    * 実行するQueryを記述したSQLファイルは、 GCSのdags/sql配下にまとめて管理

```python
transform_1 = bigquery_operator.BigQueryOperator(
   task_id="transform_1",
   sql="sql/query_1.sql",
   destination_dataset_table="{{params.sample_table}}",
   write_disposition="WRITE_APPEND",
)
```

## 実行結果確認方法は？
### 実行結果ステータス
![スクリーンショット 2021-12-26 13.59.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92368/1df3b84a-3476-8bca-6afa-24b6cf437fcb.png)

![スクリーンショット 2021-12-26 13.58.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92368/403fcb6b-853e-101f-d1b5-55ebf392da55.png)

### 実行結果ログ
![スクリーンショット 2021-12-26 14.00.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92368/0d7f4a8a-66b4-28f3-312a-15e1170aced5.png)

## アラート通知方法は？
### エラー
* メール通知は `default_args`の通り
* Slack通知は[AirflowのDAGの実行状況をSlackに通知して監視する](https://note.com/dd_techblog/n/n7c3fc2559ff2)参照
    * Operatorのパラメータに on_success_callback または on_failure_callback を設定しておくとタスクの成功/失敗時に指定した関数を実行することができるので、ここにタスクの成功/失敗時にそれをSlackに通知する関数を指定しておく。

### 遅延
* airflowは各Task に対してSLA 値(時間)を設定できる
    * メール通知の場合は下記の通り。sla_miss_callback設定も可能

```python
t1 = PythonOperator(
    task_id='sla_test',
    python_callable=example_call,
    sla=timedelta(seconds=10),
    email="xxx.yyy@example.com",
    dag=dag
)
```

## リトライ方法は？
* [AirflowのTips 11選 ](https://future-architect.github.io/articles/20200131/#2-%E3%83%AA%E3%83%88%E3%83%A9%E3%82%A4)参照
    * コンソール画面からClearする（Clear=リトライなので注意。Clearを押下すると、すぐにリトライが始まる）

![スクリーンショット 2021-12-26 14.34.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92368/c595962e-ed85-5f4c-0e9e-400b44ed3963.png)

## 権限管理方法は？
* ログインユーザ
    * Userinfo
        * User Name: accounts.google.com:1234XXXXXXXX
        * Role: [Op]
    * Personal Info
        * First Name: tanakataro @ gmail.com
        * Email: tanakataro @ gmail.com

* Composerの認証

> Google Cloud クライアント ライブラリでは、アプリケーションのデフォルト認証情報（ADC）と呼ばれる方式を使用してアプリケーションの認証情報を検出している。クライアント ライブラリが使用される際は以下の順番で認証情報がチェックされます。
1. 環境変数 GOOGLE_APPLICATION_CREDENTIALSを確認
2. 環境変数が設定されていない場合、ADC ではサービスで実行されているアプリケーションに応じて、Compute Engine、Google Kubernetes Engine、Cloud Run、 App Engine、Cloud Functions によって提供されているデフォルトのサービス アカウントを使用する。 

* ウェブサーバ

> Airflow ウェブサーバーは、Identity-Aware Proxy と統合されています。Cloud Composer は IAP インテグレーションの詳細を隠し、ウェブサーバーへのアクセスに十分な権限を持つユーザー アカウントに、ウェブサーバーへのアクセスを提供します。

## スケール方法は？
* 実質GKEと同じ
* workerとなっているGKEのノードはauto scalingが有効になっていない。 -> composer作成時に決めたノードで、適切にジョブの計算資源を管理し、他Podの兼ね合いも考慮するなら、kubernetes_pod_operatorを用いてリミットを設定した上でジョブを実行させるようにする必要がある

## コストは？
* ほぼ最小構成でも約月4万円/月
![スクリーンショット 2021-12-26 14.15.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/92368/ed54756a-d954-f155-e32c-86266a4bac50.png)

## その他、気を付けることは？
* 実行時刻の考え方
    * `start_date + schecule_interval` 経過後に初回DAG実行
    * 過去分が実行されるので注意
        * intervalの設定を12時間ごとにしてstart_dateを２日前にしてDAGを実行すると、2日前の12時,24時,1日前の12時,24時の分も実行される
        * 実行したくない場合は、 catchup=False 指定により回避する

```python
dag = DAG(
    'blog_example_execution_date',
    schedule_interval=timedelta(minutes=1),
    start_date=datetime(
        2020, 1, 29, 15, 30, tzinfo=pendulum.timezone('Asia/Tokyo')), # 明示的にタイムゾーンを指定
)

...
# execution_date = 2019/01/29 15:30:00 +09:00
#   => DAG に指定した start_date と同一

# start_date = 2019/01/29 15:31:00 +09:00
#   => DAG に指定した start_date に schedule_interval を足した日時と同一
```

* メンテナンスウィンドウに注意する
* Variable: GUI上からトークンなどを設定できる。key-valueで取得できる。
    * `bucket=Variable.get('sample_gcs_bucket'),`
* 接続情報の管理（DBなど）は、Secret Managerを利用する
* connection: Airflow needs to know how to connect to your environment. Information such as hostname, port, login and passwords to other systems and services is handled in the Admin->Connections section of the UI. The pipeline code you will author will reference the ‘conn_id’ of the Connection objects.

```bash
airflow connections add 'my_prod_db' \
    --conn-type 'my-conn-type' \
    --conn-login 'login' \
    --conn-password 'password' \
    --conn-host 'host' \
    --conn-port 'port' \
    --conn-schema 'schema' \
```

```python
postgres_query = PostgresOperator(
    task_id="query_one",
    postgres_conn_id=<my_postgres_conn_id>,
    sql=<my_sql_statement>,
    autocommit=True,
)
```

# 参考
* [Cloud Composer](https://cloud.google.com/composer)
* [Cloud Composerでデータ基盤のワークフローを作る](https://medium.com/eureka-engineering/data-mgmt-cloud-composer-29ba3fcbffe0)
* [Apache Airflow 2.0 is here!](https://airflow.apache.org/blog/airflow-two-point-oh-is-here/)
* [Apache Airflow で DAG ファイルを登録する](https://zenn.dev/ymasaoka/articles/register-dag-with-apache-airflow)
* [AirflowでDAG実行時にGUI, CLI, REST APIからパラメータを渡す](https://ohbarye.hatenablog.jp/entry/2020/10/18/airflow-parameterized-dag-run)
* [Airflow の SLA設定方法](https://future-architect.github.io/articles/20200827/)
* [Cloud Composer の辛さと、それに負けない開発フローの構築 ~Composer はなぜ辛い?編~](https://note.com/jdsc/n/n3657da5d8206)
* [GCP Cloud Composerを使いこなすまでの道のり](https://qiita.com/minarai/items/28b4269736ea0df136ff)