Classi開発者ブログ

教育プラットフォーム「Classi」を開発・運営するClassi株式会社の開発者ブログです。

Classiの個別最適化エンジン CALE v2.0リリースまでの進化

はじめに

ClassiのPythonエンジニアで AL(アダプティブラーニング)チームのエンジニアリードの工藤 (id:irisuinwl) です。こんにちは。
最近のマイブームは寿司を握ることです。

さて、今回の記事ではアダプティブラーニングエンジン CALE の進化について書きたいと思います。

CALEとは、ざっくりいえば生徒の解答履歴を分析して能力を推定し、成績向上に役立つ問題を推薦するレコメンドエンジンです。

詳しくは以前の記事を参照ください:

tech.classi.jp

tech.classi.jp

Classiでは今年、新機能である学習トレーニング機能(以下、学トレ)をリリースしました。
学トレの一つに「AIが一人ひとりに合わせたおすすめ問題を提示する」機能があり、そちらでCALEが使われています。

corp.classi.jp

元々学トレリリース前ではWebテスト機能にて個別最適学習を実現するためのレコメンドを行っており、そのロジック実現のためCALE v1.0は利用されていました。

今回、学トレでのおすすめ問題機能を実現するにあたって、CALEを進化させるため、CALE v2.0 を開発しました。

この記事では、CALE v1.0からv2.0へどのように進化したかを紹介します。

  • はじめに
  • 概要
  • 従来のアーキテクチャ
  • アプリケーション基盤の変更
    • 移行を決定した背景
    • 移行作業
  • ユースケース設計とAPI設計の変更
    • 学習トレーニング機能のユースケース設計
    • API設計の見直し
  • データモデリングとストレージの変更
    • データモデリング
    • ストレージ構成の見直し
  • サービス構成の変更
  • まとめ

概要

CALE v1.0 ではリリース後の運用を通して、以下の課題が見つかりました。

  • Kubernetes の運用負担が大きい
  • 複雑なユースケースから生じる複雑なAPIエンドポイントとデータモデル
  • 当初の期待に対して過剰で複雑なストレージ構成とサービス構成

それに対してCALE v2.0では複雑にならないように以下のようにアーキテクチャ変更・開発の工夫をしました。

  • アプリケーション基盤の変更: Google Kubernetes Engine (GKE) から Amazon Elastic Container Service (ECS) へと移行
  • ユースケースの抽出とAPIエンドポイントの簡素化: ユースケースを課題配信・問題解答・レコメンドの3つを抽出、v1では8つあった API エンドポイントをv2では3つに絞り込みました。
  • ストレージ構成の変更: MySQL + Firestore + Google Cloud Storage (GCS) から MySQLのみの構成へと変更
  • サービス構成の統合: APIサービスとレコメンドロジックサービスの分離構成を統合し、モジュラーモノリスへと変更

結果としてシンプルなアーキテクチャを実現することができ、効果として以下のリードタイム短縮とレイテンシー改善をすることが出来ました

  • リリースまでのリードタイム中央値: 289時間 -> 129時間
  • 課題配信API p95 平均レイテンシー: 
    • v1:258ms -> v2: 26.8ms
  • 問題解答API p95 平均レイテンシー: 
    • v1: 413.7ms -> v2: 103.8ms
  • レコメンドAPI p95 平均レイテンシー: 
    • v1: 497.5ms -> v2: 28.6ms
続きを読む

Cloud Composerローカル開発ツールで運用業務を圧倒的に楽にする

こんにちは、データプラットフォームチームでデータエンジニアをしています、鳥山(@to_lz1)です。

本記事は datatech-jp Advent Calendar 2023 11日目の記事です。

データ基盤を運用する皆さま、ジョブの実行管理基盤には何を用いているでしょうか?

様々なOSSやサービスが群雄割拠するこの領域ですが、Google Cloud Platform(以下、GCP) のCloud Composerは選択肢の一つとして人気が根強いのではないかと思います。

一方、同サービスには時折「運用がつらい」とか「費用が高い(故に、開発者が気軽に触れる開発環境を用意できない)」といった言葉も聞かれるように思います*1

