つばくろぐ @takamii228

知は力なり

アプリケーションが依存するFWやライブラリからアップデート戦略を考える

最近はFlutterでiOS / Androidアプリ開発をしているのですが、ある程度の頻度でプラットフォームやライブラリのアップデートに追従していかないとあっという間に取り残されてしまう空気を感じています。

ネイティブのOSでいうとちょうど先日Android 11のBetaがリリースされたり、来週WWDCiOS 14が発表されたりと、年に一回は必ずアップデートがある領域ではあるのですが、Flutterはそれ以上に短いサイクルでアップデートが入っています。

Flutter自体はstableのリリース頻度を見ても3-4ヶ月に一回のペースで更新があるようです。

flutter.dev

このようにあるFWを使ってアプリを作ってサービスを継続的に提供していく場合、そのFWのバージョンアップにどうやって追従していくべきかなんとなく考えてみたのでまとめてみます。

追従していくおおまかな流れ

だいたいこんな感じで考えればよいのではないか、というのを以下にまとめてみました。

  1. アプリケーションが依存するものを洗い出す
  2. 依存するものの更新ライフサイクルを確認する
  3. FW/ライブラリのバージョンアップ or リプレースを計画する

今回はFlutterを例に順番に考えてみます。

1.アプリケーションが依存するものを洗い出す

まずアプリケーションが構成しているものを順番に洗い出して、それを要素分解して依存関係を明確にしていきます。

洗い出す観点は以下のとおりです。

  • プログラム・設定ファイル
  • SDK・ライブラリ
  • 実行環境
  • 開発環境

Flutterの場合はまず大きく①FlutterのDart部分、②Androidのネイティブ部分、③iOSのネイティブ部分に分けられます。

f:id:takamii228:20200621204050p:plain

①FlutterのDart部分

FlutterのDart部分はFlutterのSDKに含まれるPackagesを参照する部分と、Pluginをimportする部分があります。

Flutter Pluginは参照するときはpubspec.yamlに記述するだけですが、中身は入れ子でFlutterプロジェクトになっていてDartソースはもちろんAndrodやiOSのネイティブコードを含んでいるものがあるので注意が必要です。

開発にはAndroid StudioAndroid StudioのFlutter Pluginを使います(VS CodeIntelliJのケースもあるでしょう)。

f:id:takamii228:20200621204244p:plain

Androidのネイティブ部分

Androidのネイティブ部分はKotlin / JavaのコードやJAR / AARライブラリ、Android SDKのライブラリやGraldeプラグインからなります。ビルドにはGradleが利用され、build.graldeに設定が記述されます。そもそもAndroidにはJDKが必要ですね。

f:id:takamii228:20200621204548p:plain

iOSのネイティブ部分

iOSのネイティブ部分はSwfit / Objective-Cと、CocoaPodsからimportするライブラリ、XCodeのFrameworkなどからなります。ビルドはXCodeに内包されるSDKで実行されます。

f:id:takamii228:20200621204755p:plain

全体像

①ー③を1枚絵にすると以下のようになります。

f:id:takamii228:20200621204848p:plain

依存するもののアップデート頻度を確認する

洗い出したコンポーネントをカテゴリ別に分類し、更新頻度や依存するものをまとめておきましょう。

項目名 カテゴリ 依存定義場所 更新頻度 依存するもの
XCode 開発環境 開発マシン上 メジャーバージョンは年1回
マイナーバージョンは3-4ヶ月に1回
Mac OS
Android Studio 開発環境 開発マシン上 3-4ヶ月に1回 -
Flutter SDK 開発FW 開発マシン上
pubspec.yaml
3-4ヶ月に1回 Android Studio
XCode
CocoaPods
Android SDK 開発FW build.gralde
AndroidManifest.xml
年に1回 Android Studio
Gradle
iOS SDK 開発FW XCodeプロジェクト設定 3-4ヶ月に1回 XCode
Flutterプラグイン 外部ライブラリ pubspec.yaml ライブラリに依存 Android SDK
iOS SDK
Flutter SDK
Android外部ライブラリ 外部ライブラリ build.gradle ライブラリに依存 Android SDK
Gradle
依存ライブラリ
iOS外部ライブラリ 外部ライブラリ Podfile ライブラリに依存 iOS SDK
CocoaPods
依存ライブラリ
Dart Programs ソースコード - SDKに依存 Flutter SDK
依存ライブラリ
Kotlin Programs
(Java)
ソースコード - SDKに依存 Android SDK
Kotlin Version
JDK
Swift Programs
(Objective-C)
ソースコード - SDKに依存 iOS SDK
Swift Version
XCode

