リーダー職に就任しました

2024/3/1より、私はGMOペパボ株式会社の技術部プラットフォームグループのリーダー職に就任しました。 リーダー職についたきっかけ、どんなことをやっていくのか、決意表明を書こうと思います。

きっかけ

私は、長らく弊社が提供するサービスの一つである30days AlbumやBaytと呼ばれるペパボの各サービスが利用するプライベートなオブジェクトストレージを中心に、オンプレミスのレイヤからアプリケーション(バックエンド)のレイヤまで横断してエンジニアリングを手掛け、サービスを支える取り組みをしてきました(各サービスの説明については割愛します。 気になる方は弊社のテックブログをご覧ください)。 同時に、2022年〜2023年にわたって30days AlbumやBaytに関連する大規模なプロジェクトに取り組んでいました。これらのプロジェクトは30days Albumの関係者だけでなく、他事業部のエンジニアやGMOグループのエンジニアと協力して取り組む場面が多々あり、その当時からプロジェクトの進捗を管理したり、チームメンバーから課題をヒアリングして課題解決のための手法を一緒に考えたりということをしていました。 当時からすでにリーダーっぽい動きをしていたこともあってか、 @hiboma から「エンジニアリングリードやチームマネジメントで力を発揮できそう」という助言をいただきました。 今後のキャリアをどうしていくか、当時の選択肢では「シニアエンジニア or マネージャ」の2択になっていましたが、ここに「エンジニアリングリード」という選択肢が私の中で生まれました(どのキャリアに進むのかまだ決めたわけではありません。あくまで選択肢が増えただけです)。 その後、マネージャの@ginbearと話し合いながら今後のキャリアについて考えた結果、リーダーをやってみることになりました。

どんなことをやっていくの?

プラットフォームグループのリーダーには以下のミッションが与えられます。

「メンバーの能力が十分に発揮され、チームの成果を最大化すること」

勤怠や安全衛生といったいわゆる労務管理はしません。これはマネージャ職が行います。 具体的には以下のミッションに取り組んできます。

主要ミッション(チームビルディングと目標管理)

私の場合、主なミッションとして以下が与えられています。

  • チームビルディング
    • チームの生産性の向上のために、生産性を阻害する課題を発見し解決していくこと目指す
    • MTGなどのファシリテート、他事業部との仲介、チームメンバーとの1on1、チームの課題提起 
  • 目標管理
    • 技術方針に則り、事業部ごとの課題に基づいてチームの目標を設定し、目標達成に向けて管理します
    • アクションアイテムの設定、スケジューリング・期限設定、コスト管理、その他フォロー

サブミッション(成長支援と採用)

原則は主要ミッションに注力して取り組みますが、もし余裕があったらチャレンジする要素として以下の二つがあります。 あくまでサブミッションであり、マネージャからもまずは先述した主要ミッションを遂行することを求められています。

  • 成長支援
    • パートナーが成長・上位の等級へ昇格できるようにサポートする体制を作る
  • 採用
    • 面談などを通じて、スキルマッチや文化へのマッチを評価する

決意表明

リーダーとしてチームを引っ張る以上、私がボトルネックになることはチーム全体の大幅なボトルネックになるためこれまで以上に責務が大きくなります。 既にリーダーっぽい動きをしていたからといって「これまでみたいなことをしてれば良いでしょう」という考えでいたら間違いなくチームは鈍化します。 特に最近30days Album以外にもEC支援のサービス(カラーミーショップなど)のインフラを担うチームにもジョインしており、こちらでもリーダーとしての役割が求められます。 サービスが変われば事業の方針・技術スタック・文化も大きく変わります(既にギャップを受けています💦)。 逆にいえば、両者のいいところ・課題と言えるところを俯瞰して改善していくことでチームをブーストできるのではないかと思います。

@rsym1290 がリーダーになったことで、チームが良くなった!」そう言ってもらえるようなリーダを目指して頑張ります。

最後に

この本を買ったので勉強します!

www.oreilly.co.jp

YAPC::Kyoto 2023に参加してきたので感想でも書こうかな

3/19に開催されたYAPC::Kyoto 2023に登壇してきました!

yapcjapan.org

発表が無事に終わり家に帰ってきたわけですが、どうやら私のYAPCはまだ終わっていないようです。

というわけでYAPCに参加した感想を書こうと思います。

なぜYAPCに参加した?

主な理由はこんなところでしょうか

  • 同じ部署で人一倍アウトプットに力を入れている @ryuichi_1208 さんに触発された
  • 周りの後押しのおかげで登壇する勇気が持てた

実は私、これまで社外登壇をしたことがありませんでした。 一応何年かエンジニアをやっており、心のどこかでは「いつか登壇を経験してみたいな」と思いつつ、周りがすごい人だらけで圧倒されて「自分が今までやってきたことを発表する自信がない」という理由でずっと躊躇っておりました。

