継続的インテグレーション (CI) は、ソフトウェア開発の DevOps に関連してよく使用される用語です。CI はプロジェクトにプッシュされた新しいコードの正当性を確認するために自動的に検証を実行するプロセスであり、そのバリデーションはプロジェクト用に記述されたテストに基づいて行われます。つまり、プッシュされるほぼすべてのアップデートに対して、テストが付属しているということです。

一般的な CI プロセスは次のとおりです。

  • コードがリポジトリにチェックインされる。
  • 新しいコードがプッシュされたという通知が CI サーバーに届く。
  • CI サーバーがリポジトリからコードを取り出し、付属のテストを実行する。指定したコマンドがあれば、それも実行する。
  • CI サーバーからコマンドの実行結果が通知される。

通常のソフトウェア プロジェクトには複数の開発者が参加しています。このため、リポジトリにコードがプッシュされるたびにプロジェクトの安定性を確認できる CI は便利な手法です。

継続的デプロイメント (CD) は、CI をさらに一歩進めた手法です。テストを実行し、ビルドを生成した後、CD はビルドを本番環境にデプロイします。CI とは違い、CD ではアプリを本番環境にプッシュするためのデプロイ コマンドをスクリプト内に指定します。

このオペレーションでは、CI サーバーとリポジトリという 2 つの重要なツールを使用します。リポジトリはコードが格納される場所であり、CI サーバーはインテグレーションとデプロイメントがトリガーされる場所です。この記事では、GitHub をコードのリポジトリ、CircleCI を CI サーバーとして使用します。

作成する内容

この記事では、人気の映画を表示するシンプルな Android アプリを作成します。API からデータをフェッチして表示するアプリです。アプリの作成後は、このアプリのテストを記述し、CI/CD のセットアップを行います。

前提条件

この記事は、Kotlin を使った Android 開発の基礎知識を持つ開発者を対象としています。まだあまり経験を積んでいない方も、このコースを入門編として活用していただけます。さらに、次の準備が必要です。

  • 動作が安定している最新版の Android Studio をインストールする
  • GitHub アカウントを用意する
  • macOS がインストールされているコンピューターを用意する (このプロジェクトでは、macOS で公式にサポートされている、fastlane を使用します。他のプラットフォームでのサポート予定についてはこちらを確認してください)

準備がすべて完了したら、次のステップに進みましょう。

アプリを作成する

時間を短縮したいので、ゼロからアプリを開発する代わりに、今回はスターター プロジェクトを利用します。スターター プロジェクトはこちらからダウンロードできます。先に進む前に、スターター プロジェクトに含まれているクラスやファイルについて理解しておくことが大切です。

プロジェクトには、次のクラスおよびファイルが含まれています。

  • APIClient.kt: Retrofit のインスタンスにアクセスするためのオブジェクト。Retrofit は Android と Java 用のタイプセーフな HTTP クライアントであり、Square で開発、管理されています。アプリでのネットワーク リクエストに使用します。
  • ApiInterface.kt: アプリの開発中にアクセスするエンドポイントを格納するインターフェイス。Retrofit はこのインターフェイスを使用します。
  • MovieListAdapter.kt: リスト上のデータを管理するために使用するアダプター クラス。リストのサイズ、行の見た目、データのリストへのバインド方法を制御します。DiffUtilCallback クラスを使って、リストに含まれるコンテンツの一意性をチェックします。
  • MovieModel.kt: サーバーからの応答をマッチングするデータ クラスが含まれるクラス。
  • MainActivityViewModel.kt: MainActivity がネットワーク リクエストを行い、結果を受け取るために使用する ViewModel。
  • MainActivity.kt: 他のすべてのクラスをリンクするクラス。アプリはここからスタートします。

スターター プロジェクトの中身を確認したら、アプリに最後の仕上げを追加します。

まず、The Movie DB から API キーを取得します。まだアカウントをお持ちでない場合は作成してください。アカウントのセットアップが完了したら、右上のユーザーの画像をクリックし、ドロップダウン メニューの [Settings (設定)] をクリックして、設定を開きます。次に、縦型メニューの [API] タブをクリックして、API キーをリクエストします (まだリクエストを完了していない場合)。

続いて、API キー (v3 auth) の値をコピーします。

キーをコピーしたら、gradle.properties ファイルを開いて以下のコードを追加します。

API_KEY = "MOVIE_DB_API_KEY"

注: MOVIE_DB_API_KEY はコピーしたキーで置き換えます。

すると、Gradle ファイルの同期を勧める通知が表示されるので、Gradle ファイルを同期します。

