人事の小澤です。今回は昨年インターンに参加してくれた中西さんの記事です。

はじめに

こんにちは!技術本部サービスファシリティーグループの中西です。

今回はCyberAgentのインターンシップ事業の一環である「WORKコース」を利用して、2ヶ月間インターン生として業務させて頂きました。
本記事では業務内容の紹介と、インターンの様子についてご紹介します!

インターンでの業務内容

「Golangを用いたロードバランサーコンフィグ収集ツール"mirumiru"の開発」を行いました。
私はプログラミングの経験が浅く、1つのプロダクトを完成まで昇華させた経験がなかったのですが、メンターの皆様の力を借りながらなんとか完成させることが出来ました。

動作例

初めに設定例と動作例を示したいと思います。

最初に、ロードバランサーにSSHを行うための設定を記載します。
TOMLというミニマムな言語仕様に則って config.toml というファイルを作成し、以下のように記載します。

[global]               # global部分では、デフォルトの設定を記載できます。
user   = "user"        # userは、sshのユーザー名を設定できます。
pass   = "password"    # passは、sshのパスワードを設定できます。
enable = ""            # enableは、ロードバランサーにおけるenableパスワードを設定できます。

[LB]                   # 各ロードバランサーごとの設定を行います。

[LB.lb-1]              # lb-1という固有名を付けられます。この名前でロードバランサーを判別しています。
addr   = "0.0.0.0:22"  # addrは、ロードバランサーのIPアドレスを設定できます。
plat   = "hoge"        # platは、"bigip","a10"のどちらかを設定できます。
region = "dc"          # regionは、どこにロードバランサーがあるかを設定できます。主にデータセンターの名前などが設定されます。

[LB.lb-2]
addr   = "0.0.0.0:22"
user   = "user1"       # もしglobalで設定した以外のユーザー名を使用したい場合は、ロードバランサーごとに記載することでその値が適用されます。
pass   = "pass1"
plat   = "hoge"
region = "dc"

上記のようなファイルを用意し、バイナリと同じディレクトリに置くことで実行することができます)。

また、設定ファイルと実行するロードバランサーはオプションでも設定が可能です。

$ ./mirumiru -h                                                                                                                                                                          [~/go/bin]
Usage of ./mirumiru:
  -config string
        config file path (default "config.toml")
  -name string
        check LB name (if unset this value, check all LB)

実行すると以下のように、ロードバランサーの設定をパースした結果がJSON形式で出力されます。

(見やすくするために、jqコマンドを用いて整形しています)

$ ./mirumiru | jq .
{
  "data": [
    {
      "region": "dc00",
      "partition": "partition01",
      "vs_name": "VS_01",
      "destination": "192.0.2.2:443",
      "pool": "P_01_443",
      "lb_act": "192.0.2.1"
    },

    ...........
  ],
  "id": "some_id",
  "name": "mirumiru",
  "updated_at": 1484806452
}

実際の運用では、このJSONを読み込んでWeb上で表示可能なビューワーが合わせて動作しており、Webブラウザから現在の設定が確認可能になっています。

どうしてmirumiruは必要だったのか

CyberAgentでは多くのデータセンターを保有しており、その中で(時には複数のデータセンターを跨いで)多くのネットワークを管理し、多くのサーバを管理しています。
その中ではOpenStackなどが動いているのですが、多くのハードウェアロードバランサーが導入されています。

このような場合、データセンターを直接管理しているメンバーであればどのロードバランサーにどのVIPなどが紐付いているのかを把握していたりするのですが、その他のチームですとどうしても把握しきれない部分があります。
もしVIPなどの情報が必要になった場合、その都度分かっていそうなメンバーに問い合わせをしていたのですが、それなりに負荷が大きくなってきたため、今回可視化するためのソフトウェアを自作しました。

mirumiruの要件

mirumiruの要件として、以下の項目がありました。

  • 誰にでも扱えるような(使用言語に対する熟練度に関係なく扱えるような)ソフトウェアである
    • ライブラリの運用方法などを知らずとも、安全に正常に動作出来る必要がある
  • インストール作業が非常に簡単である
    • 上記と同様
  • 複数のベンダーに対応している
    • 複数ベンダーのロードバランサーを利用しているため、その分対応している必要がある
  • 設定を柔軟に扱える
    • CIで回す場合、cronで回す場合、手動で実行する場合など、様々な状態で利用されることを考える

