Digdagのbackfillを無効化する

Digdagでスケジュール実行の設定をしているワークフローを一時停止したあとに停止を解除した場合は、停止していた期間に実行されるはずだったセッションを実行するBackfillという機能があります。
GUIでPAUSEした後にRESUMEしたときや、CLIでのenable/disableを実行したときが該当します。

この機能はデフォルトで有効になっているため、冪等性が保たれていないジョブなどを一時停止した場合はBackfillを無効にする必要があります。

Backfillを無効にする方法として、digdag rescheduleを実行する方法とマニフェストファイルでskip_delayed_byを設定する方法があります。

rescheduleコマンドを使う方法

rescheduleコマンドでスケジュールを更新して現時刻よりも前にスケジューリングされたセッションをスキップするように設定します。

まずはBackfillを無効にしたいワークフローのIDを取得します。
CLIからは以下のコマンドで取得できます。

$ digdag schedules

取得したIDで以下のコマンドを実行すればBackfillを無効にできます。

$ digdag reschedule <schedule id> --skip-to "$(date '+%Y-%m-%d %H:%M:%S %z')"

このやり方で注意しなければいけないポイントとして、Backfillが無効になるのは今回の一時停止で実行されなかったセッションだけであるところです。
後日また一時停止したときにもBackfillを無効にする場合は再度rescheduleコマンドを実行する必要があります。

冪等性が無い場合など、毎回Backfillを無効にしたい場合は2つめのやり方で設定できます。

skip_delayed_byを設定する方法

マニフェストファイルのscheduleskip_delayed_byを設定することで、指定された期間よりも以前のセッションをスキップするように設定できます。
例としては以下のようになります。

schedule:
  skip_delayed_by: 3m

+setup:
  ...

この例の場合は一時停止を解除した時刻の3mよりも前のセッションを無効にします。
つまり、この設定を1sとかにしておけばBackfillを完全に無効にできます。

ワークフローの内容によりBackfillが実行されると不都合がある場合は、この設定を入れておくことをオススメします。

Go ModulesなプロジェクトのDockerビルドを高速化する

Go Modulesに対応させたプロジェクトをDocker内でビルドして実行するとします。
単純にDockerfileを書いてしまうとソースコードに変更が入るたびにModulesのダウンロードが走ってしまい、とても時間がかかってしまいます。
そこでDockerのイメージレイヤのキャッシュ機能を使って高速化する方法を紹介します。

Go Modulesのキャッシュ

キャッシュが効かずに毎回Modulesのダウンロードが行われる原因としては、Docker内でGoのビルドをするためにソースコードをコピーしている COPY コマンド部分が、前回の状態と異なるためキャッシュを使うことができないところにあります。
そのため、ソースコードをコピーする前にModulesのダウンロードを行えば、ソースコードの変更を行っても前回のキャッシュが使え、大幅な短縮になります。

以下が一例です。

FROM golang:1.12.4 as build

ENV GO111MODULE=on
WORKDIR /go/src/path/to/proj

COPY go.mod .
COPY go.sum .

RUN set -x \
  && go mod download

COPY . .

RUN set -x \
  && go build -o /go/bin/app

FROM ubuntu:18.04

COPY --from=build /go/bin/app /go/bin/app

CMD ["/go/bin/app"]

ソースコード全体をコピーする前にModules関連だけをコピーして、 go mod download を実行してダウンロードしています。
これでModulesに変更が入らない限り COPY . . から実行されるため、大幅な短縮になります。

KubernetesのPodでDockerコンテナを実行する

KubernetesのPodでDockerコンテナを実行する方法を紹介します。

Dockerコンテナ内でコンテナを起動する方法のことをDinD (Docker inside Docker)などと言われていますが、同じことをKubernetesのPodで実行するには少し工夫する必要があります。
アプローチとして2種類のやり方があり、双方にメリットとデメリットがあるため状況に合わせて適切なものを選択する必要があります。

アプローチ

1つめのやり方はPodが動いているホストマシンのDockerデーモンを共有する方法で、DooD (Docker outside of Docker)とよばれるものをPodに適応させたパターンです。
2つめのやり方はPod内でDockerデーモンを動かす、DinD (Docker inside Docker)と呼ばれるものを適応させたパターンです。

それぞれの方法のやり方とメリット・デメリットについて紹介します。

DooDパターン

DooDパターンをk8sに適応させるのは簡単で、ホストの /var/run を共有してDockerデーモンにアクセスできるようにするだけです。

k8sマニフェストファイルで表すとこのようになります。

apiVersion: v1
kind: Pod
metadata:
  name: dood
spec:
  containers:
    - name: docker
      image: docker:19.03
      command: ["docker", "run", "nginx:latest"]
      volumeMounts:
        - mountPath: /var/run
          name: docker-sock
  volumes:
    - name: docker-sock
      hostPath:
        path: /var/run

DooDパターンの場合、コンテナ内で作成されたコンテナはホストが管理するその他のコンテナと同列の扱いになることに注意する必要があります。
つまり、コンテナを作成したPodと同列の扱いのコンテナとなります。
しかしながらPod内で作成したコンテナはk8sの管理下にはありません。
また、もし他にもコンテナを作成するPodが存在した場合は、コンテナ名などのリソースが競合する可能性があるので注意しなければいけません。
偶然発生する可能性は低いですが、k8sのリソースと競合する可能性もあります。

