IBM CloudのToolchainを利用してKubernetesへdeployment以外をdeployする

## IBM CloudのToolchain

IBM CloudではGitLabやCI/CDの仕組みを組み合わせた[DevOps Toolchain](https://cloud.ibm.com/docs/apps?topic=apps-devops-toolchains&locale=ja)を提供しています。

ソースのビルドからコンテナ化、Kubernetes/OpenShiftへのDeployにも対応しているのですが、対象がCronJobとしてDeployしようとした場合などに問題があったので対応方法をまとめました。

## ToolchainによるKubernetesへのdeploy

このDevOps ToolchainはKubernetesやOpenShiftへのdeployに対応しています。
Kubernetesへのdeployの場合、以下の3ステージでdeployが行われます。

+ BUILD

  gitからのソースの取得

+ CONTERNALIZE

  Dockerfileを利用したコンパイルやコンテナイメージの作成とContainer Registryへの登録

+ DEPLOY

  yamlファイルを利用したPod(コンテナ)のdeploy

## 標準のdeployスクリプトで正しく動かない場合

Webアプリなどで、作成したコンテナイメージをdeploymentをdeployする場合には大変便利なのですが以下のように少し変わった処理を行うと、そのままでは動きません。

+ CronJobやDaemonSetなどのDeployment以外のDeploy

    デフォルトではこのToolchainでのdeployはDeploymentにのみ対応しています。
    これ以外をDeployの対象としてもそのままではうまくいきません。

+ 既存コンテナイメージのDeploy

    「CONTERNALIZE」ステージを行わず、Container Registryへ登録済みのイメージや、docker.ioなどの既存イメージをdeployするのは失敗してしまいます。

+ 複数のDeploymentのDeploy

    未検証ですが、ビルドした一つのコンテナイメージを別のDeploymentとして登録する場合（複数namespaceへの同時deploy)もうまくいかないものと思われます。

これらは

+ 「DEPLOY」ステージでのYAMLファイルの更新に失敗するため、もしくは
+ Deploy結果の成否判定に失敗するため

に発生します。

## 「DEPLOY」ステージでのYAMLファイルの更新の問題

Deployment以外のdeployと、既存イメージのdeployが共にうまくいかない理由は、Deployの際に行われるYAMLファイルの更新がdeploymentのみを想定しているためです。

「CONTERNALIZE」ステージでビルドされたimageはタイムスタンプを利用したバージョン番号が自動で付与されてContainer Registryへ登録されます(latestなどの別名は付与されません)。
そのため、ビルド・格納されたimageをDeployの対象とするためには、deploy内容を定義するYAMLファイルを動的に更新する必要があります。

「DEPLOY」ステージは、`https://jp-tok.git.cloud.ibm.com/open-toolchain/commons/-/raw/main/scripts/check_and_deploy_kubectl.sh`というスクリプトが主な処理を行っており、YAMLの更新もこのスクリプト内で行われます。更新処理は以下のように行われます。

```
# find the yaml document index for the K8S deployment definition
DEPLOYMENT_DOC_INDEX=$(yq read --doc "*" --tojson $DEPLOYMENT_FILE | jq -r 'to_entries | .[] | select(.value.kind | ascii_downcase=="deployment") | .key')
if [ -z "$DEPLOYMENT_DOC_INDEX" ]; then
  echo "No Kubernetes Deployment definition found in $DEPLOYMENT_FILE. Updating YAML document with index 0"
  DEPLOYMENT_DOC_INDEX=0
fi
# Update deployment with image name
yq write $DEPLOYMENT_FILE --doc $DEPLOYMENT_DOC_INDEX "spec.template.spec.containers[0].image" "${IMAGE}" > ${NEW_DEPLOYMENT_FILE}
DEPLOYMENT_FILE=${NEW_DEPLOYMENT_FILE} # use modified file
```

`DEPLOYMENT_DOC_INDEX`でdeploymentの定義を含む更新対象のYAML(`---`で連結されたYAMLの場合、どれが対象か)を特定し、`yq write`で更新をしています(`yq`は`jq`コマンドのYAML版のようなコマンドです。別途整理する予定です)

この更新処理に以下のような構成となっているため、Deployment以外のdeployと、既存イメージのdeployが共に失敗します。

1. 更新対象のYAML特定時に`"deployment"`を明示的に探しており、`CronJob`, `DaemonSet`などを想定していない。
1. 更新対象のYAML特定時および更新時に、複数の更新対象を想定していない
1. 更新する`image`のYAML内の場所が`"spec.template.spec.containers[0].image"`と、deploymentのYAMLを決め打ちとなっている。

    1. 対象のYAMLが特定されない(=deployment以外の場合)でも更新され、この場合には指定された位置にnodeが追加されてしまう
    1. Deploymentの対象が既存のコンテナイメージ(「CONTERNALIZE」ステージの登録結果でない)の場合、元のimage名のチェックを行わず、常に上書きしているため、Deployしたいイメージが置き換えられてしまう

## Deploy結果の成否判定失敗

上記の問題を修正すると、Deployはできるようになるのですが、Ｄｅｐｌｏｙ結果の成否判定に失敗するケースがあります。これは、「CONTERNALIZE」ステージでビルドされたイメージを使ったPodの有無をチェックするためです。