これらを満たすために私はGolangを採用しました。

Golangとは

[^1]

[^1]:Golangのマスコットキャラクター、Gopherくんです。この画像はRenée Frenchさんによって制作されたものです。

Golangについてはご存じの方も多いかと思いますが、改めてその特徴をご紹介します。

  • 静的型付け言語である
  • 言語仕様がシンプルで割り切っている部分があり、書きやすく学びやすいとされている
  • コンパイル時に最適化を行うので、実行速度が早いとされている
  • クロスコンパイルが可能なので、対応した環境で動作するバイナリを生成可能である

これらの条件は、私の洗い出した要件に対してかなり有効でした。

特に、シングルバイナリにコンパイルする事ができるのは、運用や保守をしていく際には有効に働きます。
管理用のサーバから接続先のロードバランサーに接続しようと思った際に、Rubyであれば bundle install 、 Pythonであれば pip install などしてからライブラリをインストールし、実行する必要があります。
例えば、「誤ってシステムのgemを参照してしまう」であったり、「誤ってvendoringしたライブラリを削除してしまう」など、運用方法について若干考える必要が出てきてしまいます。

対して、Golangはシングルバイナリで出力されるため、そのような周囲のファイル状況には全く関与しません。
実行用のバイナリと設定ファイルさえ用意すれば良いので、非常に簡潔で運用が容易になります。

よって、私はGolangでツール開発を開始しました。

より詳しいGolangの仕様については、公式ページを参照してください。

mirumiruの機能

  • config.toml で指定されたロードバランサーの設定をパースし、JSONを出力する
    • 複数台記載した場合は、複数台対応
    • A10とF5 BIG-IPをサポート
  • config.toml で指定されたロードバランサーのうち、1台だけを指定して設定をパースすることも可能(オプション指定)
  • 設定は config.toml だけでなく、オプション指定で別名のファイルを指定したり、環境変数からユーザ名などの機密情報を設定可能
    • CIなどに組み込む際に、リポジトリにパスワードなどを含める必要はなくなった

開発フロー

開発は社内GitHub Enterpriseを用いて、GitHub Flowベースで開発を行いました。
これは非常にありふれたフローですが、やはり master ブランチに安定したソースコードがある安心感はそれなりにありました。

開発中のものを実運用で使われることはなかったのですが、Pull Requestを出すことでレビューを必ず挟む運用を実現することができました。

開発時のつらかった話

この章では、開発中につらかった事の中で、特に印象的だった3つをご紹介します。

NW機器のSSHはSSHで接続できない問題

NW機器から設定を取得するためには、sshでログインしてから設定を出力するコマンドを入力し、結果を表示してもらう必要があります。
Golangには x/crypto/ssh というパッケージがあり、このパッケージを使うことでSSHを良い感じに接続してくれ、Session.Outputを用いればコマンドを実行した結果が取得できるように見えます。

が、結果として私は Session.Output を利用しませんでした。理由はいくつかありますが、大きなものとして、A10 Networks社のロードバランサーはこの Session.Output ではコマンドを実行した結果がうまく取得できないことがありました。

最終的に、SSHのコネクション作成だけ x/crypto/ssh を使い、それ以降の処理に関してはSSHの挙動を確認しつつ(ssh -vvv コマンドを使いつつ…)、送られてくるbyte列からSSHの挙動を一つずつ処理する機能を実装しました。

結果として、SSHのコネクションを使いまわした上でコマンド入力が可能になったため、 x/crypto/ssh では不可能な、1セッションにおける複数回のコマンド実行が可能となりました。
これは一回のコマンド実行で全ての設定を取得しきれない箇所への対処にも効果的に働きました。

F5社のBIG-IPはLinuxベースなので x/crypto/ssh で正常にSSH接続及びコマンド実行が可能なのですが、複数回コマンドを入力したい需要があったため、最終的にはA10社のロードバランサーと同じような実装としました。

regexp、マジで遅い