コンテナのポートをマッピングする場合も注意が必要で、デーモンが動いているのはホストマシンになるためポートマッピングされるのもホストになります。
そのためコンテナを作成したPodのIPで作成したコンテナにはアクセスできません。ホストのIPでアクセスする必要があります。

また、k8sの管理から外れるため、Podに指定したリソース制限が適用されません。
さらに -d としてコンテナを起動した場合はPodの削除時に作成したコンテナが削除されません。

このように様々な落とし穴があるため、k8sのPodの場合はDooDパターンはオススメしません。次に解説するDinDパターンを使うべきです。

メリット

  • 特別な権限を与える必要がない
  • コンテナを動かしたいイメージにDockerコマンドをインストールするだけで動かすことができる

デメリット

  • Podの中ではなくホストマシンの中にコンテナが作成される
  • k8sの管理下からはずれたところにコンテナが作成される
  • ホストでリソースの競合が発生する可能性がある

DinDパターン

DinDパターンはDooDパターンと比べると少しだけ複雑です。
dockerコマンドを実行するコンテナのサイドカーとしてDockerデーモンを動かし、dockerコマンドがサイドカーに対してアクセスするように設定します。

以下がその設定をしたマニフェストファイルです。

apiVersion: v1
kind: Pod
metadata:
  name: dind
spec:
  containers:
    - name: docker
      image: docker:18.09
      command: ["docker", "run", "nginx:latest"]
      env:
        - name: DOCKER_HOST
          value: tcp://localhost:2375
    - name: dind-daemon
      image: docker:18.09-dind
      resources:
        requests:
          cpu: 20m
          memory: 512Mi
      securityContext:
        privileged: true

Dockerデーモンを動かすコンテナは公式のイメージがあるのでそれを使います。この公式イメージは2375ポートでDockerのREST APIを公開しています。
そこに対してDocker CLIがアクセスするように環境変数 DOCKER_HOST を設定します。

DinDパターンでは作成したコンテナはDockerデーモンが動くサイドカーコンテナの中に作成されるため、Podに設定したCPUやメモリなどのリソースの制限を受けた状態でコンテナを作成できます。
また、このPodを削除したときに作成したコンテナも同時に削除されます。
ポートマッピングを使用したときもDockerデーモンが動いているネットワーク、つまりはPodのネットワークに対して行われるため、ホストのリソースを汚すことなくPodのIPでアクセスできます。
同様にコンテナ名などのリソース競合もPod内でしかおこりません。

ただし一つだけ意識しなければならないことがあり、Dockerデーモンが動くコンテナは privileged 権限で実行しなければいけません。ここが障害にならない限りDinDパターンを選択することをオススメします。

メリット

  • Pod内にコンテナを作成できる
  • ホストに影響を与えることなくPod内で完結する
  • Podの設定(リソースやネットワーク)を引き継ぐことができる

デメリット

  • サイドカーコンテナが1つ増える
  • privilegedでコンテナを動かす必要がある

まとめ

privilegedで動かしても問題ない環境であればDinDパターンがオススメです。
DooDを使う場合はリソース競合など、ホスト側でコンテナが動いていることを忘れずに使う必要があります。

QtでMacアプリを作ってみる

QtでMacのアプリをビルドし、.appの形式として書き出します。

環境

事前準備

使用するXcodeのCommand Line Toolsを設定します。

$ sudo xcode-select -s /Applications/Xcode10.2.1.app

Command Line Toolsが正しく設定されているか確認します。

$ xcode-select -p
/Applications/Xcode10.2.1.app/Contents/Developer

qtのインストール

Qtの公式サイトからオープンソース版をダウンロードします。

www.qt.io

ダウンロードしたら流れにそってインストールします。
ここでQt本体にチェックをつけないとDeveloper Toolsしかインストールされないのでチェックを入れます。

f:id:akaimo3:20190604211035p:plain

ダウンロードが終わったらインストール完了です。

プロジェクトの作成

インストールが完了するとQt Creatorが起動するので、New ProjectをクリックしてQt Widgets Applicationを選択します。
適当なプロジェクト名を入力すれば作成されます。

f:id:akaimo3:20190604211821p:plain

Qt Creatorでビルドを実行するとこのような画面が作られます。

f:id:akaimo3:20190604212046p:plain

アプリ(.app)化する

Qt Creatorからはアプリを実行できたので次は.appとして書き出します。

書き出すにはqmakeというツールを使うため、qmakeにパスを通します。
.bashrc.zshrcなどに以下を追加します。

export PATH="$HOME/Qt/5.12.3/clang_64/bin:$PATH"

プロジェクトのあるディレクトリに移動して以下のコマンドを実行すれば、プロジェクトのあるディレクトリに.appが作られます。

$ qmake -project
$ qmake
$ make

この環境ではmakeを実行するときにエラーになりました。
.proファイルに以下を追加するとmakeが動くようになります。

QT += widgets

参考

Getting Started on the Commandline/ja - Qt Wiki

c++ - Static QT Compilation problems - Stack Overflow

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'と実行して再度認証し、その後もう一度リリースのコマンドを実行します。

複数の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 と比べると見やすさや使いやすさで数段劣りますが、同じようにまとめて出力することができます。