次のステップは、API から人気の映画をフェッチするロジックの追加です。この作業はビュー モードで行います。MainActivityViewModel クラスを開き、以下の内容を追加します。

// app/src/main/java/dev/idee/cicdandroid/MainActivityViewModel.kt

import android.util.Log
import androidx.lifecycle.MutableLiveData
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response

次に、class の本文に以下の内容を追加します。

// app/src/main/java/dev/idee/cicdandroid/MainActivityViewModel.kt

private val movieListLiveData = MutableLiveData<List<MovieModel>>()

open fun getMovieLiveData(): MutableLiveData<List<MovieModel>> {
    return movieListLiveData
}

fun setMovieLiveData(movieModelList: List<MovieModel>) {
    movieListLiveData.value = movieModelList
}

init {
    fetchMovies()
}

private fun fetchMovies() {

    APIClient.client.create(ApiInterface::class.java).getPopularMovies(BuildConfig.API_KEY)
        .enqueue(object : Callback<MovieResponse> {
            override fun onFailure(call: Call<MovieResponse>, t: Throwable) {
                Log.e("MainActivityViewModel", t.toString())
            }

            override fun onResponse(call: Call<MovieResponse>, response: Response<MovieResponse>) {
                response.body()?.let {
                    setMovieLiveData(it.movieModelList)
                }

            }

        })

}

クラスが初期化されると、init ブロックが呼び出されます。このブロックで、fetchMovies メソッドを呼び出します。このメソッドは映画をフェッチし、応答として movieListLiveData オブジェクトを更新します。これは Observable オブジェクトで、MainActivity が結果をリッスンするのに使用します。

次に、MainActivity 内にビュー モデルをセットアップします。これを使って、movieListLiveData オブジェクトの更新をチェックします。リサイクラー ビューをセットアップして、フェッチした映画を表示します。

MainActivity.kt を開いて、import セクションに以下の内容を追加します。

// app/src/main/java/dev/idee/cicdandroid/MainActivity.kt

import android.view.View
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.activity_main.*

続いて、onCreate メソッドの直前に以下のオブジェクトを追加します。

// app/src/main/java/dev/idee/cicdandroid/MainActivity.kt

private lateinit var viewModel: MainActivityViewModel
private val movieAdapter = MovieListAdapter(DiffUtilCallback())

ここで、MainActivityViewModel のインスタンスを宣言します。また、リサイクラー ビューで使用できるようアダプターを初期化します。

次に、MainActivity クラスに次の 2 つのメソッドを追加します。

// app/src/main/java/dev/idee/cicdandroid/MainActivity.kt

private fun setupRecyclerView() {
    with(movieRecyclerView) {
        layoutManager = LinearLayoutManager(this@MainActivity)
        adapter = movieAdapter
    }
}

private fun setupViewModel() {
    viewModel = ViewModelProviders.of(this)[MainActivityViewModel::class.java]
    viewModel.movieListLiveData.observe(this, Observer {
        progressBar.visibility = View.GONE
        movieAdapter.submitList(it)
    })
}
  • setupRecyclerView: レイアウト マネージャーとアダプターを使用してリサイクラー ビューを初期化します。
  • setupViewModel: ビュー モデルを初期化し、movieListLiveData オブジェクトを観察して応答をリッスンします。

現時点では、これらのメソッドは参照されていません。クラスの onCreate メソッドに追加すると、アクティビティがスタートしたときにトリガーされます。

// app/src/main/java/dev/idee/cicdandroid/MainActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
    // ...
    setupRecyclerView()
    setupViewModel()
}

これでアプリの準備が完了しました。Android Studio の実行ボタンをクリックすると、次のような結果が得られるはずです。

テストを作成する

継続的インテグレーションを効果的に実施するには、テストが必要です。テストを行って、プロジェクト内でチェックとバランスを管理します。このセクションでは、アプリケーションにいくつかのテストを追加していきます。テストの依存関係について心配する必要はありません。これらはダウンロードしたスターター プロジェクトに追加済みです。

androidTestディレクトリを開きます。 /android-projects/ci-cd-android/app/src/androidTest/java/{パッケージ名} のようなパスになっています。適切なディレクトリであれば、ExampleInstrumentedTest テスト ファイルが格納されているはずです。

新しい MainActivityTest ファイルを作成して、以下の内容を追加します。

// /app/src/androidTest/java/dev/idee/cicdandroid/MainActivityTest.kt

import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.ActivityTestRule
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class MainActivityTest {

    @get:Rule
    val activityRule = ActivityTestRule(MainActivity::class.java, false, false)

    @Test
    fun appLaunchesSuccessfully() {
        ActivityScenario.launch(MainActivity::class.java)
    }

}

