Kotlin Android Extensionsを使ってみよーよ

2017年12月15日
区分
Android
報告者:
honda

この記事はactindi Advent Calendar 2017の15日目の記事です。

Androidエンジニアのhondaです。去年のアドベントカレンダーではKotlinのことを書きましたが今年もKotlinについて書きます。よろしくお願いします。

現在、Android版いこーよではKotlinを使っています。100%Kotlinです。 弊社でKotlinアプリを作ってみたい方はこちらとかこちらなどで応募お願いします。お願いします!

前置きはこのくらいにして、Android版いこーよではKotlin Android Extensionsを使っています。 なかなか、プロダクトでのKotlin Android Extensionsの採用を聞かないので、今回はKotlin Android Extensionsの素晴らしさをお伝えしたいと思います。

Kotlin Android Extensionsとはなにか?

Kotlin Android ExtensionsはKotlinの開発元であるjetbrains社が開発したAndroid開発を支援するためのプラグインです。 このプラグインを使うことによって、Androidアプリエンジニアの方ならおなじみの「findViewById」を使わずにコードからUIの要素を参照することが出来るようになります。

Kotlin Android Extensionsを導入してみよう!

Android Studio 3.0以上で新規プロジェクトを作成するとデフォルトで Kotlin Android Extensions使えるようになっています。素晴らしいですね。 既存プロジェクトの場合はbuild.gradle (Module: app)に以下を追加するだけです。

apply plugin: 'kotlin-android-extensions'

簡単ですね。

Kotlin Android Extensionsでコードを書いてみよう!

・Activity

簡単なアプリを作ってみましょう。 TextViewとButtonが配置された画面でTextViewには”Hello World!”と表示されています。 ButtonをタップするとTextViewの”Hello World!”の表示が”ハローワールド!”に切り替わります。 コードは以下の通りです。

Activityのレイアウトファイル
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="sample.samplekotlinandroidextensions.MainActivity">

    <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content"
              android:text="Hello World!"
              app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent"
              app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" />
    <Button android:id="@+id/button" android:layout_width="wrap_content"
            android:layout_height="wrap_content" android:layout_marginTop="8dp"
            android:text="ハローワールド!" app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/textView" />

</android.support.constraint.ConstraintLayout>
Activityのソース
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        this.button.setOnClickListener {
            this.textView.text = "ハローワールド!"
        }
    }
}

これでfindViewByIdを使わずにtextViewに対して文言を設定することが出来ます。 レイアウトファイルでIDに設定した名前でコード上から参照出来ます。

・Fragment

次にFragmentで試してみましょう。 先程のActivityのSample同様にButtonをタップするとTextViewの内容が変わるものを作成します。

Fragmentのレイアウトファイル
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
                                             xmlns:app="http://schemas.android.com/apk/res-auto"
                                             xmlns:tools="http://schemas.android.com/tools"
                                             android:id="@+id/frameLayout"
                                             android:layout_width="match_parent" android:layout_height="match_parent"
                                             tools:context="sample.samplekotlinandroidextensions.SampleFragment">

    <TextView android:id="@+id/helloFragmentTextView" android:layout_width="wrap_content"
              android:layout_height="wrap_content" android:text="@string/hello_fragment"
              app:layout_constraintBottom_toBottomOf="parent"
              app:layout_constraintEnd_toEndOf="parent"
              app:layout_constraintStart_toStartOf="parent"
              app:layout_constraintTop_toTopOf="parent" />
    <Button android:id="@+id/helloFragmentButton" android:layout_width="wrap_content"
            android:layout_height="wrap_content" android:layout_marginEnd="8dp"
            android:layout_marginStart="8dp" android:layout_marginTop="8dp"
            android:text="@string/hello_fragment" app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/helloFragmentTextView" />

</android.support.constraint.ConstraintLayout>
Fragmentのソース
class SampleFragment : Fragment() {

    override fun onCreateView(
            inflater: LayoutInflater,
            container: ViewGroup?,
            savedInstanceState: Bundle?): View? {
        val view = inflater.inflate(R.layout.fragment_sample, container, false)
        view.helloFragmentButton.setOnClickListener {
            view.helloFragmentTextView.text = "ハロー フラグメント"
        }
        return view
    }
}

