DockerのCMDでログファイルをtailする

Dockerは標準出力に出力したログなどはlogsコマンドで確認することができますが、Dockerで動かすアプリケーションがファイルにログを出力している場合はlogsコマンドで確認することができません。

このような場合は、CMDでtail -fすることが多いと思います。

CMD tail -f /var/log/cron.log

しかしこれだとlogsコマンドに何も流れてきません。
そこでログファイルの中身をexecコマンドで確認してみると正しくログが出力されています。
なにが起こっているのでしょうか。

Dockerのファイルシステム

原因はDockerが採用しているファイルシステムにあります。

DockerはUnion File Systemというファイルシステムを採用していて、このファイルシステムcopy-on-writeという方法で動作しています。
Union File Systemcopy-on-writeについてはこの記事がわかりやすいです。

namu-r21.hatenablog.com

直接的な原因は、CMDでtailしているログファイルはDocker imageに保存されているファイルであり、コピーされてログが書き込まれるようになったファイルではないためです。

対策

CMDでtailする前にcopy-on-writeを発生させればいいだけです。

やり方は色々あると思いますが、1つの例としてはこのようにCMDを書けば意図した通りに動きます。

CMD : >> /var/log/cron.log && tail -f /var/log/cron.log

なお、このようにCMDで&&を使って複数コマンドを実行するときは、DockerのPID1とゾンビプロセスのことを考慮する必要が出てくるので注意しましょう。

参考

dockerfile - Output of `tail -f` at the end of a docker CMD is not showing - Stack Overflow

自作したiOSライブラリの更新手順

自作したiOSのライブラリをCocoaPodsとCarthageで公開する記事はたくさんありますが、新バージョンの公開に関する記事はほとんど無く毎回困るのでまとめます。

Carthage

Carthageで公開しているライブラリの新バージョンをリリースするのは簡単で、GitHubで新しいrelease(タグ打ち)をするだけです。

そのため、準備が整ったらmasterにマージしてタグを打てばOKです。

CocoaPods

CocoaPodsは少し作業が発生します。ただ、公開するときに実行したコマンドの一部をもう一度実行するだけなので、忘れなければ簡単な作業です。

podspecの更新

podspecファイルに書いてあるバージョンをリリース予定のバージョンに更新します。
そのほかの内容は基本的に更新する必要はありません。もちろん、ここが変わるような変更を加えたリリースなら更新する必要があります。

.swift-versinoの更新

最近のXcodeには複数のバージョンのSwiftが付属しているため、ライブラリが使用するSwiftのバージョンを教えてあげないと正しく動かない場合があります。そのため.swift-versionというファイルを作成してリポジトリのルートに配置します。新バージョンのリリースでSwiftのバージョンが変わる場合はこのファイルに書いてあるバージョンも更新します。

バリデーション

更新したpodspecファイルに問題ないかチェックします。

pod lib lintpod spec lintを実行します。

pod spec lintリポジトリのチェックまで行うため、新しいバージョンのタグが存在しない場合はエラーになります。未公開の場合はpod lib lintを実行します。こちらはローカルにある情報だけでチェックします。

新バージョンの公開

ドキュメントの更新なども済ませたらmasterにマージして新バージョンのタグを打ちます。

その後、pod trunk pushを実行してpodsの公開は完了です。
前回のリリースから時間がたっているとpodsの認証が切れてしまっているので、pod trunk register hoge@hoge.com 'Hoge Huga'と実行して再度認証し、その後もう一度リリースのコマンドを実行します。

2018年の振り返り

2018年のことをエンジニアリングを中心に振り返える。

Work

仕事では大きな変化があった。社内転職をすることができ、アプリなどのフロントを中心としてたまにサーバーサイドの開発もするポジションから、基盤担当へと変わった。

アプリ開発の勢いが全盛期と比べると落ち込んだことや、将来のエンジニアとしてのスキルセットなどを考えると、より技術的に深いレイヤーの開発ができるこのポジションに付けたのは非常に大きい。

フロントなどの時代の移り変わりが速いレイヤーで、フレームワークをただ仕事で使うような状態では、やる気と時間がたっぷりある若者にアッサリと追いつかれてしまうだろう。もちろん上の方のレイヤーであっても、フレームワークやライブラリなどを開発するぐらいの技術力があれば追いつかれることはない。しかし、このレイヤーのエンジニアの大半はそうではない。経験と技術力の双方が必要になる場所で戦っていれば、勢いのある若者でも簡単には追いつけないはずだ。それは個人としての価値につながる。

仕事で取り組んだこととして主要なものはAWSからGCPへのクラウド移行だ。
具体的には、

  • Elastic BeanstalkからGKE+Istioへの移行
  • RDSからCloud SQLへの移行

これらからはネットワークや自動化のためのツール開発などで大きな学びが得られた。

来年はSREのような活動をしていく予定だ。

Book

今年読んだ技術書の中からオススメを紹介する。

Private

2018年は年明けから風邪が続いたり腰を痛めたりと体調が優れないことが多かった。厄年だからだろうか。腰痛対策として始めたストレッチにより体が柔らかくなってつま先に手が届くようになったり、だんだんと風邪を引かなくなってきたりと、終盤には改善が見られた。

