GitHub Actions(beta)向けにslack通知プラグインを作った

839.hateblo.jp でcircleciからgithub actionsに移行したもののslack通知周りが整ってないので作った。
自分が観測している範囲だと現状 https://github.com/pullreminders/slack-action しかなかったけど、あまりにもpayloadを書くのがきつそうなので敬遠していた。
あとdockerベースだと毎回このプラグインのimageが先頭でbuildされるのも速度的に気になっていた。

元々もっと困っていたのはキャッシュ周りで、それっぽいAPIがあったので調査してみたが結局現状できない、という結論になったので次はslackかなーと作り始めた。
nodejsでTypeScriptを使って書いた、templateリポジトリを使って作ると最初から最低限のCI環境(GitHub Actions)が整っているのでDX良いなーと感じた。

https://github.com/actions/javascript-template のtemplateを使って作り始める方法は https://help.github.com/en/articles/development-tools-for-github-actions のあたりに書いてある。
作ってて気づいたけど普通にbadgeの機能も提供されてて便利、ただ今はmasterブランチのものしか取れないっぽい?

f:id:husq:20190816031503p:plain

f:id:husq:20190816031554p:plain

とりあえずこんな感じで通知できるようにしている。
通知の指定方法は以下の感じぐらい。

- uses: 8398a7/action-slack@v1
  with:
    type: success
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
- uses: 8398a7/action-slack@v1
  with:
    type: failure
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

typeを指定すれば他の要素はこちら側でよしなに受け持つスタイルにしている。 毎回長文のyamlを書きたくないのでできるだけ簡潔にしたいという思いがこもっている。

一応カスタム通知をしたいという人向けにカスタムも用意した。

f:id:husq:20190816031754p:plain

- uses: 8398a7/action-slack@v0
  with:
    payload: |
      {
        "text": "Custom Field Check",
        "attachments": [{
          "author_name": "slack-actions",
          "fallback": "fallback",
          "color": "good",
          "title": "CI Result",
          "text": "Succeeded",
          "fields": [{
            "title": "short title1",
            "value": "short value1",
            "short": true
          },
          {
            "title": "short title2",
            "value": "short value2",
            "short": true
          },
          {
            "title": "long title1",
            "value": "long value1",
            "short": false
          }],
          "actions": [{
          }]
        }]
      }
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

slack payloadを自分でjson形式で指定する方法。
withで受け取れる値が現状stringしかないので、汎用的に作るなら今の所この方式しかなさそうだった。
そのままyamlがparseできればobjectなりarrayなりが使えるようになるのでもう少し書きやすい形も提供できると思うけど…。

シュッと作ってしまったのでまだまだよしなに受け持つ通知は改善の余地があるけど一旦v1.0.0ということでリリースした。

github.com github.com

現状github contextの型が不十分*1jsonとしては値が入っているのに使えないケースがまぁまぁある。
本当はjobのstatusもこのプラグインの中でハンドルして、何も指定されていない場合はsuccess or failureを自動判定するようにしたいところ…。 実際に使う場合は以下のような指定になる。

    - uses: 8398a7/action-slack@v1
      with:
        type: failure
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
      if: failure()
    - uses: 8398a7/action-slack@v1
      with:
        type: success
      env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
      if: success()

良いやつが出るまではこれをメンテしていこうと思うので気になる所があればぜひissue / PRください。

circleciのbuild/test/deployをgithub actions(beta)に移行した

まだ機能的に足りないところもあるが、頑張ったら使える感覚だった。
githubにもfeedbackが送れる所があれば送ろうと思う。

circleciでやっていたことはざっくり書くと以下。

  • test系
    • golangのbuild/lint/test
    • helm chartのlint
    • helm templateで吐き出されたyamlのlint
  • build系(only master)
    • base imageのbuild & push
    • k8s上で動かすprod imageのbuild & push
  • deploy系(only master)
    • GKE上にhelm secrets upgrade

これをgithub actionsに移行した際にcircleciとの差分を感じた機能は以下。

  • slack通知
  • filter機能
    • yml単位でしかbranchのfilter機能が存在しない
      • 1つのworkflowに複数jobを定義して、jobの依存関係を持たせているときに困る
      • e.g. featureブランチではtest jobだけ走らせるが、masterブランチではtest/buildを並列して走らせ、両方とも成功したらdeployを行う
    • circleciのようにjob単位でbranch filterが行えるようになっていてほしい
      • workflow(yml)を分けた場合は別workflowのjobの状態検知ができないことは確認
  • cache機能
    • azure pipelineの方には一応あるようだが、github actionsにはドキュメントを読んだ感じだと見当たらなかった
    • circleciはfreeかつprivateだと並列に走らせられないが、github actionsは並列実行できるので結果的にトントンぐらいの速度で終わっている
      • circleciではbuild/testが直列だったが、github actionsになって並列実行になったためトータルの待ち時間が減ったがキャッシュがないのでjob単位の実行速度は伸びた
    • dirty hackをすればおそらくキャッシュも可能
  • artifacts
  • environment variable
    • step単位でしか指定できないので全てのstepで指定したいenvironment variableがあると記述が冗長になる
  • secrets
    • env keyでのみ指定できる模様
      • circleciはenvとsecretが同一のような扱い(envは管理画面でもfilterされる)だが、github actionsはやや扱いが異なる
    • 複数回使うsteps.ifの比較値をsecrets経由で一元管理にしようとしたが、使えないらしく弾かれた
  • dockerのprepare
    • circleciでは明示的に指定する必要があったが、github actionsでは特にしていなく使えるのはDXがよかった
  • merge時の挙動
    • github actionsはmergeした際は2回actionが走る模様
      • github.event.afterが0000000000000000000000000000000000000000だったり、github.event.commitsが空配列のようなactionを確認
    • これは不具合かも?と思ったが何か理由があるのかもしれない、自分で使う際は2回走ると完全に無駄なのでsteps.ifでfilterしている
      • twitterで教えてもらったがbranch delete eventを拾っていた、github contextを見てみると確かにgithub.event.deleted.がtrueになっていた*4

