Databricks の CI （Continuous Integration（継続的インテグレーション）） パイプラインを Azure DevOps (Pipelines) にて構築する手順

## 概要

Databricks の CI （Continuous Integration（継続的インテグレーション）） パイプラインを Azure DevOps (Pipelines) にて構築する手順を紹介します。Databricks ノートブックで開発することを前提としており、Databricks Repos における Databricks Workspace 上ではノートブック形式となるが Git 上では Python ファイルとなる仕様に基づいたパイプラインとなっております。継続的な品質保証に必要となる、テスト結果の発行だけでなく、コードカバレッジの発行も実施できるようになっております。

本手順で構築できるパイプラインの概要は次のようになっております。

- カレントブランチのコードに対してテストを、PR 時や CI トリガーなどにより並列で実行が可能
- パイプライン実行の並列性を高めるために、テストケースの作成方法の工夫、Databricks Workflows 上でのテスト実行、及び、Azure Pipelines の エージェントレスジョブの活用を実施
- `tests/unit` 配下のテストケースのテストをDatabricks 上でノートブックとしてのテスト実行だけでなく、Python ファイルとしてのテスト実行も行うことで、パイプライン実行後にテスト結果とコードカバレッジの確認が可能

パイプラインの全体像を、次の画像で示します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/9590c907-a420-a1c4-005c-e5e47744bcc6.png)


Azure DevOps（Piplines）上で、テスト結果やコードカバレッジを下記画像のように確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/e9c96f5a-0858-0c12-142f-3da4d796cdde.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/7bd0b795-b436-7d17-7ef7-d1917dbde667.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/f59feb91-6845-eba2-a8b6-264b2d72eaba.png)

Azure DevOps のパブリックプロジェクトとして公開しており、下記の URL にてパイプラインの実行結果を確認できます。

https://dev.azure.com/azuredevopssub/public_databricks_cicd_pipelines/_build/results?buildId=7&view=results

本手順で利用するレポジトリーは、 Github 上に配置してあります。

https://github.com/manabian-/databricks_development_practices

## 本記事の位置付け

次の開発ガイドシリーズにおける DevOps 分野の1記事であり、リンク先には記事にて記事の全体像を整理している。

| GroupID | 分野               |
| ------- | ---------------------- |
| T10     | Spark概要                  |
| T20     | データエンジニアリング |
| T30     | データ品質チェック     |
| T40     | データサイエンス       |                                         |
| T50     | メタデータデプロイ     |
| T60     | テスト                 |
| **T70**     | **DevOps**                 |

https://qiita.com/manabian/items/67bcb6c5823bc4d5d944


## Azure Pipeline 定義
### 1. 処理概要

Databricks CI パイプラインとしては、次の処理を実施します。

1. ノートブックでの単体テストの実施
    - Databricks Repos を作成
    - Databricks Workflows(Notbook Type) でのテストの実施とテスト結果の発行
      - Databricks Workflows(Notbook Type) の作成と実行
      - Databricks Workflows 処理の完了確認    
      - テスト結果（JUnit 形式の XML ファイル）を Azure Pipelines に発行
1. Python スクリプトでの単体テストの実施
    - コードを DBFS にコピー
    - Databricks Workflows(Python script type)でのテストの実施とテスト結果の発行
      - Databricks Workflows(Python script type)の作成と実行
      - Databricks Workflows 処理の完了確認
      - テスト結果（JUnit 形式の XML ファイル）を Azure Pipelines に発行
1. 単体テストのコードカバレッジの発行
    - コードカバレッジ結果の統合と発行
      - DBFS からコードカバレッジ結果（.coverage）をコピー
      - コードカバレッジ結果の統合（ combine ）
      - コードカバレッジ結果を Cobertura 形式の XML ファイルへ変換
      - コードカバレッジ結果を発行
1. 事後処理
    - DBFS 上のコード等を削除
    - Databricks Repos 上のディレクトリを削除

### 2. 定義概要

Azure Pipelines にて`ci__execute_unit_test_on_databricks.yml` を CI パイプラインとして登録しており、複数の YAML ファイルを定義しています。YAML ファイルにおける処理フローと留意事項を下記に示します。

**ci__execute_unit_test_on_databricks.yml**

