Circle CIでCloud Functionをデプロイする
以前に Circle CIでCloud Functionをデプロイする - GeekFactory というエントリを書きましたが、gcloudコマンドでデプロイする方が簡単でした。gcloudコマンドでデプロイする手順を説明します。
GCPの設定
デプロイに使用するサービスアカウントを作成します。ロールは Project Editor を割り当てておきます。Cloud Functionsはまだベータ版なので専用のロールが用意されていないようです。
サービスアカウントを作成したら秘密鍵をダウンロードします。後からCircleCIで設定できるように、BASE64でエンコードしておきます。
base64 service-account-xxxxxxxx.json
次に、デプロイで使用するストレージバケットを作成します。名前は何でも構いません。
CircleCIの設定
下記の内容で .circleci/config.yml
を作成します。
version: 2 jobs: build: docker: - image: google/cloud-sdk working_directory: ~/repo steps: - checkout - run: echo "$GOOGLE_AUTH" | base64 -i --decode > "$HOME/gcp-key.json" - run: gcloud auth activate-service-account --key-file "$HOME/gcp-key.json" - run: gcloud --quiet config set project "$GOOGLE_PROJECT_ID" - run: gcloud beta functions deploy 関数名 --stage-bucket バケット名 --trigger-http
gcloud beta functions deploy
コマンドの使い方は Deploying from Your Local Machine | Cloud Functions Documentation | Google Cloud Platform を参照してください。
下記の環境変数を設定します。
ビルドが成功すればデプロイ完了です。
Spring Security OAuthにおけるアクセストークン取得失敗時の例外
Spring Security OAuthでアクセストークンの取得に失敗した場合のリトライを設計するため、エラーケースと例外の対応を調べてみました。
アクセストークンの取得をリトライするには以下の2つの方法があります。
- アクセストークン取得(
AccessTokenProvider#obtainAccessToken
)をリトライする。 ClientHttpRequestInterceptor
でHTTPリクエストをリトライする。
前者の場合、失敗の原因にかかわらず OAuth2AccessDeniedException
が発生するので、 OAuth2AccessDeniedException
に対してリトライを行えばOKです。後者の場合は下記を参考にしてください。(7/28追記)
以下のテストケースに記載があります。
テストケースからは下記の仕様が読み取れます。
エラーケース | 例外 |
---|---|
ステータスコード400で "{"error":"access_denied"} が返された場合 |
UserDeniedAuthorizationException |
ステータスコード403で "{"error":"access_denied"} が返された場合 |
OAuth2AccessDeniedException |
400番台のステータスコードが返された場合 | HttpClientErrorException |
500番台のステータスコードが返された場合 | HttpServerErrorException |
これだけでは足りないので実際に検証した結果が下記です。
エラーケース | 例外 |
---|---|
接続に失敗した場合 | ConnectException |
Content-Typeとリクエストボディが合致しない場合 | OAuth2AccessDeniedException |
上記を踏まえると、リトライの対象例外は ConnectException
と HttpServerErrorException
にするとよさそうです。リトライの実装方法は下記で書いているのでご参考まで。
Jenkinsでテストケースを並列実行して所要時間を短縮する
プロダクトが成長するにつれてCIの所要時間が長くなる悩みを抱えている方は多いと思います。本稿では、テストケースの並列実行でスローテストを乗り越える方法を検討します。
CIの所要時間を短縮する戦略
CIの所要時間が長くなる主な原因はテストです。だがしかし、テスト以外にコンパイルやスキーママイグレーションなども時間がかかります。まずは、ビルドレポートで何のタスクに時間がかかっているか調べましょう。Gradleの場合は --profile
オプションで各タスクの所要時間を確認できます。
ここではテストの所要時間にフォーカスします。
テストの所要時間を短縮するにはいくつかの方法がありますが、筆者のおすすめは以下を順番に行うことです。
- テストレポートでテストケースの所要時間を確認し、長時間のものから改善していく。
- テストケースを並列実行する。
- 高価なインスタンスタイプに変える。(札束で殴る)
改善に取りかかる前に、異常に時間のかかっているテストケースがないか確認することをおすすめします。テストデータを作成するためにauto commitで数千件のINSERTを回していたとか、大量の一時ファイルに書き込んでいたけど実は不要だったとか、改善できるものが見つかるかもしれません。
それでも打ち手がなくなった場合はテストケースの並列実行を検討しましょう。すでにJenkins Agentを複数ノードで運用しているのであれば、設定を変えるだけなので簡単です。これからJenkins Agentノードを増やす場合はインスタンスタイプも見直してみてもよいでしょう。金で解決できるなら早いもんです。
Jenkins Agentを複数ノードで運用する
テストを並列実行するには、まずJenkins Masterに複数のJenkins Agentを登録する必要があります。この時、テストの実行時にどのJenkins Agentか識別できるように、環境変数 JENKINS_AGENT_ID
を設定しておきます。
実行環境 | 環境変数 JENKINS_AGENT_ID の設定値 |
---|---|
Jenkins Agent #1 | 1 |
Jenkins Agent #2 | 2 |
Jenkins Agent #3 | 3 |
Jenkins Agentサーバは運用管理が面倒なので、プロビジョニングを自動化してImmutableにしておくとよいでしょう。不要になったら削除、必要になったら起動ができると理想です。
リソースの競合を避ける
データベースに依存するテストケースを同時に実行すると競合してしまうため、Jenkins Agentごとに独立したデータベースを割り当てる必要があります。CIの所要時間を考えると、1つのデータベースサーバにあらかじめ複数のデータベースを作成しておくとよいでしょう。例えば、下記のように割り当てます。
実行環境 | データベース接続先 | データベース名 |
---|---|---|
ローカルでテスト実行 | localhost |
example0 |
Jenkins Agent #1 でテスト実行 | RDS | example1 |
Jenkins Agent #2 でテスト実行 | RDS | example2 |
Jenkins Agent #3 でテスト実行 | RDS | example3 |
MySQLなら下記で新しいデータベースを作成できます。
CREATE DATABASE example0; GRANT ALL ON example0.* to 'foo'@'%' identified by 'bar';
Spring Bootであれば application-test.yml
で環境変数を使ってデータソースを設定できます。
spring: datasource: url: "jdbc:mysql://${test.db.host:localhost}/${test.db.schema:example0}?useSSL=false" username: "foo" password: "bar" driver-class-name: com.mysql.jdbc.Driver
上記のように書くと、ローカルでは mysql://localhost/example0
、Jenkins Agentでは環境変数 TEST_DB_HOST
と TEST_DB_SCHEMA
の値を使うことができます。
最後に、Jenkinsfileでテストの実行時に環境変数を渡すように設定します。
def rdsEnv() { assert env.JENKINS_AGENT_ID [ "TEST_DB_HOST=xxxxxxxx.xxxxxxxx.ap-northeast-1.rds.amazonaws.com", "TEST_DB_SCHEMA=example${env.JENKINS_AGENT_ID}", ] } node { checkout scm stage('test') { withEnv(rdsEnv()) { sh './gradlew check' } } }
データベースだけでなく、Redisなどのキャッシュや外部APIも同じです。
Jenkins Agentサーバにデータベースを立てる方法もありますが、運用管理が面倒になることや、同じサーバのCPU時間やI/O帯域を消費してしまうことから、マネージドサービスを利用する方がよいでしょう。
テストケースを分散させる
テスト設計やモジュール構成に依存しますが、一般には以下の方法があると思います。
- サブプロジェクトごとにテストケースを並列実行する。
- テストケースをパッケージ名やアノテーションでグループ化して、並列実行する。
ここでは1の方法を説明します。
Jenkinsfileでは parallel
ブロックで並列実行を定義できます。例えば、下記のように書くとGradleの api:check
タスクと batch:check
タスクを並列実行できます。
parallel( api: { node { checkout scm stage('test') { withEnv(rdsEnv()) { sh './gradlew api:check' } } } }, batch: { node { checkout scm stage('test') { withEnv(rdsEnv()) { sh './gradlew batch:check' } } } }, }
ごちゃごちゃするので省いていますが、実際にはテスト実行後にテストレポートを回収するタスクが必要です。下記を finally
で囲んで入れておくとよいでしょう。
junit allowEmptyResults: true, testResults: 'api/build/test-results/test/*.xml' publishHTML([allowMissing: true, alwaysLinkToLastBuild: true, keepAll: false, reportDir: 'api/build/reports/jacoco/test/html', reportFiles: 'index.html', reportName: 'Coverage (api)']) publishHTML([allowMissing: true, alwaysLinkToLastBuild: true, keepAll: false, reportDir: 'api/build/reports/tests/test', reportFiles: 'index.html', reportName: 'Test (api)'])
まとめ
本稿では、Jenkinsでテストケースを並列実行し、CIの所要時間を短縮する方法を説明しました。
- リソースの競合を避ける(データソース、キャッシュ、APIなど)
- テストケースを分散させる(サブプロジェクト、パッケージ名など)