コネヒト開発者ブログ

コネヒト開発者ブログ

How to Write Testable Code in Golang

f:id:itosho525:20180823212423p:plain

はじめに

本記事は コネヒト Advent Calendar 2018 の19日目のエントリーになります。

こんにちは!先日PHPカンファレンス(通称: ペチコン)でLT登壇させていただいた @itosho です。

ペチコンのことも書きたいのですが、ペチコンでもGoの話をしたので、今日はGoのテストを話をしたいと思います。具体的には、今年公開した gdp というCLIツールのテストを書く時に工夫したことを紹介します。

ちなみに、gdpがどういうツールなのかは以前 Go製のCLIツールを公開しましたという記事で説明しているので、もし興味がある方はこちらも読んでいただけると嬉しいです。

開発初期のコード

gdpにはリモートリポジトリに指定したtagが存在確認を行う処理があるのですが、当初はこんな感じのメソッドを用意していました。*1

呼び出される側

func IsExistTagInRemote(tag string) bool {
    out, err := exec.Command("git", "ls-remote", "--tags", "origin", tag).CombinedOutput()
    if err != nil {
        return false
    }

    if string(out) == "" {
        return false
    }

    return true
}

呼び出す側

func main() {
    err := Validate(tag) // tagは事前にコマンドライン引数から取得
}

func Validate(tag string) error {
    if !IsExistTagInRemote(tag) {
        return errors.New("tag is not exist in remote")
    }

    return nil
}

問題点と解決方法

このコードでも、やりたいことは実現出来ます。しかし、テストのことを考えるとこのコードには問題があります。何故なら、 Validate() メソッドが IsExistTagInRemote() メソッドに依存しているからです。IsExistTagInRemote() メソッドは外部(リモートリポジトリ)との連携があるため、Validate() メソッドのテストコードを書く際に IsExistTagInRemote() メソッドの結果を変更するのが難しく、結果としてテストしづらくなります。これがまだローカルのテストであればよいのですが、例えば、CIのテストとなると、場合によってはgitのセットアップから始める必要があり、非常に手間がかかります。

このような問題を解決する手段として、Goでは Interface を利用します。Validate() メソッドを IsExistTagInRemote() メソッドに依存させるのではなく、Interfaceに依存させます。そうすることで、モックが使えるようになるため、テスタビリティを向上させることが出来ます。

Interfaceを利用したコード

概念だけだと分かりづらいと思うので、実際のコードをご覧ください。

呼び出される側

type Git interface {
    IsExistTagInRemote(tag string) bool
}

type Cmd struct {
}

func (c *Cmd) IsExistTagInRemote(tag string) bool {
    out, err := exec.Command("git", "ls-remote", "--tags", "origin", tag).CombinedOutput()
    if err != nil {
        return false
    }

    if string(out) == "" {
        return false
    }

    return true
}

Git というInterfaceにIsExistTagInRemote() というメソッドを持たせるようにしました。GoではメソッドリストがInterface内のメソッドと一致する型はそのInterfaceを満たしていることになるため、 Cmd 型は Git Interfaceを実装していることになります。

呼び出す側

func main() {
    cli := &CLI{
        git: &Cmd{},
    }

    err := cli.Validate(tag) // tagは事前にコマンドライン引数から取得
}

type CLI struct {
    git Git
}

func (cli *CLI) Validate(tag string) error {
    if !cli.git.IsExistTagInRemote(tag) {
        return errors.New("tag is not exist in remote")
    }

    return nil
}

CLI 型にGit Interfaceを持たせて、Validate() メソッドはこのInterfaceを利用します。初期化時に Cmd型を与えているので、実際にはCmd型の IsExistTagInRemote() メソッドが実行されますが、Validate()メソッドはそれを知らない(あくまでIntefaceを利用している)ので、冒頭のコードにあった依存をなくすことが出来ました。

Interfaceを利用した際のテスト

では、これで本当にテスタブルになったのか確認していきましょう。Validate() メソッドのテストは以下のように書けます。

type MockCmd struct {
    Git
}

func (m *MockCmd) IsExistTagInRemote() bool {
    return false
}

func TestValidate_NotExistInRemote(t *testing.T) {
    cli := &CLI{
        git: &MockCmd{},
    }

    err := cli.Validate()

    expected := "tag is not exist in remote"
    if !strings.Contains(err.Error(), expected) {
        t.Errorf("Output=%q, Expected=%q", err.Error(), expected)
    }
}

ポイントは Git interfaceを満たすモックを準備していることです。Validate() メソッドのテストでは、このモックを利用することで、Gitのセットアップなしにテストを行うことが出来るようになります。冒頭のコードでは依存があったため、このようなモックを準備することが困難でした。しかし、Interfaceを利用することで、モックが使えるようになり、結果としてテストコードを書くことが出来るようになりました!

まとめ

ここまでInterfaceを利用したテストについて説明してきました。Interfaceを利用することで、依存性を分離することができ、コードをシンプルにすることが出来ました。コードをシンプルにすることのメリットはテストのしやすさだけではありません。例えば、Gitの操作をコマンドベースではなく、APIベースに変更した際、呼び出す側(今回の場合だと CLI 型)は具体的な実装に依存していないので、コードの変更を最小限にすることが出来ます。「Interfaceを制するものがGoを制す」とも言われているように、Interfaceは非常に便利な機能です。