yml自体の書き味の違いはざっくりと書くと以下の感じ。

circleci

version: 2.1
orbs:
  gcr: circleci/gcp-gcr@0.6.1
  gke: circleci/gcp-gke@0.2.0
  docker: circleci/docker@0.5.13
jobs:
  test-job:
    working_directory: /go/src/github.com/8398a7/app
    docker:
      - image: circleci/golang:1.12.6-stretch
        environment:
          CI: "true"
          GO111MODULE: "on"
    steps:
      - checkout
      - restore_cache:
          key: prepare-ci-tools-{{ checksum "scripts/prepare-ci-tools.sh" }}
      - run:
          name: prepare ci tools
          command: scripts/prepare-ci-tools.sh
      - save_cache:
          key: prepare-ci-tools-{{ checksum "scripts/prepare-ci-tools.sh" }}
          paths:
          - ./bin
      - run:
          name: helm-lint
          command: scripts/helm-lint.sh
      - run:
          name: kubeval
          command: scripts/kubeval.sh
      - restore_cache:
          key: go-mod-{{ checksum "go.mod" }}
      - run:
          name: go mod download
          command: go mod download
      - save_cache:
          key: go-mod-{{ checksum "go.mod" }}
          paths:
          - /go/pkg/mod/cache
      - run:
          name: golangci-lint run
          command: bin/golangci-lint run
      - run:
          name: test
          command: make test
      - run:
          name: build
          command: make build
      - store_artifacts:
          path: /go/src/github.com/8398a7/app/cover.html
  build-job:
    working_directory: /go/src/github.com/8398a7/app
    docker:
      - image: google/cloud-sdk
    steps:
      - checkout
      - setup_remote_docker
      - docker/check
      - run:
          name: pull app-base
          command: docker pull 8398a7/app-base:latest
      - docker/build:
          image: 8398a7/app-base
          dockerfile: build/Dockerfile
          extra_build_args: --cache-from 8398a7/app-base:latest
          tag: latest
      - docker/push:
          image: 8398a7/app-base
          tag: latest
      - gcr/build-image:
          image: app
          dockerfile: build/app/Dockerfile
          tag: $(get tag)
      - gcr/gcr-auth
      - gcr/push-image:
          image: app
          tag: $(get tag)
  deploy-job:
    machine: true
    environment:
        HELM_HOME: /home/circleci/.helm
        GOOGLE_APPLICATION_CREDENTIALS: /home/circleci/gcloud-service-key.json
    steps:
      - checkout
      - restore_cache:
          key: prepare-cd-tools-{{ checksum "scripts/prepare-cd-tools.sh" }}
      - run:
          name: prepare cd tools
          command: scripts/prepare-cd-tools.sh
      - save_cache:
          key: prepare-cd-tools-{{ checksum "scripts/prepare-cd-tools.sh" }}
          paths:
          - ./bin
      - gke/install
      - gke/init
      - run:
          name: get credentials
          command: gcloud container clusters get-credentials $GKE_CLUSTER
      - run:
          name: upgrade app
          command: bin/helm secrets upgrade prod-app ./deployments/app --install --wait --namespace app -f ./deployments/values.yaml -f ./deployments/secrets.yaml --set app.image.tag=$(get tag)
workflows:
  version: 2
  test-build-deploy-workflow:
    jobs:
     - test-job
     - build-job:
         filters:
           branches:
             only: master
     - deploy-job:
         requires:
            - test-job
            - build-job
         filters:
           branches:
             only: master

github actions

name: CI