自分も入社以来触ってきて、そういった側面があることは完全には否定できないと考えています。しかしそれでも、ツールを用いてそうした負担を大幅に軽減することは可能です。今回はGCPが公式に提供するCloud Composerローカル開発CLIツールを実際の開発フローに導入したことと、それによって得られたメリットをご紹介します。

  • Cloud Composerローカル開発CLIツールについて
  • 導入前の課題
    • Cloud Composer環境の再現がつらい
    • Cloud Composerのアップグレードがつらい
  • 向き不向きに関する考察
    • PyPIパッケージのインストール・検証🙆
    • バージョンアップの検証🙆
    • 向かないと思われる用途
  • ローカル環境構築のスクリプト化
    • 1. Variablesのダミーの準備
    • 2. Poetry Export
    • 導入した結果
      • 残った依存とrenovateとの相性
      • そもそも依存をインストールすべきか?について
  • まとめ
続きを読む

Browserslist + GitHub Packageでサポート対象への対応を簡単にする

こんにちは!エンジニアのすずまさです。

皆さんが開発しているプロダクトでは、サポート対象の OS やブラウザのバージョンは規定されていますか? Classi ではそういった推奨環境が定められています。

ビルド後に推奨環境で動作させるために、社内で GitHub Package として公開された Browserslist の設定ファイルを使ったところ体験が良かったので、今回はその紹介をしようと思います。

Browserslist とは

その名の通り、下記のようなブラウザのリストを記述することで、フロントエンドのツール間でサポートするブラウザのバージョンを共有できるツールです。

last 2 Chrome versions
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR

例えば .browserslistrc に iOS >= 16 と記述してから npx browserslistを使用すると、次の結果が得られます(執筆時点での iOS の最新版は 17.1)。

$ npx browserslist
ios_saf 17.1
ios_saf 17.0
ios_saf 16.6-16.7
ios_saf 16.5
ios_saf 16.4
ios_saf 16.3
ios_saf 16.2
ios_saf 16.1
ios_saf 16.0

ここに出力されたブラウザをサポートするように、他のライブラリに伝えることができます。

Angular CLIでは、内部でこの Browserslist が使われており、Autoprefixer と babel にサポートブラウザを伝える役割を担っています。

なぜ Browserslist + GitHub Package を導入したか

Browserslist + GitHub Package を導入したきっかけは、Angular を v15 から v16 にアップデートしたときに、 Classi で動作保証している端末で画面が一部表示されない障害が起きたことでした。

そのリポジトリでは自前の Browserslist の設定はしておらず、Angular のデフォルト設定をそのまま使用していました。

browserslist.defaults = [
    'last 2 Chrome versions',
    'last 1 Firefox version',
    'last 2 Edge major versions',
    'last 2 Safari major versions',
    'last 2 iOS major versions',
    'Firefox ESR',
];

https://github.com/angular/angular-cli/blob/66b18b654bb47a320e686c4a9c752da64c52830e/packages/angular_devkit/build_angular/src/utils/supported-browsers.ts#L12-L20

Browserslist は caniuse-lite を使ってクエリ用のブラウザ DB を参照しています。

Angular のアップデートに caniuse-lite の更新も含んでいたため、その時新しくリリースされていた iOS 17 が Browserslist に反映されてlast 2 iOS major versionsの指すものが変わってしまい、障害につながってしまいました。

他のリポジトリでも発生しそうだと思い他チームにも共有したら「それなら設定を一箇所にまとめて各リポジトリから使えるようにしたいよね」という話があがり、社内のメンバーが Browserslist 設定用の GitHub Package を作成してくれました。

Browserslist + GitHub Package誕生までのスピード感のある会話

使い方

GitHub Packages の公式ページを参考に Browserslist の config ファイルをインストール後 Browserslist の設定ファイルで extends 構文を使用すれば使うことができます。

"browserslist": [
  "extends browserslist-config-mycompany"
]

https://github.com/browserslist/browserslist#shareable-configs

GitHub Packages を使うために CI の設定ファイルも書き換える必要がありますが、GitHub Actions を使っている場合は下記のようにすると package をインストールできるようになります。

steps:
  - uses: actions/checkout@v4
  - name: "Install npm dependencies"
    uses: actions/setup-node@v3
    with:
      node-version-file: ".node-version"
      registry-url: "https://npm.pkg.github.com"
  - run: yarn install --frozen-lockfile
    env:
      NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

簡単!

最後に

Classi はリポジトリ数が多く設定が分散しやすいので、今回紹介した Browserslist + GitHub Package は今まで使ってこなかったのが不思議なくらい便利でした。

今までサポート対象のブラウザに対応してない記法を使って不具合を起こしてしまった方や、各リポジトリで設定が統一されていないことに課題感を覚えている方がいたらぜひ試してみてください!

ここまで読んでいただきありがとうございました。

今年はAdvent calendarをやらないと決めた話