とは言え、常にInterfaceを利用するべきかと言うとそうではないと思います。Interfaceを利用したコードはどうしてもコード量が多くなってしまいます。ですので、例えば、一回きりしか使わない使い捨てコードの場合は冒頭のように愚直に書いたがほうがよいと思います。gdpは社内で頻繁に利用されているツールであり、OSSとして公開しているので、可能な限り*2よいコードを目指しました。

ちなみに、このあたりの話は以前勉強会でも発表させていただいたことがあるので、興味がある方はこちらもご覧いただけるととっても嬉しいです。

speakerdeck.com

参考サイト

Goのこの手の話は五億番煎じくらいなので、たくさんお世話になりました。

明日の予告

明日のアドベントカレンダーは @ry0_adachi さんが登場するよ!

*1:なお、コードは必要な箇所のみ掲載しています。

*2:まだまだ改善の余地はあるので、Pull Requestをお待ちしております。

読書会でチームの改善が捗った話

f:id:yanamura:20181213002738p:plain:w320

本記事はコネヒト Advent Calendar 2018の14日目のエントリーです!

こんにちは!エンジニアの柳村です。

わたしの所属するチームではチームメンバー全員がスクラムマスターをできるようになろう!という目標でいろいろなことをやっています。その一環としてアジャイルコーチングという本の読書会を行っています。もともとは、単にスクラムやアジャイルのコーチングの知識をつけることが目的だったのですが、チームの開発プロセスの改善がどんどん進むという効果もあったのでそれについてご紹介します。

読書会の進め方

  • 事前準備
    • 各自が1章ずつ読み、その中で「学び」、「疑問」、「試したい」「うちのチームだとどうか」などといった視点で気になったことを付箋に書いてきます。
  • 読書会
    • 30分間
      • (2回目以降)前回のtryの確認
      • 全員の付箋をシェア
      • 疑問点について議論
      • tryすることを決める

付箋はこのように番号(章のどこの節か)書いておくと整理しやすかったです! f:id:yanamura:20181213002038p:plain:w300

ポイント

ポイントは、「tryすることを決める」ことと「前回のtryの確認」です。

本を読んで学びを得たり、疑問が解消するだけでも効果はあるとは思いますが、それを実行に移さないとせっかくの勉強もあまり意味がありません。読書会では最後に実行可能なtryをいくつか出すようにしています。「実行可能な」というところもポイントで、実行できるものでないと結局やらずに終わってしまい意味がありません。大きなtryが出た場合は小さくスライスするなどして対応しています。

また、tryを出してもやらずに忘れられてしまうということも起こります。それを防ぐために次の読書会の最初に前回のtryをやったかチェックする機会をつくっています。

結果

やってみた結果、例えばこのようなtryがあげられて実際にチームで実行しました。

try
5章:デイリースタンドアップ パーキングロットをつくろう
7章:前もって計画する バックアップボードの運用
8章:見える化する ブロッキングしてるタスクを見える化
13章:ふりかえり カラータイムライン、感情セイスモグラフを使う

やってみてうまくいったものもいかなかったものもありましたが、どちらにせよチームとしては学習することができましたし、これまでと比べて格段にチームボードやデイリースタンドアップ、振り返りなどの改善が進んだなと思っています。この読書会自体も最初からこのような形ではじまったわけではなく、改善を重ねてこうなりました。

もっと効率的だったり効果的なやり方もあると思いますので、おすすめの読書会の進め方があればぜひ教えてください!

Connehito Marché #4 ~サービスデザイン市~ を開催しました!

こんにちは。
サーバーサイドエンジニア兼チョコレート大臣*1の結城(@super_manner)です〜。 この記事はコネヒト Advent Calendar 2018の12日目の記事です。

今日は、先日行われたコネヒトマルシェという勉強会のレポートをしたいと思います!

f:id:supermanner:20181211181248j:plain

今回のマルシェのテーマ

今回は少しプログラミングから離れ「サービスデザイン」をテーマに開催いたしました。
昨今注目されている分野ということもあり、非常にたくさんの方にLTをしていただくことができ、 5分枠と10分枠でなんと合わせて10名という過去最高の盛り上がりをみせました🎉

LT内容

LT内容について簡単にご紹介させていただきますね。

デザインをしていく上で行動心理を学ぶと楽になる話

1番目の発表は, yusuke_hata_79さん。
ジャムの法則を例に、ユーザーが選択する際のストレスを減らしてあげることが大事ということを説明してくださいました。 選択をするUIにヒエラルキーをつけることで、申込みのコンバージョンがアップされたそうです!すごい!

※資料未公開

プロトタイピングtips!

note.mu

2番目の発表はRisaHiyamaさん。
Graffityの開発を行った際に得たプロトタイピングのtipsを共有してくださいました。女子大生を巻き込んだり、母校の文化祭に出店するなど行動力の化身ですね!確証をもってアプリの開発に臨むために大切なことを教えていただきました。

チームの成長の流れを掴む話

www.slideshare.net