- 処理フロー
  - `templates/jobs/create_databricks_repos_for_test.yml` を呼び出し、Databricks Repos を作成
  - `templates/stages/execute_databricks_workflows_for_test_by_notebook_type.yml` を呼び出し、Databricks Workflows(Notbook Type) でのテストの実施とテスト結果の発行
  - `templates/jobs/copy_codes_to_dbfs.yml` を呼び出し、コードを DBFS 上にコピー
  - `templates/stages/execute_databricks_workflows_for_test_by_python_type.yml` を呼び出し、Databricks Workflows(Python script type)でのテストの実施とテスト結果の発行
  - `templates/jobs/publish_code_coverage.yml` を呼び出し、コードカバレッジ結果の統合と発行
  - `templates/jobs/delete_codes_on_dbfs.yml` を呼び出し、DBFS 上のコード等を削除
  - `templates/jobs/delete_codes_on_dbfs.yml` を呼び出し、Databricks Repos 上のディレクトリを削除
- 留意事項
  - パイプライン外部で定義する必要がある変数をコメントアウトしており、`databricks_dev`変数グループを定義することが前提
  - template により他 YAML ファイルを呼び出す
  - ノートブックでのテスト、および、Python スクリプトでのテストは複数バージョンの実施が可能

**templates/jobs/create_databricks_repos_for_test.yml**

- 処理フロー
  - Databricks repos API により、Databricks 上の Repos 上にディレクトリを作成
  - Databricks repos API により、Databricks 上の Repos を作成
  - Databricks repos API により、Databricks 上の Repos における、ブランチ、あるいは、タグを更新  
- 留意事項
  - 特になし

**templates/stages/execute_databricks_workflows_for_test_by_notebook_type.yml**

- 処理フロー
  - `templates/jobs/execute_databricks_notobook_task_workflows_for_unit_test.yml` を呼び出し、Databricks Workflows の作成と実行を実施
  - `templates/jobs/check_status_on_databricks_workflows.yml` を呼び出し、Databricks Workflows の完了を監視
  - `templates/jobs/publish_test_results_of_unittest.yml` を呼び出し、テスト結果のファイルを DBFS からコピーし、Azure Pipelines に公開
  - `templates/jobs/delete_databricks_workflows.yml` を呼び出し、Databricks Workflows を削除