ここで注意しなければならないのがonCreateViewでのUIの要素の参照方法です。 上記のサンプルの用にonCreateViewではinflateしたviewに対してUIの参照を行わないといけません。

override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?): View? {
    val view = inflater.inflate(R.layout.fragment_sample, container, false)
    this.helloFragmentButton.setOnClickListener {
        this.helloFragmentTextView.text = "ハロー フラグメント"
    }
    return view
}

上記の様にUIに参照しようとするとNullPointerExceptionが発生します。 onCreateView以外では下記の様にUIに参照出来ます。

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    this.helloFragmentButton.setOnClickListener {
        this.helloFragmentTextView.text = "ハロー フラグメント"
    }
}

・RecyclerView

次にRecyclerViewで試してみましょう

RecyclerViewに表示するItemのレイアウトファイル
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView android:id="@+id/sampleRecyclerItemTextView" android:layout_width="wrap_content"
              android:layout_height="wrap_content" android:layout_marginBottom="8dp"
              android:layout_marginEnd="8dp" android:layout_marginStart="8dp"
              android:layout_marginTop="8dp"
              tools:text="TextView"
              app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent"
              app:layout_constraintHorizontal_bias="0.0" app:layout_constraintStart_toStartOf="parent"
              app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
RecycleViewのAdapterコード
class SampleRecyclerViewAdapter(private val itemDataList: List<String>) :
        RecyclerView.Adapter<SampleRecyclerViewAdapter.ViewHolder>() {
    override fun getItemCount(): Int = itemDataList.count()

    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder? {
        parent ?: return null
        return ViewHolder(LayoutInflater.from(parent.context).inflate(
                R.layout.view_sample_recycler_item, parent, false))
    }

    override fun onBindViewHolder(holder: ViewHolder?, position: Int) {
        holder?.itemView?.sampleRecyclerItemTextView?.text = itemDataList[position]
    }

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        init {
            itemView.setOnClickListener {
                Toast.makeText(
                        itemView.context, 
                        itemView.sampleRecyclerItemTextView.text, 
                        Toast.LENGTH_SHORT).show()
            }
        }
    }
}

RecycleViewのAdapterがListに入った文言をItemのレイアウトファイルを使ってリスト表示しています。 レイアウトのsampleRecyclerItemTextViewへはViewHolder内でitemView経由で参照します。

Kotlin Android Extensionsの挙動を理解しよう!

さて、とても便利なKotlin Android Extensionsですが、内部でどんなことをしているのでしょうか? Activityのサンプルコードを逆コンパイルしてJavaコードに変換して、どんなことをしているのか理解していきましょう。

public final class MainActivity extends AppCompatActivity
{

    public MainActivity()
    {
    }

    public void _$_clearFindViewByIdCache()
    {
        if(_$_findViewCache != null)
            _$_findViewCache.clear();
    }

    public View _$_findCachedViewById(int i)
    {
        if(_$_findViewCache == null)
            _$_findViewCache = new HashMap();
        View view1 = (View)_$_findViewCache.get(Integer.valueOf(i));
        View view = view1;
        if(view1 == null)
        {
            view = findViewById(i);
            _$_findViewCache.put(Integer.valueOf(i), view);
        }
        return view;
    }

    protected void onCreate(Bundle bundle)
    {
        super.onCreate(bundle);
        setContentView(0x7f09001b);
        ((Button)_$_findCachedViewById(R.id.button)).setOnClickListener((android.view.View.OnClickListener)new android.view.View.OnClickListener(this) {

            public final void onClick(View view)
            {
                view = (TextView)_$_findCachedViewById(R.id.textView);
                Intrinsics.checkExpressionValueIsNotNull(view, "this.textView");
                view.setText((CharSequence)"\u30CF\u30ED\u30FC\u30EF\u30FC\u30EB\u30C9\uFF01");
            }

            final MainActivity this$0;

            
            {
                this$0 = mainactivity;
                super();
            }
        }
);
    }

    private HashMap _$_findViewCache;
}

_$_findViewCache というハッシュマップのプロパティを用意して、Viewを管理しています。 参照時に_$_findViewCacheにViewインスタンスが無ければ、findeViewByIdをしてViewインスタンスを格納して参照し、すでに_$_findViewCacheにViewインスタンスがあれば、それを参照するという処理だということがわかりました。

注意点