3番目の発表はAyumuNishibeさん。
チームで成果を出すのはサービスデザインにおいて大切なこと。 定量データ・定性データ双方を読み解き、みんなで一丸となってデータと向き合うことの重要性を改めて考えさせられる発表でした!

リサーチあるあるから見る、UXリサーチの使いどころ

www.slideshare.net

4番目の発表はvegemakiさん。
バリバリの関西弁を操りながら、様々なUXリサーチ手法を可愛らしいイラストと共に解説してくださいました。UXリサーチをこれから始める方は、この資料を読むだけでも基本的なところが理解できます!

tock pop(トックポップ)ロゴ&サービス紹介動画のデザイン

www.slideshare.net

5番目の発表は稲毛誠さん。
ロゴ制作時の試行錯誤のプロセスをかみくだいで解説してくださいました。たくさんのアイデアに非デザイナーの私は圧倒されました...。また、動画制作についても制作の際の工夫を伝授してくれました!

ブランディングのためのUXライティング

speakerdeck.com

6番目の発表はRina_Satoさん。
「ことば」のデザインについてのお話。UXライティングというものをお恥ずかしながら存じ上げなかったのですが、日本語の与える印象一つでこんなにも変わってくるのか〜と驚きました。

チームの議論の土台をつくるためにデザイナーのわたしができること

7番目の発表はNatsukiWatanabeさん。
サービスデザインをするためには、チームで価値を作るということが重要。 デザイナーとしてのサービスデザインの関わり方を、ママリの開発事例をもとにお話しくださいました。日々のチーム開発でも活躍してくださっています!

国内最大のハンドメイドマーケットを支える同僚を支える技術。

8番目は鹿さん。
実は、画面の向こうのユーザーだけがユーザーじゃない。一緒に働いている同僚もユーザーなんだよ、ということを何度も繰り返されていた点が非常に印象に残りました! 鹿さんのプロダクト愛がたくさんつたわってくる発表でした!

※資料未公開

遊びこころの大切さ。あると楽しい一手間。

9番目の発表はkiyoeshiさん。
新規サービスのロゴデザインをする際に、「ひとさじの遊び」を加えることでたくさんの楽しいアイディアが生まれるという話でした。 こちらは、近日きよえ氏がnoteに公開するとのことなので、詳しくはそちらをご覧ください!

※資料未公開

幼稚園児はできてる超高速PDCA

www.slideshare.net

10番目の発表はmiteoさん。
マシュマロチャレンジの結果をもとに、以下にPDCAを早く回していくのが重要かということをお話しくださいました。机上の空論で固めがち….耳が大変痛く、気をつけようと思いました。

懇親会

サービスをこよなく愛するデザイナーさんたちが集まって和気あいあいと会話を楽しんでいただきました!
皆さんサービスとデザインが本当に大好きな方たちばかりで、時間ギリギリまでわいわいと盛り上がりを見せておりました😋

参加くださった皆様で写真をパチリ
参加くださった皆様で写真をパチリ

運営もがんばります

さて、コネヒトマルシェも早いもので4回目を無事に開催することができました。
運営一同、毎回振り返りをしてより良い運営を目指していますので、ぜひ興味のあるトピックの市にいらしてくださいね😊
ツイッターもよかったらフォローお願いします! twitter.com

明日は@ry0_adachiさんのエモい記事です!お楽しみに📖

*1:社員のお母様が買ってきてくださったアドベントチョコを渡す係

Kotlin1.3でリリースされたCoroutineを試してみた

2017年11月にAndroidエンジニアとしてjoinした関根です。 最近はiOSアプリの開発も担当しております。 こちらはコネヒト Advent Calendar 2018 - Qiitaの10日目の記事です。

さて、先日Kotlin1.3の正式版がリリースされました。

下記が主要なリリースの一覧となります

  • CoroutineのStable版
  • Kotlin/Native ベータ
  • マルチプラットフォームの拡充
  • Ktor 1.0 ベータ
    • ※現在はStable版となっています

かねてからマルチプラットフォーム化を進めること目標としてきたKotlinですが、今回のリリースで、その未来へ大きく前進したように思えます。 Kotlinからますます目が離せないですね!

さて、それではマルチプラットフォームのご紹介を・・・・といきたいところですが、今回はStableとなったCoroutineの紹介をさせていただこうと思います。 下記が今回のアジェンダです。

  • Coroutineとは何者か?
  • ChannelでのCoroutine間通信
  • Coroutineを利用したPub/Subの仕組み

Coroutineとは何者か?

今回、StableとなったCoroutineはKotlinでのnon-blockingプログラミングを支える機能です。kotlinxパッケージの配下に存在します。 利用目的としては ファイルI/OやネットワークI/Oなどの非同期処理が一番に浮かびます。

// Coroutineの生成
GlobalScope.launch(Dispatchers.Main) {
    progressDialog.isVisible = true
    // asyncでnon-blocking関数化,awaitで待ち受けの開始
    // ※ここで処理は停止されるが、ブロッキングは発生しない
    val bigData = async { loadBigData() }.await()
    progressDialog.isVisible = false
    displayData(bigData)
}