そんな中、@ryuichi_1208 さんからYAPCが開催されることを教わりました。@ryuichi_1208 さんは、私の所属している部署に入社してから人一倍アウトプットに力を入れており、これが部署のメンバーに良い影響を及ぼしていました。これまで以上に部署からのテックブログ・社外発表(コロナ禍ということもありオンライン中心でしたが)が増えてきたように思います。こういった活動がどんどん増えていることを受けて「私も挑戦すべきだ」という気持ちが少しずつ出てきていました。

さらに、弊社の @hiboma さんや @pyama86 さんから、私が携わっている30daysAlbumやBaytではPerl製のMogileFSを利用していること、かりにそうでなくともYAPCではPerlに縛られずいろんなテーマで発表できること、私の普段の業務のことも十分発表テーマになること、などなどいろいろな助言をいただきました。

これをうけて「じゃあいってみるか!」と最後は勢いで発表を決意しました!

京都についてからの自分

勢いで発表を決意したもの。。。京都についてからは終始ガチガチでした。 前日の3/18にReject Conがあったのでそれを聴きにいき、その後、Helpfeelの @honchang_ さんによって開催された懇親会に参加しましたが、この間ず〜っと緊張しておりました。自分の出番でもないのに。初めての発表でどうなるのか全く想像がつかなかったのです。「発表うまく話せるだろうか」「質疑応答はどうなるだろうか」「参加者の反響はどうなのだろうか」などなど。。。 さらに自分が発表をする部屋は100人以上は入れそうな広い会議スペースで、しかも自分の発表時間は40分の長丁場。

一緒に京都に来ていた @ryuichi_1208 さん、 @hiboma さんにも緊張が筒抜けでした😅。 そんななか懇親会に来ていたDeNA@tamaclaw さんからこんな助言をいただきました。

「真面目すぎ!仮に失敗しても次の日にはみんな覚えてないんだから気にする必要はない!もっと気楽に!」

これを聞いて「確かに!」となりました。 みんなの記憶に残るのはいい発表だけで、失敗な発表は悪く記憶に残るのではなくそもそも忘れ去られるだけ。 いいかえれば失敗しても何か失うわけではないということ。 ちょっとだけ肩の荷が降りた気がしたし、今後の自分がアウトプットを頑張る上での格言になるかも?と思いました。 まぁなんだかんだで発表直前まで緊張と不安は残ったままだったすがね💦

発表を終えて

発表当日、なんだかんだで緊張したままでした。 それでも、いざ発表を始めたら練習通りに話せたかな〜と思います。 質疑応答はもうちょっと整理して答えられた方がよかったかなぁとは思いましたが、発表自体は無事に終えることができました。 発表を聞いていた弊社の @kenchan さんからも「発表よかったよ」と褒めてもらえました。 発表を終えた途端、とんでもないくらい肩の荷が降りましたね〜。 twitterをみても共感のツイートがあったりと発表してよかったなぁと思いました。

今回の発表を通じて一番得ることができたのは

「真面目すぎ!仮に失敗しても次の日にはみんな覚えてないんだから気にする必要はない!もっと気楽に!」

これだと思います。 もちろんYAPCでいろんな方々の発表を聞いていろんなものを得ることができました。 ただ、初めて発表した経験そのものが自分にとってとても大きい成果で、さらに @tamaclaw さんの教えもまた大きな成果でした。 よく「失敗を恐れるな」とは言われますが、これだけだとあまり腑に落ちないものがありました。 でもこの助言は「失敗したらどうしよう。。」というマイナスな気持ちに対する一つの解だと思いました。 そしてこの教えを受けられたのは、@honchang_ さんが懇親会を開催してくださったからこそだと思います。 ありがとうございました。

最後に

初の登壇で緊張だらけでしたが終わってみればすごくいい経験でした。

ブログを書いたので今度こそ私のYAPCはおわり。。。 じゃないかもしれない。 まだ何か書くかも?

弊社のテックブログで登壇レポートを書きました。 今度こそ終わり!

tech.pepabo.com

npm version9以降でユーザ名・パスワードでログインする場合は --auth-type=legacy が必要

レジストリを指定してnpmパッケージをインストールしようとした時に遭遇したエラー。 ちなみにこのレジストリは、ユーザ名・パスワードでのログインを想定している。

$ npm login --registry=https://<registry>/
npm notice Log in on https://<registry>/
npm ERR! code ENYI
npm ERR! Web login not supported

npm ERR! A complete log of this run can be found in:

npmドキュメントを確認するとこんなことが書いてあった。

https://docs.npmjs.com/cli/v9/commands/npm-login#description

When using legacy for your auth-type, the username and password, are read in from prompts.

ユーザ名・パスワードをプロンプトから読み込んで認証するときは、--auth-type=legacy をつける必要があった。

npm install - npm login not allowing login to github - Stack Overflow によると、 npmのバージョン9以降だとこのオプションが必要らしい。

$ npm login --auth-type=legacy --registry=https://<registry>/
npm notice Log in on https://<registry>/
Username: xxxxx
Password: 