Kotlin Android Extensions、とても便利ですが気をつけなければならない部分があります。

・誤って他の画面のUIへの参照するコードが書けてしまう。ビルドも通る。

Kotlin Android ExtensionsはData Bindingのようにコード生成しているのではなく、UIの要素をハッシュマップで管理しています。 誤って他の画面の要素を参照するコードを書いてしまっても、ビルドは正常に通ってしまうので気づけません。 実行時に_$_findCachedViewByIdでNullが返却されるため、NullPointerExceptionでクラッシュして、気づくことになります。 レイアウトファイルの要素のIDは他の画面の要素のIDが被ったり、どこの画面の要素なのかわからなくなることを防ぐために命名規則を設けておくと良いかもしれません。 例えば、「sampleActivityTextView」というような感じです。

・設計と相談

すでにデータバインディングを使っているような既存のプロジェクトではKotlin Android Extensionsを使っていくとなると設計を変えざるを得ないと思います。 使用する場合は設計と相談の上、ご使用ください。

まとめ

  • Kotlin Android Extensionsはjetbrains社製のAndroid開発支援プラグイン。
  • Kotlin Android Extensionsは導入が簡単!
  • Kotlin Android Extensionsを使うとfindViewByIdを使わずに簡潔にUIを参照するコードが書けるようになる!
  • FragmentやRecyclerViewのAdapterなどでも使える!けど、Activityでのやり方とは違うので注意。
  • Kotlin Android ExtensionsはUIの要素をViewインスタンスとしてハッシュマップで管理している。
  • 間違って他の画面のUIを参照するとクラッシュするので命名規則などで回避。
  • 使う時は設計と相談。

GASでスプレッドシートを扱うときのTips

2017年12月07日
区分
gas
報告者:
kadota

こんにちは、kadotaです。アクトインディでPCおじさんをしています。

この記事は actindi Advent Calendar 2017 の7日目の記事です。

G Suite (旧Google Apps) を利用している環境では、Google Apps Script (略してGAS) でちょっとした工夫、効率化ができるのがうれしいところだと思います。
今回はGASでスプレッドシートを扱うときに困った状況を解決してくれる魔法の1行を紹介します。

データを専用の外部シートに分けたい

あるシートで、データを別ファイルのシートから読み込んで反映したいときがあります。

こんな例:

// データ取り込み用のシート
var sheetsA = SpreadsheetApp.getActiveSpreadsheet();
var sheetA = sheetsA.getSheetByName("シート名");
var rangeA = sheetA.getRange(startRow, startCol, numRows, numCols);

// 外部シートから取り込み
var dataUrl = 'https://docs.google.com/spreadsheets/d/XXXXXXXXXXXX.../edit#gid=0';
var sheetsB = SpreadsheetApp.openByUrl(dataUrl);
var sheetB = sheetsB.getSheets()[0];
var rangeB = sheetB.getRange(startRow, startCol, numRows, numCols);

// シートBをシートAに反映
rangeA.setValues( rangeB.getValues() );

一部のセルがおかしい…

ふつうはこれでOKなのですが、一部のセルだけ処理をする際におかしなことになることに気付きました。日付や時刻です。

これらが入っているセルを Apps Script で扱う際に、データが Date オブジェクトとして返ってくるせいでした。

セルのデータを表示そのままで取り込む

Date オブジェクトをいちいち整形しなおすしかないのか…?と思いましたが、Range クラスに魔法のメソッドがありました。 setNumberFormat です。

名前こそ number format ですが、日付も扱えます。 これでフォーマットを指定することができる上に、フォーマットを決めず「表示そのまま」にすることができるのです。その場合は、 @ を指定します。

range.setNumberFormat('@');

今回は取り込んだデータを、そのまま別のテキストに差し込むために使いたかったので、データ範囲全体に上記を適用して解決しました。

以上、魔法の1行の紹介でした。

Chromeの設定+α

2017年12月03日
区分
web
報告者:
morishita

この記事は actindi Advent Calendar 2017 の12月3日の記事です

Web系のエンジニア、デザイナーにとって最も利用する仕事道具といえば、Webブラウザだと思います。 その中でもGoogle Chromeを使うことが多いのではないでしょうか。

今回はChromeの便利な設定などについて紹介します。

Chromeの環境を複数持ちたい

