Go
GoogleCloudPlatform
kubernetes
GKE
stackdriver

GKE/GoでもStackdriver Debuggerを使いたい人生だった

eye-catch-kubernetes-400x4001.pngimages.png


tl;dr


  • GKE上で動作しているgolangアプリケーションでStackdriver Debuggerを使えるようにする

  • 公式のDebugger Agentがgoバイナリで提供されているが動作環境が 64ビットDebian Linuxイメージ となっているため嵌りポイントがあって試行錯誤した

(追記 2017/09/29)

Go1.9を使うため、image tagをgo:wheezyからgo:debianに更新しました。

詳しくはGCCBでGo1.9を使うを参照のこと。

(追記 2018/03/26)

Go用のエージェントがcd_go_agent.shからgo-cloud-debugに変わったので修正。


Stackdriver Debugger


Stackdriver Debugger を導入すれば、アプリケーションを止めたり、顧客に影響を及ぼしたりせずに、コードの任意の行でアプリケーションの状態や変数、コール スタックを調べることができます。本番環境のコードを直接デバッグできるので、バグの発見や再現に要する時間が大幅に短縮されます。

ref. https://cloudplatform-jp.googleblog.com/2016/10/stackdriver-debugger-ga.html


本番環境で動作中のアプリケーションの内部状態を直接デバッグできるという夢のようなツールです。

現時点での対応プラットフォームは以下のようになっております。

始める前に_ _ _StackDriver_Debugger_ _ _Google_Cloud_Platform.png

ref. https://cloud.google.com/debugger/docs/before-you-begin?hl=ja#supported_platforms

表の通り、いまのところGoについてはGAEでもサポートされておらず(!)、GCE、GKEだけになります。

GCEについては、公式の導入ドキュメントが存在しており、この通りにagentをGCEにダウンロードして、agent経由でgoバイナリを起動してあげればよいです。

しかしながら、GKEについては表で○がついている割にはドキュメントは存在してません…

GCE直に使うことはまずない(?)ので、GKEでも使えるようにしてみましたよ、という記事になります。


Clusterの作成

gcloudの設定とプロジェクトの作成は済んでいるものとして話を進めます。

まず以下のようなコマンドでClusterを作成します。

gcloud container clusters create $CLUSTER_NAME --scopes compute-rw,storage-rw,https//www.googleapis.com/auth/cloud-platform

オプションは必要に応じて設定してもらうとして、--scopesは上記の指定が必須になります。


gcloud compute instances create コマンドを使用して、コマンドラインからインスタンスを作成することもできます。かならず --scopes パラメータを --scopes compute-rw,storage-rw,https://www.googleapis.com/auth/cloud-platform に設定してください。これにより、Cloud Platform バックエンドへのアクセスに必要な Cloud Platform OAuth スコープとその他のスコープが有効化されます。

ref. https://cloud.google.com/debugger/docs/setting-up-go-on-compute-engine#create_a_compute_engine_instance


GCEについて書かれていますが、GKEもGCE上で動作しているので同様です。


初期イメージのGoogle Container Registry(GCR)へのpush

以下のような適当なgoアプリを使うことにします。


main.go

package main

import (
"log"
"net/http"
)

func handler(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("Hello, world!\n"))
}

func main() {
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe("0.0.0.0:3000", nil))
}


このアプリを実行するDockerfileを適当に用意して、GCRにpushしておきます。


Dockerfile

FROM golang

WORKDIR /go/src/hello
COPY main.go .

RUN go build
CMD ["./hello"]
EXPOSE 3000


gcloud docker -- build -t gcr.io/$PROJECT_ID/hello:latest

gcloud docker -- push gcr.io/$PROJECT_ID/hello:latest

$PROJECT_IDやrepositoryへのパス1は適当に自分の環境に合わせてください。


k8sへのデプロイ

GCRにpushしたimageを、k8s上にデプロイします。


deployment.yaml

---

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: hello
spec:
replicas: 3
template:
metadata:
labels:
app: hello
tier: frontend
spec:
containers:
- name: hello
image: gcr.io/$PROJECT_ID/hello:latest
ports:
- containerPort: 3000


service.yaml

---

apiVersion: v1
kind: Service
metadata:
name: hello
labels:
app: hello
spec:
type: LoadBalancer
ports:
- port: 80
targetPort: 3000
selector:
app: hello

Dockerfileで3000番をexportしたので、これに80番でアクセスさせてます。

$PROJECT_IDは展開されないので作成したIDで置き換えてください。

kubectl create -f deployment.yaml -f service.yaml

kubectl get pod
kubectl get deployments
kubectl get service

YAMLから諸々作成します。