みなさん、こんにちは。日頃からClassi開発者ブログをご覧いただき、ありがとうございます。編集部のid:tetsuro-ito です。 すっかり秋も深まって寒くなってきました。今年の夏は暑く、冬も暖冬傾向が続くそうですが、寒いものはやはり寒いですね。季節の変わり目は体調を崩しやすい時期なので、注意したいところです。

さて、冬になると、IT業界では恒例のイベントとなったAdvent calendarがさまざまな切り口で開催されます。 Classiも2016年から毎年このイベントの企画をやってきました。

私も入社して以来、毎年必ずこのイベントにはエントリを書いていますが、最初の方では執筆者の確保が難しく、一人で複数記事を投稿することでどうにか企画を成立させていました。その後、エンジニアが多く入社してきたこともあって、一人一記事で25日を埋めることができるようになったり、書かれる内容もエンジニアだけではなく、他の職種のメンバーも書いてもらったりしていて、広がりを見せていました。

また、当時は会社のブログの場所がなかったため、Qiitaのプラットフォームに記事を投稿していましたが、2020年に開発者ブログが誕生した後は、投稿先もこちらにすることができたので、年末は賑わいを見せていました。

メリットとして良い面もたくさんありますが、デメリット部分もいくつかあり、今年は会社のAdvent calendarは実施しないという意思決定を開発者ブログ編集部で行いました。

なぜ今年はAdvent calendarをやらないと決めたか

どうしてこのような意思決定にいたったかのいきさつを少しお話しします。 開発者ブログの編集部では過去に編集長の id:aereal さんが方針を書いています。

停滞した開発者ブログを復活させるまで

そこで開発者ブログを 「より充実した発信環境を求めるClassiの技術者に向けた執筆の場」 とすることにしました。

これをもう少し分解すると:

  • 読み手にとっての価値より書き手にとっての価値を第一に考える
    • 場を作ったり書くことを継続することに踏み出せないでいるメンバーが主な対象
    • そのために慣れた書き手たちが技術ブログを暖めたり、期待値コントロール、レビュアーとしてサポートする
  • 以下のような点は「できたら良いこと」という扱いにして、当面は第一の目的としない
    • 会社のブランディング・認知向上
    • 採用への貢献

このブログの方針は今でも変わっていません。あくまでもClassiの技術者のよりよい発信の場であり、書き手にとっての価値を第一に考えることやブランディングや認知向上の手段は二の次として考えています。 上記の方針と照らして考えた際、Advent calendarの実施やその執筆者集めはイベント的には良いのですが、書き手の”書きたい”を優先的に考えることと相反しているようにも思えました。

また、これまで7年間続けてきた風習のような取り組みですが、惰性で続けてきてしまっている意味合いも少なからずあると思います。季節もののイベントですので、そういうものだと捉えても良いのですが、一度立ち止まってどうしてこの取り組みを実施するかを考え直してみた結果、一度やめてみる選択肢もあるのではないかという観点があがりました。業務の中でもよくある話ですが、一度始めてしまった取り組みをなかなかやめられないことはたくさんあるでしょう。そうした取り組みでも、立ち止まってなぜを考え、改めて取り組む必要があるならばまた再開すればよいだろうと結論づけました。

また、もう一つの理由があります。 それは短期間にたくさん投稿を集中させることによるブログ編集部のレビュー負荷の高まりと、その後の燃え尽き症候群の回避です。

下記は今年の開発者ブログの記事投稿数の月次推移です。

開発者ブログの記事投稿数月次推移

編集部では隔週に1本程度の記事が出るという緩いKPIがあります。月に2本記事が出ていれば、ミニマムのラインは出せている認識です。グラフを見ていただけるとわかりますが、昨年12月はAdvent calendarを実施したおかげで、16本もの記事を出すことができました。

これはこれですごいのですが、編集部のメンバーが個々の記事をレビューしたり、執筆者を集めたりということを行っていたので、非常に負荷が高まる時期になってしまいました。 その反動から2ヶ月記事が出ない状況が続いてしまいました。これは書き手の方のネタも出てしまったという要因もありますが、何よりブログ編集部の燃え尽き症状などもあったのではないかと考えています。 年度末に編集部で振り返りを実施した際に、改めてこの事実を認識し、改善したことで、その後は順調に記事を出せるような状況に戻りました。

このような背景を総合的に加味して、Classiでは今年のAdvent calendarを会社で実施することを見合わせることに決定しました。あくまで会社としてやらないだけで、個々のメンバーがそれぞれの技術領域のAdvent calendarに参加したり、記事を書いたりすることは推奨していますし、このイベント自体を否定するわけではありません。しかし、我々は書き手のことを考え、継続的に発信し続ける場所を維持したいので、このイベントから降りるという選択をしてみたわけです。本ブログでは12月も通常運行を続ける予定ですので、引き続き地に足のついた記事をお届けできれば幸いです。