開発当初の私「パース?正規表現でエイッすればええやろw」
私「regexpモジュールあるやん〜使っちゃお〜」
私「出来たぞ〜 レビューしてもらおう!」
メンターの方「それ遅いので使うの辞めましょう」
私「!!!???」

Golangの正規表現(regexpパッケージ)は、非常に遅いことが知られています。
実際にどのぐらい遅いのか、こちらの記事のソースコードを引用して、私の手元でも計測してみました。

$ cat main_test.go
package bench_test

import (
        "regexp"
        "testing"
)

func BenchmarkStringMatchWithRegexp(b *testing.B) {
        s := "0xDeadBeef"
        re := regexp.MustCompile(`^0[xX][0-9A-Fa-f]+$`)
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
                re.MatchString(s)
        }
}

func BenchmarkStringMatchWithoutRegexp(b *testing.B) {
        s := "0xDeadBeef"
        isHexString := func(s string) bool {
                if len(s) < 3 || s[0] != '0' || s[1] != 'x' && s[1] != 'X' {
                        return false
                }
                for _, c := range s[2:] {
                        if c < '0' || '9' < c && c < 'A' || 'F' < c && c < 'a' || 'f' < c {
                                return false
                        }
                }
                return true
        }
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
                isHexString(s)
        }
}

$ go version
go version go1.7.4 darwin/amd64
$ go test -run NONE -bench . -benchmem
testing: warning: no tests to run
BenchmarkStringMatchWithRegexp-4         3000000               471 ns/op               0 B/op          0 allocs/op
BenchmarkStringMatchWithoutRegexp-4     30000000                44.8 ns/op             0 B/op          0 allocs/op
PASS
ok      GHE_HOST/nakanishi-kento/tests  3.310s

引用させて頂いた記事の確認されていたGo1.4と同様に、執筆現在最新版であるGo1.7.4においてもほぼ同様の結果が得られました。
やはり regexp パッケージはかなり遅いようです。

私の最初の実装だと、送られてきた[]byte文字列をstring型に変換し、変換した文字列をregexpを用いて正規表現で処理を行っていました。
また、毎回毎回処理ごとに正規表現モデルの生成(regexp.Compile)を行っており、ここが遅い原因となっていました。

メンターの方に指摘された後に、文字列処理に regexp パッケージのものを使うのをやめ、 strings パッケージを利用するようにしたところ、高速化することに成功しました。

Golangのエラー処理が大変

Golangを書いたことのある方なら誰もが一度は違和感を持つのでは無いでしょうか。
Golangでエラー処理をしっかりする場合、ソースコードの随所に以下のような文字列が並ぶようになるのではないかと思います。

returnValue , err = doSomething(args)
if err != nil {
    log.Fatal(err)
}

毎回if文でチェックを行い、もし問題があればそこでプログラムを異常終了します(log.Fatalos.exit(1)を内部で呼んでいます)。
どうもこの部分が冗長に見えてしまい、分報チャンネルにてその旨を書いていると、メンターの方から神の一声をいただきました。

メンターの方「この構文に違和感を持たなくなったら一人前です」
私「なるほど」

なるほど。

インターンの感想

インターン中に大きく感じたのは、私の中で悩みだった、「どのようなコードを書くべきか」という部分が少し腑に落ちたように感じました。
インターン中に読んだリーダブルコードであったり、メンターの方のレビューであったりで、「どう書いたら読みやすいのか」「分かりやすい変数名とはなんなのか」などのことが理解でき、1人で「機能が多いシェルスクリプト」程度にしかプログラミングを書いていなかった自分にとって、目からウロコな事がたくさんありました。

2ヶ月間という纏まった期間に、1つの空間で開発業務を行わせていただけたのも良い経験でした。自分自身を新しい環境に投じるのはそれなりに体力などのコストを消費しますが、その分新しい刺激が得られるのは本当に良い経験でした。

本当に良い2ヶ月間を過ごさせて頂きました。
メンターの十場さんをはじめ、同じチームだった皆さま、インターン生とランチに行ってくださった皆さま、人事の皆さま、オフィス見学をさせて頂いた皆さま、インターン中に関わった皆さまに感謝申し上げます。

最後に、私に福利厚生として提供されたドリンクの写真を貼ってお別れとさせて頂きます。

本当にありがとうございました!

drink

インターンシップ参加者による記事です。