[Azure] REST APIでZipファイルをデプロイ

# はじめに

Azure Web Appsにデプロイする方法はいろいろありますが、その中でREST APIを使用してZipファイルをデプロイする方法をやってみます。

[ZIP または WAR ファイルを使用した Azure App Service へのアプリのデプロイ](https://docs.microsoft.com/ja-jp/azure/app-service/deploy-zip#deploy-zip-file-with-rest-apis)

[Deploying from a zip file or url](https://github.com/projectkudu/kudu/wiki/Deploying-from-a-zip-file-or-url)

# REST APIでZipファイルをデプロイする処理

CircleCIの[再利用可能なコマンド](https://circleci.com/docs/ja/2.0/reusing-config/#%E5%86%8D%E5%88%A9%E7%94%A8%E5%8F%AF%E8%83%BD%E3%81%AA%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%81%AE%E3%82%AA%E3%83%BC%E3%82%B5%E3%83%AA%E3%83%B3%E3%82%B0)として実装していますが、デプロイ処理そのものはシェルスクリプトです。
REST APIを利用できる環境ならば適宜変更して利用できると思います。

<details><summary>REST APIでZipファイルをデプロイするCircleCIコマンド（全て）</summary><div>

```yml:./circleci/config.ymlから抜粋
version: 2.1

commands:
  azure_zip_deploy:
    description: "Azure ZIP Deploy"

    parameters:
      app:
        type: string
      user:
        type: string
      password:
        type: string
      zip:
        type: string
      async:
        type: enum
        enum: ["true", "false"]
        default: "false"

    steps:
      - run:
          name: Deploy to Azure
          command: |
            dir="./deploy"
            mkdir ${dir}

            deploy_result="${dir}/result.html"
            deploy_header="${dir}/header.txt"
            deploy_url="https://<< parameters.app >>.scm.azurewebsites.net/api/zipdeploy?isAsync=<< parameters.async >>"

            deploy_http_code=$(curl -s -X POST -u << parameters.user >>:<< parameters.password >> ${deploy_url} -T << parameters.zip >> -o ${deploy_result} --dump-header ${deploy_header} -w '%{http_code}')

            echo "Deploy HTTP code is ${deploy_http_code}."

            if [ ${deploy_http_code} -eq 200 ]; then
                echo "Deploy successful."
                exit 0
            elif [ ${deploy_http_code} -eq 202 ]; then
                echo "Deploy accepted."
            else
                echo "Deploy failed."
                exit 1
            fi

            pollable_url=""
            regex="^Location:\s(.+)$"

            while read line
            do
              if [[ ${line} =~ ${regex} ]]; then
                pollable_url=$(echo ${BASH_REMATCH[1]} | tr -d "\r\n" | tr -d "\n")
                break
              fi
            done < ${deploy_header}

            if [ ${pollable_url} = "" ]; then
              echo "Pollable URL not found."
              exit 1
            fi

            echo "Polling deployment status."
            polling_count=0
            polling_wait=30

            while true
            do
              polling_count=$((++polling_count))
              status_result="${dir}/status_${polling_count}.json"
              status_http_code=$(curl -sS -u << parameters.user >>:<< parameters.password >> ${pollable_url} -o ${status_result} -w '%{http_code}')

              echo "Deployment status HTTP code is ${status_http_code}."

              if [ ${status_http_code} -lt 200 -o ${status_http_code} -ge 300 ]; then
                  echo "Request status failed."
                  exit 1
              fi

              result=$(cat ${status_result})
              complete=$(echo ${result} | jq '.complete')
              status=$(echo ${result} | jq '.status')
              status_text=$(echo ${result} | jq -r '.status_text')

              if ${complete}; then
                if [ ${status} -eq 4 ]; then
                  break
                fi
                echo "Deploy failed."
                echo "${status_text}"
                exit 1
              else
                echo "${status_text}"
              fi
              sleep ${polling_wait}
            done

            echo "Deploy successful."

      - store_artifacts:
          path: ./deploy
```

</div></details>

# 解説

先程のコマンドを細かく解説していきます。

## パラメータについて

```yml
    parameters:
      app:
        type: string
      user:
        type: string
      password:
        type: string
      zip:
        type: string
      async:
        type: enum
        enum: ["true", "false"]
        default: "false"
```

| パラメータ | 値 |
|:-----------|:------------|
| app | Azure App Serviceのリソース名 |
| user | デプロイ ユーザ |
| password | デプロイ ユーザのパスワード |
| zip | zipファイルのパス |
| async | 非同期デプロイ |

デプロイに時間がかかるとタイムアウトする場合があります。（デプロイは継続されてます）
その場合は非同期でデプロイします。

## デプロイAPIへリクエスト

```bash:リクエスト
deploy_result="${dir}/result.html"
deploy_header="${dir}/header.txt"
deploy_url="https://<< parameters.app >>.scm.azurewebsites.net/api/zipdeploy?isAsync=<< parameters.async >>"

deploy_http_code=$(curl -s -X POST -u << parameters.user >>:<< parameters.password >> ${deploy_url} -T << parameters.zip >> -o ${deploy_result} --dump-header ${deploy_header} -w '%{http_code}')
```

curlコマンドでリクエストします。
レスポンスの本文とヘッダーはそれぞれファイルに保存し、HTTPステータスコードを変数に入れます。

```bash:HTTPステータスコード判定
if [ ${deploy_http_code} -eq 200 ]; then
    echo "Deploy successful."
    exit 0
elif [ ${deploy_http_code} -eq 202 ]; then
    echo "Deploy accepted."
else
    echo "Deploy failed."
    exit 1
fi
```

HTTPステータスコードが200の場合は「デプロイ成功」と判断して正常終了します。
非同期デプロイをした場合は202となるので「デプロイが受理された」として処理を続行します。
それ以外は「デプロイ失敗」でエラーとします。

## デプロイ ステータスURLを取得

非同期デプロイを行った場合はデプロイの結果を別途取得する必要があります。
そしてそのためのURLはヘッダー情報に含まれています。

> Add ?isAsync=true to the URL to deploy asynchronously. You will receive a response as soon as the zip file is uploaded with a Location header pointing to the pollable deployment status URL.

```bash
pollable_url=""
regex="^Location:\s(.+)$"

while read line
do
    if [[ ${line} =~ ${regex} ]]; then
        pollable_url=$(echo ${BASH_REMATCH[1]} | tr -d "\r\n" | tr -d "\n")
        break
    fi
done < ${deploy_header}

if [ ${pollable_url} = "" ]; then
    echo "Pollable URL not found."
    exit 1
fi
```

ファイルに保存しておいたヘッダーからURLを抽出します。
正規表現で`Location`を探して値を変数に入れます。（改行コード入ってしまったので削除しています）
URLの有無を確認していますが、`Location`が空だったケースは確認していません。

## デプロイ ステータスをポーリング

一定時間毎にリクエストして結果を確認します。
次の処理ではデプロイが終了していない場合、一定時間`sleep`して再度実行するようにしています。
デプロイ成功を確認したら`break`で`while`から出ます。

```bash:ポーリング
polling_count=0
polling_wait=30

while true
do
  polling_count=$((++polling_count))

  # リクエスト処理（後述します）

  sleep ${polling_wait}
done

echo "Deploy successful."
```

次はステータスをリクエストする処理です。

```bash:ステータスをリクエスト
  status_result="${dir}/status_${polling_count}.json"
  status_http_code=$(curl -sS -u << parameters.user >>:<< parameters.password >> ${pollable_url} -o ${status_result} -w '%{http_code}')
  
  echo "Deployment status HTTP code is ${status_http_code}."
  
  if [ ${status_http_code} -lt 200 -o ${status_http_code} -ge 300 ]; then
      echo "Request status failed."
      exit 1
  fi
```

先程抽出したURLにリクエストします。
レスポンスの本文をファイルに保存し、HTTPステータスコードを変数に入れます。
成功は200、処理中は202になるようなのでそれ以外はエラーとしています。

```bash
  result=$(cat ${status_result})
  complete=$(echo ${result} | jq '.complete')
  status=$(echo ${result} | jq '.status')
  status_text=$(echo ${result} | jq -r '.status_text')

  if ${complete}; then
    if [ ${status} -eq 4 ]; then
      break
    fi
    echo "Deploy failed."
    echo "${status_text}"
    exit 1
  else
    echo "${status_text}"
  fi
```

先程保存したレスポンス本文（JSON）から必要な値を取り出します。
詳しい説明が見つからなかったのですが[こちらのAPI](https://github.com/projectkudu/kudu/wiki/REST-API#deployment)と内容から「completeがtrueかつstatusが4」であればデプロイ成功と判断してよさそうです。

# 既知の問題

## オートスワップに関して

オートスワップとの競合により、デプロイ結果が403エラーになる現象が確認されました。
ポーリングとオートスワップは相性が悪く、ポーリングが終わってから明示的にスワップする必要があるようです。
（2020/10/01時点）

> Unfortunately, this is the combination (polling and autoswap) which does not work well. If you need to poll the status, either poll and do swap explicitly upon deployment completion (meaning no using auto-swap).

https://github.com/projectkudu/kudu/issues/3212#issuecomment-701912968

# 最後に

シェルスクリプトの経験が少ないのでheaderからLocationを抜き出したり、ポーリングしたりは苦労しました。
もっと良い書き方があればご指摘いただけると幸いです。