「Terraform活用大全 - IaCの今。」に登壇してきました

こんにちは、 Classi でソフトウェアエンジニアやってます koki です。
Findy さま主催の「Terraform活用大全 - IaCの今。 Lunch LT」に「Terraform に test コマンドがやってきた」というタイトルで登壇してきました。

イベントのアーカイブ動画は Findy にログインするとイベントページから閲覧できます。

登壇のきっかけ

SNS 経由で Findy さまからお声がけいただきました。 よく Zenn で Terraform に関する記事を書いていたのを見ていただけたようです。
ちょうど最近リリースされたTerraform の test コマンドをキャッチアップしたいと思っていたところだったので、自分の学びのためにも登壇を快諾しました。
これが俗に言う登壇駆動学習ですね。

発表内容

Terraform v1.6 で新しく追加された test コマンドについてご紹介しました。

このアップデートによって、例えば以下のように Terraform のテストを書くことができるようになりました。

# example.tftest.hcl

run "example_test" {
  # `command = apply` にすると実際にリソースを作成して検証することができる
  command = plan
  assert {
    # リソースの attribute をテストする
    # この例の場合は S3 バケット名をテストしている
    condition     = aws_s3_bucket.main.bucket == "example-bucket"
    error_message = "失敗したときのエラーメッセージ"
  }
}

基本的なテストの書き方と実行方法以外にも、次のような機能を簡単にご紹介しました。 詳しくはスライドや参考資料*1をご参照ください。

  • テストファイル用のディレクトリを使う
  • テスト内で変数を設定する
  • Custom Conditions と組み合わせて使う
  • その他できること ( 一部 )
    • プロバイダの設定を上書きする
    • output の値を検証する
    • Data Source を読み込む
    • モジュールを読み込む

また、他の方々の発表もどれも大変参考になる内容でしたので、こちらもぜひご覧ください。

感想

社外のイベントに登壇するのは初めてだったので緊張しましたが、とても楽しかったです。 色々な企業様の事例を聞くことができて大変勉強になりましたし、僕自身も Terraform の新機能にキャッチアップするいい機会になりました。
たとえ個人でも技術記事を書いているとこういう機会をいただけることがあるんだなぁと思いました。 Terraform に関する内容に限らず、今後も継続的に発信していきたいです。

SRE留学体験記(第3期生)

こんにちは。プロダクト本部Growth部でエンジニアをしている id:ruru8net です。以前はこちらの記事を書かせていただきました。
tech.classi.jp tech.classi.jp

この度わたしはSRE留学の第3期生として、2023年5月-10月の半年間SREチームに留学をしました。
SRE留学とは?や、第1期生、2期生の体験記はこちらをご覧ください。
tech.classi.jp tech.classi.jp

SRE留学を志望した理由

留学以前は主に認証機能の運用開発をするプロダクトチームで仕事をしていました。弊社のプロダクトチームはただアプリケーションコードの実装をするだけでなく、担当機能のインフラ管理や監視等もすることが多々ありその中でAWSに触れ、サービスの様々なことを考えながらも全体を操作している感覚を漠然と楽しいなと思うようになりました。またCI・CDを考えたり物事の自動化や効率化が好きだったのでそのあたりに特化して仕事をするのはSREなのでは?と思うようになりSRE留学に興味を持ちました。

留学後にありたい姿

新しくAWS等のインフラ知識を積極的に獲得し今後の開発に生かしたい

プロダクトチームでは主にRuby on RailsとAngularでのバックエンドとフロントエンドを開発することが主だったのですが、何か機能を作ろうとした際にはもちろんその機能がどのくらいの規模で使われるのか、監視はどのように行うのかなどインフラに関することも考える必要がありました。バックエンドとフロントエンドに注力する機会が今まであったからこそ、この機会にインフラに注力して知識や経験を獲得することで、サービスを考える際にフロントエンド、バックエンド、インフラに対して一本の道を通して様々なことを考えられるようになれるのではと思い、その状態を目指したいと思いました。

チームを横断した考え方や動きができるようになりたい

プロダクトチームではどうしてもその機能、チームに閉じた動きをしてしまいがちなのですが、SREという帽子を得ることでチームの壁を感じにくくなるだろうと思いました。また過去の留学生が留学中や留学後にそのような振る舞いをしているように見え、自分もそのようになりたいと考えました。