このテストは、アプリがエラーを出さずに正常に起動するかどうかをチェックします。次に、同じディレクトリ内に MainActivityViewModelTest という名前のファイルを作成して、以下の内容を追加します。

// /app/src/androidTest/java/dev/idee/cicdandroid/MainActivityViewModelTest.kt

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class MainActivityViewModelTest {

    private lateinit var viewModel: MainActivityViewModel
    private val list = ArrayList<MovieModel>()
    @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule()

    @Before
    fun setUp() {
        viewModel = MainActivityViewModel()
    }
    
    @Test
    fun testWhenLiveDataIsUpdated_NewValueTakesEffect() {
        list.add(MovieModel("","",""))
        viewModel.setMovieLiveData(list)
        Assert.assertEquals(viewModel.getMovieLiveData().value!!.size,1)
    }

}

このテストは、LiveData オブジェクトを更新し、結果を取得するメソッドが適切に機能していることを確認します。実際にテストを実行して、問題がないことを確認してください。

Android アプリが完成し、テストを作成したので、次はリモート リポジトリにチェックインします。GitHub プロファイルを開き、新規プロジェクト (リポジトリ) を作成します。

プロジェクトに適切な名前を付け、説明を追加します。たとえば、ci-cd-android という名前を付けることができます。プロジェクトを作成すると、ローカル プロジェクトをリポジトリにプッシュする手順が表示されます。

ここではこの手順をスキップして、次のセクションに進みます。

CircleCI で CI をセットアップする

このセクションでは、CircleCI を使用して CI を Android プロジェクト用にセットアップします。Android プロジェクトのルート ディレクトリに .circleci という名前のフォルダーを作成し、このフォルダー内に config.yml というファイルを追加します。

config.yml ファイルを開いて、次の内容を貼り付けます。

version: 2.1

orbs:
 android: circleci/android@0.2.0

jobs:
 build:
   executor: android/android

   steps:
     - checkout
     - run:
         command: ./gradlew build

このスクリプトには、リポジトリにチェックインしたコードに対して実行されるコマンドが含まれています。このファイルをパイプラインと呼びます。

このスニペットには、複数のセクションがあります。version は、使用する CircleCI のバージョンを指定する部分です。バージョン 2.1 の場合は jobs セクションがあります。ジョブがビルド プロセスを実行します。設定ファイルを定義するときには、少なくとも 1 つ以上のジョブが必要です。

端的に言えば、定義したジョブがコードをチェック アウトし、必要な依存関係をダウンロードし、テストを実行して、結果をアップロードします。

続いて、プロジェクトの更新をリポジトリにプッシュします。このためには、以下のコマンドを順に実行します。

# adds all the changes in the working directory to the staging area
git add .

# Save changes to the local repository
git commit -m "added tests and Circle CI config file"

# Push changes on the local repository to the remote repository
git push origin master

次に、CircleCI ダッシュボードで GitHub プロジェクトをセットアップします。CircleCI アカウントをお持ちでない場合は、簡単に作成できます。

もう GitHub アカウントは用意しているので、時間はほとんどかかりません。既に CircleCI アカウントをお持ちでしたら、ログインしてください。いずれかの方法でダッシュボードを開いたら、[ADD PROJECTS (プロジェクトの追加)] タブを選択します。

先ほど作成したリポジトリを探して、[Set Up Project (プロジェクトのセットアップ)] をクリックします。ボタンをクリックすると、次のようなページが表示されます。

[Start Building (ビルドの開始)] をクリックします。リポジトリに設定ファイルを自動で追加するかどうかを確認するメッセージが表示されたら、既に設定ファイルは追加済みなので、[Add Manually (手動で追加)] を選択します。

デプロイメント用に fastlane をセットアップする

ここから、Android プロジェクト用に継続的デプロイメントのセットアップを行う段階へと進んでいきましょう。今回は fastlane を利用します。fastlane によって、Android アプリを Google Play ストアへ自動リリースできます。

まず最新の Xcode コマンド ライン ツールをインストールします。次のコマンドを使用します。

xcode-select --install

パーミッション リクエストが表示されたら、これを受け入れ、その後に表示される利用規約に同意します。すると、最新のツールがフェッチされ、インストールされます。

Xcode のインストールが完了したら、以下のコマンドで fastlane をインストールします。

brew cask install fastlane

fastlane のインストールが完了したら、fastlane をパスに追加します。次のコマンドでバッシュ プロファイル ファイルを開きます。

open -e .bash_profile