Flutterの場合はFlutterプラグインの中で依存関係があったり、更にAndroidiOSのソースが入っていてSDKを参照してたりするのでより注意が必要です。

FWのバージョンアップ、ライブラリのバージョンアップ or リプレースを計画する

上記の表を並べて見てみると大きなアップデートサイクルとして1年単位のOSメジャーバージョンアップとFWのバージョンアップが4半期に一回程度あるので、少なくともこれらを考慮してバージョンアップを計画しておけばよさそうです。

細かい外部ライブラリのパッチやバグ改修には、もしかしたらFWのアップデートが必要かもしれません。また外部ライブラリのみのパッチであれば月次単位でのアップデートでもよさそうです。

OSのメジャーバージョンアップ対応やフレームワークのバージョンアップは影響が広範囲に及ぶため、マイナーバージョンアップと比べて修正箇所が多くなる可能性があるため作業量を多めに見積もっておきましょう。

いずれにしても、どいういう時にどのバージョンアップを計画しておくのか、事前に決めておくことが大事です。決して据え置きにして蓋をすることはやめましょう。あとになって必ず負債化することになります。

アップデートを安全に行うために

更新のバッチサイズを小さくする

フレームワークのバージョンアップを控えていると、それに合わせてライブラリもバージョンアップしておけばいいや、となりがちです。

しかし、アプリケーションのバージョンアップは広範囲であればあるほど影響範囲が広くなり、変更箇所が多くなって対処に時間がかかります。

なのでバージョンアップの回数分コストが発生するためこまめに複数回やるか、一括で一気にやるかとのトレードオフにはなりますが、影響範囲が狭い範囲でバッチサイズを小さくした細かいバージョンアップを定期的に行っておくほうがリスクを低く抑えられるでしょう。

E2Eテストを用意する

FWをバージョンアップするときに、アプリケーションの振る舞いが変わっていないかをFWのDiffから追うことは結構大変です。

FlutterのRelease Noteを見ると、破壊的な変更は説明があるものの、細かいPRを一つ一つ追っていくのはかなり大変です。

flutter.dev

このとき、アプリケーションの外から見た振る舞いをEnd-to-Endテストで記述して自動で実行できるようにしておくと、バージョンアップで壊れている箇所がないかをすぐに確認することができます。また失敗するテストから修正が必要な箇所をすぐに特定できるようにもなります。ただし、重厚に全機能を網羅するテストを用意するのはメンテナンスコストとのトレードオフにもなるため、まずは正常系の画面遷移のパスを網羅する程度のものを準備しておくとよいでしょう。Flutterの場合はFluter drive testでE2Eテストが記述できます。

アップデートに追従しにくいライブラリはリプレースできないか考える

FWの頻繁な更新に対応しようとすると、場合によっては3rdパーティによるメンテナンスが追いつかないケースもあります。そういう場合はFWの更新と天秤にかけた意思決定が必要です。場合によっては違うライブラリに置き換えたり、自前で作成してしまうなどして依存関係を見直すようにしましょう。

プラグインの選定方法に関しては以前記事をまとめたのでこちらも参考にしてみてください。

takamii.hatenablog.com

この段階でいろいろ考えてみてどうしても負債が解消できないとなった場合は、FWを再選定したりアプリケーションを大々的に作り変えることも選択肢の一つになるかもしれません。

アプリケーションの外の依存関係も明らかにしておく