留学中にできたこと

SREチームではカンバンを使ってタスクを管理しており、留学中は日々そのタスクをこなしていくことがメインになります。その中でわたしが実施したタスクを2つ紹介します。

ECSタスクの異常停止の検知とイベントログの保存

これはECSのタスクが起動に失敗しかつその検知が遅れサービスにアクセスできない障害が発生しまったこと、また暫定対応後に起動失敗の原因を調査しようとした際に該当ECSタスクのイベントログがすでに消えてしまっていたことからネクストアクションに積まれていたものでした。

弊社にはサービスのアラートを流すSlackチャンネルがあるため、起動失敗や異常停止を検知した際には以下のようにチャンネルに通知するようにしました。

Slackに投稿されるメッセージ

Amazon EventBridgeにて異常停止、起動失敗のイベントのみをフィルタし、Amazon SNSとAWS Chatbotを使いSlackに投稿しています。

構成図

またECSのイベントログもAmazon EventBridgeを使いCloudWatch Logsに保存するようにしました。

この仕組みを構築後、リアルタイムでECSタスクの停止を検知できるようになりMTTRを向上させることができました。また過去のログも遡れるようになり事後調査がしやすくなりました。実際にいろんな人にこの仕組みができて助かった、というような声をもらうことができとても嬉しかったです。

Amazon Auroraの監査ログをCloudWatch Logsを経由せずS3に保存する

弊社のAWSにかかっているコストを調査したところCloudWatch Logsの特にAmazon RDSの監査ログの保存にコストがかかっていることがわかりました。コストバランスを意識し改善する機会はSREチームに所属してからとても増えました。
また個人的にはRDSに触るのは研修ぶりというくらいに久しぶりで前提知識がほとんどなく、まずはRDSの勉強から始めました。
監査ログってどこで書き込まれているの?どこで設定されているの?Amazon Aurora、MariaDB、MySQLはどう違うの?などなど様々なことを調べました。SREチームのリーダーが「ただタスクを急いで消化するのではなく、ゆっくりでいいからタスクを進めながらたくさん寄り道をして欲しい」と言ってくれたのもあり、丁寧に勉強することができました。
最終的にはEventBridge SchedulerとLambdaを使用し、仕組みを構築することができました。この仕組みについては改めてブログを書こうと思います。
当初の自分のレベルより格段に上のタスクをこなすことができ、とても自信がつきました。

留学中に難しかったこと

大きな課題をタスクに分割すること

SREチームに与えられる課題は漠然としていて大きいものが多々あり、例えば、コストを削減して欲しい、ログをいい感じに管理して欲しい、などがありました。特にログの管理に関しては最初に見た時に、「なんとなくログの管理が統一化されていないのはわかるけどじゃあ統一するために何から始めたらいいのだろう、そもそもどの状態に統一すればいいのだろう」と課題を着手できるサイズのタスクに分割することができませんでした。そこで課題のタスク分割をSREのチームリーダーと一緒に進めさせてもらいました。
まずは現状把握をする、その後に現状の課題感を出してやることリストを作る、という流れを一緒に取り組ませてもらいました。
私は何かを進める時にまず明確にゴールを決めないと、と思っていたのですが、ここでは課題感を無くすために必要なタスクを洗い出すことで、課題感がなくなればそれはゴールだよねという考え方を教えてもらいとても納得感がありました。

プロダクト開発ではどちらかというと作りたい機能や目的を最初に明確にすることが多く、決まったゴールに辿り着くために必要な道筋を考えるということが多かったです。ですが今回のように「正しく管理されている状態」は一言でいうのは難しく、逆に言えば「現時点での問題点が解消されている状態」と言い換えられることがわかりました。課題に対する新しいアプローチ方法を体験しながら学ぶことができ、勉強になりました。

横断的な振る舞い、考え方の難しさ

留学後にありたい姿としても書いていたのですが、これを実現することはとても難しかったです。留学前は一つの閉じたチームにいるから他のチームの課題がなかなか見えない、SREという横断チームに所属すれば必然的に他のチームのことが見えてくるのでは、と考えていました。もちろんそんなことはなく、ただ横断チームに所属するだけではだめでそもそもの動き方や仕組みを考える必要があるというのを実感しました。またSREチームに対して投げられた質問等に対しても、自信の無さからか他の誰かの反応を待ってしまうことが多く主体的にガツガツ動けた、とは言えませんでした。

今後

累積矢面時間を稼ぎたい

