KubernetesのJobを管理するカスタムコントローラーを作成した

KubernetesのJobは完了しても登録したJobは削除されず kubectl get job などを実行したときの視認性がよくありません。
これを解消するための設定として .spec.ttlSecondsAfterFinished がありますが、長いことalpha版でGKEでは使用することができません。

単純に消すだけであればclient-goを使って書いたスクリプトをCronJobで定期実行すればよさそうですが、カスタムコントローラーを作ってみたかったため akaimo/job-observer というコントローラーを作成してみました。

akaimo/job-observer

カスタムコントローラーを作るにあたっていくつかの方法がありますが、Kubernetes本体のことも知りたかったため、本家のコントローラーと同様のアプローチである k8s.io/code-generator を使って作成しました。

インストール方法

デフォルト設定のコントローラーをインストールする場合は以下の一行を実行するだけです。

$ kubectl apply -f https://raw.githubusercontent.com/akaimo/job-observer/master/bundle.yaml

これはhelmのtemplateコマンドで出力したものなのでカスタマイズができます。
現状はあまり多くのことはできませんが...

カスタムリソース

Cleaner リソースでPodの停止から任意の時間が経過したJobを削除することができます。

apiVersion: job-observer.akaimo.com/v1alpha1
kind: Cleaner
metadata:
  name: my-cleaner
spec:
  ttlAfterFinished: 1m
  cleaningJobStatus: Complete
  selector:
    matchLabels:
      app: my
      version: mikan

削除対象を絞り込むことができ、ServiceでPodを指定するときと同じ方法でLabelを使って選択できます。
また、Jobの終了ステータスで指定することもできます。

Cleaner リソースを複数個登録することで、対象の条件ごとに削除までの期間を指定できます。
例えば、成功したJobは1h、失敗したJobは24hといったような指定ができます。

今後について

今の所、わざわざコントローラーを入れるほどの内容ではないというのが正直なところです。

コントローラーを作成することにもなれてきたので、Jobの終了時にSlackなどに通知する機能を追加してみようかと考えています。

Ubuntuで開発環境を整える

MacのDocker周りの遅さに嫌気が差したのでUbuntuに移行してみることにしました。
自分用のメモも兼ねて構築の流れを残しておきます。

WSL2にしなかった理由

最初はWSL2で構築する予定でしたが、検証していると問題が発生したため辞めました。

中でも致命的だったのがGoのModulesがWindows側のIDEから実行できないことです。

github.com

小さなプロジェクトや大部分を把握している場合なんかはvimで開発していますが、Kubernetesなどの大きなもののコードを読むときはIDEがあると便利です。
コードを読むだけならWindows側に置いておけばいいかなとも思いましたが、Windowsのことをあまり理解したいと思えなかったので辞めました。

気が向いたら再チャレンジするかもしれません。

環境

自作PCに直接Ubuntuをインストールするとドライバやらが面倒なので仮想化しています。

また、仮想化していると簡単にバックアップとリストアができるので気軽にアップデートなどを行えて便利です。

構築の流れ

OS周り

開発環境

構築完了

VMやOS周りの設定さえ終えればあとの大部分はAnsibleがやってくれます。

以前と比べるとUbuntuでも動くソフトウェアが増えていて(Toolbox Appや1Passwordなど)だいぶ扱いやすくなった印象です。

vimの補完プラグインasyncomplete-around.vimを作りました

asyncomplete-around.vimとは

asyncomplete-around.vimprabirshrestha/asyncomplete.vimのSourceプラグインです。

github.com

現在のカーソル位置を中心として上下20行(デフォルト値)に存在する単語を補完対象として表示します。

asyncomplete.vimは補完ポップアップを表示するプラグインで、これに適した補完対象(Source)を渡すプラグインと組み合わせることでポップアップに単語が表示され補完できるようになります。

asyncomplete.vimのソースプラグインとして代表的なものとして、LSPプラグインprabirshrestha/vim-lspprabirshrestha/asyncomplete-lsp.vimがあります。

作った経緯

もともと補完プラグインとしてShougo/deoplete.nvimを使っていました。
使用感に不満はありませんでしたが、Python環境を用意するのが面倒だと感じていました。

LSPとしてprabirshrestha/vim-lspを使っていたため同じ作者のprabirshrestha/asyncomplete.vimに乗り換えたところ、Buffer関連の補完が少し弱く、LSPで補完できないちょっとした単語を繰り返し入力することを面倒に感じていました。
少し調べると独自の補完ソースを追加できるとの記載があったため、asyncomplete-around.vimを作成することにしました。

オススメ設定

ここで私の設定を紹介します。
コードの詳細はGitHubにて公開しているため、適宜そちらを参照してください。

github.com