今回はアプリケーション開発・更新にフォーカスしていますが、サービス提供という一つ上の目線で見てみると、その周りにあるものも依存するものとして捉えることができます。単純にヒト・モノ・カネの観点でいうと、関わっているエンジニアや委託している外部ベンダ、マシンや計算リソース、予算管理などが該当します。また社内の運用ルールなども含めて、起こりうる変更に備えておけばよりアジリティ高く変化に対応できるようにできそうです。

まとめ

プラットフォームやフレームワークを選定し外部に公開されているライブラリと組み合わせてアプリケーションを構成するときは、上記のようなアップデートライフサイクルを考慮した開発方針を事前に決めて追加開発の中で計画的に更新していくことで、技術的な負債の発生を未然に抑止し、アプリケーションを継続的に安定稼働させることができるようになるでしょう。初期リリース偏重で、アップデート戦略なしに小手先の末端のソースコードを継ぎ足すだけの機能開発を続けていると、あっという間にコアな部分で負債が発生する未来がやってきてしまいます。

一昔前のSpringやAngularJS、LaravelのLTS版を見てみると数年単位で塩漬けにしていても安泰ではありましたが、今はもうそのような世界はありません。Javaのバージョンアップライフサイクルに代表されるように、ライブラリやFWの寿命が昔に比べて短命になっていると感じます。

このように作って終わり、あとは塩漬けの時代は終わりつつあるので、継続的に依存ライブラリやFWをどう更新してくか、アプリケーション開発の初期段階できちっと考えておきたいものです。

APKファイルとIPAファイルのバージョン情報をコマンドラインから確認する

apkファイルとipaファイルのバージョン情報をコマンドラインで確認する方法を備忘録として残しておきます。

apkとipaのバージョン情報について

apkファイル及びipaファイルのバージョン情報は、ストア申請のときのバージョニング情報として利用されるものです。それぞれ公式に詳細が記載されています。

両OSともにx.y.zというバージョンの文字列と、バージョンの一意性を表す数字が定義されています。微妙に呼び名が異なるので注意が必要です。

OS バージョン名 ビルド番号
Android versionName versionCode
iOS version number build string

versionNameとversin numberがストアで表示されるときのバージョン番号ですね。

apkのversionNameとversionCodeをコマンドラインから確認する

apkのバージョン番号とビルド番号を確認するのは、Android Studioでapk analyzerにかければすぐにわかりますが、ここではコマンドラインを使う方法を紹介します。

コマンドとしてはAndroid SDKに含まれるaapt2コマンドを使うことでAndroidManifest.xmlの情報が取得できます。それをgrepして加工してあげればほしい情報が出力できます。

developer.android.com

$ aapt2 dump badging sample.apk | grep "versionName" | sed -e "s/.*versionName='//" -e "s/' .*//"
1.2.3
$ aapt2 dump badging sample.apk | grep "versionCode" | sed -e "s/.*versionCode='//" -e "s/' .*//"
100

ipaのversion numberとbuild stringをコマンドラインから確認する

ipaについてはBitriseが公開しているipa_analyzerを使うのが便利です。

github.com

ipaのバージョン情報・ビルド番号はipaが持つinfo.plistの中のCFBundleShortVersionStringおよびCFBundleVersionに記載されています。

ipa_analyzerを使うとinfo.plistの値をjson形式で情報を出力してくれるので、jqコマンドでフィルターをかければよいでしょう。

$ ipa_analyzer -i sample.ipa -p --info-plist --prov | jq -r '.info_plist | .content | .CFBundleShortVersionString'
1.2.3
$ ipa_analyzer -i sample.ipa -p --info-plist --prov | jq -r '.info_plist | .content | .CFBundleVersion'
100

CIマシンでバージョニングを自動化するときに、正しくバージョニングできているかを上記のコマンドを使って確認するとよいでしょう。

Flutterで使うAPI関連のDartコードをSwagger/OpenAPIで自動生成する

はじめに

Flutterの開発の中でSwagger/OpenAPIのYamlからコードを自動生成して使うことがあったのでまとめておきます。

