Cybozu Inside Out | サイボウズエンジニアのブログ

 

Cybozu Tech Meetupスタートします

こんにちは、Yakumoチーム兼コネクト支援チームの@ueokandeです。 コネクト支援チームは、「社内のエンジニアと社外のエンジニアのコネクトを支援する」ことを目的に活動しています。 この度『Cybozu Tech Meetup』という技術発信イベントをスタートすることにしました。

Cybozu Tech Meetupのロゴ画像

Cybozu Tech Meetupとは

Cybozu Tech Meetupは、社外の方にサイボウズのエンジニアや技術について知ってもらおうと思い、スタートしました。 このイベントでは、現場で働くエンジニアが普段の活動や開発・運用で得た知見などをお届けします。 またコネクト支援チームとしても、社内のエンジニアにもっと外へ飛び出してもらいたいという狙いもあります。

これまでに「Cybozu Meetup」というオフラインイベントを定期的に開催してきましたが、この度「Cybozu Tech Meetup」という名に改めました。 開催場所もオンライン、オフラインは問わず柔軟な開催をしていきたいと思います(が、暫くはオンライン開催のみになるでしょう)。

ここ数週間、新型コロナウイルスの影響で外出自粛要請が出されて、各イベントの中止も続いています。 そんな情勢だからこそ、技術発信をしてエンジニア界隈を元気づけていきたいという思いがあります。 そこでCybozu Tech Meetupの企画・開催に至りました。

第1回目のテーマは『kintone開発チーム』

記念すべき第1回目のテーマは『kintone開発チーム』ということで、kintone開発チームの内情についてお話しします。 kintoneはサービス開始から今年で9年目。開発プロセスも工夫を重ねてきました。 kintone開発チームはプロダクトマネージャー、プログラマー、デザイナー、QA、プロダクトライターのメンバーで構成されており それぞれ別の視点から開発に携わっています。

トークセッションは以下のとおりです。

  • kintone × リモートモブプログラミング / 西 大樹
  • ひよっこ kintone 開発プログラマーの冒険譚 / 中川 遼太郎
  • IT業界未経験者が kintone QA として1年間がんばった話 / 川畑 ひとみ

今回の開催はYouTube Liveによる配信を予定しています。 このイベントへの参加や、詳しいイベント情報は告知ページからどうぞ!

cybozu.connpass.com

おわりに

Cybozu Tech Meetupは今後も定期的に開催する予定で、それぞれの回で異なるチームにフォーカスします。 社内には製品開発チームだけでなく、横断的に活動しているチームなどもあり、それぞれのチームごとに異なる特色や知見があります。 次回以降の開催もご期待ください!

 

LernaとYarn WorkspacesでMonorepo管理

こんにちは、フロントエンドエキスパートチームの小林(@koba04)です。

本記事では、Lerna と Yarn Workspaces を使った Monorepo 管理について解説します。

Monorepoとは

本記事では、単一のリポジトリで複数のモジュールやパッケージ(今回の場合は npm パッケージ)を管理する手法を Monorepo と呼んでいます。

有名なところだと、BabelJestCreate React App などが後述する Lerna を使い複数パッケージを単一のリポジトリで管理しています。 他にも React も Lerna は使っていませんが単一リポジトリで複数パッケージを管理しています。

また、上記のようなライブラリ以外にも企業で利用している npm パッケージを Monorepo として管理している例もあります。下記は Shopify の例です。 packages/ ディレクトリ以下を見ると、ASTの utility から React 関連のライブラリまで色々あることがわかります。

https://github.com/Shopify/quilt/tree/master/packages

単一のリポジトリで複数パッケージを管理するメリット、デメリットには下記のような点があります。

メリット

  • 依存関係のある複数パッケージの開発が簡単
    • 都度パッケージをリリースしたり、npm link をする必要がないため
  • 依存パッケージの管理をまとめてできる
    • Renovate などを使い依存パッケージのバージョンを管理している場合には特に楽

デメリット

  • Issue や PR をすべて 1 つのリポジトリで管理する必要がある
  • Monorepo での開発ワークフローを理解している必要がある

サイボウズでは、すでに kintone のプラグインやカスタマイズ開発を行うための npm パッケージをいくつも提供しており、これらは現状個別のリポジトリで管理されています。 しかしながら、Renovate による依存パッケージの更新対応コストや、お互いに依存関係のあるパッケージの開発など、リポジトリが分かれていることによるデメリットがありました。

現在、新しく kintone のプラグインやカスタマイズの開発体験を向上させるためのツール開発を行っており、これを機会に Monorepo に移行することにしました。 開発体験を向上させる取り組みについては、別記事として紹介したいと考えています。

実際に適用しているリポジトリは下記より確認できます。 現在は 2 パッケージのみですが、今後どんどん増えていく予定です。

https://github.com/kintone/js-sdk

まず最初に、Lerna と Yarn Workspaces について簡単に紹介します。

Lerna

https://lerna.js.org/

Lerna は ”A tool for managing JavaScript projects with multiple packages.” と公式サイトにある通り、複数パッケージを管理するためのツールです。