on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@master
    - uses: actions/setup-go@v1
      with:
        version: 1.12.6
      id: go
    - run: scripts/prepare-ci-tools.sh
    - run: scripts/helm-lint.sh
      env:
        CI: true
    - run: scripts/kubeval.sh
      env:
        CI: true
    -  run: go mod download
    - run: bin/golangci-lint run
    - run: make test
    - run: make build
    - uses: actions/upload-artifact@master
      with:
        name: coverage
        path: cover.html
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@master
      if: contains(github.ref, 'master')
    - name: docker login
      run: echo $DOCKER_PASSWORD | docker login -u 8398a7 --password-stdin docker.io
      if: contains(github.ref, 'master')
      env:
        DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
    - name: pull app-base
      run: docker pull 8398a7/app-base:latest
      if: contains(github.ref, 'master')
    - name: build app-base
      run: docker build -t 8398a7/app-base:latest --cache-from 8398a7/app-base:latest -f build/Dockerfile .
      if: contains(github.ref, 'master')
    - name: push app-base
      run: docker push 8398a7/app-base:latest
      if: contains(github.ref, 'master')
    - name: build gcr.io/gcp_project/app
      run: docker build -t gcr.io/gcp_project/app:$(get tag) -f build/app/Dockerfile .
      if: contains(github.ref, 'master')
    - name: install gcloud sdk
      run: |
        export CLOUD_SDK_REPO="cloud-sdk-$(lsb_release -c -s)"
        echo "deb http://packages.cloud.google.com/apt $CLOUD_SDK_REPO main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
        curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
        sudo apt-get update && sudo apt-get install -y google-cloud-sdk
      if: contains(github.ref, 'master')
    - name: initialize gcloud sdk
      run: |
        echo $GCLOUD_SERVICE_KEY > ${HOME}/gcloud-service-key.json
        gcloud auth activate-service-account --key-file=${HOME}/gcloud-service-key.json
        gcloud --quiet config set project gcp_project
        gcloud --quiet config set compute/zone gcp_zone
        gcloud auth configure-docker --quiet --project gcp_project
      env:
       GCLOUD_SERVICE_KEY: ${{ secrets.GCLOUD_SERVICE_KEY }}
      if: contains(github.ref, 'master')
    - name: push gcr.io/gcp_project/app
      run: docker push gcr.io/gcp_project/app:$(get tag)
      if: contains(github.ref, 'master')
  deploy:
    runs-on: ubuntu-latest
    needs: [test, build]
    steps:
    - uses: actions/checkout@master
      if: contains(github.ref, 'master')
    - name: install gcloud sdk
      run: |
        export CLOUD_SDK_REPO="cloud-sdk-$(lsb_release -c -s)"
        echo "deb http://packages.cloud.google.com/apt $CLOUD_SDK_REPO main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
        curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
        sudo apt-get update && sudo apt-get install -y google-cloud-sdk
      if: contains(github.ref, 'master')
    - name: initialize gcloud sdk
      run: |
        echo $GCLOUD_SERVICE_KEY > ${HOME}/gcloud-service-key.json
        gcloud auth activate-service-account --key-file=${HOME}/gcloud-service-key.json
        gcloud --quiet config set project gcp_project
        gcloud --quiet config set compute/zone gcp_zone
      env:
       GCLOUD_SERVICE_KEY: ${{ secrets.GCLOUD_SERVICE_KEY }}
      if: contains(github.ref, 'master')
    - run: scripts/prepare-cd-tools.sh
      env:
        HELM_HOME: /home/runner/.helm
      if: contains(github.ref, 'master')
    - name: get credentials
      run: gcloud container clusters get-credentials gke_cluster
      if: contains(github.ref, 'master')
    - name: upgrade app
      run: bin/helm secrets upgrade prod-app ./deployments/app --install --wait --namespace app -f ./deployments/values.yaml -f ./deployments/secrets.yaml --set app.image.tag=$(get tag)
      env:
        HELM_HOME: /home/runner/.helm
        GOOGLE_APPLICATION_CREDENTIALS: /home/runner/gcloud-service-key.json
      if: contains(github.ref, 'master')

見ての通り、circleciのjob単位branch filter機能がないのでそれに該当するものは全てにsteps.ifでブランチのfilterをかけている。

  • また、masterマージ時の2回発火を防ぐためgithub.event.afterでも条件を書いている。
    • また、master時にbranch delete eventでjobを走らせないためにgithub.event.deletedを見て条件を書いている。
    • 8/17頃からbranch delete eventでは発火しなくなったようなので条件を書く必要がなくなった

正直全てのstepで同じifを何度も書くのは見栄えもメンテナンス性も悪いので、job単位branch filterはほしいなぁと感じた。
ちなみにworkflowをrerunさせたときにはgithubで取得できる値がちょっと違うようで、deploy系がうまく動作しなかった。

  "ref": "refs/heads/master",
  "sha": "dd47c626ef90ca4ee193d02a1cc0a253a5ba53a6",
  "repository": "8398a7/app",
  "repositoryUrl": "git://github.com/8398a7/app.git",
  "actor": "8398a7",
  "workflow": "CI",
  "head_ref": "",
  "base_ref": "",
  "event_name": "push",
  "event": ***
    "action": "rerequested",
    "check_suite": ***
      "after": "dd47c626ef90ca4ee193d02a1cc0a253a5ba53a6",
      "app": ***

event.check_suiteの下にafterが生えるようになっていた。
"description": "Powers your .github/main.workflow.", とか書かれてるし、まだHCLのものの挙動を引きずってたりするのかもしれない…。

上記のような感じで移行してみたが、circleciのfree plan上限が割と厳しい月もあるのでgithub actionsに移行してこのあたりは解消されそうなのがよかった。
GAになるまでに改良されたり変更されたりすると思うので継続的に更新していきたいと思う。

GKEのsentryでfilesにGCSを使う

Sentry(sentry:9.1.1-onbuild)をGKE上で構築しようとした際にfilesをGCSに置きたかったので調べてみた。 9.1.1のimageを使うとsentry.conf.pyにはデフォルトでは以下のように書かれている。

    ################
    # File storage #
    ################
    # Uploaded media uses these `filestore` settings. The available
    # backends are either `filesystem` or `s3`.
    SENTRY_OPTIONS['filestore.backend'] = 'filesystem'
    SENTRY_OPTIONS['filestore.options'] = {
        'location': env('SENTRY_FILESTORE_DIR'),
    }

上記のデフォルトではlocalにファイルが置かれるようになっているが、volumeを割り当ててやる方法でも回避できる。 ただvolumeマウントはお金も高いのであまりやりたくなかった。

s3が指定できるならGCSもいけるのでは?と思って調べてみるとどうやらいけるっぽい。 9.1.1にもgcs周りのソースファイルは入っているようだったので設定するだけで行けそうということで試してみた。

    # Uploaded media uses these `filestore` settings. The available
    # backends are either `filesystem` or `s3`.
    SENTRY_OPTIONS['filestore.backend'] = 'gcs'
    SENTRY_OPTIONS['filestore.options'] = {
        'bucket_name': env('SENTRY_FILES_BUCKET'),
    }
    ##############

設定を見てもらうとわかる通り、credential系の設定はしていないのでSA経由で権限確認をするらしい。 GitHub上のコードを確認すると以下のように書かれている。

# NOTE: See docs/filestore for instructions on configuring the shell environment
#       with authentication credentials for Google Cloud.

権限はGKEのnodeに与えておき、動かしてみると以下のエラーが出た。