注: ファイルがまだない場合は、touch .bash_profile コマンドを使用して作成します。

次に、このファイルに以下の内容を追加します。

PATH=$PATH:$HOME/.fastlane/bin

続いて、次のコマンドを実行してバッシュ プロファイルをリフレッシュします。

. .bash_profile

Android プロジェクトのターミナルに移動し、次のコマンドを実行して、プロジェクト内で fastlane を初期化します。

fastlane init

ディレクトリ内に fastlane フォルダーが作成され、fastlane が起動します。アプリのパッケージ名の入力を求められたら、パッケージ名を入力し、Enter キーを押します。アプリのパッケージ名は、app-module の build.gradle ファイル内で確認できます。ファイル内の applicationId を検索してください。

次に、JSON シークレット ファイルのパスの入力を求められるので、fastlane/api.json と入力して Enter キーを押します。最後に、fastlane を使用して Google Play に情報をアップロードするかどうか確認するメッセージが表示されるので、ここでは n と入力します。

注: fastlane では、アプリのスクリーンショットとリリース ノートをアップロードできますが、今回この手順については省略します。

下図はターミナルに表示されるプロセスのスクリーンショットです。

プロジェクトで fastlane が初期化されたら、Google Developers のサービス アカウントから資格情報ファイルを取得します。バイナリを実際に Play ストアにアップロードするには、このファイルが必要になります。

Google Play Consoleを開き、[Settings (設定)] を選択します。

デベロッパー アカウントの下で API アクセス オプションを選択します。

Google Play アカウントで API アクセスを有効にするには、[CREATE NEW PROJECT (新しいプロジェクトの作成)] をクリックします。次のような画面が表示されます。

[CREATE SERVICE ACCOUNT (サービス アカウントの作成)] をクリックします。次のようなダイアログが表示されます。

[Google API Console] リンクをクリックします。Google Cloud Console が開き、プロジェクトが表示されます。

ここまで来たら、[CREATE SERVICE ACCOUNT (サービス アカウントの作成)] をクリックします。

詳細情報を入力して、[CREATE (作成)] をクリックします。ロールをたずねられたら、[Service Accounts (サービス アカウント)] > [Service Account User (サービス アカウント ユーザー)] を選択して、[CONTINUE (続行)] をクリックします。

その後、下図のような画面が表示されます。

[CREATE KEY (キーの作成)] をクリックします。キー タイプは JSON です。作成されたキーはただちにマシンにダウンロードされます。

Google Play Condole が開いているタブに戻ります。ダイアログの [DONE (完了)] をクリックすると、新しいサービス アカウントが表示されます。[GRANT ACCESS (アクセスの許可)] をクリックし、[Release manager (リリース マネージャー)] のロールを選択して、[ADD USER (ユーザーの追加)] をクリックします。

注: [Role (ロール)] ドロップダウンから [Project Lead (プロジェクト リード)] を選択する方法もあります。[Release Manager (リリース マネージャー)] を選択すると、本番トラックとその他のすべてのトラックにアクセスできるようになります。[Project Lead (プロジェクト リード)] を選択すると、本番トラックを除くすべてのトラックを更新できるようになります。

これでキーを取得できたので、プロジェクトの fastlane フォルダーに貼り付けます。貼り付けたら、ファイル名を api.json に変更します。こうすることで、ファイル名とディレクトリが fastlane のセットアップ時の設定と一致します。

注: このファイルには Play ストア アカウントのシークレットが含まれているため、プライベート リポジトリでのみ使用してください。

fastlane のフォルダーには Fastfile という名前のファイルがあります。このファイルを使用して、fastlane で実行可能なタスクを構成します。デフォルト ファイルには、次の 3 つのブロックがあります。

  • before_all: レーンの実行前に実行する指示を挿入します。
  • lane: 実行したい実際のタスク (Play ストアへのデプロイメントなど) を定義します。レーンは必要なだけ定義できます。
  • after_all: レーンの実行に成功すると、このブロックが呼び出されます。
  • error: 他のブロックでエラーが発生した場合、このブロックが呼び出されます。

Fastfile には playstore という名前のレーンが 1 つ付属しています。このレーンは、リリース ビルドを生成し、Play ストアにデプロイします。このレーンに少し手を加えましょう。Fastfile を開き、playstore を次のように変更します。

lane :playstore do
 gradle(
   task: 'bundle',
   build_type: 'Release'
 )
 upload_to_play_store skip_upload_apk:true 
end

注: playstore レーンが含まれていない場合は、作成してから上のコードを追加します。