Swagger/OpenAPIとは

OpenAPIはREST APIの仕様を記述するフォーマットで、yaml形式で仕様を定義することができます。

2.0系と3.0系があって、3.0が出てからしばらく立ちましたが、API Gatewayはまだ2.0系のみサポートしているところが多い印象です。

www.openapis.org

swagger.io

SwaggerとOpenAPIの関係は少しややこしいようで、加えて2.0系と3.0系で仕様に差分があるため、ここでは2.0系の形式を扱うこととします。

Swagger/OpenAPIには以下の3つのツールがあります。

今回はSwagger/OpenAPIのyamlを使ってAPI仕様書を見る手順と、Flutterで使うDartのコードを生成する手順をまとめます。

Swagger UIでAPI仕様書を見る

Swagger UIはDocker Imageが用意されているので、yamlを連携してあげれば簡単に起動することができます。

API_URLでデフォルトで読み込むyamlを指定できるので、マウントした領域を読み込んだり、ネットワーク越しにしていすればよいでしょう。

$ docker run -d -p 81:8080 -v $HOME/git/swagger-dart-client/:/usr/share/nginx/html/yaml \
    -e API_URL=yaml/petstore.yaml swaggerapi/swagger-ui

http://localhost:81 にアクセスすれば確認できます。

f:id:takamii228:20200506121406p:plain

Swagger CodegenDartのコードを自動生成する

FlutterでAPIアクセスをして画面を描画する時、APIのモデルデータの一式を定義するのは大変です。

PHPJavaScriptであれば単なるArrayのMapで処理するだけなのですが、JavaDartではきっちり型定義が必要なのでList型や入れ子JSONを含む複雑なレスポンスを返すAPIの場合は定義がとても面倒です。

加えて、特にフロントエンドとバックエンドが並行して新規開発するケースでは、やっとの思いでモデルクラスを定義しきれたとしてもAPIの仕様が流動的なため頻繁な変更が入ります。Typoでミスが入り、バックエンドと繋ぐときにIFの不整合が多発する要因にもなります。

そこでSwagger/OpenAPIのyamlで仕様を定義し、クライアントコードを自動生成するという戦略をとることでその手間やリスクを減らすことがことできます。

ツールをインストールする

コードの自動生成ツールをは、Swagger/OpenAPIそれぞれ用意されているのですが今回はOpenAPIの方を利用します。

インストールは公式のガイドに従っていけばよいです。

openapi-generator.tech

npm、homebrew、docker、jar、bashの5種類用意されています。

それぞれが利用しやすいものを選べばよいと思いますが、今回はjarを利用することにします。

API定義のYamlを記述する

まず YamlAPIの仕様の肝となり、プロダクションで利用するコードの自動生成にも利用するものなので、フロントエンドとバックエンドをつなぐ重要な設計成果物であることをお互いに認識しましょう。

これが非常に重要です。

「ファイルサーバのExcelの方が最新です」ということにならないように、Gitのレポジトリで管理され、かつバージョニングされていることが望ましいです。

Yamlの記述についてはSwagger Editorを使えばSyntaxを確認できますが、yamlが巨大になってくるとブラウザが悲鳴を上げてくるのでcliで実行するとよいでしょう。

$ java -jar openapi-generator-cli.jar validate -i petstore.yaml 
Validating spec (petstore.yaml)
No validation issues detected.

Yamlを手動で記述するのはなかなか大変なので、記述しやすい別の設計ドキュメントから自動生成するようにしてもいいかもしれませんね(何とはいいません🙊🙊🙊)。

個人的にはバックエンドのソースコードからYamlを生成してもらえると実装と設計の一意性がより担保できると思ってます。

YamlからDartのコードを自動生成する

Yamlが出来上がったらあとはクライアントコードの自動生成をするだけです。

Flutterはdartで記述するのでdartのクライアントコードを生成するのですが、GeneratorのREADMEを読むとdartdart2とあります。

Flutterが利用するdartのバージョンは2.x系なのでdart2を利用すればいいのねと思ってコマンドを実行したら