2019

体調に気をつけながら技術力を伸ばしていきたい。
ルイさんのCコンパイラ作成入門とかやってみようかな。

WHITEPLUS Advent Calendar 2018に投稿しました

今年もAdvent Calendarをやっていて、2記事投稿しました。

libpcapでVPNのアクセスを可視化する

blog.wh-plus.co.jp

MySQLをRDSからCloud SQLに移行するときに考えたこと

blog.wh-plus.co.jp

複数のPodのログをまとめて表示する

Kubernetesを開発環境として使用しプロダクションと同じような環境で開発すると、複数のアプリケーションが可動することになると思います。
そうするとログも複数の場所に出力されることになります。

複数個のターミナルを立ち上げ1つずつログを表示してもいいですが、場所を取りますし確認する箇所が増えて面倒なので、まとめて表示する方法を紹介します。

今回は以下の2つの構成に合わせたやり方を紹介します。

  • ログを標準出力に書き出している場合
  • ログをファイルに書き出している場合

ログを標準出力に書き出している場合

これは stern というアプリケーションを使えば簡単にできます。

github.com

使い方は簡単でPodの名前を引数として渡すだけです。
正規表現でいい感じに検索してくれて、オプションを入れればコンテナで絞り込むこともできます。

Dockerの標準に沿ったアプリケーションならこれを使えば解決です。

ログをファイルに書き出している場合

様々な理由によりDockerの標準に合わせることができず、ファイルにログを書き出している場合もあると思います。

この場合は kubectl exectail することになりますが、複数のPodのログをまとめるためには少し工夫をしなければいけません。

名前付きパイプを使う

名前付きパイプを使って複数の kubectl exec の出力を1つにまとめます。

名前付きパイプについての説明はこの記事がわかりやすいです。

qiita.com

名前付きパイプに kubectl exec を出力し、名前付きパイプを cat することでログをまとめて表示します。

シェルスクリプトで一連の処理を書きます。

#! /usr/bin/env bash

FIFO="/tmp/shout"

mkfifo ${FIFO}
trap 'rm ${FIFO}; exit 1' 1 2 3 15

PODA=$(kubectl get po -o=jsonpath='{.items[?(@.metadata.labels.app=="pod-a")].metadata.name}')
PODB=$(kubectl get po -o=jsonpath='{.items[?(@.metadata.labels.app=="pod-b")].metadata.name}')
PODC=$(kubectl get po -o=jsonpath='{.items[?(@.metadata.labels.app=="pod-c")].metadata.name}')

kubectl exec -i ${PODA} -c hoge -- tail -f /var/app/log/hoge_log > ${FIFO} | \
kubectl exec -i ${PODB} -c hoge -- tail -f /var/app/log/hoge_log > ${FIFO} | \
kubectl exec -i ${PODC} -c hoge -- tail -f /var/app/log/hoge_log > ${FIFO} | \
cat ${FIFO}

stern と比べると見やすさや使いやすさで数段劣りますが、同じようにまとめて出力することができます。

GKEでプリエンプティブインスタンスを使いこなす

GCPにあるプリエンプティブ インスタンスをGKEでうまいこと使えないか、試行錯誤した結果をまとめます。

プリエンプティブ インスタンスとは

一言で言ってしまえば、AWSにあるスポットインスタンスGCP版です。

公式ドキュメントにはこのように書いてあります。

プリエンプティブ VM は、最長持続時間が 24 時間で、可用性が保証されない Google Compute Engine VM インスタンスです。プリエンプティブ VM は標準的な Compute Engine VM よりも低価格で、同じマシンタイプとオプションを使用できます。

ドキュメントに書かれているように低価格で使用できるのが最大のメリットです。
だいたい1/3ぐらいの価格で使用することができます。

f:id:akaimo3:20180722164618p:plain

しかし、様々な制限があります。
意識しておかなければならないこととして、次のようなことがあります。

  • いつ終了するかわからない
  • 最大でも24時間でシャットダウンされる
  • 常に使用できるとは限らない

これらの注意点と上手に付き合いながらGKEのNodeとして使用していきます。

使いこなす

シャットダウンの対策

まず、シャットダウンによりNode数が減ってしまう問題はGKEを使っている上では問題ありません。
KubernetesがNode数の減少を検知して、即座に元と同じ数になるようにNodeを起動し直してくれます。

しかし、一時的とはいえNodeが減るので、そこで可動しているPodは止まってしまします。
すぐに復帰するので、冗長化されているPodであればそこまで問題でもありません。せいぜい、一時的(数秒から数十秒)に別のPodに負荷が集中するぐらいです。

アプリケーションの構成などの理由により冗長化できないPodや一時的でも減っては困るPodに対しては、プリエンプティブ インスタンスに配置されないように設定をします。

プリエンプティブ インスタンスの回避

Kubernetesのtaintsとtolerationsという機能を使用してプリエンプティブ インスタンスを回避します。