Error importing module sentry.filestore.gcs: "No module named google.cloud.storage.client"

どうやらデフォルトではgcs関係のライブラリが入っていない模様。 sentryのdocker onbuild版はカスタムイメージを簡単に作れるようになっているので以下のようにしてgcsのライブラリを入れたイメージを作成。

ファイル構成

sentry
    ├── Dockerfile
    └── requirements.txt

Dockerfile

ARG TAG
FROM sentry:${TAG}-onbuild

requirements.txt

google-cloud-storage>=1.13.2,<1.14

sentryディレクトリに入って

docker build  --build-arg TAG=9.1.1 -t sentry:9.1.1 -f Dockerfile .

とすればgcsのライブラリが入った9.1.1のイメージができるのでGCRのほうにアップロードしてGKEから使うようにした。 その後、sentry上でプロフィールやorganizationの画像を適当に変更するとGCSにちゃんとアップロードできることを確認。

これでvolumeを使わず安く画像を永続化できるようになった。 なんだかんだいってpv/pvcの管理は割と面倒なので使わないに越したことはない。

IntelliJ IDEAのOpen Source Licensesを取得した

OSSにコミットしている実績があればライセンス取得が可能と聞いたので申請してみた
申請から2日ほどで通り、各種IDEが利用可能になった

現時点では以下のIDEが使える

  • AppCode
  • CLion
  • DataGrip
  • dotCover
  • dotMemory
  • dotTrace
  • GoLand
  • IntelliJ IDEA Ultimate
  • PhpStorm
  • PyCharm
  • ReSharper
  • ReSharper
  • C++
  • Rider
  • RubyMine
  • WebStorm

自分が使いたかったのはGoLandだけだが、色々使える模様
申請する際に利用したOSSRubyだが、全て使えるようになるとは太っ腹…
とりあえずIntelliJ IDEA UltimateとGoLandだけActivateした

申請するフォームは以下から遷移できる www.jetbrains.com

ページのAPPLYをクリックしてフォームへと進む f:id:husq:20190627225953p:plain

主要なものだけピックアップしてフォームに入力した内容は以下

  • Do we know you?
    • No, we are a new customerを選択
  • Tell us about your project
    • Project name
      • 自分のOSSのプロジェクトネームを入力
    • Primary language(s)
      • 使っている言語にチェック、別にどの言語を入力しても通れば全部使えるらしい
    • Project age
      • プロジェクトの期間、自分の場合は54ヶ月だった
    • Project website
      • WebAppのOSSだったのでそのままURLを入力
    • Repository URL
    • Latest release URL
      • GitHubのReleases URLを入力
    • License URL
      • MITのLisenceファイルをコミットしていたのでそのURLを入力
    • No. of active contributors
      • 自分しかいないので1
    • Project description
      • プロジェクトの概要、利用者は基本コードが書けないので自分が作ってるという風に書いた
  • Tell us about yourself
    • Your role in project
      • Ownerとか書いても良かったのかもしれないがURLを書くところっぽかったのでContirubutorsのURLを貼り付けた

申請が通った後、ライセンスの適用に少し悩んだので一緒に記載しておく
まず通るとメールが来るのでメールアドレスを登録してJetBrainsアカウントを作成する
ログインすると以下のページが見える場所があるので赤枠のところをクリック

f:id:husq:20190627231112p:plain

次にManage -> Request 1 activation codeを選択

f:id:husq:20190627231229p:plain

落としたtxtの中にactivationに利用するcodeが入っているのでコピっておく

  1. Help -> Registerからアクティベーションの画面を出す
  2. Activation codeにコピったコードを貼り付けてOKを選択

あとは上記の手順でactivationが完了する
JetBrains Accountのほうでactivationできると思っていたらできなかったので少し悩んだ

1年後も使っていたら更新方法の記事も記述したい

Certified Kubernetes Administrator(CKA)を取得した(19/06 v1.14)

f:id:husq:20190617201800p:plain
Certified Kubernetes Administrator

CKAを受けたので受験ログをまとめておく
今後受ける人の参考になれば

バックグラウンド

  • インフラ専任ではないがk8s(GKE)はそこそこ触っている
    • GKE contextとhelmが多め
  • k8s/GCPを真面目にデビューしてから4ヶ月ぐらい
    • 試験のためには結構勉強はした(15hぐらい?)

勉強概要

  • vagrant上でのk8s環境構築、実施
    • cka-practice/ckad-practiceの実施環境
    • k8s.ioのkubeadmページを見ながらvagrantクラスタ構築
    • cloud managed k8sでの学習は微妙にcloud contextが入るので個人的にはvagrant推奨
  • k8s the hardwayを2周
    • 後述するが1周で十分
  • linux academyのk8sコース受講
    • これは受けなくても良かったかも

結果

1問解けなかった問題があり、それが8%分あった

結果としては84/100%、ボーダーは74%だった
解けなかったと思った問題はなかったが92%から8%分何かとちっているらしい
結果は試験終了から大体31時間ほどで来た

Verify a Linux Foundation Certified Professional | Linux Foundation Training
のサイトから結果を確認できる(CKA-1900-002255-0100/Tanaka)

試験のルールとか概要

すべて受験時のもの
受ける際にExpert Certification - Cloud Native Computing Foundationを確認されたし
pdfなので保存した後にgoogle翻訳でファイルアップロードを使って読むと楽

  • k8sのバージョンはv1.14系
  • 閲覧してよいドキュメントは以下の3点
  • 試験問題は日本語が選択可(ちゃんと意味がわかる日本語だった)
  • 試験会場は貸し会議室を利用*1
  • chromeにextensionを入れてデスクトップ共有される
  • カメラでモニタリングされる
    • 試験会場を確認される際にも利用される
  • 試験開始15分前からポータルで試験準備ページに飛べる
    • 15分前から試験官立ち会い(リモートだけど)のもと作業を進めたほうがいい*2