- 留意事項
  - `/runs/submit` API が共有ジョブクラスターをサポートしていないため（[ドキュメントへのリンク](https://learn.microsoft.com/ja-jp/azure/databricks/workflows/jobs/jobs-api-updates#--runs-submit)）、Databricks Workflows の作成・実行・削除を実施

**templates/stages/execute_databricks_workflows_for_test_by_python_type.yml**

- 処理フロー
  - `templates/jobs/execute_databricks_python_task_workflows_for_unit_test.yml` を呼び出し、Databricks Workflows の作成と実行を実施
  - `templates/jobs/check_status_on_databricks_workflows.yml` を呼び出し、Databricks Workflowsの完了を監視
  - `templates/jobs/publish_test_results_of_pytest.yml` を呼び出し、テスト結果のファイルを DBFS からコピーし、Azure Pipelines に公開
  - `templates/jobs/delete_databricks_workflows.yml` を呼び出し、Databricks Workflows を削除
- 留意事項
  - `/runs/submit` API が共有ジョブクラスターをサポートしていないため（[ドキュメントへのリンク](https://learn.microsoft.com/ja-jp/azure/databricks/workflows/jobs/jobs-api-updates#--runs-submit)）、Databricks Workflows の作成・実行・削除を実施

**templates/jobs/execute_databricks_notobook_task_workflows_for_unit_test.yml**

- 処理フロー
  - Databricks REST API により、１タスクをもつ Databricks Workflows を作成
  - Databricks REST API により、前の手順で作成した Databricks Workflows を実行
- 留意事項
  - 特になし

**templates/jobs/execute_databricks_python_task_workflows_for_unit_test.yml**

- 処理フロー
  - Databricks REST API により、10タスクをもつ Databricks Workflows を作成
  - Databricks REST API により、前の手順で作成した Databricks Workflows を実行
- 留意事項
  - 特になし

**templates/jobs/check_status_on_databricks_workflows.yml**

- 処理フロー
  - Databricks REST API により、Databrikcs Workflows が成功（`SUCCESS`）したことを確認
  - Databrikcs Workflows が実行されない（`state.life_cycle_state`が `RUNNING` 、あるいは、`TERMINATED` とならない）場合にエラー終了とする
  - `SUCCESS`とならない場合には、エージェントレスジョブの `Delay` タスクにより指定分（`delay_for_minutes`パラメータ値）待機後に、再度ステータスを確認(最大20回)
  - 20回待機しても処理が完了しない場合にエラー終了となる
- 留意事項
  - カウンターによるループ処理を実施できないため、`job_names` パラメータ項目によるループ処理を実施

**templates/jobs/publish_test_results_of_unittest**

- 処理フロー
  - databricks-cli や pytest などの Python ライブラリをインストール
  - Databricks CLI にて、DBFS から テスト結果のファイルをコピー
  - `PublishTestResults`タスクにより、テスト結果を表示
- 留意事項
  - pytest により出力されるテスト結果ファイルの命名規則に合わせて発行を行う
  - テスト実施環境ごとにまとめるためにテスト環境のテスト結果ごとに発行処理を行う

**templates/jobs/publish_test_results_of_pytest.yml**

- 処理フロー
  - databricks-cli や pytest などの Python ライブラリをインストール
  - Databricks CLI にて、DBFS から テスト結果のファイルをコピー
  - `PublishTestResults`タスクにより、テスト結果を表示
- 留意事項
  - unittest-xml-reporting による出力されるテスト結果ファイルの命名規則に合わせて発行を行う
  - テスト実施環境ごとにまとめるためにテスト環境のテスト結果ごとに発行処理を行う

**templates/jobs/publish_code_coverage.yml**

- 処理フロー
  - databricks-cli や pytest などの Python ライブラリをインストール
  - Databricks CLI にて、DBFS から コードカバレッジ結果のファイルをコピー
  - `Coverage`コマンドにて、複数のコードカバレッジ結果ファイルを単一ファイルに統合し、Cobertura 形式の XML ファイルに変換
  - `PublishCodeCoverageResults`タスクにより、コードカバレッジ結果を表示
- 留意事項
  - `PublishCodeCoverageResults`タスクにて、複数ファイルの公開に対応していない（[ドキュメントへの](https://learn.microsoft.com/ja-jp/azure/devops/pipelines/tasks/test/publish-code-coverage-results?view=azure-devops#is-code-coverage-data-merged-when-multiple-files-are-provided-as-input-to-the-task-or-multiple-tasks-are-used-in-the-pipeline)）ため、単一ファイルに統合
  - カバレッジ生成対象ディレクトリ（`--cov`）とカバレッジ発行時の設定（`PublishCodeCoverageResults`における`pathToSources`）を同一のディレクトリに設定
  - Azure Pipelines 上で コードカバレッジ結果を確認する際には、日本語は文字化けする場合がある


**templates/jobs/delete_databricks_workflows.yml**

- 処理フロー
  - Databricks REST API にて、Databricks Workflows を削除
- 留意事項
  - `condition` を `always` に設定することで、パイプラインの成功有無に関わらず処理を実行

**templates/jobs/delete_codes_on_dbfs.yml**

- 処理フロー
  - Databricks REST API にて、DBFS 上に配置したファイルを削除
- 留意事項
  - `condition` を `always` に設定することで、パイプラインの成功有無に関わらず処理を実行

**templates/jobs/delete_databricks_respos_for_test.yml**

- 処理フロー
  - Databricks REST API にて、Repos 上に作成したディレクトリを削除
- 留意事項
  - `condition` を `always` に設定することで、パイプラインの成功有無に関わらず処理を実行

### 3. CI パイプラインにおける Databricks ランタイムの変更手順

#### 3-1. ノートブックでの単体テストにおける Databricks ランタイムの変更手順


**1. Databricks Runtime のバージョン変更**

`templates/stages/execute_databricks_workflows_for_test_by_notebook_type.yml` を呼び出す際の変数を変更する。

```yaml
  - template: templates/stages/execute_databricks_workflows_for_test_by_notebook_type.yml
    parameters:
      job_suffix_name: notebook_11_5
      databricks_cluster_runtime_version: 11.5.x-scala2.12
      test_results_path: $(TEST_CODE_PATH_ON_DBFS)/$(PIPELINE_ID)/.test_results/notebook_11_5
      delay_for_minutes: 5
```

依存関係がある `GetAndPublishCodeCoverage` ステージの `dependsOn` の項目（`GetAndPublishTestResults_*`）を変更する。

```yaml
  - stage: GetAndPublishCodeCoverage
    displayName: Get and publish code coverage
    condition: succeededOrFailed()
    dependsOn:
      - GetAndPublishTestResults_python_9_1
      - GetAndPublishTestResults_python_10_4
      - GetAndPublishTestResults_notebook_11_5
    jobs:
      - template: templates/jobs/publish_code_coverage.yml
```

**2. ノートブックでの単体テスト**


`templates/stages/execute_databricks_workflows_for_test_by_notebook_type.yml` を呼び出すテンプレートを追加。

```yaml
  - template: templates/stages/execute_databricks_workflows_for_test_by_notebook_type.yml
    parameters:
      job_suffix_name: notebook_11_5
      databricks_cluster_runtime_version: 11.5.x-scala2.12
      test_results_path: $(TEST_CODE_PATH_ON_DBFS)/$(PIPELINE_ID)/.test_results/notebook_11_5
      delay_for_minutes: 5
```

依存関係がある `GetAndPublishCodeCoverage` ステージの `dependsOn` に項目（`GetAndPublishTestResults_*`）を追加する。

```yaml
  - stage: GetAndPublishCodeCoverage
    displayName: Get and publish code coverage
    condition: succeededOrFailed()
    dependsOn:
      - GetAndPublishTestResults_python_9_1
      - GetAndPublishTestResults_python_10_4
      - GetAndPublishTestResults_notebook_10_4
      - GetAndPublishTestResults_notebook_11_5
    jobs:
      - template: templates/jobs/publish_code_coverage.yml
```

#### 3-2. Python スクリプトでの単体テストの実における Databricks ランタイムの変更手順


**1. Databricks Runtime のバージョン変更**

`templates/stages/execute_databricks_workflows_for_test_by_notebook_type.yml` を呼び出す際の変数を変更する。

```yaml
  - template: templates/stages/execute_databricks_workflows_for_test_by_python_type.yml
    parameters:
      job_suffix_name: python_11_5
      databricks_cluster_runtime_version: 11.5.x-scala2.12
      delay_for_minutes: 5
```

依存関係がある `GetAndPublishCodeCoverage` ステージの `dependsOn` の項目（`GetAndPublishTestResults_*`）を変更する。

```yaml
  - stage: GetAndPublishCodeCoverage
    displayName: Get and publish code coverage
    condition: succeededOrFailed()
    dependsOn:
      - GetAndPublishTestResults_python_10_4
      - GetAndPublishTestResults_python_11_5
      - GetAndPublishTestResults_notebook_10_4
    jobs:
      - template: templates/jobs/publish_code_coverage.yml
```

**2. ノートブックでの単体テスト**


`templates/stages/execute_databricks_workflows_for_test_by_notebook_type.yml` を呼び出すテンプレートを追加。

```yaml
  - template: templates/stages/execute_databricks_workflows_for_test_by_python_type.yml
    parameters:
      job_suffix_name: python_11_5
      databricks_cluster_runtime_version: 11.5.x-scala2.12
      delay_for_minutes: 5
```

依存関係がある `GetAndPublishCodeCoverage` ステージの `dependsOn` に項目（`GetAndPublishTestResults_*`）を追加する。

```yaml
  - stage: GetAndPublishCodeCoverage
    displayName: Get and publish code coverage
    condition: succeededOrFailed()
    dependsOn:
      - GetAndPublishTestResults_python_9_1
      - GetAndPublishTestResults_python_10_4
      - GetAndPublishTestResults_python_11_5
      - GetAndPublishTestResults_notebook_10_4
    jobs:
      - template: templates/jobs/publish_code_coverage.yml
```


## 構築手順
### 0. 事前準備
#### 1. 変数の設定について

変数グループ、あるいは、Pipeline にて、次の変数を設定。

| #    | 変数名                   | 設定値例                                                     | 概要 |
| ---- | ------------------------ | ------------------------------------------------------------ | ---- |
| 1    | DATABRICKS_WORKSPACE_URL | tps://adb-5555555555555555.19.azuredatabricks.net | Databricks Workspace URL を指定。    |
| 2    | DATABRICKS_TOKEN         | dapi1234                                                     | Databricks トークン を指定。     |
| 3    | TARGET_TEST_PATH         | tests/unit                                                | Python script type の Databricks Workflows を実行する際のテストケースを保持したディレクトリを指定。     |
| 4    | TEST_MAIN_NOTEBOOK_PATH  | tests/unit/main__ut__v1                                   | Notebook Type の Databricks Workflows を実行するノートブックのパスを指定。     |

次の変数は、YAML 上で定義しており、必要に応じて変更。

| #    | 変数名                   | 設定値例                                                     | 概要 |
| ---- | ------------------------ | ------------------------------------------------------------ | ---- |
| 5    | PIPELINE_ID | $(System.TeamProjectId)_$(System.DefinitionId)_$(Build.BuildNumber) | パイプライン実行時の一意の値を指定。     |
| 6    | TEST_CODE_PATH_ON_DBFS         | /FileStore/unit                                                     | テスト実行時に利用する DBFS 上のディレクトリを指定。     |

### 1. Databricks 上で Git 認証情報の登録

#### 1. Azure DevOps にて、`Personal access token` を取得

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/63452feb-1235-114e-65d8-8c547bcb9135.png)

#### 2. Databricks に接続後、右上のアカウント名 -> `User Setting` を選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/4b373481-b707-d27d-dbe7-7ca5a1cc0511.png)

#### 3. `Git integration` タブにて、Git の認証情報を設定

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/5e7eb80e-5569-c6c6-b240-d24177c09cca.png)

#### 4. Repos を追加できることを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/68a4b1b1-fda8-a914-3875-93f74225ef35.png)

### 2. 変数グループを作成
#### 1. Azure DevOps に接続後、`Library` -> `+ Variable Group`を選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/efb3631a-591c-c170-36da-609f38c42518.png)

#### 2. `Variable group name` にて、`databricks_dev` と入力

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/311950e5-5824-cf46-2ba7-6bb251e89bf0.png)