Lerna は、一例として下記のような機能を提供します。

  • 複数 npm パッケージを単一リポジトリで一元管理
  • lerna bootstrap コマンドを使った Monorepo すべての依存パッケージを一括インストール
  • 重複した依存パッケージの hoisting(巻き上げ)
    • hoisting とは、パッケージ間で重複している依存パッケージをそれぞれ別にインストールするのではなく、親となるディレクトリにインストールして共有することです
  • lerna publish コマンドを使った、変更があるパッケージの一括 npm publish
    • すべての npm パッケージのバージョンを同一にするか、個別に管理するか(independent mode)を選択可能
  • lerna run コマンドを使った Monorepo で管理しているパッケージが持っている npm-scripts の一括実行
  • 既存の git リポジトリから Monorepo へのインポート

このように、Lerna は複数パッケージをまとめて扱うための機能を提供しています。

Yarn Workspaces

https://classic.yarnpkg.com/lang/en/

Yarn は JavaScript のパッケージマネージャです。 Node.js が標準で提供している npm と同様に、npm パッケージの管理ができます。 パッケージマネージャとしての Yarn と npmの違いについては本記事では言及しないので、下記を確認ください。

https://classic.yarnpkg.com/en/docs/migrating-from-npm

※本記事では、Yarn v1 を使用します。

Yarnは Workspaces として、複数の npm パッケージを管理する機能を持っています。 これは npm にはない機能です。

https://classic.yarnpkg.com/en/docs/workspaces

注:npm は v7 で Monorepo サポートを計画しています。下記は RFC です。
https://github.com/npm/rfcs/pull/103

Yarn Workspaces を使うことで下記のようなことが可能です。

  • 複数 npm パッケージを単一リポジトリで一元管理
  • yarn install コマンドを使った Monorepo すべての依存パッケージを一括インストール
  • 依存パッケージの hoisting
  • yarn workspaces コマンドを使った Monorepo で管理しているパッケージが持っている npm-scripts の一括実行
  • 単一のyarn.lockファイルですべての依存関係を管理

上記の通り、Yarn Workspaces と Lerna は同じような機能を持っています。

その違いとして、

Yarn’s workspaces are the low-level primitives that tools like Lerna can (and do!) use

とある通り、Yarn Workspaces の方がより低レベルなパッケージマネージャとして機能を提供しています。

LernaとYarn Workspacesを一緒に使う

Lerna は Yarn Workspaces と一緒に使うためのオプションを提供しています。 Lerna の設定ファイルである、lerna.json"npmClient": "yarn" と指定することで、内部的に npm の代わりに Yarn を使用できます。

実際、多くの Lerna を使っているプロジェクトでは Yarn Workspaces と組み合わせて使っていることが多いです。 ただ、ここで一つ疑問が湧きます。

「 Lerna で Yarn Workspaces が提供している機能はカバーできるが、Yarn を使うメリットはあるのか?」

実際のところ、Lerna と npm だけでも多くの場合はやりたいことを実現できます。
ただ、Yarn はパッケージマネージャ本体で Monorepo をサポートしていることもあり、Monorepo であることを利用者にあまり意識させない形での Monorepo 管理を可能にしてくれます。
例えば下記のような点です。

パッケージのインストール

全パッケージの依存パッケージのインストールはリポジトリのルートで yarn install するだけであり、単一パッケージのリポジトリと同様です。個別のパッケージで依存パッケージをインストールする場合も、対象パッケージのディレクトリで yarn add {package-name} コマンドを実行するだけです。

単一の yarn.lock で管理できる

Yarn Workspaces では yarn.lock ファイルはリポジトリのルートにのみ作成され、それぞれのパッケージ毎に作られません。そのため依存パッケージのアップデートの際に発生する差分は最小限になります。

使い方

ここでは、Lerna と Yarn Workspaces を使った Monorepo 運用のための手順を記載します。

セットアップ

リポジトリのルートにある package.json"private": true を設定します。また Yarn Workspaces のための設定をします。 workspaces には、パッケージ用のディレクトリだけでなく examples のようなサンプルのためのディレクトリも対象に含めることも可能です。 kintone/js-sdk では、デモやサンプルを配置するために examples ディレクトリを使用しています。

// package.json
// workspacesのフィールドを追加
{
    :
    "private": true,
    "workspaces": [
        "packages/*",
        "examples/*"
    ],
    :
}

Lerna をインストールして lerna init を実行します。

% yarn add --dev lerna
% yarn lerna init --independent
lerna notice cli v3.19.0
lerna info Updating package.json
lerna info Creating lerna.json
lerna info Creating packages directory
lerna success Initialized Lerna files
✨  Done in 0.98s.

今回はそれぞれの npm パッケージ単位で個別にバージョンを管理したいので、--independent を指定します。 設定しない場合、すべての npm パッケージのバージョンが同一バージョンになります。

生成された Lerna の設定ファイルである lerna.json に Yarn Workspaces のための設定をします。

{
    "npmClient": "yarn", // 追加
    "useWorkspaces": true,
    "version": "independent"
}

Lerna は useWorkSpaces というオプションを持っています。 これを利用することで Yarn Workspaces のワークスペース設定をそのまま Lerna でも利用できます。

もちろん Lerna と Yarn Workspaces で対象パッケージを分けることも可能です。 この方法は Jest でも採用されており、Jest では Yarn Workspaces で packageswebsiteexamples として管理しており、 Lerna では packages のみ管理しています。

今回は、lerna コマンドで examples ディレクトリも統一的に扱いたいため、useWorkSpaces オプションを使用します。

新規パッケージの追加

新しく開発するパッケージを Monorepo に追加する際は、 lerna create コマンドを使います。

% npx lerna create @kintone/rest-api-client

npm パッケージのインストール