なかなか横断的な振る舞いができなかった、という反省に対し「累積矢面時間」というお話をもらいました。
累積矢面時間とは野村総合研究所 ICT・メディア産業コンサルティング部 主任コンサルタントである鈴木良介氏の言葉であり、鈴木氏は、「戦略的・計画的に自身のキャリアをつくっていくためには、どれだけクライアントとの矢面に立ち、成功と失敗の経験を積んだか。それに尽きる」と説いています。 より詳細な詳細な解説は下記をご覧ください。

この話を聞いたうえで今回の留学を振り返ると、自分が矢面にたったと言える時間は少なかったと言えます。ただ常に隣でSREのチームリーダーが矢面に立って物事を考えたり、意思決定をしていて、その姿を半年間間近で見ることができました。

仕事力=引き出しの量×瞬発力

とあるように、この留学によって確実に「引き出しの量」は増やせたと思っています。あとは「瞬発力」を稼ぎたい、そのために「累積矢面時間」を稼ぎたいと思いました。

留学前は「リーダー」といったような役職に対して私には難しいといったマイナスイメージを抱いてしまうことが多かったです。ですがSRE留学を経て横断的な考え方や振る舞いを身につけるために「仕事力」を増やしたいと思い、それにより今は積極的にリーダーという役職にも自ら挑戦してみたいと思えるようになりました。この辺りは留学前と留学後で大きくマインドが変わったところで、そう思えるようになったことがとても嬉しいです。

今後は一度プロダクトチームに戻りますが、留学中に得たこれらの知識やマインドを持って引き続き業務に励みたいです。

Step Functions を使って、ECS のワンショットタスクを実行する

こんにちは、プロダクト開発部の藤田です。

今回は Amazon ECS(以下、ECS)AWS Step Functions(以下、Step Functions) を組み合わせた「ワンショットタスクの実行基盤」についてご紹介します。

「ワンショットタスク」とは、指定されたコマンドやスクリプトを1度だけ実行して、終了するタスクのことです。

このようなワンショットタスクは「プライベートネットワークの内側にあるリソースへのアクセスが必要な、非定型的な操作」を実行する際などに用いられます。

例えば、プライベートネットワークの内側にある DB に対して、以下のような作業をエンジニアが実施する際に、ssh を利用して踏み台サーバーに接続して、決められた手順書を実行する・・・といったような場面を想像していただければ、イメージしやすいと思います。

  • DB にテーブルを追加するための migration 実行
  • DB のマスターデータにレコードを追加
  • DB の特定のテーブルのレコードの一斉置換
  • DB の不要になったデータの一斉削除

実行したい操作の内容や時期も不定であり、作業に必要な踏み台サーバーは常にプロビジョニングされている必要はありません。 一方で、一般的には、商用環境に対するあらゆる操作の監査ログを収集・保持するというガバナンス要件があります。

これらの要件を満たすワンショットタスクの実行基盤を ECS と Step Functions で構築しました。

また、今回構築した「ワンショットタスクの実行基盤」は実行するスクリプトのランタイムに基本的に依存しない汎用的な仕組みであるため、参考になれば嬉しいです。 本記事の Appendix に Terraform コードの例を添えて、具体的な構築方法をご紹介します。

ワンショットタスクの仕組みが必要な背景

我々のチームでは Go 言語で実装された Web API の開発と運用を行っており、対象の API は ECS サービスとして実行されています。 この Web API は GraphQL を採用しており、GraphQL schema の管理方法については以下の記事でも紹介されているのでご参照ください。

また、データストレージとして Amazon Aurora を採用しています。

日々、機能の拡充や改善を進めていく過程で、DB のテーブル定義の追加・変更が求められたり、別のテーブルへのデータ移行や不要データの削除などが必要になったりすることもあります。

これまではこのようなデータのメンテ作業が発生した際は

  • エンジニアが作業手順書を作成する
  • エンジニアが作業手順書をレビュー・承認する
  • (二人以上のエンジニアがダブルチェック用に立ち会いながら)エンジニアが ssh を利用して踏み台サーバーに接続して、手順書の内容を実行する

といった作業を行っておりました。

しかしながら、上記のような運用では、本番の踏み台サーバーに ssh で接続するためには事前に管理部門からの承認が必要でそれらの手続き完了までに一定のリードタイムが必要となります。 仮に管理部門から承認されたとしても、ssh を利用して踏み台サーバーに接続した後は作業者が持っている権限の限りではどのような操作も行えてしまうので、作業ごとに最小限の操作を許可する設定を入れるといった複雑な運用をしない限りは、本当の意味でセキュリティ上安全とは言えません。