まず、通常のインスタンスで構成されるノードプールとは別に、プリエンプティブ インスタンスだけで構成されたノードプールを作成します。 プリエンプティブのノードプールを作成するときに、ノードtaintを設定します。

f:id:akaimo3:20180722171308p:plain

そして、プリエンプティブ インスタンス配置されても問題ないPodにtolerationを設定します。

spec:
  template:
    spec:
      containers:
      ...
      tolerations:
        - key: gke-preemptible
          operator: Equal
          value: "true"
          effect: NoSchedule

これでこの設定がされているPodのみがプリエンプティブ インスタンスのノードプールに配置されるようになります。

taintsとtolerations

ちょっと仕組みがわかりにくいので、taintsとtolerationsの概念を説明します。

Nodeにtaints(よごれ)をつけ、そのtaintsをtolerations(寛容、黙許)できないPodをNoSchedule(スケジュールしない)と設定しています。
そしてPodにtolerationするtaintの内容を記述することで、taintのついたNodeに配置されるようになります。

24時間でシャットダウンされる

上で書いたプリエンプティブ インスタンスの回避の設定をしていれば24時間でシャットダウンされる制限も問題ないように感じるかもしれません。
しかし、24時間たつ前にシャットダウンされることがなかった場合、ノードプール内の複数のインスタンスが同時に24時間を経過し、同じタイミングでインスタンスがシャットダウンする可能性があります。

これだと、いくら冗長化していても運しだいですべてのPodが消えてしまう可能性があり、リスクが高くなります。

この問題の解決方法は簡単で、ノードプールを追加後、起動時間がバラけるように一部のNodeをシャットダウンしてしまえば良いです。
そうすることで複数のNodeが同時に24時間を迎えてシャットダウンされることが防げます。

常に使用できるかわからない

プリエンプティブ インスタンスがシャットダウンされたあと、GCPの状況によってはプリエンプティブ インスタンスが起動できないことがあります。

この場合、通常のインスタンスで構成されるノードプールにクラスターのオートスケールを設定しておくことで、そっちにインスタンスが追加されPodが配置されます。
tolerationsの設定がされていれも問題なく通常のインスタンスに配置されます。
なぜならtolerationsはtaintsを受け入れる設定なので、taintsが無いNodeにも配置されます。

まとめ

これを意識することでGKEを低価格で運用することができます。
ぜひ試してみてください。

GKEからCloud SQLに接続する

GKEからCloud SQLに接続するやり方としてCloud SQL Proxyを使う方法が推奨されています。

公式ドキュメントで解説されているやり方は、Cloud SQLにアクセスしたいコンテナが入っているPodにサイドカーとしてProxyを入れる方法です。
公式の例だと、wordpressのコンテナとProxyのコンテナを同じPodにしています。

GKE内にCloud SQLに接続するコンテナが一種類であれば公式のやり方で問題ありませんが、複数のサービスがCloud SQLにアクセスするとなると、全てのコンテナにサイドカーとして入れることになってしまい、あまりうれしくありません。

そこでProxyを単体のPodとして作成し、Serivceを経由してアクセスする方法をとります。

Deploymentの作成

基本的には公式ドキュメントと同じように進め、サイドカーとなっている部分だけを残し、適切なポートを開放してあげれば完了です。

注意点として、他のPodからのアクセスを受け入れる場合は、起動スクリプトでIPを指定しなければなりません。
ポート番号だけだと、ローカルホストからのアクセスしか受け付けてくれません。

具体的にはこのようにします。

command: ["/cloud_sql_proxy",
          "-instances=sample-165109:asia-northeast1:sample=tcp:0.0.0.0:3306",
          "-credential_file=/secrets/cloudsql/credentials.json"]

あとは適切にServiceを作成すれば完成です。

デメリット

サイドカーではなくPod単体として起動させるということは、クラスタ内のDBにアクセスするPodが全てこのPodを経由することになります。
そのため、このPodが起動していないと、全てのPodはDBにアクセスできなくなってしまいますので、きちんと冗長化しておきましょう。

定義ファイル

最後に定義ファイルの全体を貼っておきます。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: sql-proxy
  labels:
    app: sql-proxy
spec:
  replicas: 2
  selector:
    matchLabels:
      app: sql-proxy
  template:
    metadata:
      labels:
        app: sql-proxy
    spec:
      containers:
        - name: cloudsql-proxy
          image: gcr.io/cloudsql-docker/gce-proxy:1.11
          ports:
            - name: mysql
              containerPort: 3306
          command: ["/cloud_sql_proxy",
                    "-instances=sample-165109:asia-northeast1:sample=tcp:0.0.0.0:3306",
                    "-credential_file=/secrets/cloudsql/credentials.json"]
          volumeMounts:
            - name: cloudsql-instance-credentials
              mountPath: /secrets/cloudsql
              readOnly: true
      volumes:
        - name: cloudsql-instance-credentials
          secret:
            secretName: cloudsql-instance-credentials
---
apiVersion: v1
kind: Service
metadata:
  name: sql-proxy
spec:
  selector:
    app: sql-proxy
  ports:
    - name: mysql
      protocol: TCP
      port: 3306
      targetPort: mysql