上記のようにblockingが発生しうる処理を直列的な記述を維持しながら、non-blockingな処理を実現できるのがCoroutineの強みだと思っています。また下記のようにすると並行処理もお手軽に実現できます。

// Coroutineの生成
GlobalScope.launch(Dispatchers.Main) {
    progressDialog.isVisible = true

    // asyncでnon-blocking関数化,awaitで待ち受けの開始
    // ※ここで処理は停止されるが、ブロッキングは発生しない
    val bigDataInt = async { loadBigDataInt() }
    val bigDataString = async { loadBigDataString() }
    val bigDataPair = Pair(loadBigDataInt.await(), loadBigDataString.await()) // ここで並行処理が実施される

    progressDialog.isVisible = false
    displayData(bigDataPair)
}

Coroutineが発表されて以降、RxJavaと比較されることが多いですが、筆者としては、どちらを利用することにメリットが大きいかを適材適所で判断し、利用していくのが良いと捉えており、 今回のような単純な非同期処理だけであれば、Coroutineでも十分に有用であると考えております。

ChannelでのCoroutine間通信

アプリ開発においては非同期処理は様々な理由で発生します。 例えば、先ほど例に上げたようなAPI通信に関わるネットワーク I/Oもその一例です。 他にはバックグラウンド処理との同期をしたい場合や、OSからブロードキャストされてきたメッセージングのハンドリングなど。 そのようなユースケースの場合、異なるライフサイクルで存在するコンポーネント間での、メッセージ授受の仕組みが肝要になってきます。この仕組みはCoroutineのChannelという機能を利用することで実現が可能です。

// 送受信用のChannelを生成しておく
val channel = Channel<Int>()

GlobalScope.launch { // 送信側のCoroutine
    for (x in 1..5) channel.send(x * x)
}

GlobalScope.launch { // 受信側のCoroutine
    repeat(5) { println(channel.receive()) } // 5つデータを取得して表示する
}

このようにChannelはCoroutine間で値を安全に授受する仕組みです。 本例以外にもChannelを利用すると、パイプライン処理やFan-in/Fan-outパターンの実装を、楽に記述することができるようになります。こちらはKotlinのサンプルでも紹介されていますので、詳しくはそちらをお読みください。 https://kotlinlang.org/docs/reference/coroutines/channels.html#channels-experimental

なお現時点で、Channelはexperimentalとなっており、今後の開発状況によっては破壊的変更が加わる可能性が残っています。 十分ご検討の上ご利用ください!

Coroutineを利用したPub/Subの仕組み

さて、今回はChannelを利用してPub/Subの仕組みを試してみましたので紹介しようと思います。 ソースコードの全体像は下記です。

class PubSub<T> {
  // 1. Pub/Sub用のChannelを生成
    val bus = BroadcastChannel<T>(1)

    // 2. Channelへの送信
    fun send(item: T) = bus.sendBlocking(item)  

    // 3. CoroutineScopeを受け取りsubscribeを開始する
    fun subscribe(scope: CoroutineScope, onConsume: (T) -> Unit) {  
        scope.async(Dispatchers.IO) { // 4. IOスレッドを利用する指定
            Timber.d("async:"+Thread.currentThread().name)
            bus.openSubscription().consumeEach { // 5. 受信の開始
                onConsume(it)
            }
        }
    }
}
・・・

// 受信
val pubSub = PubSub<Int>()
pubSub.subscribe(this) {
    launch(Dispatchers.Main) { // 6. UIスレッドで実行
      // 何かUIに関わる処理
    }
}

// 送信
pubSub.send(1)

全体的な流れを見てみると以下のようになります。

1. Pub/Sub用のChannelを生成

BroadcastChannelは1対多でのデータ授受を目的としたChannelです。 EventBusのように一つのEventを複数の画面やクラスから購読する際に利用します。

2. Channelへの送信

今回の例ではsendBlockingで値を送信していますが、 呼び出し元がCoroutineの場合はsendメソッドを利用しましょう。

SendChannel.send - kotlinx-coroutines-core

3. CoroutineScopeを受け取りsubscribeを開始する

CoroutineScopeはCoroutine自身とその子Coroutineのスコープを決めるものです。 スコープを決めることにより、呼び出し元のLyfecycleに応じて中断処理を行うことが可能となります。 今回のサンプルではActivity単位のスコープとすることを想定しています。

4. IOスレッドを利用する指定

受信待ち受け処理は、I/Oスレッドで走らせています。

5. 受信の開始

openSubscriptionを呼び出すことにより、受信用のChannelが新たに生成されます。 これにより1対多のメッセージ授受を実現できます。 1対1で事足りる場合には通常のChannelを利用すると良いと思われます。

6. UIスレッドで実行

メッセージ受信後の処理をUIスレッドで行うように指定しています。 ここをDispatchers.IOのように指定するとワーカースレッドで処理が行われます。

実装は以上となります。 kotlinとkotlinxのパッケージのみで、Pub/Subが実現できると夢が広がりますね。

終わりに

今回はCoroutineとChannelについての紹介とそれらを利用したPub/Sub実装の紹介をさせて頂きました。 弊社ではRxJavaを利用した実装が多く存在するのですが、Coroutineで代用できる部分がないか、探って行きたいと思います! CoroutineとChannelの良き実装アプローチがあれば、教えてください!!