npm パッケージをインストールする場合は、インストールしたいパッケージのディレクトリで通常通り yarn add package-name するか、yarn workspace コマンドを使ってインストールします。

% cd packages/rest-api-client
% yarn add form-data
# or
% yarn workspace @kintone/rest-api-client add form-data

各パッケージで同一の依存ライブラリをインストールした場合、package.json はインストールしたパッケージのものが更新されますが、yarn.lock はルートにあるものが更新されて、インストールもルートの node_modules/ に対して行われます。

共通 npm パッケージのインストール

TypeScript などすべてのパッケージで使うような devDependencies については、ルートで入れておくと個別にインストールする手間を省くことができます(依存関係が各 package.json からわからなくなるというデメリットはありますが)。 ルートに共通で使うパッケージとしてインストールする場合には、インストールのオプションに明示的に -W オプションを付ける必要があります。

% yarn add -W --dev typescript prettier eslint

パブリッシュ

パッケージのパブリッシュは lerna publish で行います。
lerna publish はデフォルトでは CLI 上でリリースするバージョンを指定しますが、Conventional Commits の形式で commit している場合には、--conventional-commits オプションを利用することで commit log からバージョンを決定できます。この際、CHANGELOG.md も自動生成されます。

% npx lerna publish --conventional-commits
lerna notice cli v3.19.0
lerna info versioning independent
lerna info Looking for changed packages since @kintone/rest-api-client@1.0.0
lerna info getChangelogConfig Successfully resolved preset "conventional-changelog-angular"

Changes:
    - @kintone/rest-api-client: 1.0.0 => 1.1.0
    - @kintone/cutomize-uploader: 3.0.1 => 3.0.2

? Are you sure you want to publish these packages? (ynH)
:
Successfully published:
    - @kintone/rest-api-client@1.1.0
    - @kintone/customize-uploader@3.0.2
lerna success published 2 packages

新しくパッケージを npm に publish する場合は下記コマンドで publish できます。

% npx lerna publish from-package --conventional-commits

examples 以下にあるパッケージのように npm にパブリッシュしたくないパッケージには、package.json"private": true を指定します。

Clone後のセットアップ

git clone した後のセットアップは Lerna 単体の場合、独自の lerna bootstrap コマンドを利用する必要がありますが、Yarn Workspaces を使う場合には通常のリポジトリと同様にリポジトリのルートで yarn install を行うだけです。

% yarn install

まとめて npm-scripts を実行する

lerna run というコマンドを使うことで、指定した npm-scripts を Lerna で管理しているすべてのパッケージに対して一括で実行できます。
例えば、すべてのパッケージに test の npm-scripts があることを想定して一括で実行したい場合には、リポジトリのルートで lerna run test を実行します。 --stream オプションをつけることで、それぞれの npm-scripts の出力を表示できます。

kintone/js-sdk で実行すると下記の通りです。

% npx lerna run test --stream
lerna notice cli v3.20.2
lerna info versioning independent
lerna info Executing command in 2 packages: "yarn run test"
@kintone/customize-uploader: yarn run v1.22.4
@kintone/customize-uploader: $ jest --rootDir src
@kintone/customize-uploader: PASS src/__tests__/init.test.ts
@kintone/customize-uploader: PASS src/__tests__/util.test.ts
@kintone/customize-uploader: PASS src/__tests__/import.test.ts
@kintone/customize-uploader: Test Suites: 4 passed, 4 total
@kintone/customize-uploader: Tests:       7 passed, 7 total
@kintone/customize-uploader: Snapshots:   0 total
@kintone/customize-uploader: Time:        1.948s, estimated 2s
@kintone/customize-uploader: Ran all test suites.
@kintone/customize-uploader: Done in 2.95s.
@kintone/rest-api-client: yarn run v1.22.4
@kintone/rest-api-client: $ jest --rootDir src
@kintone/rest-api-client: PASS src/client/__tests__/BulkRequestClient.test.ts
@kintone/rest-api-client: PASS src/__tests__/KintoneAllRecordsError.test.ts
@kintone/rest-api-client: PASS src/__tests__/url.test.ts
@kintone/rest-api-client: PASS src/client/__tests__/File.test.ts
@kintone/rest-api-client: PASS src/__tests__/KintoneRestAPIError.test.ts
@kintone/rest-api-client: PASS src/__tests__/KintoneRestAPIClient.test.ts
@kintone/rest-api-client: PASS src/__tests__/KintoneRequestConfigBuilder.test.ts
@kintone/rest-api-client: PASS src/client/__tests__/RecordClient.test.ts
@kintone/rest-api-client: PASS src/client/__tests__/AppClient.test.ts
@kintone/rest-api-client: Test Suites: 9 passed, 9 total
@kintone/rest-api-client: Tests:       243 passed, 243 total
@kintone/rest-api-client: Snapshots:   0 total
@kintone/rest-api-client: Time:        3.37s
@kintone/rest-api-client: Ran all test suites.
@kintone/rest-api-client: Done in 3.86s.
lerna success run Ran npm script 'test' in 2 packages in 7.5s:
lerna success - @kintone/customize-uploader
lerna success - @kintone/rest-api-client

npm-scripts の一括実行は、yarn workspaces run test のように Yarn Workspaces が提供するコマンドでも可能です。

それぞれのパッケージが testlint など決まった名前で npm-scripts を用意しておくことで、コマンド 1 つで確認が可能になるため、複数パッケージのメンテナンスコストを下げることができます。

kintone/js-sdk では、下記のような npm-scripts を定義することをルールとしています。