また、事前に作成した手順書を指定された順番通りに実行する際も、手順書のコピペミスなどのヒューマンエラーが発生する可能性は否めません(そのためのダブルチェックではありますが)。

上記のような理由からかもしれませんが、実際、自分自身もデータのメンテ作業が必要な機能改修に着手する際の心理的ハードルは高く感じていました。 この状況に課題を感じ、データのメンテ作業を安全に単純化できる仕組みの構築を進めることとなりました。

ワンショットタスクの仕組み構築の方針

go run コマンドで ECS タスクとして実行できる形にする

前述した通り、我々のチームでは Go 言語で実装された Web API を ECS 上でホストしており、ECS 上で稼働している API 用の ECS タスクはデータの読み書きを行っているため、データのメンテ作業の対象となる DB と接続している状況です。

つまり、API 用の ECS タスクと同様の条件下で、実行するコマンドだけをデータのメンテ作業用の内容に書き換えて実行することができます。 Go 言語では go run <スクリプトのファイル> で、実行したいプログラムをビルド&実行できるため、データのメンテ作業の種類ごとにスクリプトを実装して、ECS タスク上で go run を実行させることは容易でした。

また、API を実装しているリポジトリでは CI/CD を構築しており、テスト対象が API からデータのメンテ作業用のスクリプトに変わったとしても、想定する振る舞いを検証することに対してハードルはありませんでした。 スクリプトの実行前と実行後のデータの変化さえも単体テストで検証できる上、スクリプトの実装やテストケースの網羅性をコードレビューの中でレビューできる点が、開発者にとって非常に良い体験となっています。

更に、チームでワンショットタスクで実行するスクリプトをコードレビューするため「事前にチームでレビューしたコードのみが実行される」という点が保証されます。 加えて、ECS タスクで実行する場合はタスクにアタッチする IAM Role で権限制御も容易であるため、セキュリティ面も改善されていると言えます。

ワンショットタスクを簡単に実行できるようにする

ECS の RunTask API では、実行する ECS クラスターや ECS タスク定義の指定も可能ですし、containerOverrides で実行する command も override できます。

そのため、この時点でデータのメンテ作業の作業者が ECS の RunTask の API を実行する(もしくは aws cli で run-task を実行する)ことで、実装・レビュー済みのスクリプトを ECS タスクとして実行可能になっています。

ただし、RunTask 呼び出しの際に指定すべきパラメーターが数多くあるため、そこにもヒューマンエラーの余地が多くあります。 そこで、ECS の RunTask の呼び出しを Step Functions を介して行うようにします。

Step Functions ではこちらで記載がある通り、ECS の RunTask の呼び出しを標準のステップとしてサポートしており、与えられるパラメーターも ECS の RunTask API に準じています。

構築する Step Functions の全体像は下図の通りです。

Step Functions の全体像

作業者が Step Functions を起動して、RunECSTask というステップを実行して、終わるといったシンプルなものになっています。

また、作業者が Step Functions を起動する際に渡す引数のイメージは下図の通りです。

Step Functions の実行画面

“commands”: [“cmd/sample/main.go”] といった引数のみで、go run で実行したいスクリプトのパスのみを指定するといったこれまたシンプルなものになっています。

実際は RunTask を実行するためには、以下のパラメーターを指定する必要があります。

  • 実行する ECS クラスター
  • 使用する ECS タスク定義
  • ECS タスクを実行する際の Subnets や SecurityGroup といった NetworkConfiguration

ただし、上記のパラメーターは我々のチームの運用においては毎回変更・指定したいものではないため、作業者が都度意識するべき設定ではありません。 そのため、これらの設定は Step Functions の定義の中にハードコーディングして、Step Functions の実行者が意識するのは「実行したいスクリプトのパスのみ」という設計が開発者の体験として負担が少ないという結論になりました。

Step Functions でワンショットタスクを実行する際のメリット

  • データのメンテ作業用のスクリプトを go run で実行できる形で実装する
  • Step Functions で ECS のタスクの実行を簡単にできるようにする

上記の2つの方針で構築を進めた結果、我々のチームでは Go 言語でスクリプトを実装し、実装したスクリプトのパスを引数に Step Functions を実行するだけでデータのメンテ作業を簡単に、かつ、安全に進めることができるようになりました。

他にも、この仕組みを使った運用にはメリットがあります。

データのメンテ作業の履歴の保存

Step Functions は標準で下図のように実行の履歴が残ります。

Step Functions の実行記録

