koukiblog

たぶんweb系の話題

KubernetesでPodに指定したServiceAccountを削除したいときは"default"を指定する

KubernetesでPodに指定したServiceAccountを削除したいときにはまったのでメモ

たとえばこんな感じでPodにServiceAccountを付与したとき

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: test
  template:
    metadata:
      labels:
        app: test
    spec:
      serviceAccountName: "foo"
      containers:
        - name: test
          image: busybox
          command:
            - sleep
            - "600"

サービスアカウントを指定を削除するために"serviceAccountName"要素を削除したマニフェストを作って適用したとする

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: test
  template:
    metadata:
      labels:
        app: test
    spec:
      containers:
        - name: test
          image: busybox
          command:
            - sleep
            - "600"

この場合、 serviceAccountName: "foo" は残ってしまう。未指定の場合は過去の設定をそのまま引き継いでしまう。明示的にserviceAccountをリセットする必要がある。

このときに、 serviceAccountName: "" のように空白を指定すると結局無視されてしまうので注意が必要。serviceAccountのデフォルトは"default"というserviceAccountなので、 serviceAccountName: "default" を指定する必要がある。

これが正解。

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: test
  template:
    metadata:
      labels:
        app: test
    spec:
      serviceAccountName: "default"
      containers:
        - name: test
          image: busybox
          command:
            - sleep
            - "600"

ちなみに、ServiceAccountNameを指定すると、ServiceAccountNameだけではなく、ServiceAccountという要素にも値が設定される。これはServiceAccountの方は既にDeprectedになっている古いAPIで互換性のために値がコピーされているだけなので気にしなくて良い。PodのSpecを見ると確認することができる。

https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/#pod-v1-core

GCPのPubSubをGoでsubscribeするときにgraceful shutdownのようなことをする