これでログインできた。

AWS SDK for Ruby V3のスタブを使ってみる

この記事は🎅GMOペパボエンジニア Advent Calendar 2022の14日目の記事です。

追記: Calendar for Ruby | Advent Calendar 2022 - Qiitaの14日目の記事としても公開しました。


RubyでS3を扱うとき、Class: Aws::S3::Client — AWS SDK for Ruby V3を使うことが多いだろう。 これを利用したアプリケーションのテストを書く場合、S3に実際にリクエストを飛ばすのは好ましくないので別の手段を取りたいところ。

実はAWS SDK for Ruby V3はスタブも提供している。 便利そうなので試しに使ってみた。

docs.aws.amazon.com

今回はS3を前提にしているが、EC2やLambdaのインスタンスでもスタブが使えるっぽい。

基本的な使い方

Aws::S3::Client.newインスタンスを生成する時に、stub_responses:を定義すればそのインスタンスで該当のAPIを実行すると定義したスタブがレスポンスとして返ってくる。 例えばGetObjectのスタブはこんなふうに定義できる。

client = Aws::S3::Client.new(
  stub_responses: {
    get_object: {
      body: 'data',
      etag: '"084ad94f7e17dcd8165b624b06d35eab"'
    }
  }
)

もしくはこう。

client = Aws::S3::Client.new(stub_responses: true)
client.stub_responses(
  :get_object, {
    body: 'data',
    etag: '"084ad94f7e17dcd8165b624b06d35eab"'
  }
)

スタブを定義したAws::S3::Clientインスタンスget_objectを実行すれば、定義したスタブ通りのレスポンスを取得できる。 ちなみにget_objectの引数に何を指定しても同じレスポンスが返ってくる。

res = client.get_object('path/to/key')
puts "body : #{res.body.read}"
puts "etag : #{res.etag}"
# 実行結果
body : data
etag : "084ad94f7e17dcd8165b624b06d35eab"

エラーをスタブしたい場合

エラーをスタブすることもできる。 やり方は https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/ClientStubs.html#stub_responses-instance_methodStubbing Errorsに書かれている。

例えばクレデンシャルが誤っていると The AWS Access Key Id you provided does not exist in our records.というエラーが返されるが、これを再現したい場合はRuntimeErrorインスタンスとしてスタブに注入すればOK。

client = Aws::S3::Client.new(
  stub_responses: {
    get_object: RuntimeError.new("The AWS Access Key Id you provided does not exist in our records.")
  }
)
response = client.get_object(bucket: 'sample-bucket', key: 'path/to/key')
# 実行結果
RuntimeErrorが発生し、以下のメッセージが表示される

The AWS Access Key Id you provided does not exist in our records. (RuntimeError) 

複数のAPIについてスタブを定義したい場合

複数のスタブをまとめて定義することもできる。 例えば以下のようにGetObjectとListObjectsのスタブを定義できる。

client = Aws::S3::Client.new(
  stub_responses: {
    get_object: {
      body: 'data',
      etag: '"084ad94f7e17dcd8165b624b06d35eab"'
    },
    list_objects: {
      contents: [
        {
          key: 'sample_key1',
          etag: '"084ad94f7e17dcd8165b624b06d35eab"'
        },
        {
          key: 'sample_key2',
          etag: '"b9bca3e0a73bde48ee06bc37dfa74753"'
        }
      ]
    }
  }
)

puts "GetObject"
response = client.get_object(bucket: 'sample-bucket', key: 'path/to/key')
puts "body : #{response.body.read}"
puts "etag : #{response.etag}"

puts "ListObjects"
response = client.list_objects(bucket: 'sample-bucket')
response.contents.each{|c|
  puts "#{c.key}, #{c.etag}"
}
# 実行結果
GetObject
body : data
etag : "084ad94f7e17dcd8165b624b06d35eab"
ListObjects
sample_key1, "084ad94f7e17dcd8165b624b06d35eab"
sample_key2, "b9bca3e0a73bde48ee06bc37dfa74753"

スタブだと不十分な場合

PutObjectやCopyObjectなどS3のオブジェクトに更新がかかるようなAPIについては、スタブだと不十分かな。 一応スタブ化できるが、たとえばPutObjectだとContent-MD5を利用したオブジェクトの整合性チェックとかがされないまま、決めうちのETagやバージョンIDを返すだけになる。 これで十分といえる場面は結構限られそう(それをいったらGetObjectなどもスタブでは不十分な場面もありそうだが)。

スタブで不十分な場合は、MinIOを利用する方法もある。 MinIOとはgoで実装されたS3と互換性のあるオブジェクトストレージで、MinIOがサポートしているAPIであれば直接MinIOへリクエストを投稿してテストする方法も取れる。 この辺はよしなに使い分けたいところ。

MinIOについては過去にブログで取り上げたので興味がある方はどうぞ。

MinIOを触ってみた - rsym’s diary

k8sでdrain/cordonを実行するとTaints `node.kubernetes.io/unschedulable:NoSchedule` が付与される