この履歴はそのまま、データのメンテナンス作業の作業履歴として扱うことができます。 履歴一覧の時点で、開始と終了の日時と成否のステータスが分かります。

更に、各実行の詳細も下図のように確認が可能で、Resource から実際に実行された ECS タスクへのリンクに飛ぶことができます。 (※下図では説明の都合上、一部のステップを除外して表示しています)

Step Functions の実行詳細例

ECS タスクの設定で Amazon CloudWatch Logs(以下、CloudWatch Logs) 等に実行スクリプトのログを出力するようにしておけば、後から当時の実行ログを参照することも可能です。

また、Step Functions の各ステップの実行ログを CloudWatch Logs に出力することも、AWS X-Ray にトレースを送信することもできます。

つまり、データのメンテナンス作業に関わる記録は、必要な設定さえ行えば AWS のサービスを利用して残すことができるのです。

他のチームへの知見・仕組みの共有しやすさ

我々のチームに限らず、Classi 社内では API などは ECS を使ってホスティングすることが多いです。 アプリケーション開発の技術スタックに関しては Ruby on Rails が多いのですが、Rails の場合でも事前に実装した rake タスクを ECS タスク上で実行可能ですし、他の言語に関しても同様の仕組みがあることが多いでしょう。 そのため、Step Functions の構築方法を模倣できれば、社内に関して言えば、基本的に実装のランタイムに拠らず今回のワンショットタスクの仕組みは他のチームでも採用しやすいです。

幸い、今回の Step Functions は Terraformを使ってコード化して管理可能なので、既に Classi 社内でも他のチームにも知見を共有して、使ってもらっています。

この記事の最後に appendix として Terraform のサンプルコードを載せていますので、記事を読んでくださった皆様のチームでもご活用いただけると幸いです。

Appendix

ワンショットタスク用の Step Functions の Terraform サンプルコード

resource "aws_sfn_state_machine" "enjoy_ecs_run_task" {
  definition = jsonencode({
    StartAt : "RunECSTask",
    States : {
      RunECSTask : {
        Type : "Task",
        Resource : "arn:aws:states:::ecs:runTask.sync",
        Parameters : {
          Cluster : “実行する ECS クラスターの arn”,
          TaskDefinition : “実行したい ECS タスク定義の arn”,
          LaunchType : "FARGATE",
          NetworkConfiguration : {
            AwsvpcConfiguration : {
              SecurityGroups : [“ECS タスクを実行する際の SecurityGroup”],
              Subnets : [“ECS タスクを実行する際の Subnets”]
            }
          },
          Overrides : {
            ContainerOverrides : [
              {
                Name : "ecs-task-run",
                "Command.$" : "$.commands"
              }
            ],
          }
        },
        End : true
      }
    }
  })
  role_arn = aws_iam_role.enjoy_ecs_task_run.arn
  name     = "enjoy-ecs-task-run"
  type     = "STANDARD"
}

data "aws_iam_policy_document" "assume_role_step_functions" {
  statement {
    actions = ["sts:AssumeRole"]
    effect  = "Allow"
    principals {
      type        = "Service"
      identifiers = ["states.amazonaws.com"]
    }
  }
}

data "aws_iam_policy_document" "enjoy_ecs_task_run" {
  statement {
    effect = "Allow"
    actions = ["ecs:RunTask"]
    resources = [“実行したい ECS タスク定義の arn”]
  }

  statement {
    effect = "Allow"
    actions = [
        "ecs:StopTask",
        "ecs:DescribeTasks"
    ]
    resources = ["*"]
  }

 statement {
    effect = "Allow"
    actions = [
      "events:PutTargets",
      "events:PutRule",
      "events:DescribeRule"
    ]
    resources = ["arn:aws:events:{$region}:{$accountID}:rule/StepFunctionsGetEventsForECSTaskRule"]
  }
}

resource "aws_iam_role" "enjoy_ecs_task_run" {
  name               = "enjoy-ecs-task-run-state-machine-role"
  assume_role_policy = data.aws_iam_policy_document.assume_role_step_functions.json
}

resource "aws_iam_role_policy_attachment" "enjoy_ecs_task_run" {
  role       = aws_iam_role.enjoy_ecs_task_run.name
  policy_arn = aws_iam_policy.enjoy_ecs_task_run.arn
}

resource "aws_iam_policy" "enjoy_ecs_task_run" {
  name   = "enjoy-ecs-task-run-state-machine-policy"
  policy = data.aws_iam_policy_document.enjoy_ecs_task_run.json
}

© 2020 Classi Corp.