それでは!!

Travis上でDockerを利用した継続的インテグレーションを実現する with レイヤーキャッシュ

こんにちは!2018年も残すところあと3週間、コネヒト Advent Calendar 2018 - QiitaのDay-9でございます。

ブラック・ラグーンの新刊発売の衝撃がまだ鳴り止まない!の金城(@o0h_)です。 今回はもう5回は読みました、皆さんはどうですか?

ブラック・ラグーン(11) (サンデーGXコミックス)

コネヒトでは継続的インテグレーションにTravisCIを利用しています。
ローカルの開発環境、プロダクトの本番環境にはDocker(ECS)を利用しています。
本番イメージのビルドやpushもTravisCI上で行っています。

かねてより、「折角DockerベースなのにCIは"別環境"なの悲しい」「良い感じにビルドしたイメージをキャッシュして、デプロイを早くできないか」といった声が上がっていました。
そして、サーバーサイドエンジニアが集い「今四半期中に、CIの活用についての見直しを行おう」という誓いを立てたのです。

よりDockerフレンドリーなCIが他にある中で、Travis CI上でのDocker活用には少し工夫がいるかもな・・という風にも感じております*1

具体的なtipsが世の中に出回るといいな、と思い今回は現時点で我々が得ている成果・知見についてご報告いたします。

TL;DR

  • Docker自体はTravis CIでもサポートされているので難なく使える
  • Dockerのビルドをキャッシュするためには、明示的なキャッシュの作成とアップロードが必要
  • マルチステージビルドの結果をキャッシュするには、target buildの結果を保存し --cache-from オプションを利用したビルドを行う

https://cdn.mamari.jp/authorized/5c0cc234-0d90-4bff-b968-001bac120002.jpg

今回の対象となるDockerfile(前提の共有)

JSとPHPが相乗りするイメージを利用しています。
そのため、ローカル環境でのビルド効率化のために、multi stage buildとBuildKitを利用しています。

f:id:o0h:20181209183955p:plain

これを、「Travis CI上でもばっちり使えるように、頑張ってみようぜ!」というのがこの記事の趣旨です。

【ここで参考になりそうな記事】

やりたいこと・目的

Travis CIは、以下の用途で利用しています

  • push/prごとのテストやスタイルチェックと言ったビルド
  • Amazon ECRへのデプロイ
    • docker build コマンドの実行(都度ゼロベースでのビルド)
    • レジストリへのpush、サービスイン

これを、以下のように変更するのが目的です

  • push/prごとのテストやスタイルチェックと言ったビルド
    • PHP系のテスト等は、Dockerコンテナ上で実行する
      • そのために、ビルドに係る処理コストを最小化する
  • Amazon ECRへのデプロイ
    • レイヤーキャッシュを利用して docker build を実行する
    • レジストリへのpush、サービスイン

Dockerのイメージビルド時のキャッシュ利用

Travis CIでのビルド時に、Dockerの「前に作ったイメージ」をキャッシュしておくことは可能でしょうか?
基本的には、こちらの記事で触れている通りの方法でDockerコンテナを利用することができます。

【ここで参考になりそうな記事】

multi stage build時のキャッシュについて

ここで注意として、「マルチステージビルドを使っていると、--cache-fromに最終ステージを指定してもレイヤーキャッシュがされない」という問題があります。

具体的には、

  1. 中間ステージのビルドにキャッシュが利用されない
  2. そのため、最終ステージの「手前」の部分に変更が生じる
  3. 手前が変更されているため、以後のレイヤーでもキャッシュを利用しない

かのように見える現象があります。
例えば、以下のようなDockerfileがあったとします

FROM aaa:latest as A
# hogehoge

FROM bbb:latest as B
# fugafuga

FROM xxx
# piyopiyo

これをビルドします

docker build -t xxx .

さて、ここで使った労力をレジストリにpushしておいて、別ホストでのビルド時にもcacheを使いたい!ですね。

docker pull mine/xxx:latest
docker build -t xxx --cache-from mine/xxx:latest

結果は、残念ながら「A」のステージでキャッシュが効きませんでした。。。
という具合です。

これを回避するため、中間ビルドのイメージも個別に生成することにしました

【ここで参考にした記事】

docker save/loadを利用して、multi stage buildでもレイヤーキャッシュの恩恵を受ける

方向を転換し、「中間ステージごとにイメージを保持・読み込みを行い」「ステージごとに、必要なイメージを明示的に --cache-from 指定をして読み込ませる」というものにします。

なお、この段階で、「最終ステージ以外のものをレジストリに上げるのはどうなのかな・・・」という気持ちになったので、ローカルにイメージを置くことにしています。

docker build -t A --target A --cache-from A .
docker build -t B --target B --cache-from A --cache-from B .
docker build -t xxx --cache-from A --cache-from B --cache-from xxx .

--target を指定しつつタグ付けを行うことで、imageのsaveができるようになります。

docker save A -o A.tar
docker save B -o B.tar
docker save xxx -o xxx.tar

saveしたイメージをファイルとして共有することで、他ホストでも読み込み可能になります。