補完範囲の設定

私は画面に表示されている単語が補完できれば良いかなと思っているため50行にしています。

let g:asyncomplete_around_range = 50

Priorityの設定

asyncomplete.vimpreprocessorという仕組みがあり、補完の表示内容を編集することができます。
今回はそれを利用して補完ソースの表示順を使いやすいように制御します。

各ソースにPriorityを設定し、その値をもとにソートする関数をg:asyncomplete_preprocessorに設定します。

function! s:sort_by_priority_preprocessor(options, matches) abort
    ...
endfunction

let g:asyncomplete_preprocessor = [function('s:sort_by_priority_preprocessor')]

call asyncomplete#register_source(asyncomplete#sources#around#get_source_options({
      \ 'name': 'around',
      \ 'allowlist': ['*'],
      \ 'priority': 10,
      \ 'completor': function('asyncomplete#sources#around#completor'),
      \ }))

私はasyncomplete-lsp.vim, asyncomplete-file.vim, asyncomplete-around.vimという順番で表示させています。
asyncomplete-around.vimは指定範囲内のマッチした単語が全て候補として表示されるため、LSPで候補となっている関数や変数よりも上に表示されてしまうと、とても使いづらくなってしまうためこの順番にしています。

asyncomplete-around.vimの今後

しばらく使用してみて私の要望を満たすものだと確信できたので、上位互換のプラグインが登場しない限り今後も使用、メンテナンスをしていきます。

asyncomplete-around.vimは非同期化されていないため場合によってはカクつくことがあるかもしれません。
そのため、まずは非同期化を進めていきたいと思います。

Pull RequestのコメントからGitHub Actionsを実行する

GitHubのPull Requestのコメントから任意のGitHub Actionsを実行する必要があり、やり方に少々癖があったので紹介します。

issue_comment

ドキュメントを眺めているとPull Requestに関連するイベントはいくつかありますが、Pull Requestのコメントをトリガーとしたイベントが無いように見えます。

どうにかPull Requestのコメントをトリガーに起動できないか調べていたらCommunity Forumで以下の投稿を見つけました。

github.community

この投稿によるとissue_commentイベントでPull Requestのコメントもトリガーとなるようです。
Issueへのコメントもトリガーの対象となるので、適切にフィルタリングする必要があると書かれています。

実装

今回は/planとコメントに入力することでterraform planが実行されるGitHub Actionsを作ってみたいと思います。

stepsまでのコードが以下になります。

name: "/plan"
on:
  issue_comment:
    types: [created, edited]

jobs:
  manual_plan:
    name: "manual plan"
    if: contains(github.event.comment.html_url, '/pull/') && startsWith(github.event.comment.body, '/plan')
    runs-on: [ubuntu-latest]
    steps:

ポイントはジョブ全体でifを設定して起動する条件を設定しているところです。

contains()でPull Requestのコメントのみに反応するようにし、startsWith()/planとコメントに入力された場合のみにしています。

実行されるブランチ

上記の設定のままstepsで処理を書いていけば意図した通りに動作しそうですが、1つ問題があります。
issue_commentをトリガーとした場合、actions/checkoutでpullできるブランチがmasterブランチになってしまいます。(正確にはリポジトリのデフォルトブランチ)

Pull Requestのコメントで実行するので、そのPull Requestのブランチでジョブを実行したいです。

actions/checkout@v2からはブランチを指定してpullできるようになりました。
Actionsをトリガーした内容が入った変数、つまり${{ github }}をもとにGitHubAPIを使ってPull Requestのブランチ名を取得します。
それをactions/checkout@v2にセットし、Pull Requestのブランチでジョブを実行できるようにします。

    steps:
      - name: get upstream branch
        id: upstreambranch
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          echo "::set-output name=branchname::$(curl -H "Authorization: token ${GITHUB_TOKEN}" ${{ github.event.issue.pull_request.url }} | jq '.head.ref' | sed 's/\"//g')"

      - name: Checkout upstream repo
        uses: actions/checkout@v2
        with:
          ref: ${{ steps.upstreambranch.outputs.branchname }}

Pull Requestに結果を書き込み

せっかくなのでplanの結果をPull Requestのコメントに書き込みたいと思います。

GitHubに関連する操作はactions/github-scriptを使えば大体の事ができます。

github.com

      - id: plan
        run: terraform plan -no-color

      - name: plan success message
        uses: actions/github-script@0.9.0
        if: steps.plan.outputs.exitcode == 0
        env:
          STDOUT: "```${{ steps.plan.outputs.stdout }}```"
        with:
          github-token: ${{secrets.GITHUB_TOKEN}}
          script: |
            github.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: "#### `terraform plan` Success\n" + process.env.STDOUT
            })