Taintsの使い道についてあまりピンとこなくて、使い道を調べてたらこのことを知ったので記事にした。

Taintsのユースケース

k8sのドキュメントにユースケースが書かれている。

kubernetes.io

  • 専有NodeにTaintsを付与する
  • 特殊なハードウェアを備えたNodeにTaintsを付与して、そのハードウェアを必要としないPodが配置されないようにする
  • Nodeに問題が起きたときにPodがそのNodeに配置されないようにする

さらに読みすすめると「taintを基にした排除」の項にこんなことが書かれていた。

Nodeコントローラーは特定の条件を満たす場合に自動的にtaintを追加します。 組み込まれているtaintは下記の通りです。

...

  • node.kubernetes.io/unschedulable: Nodeがスケジューリングできない場合。

スケジューリングできない場合ということは、もしかしてdrainやcordonを使うときにtaintsが付与されるのかな? と思ったので実際に確認してみた。

drain/cordonでもTaintsが使われている

drainを実行するとPodを退避する前にcordonが実行されて、Taintにnode.kubernetes.io/unschedulable:NoScheduleが付与される。 ざっくりこんな流れ。

  1. 対象Nodeでcordonが実行される
  2. 対象NodeのTaintsにnode.kubernetes.io/unschedulable:NoScheduleが付与されて、新しいPodが配置されないようにする
  3. Podの退避or停止が実行される
  4. 退避されたPodは別Nodeで起動する

kindで検証してみる

まずは環境用意

kindでローカルにクラスタを構築して検証する。

こんなcluster.yamlを用意する。 ちなみに https://kind.sigs.k8s.io/docs/user/configuration/#name-your-cluster から引っ張ってきた。

$ cat cluster.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
- role: worker
$ kind create cluster --name sample --config=cluster.yaml
Creating cluster "sample" ...
 ✓ Ensuring node image (kindest/node:v1.24.0) 🖼
 ✓ Preparing nodes 📦 📦 📦 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
 ✓ Joining worker nodes 🚜
Set kubectl context to "kind-sample"
You can now use your cluster with:

kubectl cluster-info --context kind-sample

Thanks for using kind! 😊

出来上がったノード。

$ kubectl get nodes
NAME                   STATUS   ROLES           AGE   VERSION
sample-control-plane   Ready    control-plane   69s   v1.24.0
sample-worker          Ready    <none>          32s   v1.24.0
sample-worker2         Ready    <none>          32s   v1.24.0
sample-worker3         Ready    <none>          32s   v1.24.0

各sample-workerのLabelsとTaintsはこんな感じ。 3台ともすべて同じ。

$ kubectl describe node sample-worker
Name:               sample-worker
Roles:              <none>
Labels:             beta.kubernetes.io/arch=amd64
                    beta.kubernetes.io/os=linux
                    kubernetes.io/arch=amd64
                    kubernetes.io/hostname=sample-worker
                    kubernetes.io/os=linux
Annotations:        kubeadm.alpha.kubernetes.io/cri-socket: unix:///run/containerd/containerd.sock
                    node.alpha.kubernetes.io/ttl: 0
                    volumes.kubernetes.io/controller-managed-attach-detach: true
CreationTimestamp:  Tue, 12 Jul 2022 23:28:35 +0900
Taints:             <none>    👈node作成直後なので何も付与されていない
Unschedulable:      false

...

全sample-workerに適当なラベルenv=testingを付与。

$ kubectl label node sample-worker env=testing
node/sample-worker labeled

$ kubectl label node sample-worker2 env=testing
node/sample-worker2 labeled

$ kubectl label node sample-worker3 env=testing
node/sample-worker3 labeled

nginxのPodをenv=testingなNodeで起動する。

$ cat nginx-pod.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    env: testing
  name: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      env: testing
  template:
    metadata:
      labels:
        env: testing
    spec:
      containers:
      - image: nginx
        name: nginx
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                - key: env
                  operator: In
                  values:
                  - testing

$ kubectl apply -f nginx-pod.yaml
deployment.apps/nginx created

$ kubectl get deployment -o wide
NAME    READY   UP-TO-DATE   AVAILABLE   AGE   CONTAINERS   IMAGES   SELECTOR
nginx   3/3     3            3           28s   nginx        nginx    env=testing

$ kubectl get pods -o wide
NAME                     READY   STATUS    RESTARTS   AGE   IP           NODE             NOMINATED NODE   READINESS GATES
nginx-575bc5fb56-bc5cj   1/1     Running   0          48s   10.244.3.2   sample-worker2   <none>           <none>
nginx-575bc5fb56-q4g2v   1/1     Running   0          48s   10.244.1.2   sample-worker    <none>           <none>
nginx-575bc5fb56-srsx4   1/1     Running   0          48s   10.244.2.2   sample-worker3   <none>           <none>

これで準備ができた。

sample-workerを対象にdrainを実行してみる

