GCRにPUSHしたイメージをSpinnaker経由でGKEにデプロイするところまでのまとめ (2)

k8s自体も実際の業務用途で使ったことはないし、今回業務で本格利用するにあたって一度理解しておきたくやったことをまとめておく。

長いので２つに分けたうちの2つ目。

前回の記事は [こちら](https://qiita.com/keiichi-hikita/items/7360b626da29e28a7a8e)

今回の対象は以下。

4. Spinnaker on Google Cloud Platformのインストール
5. SpinnakerがPub/Sub経由でMessageをSubscribeし、GKEのイメージを更新す

途中 `Pub/Sub` 自体もあまり理解できてなかったので、その時のメモも込み。

# Spinnakerの導入

[Spinnaker on Google Cloud Platform をインストールして管理する](https://cloud.google.com/docs/ci-cd/spinnaker/spinnaker-for-gcp) をみながらSpinnakerを入れてみる。

まず自分はフリートライアルでGCPを使っていたので、デフォルトの流れだとクォータを突破できない。
そこで `spinnaker-1` というクラスタをインストールの過程で作ってもらうことにするが、クォータ突破可能なよう、設定を途中で修正する。


まず前に作ったクラスタは消しておく。

```
% gcloud container clusters delete test-cluster-1
```

チュートリアルに従って進めていくとCloud Shellが開くのでまずはそこから。

画面の右側にプロジェクトの選択をするようにガイドが出るので、まずはそれを選択して開始ボタンを押す。

すると実行すべきコマンドが出てくるのでそれをコピペしてCloud Shellに貼っていく。

まず `git config` する。

```
% git config --global user.email "[EMAIL_ADDRESS]"
% git config --global user.name "[USERNAME]"
```

設定ファイルを自動生成する。

```
% PROJECT_ID=[PROJECT-ID] ~/cloudshell_open/spinnaker-for-gcp/scripts/install/setup_properties.sh
```

`cloudshell edit` で設定ファイルが画面上部に表示されるので、項目を一部修正する。

```
% cloudshell edit ~/cloudshell_open/spinnaker-for-gcp/scripts/install/properties
export ZONE=us-central1-c
export REGION=us-central1
export GKE_MACHINE_TYPE=n1-standard-1
```

それ以外の設定を見ていると、Pub/Subもこんな感じで設定されている模様。

ちなみにPub/Subも「なんか送受信系のMQみたいなもん」くらいの理解だったので、この記事のもう少し後で理解のための検証結果をまとめている。

```
% gcloud pubsub subscriptions list
---
ackDeadlineSeconds: 10
expirationPolicy:
  ttl: 2678400s
messageRetentionDuration: 604800s
name: projects/[PROJECT-ID]/subscriptions/spinnaker-1-gcr-pubsub-subscription
pushConfig: {}
topic: projects/[PROJECT-ID]/topics/gcr
---
ackDeadlineSeconds: 10
expirationPolicy:
  ttl: 2678400s
messageRetentionDuration: 604800s
name: projects/[PROJECT-ID]/subscriptions/spinnaker-1-gcb-pubsub-subscription
pushConfig: {}
topic: projects/[PROJECT-ID]/topics/cloud-builds
```

なので `$DEPLOYMENT_NAME-gcr-pubsub-subscription` をSubscribeしておけばContainer Registryのイメージ変更にも対応できるはず。

Spinnakerインストールの開始。

```
% ~/cloudshell_open/spinnaker-for-gcp/scripts/install/setup.sh
```

うまくいくとログの最後にこんな感じで出る。

```
To https://source.developers.google.com/p/[PROJECT-ID]/r/spinnaker-1-config
   bf9a838..99b3894  master -> master
~/cloudshell_open/spinnaker-for-gcp/scripts/install
.  Installation complete. 
.  Sign up for Spinnaker for GCP updates and announcements: 
.    https://groups.google.com/forum/#!forum/spinnaker-for-gcp-announce 
```

## Deck(GUI)につなぐ

以下でポートフォワードをセットし・・・

```
% ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/connect_unsecured.sh
.  Locating Deck pod... 
.  Forwarding localhost port 8080 to 9000 on spin-deck-b8c7664cb-zz8xl... 
```

チュートリアルにあるボタンをクリックするとDeckの画面が出る。

## Spinnakerに外部から接続できるようにする


これを外部公開して直接操作できるようにする。

右側のチュートリアルに `Expose Spinnaker publicly` とあり、その下に以下のコマンドが書かれているので実行する。

```
% ~/cloudshell_open/spinnaker-for-gcp/scripts/expose/configure_endpoint.sh
```

しばらくすると以下のようなメッセージが出るが・・・

```
To manage your API, go to: https://console.cloud.google.com/endpoints/api/spinnaker-1.endpoints.[PROJECT-ID].cloud.goog/overv
iew?project=[PROJECT-ID]
.  Creating managed SSL certificate spinnaker-1-managed-cert for domain spinnaker-1.endpoints.[PROJECT-ID].cloud.goog... 
Created [https://www.googleapis.com/compute/beta/projects/[PROJECT-ID]/global/sslCertificates/spinnaker-1-managed-cert].
NAME                      TYPE     CREATION_TIMESTAMP             EXPIRE_TIME  MANAGED_STATUS
spinnaker-1-managed-cert  MANAGED  2020-04-22T21:13:49.849-07:00               PROVISIONING
```

この時点で画面の右側のチュートリアルが実は変わっていて、以降それに従って進めることになる。
（確かにチュートリアルにもそう書いてるけど、意味がわからんで10分くらい無駄にした）

チュートリアルに

**Configure OAuth consent screen**
Go to the OAuth consent screen.

とあるので、このリンクをクリックして進める。
アプリケーション名を `Spinnaker` として、あとはそのまま放置。

それがおわったら、次はCloud Shellの画面に戻って、同じく右側のチュートリアルに以下があるのでそちらに行く。
（ここも「戻るべきタイミングなのね」と理解するのに少し時間がかかった）

**Create OAuth credentials**
Go to the Credentials page and create an OAuth client ID.

アプリケーションの種類を `ウェブアプリケーション` として、それ以外はデフォルトのまま `作成` ボタンを押す。
このときに出た `クライアントID` , `クライアントシークレット` はメモしておく。

次に **Expose Spinnaker and allow for secure access via IAP** の下にあるコマンドを実行。
ここで先程メモしておいた値を使う。

```
% ~/cloudshell_open/spinnaker-for-gcp/scripts/expose/configure_iap.sh

Enter your OAuth credentials Client ID: <-- ここと
Enter your OAuth credentials Client secret: <-- ここ
```

しばらくすると以下のように表示される。

```
.  ACTION REQUIRED: 
.    - Navigate to: [URL1]
.    - Add [URL2] to your Web client ID as an Authorized redirect URI. 
```

ここでやるべきは、

- URL1に接続して画面を表示する
- 画面に存在する `承認済みのリダイレクト URI` にURL2を貼る（URL2の末尾にあった、 `:handleRedirect` まで含めて貼り付け

というものらしい。

で、この後しばらくはアクセスできないっぽいことが書いてある。

> This phase could take 30-60 minutes. Spinnaker will be inaccessible during this time.

なのでちょっと待ってみた。
で、待った結果、以下のように書かれているリンクからジャンプできた。

**Conclusion**
Connect to your Spinnaker installation [here](https://spinnaker-1.endpoints.[PROJECT-ID]-274504.cloud.goog/) .

**View Spinnaker Audit Log**
View the who, what, when and where of your Spinnaker installation [here](https://console.developers.google.com/logs/viewer?project=[PROJECT-ID]&resource=cloud_function&logName=projects/[PROJECT-ID]/logs/spinnaker1AuditLog&minLogLevel=200) .


# ちょっと寄り道してPub/Subを理解した時のメモ

Pub/Subの基本がわかってなかったので調べた中、自分としてなんとなく分かりにくかったのは、

- Topicを作り
- Subscriptionを作り
- Topicにメッセージを投げると
- SubscriptionからそのMessageを取得できる

という概念。
PublisherはSubscriptionにMessageを投げるのではなくTopicに投げるわけですね。

では、Container RegistryにイメージがPUSHされたときに、TopicにMessageを投げるということはできるのだろうか・・・と調べていたら、どうやらtopic=gcrしておけばmessageは勝手に飛ぶらしく、subscription作ってあればメッセージもpullできるらしい。
(**これが今回の肝なので...**)

例えばこう。

```
% gcloud pubsub topics list
---
name: projects/[PROJECT-ID]/topics/gcr // gcrというtopicはSpinnakerのインストールの過程で作られている

// gcr-triggers, gcr-triggers2というsubscriptionを事前に作っておいたのでそれだけ表示している
// Spinnakerインストール過程で作られたやつも本来は表示されるのだけどこの項目に関係ないので除外

% gcloud pubsub subscriptions list 
---
ackDeadlineSeconds: 10
expirationPolicy:
  ttl: 2678400s
messageRetentionDuration: 604800s
name: projects/[PROJECT-ID]/subscriptions/gcr-triggers
pushConfig: {}
topic: projects/[PROJECT-ID]/topics/gcr
---
ackDeadlineSeconds: 10
expirationPolicy:
  ttl: 2678400s
messageRetentionDuration: 604800s
name: projects/[PROJECT-ID]/subscriptions/gcr-triggers2
pushConfig: {}
topic: projects/[PROJECT-ID]/topics/gcr

% docker push gcr.io/[PROJECT-ID]/my-go-practice:2.0 // ためしにDockerイメージをPUSHしてみる
The push refers to repository [gcr.io/[PROJECT-ID]/my-go-practice]
040774806d36: Layer already exists
9f6b35a80fe1: Layer already exists
feae5ce26f02: Layer already exists
29a23d067e48: Layer already exists
1af88e92b20b: Layer already exists
d35c5bda4793: Layer already exists
a3c1026c6bcc: Layer already exists
f1d420c2af1a: Layer already exists
461719022993: Layer already exists
2.0: digest: sha256:5fda16907f68e49ba6d0e9826b24a09fc475d9c5d392285a76704d3bcafce192 size: 2209

% gcloud pubsub subscriptions pull --auto-ack gcr-triggers2 // gcr-triggers2を追加で作っていたのをPULL
(messageが表示)

% gcloud pubsub subscriptions pull --auto-ack gcr-triggers  // gcr-triggersもPULL。同じMessageが入っている
(messageが表示)
```

こんな具合でMessageを取得できる。

ちなみにpayloadはこんな感じ。（これが後々重要になってくる）

```json
{
    "action": "INSERT",
    "digest": "gcr.io/[PROJECT-ID]/my-go-practice@sha256:20737c1fd2b2a217d58a22d8209b026fd930988ba7b16b2f6a49ff6c4b65df2f",
    "tag": "gcr.io/[PROJECT-ID]/my-go-practice:4.0"
}
```

# Spinnaker上での設定を始める

[Spinnakerの管理](https://cloud.google.com/docs/ci-cd/spinnaker/spinnaker-for-gcp#manage_spinnaker) に従って進める。

ここ、チュートリアルを日本語で読むほうがわかりにくい気がする・・・。
GCPに慣れてない自分としてはピンと来なかったのだが、やることは、

- 普通にGCPのコンソールに入る
- Kubernetes Engine > アプリケーション と遷移
- 一覧に出ている `spinnaker-1` のリンクをクリックする

という流れ。

すると右側に `アプリケーション情報` があるので、そこから先はチュートリアルに書いてあるとおりの流れで追っかけられるようになる。


で、これだけやった。（チュートリアルのままだとパスが間違っている）

```
% ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/add_gke_account.sh
% ~/cloudshell_open/spinnaker-for-gcp/scripts/manage/push_and_apply.sh
```


# Pub/SubのPayloadの中から、イメージのタグを抜き出せるようにすればいいのかを調べておく

実際にはこのようなやり方にするかはわからないけど、最新のタグが付与された場合にRolling Updateするようにしてみようと思った。（検証なので）

Container RegistryにDocker ImageをPUSHした時のPub/Subメッセージのフォーマットは以下のようになることは↑で確認してある。

```json
{
    "action": "INSERT",
    "digest": "gcr.io/[PROJECT-ID]/my-go-practice@sha256:20737c1fd2b2a217d58a22d8209b026fd930988ba7b16b2f6a49ff6c4b65df2f",
    "tag": "gcr.io/[PROJECT-ID]/my-go-practice:4.0" <-- ここ
}
```

`digest` の部分をArtifactとして後続のタスクに渡してあげるにはどうすればいいのか・・・ということを調べた。


[Triggering on Pub/Sub Messages](https://www.spinnaker.io/guides/user/pipeline/triggers-with-artifactsrewrite/pubsub/)

をみると、

> Otherwise, you need to supply a translation template so that Spinnaker can translate the Pub/Sub payload into a Spinnaker artifact. To do this, create a Jinja template. Note that the output of the Jinja transform must be a JSON list of Spinnaker artifacts. The translation template itself can be any valid Jinja transform.

と書かれている。自分はこのページだけでは意味が理解できず少し苦労した。

最終的に [Artifacts in trigger payloads](https://www.spinnaker.io/reference/artifacts-with-artifactsrewrite/in-pipelines/#artifacts-in-trigger-payloads) でやっと理解できた。（ここにたどり着くのに時間がかかってしまった・・・）

つまり、

- 本来ArtifactはSpinnakerの指定のフォーマットで定義すべきだが
- そう出来ない時のために、Jinja Templateを使って、3rd partyなJSONからどのフィールドを引っ張ってくればいいかを定義できるよ

と書いてある。

つまりこんなJinja Templateを作ったとする。

```
% cat pubsub_payload_template.txt
[
  {
    "name": "gcr.io/[PROJECT-ID]/my-go-practice",
    "type": "docker/image",
    "reference": "{{ digest }}"
  }
]
```

- Pipelineに届いたJSONから(今回はPub/Subのメッセージに該当)
- `digest` フィールドを抜き出して
- Artifactのフォーマットに必要な `reference` にマップしてあげるよ

という意味になる。
(**ここに気づくまでに時間がかかった...**)


で、このように3rd partyなJSONからArtifactとして必要な情報を引き抜くのだが、それはデプロイを行うための `Stage` 側に定義するManifestでなんとかできるらしい。
(**自分的に一番わかりにくかったのがここ！！**)


# 試しにPipelineを作ってみる


基本的には [こちら](https://www.spinnaker.io/guides/user/pipeline/managing-pipelines/) の流れに従って実施。

Triggerはこんな感じに定義をしている。
おそらくここはGUIを見ていただければすぐわかる。


(trigger_pub_sub.png)


- Container RegistryからのPublishを受け取る
- Payload Constraints は、イメージをPUSHした時のメッセージの形式に従うように
- (本当は `digest` の有無をチェックすべきなので、 `Payload Constraints` に書いておくべき。いろいろ試してて変わったんですが伝わると信じる)

次のStage **(Deploy(Manifest))** は以下の様な感じで定義する。

(画像)

単に書いてあるマニフェストを投げるわけなのだが、このテキストフィールドの下の方にポイントがあるのでそこは別途解説。


**Manifest** というラベルのすぐ右にある、 `Copy from running infrastructure` をクリックすると今動いているクラスタからManifest引っ張ってこれるので、一旦自分はサンプル的にデプロイしてから、それを使ってManifest引いてきた。


# Trigger用のテンプレートを定義してそれをSpinnakerに食わせる

↑に書いた、 `3rd partyなJSONをSpinnakerのArtifact側に変換してあげるよ定義` をする。

まずテンプレートを作成する。
Cloud ShellとかでHomeに作れば楽。

```
% cat pubsub_payload_template.txt
[
  {
    "name": "gcr.io/[PROJECT-ID]/my-go-practice",
    "type": "docker/image",
    "reference": "{{ digest }}"
  }
]
```

これを `hal` コマンドにてSpinnakerに食わせる。

```
% hal config pubsub google subscription edit gcr-pub-sub --template-path $HOME/pubsub_payload_template.txt
```

こうすることで、Pub/Subが受け取ったJSONをArtifactに変更してくれるようになる。

# DeployするためのStageにおいてManifestを記載する

Deploy
既存の構成を引っ張ってくるとこんな感じになっていて、そこから一部だけ修正をする。

```yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  annotations:
    artifact.spinnaker.io/location: default
    artifact.spinnaker.io/name: hello-server
    artifact.spinnaker.io/type: kubernetes/deployment
    deployment.kubernetes.io/revision: '1'
    moniker.spinnaker.io/application: hello
    moniker.spinnaker.io/cluster: deployment hello-server
  generation: 1
  labels:
    app: hello-server
    app.kubernetes.io/managed-by: spinnaker-for-gcp
    app.kubernetes.io/name: hello
  name: hello-server
  namespace: default
  selfLink: /apis/extensions/v1beta1/namespaces/default/deployments/hello-server
spec:
  progressDeadlineSeconds: 600
  replicas: 1
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      app: hello-server
  strategy:
    rollingUpdate:
      maxSurge: 25%
      maxUnavailable: 25%
    type: RollingUpdate
  template:
    metadata:
      annotations:
        artifact.spinnaker.io/location: default
        artifact.spinnaker.io/name: hello-server
        artifact.spinnaker.io/type: kubernetes/deployment
        moniker.spinnaker.io/application: hello
        moniker.spinnaker.io/cluster: deployment hello-server
      labels:
        app: hello-server
        app.kubernetes.io/managed-by: spinnaker-for-gcp
        app.kubernetes.io/name: hello
    spec:
      containers:
        - image: gcr.io/[PROJECT-ID]/my-go-practice # <-- ここ
          imagePullPolicy: IfNotPresent
          name: my-go-practice
          resources: {}
          terminationMessagePath: /dev/termination-log
          terminationMessagePolicy: File
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      schedulerName: default-scheduler
      securityContext: {}
      terminationGracePeriodSeconds: 30
status: {}
```

`# <--ここ` となっているところだけ直したかもしれない。
（そのまま引っ張ってきただけでもこうなってたかもしれない）

とりあえず上記のように `image: gcr.io/[PROJECT-ID]/my-go-practice` と書いてあるところから、

```json
[
  {
    "name": "gcr.io/[PROJECT-ID]/my-go-practice",
    "type": "docker/image",
    "reference": "{{ digest }}"
  }
]
```

の `name` が引かれ、そこから `reference` に行き、 `digest` が特定され、それがデプロイされる・・・という流れで動くらしい。

[前回の記事](https://qiita.com/keiichi-hikita/items/7360b626da29e28a7a8e) のようにイメージを更新してPUSHすると、そのイメージがデプロイされるのがわかるはず。

例えば、以下のコードの一部を変えてGCRにPUSHしてみれば分かるはず。

```go
package main

import (
	"net/http"
)

func main() {
	http.HandleFunc("/", handler)
	http.ListenAndServe(":8080", nil)
}

func handler(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusOK)
	w.Write([]byte("Hello, world!\n"))
	w.Write([]byte("Version: 1.0.0\n")) // <-- ここを好きに変えてみる
}
```

動作確認。

```
% curl [EXTERNAL-IP]
Hello, world!
Version: 1.0.0
```

これでやりたかったことは出来た。

# まとめ

とにかく自分的に分かりにくかったのがArtifactの扱いと、それの反映の仕方。
実際はCloud Buildとかを交えつつコードのPUSHを契機にイメージを作っていくフェーズが間に入るのだろうけど、そこは別なやり方でやれるので今回は割愛。

似たようなことで悩んでいる方の参考になれば幸いです。