LFSをやってみたので振り返る

長期休暇を利用してLFSをやってみたので振り返ってみたいと思います。

きっかけ

日頃から業務でLinuxを使ってサービスを運用していますが、最近はクラウドやコンテナなどを使うことがほとんどでLinuxのほんの一部だけを知っていれば運用できています。
しかし、何らかの障害が発生したときの調査は平常時に使う一部の知識だけでは難しいことが多いです。
そんな状態から脱出すべく、以下の書籍を読みました。

gihyo.jp

www.sbcr.jp

これらを読んでいるとLinuxの仕組みがわかってきますが、ただテキストを読んでいるだけだとどうしても印象が薄くなってしまい忘れてしまうことが目立ってきました。
そこで何か手を動かしてできることがないか調べてみるとLFSLinux From Scratch)を見つけたので、やってみることにしました。

期待したこと

LFSをやることで以下の内容を取得できることを期待して手を動かしてみました。

  • Linuxの基盤となる部分を知る
  • サービスを運用するときの障害対応力を上げる

構築中に感じたこと

Linuxで使われるファイルやディレクトリを手作業で作成する

LinuxをインストールするとFHSに即したディレクトリが構築され、/etc/passwd/etc/groupには初期のユーザー情報が記述されています。
LFSで1から構築する場合はこれらのファイルやディレクトリも自分で作成することになります。
当たり前だと言えばそうかもしれませんが、なかなか新鮮な感覚でした。

ソフトウェアのバージョンを合わせることの重要性

LFSは構築に使うパッケージを全て自前でビルドします。
その関係上、まずはホストシステムでLFSの構築に使う環境の構築を行います。この環境の構築はホストにインストールされているソフトウェアを使うため、このバージョンが異なっていると予期せぬエラーに遭遇することもあります。

Linux KernelやGlibcなど、変更するのが大変なものもありますが、ホストのOSを変えるなどして、できる限り合わせる必要があります。
最低でもマイナーバージョンまでは揃えるべきです。

ホスト環境をチェックするスクリプトが載っているので、よくチェックすることをオススメします。

lfsbookja.osdn.jp

クラウドでは使用頻度が少ないコマンドを実行する

LFSを始めると最初に専用のパーティションを作成することになります。
パーティションを作成するためにfdiskコマンドを実行したり、それをマウントするためのmountコマンド、/ets/fstabへの記述、そして構築が進んでいくと作り途中のLFS環境に入るためのchrootコマンド、デバイスノードを作成するmknodコマンドなど、クラウドでサービスを運用しているとあまり使わないコマンドを実行します。

クラウド以外で運用する場合やLinuxに関連する開発を行う場合、これらのコマンドは定期的に使うことになるので久しぶりに実行して思い出すことができたのは良い機会だったと思います。

知らないこと、忘れていることはその場で調べる

これが今回の構築で一番良かったところです。

LFSのドキュメントはとてもよくできており、ほぼ全ての作業をコピペするだけで構築できます。
そのコピペするコマンドの中には上記の本で触れられているがあまり使わないコマンドや、そのそも出てこないものなどもあります。
それらのコマンドを知らないままでもコピペで実行すれば構築できてしまいますが、コマンドのオプションも含め、知らないものは全て調べます。
時間はかかりますが勉強になります。

また、LFSでは全てのパッケージをビルドするため、インストールする全てのライブラリに触れます。
その中には知らないパッケージや生成されるコマンドがあると思います。
LFSのドキュメントに簡単には説明が書いてありますが、これも調べるととても勉強になります。

期待したものは得られたか

構築が完了した時点で期待した成果が得られたのか振り返ります。

Linuxの基盤となる部分を知る

これに関しては十分に達成できたと思います。
ただ、ライブラリやコマンドはビルドしただけなので、ふつうのLinuxプログラミングで書かれている内容をもう一度やってみるとさらなる理解が得られそうな気がしています。

サービスを運用するときの障害対応力を上げる

障害対応力は構築が完了した段階だとまだ上がった実感はありません。
少なからず関連はしていると思うので、実際に対応を行ってみて変化を感じるかどうか様子を見てみたいと思います。

今後にむけて

サービスをLinuxの上で運用していくことはまだしばらく続くと思うので、さらなる理解と障害対応力を磨いて行きたいと思います。
そのために次はこれらの書籍をよんでみようかと思っています。

www.seshop.com

www.oreilly.co.jp

TerraformのStateをS3からTerraform Cloudに移行する

TerraformのStateをチームで管理するときに、複数の環境からplanやapplyを実行できるようにするためにS3などのクラウドのストレージをバックエンドにすることが多いと思います。