+ CronJobの場合、実行開始時刻がDeploy直後に指定されていた場合以外は、Podが存在しないため、Deployに失敗したと判定されてしまいます。
+ Deploymentの対象が既存のコンテナイメージ(「CONTERNALIZE」ステージの登録結果でない)の場合、「CONTERNALIZE」ステージでビルドされたイメージを使ったPodは存在しないため、Deployに失敗したと判定されてしまいます。

スクリプト自体を様々な用途を想定して作成するのは難しいので、これらはやむを得ない部分もあると考えています。しかし、ユーザーとしてはツールチェーンがつくれないと困ってしまうので前述したようなDeployへの対処例を以下に示します。

## 対処方法

対処としては、`https://jp-tok.git.cloud.ibm.com/open-toolchain/commons/-/raw/main/scripts/check_and_deploy_kubectl.sh`をダウンロードし、修正したものをgit内に取り込むか、デプロイ・スクリプトとして直接指定し、対応します

### CronJobやDaemonSetなどのDeployment以外のDeploy

- 「DEPLOY」ステージでのYAMLファイルの更新の問題への対処
　　　　　　
   汎用的なスクリプトとして対応するには、更新処理をDeploymentから、CronJobやDaemonSetなど必要な検索に修正し、各定義に合わせimageの位置を修正します。
もしくは、以下のようにsedなどでYAMLを直接書き換えます。ただし、以下の例ではYAML中にコンテナ・イメージ以外に「 image:　」という文字列が含まれていると正しく動きません。

```
# echo -e "Updating ${DEPLOYMENT_FILE} with image name: ${IMAGE}"
# NEW_DEPLOYMENT_FILE="$(dirname $DEPLOYMENT_FILE)/tmp.$(basename $DEPLOYMENT_FILE)"
# # find the yaml document index for the K8S deployment definition
# DEPLOYMENT_DOC_INDEX=$(yq read --doc "*" --tojson $DEPLOYMENT_FILE | jq -r 'to_entries | .[] | select(.value.kind | ascii_downcase=="deployment") | .key')
# if [ -z "$DEPLOYMENT_DOC_INDEX" ]; then
#   echo "No Kubernetes Deployment definition found in $DEPLOYMENT_FILE. Updating YAML document with index 0"
#   DEPLOYMENT_DOC_INDEX=0
# fi
# # Update deployment with image name
# yq write $DEPLOYMENT_FILE --doc $DEPLOYMENT_DOC_INDEX "spec.template.spec.containers[0].image" "${IMAGE}" > ${NEW_DEPLOYMENT_FILE}
# DEPLOYMENT_FILE=${NEW_DEPLOYMENT_FILE} # use modified file
sed -i -e "s,^\\( *image: \\).*$,\\1${IMAGE}," $DEPLOYMENT_FILE
cat ${DEPLOYMENT_FILE}
```

- Deploy結果の成否判定失敗への対処

　　DaemonSetの場合、Deploymentと同様にPodが生成されるため、特に対処は必要ありません。

　　CronJobの性質上、Podの状態で結果を判定することはできません。以下の判定処理をコメントアウトしするようにする必要があります。ツールチェーンが成功、となった場合でも、指定時刻での実行時にエラーとなる可能性があるので注意が必要です。
　　後続の処理では`if [ "$STATUS" == "fail" ]; then`で成否を判定しているため、STATUSにpassを明示的にセットする必要はありません。

```
# if kubectl rollout status deploy/${DEPLOYMENT_NAME} --watch=true --timeout=${ROLLOUT_TIMEOUT:-"150s"} --namespace ${CLUSTER_NAMESPACE}; then
#   STATUS="pass"
# else
#   STATUS="fail"
# fi
```

### 既存コンテナイメージのDeploy

書き換えや判定を正しく動かすには、コンテナイメージの名称が正しく認識されるよう「DEPLOY」ステージのステージ構成の環境プロパティー(デプロイスクリプトの環境変数に設定されます)に値を追加します。

コンテナイメージの名称(環境変数IMAGE)は`check_and_deploy_kubectl.sh`では以下のように構築されます。

```shell
if [ -z "${IMAGE_MANIFEST_SHA}" ]; then
  IMAGE="${REGISTRY_URL}/${REGISTRY_NAMESPACE}/${IMAGE_NAME}:${IMAGE_TAG}"
else
  IMAGE="${REGISTRY_URL}/${REGISTRY_NAMESPACE}/${IMAGE_NAME}@${IMAGE_MANIFEST_SHA}"
fi
echo "IMAGE $IMAGE"
```

従って、`REGISTRY_URL`,`REGISTRY_NAMESPACE`,`IMAGE_NAME`,`IMAGE_TAG`(または`IMAGE_MANIFEST_SHA`)を環境プロパティーに追加することで、YAMLの`image:`の書き換えやDeploy結果の成否判定が正しく行われるようになります。

### 複数のDeploymentのDeploy

「CONTERNALIZE」ステージでビルドされたimageを複数同時にDeployする(別namespaceに同時にDeployするなど)場合には、「DEPLOY」ステージに対し「ステージのクローン」を行い、それぞれの「DEPLOY」ステージでdeployに利用するYAMLを個別に指定します。

単一の「DEPLOY」ステージでで複数のDeployを行う(一つのYAMLに複数のdeploymentを定義する)のは、`DEPLOYMENT_DOC_INDEX`をすべて見つけられるようにし、すべて更新するように直す必要があり、結果判定も難しいため、おすすめしません。