https://github.com/kintone/js-sdk/blob/master/CONTRIBUTING.md#create-a-new-package

  • build … TypeScript のコンパイルなどの成果物を生成する処理
  • lint … ESLint などの Lint 処理
  • test … テスト
  • test:ci … CI 上で実行するテスト
  • prerelease … パッケージのリリース前に行いたい処理

上記はルールだけだと漏れてしまうことが容易に想像できるため、上記の npm-scripts をすべて実装しているかを下記のテストでチェックしています。

https://github.com/kintone/js-sdk/blob/a603caabadc695a34f3202eb45699e505b58eb80/tests/npmScripts.test.ts

既存リポジトリからの移行

途中で Monorepo 運用に変える場合、既存のリポジトリを Monorepo での管理に移行する必要があります。Lerna は import コマンドという既存のリポジトリを Monorepo に移行するためのコマンドを提供しています。これを利用することで、元のリポジトリの commit log を残したまま Monorepo に移行できます。 実行方法は、lerna import path/to/target と元のリポジトリを指定するだけです。

ただし Troubleshooting にもある通り、Merge conflict commits があるリポジトリの場合は import に失敗してしまいます。

これは Troubleshooting に従って、--flatten オプションをつけることで import 可能ですが、この場合はMerge commit 単位で commit がまとめられてしまいます。 Merge commit 単位でまとめて欲しくない場合は、一度リポジトリの commit をすべて rebase した状態にすることで import 可能な状態にできます。 @kintone/rest-api-clientkintone/js-sdk への移管のタイミングで上記の方法で import しました。

パッケージを import した後は、最後にリリースした commit に対して package-name@version の形式で GitHub に tag を push する必要があります。 これにより、 Lerna は次回のリリース時にこの tag を起点として差分を検出します。

TypeScriptの型定義

TypeScriptを使っている場合には、型定義パッケージ (@types/xxx) をインストールして利用します。
その際、インストールされるパッケージが hoisting されることによって型定義が node_modules/ に見つからずエラーになってしまいます。 そのため、compilerOptionstypeRoots にリポジトリのルートにある node_modules を追加する必要があります。

"typeRoots": [
    "node_modules/@types",
    "../../node_modules/@types",
],        

ts-nodeを使ったサンプルパッケージの実行

現在 examples のディレクトリには、rest-api-client のサンプルスクリプトを管理するパッケージがあります。
このパッケージは元々 rest-api-client 本体に含まれていたものを切り出したものです。 以前は ts-node で直接実行していたのですが別パッケージとして切り出した結果、スクリプトの実行に rest-api-client のコンパイルが都度必要になってしまいました。これまで通りの体験でスクリプトを実行するために、TypeScript の Compiler Options である paths を使いリンクすることで、別パッケージに切り出した後も引き続き ts-node コマンドでコンパイルせずに実行できるよう対応しました。

    "baseUrl": "../../",
    "paths": {
      "@kintone/rest-api-client": ["packages/rest-api-client/src"]
    },       

ts-nodepaths オプションを有効にするために tsconfig-paths を使用しています。

おわりに

今回は、Lerna と Yarn Workspaces を組み合わせて効率的に複数の npm パッケージを管理する方法を紹介しました。
今回は紹介できませんでしたが、今後パッケージが増えてお互いに依存関係を持つような状態に備えて、TypeScript の Project References の導入も検討しています。

サイボウズでは、Web アプリケーションのフロントエンドだけでなく、プラットフォームのためのツール作りを OSS でやりたいというエンジニアも絶賛募集中です!

https://cybozu.co.jp/company/job/recruitment/list/front_end_expert.html

 

複雑怪奇な nginx を Go と Docker でユニットテストする

全国の nginx 職人のみなさま、こんにちは。野島(@nojima)です。

私の所属するYakumoプロジェクトでは、nginx を Go と Docker によってユニットテスト1しています。 手元で簡単に実行でき、ブランチへのpushのたびにCIでテストされるので、非常に便利です。 この記事では、このnginxのユニットテストについて紹介してみたいと思います。

背景

nginx は極めて柔軟なロードバランサであり、プロダクション環境ではその柔軟さを生かして多彩な役割を担っています。 我々の nginx は、ユーザーからのリクエストを AP サーバーに振り分け、アクセス制限を行い、リクエストをリダイレクトし、HTTPヘッダを付与したり削ったりしています。 しかし、nginx は便利な反面、その設定は極めて複雑になり、読解したり変更したりするのが難しくなっています。 そこで、nginx をユニットテストする仕組み を Go と Docker で作りました。

ユニットテストを導入する前は、nginx の動作確認にはデータセンターへのデプロイが必要でした。 デプロイには結構時間がかかるので、動作確認はとても面倒でした。

ユニットテストの導入により動作確認は非常に効率化されました。 ユニットテストは他のサーバーに依存していないので、ローカル環境やCI環境でも実行できます。 よって、データセンターにデプロイすることなく手元でサクッと動作を確認できますし、トピックブランチを CI で常にテストしておくこともできます。

テストはすべて Docker の内側で行われるので、ローカル環境に特殊なセットアップをしておく必要はありません。 Docker がインストールされていれば動きます。

サンプルコード

この記事のために説明用のサンプルコードを用意しました。 ローカルに clone してきて make test するとテストを実行できます。

https://github.com/cybozu/SAMPLE-test-nginx-with-go-and-docker

テスト対象の nginx