当日の流れ

閲覧可なドキュメントは上記の述べた3種類だが、ほぼk8s.io/docsしか見ていない*3

試験ページは15分前から解放されると認識していたが5分前に開いたら思ったより時間がかかった
結果的に開始が15分遅れて、貸し会議室の時間が大丈夫かヒヤヒヤした(後ろにマージン30分持たせておいたのでセーフだった)
机を移す際は4つの角が全部映るぐらいカメラを引いてほしいと言われたがちゃんと映ってるか確認しながらmacのインカメで映すのが微妙に大変だった

試験前の準備は試験官と英語のチャットで行うが、英語力が低すぎて*4手間取ったのもある…
試験問題自体はちゃんと読める日本語だったので本当に助かった

試験が終わるとアンケートページが出てくるので答えて全部終わり
結果は3日以内にメールとポータル上で確認できるとのこと

試験中は1度だけ試験官に顔がちゃんと映っていないと注意された
多分ちょこちょこ映ってなかったと思うがある程度はスルーしてるのかもしれない
口元を隠すと注意されるというのは見ていたのでそのへんの仕草は意図的にしないとように心がけていた

試験の所感

3時間枠だが、1時間半ほどで1問を除いて解き終わった
1問は圧倒的に難しいやつだったので意図的に飛ばし、それらを除いて一旦見直しで30分ちょっと
ラスト1時間はその問題に取り組んでいたが、難しそうなので10分前に諦めて試験終了(部屋の時間も少し不安だったし)

圧倒的に難しい1問は他の92%達成率の方もブログで触れていたのでこれかぁ〜という気持ちになった
オンプレクラスタの知見がかなりある人じゃないと解けなさそうとは感じた

事前にやったことがないような問題もあったが、k8s.io/docsを見ればなんとかなるな〜という感じの問題だった

やった内容に深く関係するリンク

vagrant環境で勉強した内容

k8s構築で試した構成は以下の感じ

  • cni
    • calico
    • flannel
    • loopback*6
  • container runtime
    • docker
    • crio(試したけど動かなかったので実質やってない)
    • containerd
    • gVisor*7

のあたりの構成で構築した
candidate handbooksに

The Certified Kubernetes Administrator (CKA) and Certified Kubernetes Application Developer (CKAD) exams are released on Ubuntu 16.

と記載されているのでboxはubuntu/xenial64を使った

構築関係で試したのは以下のあたり

  • kubeadmを使ってflannel構成で構築した後、同じVMでcalicoに構成変更する
  • kubeadmを使ったupgrade(master/worker)*8
    • workerをアップデートするための手法(kubeadm drainなど)
  • container runtimeをnodeによって切り替えてみる
    • node単位でdocker使ったりcontainerd使ったり
  • masterでもpodを動かすようにする
    • taintを外すだけ
  • hpaがちゃんと動作するか確かめる
    • metrics-serverを入れる必要があるが、現時点(19/06)では軽微な修正が必要

自分が受けた時点ではv1.14系でどういうクラスタを使うかも明示されていた
masterが複数構成になっているクラスタはなかったのでそういった理由でsingle master clusterの構築で色々試した

勉強環境の準備

基本的にいつも使っているようなdotfiles環境はないのでplainな環境でやるように心がけていた

先述の通りvagrant環境を構築したので一応公開しておく
GitHub - 8398a7/cka-practice: The vagrant environment used when practicing CKA.

provisionしているshの役割はそれぞれ以下の通り

  • 00: k8s.io/docsの手順によるdocker install
  • 01: k8s.io/docsの手順によるkubelet kubeadm kubectlのinstall
    • version変数を変えれば古いバージョンでの構築も可能
    • 1.13.7から1.14.3への更新とかはこのあたりを調整して試した
  • 02: kubeletの設定
    • /etc/systemd/system/kubelet.service.d/10-kubeadm.conf の引数にvmのprivate ipを付与
  • 03: master nodeのセットアップ
    • kubeadm initの実行を行い、出力されたログを見てworker nodeでkubeadm joinを実行する
  • 04: calicoとkubeconfigのセットアップ
    • privileged: falseを指定してvagrantユーザで実行させるようにしている
    • network policy周りの検証をするならcalicoなのでprovisionする場合はcalicoを選択

master nodeは00-04まで、worker nodeは00-02までを行う
worker nodeは02まで終わったらmaster nodeの03を待った上で手動でkubeadm joinを行っている

最低限使うものだけは空で打てるように覚えた

bashrc

source <(kubectl completion bash) # これはデフォルトのnodeではされているっぽくて不要だった

vimrc

set number
set relativenumber
set expandtab
set smartindent
set tabstop=2
set shiftwidth=2
imap <C-j> <Esc> # 一応定義しているが試験中は諸事情により使ってない

tmux

unbind C-b
set -g prefix C-t # C-bはemacsキーバインドと被ってるので変えている
set -g default-terminal "screen-256color" # 色付け
bind h select-pane -L # 画面の移動はvimキーバインドで移動したい
bind j select-pane -D
bind k select-pane -U
bind l select-pane -R