ログ通り、cordonが実行された後に各podが退避されていることがわかる。

$ kubectl drain sample-worker --ignore-daemonsets
node/sample-worker cordoned
WARNING: ignoring DaemonSet-managed Pods: kube-system/kindnet-wzcdv, kube-system/kube-proxy-dwkzb
evicting pod default/nginx-575bc5fb56-q4g2v
pod/nginx-575bc5fb56-q4g2v evicted
node/sample-worker drained

sample-workerのステータスに SchedulingDisabled が追加されており、sample-worker上で動いていたPod 消えて別Nodesample-worker3で新しいPodが起動する。

$ kubectl get nodes -o wide
NAME                   STATUS                     ROLES           AGE     VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE       KERNEL-VERSION              CONTAINER-RUNTIME
sample-control-plane   Ready                      control-plane   6m40s   v1.24.0   172.23.0.2    <none>        Ubuntu 21.10   4.18.0-348.el8.0.2.x86_64   containerd://1.6.4
sample-worker          Ready,SchedulingDisabled   <none>          6m3s    v1.24.0   172.23.0.5    <none>        Ubuntu 21.10   4.18.0-348.el8.0.2.x86_64   containerd://1.6.4
sample-worker2         Ready                      <none>          6m3s    v1.24.0   172.23.0.4    <none>        Ubuntu 21.10   4.18.0-348.el8.0.2.x86_64   containerd://1.6.4
sample-worker3         Ready                      <none>          6m3s    v1.24.0   172.23.0.3    <none>        Ubuntu 21.10   4.18.0-348.el8.0.2.x86_64   containerd://1.6.4

$ kubectl get pods -o wide
NAME                     READY   STATUS    RESTARTS   AGE     IP           NODE             NOMINATED NODE   READINESS GATES
nginx-575bc5fb56-bc5cj   1/1     Running   0          2m18s   10.244.3.2   sample-worker2   <none>           <none>
nginx-575bc5fb56-gx5c7   1/1     Running   0          32s     10.244.2.3   sample-worker3   <none>           <none>
nginx-575bc5fb56-srsx4   1/1     Running   0          2m18s   10.244.2.2   sample-worker3   <none>           <none>

describeを見るとnode.kubernetes.io/unschedulable:NoScheduleというTaintsが付与されていることがわかる。

$ kubectl describe node sample-worker
Name:               sample-worker
Roles:              <none>
Labels:             beta.kubernetes.io/arch=amd64
                    beta.kubernetes.io/os=linux
                    env=testing
                    kubernetes.io/arch=amd64
                    kubernetes.io/hostname=sample-worker
                    kubernetes.io/os=linux
Annotations:        kubeadm.alpha.kubernetes.io/cri-socket: unix:///run/containerd/containerd.sock
                    node.alpha.kubernetes.io/ttl: 0
                    volumes.kubernetes.io/controller-managed-attach-detach: true
CreationTimestamp:  Tue, 12 Jul 2022 23:28:35 +0900
Taints:             node.kubernetes.io/unschedulable:NoSchedule    👈付与された
Unschedulable:      true

...

Taintsを削除する場合

Taints node.kubernetes.io/unschedulable:NoSchedule を削除するときはuncordonすればOK。

$ kubectl uncordon sample-worker
node/sample-worker uncordoned

$ kubectl get node -o wide
NAME                   STATUS   ROLES           AGE   VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE       KERNEL-VERSION              CONTAINER-RUNTIME
sample-control-plane   Ready    control-plane   11m   v1.24.0   172.23.0.2    <none>        Ubuntu 21.10   4.18.0-348.el8.0.2.x86_64   containerd://1.6.4
sample-worker          Ready    <none>          10m   v1.24.0   172.23.0.5    <none>        Ubuntu 21.10   4.18.0-348.el8.0.2.x86_64   containerd://1.6.4
sample-worker2         Ready    <none>          10m   v1.24.0   172.23.0.4    <none>        Ubuntu 21.10   4.18.0-348.el8.0.2.x86_64   containerd://1.6.4
sample-worker3         Ready    <none>          10m   v1.24.0   172.23.0.3    <none>        Ubuntu 21.10   4.18.0-348.el8.0.2.x86_64   containerd://1.6.4

$ kubectl describe node sample-worker
Name:               sample-worker
Roles:              <none>
Labels:             beta.kubernetes.io/arch=amd64
                    beta.kubernetes.io/os=linux
                    env=testing
                    kubernetes.io/arch=amd64
                    kubernetes.io/hostname=sample-worker
                    kubernetes.io/os=linux
Annotations:        kubeadm.alpha.kubernetes.io/cri-socket: unix:///run/containerd/containerd.sock
                    node.alpha.kubernetes.io/ttl: 0
                    volumes.kubernetes.io/controller-managed-attach-detach: true
CreationTimestamp:  Tue, 12 Jul 2022 23:28:35 +0900
Taints:             <none>    👈消えてる
Unschedulable:      false

...