GCPのPubSubのTopicをGoでsubscribeするとき、ライブラリ( https://github.com/googleapis/google-cloud-go ) を使えば下記のように簡単に記述できます

client, err := pubsub.NewClient(ctx, "project-id")
if err != nil {
    log.Fatal(err)
}

sub := client.Subscription("subscription1")
err = sub.Receive(ctx, func(ctx context.Context, m *pubsub.Message) {
    fmt.Println(m.Data) // process message 
    m.Ack() // Acknowledge that we've consumed the message.
})
if err != nil {
    log.Println(err)
}

しかしこれではTopicのメッセージを処理中に、コンテナが停止するなど何らかの理由でサーバーが停止するときに処理は行なっているもののAckを返していない、という状態になってしまいます。

シグナルハンドリングを行い、停止までに一定の猶予を設けることで、webサーバでよくあるgraceful shutdownのような挙動にするには、こんな感じにします

cctx, cancel := context.WithCancel(ctx)
sub := client.Subscription("subscription1")
err := sub.Receive(cctx, func(ctx context.Context, msg *pubsub.Message) {
  fmt.Println(m.Data) // process message 
  m.Ack() // Acknowledge that we've consumed the message.
}

quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
<-quit
// Start shutdown process
cancel()
// Waiting pubsub receive shutdown 2 seconds..
time.Sleep(2000 * time.Millisecond)

Receive関数渡したcontextを、SIGINTをハンドリング待ってから、cancelし、その完了を一定期間待つことで実現できます。この例では2秒待っていますがどのぐらい待つのかは処理内容によって考える必要があります。 メッセージの処理に時間かかり、処理中に設定したタイムアウト期間を越えることが想定される場合は、Receive関数に渡す無名関数内で必要に応じてcontext.Doneを確認するとよいと思います(あまりないと思いますが)

2019年まとめ

2019年の個人的なまとめ。2019年時点の認識を、あとで自分が読み返すのは有用そうだなーと思ったので残していくことにしました。

仕事

GCP上にKubernetesを基盤にしてマイクロサービスアーキテクチャを利用したシステムを開発していて、それを無事リリースすることができました。初期のアーキテクチャ設計とインフラ設計・構築をメインで担当していました。 アプリケーションはバックエンドはGo, フロントエンド(BFF)はNode.js を採用し、KubernetesはGKEを利用しました。

こう書くと流行りのバズワードを片っ端から採用したっぽく見えてしまってあれなんですが、一応それぞれ理由があって採用してます。

理想的にはそのサービスが順調に成長し、サービスの成長による様々な問題にKubernetesで構築したインフラ、マイクロサービスアーキテクチャを採用したアプリケーション群がどう対応していけるのか観察したいなーと考えていたが、サービス自体はずっと低調で運用にあたって特に新たに得るものはなかったのがなかなか難しいところ。 GKEで本番運用するための基本的なところは抑えられたかなーとは思っているけど、まだ何か落とし穴ありそうで怖い。

weave/flux使ってCD構築したり

Weave Fluxを利用してGKE環境のCDを構築した - koukiblog

Node.jsのコンテナを良い感じにしたりしていました

KubernetesにNodeJSアプリをデプロイするときにやったこと - koukiblog

透過型のL7プロキシが欲しくてIstioを採用したのだけど、これはちょっと失敗でした。いま作り直すならIstioは外すかな・・

読書

今年一番読んでよかった本は「イノベーターズ」でした。コンピューターが存在せず人が計算していたところから、現代のインターネットにたどり着くまでの歴史が非常に面白く書かれています。

https://www.amazon.co.jp/dp/4062201771/ref=cm_sw_r_tw_dp_U_x_OpRcEbEWCS0

イノベーターズを読んだ結果、この本に続きがあるとしたらどうなるのかなーと考えるようになりました。やっぱAIとブロックチェーンなのかなーと思い、AIはもう今更感あるので、ブロックチェーンのキャッチアップ、勉強をちょっと始めました。

その他

Fortniteにはまり、ひたすらやってました。来年はもっとうまくなりたい。 グローバルだし、ソーシャルゲームのような柔軟な運営とアップデートだし、プレイヤー同士コミュニケーションもとれるし、AAAタイトルのようなクオリティだし、競技シーンも発達してるし、基本プレイ無料だし、もうこれでいいのでは?という感じです。

来年

Webサービス開発・運用は自分の得意分野として引き続きキャッチアップしつつ、ブロックチェーン注目しようかなと思っています。

調べてみると技術的にはインターネット周りの技術と地続きな感じで面白いし、今まで新しいもの好きのようなweb業界の人たちが否定的な一方、デー子で金融系システムの保守ずっとやってるような人は遠くない未来に確実に来るものとして捉えていたり非常に興味深いです。

Web業界の仕事は昔ながらのWeb + DBサーバな三層アーキテクチャのままのところが多そうだし、エンタープライズ系の案件のほうが技術的には先進的なものを扱えるようになっていくかもしれないなーと思ってます。

GCPのAuditLogの探し方

毎回忘れるのでメモ

フィルタ

Stackdriver Logging で以下のようなフィルタを設定すればログを表示できる。resource.typeは必要なリソースに変更すればよい

resource.type="service_account"
protoPayload.@type="type.googleapis.com/google.cloud.audit.AuditLog"

監査ログを出力するGCPリソースの一覧

ここに一覧がある。

https://cloud.google.com/logging/docs/audit/?hl=ja

GCPリソースとStackdriver Logging上のresource.typeのマッピング

ここに一覧がある。

https://cloud.google.com/logging/docs/api/v2/resource-list?hl=ja#resource-types

Kubernetesで完了済みのJobをまとめて削除する

Kubernetesの完了済みJobリソースをまとめて削除するには、field-selectorに"status.successful=1"を指定することで削除できます。

kubectl delete jobs  --field-selector status.successful=1

field-selector に指定できるフィールドはリソース毎に定義されていて、Jobの場合は、metadataまたは、 status.successfulのみが指定できます。どこにもドキュメンテーションされていないようです。

指定できるフィールドは、kubernetesのコード内で見ることができます。 kubectl get で取得しても、status.successfulというフィールドは存在しないのでかなりわかりにくいですね。。

func addConversionFuncs(scheme *runtime.Scheme) error {
    return scheme.AddFieldLabelConversionFunc(SchemeGroupVersion.WithKind("Job"),
        func(label, value string) (string, string, error) {
            switch label {
            case "metadata.name", "metadata.namespace", "status.successful":
                return label, value, nil
            default:
                return "", "", fmt.Errorf("field label %q not supported for batchv1.Job", label)
            }
        },
    )
}

https://github.com/kubernetes/kubernetes/blob/master/pkg/apis/batch/v1/conversion.go#L34

完了済みジョブを削除するときの条件が複雑な場合は、go-clientを利用したプログラミングが必要になりますが、大量に完了済みのジョブが残っているのが気になるくらいであれば、kubectl delete で解決してしまうのもよいんじゃないかなと思います。(まだAlphaですがTTLコントローラを利用する方法もあります )

1日1回CronJobを利用して完了済みのジョブを削除するだけであれば、下記のようにシンプルに済ませることができます。

Delete complete k8s jobs everyday · GitHub

備考

status.successfulはこの投稿で知りました

kubernetes - How can I find the list of field selectors supported by kubectl for a given resource type? - Stack Overflow

GolangでGKE用(StackdriverLogging)のログを出力する

GKE(Google Kubernetes Engine)でアプリケーションを開発する場合、ログは標準出力に出力し、GKEデフォルトでインストールされるfluentdでStackdriverLoggingに出力することが多いと思います。

その場合、特定のJSONフォーマットで出力することでStackdriverLogging上で扱いやすくなります。フォーマットの詳細はこちらです。 https://cloud.google.com/logging/docs/agent/configuration#special-fields

Golangの場合、デフォルトのLoggerでは

という問題があったので、どうすればよいのか調べてみました。

まず、ログ用のライブラリですが、logrusというライブラリを使うのがよさそうでした。 https://github.com/sirupsen/logrus

構造化したログを扱うことができ、フォーマットも指定することができます。

JSONのFormatterは標準で用意されているので下記のように利用することでJSON形式でログを出力することが可能です。init() でformatterを指定している箇所で環境変数を使った分岐を使えば、手元ではテキスト形式、サーバではJSONという分岐も可能です。

import (
    "os"
    log "github.com/sirupsen/logrus"
)

func init() {
    // Log as JSON instead of the default ASCII formatter.
    formater := log.JSONFormatter{
        FieldMap: log.FieldMap{
            log.FieldKeyLevel: "severity",
        },
    }

    log.SetFormatter(&formater)

    // Output to stdout instead of the default stderr
    log.SetOutput(os.Stdout)

    // Only log the warning severity or above.
    log.SetLevel(log.InfoLevel)
}

func main() {
  log.Info('hello') // => {"msg": "hello", "level":info, "time":"2019-11-24T18:06:23+09:00"}
}

StackdriverLoggingに連携するにあたって、levelは、"severity"というkeyで連携する必要があります。これは、JSONFormaterにFieldMapを指定することで実現できます。

func init() {
    // Log as JSON instead of the default ASCII formatter.
    formater := log.JSONFormatter{
        FieldMap: log.FieldMap{
            log.FieldKeyLevel: "severity",
        },
    }

    log.SetFormatter(&formater)
}

この解決策は、下記Issueで紹介されていました

github.com

また、logrusで用意されているレベルとStackdriverLoggingで定義されているレベルには差異があるので、カスタムレベルを定義する方法があれば、それを利用したいところですが、logrusには用意されていないようでした。info, warning,errorは利用できますが、それ以外を利用するとStackdriverLoggingでは正しく扱えないので注意が必要です。 それぞれがサポートしているレベルは下記です。

StackdriverLogging: https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity

logrus: https://github.com/sirupsen/logrus/blob/39a5ad12948d094ddd5d5a6a4a4281f453d77562/logrus.go#L27

Custom Log Levelについては、Issueで要望があがっていますが、いまのところ実装される見込みは薄そうです。

github.com

ということで手軽にJSON形式のログを出力する場合はlogrusがおすすめです。Golang標準のloggerインターフェースを実装しているので、後から差し替えるのも簡単だと思います。

gorilla/sessionでメモリリークが起きる原因を調べた

はじめに

開発中のGoで書かれたアプリケーションがメモリリークを起こしていて、gorilla/sessions ( https://github.com/gorilla/sessions ) というライブラリをバージョンアップしたら解決した。

メモリリークを解決したPRはこれ。

github.com

gorilla/contextが原因らしく、Contextが存在するよりも前にContext相当のことをしていたライブラリなので、併用するとよくないことが起こりそうだなーとは思うものの、なぜメモリリークするのかが理解できなかったので調べてみた。

調べた結果

Go 1.7で追加されたContextとgorilla/contextの競合によりメモリリークが発生するようになったのかと思い、少し混乱したのだが、

はそれぞれ独立した事象だった。が、後者のContextの競合は、メモリリークが発生する可能性をより高めてしまっている。 具体的には、 gorilla/contextと http.request.WithContextを併用すると、メモリリークに対する既知の対応を入れた場合でも、gorilla/contextで保存した内容を削除できずにメモリリークが起きる。

メモリリーク

gorilla/session を使う場合は、リクエストの最後にClearを行わないとcontextに保存した内容が削除されず、メモリリークが起きる。 これはgorilla/contextの仕様であり、GoのContextとは関係ない問題だった。 gorilla/muxを利用する場合はライブラリが面倒を見てくれるので利用者は気にしなくて良くて、gorila/session または gorilla/contextを単独で利用している場合に限り、利用者側が気をつける必要がある。

gorilla/contextとGoのContextの競合

gorilla/contextは、http.Requestのポインタをkeyにしたmapにデータを保存している。

var (
    mutex sync.RWMutex
    data  = make(map[*http.Request]map[interface{}]interface{})
    datat = make(map[*http.Request]int64)
)

https://github.com/gorilla/context/blob/master/context.go#L13..L17

一方、http.Request.WithContextは、requestをshallow copyした上でcontextをセットするので、引数として渡したrequestと戻り値のhttp.requestのポインタが変わってしまう。

WithContextの定義はこれ。

func (r *Request) WithContext(ctx context.Context) *Request {
    if ctx == nil {
        panic("nil context")
    }
    r2 := new(Request)
    *r2 = *r
    r2.ctx = ctx
    r2.URL = cloneURL(r.URL)
    return r2
}

go/request.go at 3409ce39bfd7584523b7a8c150a310cea92d879d · golang/go · GitHub

WithContextを利用すると、gorilla/contextで保存する先のmapが変わってしまう。 コードにすると以下のようなことが起こる。

package main

import (
    "context"
    "fmt"
    "net/http"

    gcontext "github.com/gorilla/context"
)

func main() {
    r := new(http.Request)
    fmt.Printf("%v\n", &r) // => 0xc000010020

    gcontext.Set(r, "key", "value") 
    fmt.Printf("gorilla/context: %v\n", gcontext.Get(r, "key")) // => gorilla/context: value

    r = r.WithContext(context.Background())
    fmt.Printf("%v\n", &r) // => 0xc00009c010

    fmt.Printf("gorilla/context: %v\n", gcontext.Get(r, "key")) // => gorilla/context: <nil>
}

上記の例では省略したが、context.Clear(r) を行ってもセットした値を消すことはできない

ライブラリ側の解決策

gorilla/sessionが、1.6 以前のサポートを切ったので、gorilla/contextを利用しなくなった。 上記の回避策が不要になった。 なので、最新のgorilla/sessionを使えばメモリリークは発生しない。

再掲だが、PRはこちら

github.com

gorilla/contextを直接利用すると変わらずメモリリークの可能性はあるので注意

備考

調べたときに利用したページを自分の備忘のため残しておく

そもそもContextとは

ちゃんと理解できてなかったので改めて調べた。こちらのページに日本語訳があって助かった www.ymotongpoo.com

なぜgorilla/muxを利用するとメモリリークしないのか

gorilla/mux ( https://github.com/gorilla/mux ) の mux.go の旧バージョンを参照すると、ServeHTTPメソッドに下記の記述があった。

func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {

       ~~~~

    if !r.KeepContext {
        defer contextClear(req)
    }

       handler.ServeHTTP(w, req)
}

実行されているcontextClearの定義は gorila/context の context.Clear。muxではライブラリ側で、gorilla/contextのcontextClearが実行されていたことが確認できた

ちなみに、gorilla/sessions でメモリリークの回避策で紹介されていた ClearHandlerの定義はこちら

func ClearHandler(h http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer Clear(r)
        h.ServeHTTP(w, r)
    })
}

ほぼ同じ効果が期待できそう。

muxのGoのContextへの対応

前述の通り、メモリリークとGoのContextへの対応はまた別の問題。gorilla/muxでは、Goのバージョンによって挙動を変更していた。

Store vars and route in context.Context when go1.7+ is used by ejholmes · Pull Request #169 · gorilla/mux · GitHub

修正のきっかけはこれっぽい

mux.Vars breaks with Go 1.7 · Issue #168 · gorilla/mux · GitHub

メモリリークが報告されている様子

https://groups.google.com/forum/#!msg/gorilla-web/clJfCzenuWY/N_Xj9-5Lk6wJ