OpenShift on IBM Cloud でCI/CDのツールチェーンを動かしてみた

IBM Cloud のOpenShiftをデプロイを開始した後の応答ページに表示される継続的デリバリーの有効化について検証した結果である。Red Hat OpenShiftが、IBM Cloudのサービスに加わる以前から、IKSとツールチェインを連携して、DevOpsの環境を作ることができていた。今回は、OpenShift on IBM Cloud がGAとなったので、従来のKubernetesとツールチェーンの組合せケースを思い描きながら、検証を進めていく。

![Enabling_ToolChain.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42829/ad981360-f852-e9da-f7c8-7f4b43452504.png)

従来からのKubernetesサービスとOpenShiftを区別するために、前者をIKS-K8s、そして、後者をIKS-OSと表すことにする。将来、この二つのサービスは分離されたり、IKS-K8sがサービス停止も考えられるが、2019年8月の時点では、この様に区別して扱う。


## IKS-OSクラスタのデプロイ

[IBM Cloud](https://cloud.ibm.com/) のポータル画面のハンバーガーメニューの中から、OpenShiftをクリックして、設定項目をインプットして、デプロイを始める。

![OpenShift_Cluster_S.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42829/8deb7ca6-4ef5-fe1b-3cd2-5d4257a17f9b.png)


ブラウザに表示される情報で、簡単にOpenShiftのクラスタを、デプロイできる。そのため、詳しい説明はしない。今回のクラスタ構成は、最小のK8sノード x1、場所は TOK05 一箇所という、開発環境を想定した構成で検証することにした。


## ツールチェーンとは

ツールチェーンは、もともと IBM Cloud に備わるものであり、次の画面コピーのように、４つの機能を提供する。


* Issues: 課題トラッキング、コードの修正要求など、GitLabの機能
* Git: ソースコードのバージョン管理、参照と修正
* Eclipse Orion: ブラウザ上で利用できるエディタでGitと統合され、編集、コミット、プッシュの機能がある
* Delivery Pipeline: Git Pushなどのイベントを契機にして、コンテナのビルド、サービス開始の機能を提供する。このようなツールとして Jenkins が有名であるが、これはIBMが開発したツールである。

![ToolChain.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42829/56f10e88-33d4-6083-c09c-85907fe244f8.png)


デリバリーパイプラインは、次の画面コピーを見れば直感的に理解できると思うが、それぞれのステージの概要を箇条書きにした。

* ビルド: 例えば、Javaであれば、ソースコードからビルドして、実行可能な形式に変換。例えば ear,war,jarなどの形式。
* コンテナ化: 実行可能なアプリケーションを、コンテナイメージに変換して、コンテナレジストリに登録。
* デプロイ: Kubernetesクラスタ、OpenShiftクラスタに、マニフェストを適用して、コンテナをデプロイする
* サービス: デプロイしたコンテナを、サービスを通じて、K8sクラスタ外部または、内部に公開、すなわち、アクセス可能にする

![Delivery-PipeLine.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42829/4821267f-7564-1b42-b241-7cc01c25303e.png)


それぞれのステージに必要なシェルを実行して、段階を進めるが、デリバリーパイプラインが、テンプレートとなるシェルを提供するので、修正、または、利用する。



## ツールチェーンの作成

最もシンプルな例で、使い方を見ていく。前述の「ツールチェーンの有効化」をクリックして表示される画面の中から、デプロイメント・ターゲットの選択肢からKubernetesをチェックする。さらに「Kuberbetesアプリの開発」をクリックする。これで、ツールチェーンの最小構成を作るための画面が表示される。
![create_toolchain.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42829/9e383108-025e-6cfa-caed-65775f65a5cd.png)


ツールチェーン作成に必要な項目を入力して生成する。

![Input-to-create-tc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42829/785a05de-919b-5238-1e27-3f9cf7c7f65b.png)

入力項目の説明

* ツールチェーン名: デフォルトで長い名前が与られるので適当に修正する
* リージョン: IKS-OSクラスタのリージョンに合わせる
* リソース・グループの選択: IKS-OSクラスタを作ったリソースグループを指定
* Select a source provider: Git Repo and Issue Tracking の他にGitHub,GitLabなども選択可
* アプリ名: この画面コピーの例では、長すぎてデプロイ時にエラーとなった、適当な長さに訂正する
* IBM Cloud APIキー: IAMで管理されるキーを「作成ボタン」をクリックして生成する
* コンテナレジストリーのリージョン: 遠いと時間がかかるので Tokyo を指定
* コンテナレジストリの名前空間: 事前にコンテナレジストリで名前空間を作っておいてセット
* クラスター・リージョン: IKS-K8sやIKS-OSクラスタの場所を指定
* リソース・グループ: クラスタの作成場所を指定
* クラスター名: OpenShiftのクラスタを作った時の名前をセット
* クラスターの名前空間: デプロイするK8sの名前空間

続き
![Input-to-create-tc-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42829/50e1ee2a-b177-4ec7-5efe-ea43d9d65b02.png)

* 所有者名: デフォルトの値を利用
* リポジトリー名: 長いので適当に短くしておくと読みやすい
* チェックボックス: すべてチェック

以上の項目をセットして「作成」ボタンとクリックして、ツールチェーンを作る。
これで以下のようなツールチェーンの画面が表示される。

![complete_tc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42829/ea3b6678-84f2-caec-d309-65de1718ef14.png)


Gitには、サンプルのソースコードがセットされ、自動的にデリバリーパイプラインが実行される。結果を確認してみる。


## トラブルシューティング#1 コンテナ化

IKS-K8sでは問題なくデプロイまで完了していたが、コンテナ化のステージが赤色になり、不具合により途中で止まったことがわかる。ここから、OpenShiftでサンプルアプリを動かすために、原因の調査と対応を行う。

![tc-error-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42829/2714b7ea-91a7-9338-fb8c-4c3869e947c8.png)


「ログおよび履歴の表示」をクリックして、ログを表示する。 ログから停止した場所を目視で探すと以下のメッセージを発見した。原因は「The supplied image name is invalid.」のメッセージで、指定されたイメージ名は無効とのことだ。

~~~
jp.icr.io/takara/todo-tkr  1.0   532130a18d5d  takara  2 months ago   44 MB   No Issues   
jp.icr.io/takara/webpage   v1    f83b2ffd963a  takara  2 weeks ago    45 MB   7 Issues 

OK
==========================================================
BUILDING CONTAINER IMAGE: hello-containers-OpenShift:1-master-488f7e8c-20190807020627
+++ bx cr build -f ./Dockerfile -t jp.icr.io/takara/hello-containers-OpenShift:1-master-488f7e8c-20190807020627 .
FAILED
The supplied image name is invalid.
Correct the image name, and try again.
~~~

上記のログでは「bx cr build -f ...」の直後でエラーが発生している。コンテナ名の文字列に -OpenShift と加えたため、コンテナ名の最大文字列数を超えたようだ。コンテナの名前を短く修正したところ解決した。余計なことをして、失敗の原因を作った形だ（恥!）。



## トラブルシューティング#2 デプロイ

コンテナ化のステージは無事に完了することができたが、次のステージで、エラーが出てしまった。途中停止したのは、ジョブ「Deploy to Kubernetes」のステップだ。どうやらOpenShiftクラスタへのデプロイに失敗したということの様だ。

![tc-error-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42829/366a289d-b0fb-bdee-c75f-0b2f007faac7.png)


前回の問題判別と同じように「ログおよび履歴の表示」で問題発生箇所を探す。次のログには「error: timed out waiting for the condition」が記録されているので、デプロイが完了しなった事が読み取れる。

~~~
DEPLOYING using manifest
+++ kubectl apply --namespace prod -f tmp.deployment.yml
deployment.extensions/hello-app created
+++ set +x
CHECKING deployment rollout of hello-app

+++ kubectl rollout status deploy/hello-app --watch=true --timeout=150s --namespace prod
Waiting for deployment "hello-app" rollout to finish: 0 of 2 updated replicas are available...
error: timed out waiting for the condition
+++ STATUS=fail
+++ set +x
DEPLOYMENT FAILED

Finished: FAILED
~~~

上記のログの状況から、コンテナは実績があるハズであるから、OpenShiftに関わる環境に問題が推察される。そこで、ポッドの状態を確認したところ、STATUSが「CrashLoopBackOff」と表示され、この事から起動と停止を繰り返している事が読み取れる。

~~~
$ oc get pod
NAME                                READY     STATUS             RESTARTS   AGE
hello-app-866ff97966-n8r72          0/1       CrashLoopBackOff   9          23m
hello-app-866ff97966-rlskk          0/1       CrashLoopBackOff   9          23m
~~~

ポッドの中のコンテナの終了コードを確認したいので、次のコマンドで詳細を表示した。Exit Code = 1 で終了しており、プログラムの異常終了が「CrashLoopBackOff」に至った原因であると解った。

~~~
$ oc describe po hello-app-866ff97966-n8r72
Name:               hello-app-866ff97966-n8r72
Namespace:          default
Priority:           0
PriorityClassName:  <none>
Node:               10.193.10.58/10.193.10.58
Start Time:         Wed, 07 Aug 2019 11:25:13 +0900
Labels:             app=hello-app
                    pod-template-hash=4229953522
Annotations:        openshift.io/scc=restricted
Status:             Running
IP:                 172.30.192.53
Controlled By:      ReplicaSet/hello-app-866ff97966
Containers:
  hello-app:
    Container ID:   cri-o://98f216f997999c700209696741a6701058acf2e17c49651435df305b6afa23c1
    Image:          jp.icr.io/takara/hello-con:3-master-488f7e8c-20190807022246
    Image ID:       jp.icr.io/takara/hello-con@sha256:d2760df12580c37b8f1fc88b6ff6edddb73326a9d3afecc48c40c271654088a3
    Port:           80/TCP
    Host Port:      0/TCP
    State:          Waiting
      Reason:       CrashLoopBackOff
    Last State:     Terminated
      Reason:       Error
      Exit Code:    1
<以下省略>     
~~~

次に、コンテナが異常終了する原因を探るため、ポッドのログを表示して確認する。ポッドの一つを指定してログを表示したところ、そこにはスタックトレースが残っており、その切掛は「Error: listen EACCES: permission denied 0.0.0.0:80」である。これはサーバーポート TCP 80 をオープンしようとしたところ、権限が無いためにエラーで弾かれた結果である。

~~~
$ oc logs hello-app-866ff97966-n8r72
 Application Running on port80
events.js:180
      throw er; // Unhandled 'error' event
      ^

Error: listen EACCES: permission denied 0.0.0.0:80
    at Server.setupListenHandle [as _listen2] (net.js:1211:19)
    at listenInCluster (net.js:1276:12)
    at Server.listen (net.js:1364:7)
    at Function.app.listen (/app/node_modules/express/lib/application.js:533:24)
    at Object.<anonymous> (/app/app.js:25:5)
    at Module._compile (internal/modules/cjs/loader.js:777:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:788:10)
    at Module.load (internal/modules/cjs/loader.js:643:32)
    at Function.Module._load (internal/modules/cjs/loader.js:556:12)
    at Function.Module.runMain (internal/modules/cjs/loader.js:840:10)
Emitted 'error' event at:
    at emitErrorNT (net.js:1255:8)
    at processTicksAndRejections (internal/process/task_queues.js:77:11) {
  code: 'EACCES',
  errno: 'EACCES',
  syscall: 'listen',
  address: '0.0.0.0',
  port: 80
}
~~~

TCPポートの1024以下は、特権ポートと呼ばれ、rootでなけれbオープンする事ができない制限がある。IKS-OSの実行環境では、IKS-K8sよりも厳格に動作している事が伺われる。 このサンプルアプリのマニフェストを、Gitから読むと、nodeportで開いているので、内部公開ポートの番号は、移動しても良い事がわかったので、ソースコードを変更して、TCPポート 80 を 3000へ変更する。それに伴い、Dockerfile、K8sマニフェスト deployment.ymlも修正する。

ツールチェーンの画面から、Ecripse Orionを起動して、編集後、コミット、プッシュすると、再びデリバリーパイプラインが実行される。

![Olion_Git.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42829/4ac4d4f7-31b1-ea62-da34-2fec02bf915d.png)

これでデリバリーパイプラインは、最後までオールグリーンとなった。
![Complete_DP.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42829/75683201-fa9e-c1de-8f1a-ba8397ccf079.png)

ウェブアプリケーションを、NodePortで開いているため、コマンドラインを使って調べる必要がある。OpenShiftのコマンドラインを利用できるように、ログインして、アクセスに必要な情報を確認する。


## OpenShiftへのログイン

OpenShiftのクラスタのデプロイが完了したら、「OpenShift Web コンソール」を開いて、oc login のコマンドをコピーして、ターミナルで実行する。 必要なコマンドのインストールや、コマンドの実行方法は、この画面コピーの「アクセス」タブに表示されるので、案内に従って、ログインする。
![OpenShift_Console.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42829/1a5122a0-c5fa-794d-bc8b-3cabb740bc4c.png)

oc loginすることで、kubectl コマンドも利用可能な状態となる。

~~~
$ oc login https://c100-e.jp-tok.containers.cloud.ibm.com:31456 --token=*********
Logged into "https://c100-e.jp-tok.containers.cloud.ibm.com:31456"
<中略>
 
$ oc get node
NAME           STATUS    ROLES           AGE       VERSION
10.193.10.58   Ready     compute,infra   5h        v1.11.0+d4cacc0

$ kubectl get node
NAME           STATUS   ROLES           AGE   VERSION
10.193.10.58   Ready    compute,infra   5h    v1.11.0+d4cacc0
~~~


## トラブルシューティング#3 デプロイ後の問題判別

デプロイメントコントローラの管理下で、ポッドが２つ起動している事が読み取れる。ところが、サービスのオブジェクトが存在しない。 この deployment.yml には、デプロイメントとサービスの二つが記述されて、セパレータを「---」で二つのマニフェストが連結されているので、仕様どうり働けば、このような状態にはならない。

~~~
$ oc get pod -n prod
NAME                         READY     STATUS    RESTARTS   AGE
hello-app-84db685b9b-rh2ww   1/1       Running   0          3m
hello-app-84db685b9b-zr5gh   1/1       Running   0          3m

$ oc get svc -n prod
No resources found.
~~~

ツールチェーンのログを参照する限り、エラーは発生していないが、サービスのマニフェストが実行された形跡が無い。デリバリーパイプラインの中のデプロイメント・ステージで、利用されるシェル https://raw.githubusercontent.com/open-toolchain/commons/master/scripts/check_and_deploy_kubectl.sh を確認すると、元のマニフェストから、デプロイメント部分を切り出して新たなマニフェスト作っている事が読み取れる。


## サービスのステージ追加

シェルを追加して、コマンドを書き足しても良いが、既存のテンプレートは、出来るだけそのまま利用するために、サービスのマニフェストだけをデプロイするためのステージを追加することにした。ギヤーメニューから「ステージのクローン」を選択して、ステージを追加する。
![add_stage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42829/dbb6da27-b71b-35ec-32db-747317a71cbe.png)

コピーしたステージのボックの「構成」をクリックして設定を追加する。


ステージ名: サービス

入力タブ
* 入力タイプ: Gitリポジトリー
* ステージ・トリガー: 前のステージが完了したらジョブを実行

ジョブタブ
* ビルド・スクリプト: 最終行をコメントして、service.ymlのデプロイするcコマンドを追加
* 上記以外は変更なし

~~~
# source <(curl -sSL "https://raw.githubusercontent.com/open-toolchain/commons/master/scripts/check_and_deploy_kubectl.sh")

kubectl apply -f service.yml
~~~

ワーカータブ
* 変更なし

環境プロパティタブ
* 項目の変更は不要

最後に「保存」ボタンをクリックして完了。その後、「ステージの実行」のプレイボタンをクリックして実行する。
このステージが成功して、サービスがデプロイされていれ設定完了。


## デリバリーパイプラインの結果確認

ターミナルからIKS-OSクラスタの名前空間 prod のオブジェクトをリストして確認すると、目的のサービスも作られている。

~~~
$ oc get all -n prod
NAME                            READY     STATUS    RESTARTS   AGE
pod/hello-app-bf74b8c47-jt8kk   1/1       Running   0          5m
pod/hello-app-bf74b8c47-zp42r   1/1       Running   0          5m

NAME                    TYPE       CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
service/hello-service   NodePort   172.21.112.209   <none>        3000:31551/TCP   4m

NAME                        DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/hello-app   2         2         2            2           5m

NAME                                  DESIRED   CURRENT   READY     AGE
replicaset.apps/hello-app-bf74b8c47   2         2         2         5m
~~~


## インターネットからのアクセステスト

ツールチェーンによって、デプロイまでの工程が完了したので、ノードのIPアドレスを求めて、curlコマンドでアクセスして、サンプル・アプリケーションの応答を確認する。

~~~
$ bx ks workers oc-3
OK
ID                                               パブリック IP   プライベート IP   フレーバー           状態     状況    ゾーン   バージョン   
kube-bl52baot0n7h8glkhpr0-oc3-default-000001e2   165.**.**.*    10.193.10.58      b3c.4x16.encrypted   normal   Ready   tok05    3.11.129_1518_openshift   
~~~

ちょっと素っ気無い応答であるが、アプリケーションのポッドの応答が確認できた。

~~~
$ curl http://165.**.**.*:31037;echo
Welcome to IBM Cloud DevOps with Docker. Lets go use the Continuous Delivery Service
~~~

# まとめ

OpenShiftにJenkinsをデプロイしても、同じ事ができるが、ツールチェーンを利用すれば、Jenkinsを管理する事なく、マネージドのDevOpsの環境を手に入れる事ができるところがポイントである。また、ツールチェーンには、Jenkinsのような豊富なプラグインは無いので、Jenkinsのように、様々な応用はできないが、シンプルで目的を達成するために有用と考える。

OpenShiftとツールチェーンを組み合わせた利用では、発生した問題の原因を解決して、デリバリーパイプラインが動作するに至った。これら問題判別と対策を講じるために必要な知識は、OpenShift固有というより、Kubernetesのアドミニストレーションのスキルである。Kubernetesとツールチェーンを組み合わせた場合と比較して、OpenShiftクラスタの方が手間が掛かるなどは感じられなかった。 ここで遭遇した問題は、Kubernetesも常に進化しているため同じ事が予測されるためである。

今回、デリバリーパイプラインの最適化については検討してないが、本格的に利用するに当たっては、十分な考慮が必要と考える。

OpenShift特有の機能を積極的に利用して利便性を追求するか、それとも、出来るだけKubernetesのコア機能を中心に利用して、ポータビリティを維持するかは、今後、良く考えなくてならない課題であると感じた。