kubectl createで生成したDeploymentのyamlを元にDaemonSetのyamlを作成する

k8sではkubectl runkubectl createで各リソースのyamlを生成できる。

例えばPodのyamlを生成するならこう。

kubectl run <pod名> --image=<image名> --dry-run=client -o yaml > pod.yaml

Deploymentのyamlを生成するならこう。

kubectl create deployment <pod名> --image=<image名> --replicas=<レプリカ数> --dry-run=client -o yaml > deployment.yaml

その他、作成できるリソースは kubectl create --helpで確認できる。

$ kubectl create --help
Create a resource from a file or from stdin.

 JSON and YAML formats are accepted.

...

Available Commands:
  clusterrole         Create a ClusterRole.
  clusterrolebinding  Create a ClusterRoleBinding for a particular ClusterRole
  configmap           Create a configmap from a local file, directory or literal value
  cronjob             Create a cronjob with the specified name.
  deployment          Create a deployment with the specified name.
  ingress             Create an ingress with the specified name.
  job                 Create a job with the specified name.
  namespace           Create a namespace with the specified name
  poddisruptionbudget Create a pod disruption budget with the specified name.
  priorityclass       Create a priorityclass with the specified name.
  quota               Create a quota with the specified name.
  role                Create a role with single rule.
  rolebinding         Create a RoleBinding for a particular Role or ClusterRole
  secret              Create a secret using specified subcommand
  service             Create a service using specified subcommand.
  serviceaccount      Create a service account with the specified name

...

もしくは kubectl CLI | Kubernetes で確認できる。

この中にはdaemonsetがない。 kubectl createでDaemonSetのリソースを作成することはできないようで、yamlの作成もできないらしい。 とはいえ、DaemonSet用のyamlを用意するにしてもイチから書くはもちょっと面倒くさい。

どうすればいいのか?

Depolymentのyamlを生成して、そのyamlを一部編集してDaemonSet用のyamlを用意することになるらしい。

参考:stackoverflow.com

実際にやってみる

こんなノードを予め用意。 DaemonSetを利用してsample-worker/sample-worker2/sample-worker3に1つずつnginxのPodを配置してみる。

$ kubectl get nodes
NAME                   STATUS   ROLES                  AGE   VERSION
sample-control-plane   Ready    control-plane,master   25m   v1.21.1
sample-worker          Ready    <none>                 24m   v1.21.1
sample-worker2         Ready    <none>                 24m   v1.21.1
sample-worker3         Ready    <none>                 24m   v1.21.1

kubectl createでdeploymentのyamlを作成する。

$ kubectl create deployment nginx --image=nginx --dry-run=client -o yaml > nginx-deployment.yaml

$ cat nginx-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: nginx
  name: nginx
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx
        name: nginx
        resources: {}
status: {}

nginx-deployment.yamlをコピーしてnginx-daemonset.yamlを用意。 以下のように変更する。 ポイントはkindのDeploymentをDaemonSetに置き換えることと、replicasを削除すること。 その他不要な行も削除する。

$ diff -u nginx-deployment.yaml nginx-daemonset.yaml
--- nginx-deployment.yaml       2022-05-21 22:24:18.000000000 +0900
+++ nginx-daemonset.yaml        2022-05-21 22:25:30.000000000 +0900
@@ -1,24 +1,18 @@
 apiVersion: apps/v1
-kind: Deployment
+kind: DaemonSet
 metadata:
-  creationTimestamp: null
   labels:
     app: nginx
   name: nginx
 spec:
-  replicas: 1
   selector:
     matchLabels:
       app: nginx
-  strategy: {}
   template:
     metadata:
-      creationTimestamp: null
       labels:
         app: nginx
     spec:
       containers:
       - image: nginx
         name: nginx
-        resources: {}
-status: {}

これでapplyできる。

$ kubectl apply -f nginx-daemonset.yaml
daemonset.apps/nginx created

$ kubectl get daemonset
NAME    DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
nginx   3         3         3       3            3           <none>          33s

$ kubectl get pods -o wide
NAME          READY   STATUS    RESTARTS   AGE   IP           NODE             NOMINATED NODE   READINESS GATES
nginx-bs4nc   1/1     Running   0          39s   10.244.3.2   sample-worker2   <none>           <none>
nginx-j5h7m   1/1     Running   0          39s   10.244.2.2   sample-worker3   <none>           <none>
nginx-zlfq5   1/1     Running   0          39s   10.244.1.2   sample-worker    <none>           <none>

ngx_dynamic_upstreamを用いてdockerコンテナのblue/greenデプロイをしてみる

出来上がった物を先に見たいという方はこちらをどうぞ。

github.com

きっかけ

最近いろんなアプリケーションをdockernizeすることがふえており、そんな中でてきた課題が「どうやってダウンタイムなしでデプロイしようか」というもの。 もしAWS環境ならCodeDeployを活用すれば良さそうな話だが、自分が今扱っているアプリケーションはわけありでAWSなどは活用できず、dockerだけでどうにかする必要があった。