サンプルコードでは、テスト対象となる nginx は以下のような設定になっています(一部抜粋)。 APサーバーにリバプロするエンドポイントや、常に 400 を返すエンドポイントなどがあります。

server {
    listen 80;

    location / {
        proxy_pass http://${AP_SERVER_ADDR};
        ...
    }

    location /secret/ {
        deny all;
    }

    ...
}

注目してほしいのは、proxy_pass の部分です。 APサーバーのアドレスを直接設定ファイルに埋め込むのではなく、AP_SERVER_ADDR という環境変数に切り出しています。 これは、テストする際にAPサーバーをモックサーバーに置き換える必要があるためです。

切り出した環境変数は、コンテナ起動時に envsubst で具体的な値に展開します。 サンプルコードでは entrypoint というシェルスクリプトがその作業をやっています。

サンプルコードでは環境変数は AP_SERVER_ADDR のみでしたが、テスト環境と実環境で値が変わる設定項目があればすべて環境変数に切り出しておきます。 例えば、resolver を使っている場合、テスト環境では Docker の DNS サーバーである 127.0.0.11 を指定しないといけないので、環境変数に切り出します。

テストの概観

テストコードの説明に入る前に、テストの概観を図を使って説明します。 ローカル環境でも CircleCI 環境でも実行できるようにするために、テストは以下のような構成になっています。

テストのアーキテクチャ

太い青枠で囲われた部分が Docker コンテナを表しています。nginx-tester と nginx という2種類のコンテナがあります。

  • nginx-tester

    • go test -v ./... を実行するコンテナです。
    • テストの実行には Go と Docker が必要なので、nginx-tester のイメージには circleci/golang:1.14 を使っています。
    • nginx-tester は必要に応じて nginx コンテナを起動します。
    • コンテナの中から別のコンテナを起動するために Docker outside of Docker の構成を取っています。つまり、ホストの /var/run/docker.sock をコンテナ内にマウントすることで、コンテナからホストの docker を操作できるようにしています。
  • nginx-xxxxxx

    • テスト対象となる nginx を格納しているコンテナです。
    • コンテナ名の -xxxxxx の部分は実際にはランダムな文字列です。これは、同時に複数個起動したときに名前が被るのを防ぐためです。

nginx-tester と nginx が相互に通信できるようにするために、nginx コンテナと nginx-tester は同じ docker network に所属しています。 この docker network はテストの起動前にシェルスクリプトで作成しておきます

AP のモックサーバーは独立したコンテナではなく、nginx-tester 内の goroutine として起動します。図の破線で囲われた部分が goroutine を表しています。

次に CircleCI 上でどのようにテストを実行するかを説明します。 CircleCI では、setup_remote_docker を使ってリモート環境をプロビジョンできます。 テストはこのリモート環境の中で実行されます。

CircleCIでの実行の様子

CircleCI でこのテストを走らせるにあたって、注意しないといけないのは次の2点です。

  1. リモート環境と Primary Container (config.yml に書かれたステップを実行するコンテナ)の間では、セキュリティ上の理由から、HTTP や TCP などの通信が行えません。よって、リモート環境との通信は docker コマンドを用いたものに限定されます。
  2. リモート環境では、Primary Container 上のファイルシステムが見れません。つまり、リモート環境からはソースコードが見えないということです。

実は、nginx-tester をローカルで直接実行せずにわざわざコンテナ内で実行していたのは、1 の問題を回避するためでした。

2 の問題に関しては CircleCI の公式ドキュメントに回避策が載っています。 以下の手順を行うことで、Primary Container から nginx-tester にソースコードを渡すことができます。

  1. nginx-test-data という、空のボリュームを持つダミーのコンテナを作成する。
  2. docker cp でソースコードをダミーコンテナに転送する。
  3. docker run--volumes-from オプションを使って、ダミーコンテナのボリュームをマウントする。

さて、テストの概観がわかったところで実際のテストコードを見ていきましょう。 まずはより簡単なリバプロなしの場合から始めます。

テストコード (リバプロなし)

次のテストは GET /secret/ で 400 が返ってくることを確認するものです。

