りんごとバナナとエンジニア

エンジニア修行の記録

OpenAPIでRestfulAPIの仕様を定義する

一時期会社の他チームで、OpenAPIを取り入れようと検討されていた時期があった。結局他の開発業務が立て込んだため採用はされなかったのだが、API開発の一手法として興味があったのでちょっと使ってみた。

OpenAPIとは

OpenAPIは、RestfulAPIのインターフェースを、人間とコンピュータ両方によって読みやすい形で定義するための一手法だ。YAML形式またはJSON形式で定義を行う。
様々な言語やフレームワークによって、OpenAPIの定義ファイルをコードに変換するプラグインが用意されていることが多い。そのため、仮にソフトウェアの言語・フレームワークを変更することになっても、定義ファイルを共通で使うことができれば開発を効率化できる。また、定義ファイルをもとにドキュメントも自動生成できてしまう。

OpenAPIを書いてみる

ちょっとしたTodoアプリのAPIを定義してみた。なお、今回使っているのはOpenAPI v3.0の方だ。

paths

基本的には、 paths の子要素としてエンドポイントを定義し、それぞれのエンドポイントごとにリクエストパラメータ・リクエストボディ・レスポンス等を書いていけば良い。例えば、Todoアプリのタスク一覧をGETで取得するエンドポイントは以下のように定義する。

  /tasks:
    get:
      summary: List all tasks
      operationId: listTasks
      responses:
        '200':
          description: array of tasks
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Tasks'
        default:
          description: unexpected error
          content: 
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

また、タスクのIDを指定して編集を行うエンドポイントはこのように定義できる。 リクエストパラメータは paramter 以下で定義したうえで、エンドポイントの中で {} で囲んで使用する。

  /tasks/{taskId}:
    put:
      summary: edit a task
      operationId: editTask
      parameters: 
        - name: taskId # taskIdパラメータの定義
          in: path
          required: true
          description: id of task
          schema:
            type: string
      requestBody:
        description: task to edit
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TaskRequest'
      responses:
        '201':
          description: edited
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Task'
        default:
          description: unexpected error
          content: 
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

components

各エンドポイントのリクエストボディ・レスポンスの定義は、それぞれのpathオブジェクト内の content 内で行える。しかし、共通して使いそうなschemaが多い場合は、 components/schema 以下で定義できる。
先程の例では、$ref というキーの中で、 Tasks, Task, TaskRequest, Error というschemaが指定されていた。このようにすることで、components/schema以下で定義したオブジェクトを参照できる。

例えば、追加・編集したタスクを返すレスポンス用に、以下のようなschemaを定義した。

components:
  schemas:
    Task:
      description: one task object
      type: object
      allOf:
        - $ref: '#/components/schemas/TaskRequest'
        - required:
          - id
          - done
          properties:
            id:
              type: integer
              format: int64
            done:
              type: boolean
      example:
        id: 1
        content: Buy notebook
        urgent: 3
        important: 3
        done: false

required 以下では必須のパラメータを定義している。また、 properties 以下では各パラメータの詳細を記載している。
allOf という指定があるが、これは「子要素のschema全てを満たす」という意味だ。この場合、 TaskRequest schemaで定義されたフィールドに加え、 required 指定となっている iddone のフィールドも持たなければいけない。
ちなみに、 TaskRequest schemaは以下のように書いている。

TaskRequest:
      description: object to create or edit a new task
      type: object
      required:
        - content
        - urgent
        - important
      properties:
        content:
          type: string
        urgent:
          type: integer
          format: int64
        important:
          type: integer
          format: int64
      example:
        content: Buy notebook
        urgent: 3
        important: 3

これらを分けた理由は、新しくタスクを作る場合や編集する場合に、 iddone を指定させたくなかったためだ。 id はタスク作成の際にデータベース側で付与され、編集の際もリクエストパラメータで指定する。また、 done についてはタスクを完了にするための別のエンドポイント( /tasks/{taskId}/done )に切り出したいと考えた。このように、一部のフィールドが重なる複数のリクエストを使いたいとき、 allOf などでschemaを結合させるとうまく書ける。

example