$ java -jar openapi-generator-cli.jar generate -i petstore.yaml -g dart2 -o build
Can't load config class with name 'dart2'
...
[error] Check the spelling of the generator's name and try again.

となってエラーで失敗しました。まだコマンドラインツール上ではdart2は対応していないようです。

githubを見に行くとdart2用のテンプレートは用意されているようなのでテンプレートを指定する形で利用すればよさそうです。

github.com

テンプレートは-tオプションで指定することができます。

$ java -jar openapi-generator-cli.jar generate -i petstore.yaml -g dart -t template/dart2 -o build
[main] INFO  o.o.codegen.DefaultGenerator - Generating with dryRun=false
[main] INFO  o.o.c.ignore.CodegenIgnoreProcessor - No .openapi-generator-ignore file found.
[main] INFO  o.o.codegen.DefaultGenerator - OpenAPI Generator: dart (client)
[main] INFO  o.o.codegen.DefaultGenerator - Generator 'dart' is considered stable.
[main] INFO  o.o.c.languages.DartClientCodegen - Environment variable DART_POST_PROCESS_FILE not defined so the Dart code may not be properly formatted. To define it, try `export DART_POST_PROCESS_FILE="/usr/local/bin/dartfmt -w"` (Linux/Mac)
[main] INFO  o.o.c.languages.DartClientCodegen - NOTE: To enable file post-processing, 'enablePostProcessFile' must be set to `true` (--enable-post-process-file for CLI).
[main] INFO  o.o.c.languages.DartClientCodegen - Dart version: 2.x
[main] INFO  o.o.codegen.DefaultGenerator - Model Pets not generated since it's an alias to array (without property) and `generateAliasAsModel` is set to false (default)
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/lib/model/error.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/test/error_test.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/doc/Error.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/lib/model/pet.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/test/pet_test.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/doc/Pet.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/lib/api/pets_api.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/test/pets_api_test.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/doc/PetsApi.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/pubspec.yaml
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build//lib/api_client.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build//lib/api_exception.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build//lib/api_helper.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build//lib/api.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build//lib/auth/authentication.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build//lib/auth/http_basic_auth.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build//lib/auth/api_key_auth.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build//lib/auth/oauth.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/git_push.sh
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/.gitignore
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/README.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/.travis.yml
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/.openapi-generator-ignore
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/.openapi-generator/VERSION

生成コードをカスタマイズする

OpenAPIを使ったコード生成では何も指定しないとAPIクライアント、ドキュメント、テストコードなどが一式生成されます。

モデルクラスのみあればよいというケースでは、オプションを設定することで自動生成をスキップできます。

$ java -jar openapi-generator-cli.jar -DbrowserClient=false -DapiTests=false -DmodelTests=false \
  generate -i petstore.yaml -g dart -t template/dart2 -o build

詳細は Selective Generation を見るとよいでしょう。

また自動生成されたコードがDartのLintルールに沿ってない場合は、テンプレートのmustacheファイルをいじってルールに沿うように修正するとよいです。もしテンプレートをいじる場合は忘れずにテンプレートファイルをバージョン管理しましょう。

一式の生成の流れがまとまったらレポジトリにまとめたり、API仕様書が変更されたときにFlutterのプロジェクトへどう修正を取り込むのかのパイプラインを考えるとよいでしょう。

Flutterの場合はpubspec.yaml経由でバージョンを指定してimportすることもできるようです。やや古いですが、こちらの記事が参考になります。

まとめ

以上がSwagger/OpenAPIのYamlからコードを生成する流れになります。githubにサンプルもまとめておきました。

頻繁に変更されるAPI定義のモデルクラスの修正は、単純でありつつもミスがゆるされない作業になるため、自動生成の仕組みを上手く利用して効率化するとよいでしょう。

github.com

おまけ

Swaggerを使ったコードやドキュメントの自動生成の話はint128先生のJavaのSpringを使った例も参考になるので見てみてください。

Swaggerのテンプレートを魔改造した話 / Customize Swagger Templates - Speaker Deck