これぐらいあれば個人的にはだいたいストレスなく作業できる(通常環境に比べると剥離は激しいが)
どっちみちpodの中とかで調査する際はdotfilesは使えないわけでちょっとした機能拡張で快適に作業できるようにしておいたほうが良いなとは今回感じた
vim(vscode)もimapを定義しているが最近はEscももっぱらC-[になってしまった

ちなみに試験はブラウザ上のターミナルで行うが、BetterTouchToolsとvimiumが悪さしてimapはちゃんと動かなかった
Esc相当はとりあえずC-cで代用できたが、これができてなかったらyamlの編集に壊滅的ダメージになるところだった…

kubectlの補完は使い方を覚えていると本当に便利なので絶対に習得したほうがいい
deploymentとかで作られたpodの補完もそうだが引数の補完もかなり便利

勉強期間中は結構explainも使っていたが、実際問題見にくいのでdocsで内部検索をしたほうが見やすく出る
docsで案内されていないようなものに関してはexplainを叩いてみるぐらいでもいいかも*9
kubectlコマンド系もhelpが充実しているので大体はdocsを見なくてもわかるようになっているなと感じた

kubectl系で覚えておいたほうが良いのは以下

kubectl run nginx --image=nginx --restart=Never # pod
kubectl run busybox --image=busybox --restart=Never --rm -it -- sh # 一時作業用pod
kubectl run nginx --image=nginx --replicas=2 # deployment, replicasはなくても良い
kubectl run nginx --image=nginx --expose --port=80 # deployment, service
kubectl run busybox --image=busybox --restart=OnFailure -- date # dateコマンドを打って終了するjob
kubectl run busybox --image=busybox --restart=OnFailure --schedule="*/1 * * * *" -- date # dateコマンドを打って終了するcronjob

それぞれ --dry-run -o yaml をつけてyamlを吐き出すこともできるのでkubectl runで指定できないパラメタはつけるといい

kubectl run nginx --image=nginx --restart=Never -o yaml --dry-run > pod.yaml
# vim pod.yamlでvolumes等を編集する

kubectl run はdeprecatedなものがちょこちょこあるので本筋に乗っかるなら kubectl create 系で覚えたほうがいいかもしれない
ただrunに比べて色々指定できるパラメタが少なくてyamlの編集量が増えることと自分の試験期間中は大丈夫ということもあってrun系のコマンドを習得した

勉強中に取り組んだkindのタイプ(ぱっと思い出せる範疇)としては以下

  • Pod
  • Service
  • Deployment
  • DaemonSet
  • Secret
  • hpa
  • PersistentVolume
  • PersistentVolumeClaim
  • Job
  • CronJob
  • NetworkPolicy

pv, pvc周りに関しては素のk8sなのでGKEとかで演習していると試験構成とずれるので注意*10
kubeadmとか使って構築しておいたほうがk8sへの理解も深まるのでvagrantを使った素のk8s学習が良いと感じた

手元のvagrantでcniはflannelとcalicoを試した
flannelはnetwork policyでpodとかにアクセス制限してても効かないのでckad-practiceでnetwork policyの問題を解くときはcalicoでセットアップしている必要がある

README通りにやっても動かなかったものたち

総括

マネージドでk8sを使ってるとあまり理解していないようなことが色々と把握できて有意義な試験だったと思う
トラブルシュートする際に雑にpod作ったり、yaml書いたりして調査するスキルも上がったなぁと感じる

次はCKADでも取ってみようかな

*1:自室だとルールが微妙にグレーっぽくてややこしかったので

*2:自分の場合は要領が悪くて結構時間がかかった

*3:https://kubernetes.ioが正式ドメインだがhttps://k8s.ioでリダイレクトしてくれる

*4:ok, ok?, yes, sorry, thanksぐらいしか発言してない

*5:お世話になったのでPRを投げた

*6:これはkubernetes the hard way中で構築したのでGCP

*7:これもkubernetes the hard way中で構築したのでGCP

*8:細やかな不備があったのでPRを投げたり、issueにコメントしたり

*9:explainにもリンクが載っているケースがあるのでまずexplainでも良い

*10:pvcを作るとpvが割り当てられるとか

Windowsのdocker for desktop(k8s)をmacから使う

家では基本的に開発作業はmac、その他全般をWindowsで行っている。 スペックはWindowsのほうが高いので、Windowsのdocker serverやk8sを使えないかなーというところから。

結論を言うとfirewallとproxyの設定をすれば両方共利用可能。

なお、この記事の手法にはwsl環境を前提としている。 wslの設定方法は適当にググったら出てくると思う。 ただ、nginxさえ使えれば特にwsl環境でなくても同じことはできるはず。

WindowsのDocker ServerをmacOSから使う

まずdockerのSettingsで Expose daemon on tcp://localhost:2375 without TLS にチェックを入れてtcp 2375 portでdocker daemonに繋げるようにする。 f:id:husq:20190522221013p:plain このあたりはwsl環境でdockerを使っている人なら利用したことがあると思う。

次にWindowsボタンを押した後にfirewallと文字列を打つと Windows Defender ファイアウォール というアプリがサジェストされるのでそれを開く。 f:id:husq:20190522221450p:plain 次に詳細設定を開く。 f:id:husq:20190522221555p:plain 左側の送信の規則というボタンをクリックした後、右側の操作から新しい規則をクリック。 以下の要領で入力欄を埋めていく。

  • 種類の規則
    • ポート
  • TCPUDPのどちら?
  • すべてのリモートポート or 特定のリモートポート
    • 特定のリモートポートで2374を入力
  • 操作の実行内容
    • 接続を許可する
  • 規則の適用
    • 環境によるけど自分は全部にチェックを入れた
  • 名前と説明
    • docker proxyとか適当なものを入力、説明は自分で読んでわかるものを設定

ここまで終わったらwslを起動して sudo apt-get update && sudo apt-get install -y nginx をする。 既にnginxを入れている人ならスキップ。

sudo vim /etc/nginx/nginx.conf でnginx.confを開いて、末尾に以下を追加する。

stream {
  error_log /var/log/nginx/docker.log info;

  server {
    listen 2374;
    proxy_pass localhost:2375;
  }
}

上記を記載した後に sudo service nginx restart を行えば設定は完了。 wsl環境でdocker for desktopのdockerを使う際には DOCKER_HOST=tcp://0.0.0.0:2375 のような環境変数を設定していると思うが、macOS側で似たようなことをするために必要な設定である。

macOSで以下のようなコマンドを打つと確認できるはず。

DOCKER_HOST=tcp://xxx.xxx.x.x:2374 docker images

xxx.xxx.x.xの部分にはwindowsで使っているプライベートIPを指定する。 正しく設定されていればwindows側に存在するdocker image一覧が取得できるはず。 何かの作業を常にWindowsのdockerを使いたい場合、direnvとかを使って export DOCKER_HOST=tcp://xxx.xxx.x.x とかを設定しておくと楽になると思う。

わざわざnginxを噛まさなくても良いのでは?と思った人もいるかもしれないが、直接2375のportを開放してもmacOS側からアクセスできなかったのでnginx proxyはどのみち必要になる。

Windowsk8smacOSから使う

docker serverのときと同じようにまずportを開放する必要がある。 当然だがwindows側でk8sを起動しておく必要がある。

まず上記のスクショの手順に従って、6444tcpポートを開放する。 次にnginx.confに6444のproxyを追記して全体としては以下のようになる。

stream {
  error_log /var/log/nginx/docker.log info;

  server {
    listen 2374;
    proxy_pass localhost:2375;
  }
  server {
    listen 6444;
    proxy_pass localhost:6445;
  }
}

kubeconfigの設定がmacOS側に必要なので C:\Users\ユーザ名\.kube に移動してconfigというファイル名の中身をコピーする。 大体以下のような感じだと思う。

apiVersion: v1
clusters:
- cluster:
    insecure-skip-tls-verify: true
    server: https://localhost:6445
  name: docker-for-desktop-cluster
contexts:
- context:
    cluster: docker-for-desktop-cluster
    user: docker-for-desktop
  name: docker-for-desktop
current-context: docker-for-desktop
kind: Config
preferences: {}
users:
- name: docker-for-desktop
  user:
    client-certificate-data: snip
    client-key-data: snip

次にmacOS側で vim ~/.kube/config をしてWindowsk8s情報を追加する。

apiVersion: v1
clusters:
- cluster:
    insecure-skip-tls-verify: true
    server: https://localhost:6443
  name: docker-for-desktop-cluster
# 以下のclusterを追加
- cluster:
    insecure-skip-tls-verify: true
    server: https://xxx.xxx.x.x:6444
# ここまで追加
  name: docker-for-windows-cluster
contexts:
- context:
    cluster: docker-for-desktop-cluster
    user: docker-for-desktop
  name: docker-for-desktop
# 以下のcontextを追加
- context:
    cluster: docker-for-windows-cluster
    user: docker-for-windows
  name: docker-for-windows
# ここまで追加
current-context: docker-for-desktop
kind: Config
preferences: {}
users:
- name: docker-for-desktop
  user:
    client-certificate-data: snip
    client-key-data: snip
# 以下のuserを追加
- name: docker-for-windows
  user:
    client-certificate-data: snip
    client-key-data: snip
# ここまで追加

GCPAWSのようにただcredential情報を手動で追加するだけである。 先述の通りxxx.xxx.x.xにはwindowsマシンのIPアドレスを入れる。

この後は kubectl config use-context docker-for-windows をした後に kubectl get po --all-namespaces とかでpodの情報が取れればWindowsk8sにアクセスできている。 やややることが多いが以上の手順でWindowsマシンのdocker(k8s)にmacOSからアクセスすることができる。 firewallを直接開けるだけではうまくいかないところにややハマったが、多分これはdocker側ではなくwindows側の影響によるもののように感じた。

macはしょぼいけどwindowsはゲーミングマシンでバリバリ!という人は上記の手順でwindowsのリソースを使えると割と快適かもしれない。 ここまで書いておいてあれだが自分はmacを新調したのであんまりこの手法を使っていない。 使っていた頃はvolume周りがうまく動作しない問題と遭遇した覚えがある、githubにもissueが立っていた気がするのでvolume周りでハマったらそのあたりで調べてみると良いかも。

サービスのバックアップをGCSに移した

サービスのバックアップのストレージとしてS3を利用していたが、GCSに移した。 移行のモチベーションはいつかGKEに行くぞ、という気持ちからGCSに移しておいたほうがいいかなーという気持ち。

前にcarrierwaveからactivestorageに移行した - 839の日記という記事を書いたが、だいたいそれと同じ気持ち。 別にしないとやばいというわけでもなかったが、とりあえず少し値段も下げれそうだしやるか〜みたいな。

元々はap-northeast-1にライフサイクルを30日で仕掛けてアップロードしていた。 移行のときによく考えたら30日もいらないな、と思い14日にしたのとregionをオレゴンにしたのでAWSのap-northeast-1と比べたらそこそこ安いはず。

1日のバックアップ容量がtar.gzにして大体660MBぐらいなので30日だと20GB弱である。 ap-northeast-1だと0.025$/GBなので0.5$(50円ちょっと)しかかかってないのに、削ってどうするんだという話もある。 料金は今書きながら計算したので思ったより何も変わらないなーという気持ちにはなったが、GKEに移すとなるとネットワーク的にもまぁ移しておいて損はない。 保持期間を14日にしてオレゴンに置くと8.5GBで0.02$/GBなので0.17$で少しお得…な気持ちになったと思いこんでおく。

GCSが圧倒的にいけてないなーと思うところはライフサイクル周りの設定。 S3であればあるprefixのパスから始まっているやつはn日といった柔軟な設定ができるのに対して、GCSはバケット全体にしか適応できない。 なので、ライフサイクルの種類を分けたくなった場合はバケットごと分けるしかなく、ここはいまいちだなーと感じる。

あとはGCPのIAM/サービスアカウント周りがわかりにくい上に結局したいことができない感がすごい。 AWSであればIAMであるバケットのwriteだったりreadだったりを細かく権限設定できるのだが、 GCPは閲覧者、作成者、管理者ぐらいの区分しかないし、結局GCSにファイルをアップロードするのには少なくともRubySDKでは管理者が必要。*1

どうも、調べた感じだとアップロード前にbucket一覧を取得しに行く動作をしているようで、その兼ね合いでオブジェクト作成権限だけではだめっぽい。 しかも管理者にしてもあるバケットだけに絞れないので、すべてのバケットの管理者を与えるしかないように思える。 このあたり、無知なだけかもしれないが、AWSを使ってた身からするとかなりイケてない権限体系だなーと感じる。

バックアップは複数のサービスで使い回したかったので以下のようなものを書いている。 なお、前提としてactivestorageはlocalで利用している(現在はVPSの上で動いているので)。

require 'google/cloud/storage'

class ServiceDumper
  def dump_and_upload
    storage = Google::Cloud::Storage.new(
      project_id: 'gcp-project-id',
      credentials: "#{Rails.root}/service-dumper.json"
    )
    bucket = storage.bucket('bucket-name')
    Rails.logger.info('uploading gcs...')
    bucket.create_file(dump, upload_path)
    Slack::GcsDispatcher.success(ENV['RAILS_ENV'])
    `rm #{dump_path}.tar.gz`
    Rails.logger.info('done service dump')
  rescue StandardError => e
    Slack::GcsDispatcher.failed(ENV['RAILS_ENV'], e)
  end

  private

  def project_name
    'project_name'
  end

  def upload_path
    "#{project_name}/#{Date.today.year}-#{Date.today.month}-#{Date.today.day}/#{ENV['RAILS_ENV']}_service_dumper.tar.gz"
  end

  def dump
    Rails.logger.info('clean dump path')
    `rm -rf #{dump_path}`
    Rails.logger.info('create dump path')
    `mkdir #{dump_path}`
    Rails.logger.info('file dumping...')
    file_dump
    Rails.logger.info('pg dumping...')
    pg_dump
    `echo #{Time.now} > #{dump_path}/info.txt`
    Rails.logger.info('tar.gz creating...')
    `cd #{Rails.root}/tmp && tar czf service_dumper.tar.gz service_dumper`
    Rails.logger.info('remove raw files')
    `rm -r #{dump_path}`

    "#{dump_path}.tar.gz"
  end

  def file_dump
    `cp -r #{files_path} #{dump_path}/storage`
  end

  def pg_dump
    `pg_dump -U#{database['username']} #{database['database']} > #{dump_path}/#{database['database']}.sql`
  end

  def dump_path
    "#{Rails.root}/tmp/service_dumper"
  end

  def database
    Rails.configuration.database_configuration[ENV['RAILS_ENV'] || 'production']
  end

  def files_path
    "/absolute_path/app/#{project_name}/shared/storage"
  end
end

上記のような構成で組んでおいて、基本プロジェクトを跨いだときに変更するのは #project_name の文字列だけ変えている。 ちょっと前まで一部がcarrierwaveで動いていたが、全部がactivestorageになったので、他のロジックは完全に使い回せて少しメンテナンスコストが下がった。

GcsDispatherは自作のslack通知くんで、dumpが終わったら教えてくれる。処理はactivejobでwrapしている状態。 dump自体は利用者が少なそうな朝方に sidekiq-cron というgemを使って定期実行を仕掛けている状態。*2 2000万レコード程度のサービスだと大体4分弱ぐらいでdumpがuploadまで含めて終わる感じ。*3 なお、DBだけではなくactivestorageの内容もまとめてdumpしている。

ここはactivestorageをlocalモードで使っててよかったなと思う点なのだが、ローカルで画像をmissingにせずかつ安全に扱える点が楽である。 missingにするとraiseしまくって鬱陶しいし、GCSの本番バケットを参照するようにしていると誤って登録者のデータを変えてしまう恐れもある。 多分自分がactivestorageをGCSに移すときは何らかの手段でGCSからローカルに落としてlocalモードで動かす、みたいなことを模索しそうな気はするが今は困ってないのでどのくらいのコスト感なのか不明。

VPSで冗長構成も組んでない(お金的な意味で)サービスなので、とりあえずまめにバックアップをとっといていつVPSが飛んでもいいようにしている、という話だった。*4 単純に根が深い問題を検証するときでも、やはりバックアップが定期的に取られているとシュッと手元で本番環境を模せるし、色々と運用が楽になるなと感じる。*5

GKEに移るぞ!という気持ちと下準備だけ先行していて本筋のGKEを安く運用する方法の調査がなかなか進んでいないのでそろそろ頑張っていきたい。。

*1:閲覧者はオブジェクトの閲覧者しか指定できないっぽくて、ほしいのはバケットの閲覧権限だが、これには管理者を与えるしかなさそうに思えた

*2:サービスの特性上、朝5時をすぎると基本writeがなくなるのでそのぐらいにやっている

*3:3コア4GBのVPS1台で複数サービスを運用しているのになかなか頑張ってくれる

*4:とはいえ、VPSはcloudと比べて圧倒的に壊れない気がする、ほとんど壊れた記憶がない

*5:https://839.hateblo.jp/entry/2019/05/16/213836 でも述べた通り、根深い問題は落とさないとわからない