こんにちは、ピリカ開発チームの九鬼です。
ごみ検出モデルをエッジAIで動かすにあたり、Jetson Orin Nano開発者キットでいくつかセットアップしました。その中でトラブルのあったポイントがいくつかあったので、その内容とセットアップ方法を共有いたします。
続きを読むこんにちは、ピリカ開発チームの九鬼です。
昨年秋、Firestoreのバックアップのプレビュー版が提供され始めました。費用・管理面で日々のバックアップで利点があったため、導入した話を共有いたします。
続きを読むこんにちは、SNSピリカ開発チームの冨田です。
今年の1月にAPIサーバをPython3に移行するプロジェクトを完遂しました。
本プロジェクトは、SNSピリカ開発チームのメンバーはもちろん、それ以外のメンバー、業務委託で一時的に関わってくださった方々、テストで関わってくださった方々、すでに退社された方々など、たくさんの方々の知恵が詰まっています。
SNSピリカ1は、2011年から稼働しているサービスです。従来APIサーバはAppEngine/Python2.7上で稼働していました。
Python2.7は2020年初にPython公式のサポートが終了しました。ピリカでも少しずつマイグレーションを進めていましたが、ビジネス上の理由から他の開発に時間と人員を割かなくてはならず、マイグレーションはあまり進められずにいました。 そんな中、2024年1月末のサポートの終了がアナウンスされ、2023年7月よりプロジェクトとして進めることになりました。
結果、無事2024年1月上旬に、無事無停止でリリースすることができました。
移植作業は、大まかには3つのフェーズに分けられます。
全体として、1つのAppEngineだったものをそれぞれ用途ごとに異なるアプリケーションにしました。
システムが巨大な一方、2~3名体制の中で移植を取り組むメンバーの確保が難しかったため、少しずつ移行する方法を検討しました。
そこで、既存のPython2.7のAPIとは別に、新しいPython3で書かれたAPIのアプリケーションを立ち上げ、クライアントからのアクセスはPython3へ、未移植であればPython3 API → Python2.7のAPIにリダイレクトさせるようにしました。
SNSピリカは、AppEngineにデプロイされています。
AppEngineは以下のような特徴があります。
移植前Phase1の時点では、defaultサービスにランタイムがPython2のAPIがデプロイされていました。
クライアントから、こちらのAPIにGET example.com/users
のようにルートでアクセスされていました。
サービスごとに1つのランタイムのみを持てるので、ランタイムがPython3の別のAppEngineのサービス (python3-api
) を立ち上げて、APIのルートを/api
のようにルーティングすることにしました。
このようなルーティングは、AppEngineのdispatch.yamlで以下のように設定することで可能です。
dispatch: - url: "*/api/*" service: python3-api
Python3のAPIの処理では、移植前の最初の段階では、そのままルートのPython2のAPIにリダイレクトさせました。
つまり、この時点では、「GET example.com/api/users
-> Python3のAppEngine -> GET example.com/users
-> Python2のAppEngine」のようにルーティングされています。
これにより、クライアントからのアクセスはPython3のAPIにルーティングされますが、実際の処理はPython2のAPIにリダイレクトされるようになりました。
その後、API単位で移植をし、リダイレクトさせるのではなく、Python3のサーバーからレスポンスを返すようにしました。
この方法であれば、API単位で分割して実装・リリースができるようになりました。
また、この方法であれば、クライアントから直接ルート (Python2のサーバー) へのアクセスも残しつつ、新しいパス/api
へのアクセスは新しいPython3のサーバーへ同時にアクセスできるということも可能になりました。
SNSピリカにはiOSとAndroid版があり、これらのアップデートが行き渡るのには時間がかかり、ルートへの直接アクセスも一定期間は必要になるため、ここも大きなポイントでした。
デメリットとしては、移植が完了するまではPython3のサーバーからPython2のサーバーにリダイレクトされることになるので、レイテンシが高く、費用も高くなってしまうことでした。これは移行期間中は仕方のないこととして許容することにしました。
今回、Python2のAppEngine(第1世代)からPython3のAppEngine(第2世代)に機能がなく、代替サービスへの移行が必要な機能がいくつかありました。(公式のドキュメント)
それぞれの移行先は以下の通りです。
最初に、queueとcronの移行を行いました。 Python2のサーバー内の呼び出し箇所を、Cloud TasksのAPI・Cloud SchedulerのAPIに変更しました。
また、CloudTasksの非公式のエミュレーターであるcloud-tasks-emulatorを導入し、ローカルでの開発を行えるようにしました。
memcacheはPython2では引き続き使用し、Python3ではCloud Memorystore for Redisを使用することにしました。 (後述しますが、これがPhase2で課題となりました。)
なお、Cloud Memorystore for Redisは最低費用でも30ドル程度と費用が高かったため、本番環境のみ利用しました。 それ以外の環境ではGoogle Compute EngineのプリエンプティブルインスタンスにRedisをインストールして使用するようにしました。
プリエンプティブルインスタンスは安価な分、Googleが強制的にインスタンスを停止することがある仮想マシンサービスです。
そのため、インスタンスが落ちていないかを確認し、落ちている場合は再起動させる下記のFunctionsを日中10分ごとにSchedulerで定期実行させています。
import compute from "@google-cloud/compute"; const request = { project: "project-name", zone: "region-name-x", instance: "instance-name", }; export const startRedis = async () => { const client = new compute.InstancesClient(); const [instance] = await client.get(request); const vmStatus = instance.status; let started = false; if (vmStatus === "TERMINATED") { await client.start(request); started = true; } return { status: vmStatus, started, }; };
Python2のAPIは、1つのファイルに7000行以上のコードが書かれており、またLintも導入されておらず可読性が高くはありませんでした。そこで、API処理、ビジネスロジックの処理、外部サービスで処理レイヤーを分け、内部のロジックが外部サービスに影響しないようにすることを目指しました。
いくつかレイヤードアーキテクチャがある中で、ピリカではクリーンアーキテクチャを導入しました。2020当初クリーンアーキテクチャによる導入事例が各所で見られていたこと、またSNSピリカのAPIサーバの処理構造と整合することから決定しました。
具体的には、View, UseCase, Repository, Datastoreの4つのレイヤーに分け、それぞれの責務を明確にしました。
また、Pipfileを導入し、ライブラリやスクリプトを管理した他、flake8を導入し、Lintを行うようにしました。(最近はblack, isort, mypy等を導入して静的解析の品質をより向上しました)
Phase1でPython3のAPIを立ち上げ、Python2のAPIと共存させることができるようになりました。
ここからAPI単位で、少しずつマイグレーションを進めていきました。 キャッシュの共有ができずに、Python2・Python3で別のキャッシュが残る問題をPubSubを使って解決しました。
SNSピリカでは、以下のようにPython2のAPIではmemcacheを使用し、Python3のAPIではCloud Memorystore for Redisを使用していました。
このPython3のNDBではデフォルトでNDBキャッシュというものがあり、自動的にテーブルのキャッシュが行われていました。
つまり、Python2には明示的にキャッシュを設定していましたが、Python3には明示的に指定したキャッシュの他に、テーブルのキャッシュが存在していました。
途中まで実装してリリースしたところで、これだとPython2のAPIとPython3のAPIでキャッシュが共有されずに、別々のキャッシュが残ってしまう不具合に気づきました。 特に、NDBのキャッシュは、自動的にテーブルのキャッシュが行われるため、API全体的に意図せずにキャッシュが残ってしまっていることがわかりました。
例えば、ユーザーの表示APIをPython3のサーバーに移植し、ユーザーの編集機能はPython2に残したままにした場合、Python2のAPIでユーザーの編集を行い、その後Python3のAPIでユーザーの表示を行った場合、Python3のAPIではキャッシュが残っているため、編集前のデータが表示されてしまいました。
これはPython2のサーバーとPython3のサーバーを同時に稼働させていたために、起きた問題でした。
同じRedis・NDBを使用すれば問題はないのですが、Python2のAPIでは別のシステムであるmemcacheを使用しており、データベースのライブラリもNDBではなく別のAPIを使用して接続していたため、同じシステムに切り替えることができませんでした。
そこで、データベースの操作をしたときに、相互のキャッシュを削除するシステムを作成しました。
これにはCloud Pub/Subを使いました。
具体的には、以下のようなシステムを導入しました。
ちなみに、移植中はNDBキャッシュを停止する方法も検討しました。 実際に試したのですが、NDBキャッシュを停止すると、全体的に応答速度が0.5~1sほど増加したので、この方法は採用しませんでした。
SNSピリカには、Web版フロントエンドとは別に、見える化ページ2という別のWebサービスがありました。
これは、自治体や企業が、ごみ拾いの活動を行った際に、その活動を記録し、その記録を見える化するサービスです。 このサービスも同じPython2のAppEngineにデプロイされていました。
しかしながら、本サービスは2015年頃に初期開発していた関係で以下の技術的課題がありました。
などの問題がありました。そのため移植プロジェクトとは別に、このサービスを2021年ごろに別のサービスに移行しました。
結果、見える化ページはSNSピリカ本体から分離され、耐障害性が改善しました。
また、設定データにより柔軟にページをレイアウトできるようになり、スマホ等からも遜色なく閲覧できるよう改善しました。
Phase.2での足回り改善後、新規開発に集中していました。この関係で、2023年7月時点でPython2のAPIが7〜8割程度残っていました。2024年1月末でPython2のサポートが終了することを考えて、2023年7月にプロジェクトチームを立ち上げました。残り期間が6ヶ月と迫っている中、本格的に移行を進めることとなりました。エンジニアは約5名体制で、実装を進めるために業務委託メンバーの方にもお手伝いいただきました。
Python2のAppEngineにはAPIのほか、Web版フロントエンドとその配信APIもデプロイされていました。 これらはデフォルトのサービスに含まれていたため、ここまで移植せずにいました。 ただPython2のAppEngineを廃止する前、APIをすべてPython3に移植してからでなければ廃止できません・時間的制約もありこれらを同時に進める必要があったため、以下の順序でリリーススケジュールを決めました。
最初に、全体のインフラ構成を整理した詳細な企画書をメンバー全員で推敲しながら作成しました。 後述しますが、当時はApp EngineからCloud Runへの移行も検討していたため、その部分も含めた全体のインフラ構成をまとめました。
その後、スプレットシートで全てのAPIを一覧にし、難易度と工数をつけ、全てのAPIをタスクボードに落とし込みました。
タスクボードだと開発以外のメンバーが全体感が掴みにくいので、全社向けの進捗共有として、こちらのブログのスプレッドシートで全体のガントチャートも作成しました。
そこから、各APIを担当するメンバーを決め、1つずつ移植を進めていきました。 企画書は、移植が進むにつれて変更があったため、進捗に合わせて更新を行いました。
上記のように、時間が限られる中でPython2のAPIを完全に廃止し、全てのAPIを一気にマイグレーションすることになったため、安全に移行するためにも、厚めにテストを実施しました。
ユニットテストをかけるようにするために、公式のCloud Datastoreエミュレーターを利用し、アーキテクチャごと、特にテーブルの更新系の処理があるRepository層に対しては厚めにテストを書くようにしました。
システムテストに関しては、時間が限られており、マイグレーション後に十分なテスト期間が取れないことがわかっていたため、マイグレーションの実装と同時にシステムテストを行う必要がありました。 そこで、APIを機能ごとに分割し、機能ごとに移植を進め、機能の移植が終わると、その機能に対してシステムテストを行うという方法を取りました。 AppEngineは、バージョンをつけてデプロイすることができるため、バージョンを切り替えることで、移行前と移行後のAPIを切り替えることができました。 切り替え前のAPIを使用したアプリと、切り替え後のAPIを使用したアプリの2つを用意し、実機でテストしました。
全てのマイグレーションが終わったところで、シナリオテストも行いました。
テストを通して、不具合を事前に検知できたことはもちろんですが、以前からあった仕様バグもいくつか見つけることができました。
限られた時間・人員での対応だったため、一部利用頻度が低いものや、将来的に廃止を予定していた機能に関してはこのタイミングで廃止することにしました。
また、一部のAPIはPython2のAPIの時から不具合や仕様バグを抱えていました。それらがかえって移植を複雑にしている場合は、移植と一緒にAPI・アプリの修正も行いました。この際も、ユニットテストで正常な仕様・挙動を明確にしてから移植を進めていました。
Python2のAPIと同じく、Python2のdefaultサービスにWeb版フロントエンドの配信部分がデプロイされていました。 つまり、defaultサービスにはAPIとWeb版フロントエンドの両方がデプロイされていました。
このWeb版フロントエンドはAPIと同様にルートにアクセスがあり、どのAPIにも一致しない場合は、Web版のフロントエンドの配信API(Reactのindex.htmlにOGPを加えたり、HTMLを配信するAPI)にアクセスされていました。
またその上で、AppEngine特有の機能・制限がありました。 AppEngineは必ずdefaultサービスを持つ必要があり、以下のようにリクエストがルーティングされます。
SNSピリカのAppEngineにはAPIとWeb版フロントエンド以外にも、データベースを共有している別のサービスをデプロイしており、かつ、このサービスでは、検証用環境でAppEngineのデフォルトのURLを使用していました。
そこで、App EngineのデフォルトのURLを使用できるようにするために、以下のような方法を採用しました。
元々は、新しいPython3のAppEngineのサービス(web-frontendサービス)を立ち上げ、dispatch.yamlで/api
のパスを持たないリクエストをすべて受け取るようにすることを検討しました。
ただし、ここの場合は、AppEngineのデフォルトのURLである-dot-を使用できないため、dispatch.yamlで残りの全てのリクエストをweb-frontendサービスに流すことができないことに気づきました。
ルートのURLを利用する場合は、defaultのサービスを利用する必要があるようです。
他にも、いくつか案がありましたが、限られた時間の中で、安全に移行するためには、上記の方法が最適だと判断しました。
Python2のAPIの中には、運営メンバーが利用するadmin APIがありました。これはAppEngine/Python2のUsers APIを利用しており、権限を持つ社内のメンバーのみがアクセスできるようになっていました。
admin APIには2種類ありました。1つは運営メンバーがユーザーサポートを行うためのAPIで、もう1つは定期実行されるCloud Schedulerのハンドラーでした。
前者の運営メンバーがユーザーサポートを行うためのAPIは、社内管理画面にUIを含めて実装することにしました。 社内管理画面自体は、2022年ごろに別のプロジェクトで必要になり、同じAppEngineにCloud Load Balancing + Identity-Aware Proxyで作成してありましたので、そちらに新たにUI・APIを設置しました。
後者のCloud Schedulerのハンドラーは、Cloud Functionsに移行しました。
一気に2024年1月にマイグレーションをリリースするのはリスクだと考え、段階的なリリースを行いたいと考えました。
具体的には、全てのリクエストのうち、10%を新しいPython3のサーバーに、それ以外を古いPython2サーバーに処理に流し、徐々に新しいPython3への処理の割合を増やすことを考えていました。
通常、AppEngineの同じサービス内のリリースであれば、AppEngineのサービスのバージョンのトラフィックを分割で行うことができますが、今回は別のサービスにリリースするため、この方法は使えませんでした。
また、アプリ側で段階的リリースをすることも考え、Androidの段階的な公開という機能を使って可能になる予定でした。Python3のAPIにて2つのバージョンを用意し、/api/v1
のアクセスの時は引き続きPython2にリダイレクトさせる、/api/v2
のアクセスの時はPython3にリダイレクトさせるように実装をし、新しい方のバージョン設定したURLのアプリを段階的に公開することを考えていました。
ただ、上記のようにPython2のAPIは2024年1月で完全廃止することになり、段階的リリースはできませんでした。
ただ、今振り返ってみると、Python2のAPIをlegacyサービスとしてデプロイし直し、dispatch.yamlで*/legacy/*
を設定し、Python3のAPIからPython2へのリダイレクトのURLを/legacy/*
に変更するという方法をとれば、Python2のAPIを残したまま、段階的に移行することもできたかもしれません。
元々は、モノレポ管理を検討していました。重複したコードが多く、保守工数が高いことが課題になっていたためです。歴史的敬意からAppEngineの各種サービスとFunctionsを別リポジトリで管理しており、データモデルやモジュールを各所で定義する必要がありました。 また、費用面や運用上の課題からCloud Runへの移行を検討していました。
しかしながら、以下の課題があり今回は断念しました。
とはいえ開発生産性およびコスト改善の観点から、今後はモノレポにし、Cloud Runに移行したいと考えています。
2024年1月末にPython2のサポートが終了することを受け、2020年から開始したマイグレーション作業は、2023年7月に本格的に移行を開始し、2024年1月上旬に完了することができました。 10年の歴史のあるアプリを、メンテナンスしやすいコードにマイグレーションすることができたことは、今後の開発においても大きな財産となると思います。
もちろん、リリース後、不具合もありました。が、すぐに修正し、大きな問題なく移行を完了することができました。 また、ここに書いてること以外にも、多くの大小の壁に当たりました。システムを分離させたり、アプリの仕様を検討し直す必要があったり、UIを作る必要があったり、他のチームとの連携が必要だったり、などなど… (今でも、仕様バグがいくつか残っています)
また、最後のPhase3の移行プロジェクトでは、限られた人数でマイグレーションを完遂させなければならないプレッシャーの中で、仕様を検討したり、他のチームと調整をしつつ、実装も進めなければならなかったことが大変でした。 その一方で、納期通りに完了させることができたことは、素直にとても嬉しかったです。
改めて、このプロジェクトに関わってくださった全てのメンバーに感謝を申し上げます。
こんにちは、ピリカ開発チームの九鬼です。
AppSheetを使うと、スプレッドシートをデータベース代わりにしたアプリをノーコードで作成できます。AppSheetでは画像を表示することもできるのですが、設定すればGoogle Cloud Storage(GCS)上にある画像も参照することができます。
そこで、スプレッドシート+GCSに毎日データを追加し、AppSheetアプリでデータを見られる仕組みを構築してみました。
タカノメにおいて、1日あたり数万を超える地点の撮影データが蓄積されています。データの閲覧用ページはあるものの、統計的な結果を見るためにカスタマイズしていました。その関係で、
を地点ごとにチェックするには適していませんでした。その仕組みをつくるには数週間を要するため、すぐに対応するのは難しい状況でした。
対策を探していたところ、AppSheetで要件を満たせそうなことがわかりました。AppSheetであればUIをすぐに変更することができ、かつデータをリスト・マップ・画像付きで柔軟に確認することができるためです。そこで、AppSheetで撮影データをチェックするためのアプリを作ってみました。
以下、作成したアプリのイメージです。
Cloud Functionsにて、撮影地点一覧の取得・保存を定期的に行っています。その結果をスプレッドシートないしはStorageの特定バケットに保存し、AppSheetで参照しています。
GCS上にある画像を参照するにあたって、下記2点がネックとなっていました。特に1番目は公式ドキュメントがなかったため、AppSheetの挙動を見て実装しました。
バケット名は自由に設定可能ですが、オブジェクトの先頭は/DocId_xxx(xxxはスプレッドシートのファイルID)/
にから始まる必要があります。これ以外の接頭詞はAppSheetから認識されず、画像を参照しようとしても404になります。また、ファイルIDにあるハイフンはすべてアンダースコアに置き換える必要があります。
なお、スプレッドシート上でGCSの画像パスを記載する場合、/DocId_xxx/
以降のパスをセルに記載すればOKです。例えば、画像が/DocId_xxx/hoge/fuga.jpg
で保存されている場合、hoge/fuga.jpg
という文字列をセルに保存しておけばアクセスできます。
AppSheet上では、Dataの設定でTypeをImageとし、Table settings > Store for image and file captureでGCSへのアクセス設定を指定すれば画像が読み込み可能になります。
デフォルトでは、署名付きURLで画像が配信されます。これにより、署名がない状態での画像アクセスは制限されます。ただし、画像の実態はCDNから配信されているため、デベロッパツール等でCDN上のURLを参照すれば誰でもアクセス可能です。
もし画像をよりセキュアにした場合、Security > Options > Secure Image access を入れることができます。ONにした後、最大で1日以内に反映されます。
以上になります。皆様良いお年を!
こんにちは。 ピリカ開発チームの伊藤です。
ピリカでは6月1日より、ピリカサポーターズクラブを開始しました。 まだご覧になっていない方はこちらをご覧ください。
ピリカサポーターズクラブをはじめるにあたって新しいシステムを構築しました。 ピリカの開発チームのリソースは潤沢ではない中、全く新しいシステムを作るのはとても大きなチャレンジです。
社内からも「開発のリソースが潤沢でないならSNSピリカに注力すべき」という意見はありましたが、開発チームでは単に新しいシステムを作るだけではなく、この開発を「SNSピリカの開発を今後少ないリソースで効率的に進めるために必要な基盤の実験」としても位置付けていました。
この開発を通じて得たことのまとめとして、ピリカサポーターズクラブの構成やデプロイの仕組みをご紹介したいと思います。