example以下に記載した内容は、ドキュメントを生成した際の、各リクエスト・レスポンスの例として使われる。例が書かれていないドキュメントは非常に分かりにくいので、適切なexampleを記載することも重要だ。
exampleを書ける場所はいくつかある。

  • paths以下の各エンドポイント内にある content 内で定義
  • components/schema 以下の各schemaの子要素として定義
  • components/examples 以下に定義

どれを使うかは、examplesをどれほど共通化したいかによる。ただ実際に書いてみて思ったのだが、例を共通化しすぎるとかえって煩雑になるときが結構ありそうだ。例えば、1つのpathの中に場合分けして複数の例を書きたい場合や、同じschemaでもpathごとに使う文脈が異なるため例を分けたいという場合がある。例は様々なパターンをカバーしている方がユーザにとって分かりやすくなるので、あまり共通化に囚われすぎない方が良いかもしれない。

tmux 3.0でコピーモードの設定を行う

先日、tmuxを久しぶりに使う機会があった。 tmux.conf の中身は、以前ネットで拾ってきたものをそのまま使っていたのだが、設定内容を全然使いこなせていないうえ、あまりに久しぶりすぎて中身の意味もわからない状態になってしまっていた。そこで、思い切って tmux.conf を消して作り直すことにした。
今回はコピーモードの設定を行う。tmuxを仕事で使う時で最も重要な場面は、ssh接続で時間のかかる作業を行う時であり、その場合は出力結果をコピーしてドキュメントに残しておかなければいけないことが多い。そのため、クリップボードとつなげておくことは不可欠だ。

コピーモードでのKey Table

man tmux で表示されるマニュアルが参考になった。

man7.org

コピーモードには2種類のKey Tableが用意されており、それぞれviとemacsと名付けられている。名前の通り、viやemacs風のキーバインドを使うことができ、デフォルトではemacsの方が使われる。
自分はviの方がなじみがあるので、viのKey Tableを使うことにした。 tmux mode-keys コマンドでKey Tableを設定できる。

setw -g mode-keys vi

余談だが、 tmux.conf に書く内容は、全て tmux コマンドに続ける形でコマンドラインから実行できる。単に実行させたいコマンドの一覧を書いているだけなので、設定ファイルの書き方を調べるよりもマニュアルを見る方がわかりやすかった。

コピーモードでのキーバインド

次に、よりviに近くなるようなキーバインドに変更する。ビジュアル選択は v キーで行い、コピーは y キーにしたい。
マニュアルによると、 tmux bind-key コマンドでキーバインドを変えることができる。 bind-key [-nr] [-T key-table] key command [arguments] の形式である。また、エイリアスとして bind コマンドでも良い。

また、コピーモードでのキーバインドを設定する際は、 send-keys コマンドが必要になる。マニュアルの以下の部分が非常にわかりやすい。

Commands are sent to copy mode using the -X flag to the send-keys com‐
     mand.  When a key is pressed, copy mode automatically uses one of two
     key tables, depending on the mode-keys option: copy-mode for emacs, or
     copy-mode-vi for vi.  Key tables may be viewed with the list-keys com‐
     mand.

     The following commands are supported in copy mode:

           Command                                      vi              emacs
           append-selection
           append-selection-and-cancel                  A
           back-to-indentation                          ^               M-m
           begin-selection                              Space           C-Space
           bottom-line                                  L
           cancel                                       q               Escape
           clear-selection                              Escape          C-g
           copy-end-of-line [<prefix>]                  D               C-k
...

コピーモードで使えるコマンドは、 begin-selection などあらかじめ定義されている。指定したキー(例えば v キー)を押すと、 send-keys コマンドが呼び出され、対応するコピーモードコマンドが呼び出されるという流れになっている。
デフォルトでのキーバインド設定は、 tmux list-keys コマンドで見ることができる。

