TECH PLAY

株式会社エブリー

株式会社エブリー の技術ブログ

385

はじめに 今すぐできるレビュワーに優しいPull Requestをつくる7つのポイント 1. WhyとWhatをそれぞれ記載する 2. 説明文は構造化する 3. コミットは課題を解決した単位で行う 4. Pull Requestは適切な大きさに分割する 5. 個別説明が必要な箇所は積極的にコメントをつける 6. テストを書く 7. Pull Requestでのコメントを Slack に通知させる さいごに はじめに はじめまして。DELISH KITCHEN開発部の桝村です。DELISH KITCHENのWEBフロントやAPIサーバーの開発等に携わっています。 突然ですが、みなさんは本日もPull Requestを使ってレビュー依頼しましたか?もしくは、誰かからレビュー依頼を受けましたか? チーム開発におけるコードレビューというものは、プロダクトの品質向上やチーム内での知見共有に貢献しているものの、 チームがコードレビューに対して相当な時間や労力をかけているのも事実 かと思います。 加えて、レビュー対象の実体でもあるPull Requestの品質は、作り手である実装者に大きく依存しており、コミットから説明文まで自由に作れる反面、 レビューしやすいPull Requestを作成しないと、より一層自身やチームに大きな負担がかかる可能性 があります。 そこで、今回はレビュワー目線に焦点を当てて、レビューしやすいPull Requestをつくるために自分が心がけていることを紹介させて頂きます。 簡単かつすぐに改善できるポイントをまとめたので、ぜひ参考にして頂けると幸いです。 今すぐできるレビュワーに優しいPull Requestをつくる7つのポイント 1. WhyとWhatをそれぞれ記載する WhyとWhatが不十分な場合、レビュワーはそれらをコードから想像せざるを得なかったり、実装者へ直接確認する手間が生じて、大きな負担になる可能性があります。また、WhyとWhatが区別されず混合している場合も、実装内容の難易度や複雑性により、実装者とレビュワーの認識に齟齬が生じ得ます。 WhyとWhatをそれぞれきちんと記載することで、レビュワーは、 本来のレビュー内容である、仕様通りかどうか、改善の余地はあるか等の確認作業に集中でき 、よりコードレビューをしやすくなります。 また、Pul Request自体が履歴的な情報としてリポジトリ内に残り続ける点で、実装に関するドキュメントとしての役割も担います。よって、WhyとWhatをきちんと記載することは、長期的に見てもチームにとって非常に貴重な財産になります。 加えて、以下のような情報があると、実装内容の正当性を容易に検証できたり、アウトプットがひと目で分かる点で、よりレビュワーが実装概要を理解しやすくなります。 ローカルでの動作確認の手順 関連するPRやタスク管理システムのチケットへの参照リンク フロントエンド実装の場合、デバイス別でスクリーンショットを添付 バックエンド実装の場合、レスポンスや必要なパラメータを記載 2. 説明文は構造化する 説明文が各項目について整理されず文章のみで構成されている場合、読み手であるレビュワーは実装内容の要点を理解するのに時間がかかり、大きな負担になる可能性があります。 説明文では以下のようにマークダウン記法を使用できるので、見出しや箇条書き、コード埋め込み等のスタイルを利用することで、レビュワーは、 実装概要をひと目で理解でき 、よりコードレビューをしやすくなります。 ### Why - 実装背景 ### What - 実装内容 - 実装内容詳細(その1) - 実装内容詳細(その2) ### Ref - 関連PRへの参照リンク ### Check - [ ] レビュー依頼前に必ず実施すること(その1) - [ ] レビュー依頼前に必ず実施すること(その2) Pull Request 説明文の構造化例 参考: Basic writing and formatting syntax また、Pull Request TemplatesというPull Requestの説明文に対して開発者に含めて欲しい情報をカスタマイズし、標準化できる機能があり、これを使用すると、レビュワーにとって見やすい説明文になるだけでなく、実装者にとっても構造化する手間がなくなったり、何を記載すれば良いか明確になる点で導入するメリットが非常に大きいです。 参考: Creating a pull request template for your repository 3. コミットは課題を解決した単位で行う コミットの粒度がバラバラであったり複数の変更が入った曖昧なコミットである場合、レビュワーはどんな変更をしているのか把握しづらく、大きな負担がかかる可能性があります。 機能実装やバグ修正、リファクタ等、まずは単一の課題や目的を単位としてコミットすることで、レビュワーは、 変更概要や意図を正確かつ容易に理解でき 、よりコードレビューをしやすくなります。 加えて、以下のようにコミットメッセージにPrefix (テキストの先頭につける文字) をつけると、 どのカテゴリの修正をしたのか、プロダクションコードに影響があるコードかがひと目でわかるようになり 、よりコードレビューをしやすくなると思います。 feat : (new feature for the user, not a new feature for build script) fix : (bug fix for the user, not a fix to a build script) docs : (changes to the documentation) style : (formatting, missing semi colons, etc; no production code change) refactor : (refactoring production code, eg. renaming a variable) test : (adding missing tests, refactoring tests; no production code change) chore : (updating grunt tasks etc; no production code change) 参考: Semantic Commit Messages 4. Pull Requestは適切な大きさに分割する Pull Requestが大きすぎる場合、レビュワーは単純に時間や労力がかかるだけでなく、既存のコードへの影響範囲が大きくなるゆえに問題点の発見も困難になり、大きな負担がかかる可能性があります。 適切な大きさに分割すると、レビュワーは、 影響範囲もより限定的になるため、レビューが楽になったり、その精度も上がり 、よりコードレビューをしやすくなります。Pull Requestの粒度としては、自分の場合、スコープ、つまり機能セットを絞り込み、1つのPull Requestで解決するタスクを減らすことを意識しています。 5. 個別説明が必要な箇所は積極的にコメントをつける 特定のコードについて説明が必要な場合があると思います。例えば、実装したものの自信がなく注意深くレビューをお願いしたい時やコードのみで実装の意図が伝わりにくい時、知見を共有したい時などです。そういった場合、インラインコメントを記載すると、レビュワーは、 自ずとコメント周りのコードを注意深く確認したり、早期に問題提起・解決策の話し合いができ 、よりコードレビューをしやすくなります。 Pull Requestへのインラインコメント例 (Githubの場合) 参考: Adding line comments to a pull request また、実装者が躊躇せず積極的に発信することが、有意義な議論やコミュニケーションが生み、結果的にチームの成長や開発効率の向上に繋がると思います。 6. テストを書く テストがない場合、レビュワーは実装内容の仕様をソースコードのみから読み取る必要があり、大きな負担がかかる可能性があります。 テストがきちんと書かれていると、ただソフトウェアの品質を向上させるだけでなく、ソースコードの仕様(期待する処理結果)に関するドキュメントとしての役割も担うため、レビュワーは、 その仕様や振る舞いを容易に読み取ることができ 、よりコードレビューをしやすくなります。 7. Pull Requestでのコメントを Slack に通知させる コードレビューにて実装者のレスポンスが遅い場合、レビュワーは返信が無くて気になったり、コメント内容を忘れる等により、レビューの効率を下げ、大きな負担がかかる可能性があります。 そこで、実装者が簡単かつ最初にできることは、レビュワーによるコメントにいち早く気づくことです。Slackとの連携機能を使用することで、 レビュワーによるコメントやレビューを任意のチャンネルへ通知させることができ 、少しでも早く返信できるようになります。 SlackへのPull Request コメント通知例 (Githubの場合) 参考: GitHub と Slack を連携させる さいごに 今回は、チーム開発において、レビュワーに優しいPull Requestをつくるポイントをまとめてみました。 冒頭でお話ししたとおり、チーム開発ではコードレビューは結構な時間と労力がかかります。裏を返せば、メンバー一人一人がレビューしやすいPull Requestの作成を心がけることで、チームの開発速度が大きく改善する可能性があると思います。 今回紹介させて頂いたポイントを実際の開発現場で試して頂けると嬉しいです。 ここまでお読みいただき、ありがとうございました。
アバター
はじめに はじめまして。普段はMAMADAYSでiOSエンジニアをしている國吉です。 iOSエンジニアではありますが、アプリのストアレビュー改善企画も兼務で行っているため、時にはAndroidの実装を担当することもあります。 そこで今回は2020年8月頃にGoogleから提供されたIn-App Review APIをMAMADAYSのAndroidアプリに導入し、実際レビュー評価にどのような変化を及ぼしたのかをお話していきます。 In-App Review APIとは アプリから離脱することなく、アプリのレビューを行うことができるAPIです。 APIレベルは21以上(Android5.0)がサポートされています。 developer.android.com これまでの課題 ストアのレビュー評価はアプリのインストールに大きく関わってきます。 ただ、MAMADAYSではこれまで”独自ポップアップを実装し、Google Play Storeに遷移させレビューを行ってもらう”というフローだったため、レビューまで手間がかかってしまいユーザーの離脱が多かったです。 そのためレビュー評価4.0から全く上がらないのが課題でした。 導入してみた結果 In-App Review APIを導入し約8ヶ月程経過しましたが、徐々にレビュー評価の件数が上がってきています。 もちろんその8ヶ月間でアプリ自体に様々な機能を追加しており、利便性が向上していることも影響してると思いますが、アプリ内でレビュー完結できることがレビューという行為のハードルを下げています。 導入前と比較すると0.3程度上がり、レビュー評価は4.3~4.4程度にまで成長しました。 レビュー要求を表示するタイミング レビュー要求は表示するタイミングが重要です。 MAMADAYSでは、ユーザーが何かを達成した時(例えば記事をお気に入りした。育児記録を登録した。など)に下記の公式リファレンスに記載されている事項を考慮しレビュー要求を表示しています。 ユーザーがアプリやゲームを十分体験してから、アプリ内レビューのフローを開始してください ユーザーに過度にレビューを求めないでください 評価ボタンや評価カードを表示する前または表示中に質問をしないでください(「アプリを気に入りましたか?」といったユーザーの意見に関する質問など) 実装方法 In-App Review APIはPlay Core SDKの一部なので、gradleファイルにCoreライブラリ1.8.0以上を追加します。 dependencies { implementation 'com.google.android.play:core:1.8.2' } 様々な画面でレビュー要求を表示したいので、In-App Review APIをリクエストして表示する処理を共通関数として作成していきます。 ただし、公式リファレンスにも注意書きされていますが勝手に表示されるViewのデザインやサイズ、背景色等のカスタマイズは一切行わないようにしてください。 import com.google.android.play.core.review.ReviewManagerFactory ... fun showInAppReview(activity: Activity) { val manager = ReviewManagerFactory.create(activity) val request = manager.requestReviewFlow() request.addOnCompleteListener { if (it.isSuccessful) { manager.launchReviewFlow(activity, it.result) } else { // error } } } 最後に Googleから提供されているAPIのため実装が比較的簡単であり、レビュー評価の向上にも繋がるためまだ導入されていない方は是非導入してはいかがでしょうか。 ただ、In-App Review APIをリクエストした際、必ずレビュー要求が表示されるわけではないので、絶対にレビュー要求を表示したい画面は”独自のポップアップ”を表示するように切り分けて使っていくのもいいかもしれません。  ここまで読んでいただき、ありがとうございました。
アバター
はじめに iOSでTableviewやCollectionViewを扱っていると、UIとデータとの間で不整合が起きた際に NSInternalInconsistencyException というエラーを吐いてアプリが落ちるというのはよくある話だと思います。 TableViewに関してはiOS13から UITableViewDiffableDataSource が追加され、Apple曰くこの問題を回避できるらしいので、DELISH KITCHENのiOSアプリで採用してみました。 導入方法 Hashable化 セクションやアイテムに対応するオブジェクトがHashableに適合している必要があります。 今回は対象となるオブジェクトがユニークなIDを既に持っていたので簡単でした。 /// TableViewの各セルに対応するオブジェクト struct Item : Hashable { let id : Int ... /// 追加1 static func == (lhs : MessageDetailRowItem , rhs : MessageDetailRowItem ) -> Bool { return lhs.id == rhs.message.id } /// 追加2 func hash (into hasher : inout Hasher) { hasher.combine(id) } } UITableViewDataSourceをUITableViewDiffableDataSourceに変更する struct Section { let items : [Item] } このようなセクションがあると仮定して class SomeClass : UITableViewDataSource { let sections : [Section] func numberOfSections ( in tableView : UITableView ) -> Int { return sections.count } func tableView (_ tableView : UITableView , numberOfRowsInSection section : Int ) -> Int { return sections[section].items.count } func tableView (_ tableView : UITableView , cellForRowAt indexPath : IndexPath ) -> UITableViewCell { /// cellをデキューして加工して返す処理 } } というUITableViewDataSourceの実装があった場合の変更点を示します。 まず、 Section をHashableに適合させます。 次に、UITableViewDataSourceの代わりにUITableViewDiffableDataSourceを使うように変更します。 class SomeClass { private var dataSource : UITableViewDiffableDataSource <Section, Item> ? func setupDataSource (tableView : UITableView ) { dataSource = UITableViewDiffableDataSource < Section, Item > (tableView : tableView , cellProvider : { [weak self ] (tableView, indexPath, item) -> UITableViewCell ? in /// func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCellと一緒の内容 } } func setupSnapshot () { var snapShot = NSDiffableDataSourceSnapshot < Section, Item > () let sections : [Section] = /// 省略 snapShot.appendSections(sections) sections.forEach { snapShot.appendItems( $0 .items, toSection : $0 ) } dataSource.apply(snapShot) } } setupDataSource はViewControllerのViewDidLoad、 setupSnapshot は setupDataSource より後でデータが取得できたタイミングで実行すれば良いと思います。 また、変更がある場合は現在のスナップショットを dataSource から取得できるので、それに対して変更を加えて再度applyするだけで良いです。 performBatchUpdateなどの処理 dataSourceにapplyしたらTableViewにも反映されるので不要になります。 結果 日に数件エラーが出ていたのですが、0件になりました。 今回はTableViewに対しての改善でしたがCollectionViewにも同様のAPIが存在するので、そちらも改善していきたいと考えています。 参考 https://developer.apple.com/documentation/uikit/views_and_controls/collection_views/implementing_modern_collection_views Appleが提供しているサンプルプロジェクトです。 2021年3月18日時点では WiFiSettingsViewController や TableViewEditingViewController がUITableViewDiffableDataSourceを使っているので参考になると思います!
アバター
はじめに 前提技術スタック pre-commit、CIでのLintチェック、パッケージをクリーンアーキテクチャ構成にする pre-commit 良かった事 CIでのLintチェック 良かった事 パッケージをクリーンアーキテクチャ構成にする 良かった事 まとめ はじめに DELISH KITCHEN開発部の福山です。 社内向けシステムとしてAPI Serverを新規に構築する機会がありました。新規開発にあたり導入してみて良かった事をいくつかご紹介したいと思います。 前提技術スタック 新規GitHubリポジトリを用意してREST APIをGoで実装する。Webフレームワークはechoを採用。インフラはAWS ECS、RDSを利用。ログ情報はfluentd経由でS3やTreasuredataに格納しています。SentryやDatadogによる監視も行っています。 pre-commit、CIでのLintチェック、パッケージをクリーンアーキテクチャ構成にする pre-commit 『DELISH KITCHEN』のメインAPI Serverの方で途中から採用された内容とほぼ同じ内容となります。今回は実装初期段階で pre-commit を導入しました。 pre-commitを使えばローカルでのGit commit時に任意のスクリプトを実行出来ます。 以下の処理が行われる様に設定されています。 - go generate - go vet - gofmt - goimports - golint - wire 主に実装内容の静的解析を行っておりcommit前にルールから外れたコードを発見し修正を促します。 その他go generate契機でmockファイルの作成や wire でDIコードの生成処理( *後述 )を実行しています。 良かった事 コードレビュー時にレビュアーがコードフォーマットや生成ファイルの有無等の指摘をする必要が無くなり、仕様やバグのチェックに集中出来る様になりました。 CIでのLintチェック 自動テストは導入しているのですが、更に実装初期段階からGithub Actionsにて reviewdog/golangci-lint を導入しました。 golangci-lintには 様々なLinter が用意されておりプロジェクト状況に合わせて任意のLinterを利用出来ます。 今回の開発では以下のLintチェックを有効化しています。 - bodyclose - deadcode - depguard - errcheck - goconst - gocritic - gofmt - goimports - gosec - gosimple - govet - ineffassign - interfacer - misspell - nakedret - noctx - prealloc - scopelint - staticcheck - structcheck - typecheck - unconvert - unparam - unused - varcheck pushした内容に指摘対象のコードが存在する時にGithub上で以下の様に表示してくれます。 reviewdog/action-golangci-lint 良かった事 pre-commit と同じなのですがレビュアーに指摘される前に自動チェックが行われる為、コードレビュー時に仕様やバグのチェックに集中出来る様になりました。 具体的には gosec でセキュリティ面での指定が有ったり、 gosimple でシンプルなコードの書き方に気付かされたりとコードの品質向上のきっかけとなります。 特に実装初期段階から導入した事によりほぼ全てのコードが随時チェックされている事になるので途中から採用するよりオススメです。 パッケージをクリーンアーキテクチャ構成にする 既存のシステムでMVC的な構成で苦労する場面が有りました。Goで domain/model 的なパッケージとデータの永続化処理は切り離したいと考え今回はクリーンアーキテクチャ構成で実装する事にしました。 (色々なクリーンアーキテクチャの詳細解釈が存在するのであくまで一例とお考え下さい。) ざっくり過ぎる図解なのですが以下の様なパッケージ構成となっております。(→は依存方向) パッケージレイヤー構成の一例 最低限下記は意識しつつ、ルールに拘り過ぎて工数が掛かり過ぎない様に状況に応じて詳細実装を進めました。 - 依存方向を守る(domainは他に依存しない) - 抽象に依存する(interfaceに依存する) handlerパッケージから wire で生成されたコードでDependency Injectionされる構成となっております。 動作のポイントとしては domain/repository で定義されたinterfaceの詳細実装は infrastructure に存在しており、注入された内容として実行される様になっています。 他にはWebフレームワークの固有処理もhandler内に閉じており usecase 以降には影響しない様になっています。 良かった事 テストの容易性 一般的に言われている事ですがテスタブルになりました。 MVC構成だと要件ロジックのユニットテストを書く時も依存するデータベース情報等を実際に用意する必要が有りました。しかし抽象に依存しつつパッケージレイヤーを切った事により、例えば usecase でのユニットテスト時にパッケージ内で扱う永続化処理に対してmockを利用し任意の結果が返却出来る事になります。 従ってテスト対象パッケージ以外の状態に悩まされる事無く確認したい要件ロジックに集中してユニットテストを行う事が出来る様になりました。 ドメイン情報の認識が深まる 単一パッケージにほぼ全てのロジックを詰める様な形だと肥大したり処理の責務等は考えなくなってしまう可能性が高いのですが、 要件ロジックをどのパッケージレイヤーに書くべきかを個人的にもチーム内でも意識する様になりました。 実際にPRレビューの時にチームメンバーと議論する事が有り、結果として当初より見通しの良いコードになる事が有りました。 この部分は個々人の解釈の違いも有るので工数が膨らみ過ぎないバランスで進める様にしています。 まとめ 新規開発を行うタイミングで導入して良かった事として pre-commit 、 CIでのLintチェック 、 パッケージをクリーンアーキテクチャ構成にする をご紹介しました。 工数面のバランスや未経験な技術要素を導入するリスクも有りますが開発工程初期に開発効率を向上させる仕組みを用意するメリットは大きいと考えています。 まだ開発は続きますので良い仕組みを活かして効率良くアウトプットしていきたいと思います。
アバター
Core Web Vitalsの計測環境を整える はじめに 現在、MAMADAYSのWebチームでは昨年発表されたCore Web Vitalsを中心としたパフォーマンス改善に注力しています。 今回はパフォーマンス改善でも重要な計測部分について、MAMADAYSではどのようにCore Web Vitalsのデータを定点観測する環境を整えているのかをご紹介したいと思います。 Core Web Vitalsとは Core Web Vitalsとは、全てのサイトにおいて共通してユーザー体験をよくするために重要な、Google社が提唱するパフォーマンス指標のことです。本記事ではCore Web Vitalsの解説を目的としないため、詳細な説明は割愛しますが、Core Web VitalsにはLCP・FID・CLSという3つの具体的なパフォーマンス指標があり、将来的にはGoogle検索のランキング要因にも組み込まれると言われています。 画像出典: https://web.dev/vitals/ LabデータとFieldデータ パフォーマンス改善をする際に重要になってくるのがパフォーマンスの定点観測ですが、計測データは大きく分けて以下の2種類があります。それぞれにメリットとデメリットがあるので、両方をうまく使い分けながらサイトのパフォーマンス観測を行っていくことが大切になります。 Labデータ : Googleが開発するLighthouseなど特定の環境下で収集されたパフォーマンスデータのことです。特定の環境下で行うことにより再現可能なデータを提供でき、パフォーマンス観測もしやすいのがメリットですが、実際の利用者との実行環境の差異がある可能性があります。 Fieldデータ : 利用者の実際の環境下で収集されたパフォーマンスデータのことです。実際の利用環境のパフォーマンスが収集できることがメリットですが、収集するデータにはばらつきがあるためFieldデータに比べると観測がしにくいです。 参考: https://web.dev/how-to-measure-speed/#lab-data-vs-field-data 計測環境の検討 計測環境の検討にあたっては有料の計測サービスの SpeedCurve やNext.jsでVercelを使っていればNext.js製の Analytics も候補に出ると思います。ただ、MAMADAYSではBIツールとしてMetabase、分析データの保存先としてBigQueryを使っているのでうまく既存のアセットを生かした形でコストをかけずに実現する方法を模索していました。 Labデータの計測 Labデータの計測にあたっては、 PageSpeed Insights API を利用してLabデータの収集を行っています。 PageSpeed Insights はブラウザでサイトのパフォーマンスを確認できるツールとして便利ですが、APIも用意されており、簡単に同じデータを取得することができます。 // PageSpeed Insights APIのレスポンスの一部抜粋 { " lighthouseResult ": { " audits ": { " largest-contentful-paint ": { " id ": " largest-contentful-paint ", " title ": " Largest Contentful Paint ", " description ": " Largest Contentful Paint marks the time at which the largest text or image is painted. [Learn more](https://web.dev/lighthouse-largest-contentful-paint/) ", " score ": 0.92 , " scoreDisplayMode ": " numeric ", " displayValue ": " 1.1 s ", " numericValue ": 1110 } , " total-blocking-time ": { " id ": " total-blocking-time ", " title ": " Total Blocking Time ", " description ": " Sum of all time periods between FCP and Time to Interactive, when task length exceeded 50ms, expressed in milliseconds. [Learn more](https://web.dev/lighthouse-total-blocking-time/). ", " score ": 0.97 , " scoreDisplayMode ": " numeric ", " displayValue ": " 110 ms ", " numericValue ": 105 } , " cumulative-layout-shift ": { " id ": " cumulative-layout-shift ", " title ": " Cumulative Layout Shift ", " description ": " Cumulative Layout Shift measures the movement of visible elements within the viewport. [Learn more](https://web.dev/cls/). ", " score ": 1 , " scoreDisplayMode ": " numeric ", " displayValue ": " 0 ", " details ": { " items ": [ { " finalLayoutShiftTraceEventFound ": true } ] , " type ": " debugdata " } , " numericValue ": 0.00018970055161544525 } } } } 注意点として 公式でも記載 されていますが、Lighthouseのように特定の環境下でユーザーなしにパフォーマンス計測をする場合にFIDは計測できません。したがって、LabデータでFIDの計測を行いたい場合は代替手段としてFIDと相関のあるTotal Blocking Time (TBT)を見るようにします。 MAMADAYSではこちらのAPIを利用して、複数ページを2時間おきにデータを収集し、BigQueryに転送しています。1回のみ特定のページを毎日計測する方法だとパフォーマンスデータとしてはあまりにも信憑性に欠けてしまうので複数のページで頻繁にデータを取得するようにしています。 Fieldデータの計測 パフォーマンス改善に取り組み始めた当初、前述したLabデータの観測のみを行っていました。ただ、Labデータのみだと実際の環境下でのパフォーマンスデータが観測できないことが課題としてあり、Fieldデータの計測方法を検討しました。 Next.jsとGoogle Analyticsを利用した計測基盤の構築 まずはWeb側のデータ収集方法ですが、MAMADAYSのWebではNext.jsを採用しており、Next.jsはバージョン9.4から標準機能としてCore Web Vitalsの計測を行えるようになったのでその機能を使って公式のガイドを参考に実装しました。また、収集したパフォーマンスログはすでに連携済みだったGoogle Analyticsのイベントとして保存することで継続してパフォーマンス推移を観測できる環境を作りました。 // pages/_app.js // googleAnalyticsのイベントとしてパフォーマンスデータを保存 function performanceMetricsEvent( { id, name, label, value } ) { const eventValue = Math.round(name === 'CLS' ? value * 1000 : value); window .gtag( 'event' , name, { event_category: 'パフォーマンス' , value: eventValue, event_label: id, non_interaction: true , } ) } // Next.jsの標準機能 reportWebVitalsを定義する export function reportWebVitals(metrics) { performanceMetricsEvent(metrics); } 参考: https://nextjs.org/docs/advanced-features/measuring-performance 直面した問題点 しかし数週間こちらの計測方法で検証していたところ、送っているイベントのラベルがページロードごとのユニークな値にしているため、ラベル数が上限に達してしまい他のイベントに影響を及ぼしてしまう問題がGoogle Analyticsのアラートから発覚しました。 その時点で対応するのであれば、全体の利用者の何割かに絞って計測をすることで上記の問題は解決できそうでしたが、今後利用者の増加を考慮して計測基盤の見直しを行いました。 計測方法の改善 計測基盤を見直すにあたって、MAMADAYSでは分析にBigQueryを使用しているためBigQueryへの転送を考えました。 また大量のパフォーマンスログのデータ転送をアプリケーションとは切り離して行うために、サーバー側はパフォーマンスのログ出力のみを行い、fluentdでBigQueryへのストリーミング挿入し、dailyでシャーディングテーブルを作るように変更しました。fluentdでは fluent-plugin-bigquery というgemを使うことによって簡単にfluentdでのBigQueryへのストリーミング挿入が実現できます。 ログの出力形式 {"id":"1618905791407-4433185739018","label":"web-vital","level":"INFO","name":"LCP","path":"/articles/999","time":"2021-04-20T08:03:11.870117321Z","type":"WEB_PERFORMANCE","value":"1500"} fluentdでのinsert部分の設定 <label @web-performance-log> <filter **> @type grep <regexp> key $.parsed_log.type pattern ^WEB_PERFORMANCE$ </regexp> </filter> <filter> @type record_transformer renew_record enable_ruby <record> id ${record["parsed_log"]["id"]} time ${record["parsed_log"]["time"]} label ${record["parsed_log"]["label"]} name ${record["parsed_log"]["name"]} path ${record["parsed_log"]["path"]} value ${record["parsed_log"]["value"]} </record> </filter> <match **> @type bigquery_insert auth_method json_key json_key /etc/secrets/google-credentials/fluentd-to-bq.json project "#{ENV['BQ_PROJECT']}" dataset "#{ENV['BQ_DATASET']}" table web_performance_%Y%m%d auto_create_table true <buffer time> @type file flush_interval 30s path /var/log/fluentd-buffers/bq-event.buffer timekey 1d </buffer> schema [ {"name": "id", "type": "STRING"}, {"name": "time", "type": "STRING"}, {"name": "label", "type": "STRING"}, {"name": "name", "type": "STRING"}, {"name": "path", "type": "STRING"}, {"name": "value", "type": "STRING"} ] </match> </label> この改善により、BigQueryのストリーミング挿入でコストが多少掛かってしまいましたが、他の分析への影響を与えずにFieldデータの継続的な観測を実現できました。また、Google Analyticsへのデータ保存時にはMetabaseというBIツールで計測結果が見れるようにBigQueryへのデータの加工と転送を自前で別途行う必要がありましたが、直接BigQueryに転送できたことでその手間も省ける結果となりました。 まとめ 今回はWebパフォーマンスの計測でCore Web Vitalsをどう計測しているのかについて話しました。パフォーマンス改善において、憶測ではなく現状のボトルネックなどを正しく理解して改善する上でもパフォーマンスの継続的な計測は重要になってくると思います。計測方法やGoogle Analyticsでの問題に関して同じような課題に直面されている方の参考になれば幸いです。 MAMADAYSのWEBチームではパフォーマンス改善に注力しており、改善結果も出ているので実施した改善内容についても今後お話していきたいと思います。
アバター
はじめに 振り返り会の意義 振り返り会のやり方 ファシリテーターを誰が担当するのか 何について振り返るのか 前回の振り返り会を確認する やったこと・良かったことを洗い出す もっと良くできそうなことを洗い出す 共通認識を生み出す 批判する会ではない やってみたいことを考える よくあるNGパターン やることを決める おわりに はじめに 昨今のコロナウィルス感染拡大に伴う対応として弊社ではリモートワーク中心の働き方に変化し1年ほどが経過しました。 働き方が大きく変わっていった状況の中で、滞りなくチーム開発が進められた要因の1つが毎週開催している振り返り会にあったのではないかと私は考えています。 今回は、以前私が所属していたDELISH KITCHENのバックエンド開発のチームとプロダクトマネージャーとの間ではどのように振り返り会を実践してきたのかを紹介させていただきます。 振り返り会の意義 計画して実行した結果に対して「何が良かったのか?何が悪かったのか?次はどうするのか」を考える、いわゆるPDCAサイクルを回すことの有意性については今更議論する必要がないと思います。 PDCAサイクルによる改善活動は、個人で行う仕事であれば自分がやったことを見直し次に活かせば良いので簡単に実現できるのですが、チームで行う仕事の場合は誰か1人の力だけで行うのは非常に困難です。 リーダーが1人でチームの改善活動を行う場合、リーダーの力量以上にチームが成長することは難しいでしょう。それはリーダーの視点から気付ける課題や改善策に限定されてしまうからです。 リーダーからすると取るに足らない些細な課題が実は複数のメンバーが感じている重要な課題かもしれませんし、ある課題に対してリーダーが考えつかないような改善策が他のメンバーから提案されるかもしれません。 基点となる1人のフィルターを通してしまうと、その人の考えに大きく依存してしまいチームはいずれうまく動かなくなることが予想されます。 振り返り会では様々な課題をチームの課題として捉え、メンバーが相互作用しながら解決に導くことでチームのPDCAサイクルを回します。 また、プラクティスの共有や課題についての議論を行う対話の場ができることによって「協調するチーム」作りに寄与する重要な機会になると考えています。 振り返り会のやり方 チームで行っている振り返り会は、週に1回/半期に1回行う定期的なものとプロジェクトごとに行う不定期なものがありますが、今回は週に1回定期的に開催しているやり方について取り上げたいと思います。 やり方はKPTをベースにいくつかのオリジナリティを加えており、参加メンバーはPdMとエンジニアの4-6人ほどで開催しています。 全体は以下のような流れになっています。 前回の振り返りを確認する 取り組んだアクションはどうだったのか 解決していない課題は何か やったこと・良かったことを洗い出す もっと良くできそうなことを洗い出す やってみたいことを洗い出す やることを決める ファシリテーターを誰が担当するのか 振り返り会の進行を行うファシリテーターは職種によらず参加メンバー全員の持ち回りで進行しています。これはメンバーそれぞれがやり方を工夫する余地を持たせるためです。 最適な振り返りの方法はチームや状況によって変わるため自分がファシリテーターの時には自由にアレンジすることが許されており、振り返り自体をより良くするための案として採用しています。 また、ファシリテーターを固定してしまうとどうしても参加させられてる感・他人事感が出てきてしまうと考えているため、持ち回りにすることで自分たちのために開催しているという当事者意識を持ちやすくする効果があります。 何について振り返るのか 振り返り会で最初にやるべきことは、何について振り返るのか認識を合わせることです。 1週間を振り返るという抽象的なテーマで始めると出てくるトピックの粒度にばらつきが生じ時間配分がとても難しくなるでしょう。 振り返りの勘所がわかっているチームであれば問題ありませんが、多くのチームでは具体的なテーマを決めて何について話すかを明確にした方がスムーズに進行できるでしょう。 多くの問題を抱えたチームが自由に問題点を列挙するような振り返り会の場合、広く浅く問題について話したことで満足してしまい結局何も解決されていないなんてことは良くあるのではないでしょうか。 定期開催している場合1回の振り返り会にかける時間は短いでしょうし、次の振り返り会までに取り組めるアクションは限られるため一度に多くの問題を解決しようとせず、まずは問題の1つをテーマとして取り上げて確実に改善に取り組んでいくのが良いと思います。 と、書きましたが実際にチームでは特にテーマを決めずに1週間を振り返っています。 これは1年以上毎週振り返り会を続けており、チームの中で共通のナレッジになっているものやすでに解決した課題が大半で抽象的なテーマでもうまく進められる状態になっているからです。 前回の振り返り会を確認する 2回目以降の振り返り会の場合、まずは前回の振り返り会を確認するところから始めます。 前回決めたアクションに取り組むことができた場合結果はどうだったのか、継続していくべきかを話し合います。取り組んでみた結果効果がなければ他にやってみたい案を考えます。 取り組むことができなかった場合、なぜできなかったかを考えます。時間がなかっただけなのか何か問題があるのかを明らかにします。何度も時間がないことが理由になる場合、そのアクションは重要ではないことが多いため思い切ってやめてしまうこともあります。 前回あがった問題の中でまだ解決できていない問題についてもここで確認します。何か進展があれば議論し、解決のためにやってみたいことがあれば案を出し合います。 大きな問題は1回の振り返り会で解決できないことがあるため、このように次回に持ち越していき少しずつ解決のために取り組んでいきます。 やったこと・良かったことを洗い出す 今週やったこと・良かったことをできるだけ多くあげていきます。 これはYWTという振り返り手法におけるY(やったこと)とKPTにおけるKEEP(今後も続けたいことや良かったこと)を融合させたフェーズです。 Y(やったこと)もあげるのは今週起きたことを全員で思い出すためと、話しているうちに良かったことや課題が見つかることがあるためです。 また、良かったことのみとすると素晴らしい出来事をあげなくてはいけない気がして、全く出てこなくなってしまうことを避けるためです。 良かったこととしてあげるほどでもないことを、やったこととしてならば言いやすいこともあります。 例えば「〇〇の機能を無事リリースしました!」などです。スケジュール通り問題なくリリースできたならば良かったこととして捉えられますが、人によっては当然のことと考えるかもしれません。 深掘りしてみると実はスケジュール通り進めるために様々な工夫しており、チームのナレッジにすべきことが隠れているかもしれません。 もっと良くできそうなことを洗い出す ここで重要なのは「問題点」ではなく「もっと良くできそうなこと」を洗い出すことです。 「問題点」としてしまうと現在発生している問題にのみフォーカスしてしまい、今後問題になりそうなことやなんとなくモヤモヤしていることについて話す場がなくなってしまいます。 問題になっていない些細なことを共有するのは非常に大切です。 誰も気付いていない今後大きな問題になる可能性に気づくことができるかもしれませんし、話してみた結果問題ではないことを知ることができるかもしれません。 いずれにせよ周りのメンバーが事象に対してどのように捉えているかを知れる機会になり、チーム内の相互理解を促進させてくれるはずです。 共通認識を生み出す 実際の振り返り会で「プルリクエストのレビュー依頼が多く出ていたので優先的に進めるべきだった」という意見がありました。 当事者としてはレビューを溜めてしまったことに問題を感じて出した意見だと思いますが、チームとしては限られたリソースの中でレビューを回しており、差し込みの対応依頼などもあったため妥当な対応で問題ではなかったという着地になりました。 「問題ではなかった」という結論を導くための対話を通じて、チーム内にこのような状況であれば「レビューが溜まることがある」という共通認識が生まれています。 今後同じ状況になった時レビューする側は必要以上に焦ってレビューせずにすみますし、レビューされる側も時間がかかりそうということを事前に認識することができます。 このように振り返り会では問題を解決するだけでなく、共通認識を作ることができるという点でも効果的な機会となっています。 批判する会ではない このフェーズでは問題を起こした誰かを責めるのではなく、チームとして もっと良くできそうなことを考える というポジティブな議論指向が重要なポイントだと思います。 他のフェーズにも共通して言えることですが意見を出すハードルを下げることが大切で、課題感はあるけど自分が責められそうだからやめておこう、、、とならない雰囲気づくりを心がける必要があります。 やってみたいことを考える 「もっと良くできそうなこと」のためにやってみたいことや、新しい試みとしてやってみたいことをあげます。 このフェーズではやってみたいことをできるだけ多く考えるブレスト形式であることを重視しています。 突拍子もないアイディアから素晴らしい改善策を思いつくかもしれませんし、現実的ではない理想論から妥当な策に落ち着かせることができるかもしれません。 よくあるNGパターン 問題の逆を実行する改善案があげられることがあります。「〇〇ができていなかった」という問題に対し「〇〇をやる」というようなものです。 例えば「レビュー依頼を溜めてしまった」という問題に対し「溜めないようにする」といった改善案です。大抵の場合このような案は精神論になり解決に導くことはできないでしょう。 そのためにとるべきアプローチとして「レビュー依頼を溜めてしまった」ことでどこに支障をきたしているのか、何が要因なのかを整理しましょう。 「レビュー依頼を溜めてしまった」のならば「レビューがボトルネックになりリードタイムが長くなる」ことが実質的な問題点で、要因は「レビューに時間がかかる」「レビュー依頼されていることを忘れていた」「レビュアーが1人しかいない」など様々考えられるでしょう。 要因によって改善策は大きく変わるため、ファシリテーターを中心に分析を行ってからやってみたいことを考えるようにするとスムーズに進行できます。 やることを決める やってみたいことをブレストした後、このフェーズで次の振り返り会までに取り組むアクションを決めます。 たくさんの案が出ているはずなので、実際に実行できる粒度・内容に整理する必要があります。 あまり多くのアクションを決定しても実行できないため、いくつか選択するのが良いでしょう。選択の仕方は効果的なものを選んでもいいですし、投票でもいいです。 チームでは、やるべきことを決めたらタスク管理ツールで管理するようにしており、必要であれば担当者のアサインや期限までその場で決めてしまいます。 おわりに 以上、チームで実際に行っている振り返り会のやり方を紹介させていただきました。 私の考えが多分に含まれているためチームメイトは違う考えを持って振り返りをしているかもしれません。 チームや状況によって適したやり方は異なるため上記の方法では上手くいかないこともあると思います。また、最初から効果的な振り返り会を行うのは難しいかもしれません。 しかしながら振り返り会自体の改善を行ったり、チームの問題を解決していくプロセスは「協調するチーム」作りに大きく寄与すると思いますので、是非継続して振り返り会を開催してみてください。 これから振り返り会をやってみようという方、やり方を模索している方の参考になれば幸いです。
アバター
Delta LakeとLakehouseプラットフォームによるデータウェアハウス設計  こんにちは。ビッグデータ処理基盤の物理レイヤーから論理レイヤーの設計実装、データエンジニアやデータサイエンティストのタスク管理全般を担当している、Data/AI部門の 何でも屋 マネージャの @smdmts です。  この記事は、弊社のデータ基盤の大部分を支えるDelta LakeとLakehouseプラットフォームによるデータウェアハウス設計の紹介です。 Databricks社が主体となり開発している Delta Lake をご存じでしょうか?  Delta Lakeは、Apache Sparkを利用したLakehouseプラットフォームを実装可能とするオープンソースです。 Lakehouseプラットフォームの詳細は、こちらの 論文 に記載されています。 Lakehouseプラットフォームとは、一つのデータレイクのプラットフォームにETL処理、BI、レポート、データサイエンス、マシンラーニングを搭載することで、性能面やコスト面・仕様変更に強いなど、多方面で有利に働くとされます。 Delta Lakeとは  Delta Lakeは、以下の公式サイトの delta.io の図にあるとおり、S3やGCSなどのストレージレイヤーに機械学習や目的別に特化したデータ構造のアーキテクチャパターンです。 Delta Lakeは主にApache SparkからのRead/Writeをサポートしていますが、制約つきで Presto/Athenaによる読込 もできます。 DELTA LAKE  公式サイトで紹介されている以下の動画によると、Delta Lakeを利用した場合のデータ構造を、以下のように、Bronze、Silver、Goldと定義される三段階に構造を分離すると、より信頼性の高いデータレイクの構築可能にするとされます。 ステージ データの内容 Bronze Ingestion Tablesと呼ばれる、生ログを保存するステージ Silver Refined Tablesと呼ばれる、Bronzeテーブルをクレンジングした中間テーブル Gold Feature/Aggregation Data Storeと呼ばれる、目的別に特化したテーブル Delta LakeとLakehouseプラットフォーム  Delta Lakeに関わらずデータレイクで何らかのデータを取り扱う場合、アプリケーションのドメイン知識の考慮が必要です。 一般的なアプリケーションでは、ドメイン知識の原料となるユビギタス言語を元にデータモデルの設計がされますが、イベントソーシングを利用しない限り、ドメインモデルが出力するデータモデルの変更は可能です。 たとえば、 DELISH KITCHEN は、レシピ動画を視聴出来るサービスですが、「動画」と「レシピ」などのコアとなるドメインモデルがある事に対して、仕様変更などで「レシピ」に何らかの新しい付加情報となるデータモデルの変更や追加は可能です。  一方でデータ基盤におけるドメイン知識とは、KPIやKGIなどの観測したい対象を指します。 たとえば、動画におけるデータ分析のドメイン知識では「視聴数」や「視聴維持率」などがその対象となります。  データウェアハウスで管理されるイベントログは、基本的に過去に保存したデータモデルの変更は許されず、将来仕様変更が発生した場合でも、データ構造はKPIなどの観測したい事象に追随する必要があります。 そのため、以下のように各ステージ毎の領域別でドメイン知識の保有などの考慮が必要となります。 Bronzeステージ(生ログ) データソースから発生するデータ構造を極力変更しないデータ領域 基本的に生ログで最小限の構文解析のみ行いドメイン知識を有さない Silverステージ(クレンジング/一次集計テーブル) データ構造の仕様変更などに追随するバッファーとなるデータ領域 BronzeステージとSilverステージのデータを集計対象とする 生ログからイベント毎に分割するなど最小のドメイン知識を有する Goldステージ(最終集計テーブル) ビジネス上の価値が観測できる多くのドメイン知識を有するデータ領域 SparkやPrestoなどから読み込まれる BIツールやMLなどから利用し、エンドユーザーの知識や知恵となり得る  このように各ステージ毎にデータが持つ役割を明確にすると、観測対象となるドメイン知識の全てがGoldステージに集約されます。 また、ドメイン知識の原料となるデータとして、SilverステージとBronzeステージにデータが保存されると明文化されます。  Bronzeステージには生ログが保存され、Silverステージにはイベント毎などで分割された最小限の粒度となるドメイン知識を有するデータが保存されます。 データが保持する情報の抽象度はBronze、Silver、Goldの順番に上がり、最終的にビジネスに何らかの役に立つドメイン知識となる情報がGoldステージで参照可能となります。  Lakehouseプラットフォームのアーキテクチャは以下の図の通り、データレイクに対して一つのエンドポイントでさまざまなデータを参照可能とする仕組みです。 データレイク内のデータをドメイン知識の保有の有無など抽象度の異なるデータをBronze、Silver、Goldと分離すると、データガバナンスに良い影響をもたらす事が期待できます。 Lakehouseアーキテクチャ Delta Lakeと関心の分離  ビッグデータの処理基盤は入力元となるデータ源泉は多種多様でカオスになりがちですが、Lakehouseプラットフォーム内のデータ構造をBronze、Silver、Goldの各ステージでデータを蒸留すると、関心の分離が促進されます。 関心の分離はSoC(Separation of Concerns)とも呼ばれ、オブジェクト指向設計やモジュール設計で重要とされる「凝集度」や「結合度」の観点から重要な概念です。 Delta Lake内の各データ領域を利用者別に分類すると、以下のように分離できます。 Bronzeステージ(生ログ) データ入力部分を処理担当するインフラエンジニア SaaSによる外部入力データ連係を担当するデータエンジニア Silverステージ(クレンジング/一次集計テーブル) ドメインモデルを構築するデータエンジニア 自分が担当したアプリ成果を確認するアプリケーションエンジニア 探索的データ分析を行うデータサイエンティスト 目的となるKPIの検討を行うプロダクトマネージャ Goldステージ(最終集計テーブル) 機械学習のモデル精度をチューニングするデータサイエンティストや機械学習エンジニア 対顧客や経営層へのレポーティングを行うデータアナリスト 日々のKPIを観測する事業責任者や経営者、プロダクトマネージャ  データ領域における関心の分離は、各ステージのデータ設計や最終的な可視化対象の選定に当たる洞察に良い影響を与えます。 たとえば、アプリケーション開発者が開発した機能の状況を把握するためにはSilverステージを参照すれば、機能が正常に動作しているかを把握できます。 また、達成されるべきKGIに因果関係があるKPIがはっきりしない場合は、Silverステージのデータから探索的データ分析によりKPIの検討が可能です。  データが保持する抽象度がBronze、Silver、Goldと順番に上がることの裏返すと、Gold、Silver、Bronzeの順番にデータ量が増え探索可能となる情報が増えるということです。 一度集計してしまうと集計前のデータが欠落してしまうことから、新たな洞察を得たい時にはSilverステージより前のデータを利用したい場合もあります。 Goldステージのデータは特定の目的以外のデータは保持しないことからデータの持つ柔軟性は低いです。 観測したいKPIが未知の場合は、前ステージのSilverステージやBronzeステージのデータを集計し、Goldステージに昇格させるべきか検討する必要があります。  実際のアプリケーション運営の現場ではLTVなどのKGIに因果関係があるKPIを試行錯誤して発見に至るケースも多く、しばらくの間はBIツールからはSilverステージのテーブルをスキャンする事も珍しくありません。 一方でSilverステージはGoldステージと比較してデータ量が多くなることから、計算量や処理コストの観点では不利に働きます。 そのためSilverステージのスキャンで観測したいKPI決まると、Goldステージのデータを作成するバッチを作成し、BIツールからはGoldステージのテーブルを参照するようになります。  このように、データが保持する主な情報を各ステージ毎に分離すると、データ軸でも利用者毎の関心の分離が促されます。 「システムを設計する組織は、その構造をそっくりまねた設計を生み出してしまう」とコンウェイの法則の有名な一説がありますが、データ構造とその配置を定義するだけで、利用者毎の関心が綺麗に分離するのは興味深い事例ではないでしょうか。 Delta Lakeがデータレイクにもたらす恩恵  今回はDelta Lakeの機能詳細に触れませんでしたが、Delta LakeにはUpsertを可能とするMerge文、過去に保存した時点のデータに巻き戻すTime Travelなど様々な便利な機能が実装されており、Bronze、Silver、GoldのステージのETL処理を強力にサポートします。 たとえば、Bronzeステージは生ログのためアプリケーションの実装の都合で頻繁にカラム追加などのデータ構造が変更されますが、自動的にスキーマの変更を検出してマージするスキーマオートマージ機能は非常に便利です。  私が所属するデータ/AI部門のデータ基盤では、一部の機能をDelta Lakeを利用したLakehouseプラットフォームで実装していますが、仕様変更が頻繁に発生するデータ領域でもアジリティ高く即日〜三営業日程度で観測したいKPIを追加できる状況が実現できています。  データ構造をBronze、Silver、Goldとステージを分解するだけでも、データ利用者の関心の分離を促し、データガバナンスにも数多くの恩恵をもたらすため、データウェアハウス設計の参考にして頂ければ幸いです。  ここまでお読みくださり、ありがとうございました。
アバター
データ分析する前に知っておきたい因果関係と相関関係 はじめに エブリーでデータアナリストをしている近藤と申します。 元々サーバーエンジニアでGoを書いていましたが、昨年7月からデータアナリストとして働いています。 普段はデータガバナンスの整備やredashによるデータ提供、データによる営業支援といった業務を行っています。 因果関係と相関関係の理解 データ分析を行う意義は、データの規則性を見つけて活用し、ビジネスをドライブさせることです。 しかし、見つけた規則性の解釈を誤るとビジネスに役立たず、貴重なリソースを浪費してしまいます。 規則性を見つけて終わりではなく、見つけた規則性が一体何を意味するのかを常に考えなければいけません。 特に相関関係と因果関係の混同はよく起こりうる問題です。相関関係だけをみて因果関係があると判断すると、おそらく効果のある施策を打つことはできないでしょう。 因果関係と相関関係の違いの理解はデータ分析をする上では必須と言えます。 そこで、因果関係と相関関係を理解してデータ分析をするための考え方をまとめたスライドを作成しました。 テックブログなのにSEO最悪なのでCTOに怒られそうですが、自分が伝えたいことはスライドのほうが伝わるのでスライドにしました。 是非ご覧いただければ幸いです。 まとめ 相関関係を見つけると因果関係がどのように存在しているのかを考え、仮説を立ててリサーチデザインを決め、データを収集・分析し、因果関係に迫っていく必要があります。 相関関係と因果関係を混同しないように気をつけましょう!!
アバター
運用していたAPI Serverが気づいたら異常終了するようになっていた話 はじめに 今回は運用していたAPI Serverが気づいたら異常終了するようになっており、原因の特定と対策をした話をしようと思います。 発生していた障害 今回発生していた障害の詳細は以下になります。 ECS上で運用していたAPI Serverが異常終了するようになっていた タスクの終了ステータスを監視するスクリプトを動かし始めたタイミングで発覚 ExitCode 2 でタスクが終了している 異常終了は発生する日としない日がある 同一の日に複数回発生はしていない 異常終了が発生するのは12時から13時の間 タスク数は2で起動していたが、2つのタスクが同日に異常終了することはなかった 異常終了する直前のメトリクスに通常時と異なる箇所は見られなかった 外形監視はしていたのですが、タスクの終了ステータスは監視していなかったため発見が遅れました。 また、発見が遅れたためどの変更が原因でいつから異常終了するようになっていたのかがわからない状態でした。 原因調査 調査1 : コードの更新 まず最初に ExitCode 2 でタスクが終了していることから panic が発生しているのではないかと考えました。 今回異常終了していたAPI Serverは、同一のdocker imageを使用し、環境変数によって内部向け・外部向けを変更する構成になっており、外部向けの方でのみ異常終了は発生していました。 外部向けのAPI Serverに関しては、自動デプロイの対象になっておらず直近でデプロイも行われていなかったため、内部向けAPI Serverと差分が発生している状態でした。 差分が発生し、外部向けAPI Serverでのみ異常終了が発生していたため、差分に原因があるのではないかと考え差分をなくすためにデプロイを実施しました。 しかし、差分がなくなった状態でも状況に変化はなく、外部向けAPI Serverでのみ異常終了は発生し続けました。 調査2 : アクセスに起因したものではないか 調査1にて内部向けとの差分をなくしても状況に変化がなかったで、次は特定のリクエストによって発生しているのではないかと考えました。 API Serverではアクセスログを出力していたのですが、このアクセスログはレスポンスを返すタイミングで出力していたため、処理の途中で異常終了してしまった場合にはログは出力されていません。 そこで、調査のために処理の途中でも適宜ログを出力するようにして、処理途中で異常終了した場合にもどんなリクエストが来ていたかわかるよう変更を加えました。 しかし、異常終了が発生した後にログを確認したところ、該当の時間に処理を行っているログは出力されていませんでした。 調査3 : システム系を疑う 調査2によって、リクエストによって発生しているわけではないことがわかったので、API Serverのコード以外の要素で異常終了する理由がないかと考え調査を続けていました。 異常終了が発生するのは12時から13時の間だけのため、この時間帯に何かしらの処理が動いて、それが原因なのではないかと考えました。 API Serverのコンテナが動いているインスタンスにて該当の時間帯に動いている処理を確認したところ、ログローテートの処理がありました。 ログローテートの設定は下記のようになっていました。 { missingok notifempty compress delaycompress daily rotate 7 postrotate docker container kill -s HUP `docker ps | grep <image-name> | awk '{print $1}'` 2> /dev/null || true endscript sharedscripts } ログローテート後に、ログの出力先ファイルを変更するために条件に合致するコンテナに対して SIGHUP シグナルを送っていました。 ここではシグナルを送る先として grep <image-name> で対象のコンテナをしぼっています。 調査1にて記載していますが、異常終了していたAPI Serverは同一のdocker imageを使用し、環境変数で内部向け・外部向けを変更するようになっています。 そのため、内部向けと外部向けのAPI Serverが同一のインスタンスに存在した場合、実際にはログローテートをしていない方のAPI Serverにもシグナルが送られるようになっていました。 どちらのAPI Serverでも SIGHUP をハンドリングするようになっている場合には問題はないのですが、外向けのAPI Serverでは SIGHUP のハンドリングをするようになっていませんでした。 確認のため、検証環境にて外向けのAPI Serverに対して SIGHUP シグナルを送ってみると異常終了することが確認できました。 行った対応 原因の特定ができたので、対応策を考えます。 今回候補に上がった対応策は下記の3つになります。 - SIGHUP を送る先の抽出条件を修正する - 内向けと外向けのimage名を分離する - シグナルをハンドリングする 本来でしたら3つすべて実施したほうがいいのですが、 まずは応急処置として実装工数が一番少なく済むと判断した、シグナルハンドリングの修正を行うことにしました。 DELISH KITCHENではGoでAPI Serverの実装を行っており、Goではシグナルハンドリングos/signageパッケージに定義されている Ignore メソッドを使えばできます。 https://golang.org/pkg/os/signal/#Ignore 実際に追加した処理は下記になります。 signal.Ignore(syscall.SIGHUP) 上記の対応を実施したあと、検証環境にて外向けのAPI Serverに SIGHUP を送ったところ問題なく稼働し続けていることが確認できました。 振り返り 今回は ExitCode 2 でAPI Serverが終了していたという情報と障害が発生していた時間から原因を想像して、対処をすることができました。 対応後にチーム内にて簡単に振り返りを実施してみたところ、トレースを実施することでより詳しい情報が取得でき、原因の特定がスムーズにできたのではないかという意見がありました。 トレースする対象としてはシステムコール・パケット・ブロックIO等が考えられます。 今回の障害の場合、システムコールをトレースしてみれば SIGHUP が送られて来ていたことがわかったはずです。 実際にシステムコールをトレースしてみた例を下記に示します。 今回障害が発生していたAPI ServerはGoで記述したものをdocker上で動かしており、dockerを動かしているホスト及びAPI Serverが起動しているコンテナ内に strace がインストールされていないため、PID名前空間を共有したコンテナを起動し、起動したコンテナ内で strace を実行しています。 echo -e 'FROM alpine\nRUN apk add --no-cache strace' \ | docker build -t debug -f - . \ && docker run -it --rm --pid container:<containe_id> --cap-add sys_ptrace debug strace -fp 1 docekrで動かしているコンテナに対して、別のコンテナから strace を動かす方法については、下記のサイトを参考にさせていただきました。 https://qiita.com/minamijoyo/items/9dd59109e9fe1a35f888 straceをした状態で SIGHUP を受信するした時のログは下記になります。 [pid 6] nanosleep({tv_sec=0, tv_nsec=20000}, <unfinished ...> [pid 13] <... futex resumed>) = 0 [pid 13] futex(0xc000211d48, FUTEX_WAIT_PRIVATE, 0, NULL <unfinished ...> [pid 6] <... nanosleep resumed>NULL) = 0 [pid 6] futex(0x17c4e78, FUTEX_WAIT_PRIVATE, 0, {tv_sec=59, tv_nsec=137259289} <unfinished ...> [pid 10] <... epoll_pwait resumed>[], 128, 1, NULL, 0) = 0 [pid 10] epoll_pwait(3, [], 128, 0, NULL, 2) = 0 [pid 10] epoll_pwait(3, <unfinished ...> [pid 1] <... futex resumed>) = ? ERESTARTSYS (To be restarted if SA_RESTART is set) [pid 1] --- SIGHUP {si_signo=SIGHUP, si_code=SI_USER, si_pid=0, si_uid=0} --- [pid 1] futex(0x17efbc0, FUTEX_WAIT_PRIVATE, 0, NULL <unfinished ...> [pid 10] <... epoll_pwait resumed>[{EPOLLIN, {u32=4118929128, u64=140509679050472}}], 128, 59136, NULL, 0) = 1 [pid 10] futex(0x17c4e78, FUTEX_WAKE_PRIVATE, 1) = 1 [pid 6] <... futex resumed>) = 0 障害が起こっているAPI Serverに対して strace を実行し上記のようなログがでていることを確認できていれば、どこからか SIGHUP が送られてきていることがわかり、調査をスムーズに進めることができたと思いました。 しかし、トレースを実施すると何かしらオーバヘッド等が発生するため、なるべくなら検証環境などで不具合を再現し、その環境でトレースを行うことが望ましいです。 ですが、今回のように再現が困難な場合にはオーバーヘッドが発生することを考慮にいれ、本番環境でトレースを行うことも1つの方法としてあったと思います。 さいごに 今回は実際に起こった障害の事例を元にどういったことを考え調べていったのかについて話しました。 障害の調査をする時には、想像力を働かせて色々な原因を考えて一つ一つ確認していくことになると思います。 その時今回のように気づくのが遅れてしまうと、考えうる原因が増え対応の時間が長引くだけでなく難易度もあがってしまいます。 こうならないためにも、適切な監視を設定することが大事だと改めて感じることができました。 今回のような失敗談を記事にすることで、みなさんの障害調査の時の手助けや監視設定を見直すきっかけになれば幸いです。
アバター
はじめに 日本時間の2021年2月25日に Jetpack Compose のbeta版がリリースされました。APIも固まってきたようですので触ってみた範囲のうち、導入的なところをコードで示しつつ、感想を述べていきます。 使用環境 使用した環境は以下の通りです。他にもandroidx.activityなどにcomposeがありますが、いずれも2021年3月15日時点で最新のバージョンを使用しました。 バージョンはJetpackのLibraries(*1)から調べることができます。 Android Studio Arctic Fox | 2020.3.1 Canary 9 androidx.compose 1.0.0-beta02 最初につくるもの トップレベルの関数に @Composable を指定することで、その関数内にてComposeを使用したレイアウトを組めます。合わせて @Preview を指定すればAndroid Studio上でプレビューもできます。 このプレビューは同時に複数表示可能なので、プレビュー用の関数を複数作成すればダークテーマ対応有り/無しの表示を同時に確認できます。 @Composable fun MyScreen() { // ここでComposeを使用して表示を組む } @Preview @Composable fun PreviewMyScreen() { // MyScreenで組んだ表示がAndroid Studio上にプレビューされる MyScreen() } レイアウトたち Box、Column、Rowがそれぞれ従来のFrameLayoutやinearLayoutに相当しています。 Box { // 重なる Text("hoge") Text("piyo") } Column { // 縦に並ぶ Text("hoge") Text("piyo") } Row { // 横に並ぶ Text("hoge") Text("piyo") } 他に、gradleファイルに指定を追加することでCompose版のConstraintLayoutも使えますが、公式Document中のConstraintLayoutの補足(*2)を読むと無理して使わなくても良さそうです。 // テキスト2つを縦に並べる ConstraintLayout { val (text1, text2) = createRefs() Text( "hoge", modifier = Modifier.constrainAs(text1) { linkTo( parent.start, parent.top, parent.end, text2.top, 0.dp, 0.dp, 0.dp, 8.dp ) } ) Text( "piyo", modifier = Modifier.constrainAs(text2) { linkTo( parent.start, text1.bottom, parent.end, parent.bottom, 0.dp, 0.dp, 0.dp, 0.dp ) } ) } 表示のパーツたち Text、Button、Image、Cardなど多くの表示が揃っています。Spacerなるものもあり、わかりやすくmarginを仕込めます。 ただ、RecyclerView(ListView)相当がLazyColumn(or LazyRow)という名称であったりと、一部は従来の名前から大きく変わっている点に注意が必要です。 val items = (0 until 100).map { "item $it" } LazyColumn( // 項目の間隔を空ける verticalArrangement = Arrangement.spacedBy(8.dp) ) { item { // リストの一番上に横スクロールのリストを入れる LazyRow( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { items(items) { Text(it) } } } items(items) { // 縦スクロールのリストの項目としてテキストとボタンを横に並べる Row { Text(it) Spacer(modifier = Modifier.width(8.dp)) Button(onClick = { // ボタンクリック時の処理 }) { Text("button") } } } } ものが多すぎるので使いたいものを公式Reference(*3)から頑張って探す必要があります。androidx.composeパッケージ関連を漁れば色々と見つかります。 表示の設定を変更する これまでレイアウトのxmlで指定していたlayout_widthやpaddingなどは Modifier というobjectを通して設定します。 Modifierにサイズやpaddingを設定する拡張関数があり、ものによってはColumnなどのscope限定で使える拡張関数が存在していることもあります。 Box( // 縦横とも画面一杯に広げてpaddingを設ける modifier = Modifier .fillMaxSize() .padding(16.dp) ) { Text( "hoge", // 背景を赤色かつ角に丸みを与え、中央に配置する modifier = Modifier .background(Color.Red, shape = RoundedCornerShape(8.dp)) .align(Alignment.Center) ) } 表示の操作として行えることはModifierの関数だけなのでわかりやすいです。 ガワを作る Scaffold() でMaterial Designに則った画面を簡単に構築できます。各種AppBarやFABを設定できる口があるので、従って作るだけでそれらしい画面になります。 Scaffold( // 他にもbottomBarやfloatingActionButtonなどを設定できる口がある topBar = { TopAppBar( title = { Text("title") }, actions = { IconButton(onClick = { // メニュークリック時の処理 }) { Icon( imageVector = Icons.Default.ImageSearch, contentDescription = "search" ) } } ) }, content = { // ここで画面の表示を作る MyContentScreen() } ) その他 viewModelを viewModel() で取得できたり、Navigationによる表示切り替えも行えるため、やりたいことは一通り行えそうであることが感じとれます。 また、これまでに作成した既存のViewは AndroidView なるものを使用することでComposeの世界に引き込んだりもできます。 他にCompose独自に覚えることとして、remember系の関数で値を保持したり、表示更新の契機としてStateを操作したりと従来にはなかった考え方を覚えて行く必要があります。 このあたりはReactのComponentで表示を作るときに近いものを感じました。 ハマったところ Android StudioがCanaryであったり、Composeがbetaであるためか、いくつかハマったところがありました。 viewModel()を使うとプレビューが表示されない viewModel()を使わずにViewModelの実体を渡すか、あるいはViewModelから取得した値だけをComposableな関数へ渡す プレビューを使わず、エミュレータや実機で確認するだけなら問題なし 自動importがよきに行われないものがあるため毎回手動でimportを書くことになるものがあった viewModel()を使うための import androidx.lifecycle.viewmodel.compose.viewModel var value by remember { mutableStateOf("") } などと by を使ってStateのvalueへのシンタックスシュガーを利用する場合の import androidx.compose.runtime.getValue / setValue stackoverflowの回答(*4)に助けられました (3/29追記) by xxx.observeAsState を使用した場合のgetValue(※7)や by remember を使用した場合のsetValueのimportなど、一部に対応されたようです BottomSheetやSnackbarの使い方のベストプラクティスがわからない Textなどと同じように作ることでとりあえず表示は行えるが、BottomSheetScaffoldやSnackbarHostなるものがあるため、よりよい使い方があると思われる さいごに 今回の記事は公式Document(*5)を一通り読んでその中のおおよそを触ったものの一部です。Composeの情報は多いため覚えきることも紹介しきることも難しいですが、触ってみた範囲ではxmlで組むより簡単に表示を構築できる印象がありました。 対応されたAndroid Strudioとともにstable版になる日が楽しみです。 Jetpack Composeを使ったチャレンジとして Android Dev Challenge (*6) なるものも開催されているので、挑んでみるのも良いと思います。 参照 *1 JetpackのLibraries https://developer.android.com/jetpack/androidx/explorer *2 公式Document中のConstraintLayoutの補足 https://developer.android.com/jetpack/compose/layout#contraintlayout のNote部分 *3 公式Reference https://developer.android.com/reference/kotlin/androidx/compose/material/package-summary *4 stackoverflowの回答 https://stackoverflow.com/questions/64951605/var-value-by-remember-mutablestateofdefault-produce-error-why *5 公式Document https://developer.android.com/jetpack/compose *6 Android Dev Challenge こちらは最終チャレンジのWeek 4 https://android-developers.googleblog.com/2021/03/android-dev-challenge-4.html ※7 Android Studio Arctic Fox Canary 12のFixes https://androidstudio.googleblog.com/2021/03/android-studio-arctic-fox-canary-12.html
アバター
誰でもわかるStoreKitTesting はじめに はじめまして。エブリーでiOSエンジニアをしている佐藤です。 DELISH KITCHENで、主にプレミアムサービスや課金周りを担当しています。 今回は、WWDC2020で発表されたStoreKitTestingについて紹介したいと思います。 概要 概要としては以下が挙げられるかと思います。 AppleStoreサーバに接続せずにローカル課金テストができる ローカルでテスト商品を作れる 購入トランザクションの管理ができる 割引系(お試しオファー、プロモーションオファーなど)のデバッグが可能 プロモーションオファーはローカルの秘密鍵を作成可能 レシートはローカルで署名されている 課金のユニットテストが可能 ローカルで実行できる課金環境がとても充実してきていますね。 次にStoreKitTestingの導入の流れを簡単に説明していきたいと思います。 Configurationファイルを作成 まずはNew FileでStoreKit Configuration Fileを選択し、作成します。 次にサブスクリプショングループを作成します。 次に課金商品を作成していきます。 選べるのは以下の3つ。 消耗型 非消耗型 自動更新サブスクリプション 作成が終わるとこんな画面になります。 Product IDや期間などは任意で選択可能です。 オファーの作成 更にStoreKitTestingではAppleが用意している様々な購入オファーの選択も可能です。 そのひとつが、 Introductory Offer(お試しオファー) です。 これは初回購入ユーザーに対してアプローチ可能なオファーになります。 選べるのは以下の3つ Pay As You Go(都度払い) Pay Up Front(前払い) Free (無料) 2つ目が Promotional Offer(プロモーションオファー) これは再登録者や継続ユーザーへアプローチ可能なオファーです。 詳しい実装は割愛しますが、AppleStoreConnectで作成した秘密鍵をもとに署名を作成し、StoreKitでの購入時に必要なパラメータを含めると購入可能になります。 詳しい実装方法 StoreKit Testingではローカルで秘密鍵を作成可能で、それをもとに署名を行うことで購入テストが可能になります。 残念ながらiOS14から使用可能になったもうひとつのオファーである、 オファーコード はStoreKit Testingで使用することはまだできないようです。 参照 Configuration Fileを設定する Product > Scheme > Edit Scheme > Runを開き、 以下画像のように作成したConfiguration Fileを設定します。 Schemeごとに設定可能なので、 任意のSchemeに作成したConfiguration Fileを設定することでStoreKit Testingでの購入が有効になります。 様々なデバッグ StoreKitTestingでは様々な購入デバッグが可能です。 以下のようなものが設定可能です。 既定購入ストアの設定(購入する国別ストアの選択) 既定表示言語 タイムレート(購入有効期限の時間短縮率) 購入割込のデバッグ有効化 トランザクションを失敗させる(エラー種別も選択可能) 購入確認の表示 プロモーションオファーのローカル秘密鍵とKeyIDの生成 ローカル証明書の生成(StoreKitTestingでのレシート検証のため) ※選択できるエラー種別 またトランザクションの管理も行うことができます。 まとめ StoreKitTestingの設定方法をかんたんにまとめてみましたがいかがだったでしょうか。 今まで開発中は実機でSandboxでの課金テストを行っていましたが、StoreKitTestingによって開発効率は上がったように感じます。 直近ではプロモーションオファー関連の開発デバッグを簡単に行うことが出来、導入のメリットを感じることができました。 課金テストの自動化など安全面でも導入の強みはあるのではないかと思いますので、 まだ未導入の方もしくはIn App Purchase実装の練習をしたい方などはぜひお試しを!
アバター
はじめに DELISH KITCHENでは日々多くのレシピ動画を公開していますが、その動画は全てAdobe Premiere Pro(以下 Premiere Pro)を使用して編集しています。 今回はPremiere Proのエクステンションを作成して動画の編集効率を向上させた話をご紹介します。 これまで発生していた問題 レシピ動画にどのような材料を使っているか、どのような工程があるかは全てダッシュボード(データ管理用Webサイト)で管理しています。 動画編集者は動画データをPremiere Proに取り込み、対象レシピの情報をダッシュボードから開き、見比べながら作業していました。 動画にテロップを表示する際には、材料や工程の文章をダッシュボードからPremiere Proにコピペしながら編集作業を行っていました。 肉巻き半熟卵の動画テロップ例 この作業は地味に時間がかかる上にコピペミスをしやすく、手戻りが発生したりミスが残ったままレシピ動画が公開されてしまうこともありました。 そこで、編集作業の効率化 & ミスの削減のためにPremiere Proのエクステンションを開発しました。 エクステンションの紹介 エクステンションを有効にした状態でPremiere Proを開くと以下のような画面になります。 画像右上のパネルがエクステンションの画面です。 これから編集するレシピの情報が自動的に表示されるため、自分でダッシュボードを開く必要はありません。 Premiere Proでエクステンションを開いたところ レシピのタイトルを挿入するときはタイトルの項目にある緑のボタンをタイムラインの好きな位置にドラッグ&ドロップします。 そうすると自動的にDELISH KITCHENで決められたデザインのタイトルテロップが挿入されます。 あとは表示する秒数やデザインを微調整すればタイトルテロップは完成です。 タイトルテロップをドラッグ&ドロップで挿入 材料も同様に、表示させたい位置にドラッグ&ドロップすれば材料用のテロップが表示されます。 材料テロップをドラッグ&ドロップで挿入 このようにタイトルや材料などのテロップをドラッグ&ドロップで動画内に挿入できるので、これまでのコピペ作業は必要なくなり、 ポチポチするだけである程度レシピ動画が作れてしまうというのがこのエクステンションの良いところです。 使用している技術 Adobe CEP このエクステンションはAdobe CEP (Common Extensibility Platform) という技術を用いて作られています。 これはHTML5/JavaScript/Node.jsを使って様々なAdobeソフトウェアのエクステンションを開発できるというものです。 詳しくは以下のGitHubや記事で詳しく解説されています。 Adobe-CEP/Getting-Started-guides: Getting Started guides and samples for CEP extensions Adobe-CEP/CEP-Resources: Tools and documentation for building Creative Cloud app extensions with CEP CEP スーパー メガ ガイド: HTML5 + Node.js で Adobe のツールを拡張する | aphall.com エクステンションの画面は Chromium Embedded Framework というアプリケーションに埋め込んで使用するChromiumフレームワークを用いて表示しているため、 一部制限はありますがほぼ通常のChromeでHTML5/JavaScriptを実行するのと遜色ありません。 またNode.jsが実行できるためローカルファイルの読み書きも行えます。 AdobeソフトウェアのバージョンによってAdobe CEPのバージョンも変わり、それによって使えるAPIやChromium / Node.jsのバージョンも決まってくるため、 事前にサポートするバージョンを確認しておいた方が開発がスムーズになります。 参考: CEP-Resources/CEP 10.0 HTML Extension Cookbook.md at master · Adobe-CEP/CEP-Resources Nuxt.js Chromiumの上でHTML5/JavaScriptが実行できるとなると、ほぼWebサイトを作成するのと変わりません。 そのため、このエクステンションは Nuxt.js を使用しています。 (当初は素のVue.jsだったのですが、最近Nuxt.jsにリプレイスしました) DELISH KITCHEN WEB を構成する技術のお話 でも紹介したように、 DELISH KITCHENのWebサイトにはNuxt.jsを使用しています。またレシピ管理用のダッシュボードでも同様にNuxt.jsを使用しています。 Nuxt.jsはサーバーサイドレンダリング(SSR)が特徴として挙げられがちですが、SSR以外にもページのルーティングルールやコンポーネント、プラグインの設置場所などが全て定められているというのが大きな特徴で、 Nuxt.jsで開発したことのあるWebエンジニアであればすぐにプロジェクトの構成が把握できるのが強みです。 そのためこのエクステンションでもNuxt.jsを使うことによって自分以外のエンジニアがエクステンションを開発する際にもプロジェクト構成の把握が容易になり、 共通のライブラリやプラグインが使用できるため開発がスムーズになります。 工夫した点 編集対象のレシピ情報を自動的に表示する 動画を編集する際に、毎回ダッシュボードから編集対象のレシピを開くのは地味に面倒な作業なので、Premiere Proを開くと自動的にレシピの情報が表示されるようにしました。 仕組みは単純で、以前からレシピ編集のルールとしてPremiere Proのプロジェクトファイルには <レシピID>_<レシピタイトル>.prproj というような命名規則で名前をつけることが決まっていたので、 Premiere ProのAPIでファイル名を取得し、正規表現でレシピIDを抜き出してからDELISH KITCHENのAPIでレシピ情報を取得し表示しています。 ドラッグ&ドロップでテロップを挿入 なるべく直感的にテロップを挿入できるようにしたかったため、ドラッグ&ドロップで挿入できるようにしました。 ドラッグ&ドロップ自体は通常のWebと同じく drag イベントをハンドリングし dataTransfer.setData() でテロップのデータを渡せば良いのですが、 setData() の第一引数に渡すformatは "com.adobe.cep.dnd.file.0" という文字列でなければいけません。 (複数データを渡す場合は最後の数字をインクリメントしていく) 参考: Samples/ext.js#L84 Adobe-CEP/Samples PRTL ドラッグ&ドロップでテロップを挿入、と言いましたが、実際には何のファイルをドラッグ&ドロップで渡しているかというと、PRTLというフォーマットのファイルを渡しています。 これは現在Premiere Proでレガシータイトルと呼ばれているテロップのフォーマットで、XML形式のファイルになっています。 テンプレートとして使用するPRTLファイルは予めエクステンション内に保存してあり、 材料などのテロップ挿入ボタンがドラッグされた瞬間にテンプレートのPRTLを読み込み、指定された箇所の文字列を材料名で置き換えます。再度PRTLファイルとして書き出した後に dataTransfer.setData() にそのファイルパスを指定すると、 ドロップした場所にそのPRTLファイルがテロップとしてインポートされ表示されるという仕組みです。 余談 Premiere Proにはレガシータイトル以外にもエッセンシャルグラフィックスというテロップの機能があります。 こちらもテンプレートがありAPIでPremiere Proに取り込むことができるのですが、若干扱いづらく、XML形式であることや直感的にドラッグ&ドロップで取り込めるという点でPRTLを採用しました。 ただし、PRTLはフォーマット仕様が公開されておらず(公開されていたらどなたか教えて下さい)、独自にパースする必要があります。 また、Premiere ProでテロップをPRTLファイルとして書き出せるのはCS6までで、それ以降のバージョンでは書き出し機能は削除されてしまいました。 このような状況で、将来的にPremiere Proがレガシータイトルをサポートしなくなる恐れもあるため、できればエッセンシャルグラフィックスを使ってテロップを挿入できるようにしたいところです。 エクステンションのパッケージング Adobe CEPのエクステンション自体はPremiere Proのエクステンション用のディレクトリに配置すれば使用できるのですが、動画編集をするスタッフに毎回その場所に配置してもらうのも手間がかかります。 そこで、エクステンションをインストーラーでインストールできるようにしました。 全員Mac上でPremiere Proを使用するので、インストーラーとしてpkgファイルを生成します。 パッケージングの詳細や使用したコマンド、npmパッケージなどは以下を参考にしてください。 Packaging and Signing Adobe Extensions CEP-Resources/ZXPSignCMD at master · Adobe-CEP/CEP-Resources codearoni/zxp-sign-cmd: A JS wrapper for Adobe's extension signer パッケージングを手動で行うのも面倒なので、GitHub上でPullRequestをmasterにマージした際にCIでパッケージングし、 そのpkgファイルを tcnksm/ghr を使用してGitHub Releasesに自動的にアップロードするようにしました。 これでpkgファイルをGitHub Releasesからダウンロードして開けばMacでおなじみのインストーラーが起動し、エクステンションをインストールできるようになります。 エクステンションのインストーラー まとめ このエクステンションを使うことによってPremiere Pro内でテロップの挿入が完結できるようになりました。 また、ドラッグ&ドロップでテロップを非常に簡単に挿入できるようになり、コピペミスを減らすことができました。 もちろん動画編集作業はこれだけで終わりではないのですが、地味に時間のかかっていた作業を減らすことができ、編集効率を向上させることができました。 このPremiere Proのエクステンションは2018年に作成したものですが、3年弱経った現在も毎日使われています。 自分が作ったものが毎日使ってもらえているというのは開発者冥利に尽きる思いですね。 DELISH KITCHENのサービスも毎日沢山の方に使っていただくため日々開発に励んでいます。 これからもDELISH KITCHENをよろしくお願いします。
アバター
はじめに MAMADAYSにはiOSとAndroidのアプリがあります。 Flutterなどのクロスプラットフォーム開発ではなく、それぞれネイティブで開発しています。 この記事ではMAMADAYSのiOSアプリの全体的な構成を紹介します。 全体の雰囲気を掴んでもらうことを目的とし、細かい採用技術はまた別の機会に紹介できればと思います。 MAMADAYSアプリの機能 MAMADAYSアプリには大きく次のような機能があります。 メディア MAMADAYSには動画を含む数千本の記事があり、ユーザーさんの状態やお子さんの月齢に合わせて適切な記事をおすすめする仕組みがあります。 リリース当初は記事を WKWebView で表示していましたが、表示速度向上などのためにネイティブに変更しています。 動画再生には AVPlayer を用いていて、自動再生処理や待ち時間短縮のために、それなりに複雑な非同期的な制御が必要になっています。 育児記録 お子さんの睡眠や授乳などを簡単な操作で記録し、グラフでわかりやすく振り返ることができる機能です。 この機能のみ、 Firebase Cloud Firestore を利用しています。 Firebase Cloud Firestore の採用には以下のような利点がありました。 サーバーの実装が不要 クライアントの実装が簡単 非同期通信で待ち時間なし 入力した記録をパートナーにリアルタイムに同期できる リリース当初の時間が無い中では、工数削減、品質向上に非常に役立ったと思います。 ただし、育児記録機能と他機能の連携を進める上で障害になってきているため、将来的には独自のAPIに置き換えることになりそうです。 離乳食 離乳食の進み具合を記録したり、時期にあった調理法を探せる食材リスト機能があります。 カレンダー 家族の予定を一つのカレンダーで共有できます。 また、思い出として写真をアップロードし、簡易的なアルバムのようにする機能があります。 パートナー共有 上記全ての機能はデータを家族内で共有できるようになっています。 アーキテクチャー アーキテクチャーはMVVMとしています。 ただし ViewController から ViewModel の接続はメソッドコール、 ViewModel から ViewController の接続はクロージャーで、Rx等によるバインディングを用いていないので一般的なMVVMとは異なるかもしれません。 Viewはほぼ UITableView で構成されており、Viewの更新は UITableView のリロードですることが多いため、個別のデータをバインディングするメリットを感じなかったためにこのような構成になっています。 また、ViewControllerの階層化を積極的に使っています。 コンテナを使って階層化することによってView Controllerの肥大化を防ぐようにしています。 まとめ MAMADAYSのモバイルアプリは2019年10月にリリースして以来コンスタントにアップデートを続け、離乳食、カレンダー、妊娠週数と大きめの機能を追加してきました。 今後もユーザーさんの声を聴きながら進化を続けていきます。 開発者としてはコードをできるだけシンプルで変更に強い状態に保つことで、開発速度と品質でサービスに貢献したいと考えています。
アバター
DELISH KITCHEN RS事業部では、小売向けにサイネージやチラシ等のサービスを提供しています。 従来は、そのサービスの管理が出来るWebアプリのみ運用していたのですが、新たに広告配信設定用のWebアプリが必要になりました。 そこで Nx を使って、2つのアプリをmonorepoで管理し、コードの共通化を計りました。 Nxとは Nx はmonorepo用の拡張可能な開発ツールセットです。堅牢なCLI、キャッシュシステム、依存性管理などを提供すると共に、Jest、Cypress、ESLint、Prettierなどのモダンなライブラリの統合をサポートしています。元GoogleのAngularチームにいたメンバーによって創設されたNrwlが開発しており、Googleは全てのプロジェクトをmonorepoで管理しているという有名な話がありますが(詳細は知りません)、それと似た開発体験を提供することを目的に開発されているそうです。 Nxへの移行 RS事業部で開発しているWebアプリはAngularで作られており、それをまずNxに移行しました。 従来のディレクトリ構成 NxはAngularをサポートしているので、移行自体は簡単でした。 まずNxで新しくworkspaceを作成します。 npx create-nx-workspace --preset=angular その後、既存のアプリのコードを apps 以下に配置し、angular.jsonやtsconfig.json、tslint.jsonなどの設定ファイルを修正し、既存のアプリで使用していたサードパーティのライブラリ(dayjsなど)を新しいworkspaceに追加して、移行を完了しました。 現在のディレクトリ構成 ※ 現在のcreate-nx-workspaceは、テストフレームワークにデフォルトでJestとCypressが選択されており、AngularデフォルトのKarma、Protractorを使用したい場合は、別途以下のコマンドでアプリを作成する必要があります。 nx generate @nrwl/angular:app myapp --unit-test-runner=karma --e2e-test-runner=protractor` ライブラリの作成 複数のアプリから共通のコードを使用するために、ライブラリを作成します。 nx generate @nrwl/angular:lib shared 上記のコマンドで libs 配下に shared ディレクトリが作成されます。 今回は例として SampleComponent をライブラリに作成します。 nx generate component sample --project=shared 作成したら、 shared.module.ts のexportsに SampleComponent を追加します。 次に、アプリから SampleComponent を使用するために、tsconfigにパスを追加します。 { ... "compilerOptions": { ... "paths": { "@lib/shared": ["libs/shared/src/index.ts"], "@lib/shared/*": ["libs/shared/src/lib/*"] }, ... }, ... } あとは使用したいモジュールでimportすると、使用可能になります。 import { SharedModule } from '@lib/shared'; @NgModule({ imports: [SharedModule], bootstrap: [AppComponent] }) export class AppModule {} また、直接 SampleComponent をimportしたい場合は、以下のコードで可能です。 import { SampleComponent } from '@lib/shared/sample/sample.component.ts' CSSの共通化 上記で作成したsharedライブラリに共通のCSSも置いて、使えるようにします。 場所はどこでもいいのですが、私は libs/shared/src/styles にファイルを配置しています。 html, body { height: 100%; } body { margin: 0; font-family: 'Helvetica Neue', Arial, 'Hiragino Kaku Gothic ProN', 'Hiragino Sans', Meiryo, sans-serif; } そしたら、angular.jsonの、このスタイルを適用したいアプリの箇所に以下を追加します。 "app": { "projectType": "application", ... "architect": { "build": { ... "options": { ... "styles": [ "libs/shared/src/styles/styles.scss", // この部分 ], ... } } } } また、partialファイル(_mixin.scssなど)をsharedに置いて参照することも可能です。 これも好きな場所にファイルを配置して、angular.jsonでパスを指定するだけです。 "app": { "projectType": "application", ... "architect": { "build": { ... "options": { ... "stylePreprocessorOptions": { "includePaths": ["libs/shared/src/styles/partials"], // この部分 }, ... } } } } ここで注意なのが、指定するのはファイルパスではなくディレクトリパスということです。こうしておくことで、partials以下にあるファイル(_mixin.scss)を @import 'mixin' という形で使うことができます。 CI/CD monorepoにするとCI/CD周りも変わってきます。おそらく多くの人が、変更があったアプリ、またはライブラリだけをテスト、デプロイしたいと考えると思います。Nxはその希望を叶えてくれます。Nxには affected コマンドがあり、変更の影響があるプロジェクトのみに対してテスト、ビルドを実行する機能があります。 CI/CDはGithub Actionsで行っていて、実際のワークフローを例に紹介したいと思います。Github ActionsはPull Requestを作った時と develop 、 master ブランチにマージされたタイミングで走るように設定しています。 例えば、Pull Requestを作った時に走らせるビルドは以下のように設定しています。 name: build on: pull_request jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: fetch-depth: 0 - uses: actions/setup-node@v1 with: node-version: '12.x' - name: Run cache/restore node_modules uses: actions/cache@v1 with: path: node_modules key: v1-${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }} - name: Run build run: make affected-build BASE=${{ github.event.pull_request.base.sha }} // nx affected:build --base=${BASE} を呼んでるだけ ここで、 affected はbase optionでブランチやコミットIDを指定でき、baseとHEADの差分から、影響のあるプロジェクトを判断します。その仕様から、 actions/checkout@v2 では fetch-depth: 0 を指定することで、コミット履歴を全部取得するようにしています。 また master にマージされた際のデプロイは以下のように設定しています。 name: deploy on: push: branches: - master jobs: build: ... deploy: needs: build runs-on: ubuntu-latest steps: - name: Run cache/restore dist uses: actions/cache@v1 with: path: dist key: v1-${{ runner.os }}-dist-${{ github.run_number }} - name: Run deploy run: | app_paths="dist/apps/*/" if ! ls -d $app_paths &>/dev/null ; then exit 0 fi for app_path in $app_paths; do app=$(basename "$app_path") if [ "$app" == "appName1" ]; then elif [ "$app" == "appName2" ]; then fi done 差分があるもののみデプロイするという仕組みはNx自体にはないので、buildフェーズで生成されたものをデプロイするというスクリプトを書いて対応しています。 現在のデプロイの仕組みだと、 master に入ったものは全部対象になってしまうので、リリース前に全プロジェクトの確認が必要になってしまいます。また master に入ってもリリースのタイミングは調整したいこともあるでしょう。monorepoにして一番の課題で、依然他に良いフローがないか検討中の部分です。 まとめ 今回はNxを使ってnpm projectをmonorepo管理した話をしました。Nxを使ったComponentやCSSの共通化、CI/CDの運用とかは普段聞く機会がないので、誰かの参考になれば幸いです。
アバター
自己紹介 はじめまして && あけましておめでとうございます。MAMADAYS開発部長をやっている齊藤です。 開発部長という肩書きですが、マネージメント業務だけではなく、モバイルアプリ開発以外を守備範囲としたユーティリティプレイヤーもつとめ、若い子たちと一緒に草を生やす作業に勤しんでいます。 座右の銘は『枯れた技術の水平思考』です! 閑話休題 弊社オウンドメディアの every.thing にも、CTOの id:kajidai との対談が掲載されていますので、時間に余裕がある方は見ていただけると嬉しいです。 【前編】エブリーCTOが聞く!MAMADAYS開発部長のルーツは音楽!?ゲーム開発から組織づくりにも携わってきたこれまでの経歴とは | every.thing(エブリシング)| エブリーのこと、ぜんぶ 【後編】エブリーCTOが聞く!『MAMADAYS』を主力事業に成長させたい!と語るMAMADAYS開発部長の今後の展望 | every.thing(エブリシング)| エブリーのこと、ぜんぶ はじめに App/Webのリリースから1年余りが経過した MAMADAYS の、サービス全体像とバックエンド観点でのシステム紹介を行います。 個々の詳細は、後日、若者たちが書いてくれると思いますが、本記事で少しでも MAMADAYS の開発に興味を持っていただけると幸いです。 MAMADAYSのサービス全体像 MAMADAYSでは 『子育てを通じて喜びと幸せを感じられる社会に』 をミッションに掲げ、出産・育児に関する課題を解決するため、プロダクト開発を行っています。 出産・育児の課題は多岐に渡るため、複数の機能を提供する多機能アプリとして展開しつつ、データを活用して1人1人に最適化した情報をお届けできるよう日々改善を重ねています。 corp.every.tv MAMADAYSのバックエンドシステム全体像 基本的な部分は DELISH KITCHEN と同じような感じにしていますが、以下のようにいくつか異なっている点もあります。 コンテナオーケストレーションとして、AWS ECS ではなく Kubernetes (AWS EKS) を利用 CI/CDとして、CircleCIではなく github Actions や AWS CodeBuild を利用 リリース時期やサービス規模の違いによりそのタイミングで最適と思う選択であり、どの技術が優れているとかではないです。 利用サービス MAMADAYSで利用している基盤や外部サービスは以下の通りです。 インフラ AWS EKS AWS Aurora AWS S3 AWS CloudFront AWS Route53 Elasticsearch ログ/分析 fluentd BigQuery metabase 開発/運用 github github Actions AWS CodeBuild terraform kustomize APIサーバ iOS/Android/WEBなど、クライアント向けに提供しているAPIサーバです。 DELISH KITCHEN 同様に MAMADAYS でも、Golang の軽量フレームワークである echo を利用しています。 テスト周りでは、私が元々 Rails 厨だったこともあり Rspec っぽい記述ができる GoConvey を利用し、カバレッジも Codecov で計測してたりします。 利用システム Golang echo GoConvey Codecov WEBシステム WEBにおけるMAMADAYSサービスを提供しています。 記事の入稿やお客様からのお問い合わせに対応するためのツール類も含め、MAMADAYSのWEB面は React のフレームワークであるNext.jsを利用しています。 リリース1年が経過した頃、開発メンバーが増えたこともあり TypeScript 化を行ったのですが、ものすごく手軽に対応できたので Next.js 様様です。 『もしかして:Nuxt.js』と毎回問われてしまうぐらいググラビリティが低く、検索するたびに悲しい気分になりますが、それはそれで愛おしいです。 利用システム Next.js Node.js 検索システム 記事/動画情報の検索を提供するサーバです。 検索エンジンには Elasticsearch を利用しています。 元々は使用したいプラグインが対応されていなかったため独自運用していたのですが、AWS Elasticsearch Service のバージョンも上がったので、マネージドに移行することを検討しています。 利用システム Elasticsearch 最後に かなりざっくりとした内容となってしまいましたが、当たり前の技術を当たり前に、それでいて新しい風を取り込みながら MAMADAYS は開発・運用しています。 まだまだ成長途上のサービスですので、これからも世の中の出産・育児に対する課題に技術でどう解決していくかを考えながら取り組んでいこうと思います。 ここまでお読みいただき、ありがとうございました。
アバター
こんにちは、エブリーでCTOをやっている梶原と申します。 1年の始まりということで昨年の振り返りと今年の取り組みについて書きます。 はじめに、エブリーが提供している中でエンジニアが関わっているプロダクトをご紹介すると、 食の課題解決に取り組むレシピ動画サービスの「DELISH KITCHEN」と育児の課題解決に取り組むママ・パパの365日に役立つ情報やサービスを提供する「MAMADAYS」の2つのプロダクトを開発しています。 2020年の振り返り 開発組織について まず自分自身の変化として、2020年の春くらいに当時見ていた事業を引き継いで開発業務に専念することになりました。そして開発組織については、当時、各事業部にぶら下がっていたエンジニアチームを、開発本部というエンジニア組織に統合することにしました。エンジニア全体での勉強会やエンジニアブログを開始したのも昨年です。結果、課題としてあったエンジニア間の連携の希薄化、エンジニアのキャリアサポートや適切なアサインメントは徐々に解消されつつありますが、まだまだこれからなので今年も課題の解消に取り組みます。 データに関する取り組み これまではDELISH KITCHEN事業部のみに存在したデータエンジニアリングの組織を全社の横断組織として再編し、組織のミッションとして「DIKWモデル」の構築を掲げました。 データ基盤からBIツールを用いて各種分析業務は行っていましたが、中にはExcelやスプレッドシートで独自に管理、集計、分析されていたり、手動データを打ち込んでいるもの、データの定義が扱う人により違うものなど、属人的な扱いのデータが様々あり非効率な部分が多くありました。 これらの課題に対してデータ基盤への集約と集計のとりまとめと実施を行い、結果としてデータによる意思決定が格段に進みました。 アーキテクチャの再設計 DELISH KITCHENアプリはリリースから4年が経過して、新しくジョインするエンジニアの初期の学習コストや開発スピードが徐々に低下しつつあり、先んじて対策を打つという意思決定を行いリファクタリングとリアーキテクチャリングを開始しました。 2021年に向けて データに関しては、集約のフェーズが終わり、次はデータの利活用のフェーズです。機械学習を用いたプロダクトの改善や事業者との取り組みをこれまで以上に推進していきます。プロダクト開発では、息の長いプロジェクトになるであろうリアーキテクチャプロジェクトを昨年に引き続き取り組んでいきます。 また今年は小売のデジタル化が更に進む1年になるであろうと考えています。これまでにもアプリ内のチラシ・クーポン機能による集客支援、スーパー内に設置したデジタルサイネージによる店内販促、ライブコマースによるオンライン購買活動支援など取り組んできました。今年はこれらのものをベースに小売様と協力して新しいユーザ体験を生み出すべくプロダクト開発をできたらと考えています。 2021年もよろしくお願いいたします!
アバター
DELISH KITCHEN iOSアプリ開発のCI環境について はじめに はじめまして。2020年4月にエブリーに新卒で入社した山口です。 iOSエンジニアとして入社後、DELISH KITCHENクライアントグループで、日々iOSアプリの改善や新機能開発の業務に関わっています。 さて、多くの方にご利用いただいているDELISH KITCHENのiOSアプリですが、日々の開発フローをできるだけ効率化させるためにCI環境を整備しています。 今回の記事では、実際にどんなCI環境が構築されているかをご紹介します。 構成 さっそくですが、こちらが全体図になります。 フローとしてはこんな感じで、Githubのタグをトリガーにして、テストやAppStoreにipaを提出するまで(実際にApple側に提出する一歩手前まで)を自動化しています。 次に、それぞれの処理内容をざっくりと紹介します。 Makefile まずは運用しているMakefileの中身について大まかに紹介します。 特別変わっているところはないのですが、DELISH KITCHENのiOSアプリ開発では、必要なコマンド類はMakefileに集約してあります。 またCocoaPodsやfastlaneなどgem経由でインストールするものもBundlerを使用していて、 make install のようなコマンドでインストールできるようになっています。 特にfastlaneは bundle exec fastlane xxxx を都度書かなくても良いように必要な部分は、MakefileでWrapされています。 すべて make xxxx で必要な処理の流れを呼び出せるので、CircleCIなどでそのままできますし、例えばPCが変わっても開発構築を頑張らなくて良いです。 Github このCI環境では動作の起点となります。 開発者が、特定のプレフィックス付きのタグを切ったときや、特定のブランチにマージされた場合に、それがトリガーとなって次のCircleCIが動きます。 実際の運用では、このような感じで使っています。 developブランチにマージされた時 特定のプレフィックスがついたタグを切った時 例えば、 enterprise.{VERSION} のような感じ CircleCI Githubのトリガーをもとに実行され、タグのプレフィックスなどによって、予め定義されているそれぞれのジョブが実行されます。 ここで簡単な環境変数や使用するXcodeのバージョンが設定され、予め定義されているSTEPごとに実行しています。 実際に運用している config.yml では、workflowが4つ(Githubのトリガー分)存在しています。 すべて内部的にはmakeコマンドを叩くような構成で、テストからApp DistributionやTestFlightへのアップロードまで自動化されています。 また、毎度gemやCocoaPods、Carthageをフルで走らせると無駄な時間がかかってしまうため、前のデータをキャッシュすることでインストールの高速化を図る工夫をしたりしています。 fastlane fastlaneのことは皆さんご存知と思いますので具体的な話は省略しますが、運用でどう使用されているかを紹介します。 DELISH KITCHENの開発環境でも、Matchを利用した証明書の管理から、バージョン上げ、ビルド、テスト、アップロードまで利用しています。 プラグインとしては、 - versioning - firebase_app_distribution を使用しています。 どちらも名前の通りで、アプリのビルド番号や、バージョン情報をfastlane側から上げることができるプラグインと、App Distributionにビルドをアップロードすることができるプラグインです。 実際の運用では、デフォルトのAppStore Connect(TestFlight)へのアップロードと、プラグインを利用してApp Distributionへのアップロード。同時に、ビルド済みのアプリをGithubのリリースにアップロードするところまでをサポートしています。 このようにして、Githubでタグを切るところから実際にスマートフォン上で動ける環境になるまでを自動化させています。 まとめ 一度構築してしまえば、そのフローをすべて自動で走らせることが可能になるため、チーム開発や継続的なアップデートをするような環境では、開発の効率化にかなり貢献できます。また現在だと、fastlaneを始めとして、かなり有用なツールが揃っているので、CI環境を作りやすいように感じます。 この記事でDELISH KITCHENのiOSアプリ開発のCI環境を知っていただけましたら幸いです!
アバター
ansibleとterraformって何が違うんですか? ansibleとterraformって何が違うんですか? はじめに 作っただけでは動かない コンピューター ネットワーク ロードバランサー、DNS、TLS証明書……、etc 動かし続けるのはもっと大変 アプリケーションを作ることだけが仕事ではない ansibleとterraform IaC 自動化 均質化 明文化 履歴管理 IaCの2つのアプローチ 手続き型 宣言型 ansibleとterraform ansible terraform 使い分け おわりに はじめに 弊社ではインフラは主にAWS(一部GCP)を利用しており、構成管理にansibleとterraformを使用しています。 新しく入社された方や、これまでインフラをあまり触ってこなかった方がこれらに触れる際、本件表題のような質問をされることが何度かありました。 私はこのたぐいの質問をされるたびにそれぞれの成り立ち、歴史的背景、ソフトウェアとしての実装と振る舞いの違い、そして弊社での使われ方について説明をしてきました。 大抵はそれで納得してその後は疑問なく使い分けができるようになっていく(ように見える)のですが、やはり中には腑に落ちないようであったり、使い分けに悩まれる様子がみられる方もいます。 説明している最中、自分でも「この説明で適切なのだろうか」「そもそもなぜこれらが混同されやすいのだろうか」と疑問に思うことがよくあり、良い機会ですので今回文章としてまとめました。 この文章が表題の疑問の回答になり、今後の学習のお役に立てれば幸いです。 作っただけでは動かない ansibleやterraformの話をする前に、基礎の話から押さえておきましょう。 インフラエンジニアであったり、サービスの保守運用の経験がある方であれば「何を当たり前のことを」と思うかもしれませんが、 アプリケーションは作っただけでは動きません 。 なぜでしょうか?「アプリケーションは完璧に仕様を満たしています!動かせばいいだけじゃないか!」そう思われますか? そう、動かせばいいだけなのです! しかしながら、アプリケーションを動かすためには、アプリケーションの他にもたくさんのものが必要になります。 それらは、普段手元のパソコンで開発していると当たり前のように存在しているために気づかないものであったり、インターネットを通してたくさんの人にサービスを届けるために必要なものであったり、様々です。 ひとまずこの文章内では、これらのことをひとまとめに「リソース」と呼ぶことにしましょう。インフラとはリソースの集合であると言えます。 リソースという言葉は色々な場面で色々な意味合いで用いられます。 ここでいう「リソース」はこの文章内ではこのように名前をつけて呼ぶ、という程度の意味で捉えてください。 リソースの例をあげていけばきりがないのですが、いくつか代表的なものを取り上げましょう。 コンピューター あなたの手元にはコンピューターがありますか?いえ、答える必要はないですね。この文章を目にしているということは、そういうことですね。 当然のことですが、アプリケーションを動かすためにはコンピューターが必要です。 オンプレミスであれば機器を購入するかレンタルするかして、データセンターに配置しなければなりません。あるいはVPSを契約してもいいかもしれません。 もちろんAWSやGCPなどのクラウド環境で仮想コンピューターを用意しても良いでしょう。 ネットワーク あなたのコンピューターは何かしらのネットワークに繋がっていますか?いえ、答える必要はないですね。この文章を目にしているということは、そういうことですね。 インターネットを通じてサービスを提供するのであれば、何かしらインターネットへの接続性が必要になります。 あるいは、既存のネットワーク内に配置するのであれば、多少のネットワーク構成の変更が必要になるかもしれません。 また、接続性を確保することと同様に、不必要な接続を遮断する設定も重要です。 このノリももう飽きてきませんか?私は飽きました。ですので、あとはまとめていくつか紹介しましょう。 ロードバランサー、DNS、TLS証明書……、etc これらは必須というわけではありませんが、ほとんどの場合必要になるでしょう。 どれも購入・あるいは契約し、設定し、配置しなければなりません。 動かし続けるのはもっと大変 さてアプリケーションを動作させるために必要ないくつかのリソースを確保しました。アプリケーションは正しく動作し、目的を完全に達成しました。 素晴らしいことです!ではこれで全ては解決でしょうか? そうならばどれほど良かったでしょう……。 ほとんどの場合、アプリケーションは変更され続けます。あなたは今アプリケーションを変更しました。ではそれをどのようにすれば反映できますか? 変更前のアプリケーションは動いていて、以前と変わりなく使われています。これをダウンタイムなく新しいものに置き換えるためにはどのような手順が必要でしょうか? あるいは、あなたのアプリケーションは非常に人気が高く、多くのユーザーの要求を満たすために、100台のサーバで動いているかもしれません。 どのようにして100台のサーバに同時に、齟齬なく変更を反映させれば良いでしょうか? このようなシナリオはたくさん考えられます。もちろんありうる全てのシナリオを想定するのは不可能ですし、コストに見合いません。 しかしながら、日常的に発生するようなシナリオに対してはどのように対処するかを想定しておくべきでしょう。 アプリケーションを作ることだけが仕事ではない これまで紹介してきた通り、アプリケーションは作る場面だけでなく、実際に動作させる場面でもたくさんの考慮すべきこと、行うべきことがあります。 いうまでもなく、それは知識や経験、技術が必要な、専門的な作業です。 ansibleやterraformは、アプリケーションを動作させるために必要になるこれらの作業を錯誤なく、確実に、簡単に行えるようにするためのソフトウェアです。 ansibleとterraform ansibleやterraformはどのようにこのような作業の役に立つのでしょうか? それを説明するためにはまずInfrastructure as Code(以後IaCと省略)の概念を説明する必要があります。 なぜなら、ansibleとterraformはIaCを実現するためのツールだからです。 そもそも、Infrastructure(口語ではもっぱらインフラと省略されますので、この文章でも以降インフラと表記します)とは何でしょうか? 前項であげたリソースの例はどれもインフラを構成する要素ですが、他にもデータベースやミドルウェアなどを含む場合もあります。 あるいはCI環境などもインフラの一部だと捉える人もいるかもしれません。 はっきりとこういうものだ、という定義は難しいのですが、私は「アプリケーションを動作させるに足る環境」であると理解しています。 つまり サービス=アプリケーション+インフラ であると言えます。 IaC サービスを提供するためのインフラを準備する時、IaCを用いず従来通りの手法で行う場合、一般的にはドキュメントやチェックシートによって管理することになります。 小規模かつ変更の少ないインフラであればこのような手法でも十分に運用可能です。 しかし、インフラの規模が大きくなるにつれ、それぞれのリソースの相互作用が増え、構造が複雑になっていきます。 それだけでなく、管理するリソースが増えるということは、それだけ変更が必要になる頻度も上がります。 このような場合、ドキュメントやチェックシートによる管理は実際のリソースとの差異が生まれやすくなり、その差異を確認することも困難になっていきます。 この課題を解決するための手法がIaCです。 アプリケーションは何らかの表現形式で記述した コード によって表現されています。 IaCは、この考え方をインフラに応用した手法と言えます。 考え方としては単純で 「インフラを構築するためのアプリケーションを作り、それによってリソースの変更を人間の手ではなく、コンピューターにやらせよう。 インフラが正しく構築できるかどうかはそのアプリケーションのコードの正しさによって管理しよう」 というものです。 この手法によって、インフラ構築の自動化・均質化・明文化・履歴管理が可能になります。また、これらが可能になることにより再現性が非常に高く保てます。 自動化 従来手法では人間が手で行なっていた変更作業を、コンピュータが行うことで自動化が可能になります。 また、原則的には記述されたことと実際の差異が生まれないため、リソースの管理が容易になります。 均質化 従来手法ではドキュメントや実際の変更作業のクオリティ、読みやすさ・漏れ・ミスが担当の人間の経験に左右されていたのが、ある程度均質化されます。 もちろんコードであっても担当の人間によってクオリティは多少揺らぎは生まれますが、自然言語による文章表記よりもその幅は大きく減少します。 明文化 自然言語文章ではどうしても人間の解釈次第になってしまい、曖昧になってしまったり、当たり前に行うことなのでわざわざドキュメントに書かないというようなことがあります。 IaCでは書かれたコードの解釈も実行もコンピューターが行うため、曖昧さが排除され、インフラを構成するリソースのあるべき状態を明文化することが可能になります。 履歴管理 これは厳密にはIaCの特性というわけではないのですが、実際には改善することがほとんどです。 現代ではアプリケーションのコードは何かしらのバージョン管理システム(多くの場合gitでしょう)によって履歴管理が行われ、版ごとの差分や変更の意図などを記録することが可能になっています。 IaCではインフラを構成するリソースはコードによって表現されるので、従前より利用しているこのようなバージョン管理システムを利用することで、アプリケーションと同様に履歴を管理することが容易になります。 IaCの2つのアプローチ 現在、IaCには主に手続き型と宣言型の2つのアプローチがあります。 これはアプリケーションコードを記述する手法に手続き型・オブジェクト指向型・関数型のようにいくつかのアプローチがあるのと同じような状況です。 手続き型 手続き型はその名の通り、インフラを構築していく 手順 を記述していくアプローチです。 メリットは、単に手順をそのままコードに書き下していくような形になるため、最初の障壁が低く、初学者でも詰まりにくいことです。 また、コードも従来アプリケーション開発で用いられてきた汎用のプログラミング言語であることも多く、自由度が高いことも挙げられます。 デメリットは、冪等性の担保に対してのサポートが弱いことです。また、手順の積み重ねになるので、全体としての意図の見えにくくなる点や、大きな変更に弱いという点も挙げられます。 そのほか、自由度が高いが故にバグが入りうることもデメリットと言えるでしょう。 宣言型 宣言型はインフラを構成する リソースの状態 を記述していくアプローチです。現在の状態から宣言された状態への差分を自動的に埋めるように処理が行われます。 メリットは、最終的にリソースがあるべき状態を記述するため、バグが起こりにくく、冪等性も担保されやすい点や、全体としての意図が見えやすくなる点が挙げられます。 デメリットは、専用のDSLが用いられることが多いことや、手続き型より抽象的な記述になるため、最初の障壁が高いことが挙げられます。 また、手続き型よりも自由度が低いことが多く、アプリケーションコードを書きなれた人間からすると、フラストレーションを感じることが多いように思います。 ansibleとterraform さてこの時点までざっと5000字ほどあるのですが、ようやく本題です。みなさんこの文章の本題を覚えていますか? 私は忘れかけていました。なので改めて記載しますが、この文章は「ansibleとterraformって何が違うんですか?」という疑問の答えになることを目標に書かれています。 では具体的に何が違うのでしょうか? ここまでの文章を読んだのであればある程度想像がつくでしょうが、それぞれIaCを実現するアプローチが手続き型と宣言型であるという点が一番大きな差異であると言えます。 ansible ansibleは 手続き型 アプローチを採用しており、構成手順をplaybookと呼ばれるファイルにYAMLで記述するのが特徴です。 ansible自体はPythonで記述されており、プラグインもPythonで記述することができます。 類似の手続き型アプローチを採用したツールとして、chef、puppet、fabricなどがあります。これらの中でのansibleの特徴は、よりシンプルで扱いやすい点が挙げられます。 ただ、これらのツールでできることにはそれほど大きな違いはない印象です。各々の組織の要件にあったものを用いると良いでしょう terraform terraformは 宣言型 アプローチを採用しており、hclという独自言語でtfファイルにリソースを宣言するのが特徴です。 また、terraformはリソースの状態を管理するためにstateファイルというjson形式のファイルを用います。 このため、既存のリソースをterraformでの管理に切り替えるのには比較的手間がかかります。 リソースをterraformで管理したいのであれば、多少手間でも最初からterraformを使って定義することをお勧めします。 terraform自体はGoで記述されていますが、プラグイン(実際にはproviderやprovisionerと言います)はterraform本体のプロセスとgrpcで通信を行う別のプロセスとして実行されるため、各自好きな言語で記述することができます。 使い分け これまで述べてきたように、ansibleとterraformではIaCに対するアプローチが異なります。ansibleは 手順を自動化 し、terraformは 状態管理を自動化 します。 弊社では主にterraformをインフラの構築全般に使い、ansibleはサーバ単体の構築に用いています。 サーバ単体の構築は大抵の場合、リソース間の相互作用がそのサーバ単体の中で閉じられているため、全体像がそれほど複雑になりません。 また、サーバの中に配置したいものは一般的なミドルウェアであったり、あるいは自前で開発したバッチスクリプトであったり、多岐に渡るため、シェルスクリプトと一対一のように記述できるansibleが扱いやすく、採用の動機になります。 対して、VPCやセキュリティグループ、RDBなどはリソースごとに相互作用があり、どこかで定義した値を別の場所で使う、ということが多くあります。このようなシーンではterraformはうまく相互関係を解決してくれます。 また、稀にではありますがごく一時的に手作業でパラメータを変更してしまう場合もある(実際はこのような作業はあまりよくないですが、軽微な変更の検証など)ので、冪等性をツール側で担ってくれる点も採用理由の一つです。 弊社ではこのように使い分けをしていますが、近年はほとんどのサービスをECSかEKSの上に構築しており、サービスの管理はECSであれば ecs-deploy 、EKSであれば kustomize や helm を使っており、個々のコンテナはDockerfileで管理されているため、相対的にansibleの利用頻度は下がっています。 おわりに 以上、ansibleとterraformの違いについて、それぞれの特徴の説明と、弊社での使い分けについて解説をしました。 今回はそれぞれのツールの具体的な使い方やプラクティス、tipsなどの説明には踏み込めませんでしたが、いずれ機会があればそのあたりにも触れてみたいと思います。
アバター
DELISH KITCHEN WEBについて はじめに はじめまして。DELISH KITCHENバックエンドチームの梅木です。 DELISH KITCHENのバックエンドチームはアプリ向き合いとWEB向き合いのチームとで別れており、自分はWEB向き合いのチームに配属されています。 担当業務としては、DELISH KITCHENのWEBフロントの開発はもちろん、APIサーバーやインフラと、WEBサービスに関しての改修では境界を設けずに、開発・運用・監視を日々行なっています。 本日はDELISH KITCHENのWEBサービスのシステムについて、ご紹介できればと思います。 DELISH KITCHEN WEBで使われている技術スタック DELISH KICTEHEN WEBで使用されている技術スタックはこちらになります。 フロント Nuxt.js(Universal mode): 2.14 アプリケーションサーバー & BFF: express 4.x ランタイム: node 12.x コードフォーマッター: prettier / stylelint / eslint バリデーション: vee-validate 3.x テストフレームワーク: jest エラー監視: sentry ビデオプレイヤー: Videojs 状態管理: vuex ページ遷移: vue-router ページのメタ情報生成: vue-meta SSRレンダリング: vue-server-renderer APIサーバー Golang echo : 1.x インフラ AWS ECS AWS Route53 AWS Cognito AWS API Gateway AWS s3 AWS Lambda AWS CloudFront Elasticsearch ansible docker パッケージ依存関係解消ツール renovate ログ/分析 TreasureData redash 開発/運用ツール Github CircleCI Datadog terraform DELISH KITCHEN WEBでは、基本的にNuxt.jsのフレームワークのルールに従って開発しています。 vuex、vue-router、vue-metaやvue-server-rendererなどのライブラリも、最初からNuxt.jsの中に組み込まれているものを使っています。 Universal modeのNuxt.jsはBackend for Frontendとして機能するため、GolangのAPIサーバーとは別々で管理されており、Nuxt.jsアプリケーションだけを考えて開発を行うことができます。 Nuxt.jsとGolangのAPIサーバーをそれぞれdockerコンテナ化し、そのコンテナ化したマイクロサービスをAWS ECSでec2インスタンスにデプロイするという構成で運用しています。 他にも、メールアドレスログインでAWS Cognito、検索や絞り込みシステムでElasticsearch、レシピのサムネイルを縮小化しwebp拡張子で配信するシステムでAPI Gateway x Lambdaを組み合わせたサーバーレスアプリケーションを利用するなど、WEBチームでは、WEBフロント開発だけでは終わらずクラウドインフラを多く使い、裁量が広い範囲でWEBサービスの開発を行なっています。 Nuxt.jsを採用した理由 現在、DELISH KITCHEN WEBはNuxt.jsで構成されておりますが、Nuxt.jsで運用しているのは下記のような背景があります。 背景 DELISH KITCHEN WEBの1stリリースでは、 Riot.js というSPAライブラリで構築されていました。普通にブラウザで動かすSPAアプリケーションとしては何も問題なかったのですが、DELISH KITCHEN WEBはメディアサイトであるため、ユーザーだけではなくSEO対策としてクローラーのことも意識して開発する必要があります。クローラーがページにアクセスしたときは、ブラウザからアクセスされたときと同じjsが返却されるわけですが、当時のクローラーのレンダリングエンジンはjs解析にそこまで強くなく、クローラーにページの内容を読み取ってもらうことができませんでした。その為クローラーからのアクセスだった場合は静的なhtmlのページを返す必要があり、Express(nodejsのアプリケーションサーバー)がhtmlを生成してクローラーに読み取ってもらうという方針を取っていました。 問題 上記の背景により、Riot.js + Expressの構成でプロジェクト運用していましたが、ユーザー向けのブラウザで実行するコード(SPA用のコード)とクローラーに向けのコード(SSR用のコード)の2重管理をしていたので、特に新規ページや新規機能の開発、確認や運用コストが大きいという問題がありました。クローラーからのアクセスを考慮すると、クローラとユーザーごとに返却するHTMLを変えることが、SEO効果に悪い影響があるのか未知でした。そのような問題があるなか、事業側から新しい施策に打ち出したいと話があり、要件としてWEBサービスにメールアドレスログイン機能や課金機能が必要でした。コードが2重管理されているプロダクトでそのような大きい機能を無事に実装し運用できるかという不安がチーム内にありました。 他にも当時の構成について、下記のような意見もありました。 Riot.jsを本番運用しているプロダクトが少なく、開発で困ったときに参考にできる知見もあまりない。 当時のDELISH KITCHENは、ユーザーからのインタラクションでdomを表示/非表示の切り替えが発生する機会が少なく、リッチなアニメーションもないため、WEBサービス自体がブラウザでレンダリングさせるSPAアプリケーションで構成する必要がない。 ユーザーへはSPAである必要がなく、クローラーを考えるとSSRが必要 ということで、ユーザー側にもクローラーにも最初から同じコードからSSRで生成されたhtmlを返したほうがいいのではないかと考えました。大きな機能をWEBサービスに入れる前に、Universalアプリケーション(SSR + SPA)の開発ができる技術を使ってシステムリプレースをすることになりました。 検討 フレームワークレベルでUniversalアプリケーション開発が担保されている技術を検討したところ、Angular Universal / Next.js / Nuxt.jsの選択肢が出てきました。その選択肢の中からNuxt.jsを選びましたが、理由は下記となります。 当時のバックエンドチームには、バックエンドに強いメンバーがほとんどでwebフロント開発に長けているメンバーは多くなかったため、Nuxt.jsのような薄いフレームワークが取り掛かりやすいのではないかと考えた。 バックエンドチームのメンバー内でVue.jsの開発経験者が一名在籍していた。 Riot.jsのSFC(Single File Component)の作りが、Vue.jsでのコンポーネントと同じであるため、システムリプレースもしやすいのではないか。(template / script / styleとブロックを分ける構成がほぼ同じ。) 下はレシピのサムネイルとタイトルを表示するコンポーネントです。 Riot.jsのSFC <delish-recipe-item> <a href="/recipes/{ opts.recipe.id_str }"> <img riot-src="{ utils.resizeImg(opts.recipe.square_video.poster_url, opts.size) }" /> <div class="item__title-wrap"> <p class="item__title">{ getTitle() }</p> </div> </a> <style type="scss"> .item__title-wrap { text-align: left; flex: 1; margin-left: .75em; padding: 0 1em .5em 0; border-bottom: 1px solid @color5; } </style> <script> import utils from '../../misc/utils'; this.utils = utils; this.getTitle = () => { let str = this.opts.recipe.lead + this.opts.recipe.title; if (str.length < 29) { return str; } return str.slice(0, 27) + '...'; }; </script> </delish-recipe-item> Vue.jsのSFC <template> <div class="delish-recipe-item"> <nuxt-link :to="`/recipes/${recipe.id_str}`"> <img :src="utils.resizeImg(recipe.square_video.poster_url)" /> <div class="item__title-wrap"> <p class="item__title">{{ recipeTitle }}</p> </div> </nuxt-link> </div> </template> <script> import utils from '../../misc/utils'; export default { props: { recipe: { type: Object, required: true, } }, computed: { recipeTitle() { const str = this.recipe.lead + this.recipe.title; if (str.length < 29) { return str; } return str.slice(0, 27) + '...'; } } } </script> <style type="scss" scoped> .delish-recipe-item { .item__title-wrap { text-align: left; flex: 1; margin-left: .75em; padding: 0 1em .5em 0; border-bottom: 1px solid @color5; } } </style> 実際に移行してみてどうか? はじめに課題として上げた、ユーザ向けとクローラ向けのコード二重管理がなくなり、同じコンポーネントで一元管理できるようになったため、開発やQAのコスト削減を実現しました。 Nuxt.jsにリプレースしてから、SEO対策向けに多くの機能リリースを行ったことから、これらのコストを下げられたのは良かったと思っています。 Nuxt.js(Vue.js)の豊富なドキュメント、ライブラリや活発なコミュニティの恩恵を受けられ、開発して困る問題も大半は解決されました。多くの事例を参考にしながら開発をスムーズに行うことが出来ています。 追加で、webpackもNuxt.jsに組み込まれているため、ページ単位のjs分割やコンポーネントのdynamic importも可能となり、パフォーマンス対策もできています。 Nuxt.jsはフレームワークであり一定のルールに沿って開発するため、UIライブラリのRiot.jsと違い、チームメンバーの間で認識を揃えながら開発もできます。 最後に 今回はDELISH KITCHEN WEBで使われている技術スタックと弊社でNuxt.jsを採用した理由をまとめてみました。 DELISH KITCHEN バックエンドチームでは、WEBフロント開発だけではなく、APIサーバーやクラウドインフラの運用も行なっていきます。プロダクトを良くするのに必要であれば、自ら提案し新しい技術に触れられる環境です。 現在運用が安定してきた中、これからも新規機能をリリースしたり、テストやtypescript導入するなど開発改善を行うことも考えています。 このブログでDELISH KITCHEN WEBについて少しでも知っていただけたら幸いです。最後までお読みいただき、ありがとうございました。
アバター
自己紹介 はじめまして。DELISH KITCHENバックエンドチームのマネージャーをやっている内原です。 本日はDELISH KITCHENにおける、バックエンド観点でのシステム紹介を行います。この紹介によりDELISH KITCHENの開発に興味を持ってもらえると嬉しいです。 はじめに DELISH KITCHENのサービス全体像とバックエンドシステムの構成や仕様などを紹介します。 ご覧の通り、複数のマイクロサービスが様々なミドルウェアを利用しつつ、DELISH KITCHENサービスの提供を実現しています。 DELISH KITCHENのサービス全体像 DELISH KITCHENの一番主要な機能は、レシピ動画を提供することでお客様の料理体験をよりよいものにすることです。 これだけ聞くと、単に動画を配信しているだけのサービスのように思われるかもしれませんが、実際には料理にまつわる様々な事柄をサポートするシステムで、多くの機能を提供しています。 DELISH KITCHENを提供しているプラットフォーム 料理を作ろうと思った時や買い物をしている時、なんとなく流行りのレシピを知りたいと思ったときなど、色んなシチュエーションでご利用いただけるように、以下のプラットフォームでDELISH KITCHENを提供しています。 iOS/Android アプリ WEB 小売店でのデジタルサイネージ Amazon Echo DELISH KITCHENが提供しているサービス 単に料理を作る際に役立つだけではなく、特売情報をお知らせしたり買い物リストと連携することでお買い物をサポートします。 DELISH KITCHENが提供している主なサービスは以下の通りです。 人気、特集、新着、パーソナライズされたリコメンドなど、能動的なレシピ提案 カテゴリ内検索、キーワード検索など、受動的なレシピ提案 各地域の小売店特売情報と、特売商品を用いたレシピ提案 レシピのお気に入り管理 レシピのレビュー レシピ材料の買い物リスト管理 プレミアム会員向け機能やプレミアム会員限定レシピの提供 DELISH KITCHENポイントによるポイント交換、ポイントがもらえるクーポン機能 機種変更時や複数端末での同期用にメールアドレス/SNSによるログイン機構 プッシュ通知 DELISH KITCHENのバックエンドシステム全体像 複数のシステム構成 DELISH KITCHENは複数のマイクロサービスで構成されており、それぞれが別のシステムとして独立しています。 個々のシステムにおいては共通している部分も多いですが、提供している機能や使用しているサービスには差異があります。 共通の基盤/利用サービス DELISH KITCHENの各サービスで共通的に利用している基盤や外部サービスは以下の通りです。 Golang, echo インフラ AWS ECS AWS RDS AWS AutoScaling AWS ElastiCache AWS S3 AWS CloudFront AWS Route53 ログ/分析 fluentd redash TreasureData Databricks 開発/運用 github CircleCI terraform ansible 共通APIサーバ iOS/Android/WEB/サイネージなど、クライアント向けに提供しているAPIサーバです。お客様に対して提供される機能は、すべてこのAPIサーバを経由しています。 各種内部向けAPIサーバへの中継もここで行われます。 WEBシステム WEBにおけるDELISH KITCHENサービスを提供しています。 レシピなどのデータは共通APIサーバを参照しています。共通APIサーバと通信する構成であるため、BFF(Backends For Frontends)として稼働しています。 利用システム CloudFront Nuxt.js Node.js 検索システム レシピ情報の検索を提供するサーバです。 検索エンジンには Elasticsearch を利用しています。 特売情報システム 特売情報を提供するサーバです。 データとしては店舗、特売商品、チラシなどを扱っています。 別途、店舗側にて特売情報を入稿するためのtoC向けシステムも存在しますが、今回は省略します。 サイネージシステム 小売店舗に設置しているサイネージ端末が利用するサーバです。 個々のサイネージに対し、レシピ/広告を指定した配信設定が可能です。レシピ情報は共通APIサーバを参照しています。 別途、店舗側にて配信設定を管理するためのtoC向けシステムも存在しますが、今回は省略します。 ポイントシステム デリッシュポイントという、DELISH KITCHEN内でお得に使えるポイントを管理するサーバです。 ポイントを別商品に交換するには ドットマネー を利用しています。 クーポンシステム デリッシュポイントをもらうことができるクーポンサービスを提供するサーバです。 外部のクーポンサーバとやり取りをしています。 課金システム プレミアムサービスを利用可能とするため、ストア決済、キャリア決済を実現するサーバです。 プレミアムサービスはサブスクリプション形態であるため、アプリ外で購読状態を管理しています。 iOS In App Purchase Android Google Play Developer API (subscription) プッシュ通知システム アプリ向けにプッシュ通知を提供するサーバです。 サーバの実態としてはmercari社製の gaurun を利用しています。 最後に 上記のように、DELISH KITCHENは多くのシステムで構成されたサービスです。 開発、運用で使用しているインフラ技術、外部サービスも多岐に渡りますので、興味を持ってもらえると嬉しいです。 ここまでお読みいただき、ありがとうございました。
アバター