docker load A.tar
docker load B.tar
docker load xxx.tar

ということで、大まかな方針として「中間ステージ・最終ステージをそれぞれビルドして、セーブして、次のビルドの前にロードする」というやり方に決めました。

【ここで参考にした記事】

ビルド結果のキャッシュを作る

Travis CIは、 .travis.yml 内で「このディレクトリをキャッシュする」というパスを任意に指定することができます。
このディレクトリ下に書き出されたファイルは、次のビルドの際に冒頭で読み込まれ利用可能になるという仕組みです。

それを踏まえて、docker save の出力先を「キャッシュ対象ディレクトリ」にしてしまえば良さそう。

$HOME/docker というディレクトリを設け、そこに放り込むことにします。

# .travis.yml
cache:
  directories:
    - $HOME/docker

Travsi CIは、ビルドのメインとなる script ステージの後〜 deploy ステージの間に、キャッシュを作成・クラウド(S3)アップロードを行います。

  1. OPTIONAL Install apt addons
  2. OPTIONAL Install cache components
  3. before_install
  4. install
  5. before_script
  6. script
  7. OPTIONAL before_cache (for cleaning up cache)
  8. after_success or after_failure
  9. OPTIONAL before_deploy
  10. OPTIONAL deploy
  11. OPTIONAL after_deploy
  12. after_script

from: Job Lifecycle - Travis CI

なので、 before_cache の段階で docker save を実行してしまえばよいです。*2