パッと思いたのは「nginxのコンテナを前段に立ててデプロイの度にupstreamを書き換える」という方法。 blueコンテナとgreenコンテナが記載されたupstream.confをボリュームマウントさせてnginxコンテナを起動しておき、デプロイのときにそれぞれをdown/upさせながらデプロイさせるというもの。 軽くググってみても似たことを試している事例は結構あった。

でも都度ファイルを書き換える事をしていると、リポジトリでのバージョン管理がとてもやりにくくなるのでupstream.confを書き換えなくてもblue/greenデプロイできるようないい方法無いかな〜と模索していた。 そんななか見つけたのがngx_dynamic_upstreamである。

ngx_dynamic_upstreamとは?

GitHub - cubicdaiya/ngx_dynamic_upstream: Dynamic upstream for nginx

nginxモジュールの一つで、upstreamの動的な更新を可能とする。 APIはHTTPで提供されており、curlコマンドなどでupstreamに登録されたサーバの一覧をとったり、down/upさせたり、サーバの追加/削除などが可能になる。

また、nginxを起動するときは初期値としてupstreamの定義が必要だが、nginxを起動した後であれば、API経由でupstreamを更新してもconfigファイル自体の書き換えは行われない。 おそらくオンメモリでupstreamの情報を持っており、それだけを更新しているのだろう。

今回の検証環境

ざっとこんな構成にしてみる。

今回試したいのはロードバランサの部分なので、アプリケーション部分は適当にdocker runするだけでも使えそうなものとしてdocker/getting-startedを採用した。 ちなみにdocker/getting-startedとはdockerのチュートリアルを見れるアプリケーションである。 詳しくは割愛するので、気になる人は各自GitHubをのぞくなりコンテナを起動するなりしてくだされ。

ロードバランサをビルドするためのDockerfile

ngx_dynamic_upstreamはリポジトリをcloneしてソースビルドでインストールする必要がある。 ngx_dynamic_upstream の開発者である cubicdaiyaさんが便利なビルドツールnginx-buildを提供していたので使ってみることにした。 ubuntu:20.04をベースイメージとしてその中でビルドした。

作成したDockerfileはこちら:https://github.com/rsym/docker-bg-deployment/blob/main/Dockerfile

ビルドの流れはこんな感じ。

  1. 必要なパッケージをインストール
    • nginx-buildをgo getで入手するためにgolangをインストールする
    • ngx_dynamic_upstreamをgit cloneするためにgitをインストールする
    • その他、nginx-buildには必要パッケージをよしなにインストールしてくれる機能があるが、それだけでは入らなかったものがあったので個別に入れることにした
  2. nginx-buildを実行
    • 指定するオプションは以下
      • nginx:1.21.6の公式コンテナで採用されているオプション(docker run --rm -it nginx:1.21.6 nginx -V で確認できる)
      • ngx_dymanic_upstreamを追加するためのオプション --add-module=/path/to/ngx_dynamic_upstream
  3. make installを実行
  4. 必要なユーザやディレクトリを作成
    • 2.で指定したオプションに合わせて必要なもの作成する
  5. upstreamを定義したnginx.confをイメージへCOPY
  6. EXPOSE書いたりCMD書いたり

blue/greenデプロイをしてみる

deploy.shというデプロイスクリプトを実行するだけでblue/greenデプロイをしてくれるようにした。 ちなみにコンテナがどれも起動していないときはloadbalancerとblueコンテナを起動してくれるようにしている。

$ docker-compose ps
NAME                COMMAND                  SERVICE             STATUS              PORTS
app-blue            "/docker-entrypoint.…"   app-blue            running             80/tcp
loadbalancer        "nginx -g 'daemon of…"   loadbalancer        running             0.0.0.0:80->80/tcp, :::80->80/tcp

http://localhostにアクセスするとapp-blueに流れていることがわかる。

$ docker-compose logs -f

...

app-blue      | 172.18.0.4 - - [16/Mar/2022:11:14:52 +0000] "GET /tutorial/ HTTP/1.0" 304 0 "http://localhost/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36" "-"
app-blue      | 172.18.0.4 - - [16/Mar/2022:11:14:53 +0000] "GET /assets/images/favicon.png HTTP/1.0" 200 521 "http://localhost/tutorial/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36" "-"

この状態でdeploy.shを実行するとapp-greenに切り替えてくれる。 「app-green起動」→「upstream書き換え(app-green up & app-green down)」→「app-blue停止」という流れでデプロイしている。

$ sh deploy.sh
[+] Running 1/1
 ⠿ app-green Pulled                                                                                                             2.4s
[+] Running 1/1
 ⠿ Container app-green  Started                                                                                                 0.6s
server 172.18.0.2:80 weight=1 max_fails=1 fail_timeout=10;
server 172.18.0.3:80 weight=1 max_fails=1 fail_timeout=10;
server 172.18.0.2:80 weight=1 max_fails=1 fail_timeout=10 down;
server 172.18.0.3:80 weight=1 max_fails=1 fail_timeout=10;
[+] Running 1/1
 ⠿ Container app-blue  Stopped                                                                                                  0.2s