tmux list-key
bind-key    -T copy-mode    C-Space               send-keys -X begin-selection
bind-key    -T copy-mode    C-a                   send-keys -X start-of-line
bind-key    -T copy-mode    C-b                   send-keys -X cursor-left
bind-key    -T copy-mode    C-c                   send-keys -X cancel
bind-key    -T copy-mode    C-e                   send-keys -X end-of-line
bind-key    -T copy-mode    C-f                   send-keys -X cursor-right
bind-key    -T copy-mode    C-g                   send-keys -X clear-selection
bind-key    -T copy-mode    C-k                   send-keys -X copy-end-of-line
bind-key    -T copy-mode    C-n                   send-keys -X cursor-down
bind-key    -T copy-mode    C-p                   send-keys -X cursor-up
...

tmux.conf にも、この形式の通りに書けば良い。 v キーと y キーであれば、以下のように書ける。

bind-key -T copy-mode-vi v send-keys -X begin-selection
bind-key -T copy-mode-vi y send-keys -X copy-pipe-and-cancel "pbcopy"

なお、OSのクリップボードと連携させるためには、以前は reattach-to-user-namespace ライブラリが必要だったが、 tmux2.6以降では必要なくなったそうだ。

blog.nijohando.jp

Kafkaのメッセージング方式の特長を学び直した

自社サービスでKafkaを使っており、自分でも少し触ったことがあるのだが、Kafkaというのがメッセージングにおいてどんな特長を持ったサービスなのかについては、topicを介してproducerとconsumerがメッセージをやり取りする、という程度の理解しかなかった。これではいけないと思い、年末年始に公式ドキュメントを読み直したのでまとめておく。

kafka.apache.org

メッセージングサービスの大まかな分類

世の中のメッセージングサービスのモデルにはいろいろあるが、最もよくあるモデルはqueueとpub-subだろう。
queueモデルは、複数のconsumerがqueue内のレコードを読むが、それぞれのレコードは1つのconsumerの所にしか行かない。queueから出したらもうそのレコードはqueueの中にはない。
対してpub-subモデルは、複数のconsumerそれぞれに全レコードが配信される。ラジオの放送局が流す電波をどのラジオでも受け取って放送を聴けるように、1つのconsumerがレコードを受け取っても、そのレコードは他のconsumerも受け取ることができる。

これらのモデルには一長一短がある。queueモデルの場合、複数のconsumerを用意するだけで簡単に処理速度を上げることができる。しかし、1つのrecordは1つのconsumerにしか行かないので、障害発生時にデータロストしやすい。また、queueから渡される時点ではデータは順序どおりだが、各consumerは非同期で処理を行っているので、受け取る際にはデータの順序が入れ替わってしまったり、重複して受け取られてしまったりする可能性がある。
これに対してpub-subモデルの場合、どのconsumerも全データを受け取ることができるので、障害にも比較的強い。しかし、consumerの数を増やしても処理速度に跳ねにくい。

Kafkaの特長

Kafkaはqueueモデルとpub-subモデルを合わせ持ったような性質がある。まず、Kafkaは複数のconsumerをconsumer groupとして論理的に区分けすることができる。そして、同じconsumer group内のinstanceに対しては、recordはロードバランシングされる。違うconsumer groupには、それぞれに対して全recordが送られる。
すなわち、同じconsumer group内のconsumer instance同士では、queueモデルのようにレコードを読む処理を分担できる。一方で、異なるconsumer groupの間ではpub-subが成り立っている。

またKafkaの場合、topic内に複数のpartitionを持っている。topicは論理的な区分けでしかなく、partitionが物理的な区分けである。(そのため、1topic内のpartitionが複数のサーバに分離していることもある)
そしてconsumer groupに対し、1つのpartitionを1つのconsumer instanceに割り当てている。

f:id:Udomomo:20200105220629p:plain