kubectl get serviceEXTERNAL-IPが確認2できるので、そこにアクセスするとContainerが実行されます。


Google Container Builder(GCB)によるデプロイ

Containerのビルドとk8sへのデプロイにGCBを使います。

cloudbuild.yamlを以下のように記述します。(詳細は後述します)


cloudbuild.yaml

---

steps:
- name: gcr.io/cloud-builders/gcloud
args:
- debug
- source
- gen-repo-info-file
id: gen-repo-info
- name: gcr.io/cloud-builders/wget
args:
- -q
- https://storage.googleapis.com/cloud-debugger/compute-go/go-cloud-debug
id: wget-debugger-script

- name: gcr.io/cloud-builders/go:debian
args:
- build
env: ['PROJECT_ROOT=hello']
id: go-build

- name: gcr.io/cloud-builders/docker
args:
- build
- -t
- 'gcr.io/$PROJECT_ID/hello:$BRANCH_NAME-$REVISION_ID'
- -t
- 'gcr.io/$PROJECT_ID/hello:latest'
- '.'
id: docker-build

- name: gcr.io/cloud-builders/docker
args:
- push
- 'gcr.io/$PROJECT_ID/hello:latest'
id: push-image-latest
- name: gcr.io/cloud-builders/docker
args:
- push
- 'gcr.io/$PROJECT_ID/latest:$BRANCH_NAME-$REVISION_ID'
id: push-image-revision

- name: gcr.io/cloud-builders/gcloud
entrypoint: bash
args:
- -c
- |
gcloud container clusters get-credentials slack-bot --zone us-west1-a
kubectl set image deployment/hello hello=gcr.io/$PROJECT_ID/hello:$BRANCH_NAME-$REVISION_ID
id: deploy-gke


基本的にはGCE用のドキュメントに書かれている手順を、GCBを使って実現していけばよい、ということになります。

以下、順を追って説明していきます。

見出しはidです。


gen-repo-info

  - name: gcr.io/cloud-builders/gcloud

args:
- debug
- source
- gen-repo-info-file
id: gen-repo-info

ソースコンテキストファイルの作成です。

ローカルで実行して生成されるファイルの中身をみてみれば分かりますが、repositoryの情報とrevision情報が入っています。

debuggerの起動に使用します。


wget-debugger-script

  - name: gcr.io/cloud-builders/wget

args:
- -q
- https://storage.googleapis.com/cloud-debugger/compute-go/go-cloud-debug
id: wget-debugger-script

Debuggerエージェントをダウンロードします。

このバイナリをwrapperとしてアプリケーションを起動します。


go-build

  - name: gcr.io/cloud-builders/go:debian

args:
- build
env: ['PROJECT_ROOT=hello']
id: go-build

gcr.io/cloud-builders/goを使ってソースコードをbuildします。

前述のDockerfileでは、Dockerfile内でbuildしてそのまま起動していましたが、そうするとソースファイル3をContainer内にCOPYする必要がでてきて、シングルバイナリになるgoアプリの場合、不要になります。

Container imageはなるべく小さくしたいので、GCBでは別stepでバイナリをbuildして、GCRにpushするimageにはそのバイナリをCOPYして起動するだけ4、とする場合が多いのでこうしています。

またREADMEを読むと分かるように、PROJECT_ROOTを指定することでGOPATHを解決しています。5


docker-build

  - name: gcr.io/cloud-builders/docker

args:
- build
- -t
- 'gcr.io/$PROJECT_ID/hello:$BRANCH_NAME-$REVISION_ID'
- -t
- 'gcr.io/$PROJECT_ID/hello:latest'
- '.'
id: docker-build

Build Step内では置換される変数がいくつかあります。

これらを使って、tag名を動的に決定します。

ここではrevisionつきのtagとlatest tagの2つを作っています。


push-image

  - name: gcr.io/cloud-builders/docker

args:
- push
- 'gcr.io/$PROJECT_ID/hello:latest'
id: push-image-latest
- name: gcr.io/cloud-builders/docker
args:
- push
- 'gcr.io/$PROJECT_ID/latest:$BRANCH_NAME-$REVISION_ID'
id: push-image-revision

BuildしたimageをGCRにpushしています。

imagesを指定することでbuild成功時に自動的にGCRにpushすることも可能ですが、このbuild stepではGKEへのdeployもやってしまいたいので、その前に自前でpushする必要があります。


deploy-gke

  - name: gcr.io/cloud-builders/gcloud

entrypoint: bash
args:
- -c
- |
gcloud container clusters get-credentials hello
kubectl set image deployment/hello hello=gcr.io/$PROJECT_ID/hello:$BRANCH_NAME-$REVISION_ID
id: deploy-gke