Going to remove app-blue
[+] Running 1/0
 ⠿ Container app-blue  Removed                                                                                                  0.0s
deployment is completed!
===== current upstream =====
server 172.18.0.2:80 down;
server 172.18.0.3:80;
===== current service =====
NAME                COMMAND                  SERVICE             STATUS              PORTS
app-green           "/docker-entrypoint.…"   app-green           running             80/tcp
loadbalancer        "nginx -g 'daemon of…"   loadbalancer        running             0.0.0.0:80->80/tcp, :::80->80/tcp

切り替え中のdocker-composeのログはこんな感じ。

app-green     | 2022/03/16 11:17:18 [notice] 1#1: using the "epoll" event method
app-green     | 2022/03/16 11:17:18 [notice] 1#1: nginx/1.21.6
app-green     | 2022/03/16 11:17:18 [notice] 1#1: built by gcc 10.3.1 20211027 (Alpine 10.3.1_git20211027)
app-green     | 2022/03/16 11:17:18 [notice] 1#1: OS: Linux 4.18.0-348.el8.0.2.x86_64
app-green     | 2022/03/16 11:17:18 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576
app-green     | 2022/03/16 11:17:18 [notice] 1#1: start worker processes
app-green     | 2022/03/16 11:17:18 [notice] 1#1: start worker process 32
app-green     | 2022/03/16 11:17:18 [notice] 1#1: start worker process 33
app-green     | 2022/03/16 11:17:18 [notice] 1#1: start worker process 34
app-green     | 2022/03/16 11:17:18 [notice] 1#1: start worker process 35
app-blue      | 2022/03/16 11:17:19 [notice] 1#1: signal 3 (SIGQUIT) received, shutting down
app-blue      | 2022/03/16 11:17:19 [notice] 32#32: gracefully shutting down
app-blue      | 2022/03/16 11:17:19 [notice] 32#32: exiting
app-blue      | 2022/03/16 11:17:19 [notice] 33#33: gracefully shutting down
app-blue      | 2022/03/16 11:17:19 [notice] 33#33: exiting
app-blue      | 2022/03/16 11:17:19 [notice] 34#34: gracefully shutting down
app-blue      | 2022/03/16 11:17:19 [notice] 32#32: exit
app-blue      | 2022/03/16 11:17:19 [notice] 34#34: exiting
app-blue      | 2022/03/16 11:17:19 [notice] 33#33: exit
app-blue      | 2022/03/16 11:17:19 [notice] 35#35: gracefully shutting down
app-blue      | 2022/03/16 11:17:19 [notice] 35#35: exiting
app-blue      | 2022/03/16 11:17:19 [notice] 34#34: exit
app-blue      | 2022/03/16 11:17:19 [notice] 35#35: exit
app-blue      | 2022/03/16 11:17:19 [notice] 1#1: signal 17 (SIGCHLD) received from 34
app-blue      | 2022/03/16 11:17:19 [notice] 1#1: worker process 33 exited with code 0
app-blue      | 2022/03/16 11:17:19 [notice] 1#1: worker process 34 exited with code 0
app-blue      | 2022/03/16 11:17:19 [notice] 1#1: signal 29 (SIGIO) received
app-blue      | 2022/03/16 11:17:19 [notice] 1#1: signal 17 (SIGCHLD) received from 32
app-blue      | 2022/03/16 11:17:19 [notice] 1#1: worker process 32 exited with code 0
app-blue      | 2022/03/16 11:17:19 [notice] 1#1: worker process 35 exited with code 0
app-blue      | 2022/03/16 11:17:19 [notice] 1#1: exit

再びhttp://localhostにアクセスするとapp-greenへ流れていることがわかる。

app-green     | 172.18.0.4 - - [16/Mar/2022:11:21:01 +0000] "GET /tutorial/ HTTP/1.0" 304 0 "http://localhost/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36" "-"
app-green     | 172.18.0.4 - - [16/Mar/2022:11:21:08 +0000] "GET /tutorial/ HTTP/1.0" 304 0 "http://localhost/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36" "-"

ちなみにコンテナ内のnginx.confに記述したupstreamはコンテナ起動時のまま。 でもちゃんとapp-greenにリクエストが流れてくれる。

$ docker exec -it loadbalancer cat /etc/nginx/nginx.conf

...

    upstream backends {
        zone zone_for_backends 128k;
        server app-blue;
        server app-green down;
    }
}

$ curl "http://127.0.0.1/dynamic?upstream=zone_for_backends"
server 172.18.0.2:80 down; ←app-blue
server 172.18.0.3:80;   ←app-green

以後deploy.shを実行するたびにapp-blue→app-green→...となってくれる。


工夫の余地はあるかもしれないが、ngx_dynamic_upstreamを用いてblue/greenデプロイを実現することはできそう。