(出典: https://kafka.apache.org/intro)

これにより、各partitionを読むconsumerが1つだけになり、各partiton内のデータは必ず順序通りconsumeされる。(もちろん、複数のpartition間の順序は保証されない)
なお、この方法の制約として、consumerがpartitionの数以上存在することはできない。

【Java】雰囲気でmockしていた自分のためのMockito再入門

Mockitoはテストの際に何度も使ったことがあるが、mockやspy, injectmocks等の用語の意味をなんとなくでしか理解しておらず、使う際に何度も詰まってしまっていた。このたび、公式ドキュメントを改めて読み直してみたのでまとめておく。

javadoc.io

mockとは

Mockitoでは、インターフェースやクラスをmockすると、そこに定義されたメソッドを呼んでもコンパイルエラーにならなくなる。また、mockは各メソッドが呼ばれた回数も記憶する。
ドキュメントには、例えとしてListインターフェースをmockしたコードが載っている。(もちろんこんなことをやる機会はほとんどないだろう)

 import static org.mockito.Mockito.*;

 List mockedList = mock(List.class);

 mockedList.add("one");
 mockedList.clear();

 verify(mockedList).add("one");
 verify(mockedList).clear();

mockの意味はこれだけ。自分が勘違いしていたところでもあるが、mockだけをしてどこにもstubしない場合もある。例えば、単に verify を使ってメソッドの呼び出し回数をテストしたいだけの時などは、stubする意味がない。
また、上記でmockオブジェクトを定義するところの記述は、 @Mock アノテーションを使えばより簡潔に書ける。ただしこの場合、テストクラス内(またはその親クラス)のどこかに MockitoAnnotations.initMocks(testClass); の記載が必要となる。

stubとは

単にmockしただけだと、mockオブジェクトのメソッドは返り値の型に応じてデフォルトの値を返すことしかしない(int型なら0, boolean型ならfalseなど)。これではテストにならない。
そこで、特定の引数によって特定のメソッドが呼び出されたとき、その結果をoverrideすることができる。これがstubである。ナイフで結果を差し込むイメージだろうか。

LinkedList mockedList = mock(LinkedList.class);

when(mockedList.get(0)).thenReturn("first");
when(mockedList.get(1)).thenThrow(new RuntimeException());

上の例では、 when を使っている部分がstubにあたる。

spyとは

Mockitoでは、ある特定のオブジェクトのspyを作ることができる。(mockと違い、対象はオブジェクトである)
spyの特徴は、メソッドを呼び出したときに、spyしたオブジェクト内で定義された本物の処理が呼ばれることにある。先程書いたように、普通のmockではデフォルトの値が返ってくるだけ。
さらに、spyにstubすることもできる。このように一部のメソッドだけstubすると、他のメソッドは本当に呼ばれるので、partial mockingということができる。なお、partial mockingはテストコードを複雑にするとして、使用が推奨されていない。

List list = new LinkedList();
List spy = spy(list);

when(spy.size()).thenReturn(100);

spy.add("one");
spy.add("two");
System.out.println(spy.get(0));

System.out.println(spy.size());

上記の例では、 spy.add メソッドは本当に呼び出され、 "one" と "two" の2つの要素が追加される。しかし、 spy.size メソッドはstubされているため、sizeを取得すると100が返ってくる。

InjectMocks

@InjectMocks を指定したクラスに、mockオブジェクトやspyオブジェクトをinjectしてくれる。しかし、ドキュメントを読んでみると、どのような場合でもinjectできるわけではないようだ。
injectが成功するのは、以下の3つの場合に限られる。(spyを使う機会は少ないので、以下はmockの場合のみ書いている)

  • Constructor Injection: mockしたクラスが、InjectMocks指定されたクラスのコンストラクタ引数になっている
  • Property Setter Injection: コンストラクタに引数はないが、mockしたクラスをセットできるsetterがinjectMocks指定されたクラスに定義されている
  • Field Injection: コンストラクタに引数はないが、mockしたクラスをセットできるフィールドがinjectmocks指定されたクラスに定義されている

すなわち、mockしたクラスではない別の引数がコンストラクタにある場合、mockしたクラスを扱うsetterやfieldがあっても、injectに失敗する。

約1年間ブログを続けられている理由を振り返る

この記事は、write-blog-every-week Advent Calendar 201920日目の記事です。

ブログを定期的に書くようになって大体1年くらい経つ。このブログを作ったのは2017年のことだが、最初の頃はほとんど更新せず、2018年は月1回未満のペースだった。しかし2019年は、ほぼ1年にわたって月3-4記事書くことができている。
自分は何かを習慣づけることが決して得意な方ではなく、またブログを最優先事項にしているわけでもない。それでいてなぜブログをここまで続けられているのか自分でも気になっているので、一度振り返ってみたい。自分にとって大きかったのは、Slackコミュニティ・ブログメンタリング・そしてブログに対する意味の捉え直しの3つだったと思う。

Slackコミュニティに入って締め切りを作る

まずブログのことを意識から飛ばさないために、2018年の後半にwrite-for-every-weekというSlackコミュニティに入った。このAdvent Calendarの他の記事でも紹介されているが、ブログを週1回ペースで書いていき、3週分溜まったら退会というわかりやすいルールのコミュニティだ。
入ってしまった以上、そう簡単に退会にはなりたくない。コミュニティからは水・金・日などに定期的にリマインダーが飛んでくるため、ブログのことをすっかり忘れていたということは起こらなくなった。

ブログメンタリングで習慣づける

しかしコミュニティに入ったばかりの自分は、ブログを自分から書くという習慣が全く根づいていなかった。そのため、コミュニティの力だけでは、締切守りたさにテキトーな記事や技術以外の記事を量産してしまうのではないかという不安が残っていた。
そんなとき、運良くカックさんによるブログメンタリングの募集が回ってきて、2019年の1-3月の間メンタリングをしていただくことになった。

ブログメンタリングは、write-for-every-week同様に毎週定期的に記事を書くのだが、カックさんからのFBを受けたり、ネタの相談をしたりできる。カックさん自身も毎週ブログを更新されており、特にネタの出し方や記事への膨らませ方のパターンはとても参考になった。このメンタリングを通じて、毎週記事を書くペース感をつかめたと思う。
(ちなみにカックさんのブログメンタリングの取り組みは今も続いている。定期的に募集があるので、興味がある方はぜひ参加してほしい)

ブログの意味を捉え直す

メンタリングが終わった後も、ここからが本番ということで頑張って記事を書いていた。しかし、やはり仕事やプライベートが忙しい時期もあり、必ずしもブログに時間を割ける週ばかりではない。時には過去にストックした小ネタや豆知識でやり過ごしたり、3週分記事が溜まってしまい、コミュニティから退会処理される前に慌てて平日に書いたりする週もあった。

普段の自分ならそこで更新ペースが落ちていったと思う。しかし運が良かったのは、ある日カックさんの過去の登壇資料をもう一度見返したことだった。そこに写っていたこのスライドを見たとき、自分の意識が変わった。

この内容自体はブログメンタリング中にも見ており、自分が3月に書いたメンタリング卒業記事の中にも書いてあった。 ただ、「忙しさのあまりブログが書けなくなりつつあった + 自分のスキルに危機感を持ち始めた」という2つの問題意識を抱えている時に目にしたことで、改めて心に刺さったのだと思う。
確かに、同じように仕事をしていても、「今週は何も成長してないな...」と思う週もあれば「今週はいろんなことを学べた気がする!」と思う週もある。そして自分が多くの学びや気づきを得た週は、そこから自然とネタが出てくるので、後はそれを文章にするだけで記事を更新できていた。

このスライドをきっかけに、自分の中で

  • 「今週は忙しくてブログのネタが浮かばなかった」-> 「今週の記事を書けないくらい、ここ最近は何も学んでいなかった」
  • 「ブログを続けられるようがんばろう」 -> 「もっと自分から知識や知見を吸収しに行こう。ブログはその記録」

というように意味を捉え直すことができた。

ブログとの付き合い方

そういうわけで、今の自分は記事執筆自体に力を入れている感覚はない。ブログはあくまで学んだことの記録にすぎないからだ。
そのかわり、分からないことや十分な知識がないと感じることがあったら、放置せずにちゃんと調べて、それをメモに残すことを心がけている。メモに残しておけば、時間が経って週末になっても記事を書きやすい。(もちろんその日のうちに記事を書くのがベストなのだが、平日忙しいことに変わりはなく、執筆の時間を取るのは未だに難しい。平日更新を続けている人たちは改めて凄いと思う)

来年もブログは続けるつもりでいるが、特にKPI(PV数やブクマ数など)は置いていない。「write-blog-every-weekを退会にならない」というのみだ。けれど、自分が学んだことが一人でも誰かの役に立ったり、強い人がコメントを書いてくれてさらなる気づきを得られたりすればいいなと思っている。

minikube Clusterをdeleteしたらkubectlが使えなくなった

ローカル環境でKubernetesのClusterを動かす方法はいろいろある。よく紹介されているのはminikubeを使う方法だが、自分の場合Docker Desktopを使っている。Docker DesktopにはKubernetesが付属しており、追加のインストールなしでClusterを立ててkubectlで操作できる。 先日試しにminikubeを少しだけ使ってみたものの、仕事のタスクを抱えていたため使い慣れたDocker Desktopに戻すことにした。

minikubeのClusterを停止させ、削除する。

minikube stop
minikube delete

すると、Docker Desktopをkubectlで操作することができなくなってしまった。

kubectl config use-context development
Error from server (NotFound): the server could not find the requested resource

kubectl config current-contextkubectl config get-contexts 等も試したが同じエラー。

途方にくれていたが、minikubeのClusterを消したせいで、contextを正常に読み込めていないからではないかと気づく。本来であればkubectlはminikubeと一緒にインストールされるのだが、今回はもともとDocker Desktopによってインストールされたkubectlを使っており、そこにminikubeの設定が追加された形になっていそうだった。
そこでminikubeのcontextを削除してみる。

kubectl config delete-context minikube
kubectl config use-context development

今度は正しくcontextを切り替えることができ、他の操作もできるようになった。

KubernetesのRBACを触って学ぶ

先日Kubernetesの特定のnamespaceでJobを実行しようとしたところ、権限がなかったため、まずClusterの管理者に権限を申請するところから始める必要があった。これを機会に、KubernetesのRBACについて学んでみた。

RBACとは

RBACとは、Role-based access controlの略。どのリソースに対するどんな操作を許可するかを、UserやServiceAccount単位で指定することができる。
他のリソース同様、RBACもyamlファイルで管理する。Role の設定ファイルでRoleを定義し、 RoleBinding の設定ファイルでそのRoleをどのUser, ServiceAccountに付与するかを指定する。 この他に ClusterRole, ClusterRoleBinding の設定ファイルも作ることができる。 Role, RoleBinding は特定のnamespaceにおける権限を指定するのに対し、 ClusterRole, ClusterRoleBinding はnamespaceによらない権限を指定するためにある。名前の通り、Cluster全体に適用される権限ということなのだろう。

新しいRoleを追加する

今回、既存のRoleファイルはnamespace内で全ての操作ができる事実上のmaster権限のものしかなかった。さすがにそこまでの権限は不要なので、新しくRoleファイルを作ることにした。

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: development
  name: running-job
rules:
  - apiGroups: [""]
    resources: ["pods","pods/log"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["batch"]
    resources: ["jobs"]
    verbs: ["create", "get", "list", "watch", "update", "patch", "delete"]

metadata.namespace には、そのRoleの対象となるnamespaceを指定する。 metadata.name はこのRoleの名前であり、これはRoleをbindする際にも使う。
権限の内容は rules に書いていく。apiGroupsは複数指定することができ、それぞれにおいて対象となるresourceと、許可する操作( verbs )を指定できる。上記のようにするとJobの実行権限の他に、podsの一覧の取得や、各podのログの確認も行うことができる。
なお、公式ドキュメントによると、Roleファイルは"purely additive"であるとのこと。すなわち、AWSのpolicyのように Deny の設定をすることはできない。

Roleを割り当てる

権限の割り当てはRoleBindingファイルで行う。

kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: running-job-binding
  namespace: development
subjects:
- kind: User
  name: udomomo
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: running-job
  apiGroup: rbac.authorization.k8s.io

重要なのは、 roleRef.name の名前をRoleファイルの metadata.name と一致させること。ここが間違っていると正しくbindすることができない。
Userの部分は、各Kubernetes Clusterのユーザ管理のやり方に従って指定する。今回の場合EKSを使っていたので、AWS IAMアカウントをそのまま指定するだけで良かった。EKSでは権限管理をpolicyではなく独自の方法で行っていると聞いたことがあるが、RBACがまさにその役割を果たしているということがようやく理解できた。