#### 3. `+ Add`を選択し変数を追加後 ,`Save` を選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/2a25c190-6e5a-bb0d-c364-f2e67050e9a9.png)

### 3. パイプラインを作成

#### 1. Azure DevOps に接続後、`Piplines` -> `create pipline` を選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/b1496ca2-c758-d8c8-a75f-47c92cc2de01.png)

#### 2. `Where is your code?` にて `Azure Repos Git` を 選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/9876dfea-13c1-5ee8-0436-7bfeb5e09b96.png)

#### 3. `Select a repository` にて、Azure pipelines の YAML 定義を保持しているレポジトリーを選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/1bb01102-0824-43aa-f948-00cd64f7ebed.png)

#### 4. `Configure your pipeline` にて、`Existing Azure Pipelines YAML file`を選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/74f66cfe-404b-b510-659c-590857dd6f42.png)

#### 5. `Select an existing YAML file`にて、`Branch` にて `main` を選択後、`Path` に`.azuredevops/ci__execute_unit_test_on_databricks.yml` と入力し、`Continue` を選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/4666aeed-8e0e-8c07-12ca-57a18a366329.png)

#### 6. `Variables` を選択し、変数を追加後、 `Save` を選択。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/2e97924e-e67b-86ff-85a4-c44c23cc3ab7.png)

#### 7. `Save` を選択。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/60462104-553f-6548-3aee-cc4e66c6ba1e.png)

### 4. パイプラインを実行
#### 1. 作成した pipelines の画面にて、`Run pipelines` を選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/06dbb193-84e3-3782-e2b5-e45abe03992a.png)

#### 2. `Branch/tag` にて `main` を設定後、`Run` を選択

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/1902a800-965b-2550-aba0-373f19faf091.png)

#### 3. すべての処理が完了し、`publish_test_results` がエラーとなる想定。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/3bf6fda6-d375-16db-3c49-a50af08f6001.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/7d71b3d1-4851-938c-c300-42f57683bda6.png)

#### 4. パイプラインの実行結果画面にて、`Tests` タブ、および、`Code Coverage` タブを確認

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/e9c96f5a-0858-0c12-142f-3da4d796cdde.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/7bd0b795-b436-7d17-7ef7-d1917dbde667.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/24031/f59feb91-6845-eba2-a8b6-264b2d72eaba.png)