func TestSecretEndpoints(t *testing.T) {
    t.Parallel()

    nginx := StartNginx(t, NginxConfig{}) // ①
    defer nginx.Close(t)
    nginx.Wait(t)
 
    resp, err := http.Get(nginx.URL() + "/secret/") // ②
    if err != nil {
        t.Fatal(err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusForbidden { // ③
        t.Errorf("status code should be 400, but %d", resp.StatusCode)
    }
}

StartNginx() は nginx コンテナを起動する関数です。詳細は後述します。 次に nginx.Wait() で起動が完了するまで待ちます。

② nginx に対して GET /secret/ を行います。http.Get() は単なる Go の標準関数です。

③ レスポンスを assert します。これも普通の Go のテストコードです。

前節では Docker を使ってテストすると説明しましたが、Docker はこのテストケースのコードからは完全に隠蔽されています。 これは、Docker にまつわる複雑さをテストケースから分離することで、テストケースを読みやすくするためです2

Docker を実際に操作しているのは StartNginx()nginx.Close() などの関数です。 それでは StartNginx() の実装を見ていきましょう。

StartNginx()

StartNginx() の肝となる部分は以下のコードです。この関数の主な仕事は docker コマンドを叩くことです。

// docker コマンドを叩いて sample-nginx:latest を起動する。
args := []string{
    "run", "--rm",
    "--name", name,
    "--net", network,
    "-e", fmt.Sprintf("AP_SERVER_ADDR=%s", config.APServerAddress),
    "sample-nginx:latest",
}
cmd := exec.Command("docker", args...)
if err := cmd.Start(); err != nil {
    t.Fatal(err)
}

このコードで注目してほしい点は、-eAP_SERVER_ADDR に値を渡していることです。 これにより、任意のAPサーバーを差し込んで nginx を起動できるわけです。 それでは、実際にAPサーバーを差し込むテストを見ていきましょう。

テストコード (リバプロあり)

次のテストは AP サーバーをモックしてリバプロの挙動を確認するものです。

func TestReverseProxy(t *testing.T) {
    t.Parallel()

    ap := StartMockAP(t) // ①
    defer ap.Close(t)

    nginx := StartNginx(t, NginxConfig{ // ②
        APServerAddress: ap.Address(),
    })
    defer nginx.Close(t)
    nginx.Wait(t)

    resp, err := http.Get(nginx.URL() + "/") // ③
    if err != nil {
        t.Fatal(err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        t.Errorf("status code should be 200, but %d", resp.StatusCode)
    }

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        t.Fatal(err)
    }
    if string(body) != "I am AP server" {
        t.Errorf("unexpected response body: %s", string(body))
    }
}

StartMockAP() でモックAPを起動しています。

StartNginx() で nginx コンテナを起動します。ここでモックAPのアドレスを差し込んでいることに注目してください。

③ AP と nginx を起動できたら、あとはもう普通のテストです。リクエストを送り、レスポンスを普通に assert しましょう。

MockAP を Nginx に差し込んでいる部分は OOP における Dependency Injection に相当します。 普通、Dependecy Injection は同じプロセス内のオブジェクトに対して行うのですが、この例だと別のプロセスに対して依存オブジェクトを差し込んでいる形になっているのが面白いところです。

それでは最後に StartMockAP() の実装を見てみましょう。

StartMockAP()

StartMockAP() は以下のようになっています(一部説明に不要な部分を省略しています)。 ポートを自動的に選ぶためにちょっと特殊なことをしていることを除けば、単に goroutine で HTTP サーバーを立てているだけです。

// 空いているポートを自動的に選ぶ
l, err := net.Listen("tcp", ":0")
if err != nil {
    t.Fatal(err)
}

handler := func(w http.ResponseWriter, req *http.Request) {
    w.Write([]byte("I am AP server"))
}
ap := &MockAP{
    host: host,
    port: l.Addr().(*net.TCPAddr).Port,
    server: &http.Server{
        Handler: http.HandlerFunc(handler),
    },
}

// 別の goroutine でサーバーを走らせる
go func() {
    if err := ap.server.Serve(l); err != nil && err != http.ErrServerClosed {
        t.Log(err)
    }
}()

なお、標準ライブラリの httptestStartMockAP と同じようなことができますが、httptest はアドレスを 127.0.0.1 にバインドしてしまうので、今回のユースケースでは利用できません。

今回のサンプルコードでは一種類の MockAP しか実装されていませんが、私達が実際に使っているテストでは様々な MockAP が実装されています。 例えば、レスポンスを一切返さない MockAP や、不正な SSL 証明書を持つ MockAP などがあります。 これらの MockAP を使うことで、手動で起こすのが面倒なケースをテストすることができます。

まとめ

Go と Docker を使って nginx をテストする仕組みを紹介しました。 この仕組みは、我々のプロダクション環境における nginx を支えています。

この記事がみなさまの nginx ライフの一助となれば幸いです。


  1. ここでは nginx をひとつのユニットとみなしています(ユニットテストにおける「ユニット」が何を指すかは定義によって異なっており、統一されていません。この記事では nginx がひとつのユニットとなるような定義を採用したと解釈してください)。

  2. テストコードでは関心の分離や単一責任の原則が蔑ろにされることがよくありますが、私はテストコードでもこれらの原則は重要だと思っています。

 

チームで行うリモートワークに対する7つの工夫

こんにちは、フロントエンドエキスパートチーム@sakitoです。

今回はフロントエンドエキスパートチームがリモートワークで工夫している内容や、解決してきた課題を共有したいと思います。

最近リモートワークをはじめた会社やチームにとって参考になれば幸いです!

※ テレワークやリモートワークという言い回しがありますが、本記事内ではすべてリモートワークで統一しています。

所属しているチームと会社の状況

サイボウズには、リモートワークをいつでも行うことができる文化があります。オフィスに出社するメンバーもいれば、フルリモートのメンバー、拠点の異なるメンバーもいます。

現在、チームメンバーは 7 人いて、その内訳は東京 4 人、大阪 1 人、愛媛 1 人、福岡 1 人となっており、チームメンバーの働く場所は複数にわかれています。

働く場所がわかれてるのもあり、日々の業務のやり取りは自然とオンライン上で行うのが中心となります。こういった状況から、チームとしてリモートで業務を進めていくのに支障がでないように、いくつかの工夫をしています。

ここからはチームで行っている工夫の紹介をします。

工夫その 1. 朝会で雑談タイムを設ける

チームでは毎日30分ほど朝会を行なっています。
内容は前日の振り返りやその日のタスクを確認するだけですが、このときに雑談する時間を意識的に設けています。

この雑談の時間を前半に15分ほど設けることで、日々直接話すことができない状況でもコミュニケーションを取ることができています。
また、さまざまな理由から開始時間に遅れて入ってくるメンバーへのフォローにもなります。

工夫その 2. 週に 1 度チームでリモートランチを行う

週に 1 度チームメンバー全員でリモートランチをする機会を設けています。 話す内容は仕事に関する話題以外にも、メンバーの近況報告やフロントエンドに関する情報などさまざまです。

オンラインでのコミュニケーションは、朝会の雑談とこちらのチームランチが主なものになります。
オフラインと比べてテキストコミュニケーションが多くなるリモートワークでは、顔を見ながら小さなコミュニケーションを重ねて信頼関係を構築することが大事だと思っています。

ランチをしている風景
ランチをしている風景

工夫その 3. 離席、出勤、退勤時はチャットで共有する

サイボウズではさまざまな働き方を選択できます。

開始や終了の時間が異なるメンバーがいることもありますが、リモートではメンバーが現在在席しているのか離席しているのかが把握できないこともあります。対策として、作業開始や作業終了、私用で離席するときなどのタイミングで、離れることをチャット(Slack)で共有しています。

出退勤を報告しているslackの画像
実際のslackの画像、ゆるく運用してます

工夫その 4. ミーティングは全員オンラインで参加する

オフラインで直接集まりつつリモートで参加する人がいるという構図のミーティングでは、オフライン側が中心で話が進みがちになってしまい、リモートの参加者が発言しにくい状況になりやすいです。
そのため、オフラインで参加する人数が何人いても、全参加者がオンラインでつないでミーティングを行うようになりました。こうすることで、全員が同じ状態で発言できるようになり、非対称性を解消しています。

(隣の席のメンバーとオンラインで話す状況もよくあります。)

工夫その 5. 定期的にチームメンバーと直接顔を合わせる

弊チームのメンバーは日本全国に散らばっていますが、全員で直接顔を合わせてコミュニケーションを取るために 3 か月に 1 度、約 3 日の出張を行っています。
このタイミングが丁度 1Q の締めになるので、1 日かけてチームについて議論を重ね、次の Q に向けての準備を行います。

そのほかにも出張先の地域の名物を食べに行き、一緒に観光をしています。直接顔を合わせることで、普段のリモートでの作業も心理的にやりやすくなっていると感じます。

※ 今年に入ってからは昨今の事情もあり、出張は行っていません。

フロントエンドエキスパートチームメンバーが集合している様子
度々顔を合わせてわいわいしてます

工夫その 6. 作業ログを残す

チームメンバーはそれぞれがさまざまなタスクを行なっています。
このタスクを個人で進める際に作業ログを残せる場所を用意しています。

作業ログを残すことで、タスクをどのように進めたか、どのような背景で意思決定を行ったかなどが共有できます。また、困っている、悩んだことを残すことでチームメンバーが非同期的にその情報を拾ってフォローすることもできます。

この作業ログは、ほかのメンバーが関連タスクを行う場合に現状把握をするために見返すのにも使えます。

工夫その 7. モブプログラミングで開発・資料作り

サイボウズでは、基本的にタスクの消化をモブプログラミング(ドライバーとナビゲーターに別れて一緒にタスクに取り組むスタイル)で行うことが多いです。
モブプログラミングの手法や工夫については、kintone チームが書いたこちらの記事(リモート・モブプログラミングという働き方)がありますので、モブブログラミングとは?という方はこちらをご覧ください。

モブプログラミングでは画面をみんなで見ながら議論しつつ 1 つのものを作り上げていくので、意思決定の合意が取りやすくなります。

開発以外にもドキュメントなど、チームとして必要なものを作るときや、個人で作っているものに対して社内の有識者にみてもらいたい場合にモブをするなど、全社的にモブでやっていく文化がサイボウズ内で育ってきていると感じています。

そのほか

マイクから入力される音声が聞こえづらいことや、キーボードの打鍵音などの雑音が入ってしまうと会話がしづらいので、可能な限りマイク付きのヘッドホンがあるといいです。

サイボウズではチーム以外の取り組みでも、リモートワークに対するさまざまな取り組みをしているので、こちらも参考にしてください!

おわりに

今回はリモートワークを進めるうえで工夫していることを紹介しました。

紹介したように、リモートワークに対するチームとしての工夫はコミュニケーションに対するものが多くなっています。
チームでは毎週 KPT を行い、問題が起きてたらなにかしらの改善を見つけて、翌週には実践するようにしています。この過程で様々な工夫が生まれてきました。

今回紹介したフロントエンドエンドエキスパートチーム以外のチームでも、様々な取り組みがサイボウズでは行われているので、他のチームから紹介されるのも楽しみにしています!

会社がリモートワークをできる制度と設備や環境を用意してくれのなら、あとは個人、チームでどれだけリモートを活用できるのか工夫していきたいですね。

 

「第 9 期サイボウズ・ラボユース成果発表会」開催

みなさんこんにちは。サイボウズ・ラボの内田です。

2020 年 3 月 30 日に第 9 期サイボウズ・ラボユース成果発表会を開催しましたので,その模様を紹介します。 今年は新型コロナウイルスの影響で,Zoom でオンライン開催されました。

サイボウズ・ラボユース

サイボウズ・ラボユースとは日本の若手エンジニアを発掘し,育成する場を提供する制度です。

ラボユース生が作りたいものをサイボウズ・ラボの社員がメンターとしてサポートし,開発機材や開発活動に応じた補助金,旅費の援助をします。開発物をオープンソースとして公開するという条件の元で著作権は開発者本人に帰属します。

今期はラボユース生 8 名が成果を発表しました。

発表会のレポート

発表会の集合写真です。(個人的に初めて Zoom で集合写真を撮りました。各人の顔が良く見えて結構いいですね。)

発表会の集合写真

サイボウズ・ラボユースの成果発表会としては初めてのオンライン開催でした。 去年までの発表会のような熱い雰囲気が作れるか心配でしたが, オンライン開催であってもラボユースらしい会になったと感じました。 ただ,恒例となっている発表会後の懇親会が開催できなかったのは残念でした…。

8 人の発表の様子を発表順に紹介します。

西川 哲也さん「手話単語分類」

drive.google.com

西川さんは 2019 年 5 月 17 日からラボユースで活動しています。メンターは中谷です。

手話と日本語は英語と日本語くらい違う言語だそうです。西川さんは手話を日本語に翻訳するシステムを作るための要素技術として,手話単語を分類(認識)するソフトウェアを作りました。1 つの手話単語を収録した動画を入力し,それが何の単語かを分類します。最終的にはスマホで手話をリアルタイムに翻訳するのが目標とのことで,スマホ使用に適する演算量や技術に限定して実装しました。

石巻 優さん「格子暗号を用いたHomomorphic Secret Sharingの実装」

docs.google.com

石巻さんは 2019 年 6 月 3 日からラボユースで活動しています。メンターは光成です。

石巻さんは,準同型暗号と秘密分散の手法を組み合わせた Homomorphic Secret Sharing を C++ で実装しました。

2 台以上のノードに情報を分散させることで各ノードに対して情報を秘匿したまま計算を行います。 加法秘密分散の手法では乗算時にノード間の通信が必要ですが,今回実装した手法は一部の乗算を通信無しで計算できます。

平田 遼さん「高速な楕円曲線の実装」

speakerdeck.com

平田さんは 2019 年 6 月 11 日からラボユースで活動しています。メンターは光成です。

平田さんは,楕円曲線暗号の計算で基本となる点 P のスカラ倍(k×P)を高速に計算するライブラリを作成しました。最も基礎となる足し算と 2 倍算は,sage-8.8 というライブラリと比べて,それぞれ約 14 倍,25 倍の高速化を実現しました。

江畑 拓哉さん「ニコニコ大百科と日本語の情報抽出方法の検討」

speakerdeck.com

江畑さんは 2019 年 6 月 17 日からラボユースで活動しています。メンターは中谷です。

江畑さんの最終目標はニコニコ大百科の知識を使った対話システムや情報検索システムを作ることだそうです。ラボユースでは,ニコニコ大百科の記事を前処理し,機械によって活用しやすい知識の形にすることを目指しました。例えば,記事中の各文において欠落した主語を,ニコニコ大百科のセクション構造を活用して補完する手法などを提案・実装しました。

和久井 拓さん「ミッキーシェイプの認識と検知」

和久井さんは 2019 年 6 月 27 日からラボユースで活動しています。メンターは中谷です。

和久井さんは現実世界で幸運のマークを探したいというモチベーションのもと,まずは画像内の幸運のマークを探すことにチャレンジしました。「幸運のマーク探し」は Kingdom Hearts 3 の世界に散りばめられている,3 つの円から構成される「幸運のマーク」を見つけて写真に収めるコレクション要素だそうです。今回作った幸運のマークを探すシステムを評価したところ,画像内に隠れた幸運のマークをいくつか見つけることができました。

和田 智優さん「クラウドシステムの非決定的性能バグ検査器」

和田さんは 2019 年 9 月 4 日からラボユースで活動しています。メンターは星野です。

和田さんは Java 製の分散並行処理ソフトウェアの「性能バグ」を見つけるツールの実装を行いました。性能バグとは,例えばノード間でのロックが伝播することで,時間制約がある処理の開始が閾値を超えて遅延するバグなどのことです。ハートビートの処理内でロックの取得が遅れると,規定時間以内にハートビートを送信できずノードが故障判定を受けてしまいます。修士論文研究の一環とのことで今後の発展も楽しみですね。

広瀬 智之さん「x86_64での自作OS」

speakerdeck.com

広瀬さんは 2019 年 10 月 1 日からラボユースで活動しています。メンターは光成です。

広瀬さんは,現代の PC で動かせる OS を作るという目標で,x86-64 向けの OS を開発してきました。これから OS を作って学ぼうとする人の参考になるようにドキュメントが整備された OS を作るのが目標だそうです。発表会では,現在完成している機能の中からスレッドを使った並行処理の仕組みを説明しました。

松井 誠泰さん「組み込みRTOS向け内部DSL / Go言語をnil安全にする静的解析ツールの開発」

docs.google.com

松井さんは 2019 年 11 月 22 日からラボユースで活動しています。メンターは川合です。

松井さんは TypeScript を用いて T-Kernel2.0 上で動作するプログラムを生成する言語処理系を開発しました。Docker 上の QEMU で T-Kernel を動かすことで,組み込み向けプログラムであってもテストを自動化できるよう工夫しました。

もう 1 つのテーマである Go 言語に対する静的解析器は,偽陽性が多いなどの問題はありつつも,Nil 安全でないプログラム片を検出できる検査器を開発できたようです。

第 10 期サイボウズ・ラボユース

第 10 期サイボウズ・ラボユースの開催が決定し,2020 年 4 月から募集を開始しています。 前期から募集テーマが 1 つ増えました。是非ご応募ください!

サイボウズ・ラボユース募集要項