通常のストレージをバックエンドにしていると、複数の環境から同時にapplyが実行されたときに壊れてしまう可能性があります。CIで実行するようにしていても、設定が適切ではなく平行に動いてしまうような場合などに壊れる可能性があります。

Terraform CloudにはStateのロック機能がありこのような事故から開放されるため、Stateの管理をS3からTerraform Cloudに移行したいと思います。

環境

  • Terraform v0.12.24
  • 元のBackendはS3
  • Terraform Cloudのアカウントは作成済み

手順

Terraform Cloudへのアクセス設定

まずはterraformコマンドがTerraform Cloudにアクセスできるようにします。

terraformのバージョンが0.12.21以降の場合は terraform login コマンドを実行して、ブラウザに表示されたトークンを入力すれば完了です。

www.terraform.io

loginコマンドが使えない場合は $HOME.terraformrc というファイルを作成し、以下のようにトークンを設定します。

credentials "app.terraform.io" {
  token = "xxxxxx.atlasv1.zzzzzzzzzzzzz"
}

また、TF_CLI_CONFIG_FILE という環境変数を設定すれば、任意の場所に任意の名前で設定ファイルを作成できます。

www.terraform.io

www.terraform.io

backendの書き換え

S3の設定が書かれていたbackendをTerraform Cloudのものに書き換えます。

  backend "s3" {
    ...
  }

これを削除して以下のように書き換えます。

  backend "remote" {
    hostname     = "app.terraform.io"
    organization = "your_organization_name"

    workspaces {
      name = "your_workspaces_name"
    }
  }

移行

準備が整ったので移行させていきます。

terraform init を実行するだけで移行ができます。
実行すると以下のように聞かれるので yes を入力します。

$ terraform init

Initializing the backend...
Backend configuration changed!

Terraform has detected that the configuration specified for the backend
has changed. Terraform will now check for existing state in the backends.


Terraform detected that the backend type changed from "s3" to "remote".
Acquiring state lock. This may take a few moments...
Do you want to copy existing state to the new backend?
  Pre-existing state was found while migrating the previous "s3" backend to the
  newly configured "remote" backend. No existing state was found in the newly
  configured "remote" backend. Do you want to copy this state to the new "remote"
  backend? Enter "yes" to copy and "no" to start with an empty state.

  Enter a value:

yes を入力し、問題なく成功すれば以下のように表示されます。

Successfully configured the backend "remote"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

これで移行が完了です。

参考

Kubernetes上で動くGoサーバーでプロファイラを動かしWeb UIで表示する

Goにはプロファイラとして標準パッケージにpprofが搭載されています。
pprofの使い方としてはすでに多数の優良記事が存在するため、ここでは扱いません。

今回はpprofをk8s上で動くサーバーに対して実行し、結果をWebUIで表示する必要があったのでそのやり方を紹介します。

環境

インフラ構成

  • GoサーバーのHTTP API用のPortが80
  • pprof用のPortが6060
  • プロファイラ結果のWeb UI用のPortが8080
    • このPortをServiceのNodePortでMacに公開

やり方

pprofの設定

まずは通常通りpprofの設定をしてGoをビルドします。

import (
    ...
    _ "net/http/pprof"
)

func main() {
    ...

    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()

    ...
}

このバイナリがk8sのPodで動いているとします。

Serviceの定義

通常のAPIへのアクセスに使うServiceが以下です。

apiVersion: v1
kind: Service
metadata:
  name: my-service
spec:
  selector:
    app: my-service
  ports:
    - name: http
      protocol: TCP
      port: 80
      targetPort: 80

これに追加してpprofのWebページにアクセスするためのServiceを定義します。
このServiceのTypeをNodePortにすることでお手軽にアクセスできるようになります。

apiVersion: v1
kind: Service
metadata:
  name: my-service-pprof
spec:
  selector:
    app: my-service-api
  type: NodePort
  ports:
    - name: http-pprof
      protocol: TCP
      port: 8080
      targetPort: 8080
      nodePort: 30080

pprofの実行

Goのバイナリが動くPodに入り以下のコマンドでpprofを実行します。

$ pprof -http=0.0.0.0:8080 /usr/local/bin/my-api http://localhost:6060/debug/pprof/profile

ここで大事なのはpprofのWebページのアドレスを 0.0.0.0 にすることです。ここを 127.0.0.1 にしていると外部からアクセスできなくなってしまいます。

あとは計測したいエンドポイントにアクセスし以下のようなメッセージが表示された後、ホストからNodePortの 30080 にアクセスすればプロファイル結果がWebUIで表示されます。

Saved profile in /root/pprof/pprof.my-api.samples.cpu.001.pb.gz
Serving web UI on http://0.0.0.0:8080
Couldn't find a suitable web browser!
Set the BROWSER environment variable to your desired browser.