私はChromeに拡張を結構インストールしています。便利なんですが、時にそれらが全くないクリーンなChromeを使いたいことがあります。拡張の影響を受けたくないだけなら、シークレットモードを使、うのが、最も手っ取り早いです。 が、すべての拡張をOFFにするのは困る場合、シークレットモードではなく別の環境を持ちたくなることがあります。
また、開発中には複数のアカウントで同時にログインして動作の違いなどを確認したいこともあるのではないでしょうか。

そういう場合には次の方法があります。

Chrome canaryをインストールする

手っ取り早い方法のひとつとして Chrome canary版をインストールすることです。

これはChromeの開発者向けバージョンで、まだ、通常のChromeに入っていない新機能が含まれているものです。 通常版のChromeとは全く独立で動作するので、普段使いのChromeにインストールしている拡張や設定の影響を受けません。 通常リリース版より不安定な可能性はあるのですが、使っている感想としては困ったことはありません。 ちなみに、canary版にはAndroid版もあります。 新しいJavaScriptやCSSの機能をいち早く試せたりするのでインストールしておいて損はないと思います。

別ユーザ(プロファイル)を使う

Chromeには複数のユーザ(プロフィール)を管理する機能が備わっています。 ユーザごとにChromeのバージョン以外は拡張・設定が独立しています。 Chromeの設定(chrome://settings/)を開いて、「他のユーザを管理」をクリックすると 次のような画面が表示されます。

ユーザ管理

この画面の右下「ユーザを追加」をクリックすると、次のようなユーザ名とアイコンを入力する画面が開くので 適当に入力します。

ユーザ登録

入力したら「追加」ボタンをクリックすると新しいChromeのウィンドウが開きます。 それが新たに追加したユーザで起動したChromeです。 ウィンドウの右肩に利用しているユーザの名前が表示されるので区別できます。 これまで使ってきたChromeとは別にブックマークと拡張が管理されますで、 用途別にユーザを作ってそれに応じた拡張をインストールしたりすると便利です。 あるいはまっさらな状態のままにしておいてもいいでしょう。

また、ChromeはGoogleアカウントへのログインを求めてきますが、 ユーザごとに別のアカウントが使えるので複数のアカウントを同時に利用したい場合にも便利です。 別Chromeユーザならば、Googleにかぎらず他のWebサービスでも同時に複数アカウントでログインすることが可能です。

登録したユーザを切り替えるにはウィンドウの右肩をクリックすると一覧が表示されるのでそこで選択できます。

ユーザ登録

Dev Tool

Web系のエンジニア・デザイナーにとって、Chrome Dev Toolもよく使うツールだと思います。 Dev Toolでの便利な設定や機能を紹介します。

コマンドメニューを開く

Dev Toolを表示します。そこで、macならば command+shift+p、Windowsならばctrl+shift+p(win)を押します。 するとatomエディタやsublimeを使っている人にはおなじみのコマンドメニューが開きます。 ここにChrome Dev Toolの機能・設定のすべてがリストアップされます。 あちこち探し回るよりも効率よく機能を呼び出したり設定を変更できます。 コマンドの一部を入力すると、候補を絞り込んでくれるので、うろ覚えの機能でも探しやすいです。

コマンドメニュー

以下にコマンドメニューを眺めていて、こんな機能があったのか! と思った機能を幾つか紹介します。

サードパーティコンテンツにマークを付ける

ネットワーク広告やその他計測ツールなど、他社のJavaScript、CSS等を自社サイトで読み込んでいる場合も多いかと思います。 Dev ToolのNetworkタブではそれらも一緒に表示されるので、どれがどこのものなのかわかりにくいです。 そんなときにはコマンドメニューでshow third party badges を選択すれば、サードパーティのコンテンツには 次のようにマークが表示されます。

Third party badges

マウスオーバーするとサードパーティのサービス名も表示されます。 同様の表示はNetworkタブの他、Consoleでも表示されます。

カバレッジ

開発を続けていると、使われなくなったJavaScriptやCSSが出てきます。変更がはっきりしていると その時に削除するのですが、いつの間にか使わなくなってしまったというものもあるのではないでしょうか。 害がなければ放置されることも多いのではないかと思うのですが、ムダも積み重ねると開発効率や 表示速度の低下の元になったりします。 使われていないJavascriptやCSSを特定するのは時に困難です。そんな時役立つのがカバレッジ機能です。 表示するにはコマンドメニューで show coverage を選択します。

coverage

上のキャプチャで示した円を描いた矢印のボタンをクリックするとページをリロードして計測を開始します。 結果は画面の下にそのページで実行されているJavaScriptやCSSがリストアップされ、 それぞれコードの何%が実行されているかが表示されます。 そのリストから1つを選択すると、画面上部にコードが表示されます。 実行されている部分は緑、実行されていない部分は赤く表示されます。

Railsだとアセットパイプラインで複数のファイルがまとめられます。 したがって該当のページだけではすべてのコードを実行しない場合もあります。 計測は止めるまで継続されるのでこの状態から続けて操作を行えば実行されたコードは緑に変わっていきます。 ページ遷移など操作を続ければ使われていないコードを絞り込んでいくことができます。
利用頻度の低い機能で実は使っていたというリクスがあるので、これだけで削除を判断するのは早計かもしれませんが、 削減候補を探すときには役立ちます。

時々、この機能を使って、使われてなさそうなコードをチェックしてはどうでしょうか。

レイヤー

Firefoxの開発ツールにもあったのですが、Quantumになってからなくなってしまったので残念に思っている方もいるのではないでしょうか。 Chromeにも実は同様の機能があります(内容がペイントされないのでFirefoxのものより見劣りはしますが)。 コマンドメニューから show layers を選択すると次のように表示されます。

layer

キャプチャは私が個人的に公開している防災に関わる「言い伝え」MAP のものですが、パンとローテートで表示を調整するとWebページのレイヤーの重なり具合を確認できます。 レイヤをクリックして選択すると、サイズやメモリ消費量などが表示されます。 これを使えば何故か表示されない要素の重なり順を確認したり、隠れていて見えない無駄要素を見つけることができるでしょう。

最後に

Chrome を活用していこーよを改善していきたいエンジニアを募集していますので、よろしくお願いします。

rails5へのアップデート作業の途中経過報告

2017年12月02日
区分
Rails
報告者:
endo

こんにちは、endoです。

アドベントカレンダー2日目の記事です。

現在、rails5へのアップデート作業をしており、その途中経過報告です。

1 rails4系の最新版にする

2 rails5を動かす

→gemをrails5対応するものにあげる

3 rails4で変更できるDeprecationコードを書き直していく

→Deprecationコードを地道に修正する

4 rails5でしか変更できないもので一気にリリースする

ざっくりと、こんな感じで動いております。

現在は、3の作業の途中です。

体感でいうと、3の入り口で全体の10%ぐらいの気がしています。

では、1の状況から振り返ります。

rails4系の最新版にする

rails4.2.10に上げました。

nokogiriが1.8.1に上がり、確認漏れがあり不具合が出ました。

CMSのDOMのParseをnokogiriで行っていますが、Parseに修正が入っており、今までの書き方では動かない場所がありました。

これは気づきにくいよ・・・(´・ω・`)

rails5を動かす

いきなりgemをrails5に切り替えると、悲しいぐらいすんなり動いてくれません。

その理由は、gemがrails5(actionpack/railtieなど)に対応していないからです。

ここは地道に上げていきましょう。

  • acts-as-taggable-on 3.5.0 → acts-as-taggable-on 5.0.0
  • coffee-rails 4.0.1 → coffee-rails 4.2.2
  • delayed_job_active_record 4.1.1 → delayed_job_active_record 4.1.3
  • capistrano 2.15.5 → capistarano2.15.9(sprocketsのdeploy対応)
  • slim-rails 3.0.1 → slim-rails 3.1.3
  • sass-rails 4.0.3 → sass-rails 5.0.6
  • sprockets 2.11.0 → sprockets 3.7.1
  • rspec-rails 3.4.0 → rspec-rails 3.7.0
  • jbuilder 2.2.5 → jbuilder 2.7.0
  • will_paginate 3.0.7 → will_paginate 3.1.6

使用しているgemで最低限上のものを上げないとrails5が動きませんでした。

気持ちにはiphone5からiphone8にアップデートするぐらいのアップデートでした。

gem updateによる副産物

今までバグがたまに出ていた箇所の修正がされており、わざわざバグ対応する手間が省けました。

https://github.com/mbleigh/acts-as-taggable-on/pull/809

上のプルリクによって、たまにいこーよ内で起きていた問題も解消されました。

gemをアップデートすることは大事ですね。

rails4で変更できるDeprecationコードを書き直していく

rails5が動いたからといって、今までの上記のgem以外が動かないとは限りません。

pry-railsは動いてくれなかったので、アップデートしました。

DEPRECATION WARNING: alias_method_chain is deprecated. Please, use Module#prepend instead. From module, you can access the original method using super.

rails consoleを叩くだけで、簡単にDeprecationが出てくれるので、本当に助かります。

ここでDeprecationで出ているものは、gemの場合もあります。

gem側が修正していないなら、forkして直すか、捨てるしかありません。

時代とともにソースコードは変化する

話は変わりますが、いこーよはhttps化が完了しております。

https://github.com/actindi/ssl_requirementでhttpsの判定をしております。

ただ、このgemでDeprecationが出ているので、http/httpsの判定をnginx側に全部寄せようとしています。

今まではアプリケーションコードで書く方が柔軟でしたが、現状はhttps化になっているので、アプリケーション側で設定するよりはnginx側に任せるよう方が役割がハッキリします。

一部httpの接続を許可する場所もありますが、複雑になりそうな気配もないので、これをきっかけに変更します。

そして、gemの役割を終えてもらいます。

総括

想像しているよりやること多いなって感じたのが本音です。

ただ、gemのアップデート作業によって不具合が修正されたり、いらないgemを消したりできるので、こういうのは嬉しいですね。

この作業終わったら、常にgemを最新の状態に保てるような体制を構築したいと思います。

以上です。

Github + Lambda + CodeBuild で自動テスト

2017年10月23日
区分
aws
報告者:
morishita

morishitaです。

いこレポの開発環境でプルリクエストに push したら Lambda と CodeBuild を使って Rspecを実行する仕組みを作ったので、ご紹介します。

どんなの?

Githubでプルリクエストを作ったり、プルリクエストにPushすると、こうなって

Github実行中

テストがすべてパスすると、こうなる仕組みを作りました。

GithubOK

Slackにも通知されます。

slack通知

なぜつくったの?

このユースケース自体は CircleCI等を使えば、わりと簡単にできてしまいます。 ではなぜ、作ったのかというと、次の通りです。

分単位課金で何並行でも使える

CircleCIでフリーだと同時に実行できるテストは1で、同時4テストを実行できるプランだと $150/月かかります。 ところが、CodeBuildだと$0.02/分で8vCPU、15GBのインスタンスが使えます。同時に何並行でも実行でき単純に時間課金のみです。現状、いこレポだと一回のテストに約5分、$0.1しかかかりません。 月に$150分使おうとすると、20営業日/月として1日75回もテストできてしまいます。 実際にはそんなに実行されないので、コスト的にも有利です。

AWSの他サービスと連携しやすい

CodeBuild は CodePipeline や、Lambda、CloudWatchと連携しやすく、今後インテグレーション していきやすそうです。

Serverless Framework を使ってみたかった

これまで、Slack に通知などのちょっとしたLambda関数は作っていて、AWSコンソールで直接実装していました。今回はもう少しコード量が増えそうだったので、コンソールではちょっとつらいなぁと思いServerless Framework を使うことにしました。 Serverless Framework と AWSの各種サービスを使ってどんなことができそうか試してみたかったというのもCIサービスを使わなかった理由です。

利用したサービス・ツール

全体の流れ

全体の流れを表したシーケンスは次のとおりです。 シーケンス

2つの処理から構成されています。ひとつはGithubからSNSへのイベント通知をトリガーにLambdaがCodeBuildを起動して、それをGithub・Slackに通知する処理。そしてCodeBuildの終了をCloudWatch Eventが受けてLambdaがGithub・Slackに通知する処理です。

図が煩雑になるのでシーケンス図上は Lambda は一つしか描いていませんが、 それぞれの処理に対応するLambda関数を一つづつ実装しました。

CodeBuild での Rspec 実行

この仕組のメインは CodeBuild で Rspecを実行しているところです。 RailsのRspec と MySQL を動かすコンテナを docker-compose を使って、実行しています。

Railsコンテナでは test-queue を使ってRspecを分散実行しており、 SimpleCov でテスト結果のコードカバレッジも記録します。

ハマったところ

test-queue 導入に起因したもの

実はテスト実行順のランダム化も設定忘れで実施できていなかったので テスト順が変わってFixtureのロードが漏れてテスト失敗が多発しました。 フィクスチャを用意していたのは都道府県や地方といったマスター系のデータだったのですが、 これは rails_helper に config.global_fixturesを設定することにより解消しました。

また、画像アップロードのテストが時々failすることがありました。 test-queue はフォークして複数プロセスでテストを実行するので、それぞれのプロセスには独立したリソースを用意する必要があります。Paperclipのアップロードされたファイルの保存場所が分離されておらず、たまたまテストタイミングが重なるとエラーとなっていたようでした。test-queueの実行スクリプトの after_fork でプロセスごとの保存場所を設定することで解決しました。

ネームスペースが異なる同名のコントローラでfail

たとえは記事ページのコントローラは AtriclesController、記事管理のコントローラはAdmin::AtriclesController というクラス名で実装しているのですが、何故かこれをテスト中取り違えられてテストがfailするということがありました。 これは不本意ながら、require 'admin/articles_controller' のように、ターゲットクラスをspecファイルで読み込むことにより解決しました1。もっとスッキリした解決法はないものかと 思っています。

decker-compose が終了しない

Rspecを実行するためには MysqlとRails2つのコンテナを動かすのですが、Specの実行が終わっても、MySQLが残るので、docker-composeが終わらなくて困りました。 なにか解はないものかとドキュメントを読み返していると--abort-on-container-exit オプションで解決できそうだということがわかりました。これを利用するとコンテナが一つでも終わると、docker-compose自体が終了します。abort と言いながらも、exit 0 で終了するので、CodeBuild的にもFAILにならなず解決できました。

CodeBuildがS3にアップする処理結果は暗号化されている

GithubやSlackに通知した処理結果には、詳細な結果情報を得るためのリンクを付けています。 このリンクのURLはS3の署名付きURLと言うもので、期間限定でS3のファイルをダウンロードできるものです。 SDKを利用すれば、getSignedUrl で簡単にこのURLが生成できると思ったのですが…、 CodeBuildはS3にアップロードするデータは暗号化してしまい、暗号化されたファイルを取得するための署名付きURLは 署名バージョン 4 署名プロセスで署名する必要があったのです。しかしgetSignedUrlはこれに対応していない。 今回は aws-signature-v4を利用してURLを生成して解決しました。 しかし、Javascriptの AWS SDKのs3::getSignedUrlはデフォルトではこれに対応していません。 s3のインスタンスを作る時に次のようにsignatureVersionv4に指定することでs3::getSignedUrlが出力するURLが署名バージョン 4 署名プロセスで署名されたものに変わります。(2017/11/06 修正)

const s3 = new AWS.S3({ signatureVersion: 'v4' });

やってみてどうだったか?

これまで、いこレポではプロダクト自体のコードがそれほど多くなく、自動テストについては サボっていてローカルでRspecを実行していたのですが、pushしたらテストが実行されるのはやはり楽です。

実装に関しては、 Serverless Frameworkの導入はやって良かったです。 Webpackと組み合わせて使えるので、設定すれば babel で ES2015 相当のJavascriptの仕様が使えます。特にasync/awaitが使えるのは大きいです。デプロイもワン・コマンドで済んでしまうのでとても楽できました。ちょっとしたものにも積極的に使っていくべきだと思いました。 また、初期案ではJenkinsを導入しようとしていたので、そのサーバ運用の手間も省けたのも大きいです。 AWS SDKを使ってサービス間をLambdaで繋いでいくことで、サーバレスでの自動化を進めて行けそうな 手応えを得られました。もう少し複雑なことをしたくなったら、Step Functions も使ってみようかなと思っています。

最後に

サーバーレスで、いろいろやってみたいエンジニアを募集していますので、よろしくお願いします。

 | 

技師部隊からの
お知らせ

【求人】エンジニア募集しています。

本頁の来客数
八十七万千百七十六名以上(計測停止中)

メンバー一覧

アクトインディ技師部隊員名簿

アクトインディ技師部元隊員

アクトインディへ

カテゴリー

アクトインディ

aaaa