# .travis.yml
before_cache:
  >
    mkdir -p $HOME/docker && rm $HOME/docker/* && docker images -a --filter='dangling=false' --format '{{.Repository}}:{{.Tag}} {{.ID}}' | xargs -n 2 -t sh -c 'docker save $0 -o $HOME/docker/$1.tar'
  1. cache保持用のディレクトリを(存在しなければ)作り
  2. 利用されていないファイルが生き残り続けないよう、ディレクトリ配下のファイルを一旦全て削除し
  3. docker imagsで取得した「存在するイメージ(タグ)」を、ファイル名にIDを使って保存する
    1. タグを利用すると/が入ってきてややこしくなるため、安全かつ一意な文字列としてIDを利用する

これで、「必要そうなイメージをキャッシュに回す」ことができるようになります。

【ここで参考にした記事】

image保存のチューニング

しかしながら、これは結構なオーバーヘッドになります。
1つは、docker save自体が結構な時間がかかること。saveするイメージは少ないほうが良いです。
2つ目に、キャッシュを使える場面で活かしきれていないこと。multi stage buildを利用するにあたり「キャッシュを最大限生かせるように」考えていますから、「めったに変更がない中間ステージイメージ」が存在するという状態も作られています。これを「毎回、絶対に作り直す」というのは効率が悪く思います。

そのため、「保存されている内容と今使っている内容がに差異がなければ、そのimageのsaveは省略する」ことで省力化できると効率が良いです。

また、Travis CIのキャッシュの保存・取得は、外部ストレージへのネットワーク経由のアップロード・ダウンロードによって行われます。それを考えると、ファイルサイズが小さい方が有利です。
このタイミングで、出力イメージのgunzip圧縮も行うことにしました。

# .travis.yml
before_cache:
  - cache_threshold=$(date “+%Y%m%d %H:%M”)
  - docker images -a --filter=‘dangling=false’ --format ‘{{.Repository}}:{{.Tag}} {{.ID}}’ | xargs -n 2 -t sh -c ‘if [ -e $HOME/docker/$1.tar.gz ]; then touch $HOME/docker/$1.tar.gz; else docker save $0 | gzip -2 >$HOME/docker/$1.tar.gz && echo “$0($1) saved”; fi’
  - find $HOME/docker/. \! -newermt “$cache_threshold” | xargs rm -rf
  1. before_cacheに入った時点での日時をメモしておき
  2. cacheディレクトリ以下のimageそれぞれについて、
    1. いま利用されているものとIDが同一であれば、touch してタイムスタンプだけ更新
    2. IDが違ったら = 内容に変更が生じていたら、docker saveを行って上書き
  3. 1のステップでメモした日時とディレクトリ下のファイルのタイムスタンプを比較、「before_cacheに入る段階より古い」ものを削除
    1. これで「もう利用してないイメージ」が破棄される

ようにしました。

Travis CI上でキャッシュ済みイメージを利用したビルドを行う

ここまでで、 $HOME/docker ディレクトリに「使えそうなイメージ」が配置されています。
あとは、これを実際に利用したビルドを行うようにしましょう。

# .travis.yml
before_install:
  - if [[ -d $HOME/docker ]]; then ls $HOME/docker/*.tar.gz | xargs -I {file} sh -c "zcat {file} | docker load"; fi

instal:
  - docker build -t A --target A --cache-from A .
  - docker build -t B --target B --cache-from A --cache-from B .
  - docker build -t xxx --cache-from A --cache-from B --cache-from xxx .

こうすることで、以前にビルドしたイメージからうまくキャッシュを利用してくれるようになりました。

script/deployでDockerイメージを利用する

あとは、dockerコマンド経由でのテスト実行や静的解析など、自由に「いつもの環境」での処理実行を行えるようになります。
script ステージでのコンテナの利用は、先に紹介したTravisのドキュメントをご参照ください。

また、デプロイ用に改めてイメージをビルドする必要がある!という場合も、同様に(必要に応じた)--cache-fromオプションの指定などで対応できると思います。

感想

本記事の内容は、まだまだ改善の余地があるような気もしています・・・それでも、チーム内で求めていた「大体こんな感じ」という水準までは、一旦持ってくることができました。そうして、公開に至っております。

また、Travisの利用するキャッシュサイズが大きくなることや、docker loadに関するオーバーヘッドが大きくなり、プロジェクトによっては今までよりビルドごとに要する時間が大きくなるんだろうな、という印象も受けています。
それでも、チーム内では「テストの確実性が上がる、本場と同等のイメージで動かせる恩恵がある」という事で合意を得ています。
ここでもまた、「やはりDocker使うならイメージが小さいのが正義・・・!」という気持ちを新たにしました。
ということで、次にやりたいのは「イメージをめっちゃダイエットする」です🐶

「ここ、もっとこうした方が良いのでは?」「ここの理解が間違っている!」といった箇所がありましたら、お気軽にフィードバックをいただけると嬉しいです!

話が固くなりましたので、最後にいらすとやさんのホトトギスを見てお別れしたいと思います。

明日は @katsutomu さんが元気な記事を書いてくれます!

f:id:o0h:20181210005627p:plain

*1:CircleCI等の他サービスを利用すると、もっと容易にDcokerベースの継続的インテグレーションができるかもね!という声は社内でも上がりました。検討の結果、今のスコープでは「CIの乗り換えはしない」という結論に至っております

*2:最初、after_deployでキャッシュを作成していたら上手く動かず、ハマりました・・・

ランチタイムを使ってブレストしているお話

f:id:kichikuchi:20181207122553p:plain

こんにちは。コネヒトでiOSエンジニアをしている @kichikuchi です。 この記事は コネヒト Advent Calendar 2018 8日目のエントリーです。

弊社では隔週金曜日に社内のフリースペースでみんなでご飯を食べながらブレストする通称アモーレ会という取り組みをしています。
約1年程前にスタートして、これまでに20回開催しました。

本エントリーではこの取り組みについて

  1. 何をやってるのか
  2. やってみてどうだったか

の順に紹介させていただきます。

何をやってるのか

アモーレ会とは弊社が運営するサービスのママリをよりよくするためのアイデアを出すことを目的にブレストをする会です。

隔週金曜日の午前中に Slack で声をかけ、4人以上集まりそうだったらお昼に社内のフリースペースで開催しています。

f:id:kichikuchi:20181207122641p:plain
アモラーが集まっている様子

業務ではなくお昼の時間を使っているため参加は自由で、これまでに開発部・企画部・人事部・コンテンツ部と様々な部の人が参加してくれています。

あまり大人数でブレストをすると収拾がつかなくなってしまうので3 ~ 4人になるようにチームを分け、テーマに沿ってチーム毎に30分程度ブレストした後それぞれどんなアイデアが出たかを発表するというのが会の大まかな流れです。

以下がこれまでにアモーレ会で扱ったテーマの一部抜粋です。

- ユーザの抱えている課題は何か
- なんでママリで質問するのか
- なんでママリで質問に答えてくれるのか
- 質問しやすくする
- 7日目のRRを向上させる
- 一年後にもママリを使い続けてもらう
- 適切な人に適切な質問を届ける

初期は課題を発見するためのブレストと深掘るためのブレストを回をまたいで行っていたんですが、徐々に単発でアイデアを出しやすいテーマにシフトしました。

どういうテーマを設定するかはアモーレ会を続けて行く中での悩みの種だったので、次節で改めて触れたいと思います。

やってみてどうだったか

約1年間続けてみて良かったこと、うまくいかなかったことがそれぞれあるので順々に書いていきたいと思います。

職種関係なくプロダクトと向き合う時間が持てるようになった

プロダクトに向き合い、より良くするためのアイデア出しは本来誰がやってもいいことのはずです。いいはずなんですが、そうはいってもそれぞれが主業務を抱えているのでその役割は自然とディレクターポジションの人に偏ってしまいます。

誰でも参加できるブレストを行う場としてアモーレ会を定期開催するようになったことで、プロダクトに向き合い、改善するためのアイデアを考える時間を職種関係なく定期的に持てるようになりました。

新鮮な視点が得られるようになった

アモーレ会には主業務としてプロダクト開発を行ってはいない、様々な職種の方が参加してくれています。 開発メンバー以外では、例えばこれまでに人事部・企画部・コンテンツ部の方が参加してくれました。

弊社の開発チームでは普段の業務中にブレストを行う機会がもちろんあります。しかし、アモーレ会には上述の通り異なるバックグラウンドを持つ人が多数参加してくれているため、開発チームだけで行うブレストでは出ないような新鮮な意見が出やすい場になりました。

テーマ設定が難しい

アモーレ会はお昼の時間を使ったカジュアルなブレスト会で、誰でも自由に参加可能なので回毎にメンバーが変わります。

そのため複数回にわたって一つのテーマを深ぼっていったり、前回の内容を踏まえた上でのテーマに取り組むということが少しやりづらくなってしまいました。

上記の理由から一回約40分で収まりそうなテーマを毎回設定する必要があるのですが、会が続くごとに取り組みやすいテーマを探すことが徐々に負担になってしまいました。

アイデアの実現が難しい

当たり前のことですが、アイデアを出すだけではプロダクトは一向に良くなりません。プロダクトをよくするためには出たアイデアを実行する必要があります。

しかし、現状のアモーレ会ではこのアイデアを出した後のフロー設計ができていません。

アイデアを実現するためにはそのアイデアの定性・定量面の裏付けを取ったり、細かい仕様を詰めたり、さらには工数に対して期待できる効果はどの程度なのかの見積もりを行う必要があります。

そこまでやった上で、開発チームがもっているKPIに則った施策と比較して優先度を高く実行すべきと判断できた時に晴れて開発フローにのせられるようになるので、アイデアを出してから実行するまでには意外と距離があります。

出したアイデアを具体化させるフローの設計が難しく、アモーレ会は現状アイデアを発散させる場所止まりになってしまっています。

おわりに

本エントリーでは誰もが気軽に参加できるプロダクトに関するブレストの場を作っている弊社の取り組みを紹介させていただきました。 ただ最後に述べたように、アイデアを具体化させるフローが整備できていないという大きな課題が残っています。

同じような取り組みをしていて、かつ実行までのフローをこんな風に設計しているよ!という知見を持っている方がいたら是非コメントいただけると嬉しいです!

明日のアドベントカレンダーは二度目の登場 @o0h_ さんです! お楽しみに〜。

JapanContainerDays v18.12にスピーカーとして参加してきました

こんにちは。インフラエンジニアの永井(shnagai)です。

f:id:nagais:20181207104504j:plain

今回は、12/4,5で行われたJapanContainerDays v18.12にスピーカーとして参加させていただいたので、 登壇内容の振り返りと2日間通じて参加した感想を簡単に書こうと思います。

この記事はコネヒト Advent Calendar 2018の7日目の記事です。

qiita.com

発表内容

コンテナを本番導入することでチーム開発がどう変わっていくかというテーマの元お話してきました。

このイベント自体は、今最も勢いのあるKubernetes関連のセッションが8割くらいを占めていた印象ですが、 オーケストレーションツールやその他エコシステム導入の一歩前でこれから本番でコンテナを使っていこうという段階の方々をターゲットに、 本番コンテナ化を後押しできればいいなというような気持ちで資料を作りました。

実際に会場でアンケートを取ったところ、本番導入していた方は2割くらいだったと思うので、一定の役割は果たせたかなと思います。 キーノート後の午後一発目の良い枠で他のセッションも面白そうなものが揃っていたので、会場に人がそこそこ入ってくれていて安心したのを覚えています。セッションを聞いてくださった方々ありがとうございました。

資料は下記で公開していますので、よろしければご覧ください。

その他の感想

今回2日間色々なセッションに参加させていただいたりイベントで出会った方との会話の中で、色々な刺激を受けたので雑に自分が感じた感想をまとめておきます。

  • Kubernetesがこんなに愛されているのは、周辺のエコシステムを含めた作り込みが出来る部分が大きいのかなと感じた。
  • ただ、やはりエコシステムを含めて全部理解して使いこなすのは難しいからKnative等の複雑さを抽象化してくれるツールが出てきている
  • Kubernetesはやはりコミュニティとして勢いがありみんな知りたい。でも実際に活用しているのは、まだ一部の優秀なエンジニアが揃った企業が多く、その使い方も1チームでやるというよりは基盤を作る側とサービス開発が別れていて、その中で自律的な開発を促してスピードを緩めないためのツールとしてk8sをうまく使っている様子。 キーノートのLINEさんの2000人の開発者が自由に開発するようなシステムを開発していて、そのバックエンドにk8sを使っているという話の規模に度肝を抜かれた。
  • コネヒトはコンテナオーケストレーションツールとしてAmazon ECSを使っていて、Kubernetesに切り替えたら圧倒的なメリット得られるかなみたいなことが想像出来てなくて、何人かコミュニティ界隈の方に聞いてみたがその答えは今の所はなさそう。
  • マルチクラウドやハイブリットクラウドだったり、開発環境から本番までだったりを全て同じ環境で動かすような話であればやはりKubernetes

知らなかった技術のことをキャッチアップ出来たり、他の会社の事例を知れたりでカンファレンス参加はとても刺激的でした。 色々と考えさせられる部分もあり楽しい2日間でした。運営の皆様ありがとうございました。

来年からは Cloud Native Days という名前に変わり、コンテナだけでなくCloudNativeなアプリケーションやその基盤を含めたもっと大きなイベントになるそうです。 今から開催が楽しみです。

コネヒトではコンテナ周りの基盤を整備したりサービス改善を行うエンジニアを募集しています。 少しでも興味もたれた方は、是非気軽にオフィスに遊びにきていただけるとうれしいです。

www.wantedly.com

明日は、 @kichikuchiによる記事です!お楽しみに!