ここでは、タスクを bundle に更新しています。これで、fastlane は apk の代わりに aab (Android アプリ バンドル) をビルドできるようになります。aab は Play ストアにアーティファクトをアップロードするときの推奨フォーマットです。さらに、upload_to_play_store コマンドに skip_upload_apk:true パラメーターを追加しています。これにより、apks がある場合には廃棄され、生成された aab のみを使用するようになります。

アプリのデプロイメントに向けて準備する

次に、アプリにいくつかの変更を加えて、デプロイできるように準備します。まず、app-module の build.gradle ファイルにリリース署名設定ファイルを追加します。これにより fastlane は、以前にアプリをリリースしたときに生成したキーストアを使用して、他のキーストアを生成できるようになります。

app-module の build.gradle に以下のように signingConfigs を追加します。

android {
    signingConfigs {
        release {
            keyAlias 'key0'
            keyPassword 'keyPassword'
            storeFile file('../KeyStore')
            storePassword 'storePassword'
        }
    }
    // Leave the rest untouched...
}

注: キー パスワードとストア パスワードを実際のパスワードに変更します。また、キー エイリアスとキーストア名も実際のものに変更します。このスニペットの場合、キーストアはプロジェクトのルート ディレクトリに格納されています。アプリケーションのキーストアの生成についてお困りの場合は、こちらを参照してください。

次に、build.gradle ファイルの buildTypes セクションを更新します。

buildTypes {
    release {
        signingConfig signingConfigs.release
        // Leave other parts untouched...
    }
}

これで、特定のキーストアを使用するようにアプリを構成できました。次に、build.gradle ファイル内に関数を作成し、アプリ バージョンのビルド番号を生成できるようにします。app-module の build.gradle ファイルの android セクションの直前に次のコードを追加します。

ext.versionMajor = 1
ext.versionMinor = 0
ext.versionPatch = 1
ext.versionClassifier = null
ext.isSnapShot = false
ext.minSdkVersion = 21

private Integer generateVersionCode() {
    return ext.minSdkVersion * 10000000 + ext.versionMajor * 10000 +
            ext.versionMinor * 100 + ext.versionPatch
}

private String generateVersionName() {
    String versionName = "${ext.versionMajor}.${ext.versionMinor}.${ext.versionPatch}"

    if (ext.versionClassifier == null) {
        if (ext.isSnapShot) {
            ext.versionClassifier = "SNAPSHOT"
        }
    }

    if (ext.versionClassifier != null) {
        versionName += "-" + ext.versionClassifer
    }

    return versionName
}

このスニペットでは、アプリのバージョンの値を格納する変数を追加しています。次に、アプリのバージョンの値の変化に応じてバージョン コードとバージョン名を生成するメソッド generateVersionCodegenerateVersionName を追加しています。

これによりアプリは、アプリのバージョンが変更されたときに、新しいユニークな方法でバージョン コードを生成できるようになります。build.gradle ファイルの defaultConfig セクションにある以下のプロパティを更新します。

defaultConfig {
    versionName generateVersionName()
    versionCode generateVersionCode()
    // ... Leave others untouched

}

次に、CircleCI 設定ファイルを更新して、実行する fastlane コマンドを指定できるようにします。config.yaml ファイルを開いて、以下のステップを追加します。

# .circleci/config.yaml
- run:
    name: Install fastlane
    command: bundle install
- run:
    name: Execute fastlane
    command: bundle exec fastlane playstore

ここでは、2 つのコマンドを使用しています。1 つは fastlane をインストールするコマンド、もう 1 つはビルドを生成し、Google Play ストアに送信するコマンドです。アプリを正常にアップロードするには、Play ストア アカウントにアプリのパッケージ名 (この例では dev.idee.cicdandroid) が既に存在する必要があります。まだアプリをアップロードしていない場合は、このチュートリアルに従ってアップロードしてください。

これで、新しいコードをリポジトリにプッシュすると、アプリがストアに配信されるようになりました。

まとめ

この記事では、継続的インテグレーションと継続的デプロイメントの基本事項について取り上げ、シンプルなデモ アプリの作成を通じて、fastlane を使用したデプロイメントの方法を解説しました。上記の内容に沿ってセットアップすれば、アプリを自動的にテストし、Play ストアに直接デプロイできるようになります。しかしこれは始めの一歩に過ぎません。CircleCI と fastlane を活用してできることはまだまだたくさんあります。

いろいろ試して、開発ライフをお楽しみください。


Idorenyin Obong はモバイル開発を専門とするソフトウェア エンジニアです。ソフトウェアに関するさまざまな記事を書くことを心から楽しんでいます。