k8s上に新しいimageをdeployします。


Dockerfileの修正

agent経由でアプリ(hello)を起動するため、Dockerfileは以下のように修正します。


Dockerfile

FROM debian:stretch-slim

RUN apt update && apt install -y wget \
&& apt autoclean && apt clean \
&& rm -rf /var/lib/apt/lists/*

# boot strap for Stackdriver Debugger
COPY go-cloud-debug .
RUN chmod +x go-cloud-debug
COPY source-context.json

COPY hello .

CMD `./go-cloud-debug -sourcecontext=./source-context.json -appmodule=hello -appversion=1.0 -- ./hello`
EXPOSE 3000


だいぶ変わったのでひとつずつ説明します。

FROM debian:stretch-slim

公式ドキュメントの要件のところ



  • 64 ビット Debian Linux イメージ


と書かれています。

ブートストラップスクリプトを実行すると、最新のDebugger agentがダウンロードされるのですが、このagentはgoバイナリ6になっており、これを実行するための環境要件がこれになります。

これを満たすためと、ここではgoをbuildする必要がなくなったので、debian:stretch-slimを使うように変更します。

実はサラっと流しましたが、cloudbuild.yamlgo-buildのところでgcr.io/cloud-builders/goではなく、gcr.io/cloud-builders/go:debianを使っていましたが、これはこの要件を満たすためです。

alpineでbuildしたバイナリは起動しないという…

RUN apt update && apt install -y wget \

&& apt autoclean && apt clean \
&& rm -rf /var/lib/apt/lists/*

go-cloud-debugの起動にwgetが必要なためインストール。

COPY go-cloud-debug .

RUN chmod +x go-cloud-debug

wget-debugger-scriptでダウンロードしておいたパイナリをコピーして、実行権限を付与しています。7



COPY source-context.json .

gen-repo-infoで生成したjsonファイルをコピーしておきます。

COPY hello .

事前にbuildしておいたバイナリをコピー。

CMD `./go-cloud-debug -sourcecontext=./source-context.json -appmodule=hello -appversion=1.0 -- ./hello`

コピーしておいたgo-cloud-debugを実行します。

パラメータはドキュメントを参照してください。

このバイナリは実行すべきコマンド文字列を返すので、これをバッククォート8で実行してあげる必要があります。

これらをgithubとかにpushします。


Container Registry Trigger

まだベータ版ですがGCRにはtriggerを設定できます。

github9と連携してmasterブランチにpushされたらbuildが開始するように設定します。

連携は簡単でConsole上から行います。


  1. Container Registry -> トリガーを作成 -> GitHub認証

  2. 連携したいリポジトリを選択 -> ブランチやビルドファイルなどの設定


IAMで権限付与

GCBでGKEへのdeployを実行するために、cloudbuildサービスアカウントにGKEの権限を付与します。


  1. IAMと管理 -> IAM -> cloudbuildアカウント選択

  2. 役割で「ContainerEngine管理者」を追加

ここまでで設定は完了です。

あとはmasterにpushすればk8sへのdeployまでやってくれるので、適当になにかpushして試してみるとよいでしょう。

無事アクセスできれば完了です。


Debugger実行

これで晴れてConsole上からLive debugできます。

Consoleにあるデバッグメニューにアクセスします。

debuggerが起動していればここでソースファイルを選べるようになっているはずです。

行番号をクリックして、スナップショットを作成すればいま稼動中のアプリの変数やstacktraceが参照できます!


ちら裏

違う実行環境用にbuildされたバイナリを実行すると、No such file or directoryっていわれて、ファイルあるのに最初なんのことか分からなかった。

Debianじゃないとダメだと気づいて、Dockerfileを変更したら、今度はgo-buildをalpineでやってるせいで、またNo such..といわれてgo:debian使うようにしたらようやく動いた。


See Also





  1. ここではhelloというパスにしました。 



  2. EXTERNAL-IPが割り振られるまでにはしばらく時間がかかります 



  3. 今回はmain.goしかないので無意味ですが 



  4. 将来的にはMulti-stage buildsでしょうか 



  5. ここでは他と揃えるためにhelloにしています。 



  6. どうやらこのバイナリのソースは https://godoc.org/cloud.google.com/go/cmd/go-cloud-debug-agent にあるようなので、これを自前でbuildすればalpineでも動作しそうです。しかし公式でサポートしていない方式なので今回は採用しません 



  7. いま思うとwgetをインストールしちゃうのでここで普通にwgetしてダウンロードでいい気もする 



  8. CMDにバッククォートを使うのはセキュリティ的によくないように思うけど仕方なし 



  9. github以外にもCloud Source Repository, BitBucketの連携にも対応