シンクロ・フード エンジニアブログ

飲食店ドットコムを運営する、株式会社シンクロ・フードの技術ブログです

アプリのローディングUIをスケルトンビューに変える対応で試行錯誤した話

はじめまして、モバイルアプリチームの佐々木です。普段は主に求人飲食店ドットコムアプリ(以下、求人アプリ)のiOS開発を担当しています。
「求人飲食店ドットコム」では飲食店専門で求人情報を掲載しており、アプリで求人情報の閲覧から応募、応募先とのやりとりまで行えます。

今回は、初めて調査からリリースまでを一貫して担当した、求人アプリのローディングUIを変更する対応でスケルトンビューを導入したので、その過程で悩んだことや学んだことについて共有します。

スケルトンビューとは

そもそもスケルトンビューとはどのようなものかご存知でしょうか?
アプリやウェブサイトでデータを読み込んでいる間にページのレイアウトだけを先に表示する、ローディングUIの一種です。スケルトンスクリーン(Skeleton Screen)やシマーエフェクト(Shimmer Effect)など、様々な呼び方があります。

スケルトンビュー

このUIのメリットとして、レイアウトが先に表示されることでユーザーはこれから表示される内容について心の準備をしやすくなり、待ち時間を短く感じさせる効果があると言われています。

導入の経緯

スケルトンビュー導入の話が出たきっかけは、WEB履歴書という画面でバグが見つかったことでした。

WEB履歴書とは応募に必要な情報をまとめた画面で、「プロフィール」「職務経歴」「経験職種」「自己PR」の四つのタブから構成されています。

WEB履歴書

この画面を開く時は各タブに対応するAPIから情報を取得しています。

求人アプリではAPI通信時にSVProgressHUDを使ってローディングUIを表示しており、WEB履歴書画面でも同様でした。
この画面では四つのタブの情報を取得するAPIへの通信を同時に並列で行い、全てが完了した時点でSVProgressHUDを非表示にする、という仕組みになっていました。
しかし、このSVProgressHUDの表示・非表示処理は、各APIで個別に制御していたり、PromiseKitを使用して一括で制御していたりと、複数APIを扱う際の実装が複雑になりがちで、今回のバグもその複雑さに起因するものでした。
そのため、以前から議論のあった別形式のローディングUIへの移行について、今回バグが見つかったことをきっかけに正式に対応することが決まりました。

WEB履歴書以外にも複数のAPIに対して並列に通信を行っている画面がいくつかあるため、これを機に求人アプリ全体で新しいローディングUIを取り入れようという話になり、影響の大きさを考えて、まずは調査から始めることになりました。

実装前の調査

スケルトンビューの導入に向けて、どういった対応が必要になるのか、どのくらい工数がかかりそうか、などを見積もるためにまず調査を行いました。

ライブラリの選定

まず最初に、ライブラリの選定を行いました。iOSで広く知られているJuanpe/SkeletonViewを候補に挙げつつ、他に適したライブラリがないかを洗い出しました。
その結果、他にも似たようなUIを実現できそうなライブラリはいくつか見つかりましたが、「Swiftで実装可能であること」「最近のリリースが古すぎないこと」「利用者が多く信頼性が高いこと」という観点から、最初に挙がったSkeletonViewが最も適していると判断し、仮実装へと進めることにしました。

仮実装での苦戦

SkeletonViewを使用した仮実装では、大きく二つの点で悩むことがありました。

①ライブラリのインストール方法

求人アプリではライブラリ管理ツールとしてCocoaPodsとCarthageを併用しており、新しいライブラリを導入する際にはビルド時間の短縮を重視してCarthageの使用を優先しています。
SkeletonViewはCocoaPods、Carthage、SPMに対応しているということで、まずCarthageでインストールを試みました。
しかし、インストール後にStoryboardを確認してもREADMEにあるようにisSkeletonableプロパティが表示されませんでした。
isSkeletonableは、UIViewのExtensionとして@IBInspectableで定義されたプロパティです。プロジェクト側で同様の@IBInspectableを追加した場合は問題なく機能するということは確認できましたが、何故かSkeletonView側で定義されたものはStoryboardに反映されませんでした。
コード上で設定する方法もありますが、SkeletonViewではスケルトン表示したい要素に対して親子すべての階層にこの設定をする必要があります。求人アプリでは、Storyboard上にある項目は可能な限りStoryboardで設定するようにしているため、再帰的にコード上で設定するのは避けたい方法でした。

調べると同様の事例が報告されており、CocoaPodsでインストールすることでisSkeletonableがStoryboardに表示されたとの情報を見つけ、CocoaPodsでのインストールを試したところ、この問題は解決しました。

isSkeletonableの設定方法

ライブラリのREADMEを参考に、showSkeleton()を呼び出す要素の下にある全階層に対してisSkeletonableをOnに設定しましたが、期待通りにスケルトン表示が動作しませんでした。showSkeleton()を呼び出す要素を変更したり、isSkeletonableを設定する階層を調整したり、実装をいくつか試した結果showSkeleton()を呼び出す要素自体もisSkeletonableをOnにする必要があることが分かりました。

こうして、READMEの記載通りに実装してもうまくいかない点があり苦戦しましたが、試行錯誤を経てスケルトンビューの仮実装を行うことができました。

仮実装を進めた結果

仮実装はWEB履歴書画面の「プロフィール」と「職務経歴」の二つのタブで行いました。
まず、比較的シンプルな構造の「プロフィール」でスケルトンビューが動作することを確認した後、UITableViewを使用した画面でも実装できるか「職務経歴」で検証しました。

まず「プロフィール」についての処理を簡易化したものが以下の通りです。

class ProfileViewController: UIViewController {
    private var profile: Profile!

    override func viewDidLoad() {
        super.viewDidLoad()
        // 画面表示と共にスケルトン表示を開始
        view.showSkeleton()
    }
    
    // その他画面の表示処理(省略)
    ...
    
    // ResumeTabViewControllerの表示処理で呼び出す
    func getDataByAPI() -> Promise<Void> {
        return Promise<Void> { resolver in
            ProfileAPI.fetchData { [weak self] result in
                guard let self else { return }
                // API通信終了時にスケルトン非表示
                view.hideSkeleton()
                switch result {
                case .success(let profile):
                    // 取得したデータをセット
                    self.profile = profile
                    resolver.fulfill(())
                case .failure(let error):
                    resolver.reject(error)
                }
            }
        }
    }
}
class ResumeTabViewController: ButtonBarPagerTabStripViewController {
    private var profileVc: ProfileViewController!
    private var workCareerVc: WorkCareerViewController!
    private var workExperienceVc: WorkExperienceViewController!
    private var selfPrVc: SelfPrViewController!

    // WEB履歴書画面の表示処理(省略)
    ...

    // WEB履歴書画面の表示時にこのメソッドを呼び出して四つのAPI通信を並列で行う
    func getChildrenTabData() {
        SVProgressHUD.show()
        firstly {
            // 各画面のデータ取得メソッドを呼び出して結果を取得
            when(fulfilled:
                [
                    profileVc.getDataByAPI(),
                    workCareerVc.getDataByAPI(),
                    workExperienceVc.getDataByAPI(),
                    selfPrVc.getDataByAPI()
                ]
            )
        }
        .done {
            // 各タブについての設定(省略)
            ...
        }
        .catch { _ in } // 各画面でエラー表示を行うのでここでは何もしない
    }
}

「プロフィール」ではデータをセットする前にhideSkeleton()を呼ぶことと、isSkeletonableの設定に気をつければshowSkeleton()hideSkeleton()を記述するだけで、スケルトン表示ができました。

UITableViewを使用した「職務経歴」でも、下記のようにデータの読み込み前後で処理を行うことでスケルトンを表示しました。

class WorkCareerViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var detailLabel: UILabel!
   
    var workCareers: [WorkCareer]?
    
    // ViewControllerの表示処理など(省略)
    ...

    // ResumeTabViewControllerの表示処理内で呼び出す
    func getDataByApi() -> Promise<Void> {
        return Promise<Void> { resolver in
            WorkCareerAPI.fetchData { [weak self] result in
                guard let self else { return }
                // API通信終了時にスケルトン非表示
                tableView.hideSkeleton()
                switch result {
                case .success(let workCareers):
                    // データに合わせて行数を可変にする
                    self.detailLabel.numberOfLines = 0
                    self.detailLabel.sizeToFit()
                    // 取得したデータをセット
                    self.workCareers = workCareers
                    resolver.fulfill(())
                case .failure(let error):
                    resolver.reject(error)
                }
            }
        }
    }
}

extension WorkCareerViewController : UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // 5はスケルトン表示時のセル数
        // workCareers?.countが取得できないときはデータの読み込み前なのでスケルトン表示をしたいセル数を指定
        return workCareers?.count ?? 5
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // セルの取得処理(省略)
        ...

        if workCareers == nil {
            // スケルトン表示時は3行に固定
            cell.detailLabel.numberOfLines = 3
            cell.detailLabel.sizeToFit()
            // tableView.showSkeleton()ができなかったためセル単位でshowSkeleton()
            cell.showSkeleton()
        } else {
            // データ取得後に関する設定
            cell.workCareer = workCareers[indexPath.row]
        }
    }
}

tableView単位でのshowSkeleton()が機能しない点で疑問を抱きつつ、SkeletonViewの実装方法として同様の方法を紹介しているサイトがいくつかあり、スケルトン表示も一応実現できたため、この方法で問題ないだろうと仮実装を終えました。
しかし、実際にはこの実装方法にはいくつかの大きな問題がありました。

実装時に判明した問題

前述の調査を踏まえて正式に対応することが決まりました。
最初はローディングUIによる問題が特に大きかったWEB履歴書画面に導入して、リリース後も特に問題が起きないようであれば他の画面にも徐々に導入していくという方針で設計をして、実装に取り掛かりました。

実装を一通り終えた段階で、レビュー時に指摘された内容も含め、仮実装の方法にいくつか問題があることが判明しました。
前章の実装例を元に、どのような問題があったかを見ていきましょう。

コードが複雑で可読性が低い

「職務経歴」の実装例ではスケルトン表示用に、UILabelの行数とセル数に関する設定をしています。
登録されたデータの長さによって行数が変わるdetailLabelについて、スケルトン表示時は三行に固定したいと、viewDidLoad()の初期表示処理の際にdetailLabel.numberOfLines = 3と設定し、データ読み込み完了時にdetailLabel.numberOfLines = 0と設定し直しています。
また、セル数についてはtableView(_:numberOfRowsInSection:)でデータ取得前には五つのセルを表示するように指定しています。

これを踏まえて全体を見ると、セルやdetailLabelという同じ項目に対する設定がコード上で離れた場所に定義されており、スケルトン表示に関する設定としても同様に離れているため、どこで何に対してどのような設定をしているのかが把握しにくい、可読性の低い実装になっています。

表示・非表示処理で単位が一致しない

「職務経歴」の実装例ではshowSkeleton()はセル単位で、hideSkeleton()tableView単位で呼び出しています。
理由としては、実装例にも記載したようにshowSkeleton()がセル単位でしか機能しなかったことに加えて、もう一つありました。
セル単位でhideSkeleton()を行うと、データ読み込み時にshowSkeleton()を五つのセルで呼び出しているため、実際の登録データが五件未満だった場合に一部のセルでhideSkeleton()を呼ぶタイミングがなくスケルトン表示が残ってしまいます。
この問題を回避するため、showSkeleton()と異なりhideSkeleton()tableView単位で行う実装にしました。

しかし、このように根本的な原因を解明せずに対処療法的な方法を取っているため、意図が分かりにくく、特に初めて実装を見た人には奇妙に感じる実装になっています。

タブを移動するとスケルトン表示が消えない

これは問題のある実装方法に加え、私自身のUIViewControllerのライフサイクルに対する理解不足も原因となって発生した問題でした。

WEB履歴書画面は、その他メニューで選択した項目がデフォルトのタブとして設定されて表示されます。
例えば、以下のGIFでは「経験職種」を選択したのでWEB履歴書画面が表示されたときには「経験職種」タブが開いた状態になっています。

「プロフィール」の実装例ではviewDidLoad()showSkeleton()を呼び出しているため、WEB履歴書画面表示時にデフォルトのタブでない場合、以下の順序で処理が行われます。

  1. 画面上に表示されていない状態でデータ読み込みを開始
  2. データ読み込みが完了してhideSkeleton()を実行
  3. 「プロフィール」タブを開いたタイミングでviewDidLoad()が呼ばれ、showSkeleton()を実行

このように、スケルトンの非表示処理が先に実行されてしまい、データの読み込みが完了した状態で表示処理が実行されるため、スケルトン表示を終了することができなくなります。

原因と修正

仮実装で起きた問題の原因

結局これらの問題の原因はSkeletonTableViewDataSourceプロトコルに準拠していなかったことでした。SkeletonViewのREADMEには、以下のようにUITableViewでスケルトン表示を行うための方法がしっかり記載されていました。

If you want to show the skeleton in a UITableView, you need to conform to SkeletonTableViewDataSource protocol.

一応言い訳をすると、調査時にこのプロトコルの存在は認識しており、実装も試したのですが、当時はなぜかうまく動作しなかったため、この方法を使わない方針で進めました。
しかし、前述の問題に直面して改めて試してみたところ、驚くほどすんなり動作しました。
おそらく、当時はisSkeletonableなど他の設定に問題があり、それが原因で正しく動作しなかったのを誤解したのだと思います。(余談ですがSkeletonViewのissueにも「原因はわからないがやり直したら上手く行った」というコメントがあり、とても親近感を覚えました)

修正後の実装

原因を踏まえて修正した、最終的な実装は以下の通りです。

class WorkCareerViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var detailLabel: UILabel!

    var workCareers: [WorkCareer]?

    // viewDidLoadだとデータ読み込み中に何度もタブの行き来があった場合に対応できないため移動
    override func viewWillLayoutSubviews() {
        if workCareers == nil {
            tableView.showSkeleton()
        } else {
            // タブ移動のタイミングによってはskeleton表示が残る場合があるので、ここでも非表示処理を行う
            tableView.hideSkeleton()
        }
    }

    // その他ViewControllerの表示処理など(省略)
    ...

    func getDataByAPI() -> Promise<Void> {
        return Promise<Void> { resolver in
            WorkCareerAPI.fetchData { [weak self] result in
                guard let self else { return }
                switch result {
                case .success(let workCareers):
                    // 取得したデータをセット
                    self.workCareers = workCareers
                    self.tableView.hideSkeleton()
                    self.tableView.reloadData()
                    resolver.fulfill(())
                case .failure(let error):
                    resolver.reject(error)
                }
            }
        }
    }
}

// UITableViewDataSourceの代わりにSkeletonTableViewDataSourceを継承
extension WorkCareerViewController: SkeletonTableViewDataSource {
    // 既存のUITableViewについての処理はそのまま(省略)

    // スケルトン表示時のセル数
    func collectionSkeletonView(_ skeletonView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 1
    }

    // スケルトン表示時にのみ適用したい設定
    func collectionSkeletonView(_ skeletonView: UITableView, skeletonCellForRowAt indexPath: IndexPath) -> UITableViewCell? {
        // スケルトン表示用の行数設定
        cell.detailLabel.skeletonTextNumberOfLines = 3
        cell.detailLabel.sizeToFit()
        return cell
    }

    // スケルトン表示したいセル
    func collectionSkeletonView(_ skeletonView: UITableView, cellIdentifierForRowAt indexPath: IndexPath) -> ReusableCellIdentifier {
        return "CellIdentifier"
    }
    
    // 以下UITableViewDataSourceに関する実装は基本的にスケルトン導入前のまま
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // データがないときはセルを表示しない
        return workCareers?.count ?? 0
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // セルの取得処理(省略)
        ...
        if self.workCareers != nil {
            cell.workCareer = workCareers[indexPath.row]
        }
    }
}

ご覧の通り、スケルトン表示に関する処理が一箇所にまとめられたことで、コードの可読性が大幅に向上しました。それだけでなく、既存の処理にはほとんど手を加えていないため、今後職務経歴の仕様が変更されて修正が必要になった場合でも、スケルトンビューの実装には影響を与えることなく対応できるようになっています。

改善されたUX

紆余曲折はありましたが、スケルトンビューを導入したことで、当初のバグが解消されただけでなく、UXが大きく二つの点で改善されました。

①読み込み中に操作ができる

これまで、SVProgressHUDの表示中は画面の操作ができないようになっていたため、四つのタブすべてのローディングが完了するまでアプリの操作が阻害されていました。
特に、四つのタブのうち一つだけ確認したい場合でも、関係のないタブの読み込みに時間がかかるとユーザーに本来必要のない待ち時間を強いていました。
今回のスケルトンビュー導入によって、タブごとに独立してローディングを表示するようになったため、各タブの読み込みが完了次第、そのタブの操作が可能になりました。

before after

②エラーが起きても画面が閉じない

スケルトンビュー導入前は四つのタブについて、データ取得からエラー表示までをまとめて行っていたため、一つでもエラーが発生するとアラートを表示してWEB履歴書画面ごと閉じるという動きになっていました。この場合、ユーザーは再度WEB履歴書画面を開いて表示し直すという操作が必要で、少し手間のかかる仕様でした。

今回の対応でタブごとに処理できるようになったため、エラー時の動きについても従来のアラート表示から、各タブごとにエラー画面を表示するように改修しました。

エラー表示

これにより、一つのタブでエラーが発生しても、目的のタブが正常に読み込めていればそのまま利用することができます。
さらに、エラー画面に再読み込みボタンを設置しているため、目的のタブがエラーになった場合でもタブ単位でリロードすることが可能になりました。これまでのようにモーダルを閉じて再度開くといった余分な操作が不要になり、利便性が向上しました。

まとめ

WEB履歴書画面にスケルトンビューを導入してから一ヶ月以上が経過しましたが、今のところクラッシュなどのバグ報告はありません。苦労して試行錯誤を繰り返した分、安定して動作する機能をリリースできたことはとても嬉しいです。

失敗の原因を振り返ると単純なものでしたが、当初はCarthageでの導入やisSkeletonableの設定が上手くいかなかったことで、SkeletonViewのREADMEよりも、検索で見つけたサイトの実装例を過信してしまいました。
今回の経験を通じて、不慣れな対応をするときは特に、一度冷静に情報を整理することが大切だと実感しました。また、UITableViewのライフサイクルや求人アプリの仕様など、基本的な部分を改めて理解し直す良い機会にもなりました。

今後はWEB履歴書以外の画面にもスケルトンビューの導入を進めていく予定ですが、今回の失敗や学びをしっかりと活かしながら、より良い実装ができたらと思います。
このブログがスケルトンビューの導入を検討している方やローディングUIに悩みを抱える方にとって、少しでも参考になれば幸いです。
最後までお読みいただき、ありがとうございました。

Lambda(Ruby) + Slack スラッシュコマンドで社内ジョブをクラウド化

初めまして、SREチームの柴山です。
今回は 社内サーバーで動いていたデプロイ補助ツールを、 Lambda+Slack でクラウド化した際の作業を備忘録として共有します。

背景

このツールはオンプレミスの社内サーバー上の Jenkins のジョブとして用意され、都度ウェブブラウザからジョブを実行して実行結果をジョブのログとして確認していました。
この度オンプレミスの社内サーバーを廃しクラウドに移行するにあたり、 Slack からも見れるようにしたいとの要望があり、本対応を行いました。

構成概要

処理の流れは以下のようになっています。

  1. ユーザーがスラッシュコマンドを実行
  2. Slack から 認証用(一時応答用) Lambda を起動
  3. リクエスト認証後、本処理用 Lambda を非同期実行
  4. 一時応答を Slack へ返す
  5. 本処理での実行結果を投稿

Slack App の作成

Your Apps から Create New App を押下して Slack App を作成します。
スコープ・設定の構成方法について選択項目がありますが、From scratch で問題ありません。
ワークスペースの指定、Slack App名を設定します。※ワークスペースは通知先を指定してください。

Slack App からチャンネルへの投稿に必要な権限を設定します。
Your Apps -> <作成した App> -> OAuth & Permissions -> Scopes -> Bot Token Scopes

項目の下部にある Add as OAuth Scope を押下し、以下の権限を付与します。

  • chat:write
    • 必須
    • チャンネルへの投稿に必要
  • chat:write.customize
    • 必要に応じて追加
    • Slack API 経由での投稿時、アイコンやBot名をカスタマイズできます。
  • chat:write.public
    • 必要に応じて追加
    • 特定のチャンネルへ App 追加をしていなくても、投稿が可能になります。
  • commands
    • 手動での設定は必要ありません。
    • 後の手順で自動的に設定されます。

Lambda の作成

以下のLambda関数を作成します。

  • 前段の認証・一時応答用 Lambda
  • 本処理用の Lambda

Lambda側のランタイムやライブラリに依存しないように、いずれもコンテナイメージを作成・使用します。
今回紹介するコードの実行に最低限必要な Dockerfile、Gemfile を共有します。
必要に応じて記述の修正・バージョンの固定などをしてください。

FROM public.ecr.aws/lambda/ruby:2.7

COPY Gemfile ${LAMBDA_TASK_ROOT}/

# Gem のビルドに必要なライブラリをインストール
RUN yum -y install gcc-c++ make \
    && gem install bundler:2.4.22 \
    && bundle config set --local path 'vendor/bundle' \
    && bundle install \
    && yum -y remove gcc-c++ make \
    && yum clean all

COPY lambda_function.rb ${LAMBDA_TASK_ROOT}/

CMD [ "lambda_function.LambdaFunction::Handler.process" ]
source 'https://rubygems.org'

gem 'json'
gem 'net'
gem 'uri'
gem 'json'
gem 'set'
gem 'pp'
gem 'date'
gem 'time'
gem 'rack'
gem 'aws-sdk-lambda'
gem 'base64'
gem 'slack-ruby-client'
gem 'jwt'
gem 'faraday-retry'

前段のLambda

Lambda の IAM に lambda:InvokeFunction 権限が必要

先に全体のコードを共有します。

require 'json'
require 'pp'
require 'date'
require 'time'
require 'rack/utils'
require 'aws-sdk-lambda'
require 'base64'

module LambdaFunction
  class Handler
    def self.verify_slack_request(event)
      # Slackのリクエストを検証する
      timestamp = event['headers']['x-slack-request-timestamp']
      slack_signature = event['headers']['x-slack-signature']

      # リクエストがエンコードされている場合はデコードする
      if event['isBase64Encoded']
        body = Base64.decode64(event['body'])
      else
        body = event['body']
      end

      # 5分以上経過したリクエストは無効とする
      if (Time.now.to_i - timestamp.to_i).abs > 60 * 5
        return false
      end

      # リクエストの署名を検証する
      req_signature = "v0:#{timestamp}:#{body}"
      my_signature = "v0=" + OpenSSL::HMAC.hexdigest('sha256', ENV['SLACK_SIGNING_SECRET'], req_signature)
      Rack::Utils.secure_compare(my_signature, slack_signature)
    end

    def self.process(event:, context:)
      if verify_slack_request(event)
        # 本処理用lambda の呼び出し
        lambda = Aws::Lambda::Client.new
        lambda.invoke(function_name: '<function_name>',invocation_type: 'Event', payload: JSON.generate({event: event, context: context}))
        # 一時的に応答
        return { text: '処理中です...' }
      else
        # 認証に失敗した場合は 401 を返す
        return { statusCode: 401, body: 'Unauthorized' }
      end
    end
  end
end

こちらの Slack公式ドキュメントを参考に実装内容を解説していきます。 https://api.slack.com/authentication/verifying-requests-from-slack

timestamp = event['headers']['x-slack-request-timestamp']
slack_signature = event['headers']['x-slack-signature']

Slack からのリクエスト HTTPヘッダー に 署名シークレット が含まれているため、変数へ格納します。

require 'base64'

body = Base64.decode64(event['body'])

Slack からのリクエスト本文は base64 でエンコードされており、デコード処理を入れています。

require 'time'

if (Time.now.to_i - timestamp.to_i).abs > 60 * 5
  return false
end

The signature depends on the timestamp to protect against replay attacks.
引用: https://api.slack.com/authentication/verifying-requests-from-slack

リクエスト時のタイムスタンプを必須とし、5分以内のリクエストのみを通すようにします。

require 'rack/utils'

req_signature = "v0:#{timestamp}:#{body}"
my_signature = "v0=" + OpenSSL::HMAC.hexdigest('sha256', ENV['SLACK_SIGNING_SECRET'], req_signature)
Rack::Utils.secure_compare(my_signature, slack_signature)

3.Concatenate the version number, the timestamp, and the request body together, using a colon (:) as a delimiter.
4.Hash the resulting string, using the signing secret as a key, and taking the hex digest of the hash.
5.Compare the resulting signature to the header on the request.
引用: https://api.slack.com/authentication/verifying-requests-from-slack

リクエストに使用されている署名シークレットを検証します。

  1. バージョン番号(v0)、タイムスタンプ、リクエスト本文(token=hogefuga...) の三つを連結します。
  2. 連結した文字列をハッシュ化し、16進数のダイジェスト値を取得します。
  3. リクエストの署名とSlack App側の署名を比較します。
    • タイミングアタック防止のため、署名の比較は Rack ライブラリを利用します

ENV['SLACK_SIGNING_SECRET'] には Slack App の署名シークレットを格納しています。
署名シークレットは以下の項目で確認ができます。
Your Apps -> <作成した App> -> Basic Information -> App Credentials

require 'aws-sdk-lambda'

lambda = Aws::Lambda::Client.new
lambda.invoke(function_name: '<function_name>',invocation_type: 'Event', payload: JSON.generate({event: event, context: context}))
return { text: '処理中です...' } # 一時的な応答として表示される文言です。

AWS API 用のライブラリを使用し、後続の Lambda を呼び出します。
invocation_typeEvent にすることで、非同期での呼び出しが可能になります。

最後に 一時応答用のテキストを返します。Slack 側からは以下のように表示されます。

operation_timeout が返ってくる場合、Lambda のメモリを512 MB 以上に上げてみてください。

本処理用のLambda

Slack から呼び出された Lambda とは切り離されているため、後続での処理に関しては基本自由になります。 今回は メッセージ送信処理の例を紹介します。

require 'slack-ruby-client'

Slack.configure do |config|
  config.token = ENV['SLACK_ACCESS_TOKEN']
end
slack_client = Slack::Web::Client.new

channel = '#通知先チャンネル'
response = slack_client.chat_postMessage(channel: channel, text: body, mrkdwn: false)

ENV['SLACK_ACCESS_TOKEN'] には Slack App の署名シークレットを格納しています。
署名シークレットは以下の項目で確認ができます。
Your Apps -> <作成した App> -> OAuth & Permissions -> OAuth Tokens -> Bot User OAuth Token
Bot User OAuth Token は Slack App がワークスペースにインストールされたタイミングで発行されます。

関数URLの設定

以下の内容で 前段側 Lambda に関数URLを発行します。

  • 認証タイプ: NONE
  • 呼び出しモード: BUFFERED (デフォルト)

発行した URL はスラッシュコマンドの設定に使用します。

Slack App での スラッシュコマンド設定

Slack App の設定ページから、スラッシュコマンドの設定を行います。
Your Apps -> <作成した App> -> Slash Commands -> Create New Command

以下の項目を設定します。

  • Command: 自由に記述
    • 必須
  • Request URL: 前段 Lambda の関数URL
    • 必須
  • Short Description: 自由に記述
  • Usage Hint: 自由に記述

※変更が反映されない場合は、ワークスペースへの再インストールを実施してください。

Slackへ App の追加

※以下の作業は スラッシュコマンドを実行したいチャンネルで実施してください。

Slackのチャンネル名を押下すると、チャンネル詳細が表示されます。

インテグレーション -> App -> アプリを追加する
ワークスペースに存在する Slack App の一覧が表示されるため、作成した App を追加してください。

以上で、全ての設定が完了しました。
対象のチャンネルから作成したスラッシュコマンドが実行可能になります。

Lambdaの処理を二つに分けている理由

Slack の スラッシュコマンドの仕様として、リクエストを受けてから3秒以内に応答を返す必要があります。
少し複雑な処理を行おうとすると、応答までに3秒以上かかりリクエストがタイムアウトしてしまいます。
参考: https://api.slack.com/interactivity/slash-commands#responding_basic_receipt

また、分けることによってパブリック状態にある Lambda と 本処理を切り分ける意図もあります。
構成は一つ増えますが、基本的には分けることをオススメします。

まとめ

今回は 社内サーバー上のジョブを Lambda + Slack スラッシュコマンドへ移行した際の設定内容を共有させていただきました。
スラッシュコマンドを使用することで、簡易的なスクリプトの実行が容易になります。
対象となったジョブは デプロイ対象を一覧表示してくれる便利系のツールで、誰でも・いつでも 実行と確認が可能でした。
Slack へ移行したことで スマートフォンなどからも操作が可能になり、利便性も上がったと思います。

当記事が、同様の実装をされる方の役に立てれば幸いです。

Slackを活用したチームの課題解決と共有プロセス

こんにちは、開発部の椿です。
飲食店ドットコムモビマルなどのサービス開発・運用保守を担当する「会員企画開発チーム」のチームのリーダーを務めています。
今回は、チーム内で実践している「問題・課題をチーム全体で解決していく」という運用方法についてご紹介します。

運用を始めた経緯

最初のきっかけ

私たちのチームでは毎月バグの振り返り会を開催し、バグ抑制のための対策立案とその振り返りを行っています。 この過程で、技術不足に起因するバグを抑えるため、問題のあるコードをメモし、チームのコーディング規約としてまとめていく方針を立てました。

発展

まず、Slackに問題のあるコードをメモしていく運用を開始しました。弊社は開発部のチャンネルは dev という接頭辞をつけるルールがあり、「会員企画開発チーム」なので kaiin (ローマ字読みがダサいですが…)という言葉を使って「#dev_kaiin_review_notes」というチャンネルを作成しました。
当初は気軽に書き込めて良いという意見がある一方で、気になるコードがあってもまだ書き込んでいなかったり、個人用のチャンネル(times チャンネル)に書いてしまったりという意見もありました。

投稿数も週に1、2個程度と、あまり活発なチャンネルではありませんでした。確かに、問題のあるコードを毎日見かけるわけではありませんよね。
そこで、より気軽に投稿できるよう「#dev_kaiin_notes」にチャンネル名を変更し、投稿内容の制限をなくしました。
その結果、問題のあるコードだけでなく、サービス仕様の落とし穴や開発方針の相談など、さまざまな内容の投稿がされるようになりました。

壁の出現

多様な内容が書き込まれるようになり、1日に1件以上のペースで投稿が増えました。 すると今度は、確認する時間が足りなくなってしまいました。
開発の方針に関わる重要な相談をしたくても、投稿順に確認していくため、なかなか順番が回ってこない…という状況が発生しました。
毎日の朝会で確認していましたが、時間が足りず、確認のためのミーティングを増やすべきかという議論まで出るほどでした。

最終的な形

最終的に、各投稿には重要度の違いがあるはずで、重要度の高い投稿から確認していくべきだという結論に至りました。 重要度の低い投稿は、後回しになっても構わないという考えです。
緊急で話したい投稿には特定のリアクション(「会員企画相談」)をつけ、優先的に確認します。
その後、リアクションのついていないものを順に確認していく運用に落ち着きました。

#dev_kaiin_notes の運用方法

現在の具体的な運用方法をご紹介します。

チャンネルへの書き込み

現在の #dev_kaiin_notes チャンネルは「ちょっと気になったこと、共有したいこと、何でも良いのでメモとして残す場所」と定義しています。 例えば、以下のような内容が投稿されています。

  • コード上の問題点
  • リファクタリング案
  • チーム内への共有事項
  • 便利なツールの紹介
  • 相談事項
  • やりたいことの提案

意識しているのはとにかく気軽に投稿するということです。 「ここのインデントがずれている」くらいの些細なことも歓迎しています。 その結果、先月は1か月で48件、営業日数で割ると1日あたり2.5件の投稿がありました。

投稿の確認

毎朝開催しているチームの朝会で、全員で投稿を確認しています。 どんな些細な投稿でも議題に上げ、投稿者に内容を共有してもらいます。

朝会では #dev_kaiin_notes の確認以外にも議題があるため、通常は最後の余り時間で確認します。 ただし、「会員企画相談」のリアクションがついた投稿は、他の内容よりも優先して確認することで、重要な案件の確認が遅れないよう対策しています。

ここで意識しているのは全員で全ての投稿を確認することです。 投稿内容を全員で共有し、誰もが平等に意見を言える場を目指しています。

確認後

確認後は必ず「次にやること」を決めます。 些細な問題点で優先度が低くなることはあっても、タスクとして切り出し、手が空いた時に改善できるようにしています。 (もちろん、単純な共有など、確認だけで完結するものもあります。)

この運用で改善できたこと

当初の目的であった技術力向上については、#dev_kaiin_notes をきっかけにコーディング規約が充実し、全員がある程度のクオリティのコードを書けるようになってきました。 また、全員で確認することで各自が問題点を意識できるようになり、技術力の底上げにつながっています。

副次的な効果もいくつか現れました。 誰でも気軽に投稿でき、全員で確認して話し合うことを日常的に行うことで、チーム内の心理的安全性が高まり、活発な議論が生まれるようになりました。 さらに、気軽な共有を通じて、個人が持っていた暗黙知をチーム全体の形式知に広げられています。

弊社開発部は2020年のコロナ禍を機にフルリモート勤務となり、それ以前よりもコミュニケーションが取りづらくなっていました。 しかし今では、#dev_kaiin_notes をきっかけに、チーム内で活発なコミュニケーションが取れるようになったと実感しています。

おまけ

弊社ではドキュメントツールとして esa.io を利用しており、朝会の議事録は esa に残しています。 しかし、投稿は Slack で行うため、投稿と議論を結びつけることが難しくなっていました。
そこで、esa の内容を Slack に転記するスクリプトを作成し、esa のコメント webhook から AWS Lambda function でスクリプトを実行する仕組みを構築しました。 これにより、Slack だけで過去の議題と結論を確認できるようになりました。

まとめ

#dev_kaiin_notes という、誰でも気軽に書き込める Slack チャンネルを活用して問題点の吸い上げや知見の共有が促進され、活発なコミュニケーションを通じて継続的に改善を図るチームを形成できました。
繰り返しになりますが、大事なことは以下の3点です。

  1. 何でも良いのでとにかく気軽に書き込める場所にすること
  2. チーム全員が参加する場で確認すること
  3. 優先度をつけて確認ができるようにしておくこと

この取り組みはサービス開発を行う他のチームにも展開され、各チームで独自の notes チャンネルを作って運用しています。

Kaigi on Rails 2024に参加しました

アプリケーション基盤チームの深野です。
普段は社内の開発環境の整備やいくつかのアプリケーションのRuby/Railsのバージョンアップなど、SRE的な業務のインフラ寄りでない部分を主に行っています。
弊社では、業務時間内の扱いとして勉強会や技術カンファレンスに参加できる制度があるため、Ruby Kaigi 2024に続いて人生で2回目の大規模な技術カンファレンスとしてKaigi on Rails 2024に参加してきました。
Ruby Kaigi 2024とは異なり、Kaigi on Rails 2024は弊社は特にスポンサードなどはしていないのですが、快く送り出していただけました。

Kaigi on Railsとは

今回私の参加したKaigi on Rails 2024は、「初学者から上級者までが楽しめるWeb系の技術カンファレンス」をコアコンセプトにした技術カンファレンスです。
今年6月に弊社の大庭が参加レポートを書いたRuby Kaigi と異なるのは、名前の通りRuby KaigiがRubyという言語を中心にした技術カンファレンスであるのに対し、Kaigi on RailsはRailsを中心とした技術カンファレンスとなっています。
Ruby Kaigiに1回、Kaigi on Railsに1回しか参加していない技術カンファレンス経験の浅い自分がこれらを比較するなら、以下のようになるでしょうか。

  • Ruby Kaigi
    • 基本的に全てRubyに関する話題
    • 開催地は全国各地
    • セッションは基本的に30分
  • Kaigi on Rails
    • Railsに関する話題が中心だが、たまに直接はRailsと関係ないものもある
    • 開催地は今のところ全て東京都内(2025年は東京駅直通の建物内でやるそうです!)
    • セッションは15分の比較的短いものと30分のもので分かれている

ただこれらは2025,2026...と続いていくとどんどん変わっていくかもしれませんし、感想としては参加者の顔ぶれや全体的な雰囲気など2つのイベントは色々似ているところも多いと感じました。

講演感想

どの講演も面白かったのですが、個人的に特に印象に残ったものの感想が以下になります。

1日目

Rails Way, or the highway(オープニングキーノート)

非常に濃密で中身のある発表で、スライドの中で出てくる言葉がとても印象的でした。特に

  • Rails Way is a philosophy of building Rails applications
  • Think as a framework author, not a custom application developer

という2つは優れたRails開発者になるためのマインドセットを的確に言語化してくれたと思いました。また、これらを実現するためにキャッチーな一言で終わるのではなく、

  • 抽象化レイヤーの間に明確な境界を引いて、各抽象化は自身より下の情報しか見ないようにする
  • RailsでデフォルトのMVCだけでは対応できない要件にぶつかった時には、Railsフレームワークの製作者のように考えてベースのclassを作ることで抽象化を実現する(こちらは少し私の解釈も入っているかもしれないです)

と言った具体的な方法論まで述べられていました。
今後Railsで開発していて単純な方法論としてのRails Wayでは解決できない問題(MVCだけで完結しない種類の要件を実装しないといけない時)にぶつかった時にどう対処するべきか、というマインドセットが手に入ったのは本当に大きかったです。
Ruby Kaigi 2024もそうでしたが、Kaigi on Rails 2024は贅沢なことに同じ時間に複数のセッションがあるため、懇親会などで昼間のセッションの話をしようとしてもなかなか同じセッションを聞けておらずあまり深い話ができないということがありました。
そんな中で、オープニングのキーノートセッションは参加者のほぼ全員が聞いていることで話題の中心になることが多かったのですが、今回のこの発表は分かりやすく、面白く、それでいて他の人にこの話題について色々語りたくなるというオープニングキーノートとして欲しいものが全てあって大満足でした。

現実のRuby/Railsアップグレード

私が最近の業務でRuby/Railsのアップグレードをしていると言うこともあり、タイトルを見た時点でかなり気になっていました。
実際に聞いてみた感想としても、自分が業務で今実際にやっていることや、これからやろうとしていることが多く紹介されており、共感できるとともに自分の今やっていることが大枠では間違っていないという自信になりました。
一方でこれまでの自分にはなかった視点の内容も多く、これから意識したり導入したりしたいというように思えました。特にこれから参考にしようと思ったのは、

  • gemを導入するかどうかの判断基準として、もしそれの更新が止まったときに自分が保守したり、別のgemに置き換えられるかの視点を持つこと
  • gemを導入する時には、Railsをモンキーパッチしているものは一見とても便利に見えても基本的に避けること

というgemの導入判断基準を紹介されているところでした。
実際にアップグレード作業をしていても、古かったりサポートが切れているgemの扱いにはよく困るので、次からはgem導入時にこれらの視点を持っていきたいです。

2日目

都市伝説バスターズ「WebアプリのボトルネックはDBだから言語の性能は関係ない」

Ruby Kaigi 2024でのosyoyuさんが作成したRubyプロファイラpf2についての発表である「The depths of profiling Ruby」が個人的にとても面白かったので、今回の発表についても是非聞きたいと思っていました。

実際に、発表内容はRuby Kaigi 2024での内容からある程度関連性がありつつも、Kaigi on RailsということもありWebサーバーにフォーカスした内容になっていました。
内容としては、「WebアプリのボトルネックはDBだから言語の性能は関係ない」という都市伝説を根拠を持って打ち砕いた上で、具体的にRubyとGoという異なる言語で実装されたWebサーバーでは、現実的な条件下でどのようなパフォーマンスの違いがあるのかを実際に検証して考察するものになっていました。
私は不勉強だったため、Ruby Kaigi 2024に参加したことで初めてRubyの言語的な制約であるGVLの存在や、Rubyの言語開発者がパフォーマンス上の問題にどのように立ち向かおうとしているかを知りました。本発表を聞いたことでそこから発展して、それらのRubyの制約やこれから導入される新しい機能が具体的にRubyによって構築されたWebサーバーのパフォーマンスとどのように繋がっているのかというところの一端に触れることが出来ました。Ruby KaigiとKaigi on Railsの橋渡しをしてもらえるような発表内容だったと個人的には思いました。

WHOLENESS, REPAIRING, AND TO HAVE FUN: 全体性、修復、そして楽しむこと(基調講演)

Kaigi on Railsを締めくくるラストの基調講演であり、これから成長していきたいエンジニアに向けられた発表内容はとても印象に残りました。
最初にタイトルを見た時には正直、どちらかというと抽象的な話なのかなと思っていたのですが、実際にはこれから経験を積んでいく段階のエンジニアが良いWebシステムを作れるようになるためにやるべきことが具体的にまとめられている内容になっていて、偶然かもしれませんが初日のキーノートとも上手く対応しているような関係にもなっていると思いました。

特に、うまく機能するWebシステムを作るために必要なシステムのWHOLENESS(全体性)への理解を深めるために、

  • LB, CDN, キャッシュサーバー, Webサーバーなどのシステムコンポーネントがなぜそこにあり、どのように機能しているのかを知ることが重要
  • なぜなら、設計とは現実世界の問題を1つずつ解いていくことの積み重ねにあるものだから
  • 分散アーキテクチャやマイクロサービスなどは設計していった結果、「そうなる」もの

という主張がされていたのはとても納得感がありました。ただ流行りに飛びつくのではなく、基礎である要素技術への理解を積み重ねていったその先として新しいアーキテクチャを理解できることが優れたエンジニアへの道だと思うので、Webシステムの構成要素をそれぞれ地道に一歩一歩勉強していくことへの背中を押してもらえました。

おわりに

Kaigi on Railsは初参加でしたが、どの発表も面白かったり、共感できたり、ためになったりして色々な角度から楽しむことができました。
発表だけでなく懇親会でも以前同じ職場で働いていたエンジニアと話せたり、新しいRuby/Railsの機能を実際に運用している他社の人からリアルな感想を聞けたりしてとても充実感がありました。
Ruby KaigiもKaigi on Railsもチケットが売り切れるのがかなり早いので、来年以降に参加しようと思っている方はチケットが販売されたら早めに購入することがおすすめです!

社内サーバー上のJenkinsジョブからAWS Codeサービス群にデプロイ処理を移行しました

はじめまして、SRE チームの佐藤です。
SREチームの業務として、サービス信頼性向上のためのインフラの構築/改善や保守業務や、開発業務効率向上のためのCI/CD整備等に携わっています。

先日、社内サーバー上のJenkinsジョブで実装されていたビルド/デプロイの仕組みをAWS Code サービス群やAWS Step Functionsを利用した新構成へ移行しました。
今回はその対応や検討の経緯を紹介させていただきたいと思います。

移行の背景

既存構成

シンクロ・フードで提供しているサービスは基本的にRuby on Railsで構築されているのですが、昔から使っているシステムではJava (Seasar2)が利用されています。
Seasar2 から Railsへのリプレースを進めているためかなりの部分がRailsに移行されていますが、完全には移行完了できておらず、現時点でSeasar2側の開発もそれなりの頻度で発生している状況でした。

CI/CDの構成もそれぞれ異なっており、Rails側はGitHub Actionsを使ったテストやAWS Code サービス群を使ってビルド/デプロイを実施していますが、一方のSeasar2側は社内サーバー上のJenkinsジョブを利用してテストからデプロイまでを実施していました。

既存構成の課題

Seasar2向けのJenkinsジョブによるCI/CDは長らく運用されていたこともあり、処理自体は非常に安定していましたが、以下の課題感も持っていました。

課題1: 設備/HW面の懸念

開発業務を継続する上で重要な役割をもったサーバーですが可用性の観点に懸念がある状態でした。
社内サーバーは文字通り社内(オフィス)に置かれているサーバーです。データセンタのように電源系統冗長化はされておらず予備の電源装置などもないため、設備メンテナンスなどでオフィスが停電する際に必ず停止期間が発生していました。
また、HW保守契約なども特に結んでおらず、機器に障害が起きた場合は交換部材を探すところから対応する必要があり万が一の際の復旧時間にも懸念がありました。

課題2: 維持管理の手間

こちらはサーバー運用でよくある課題かと思いますが、OSやソフトウェア(Jenkins自体やJenkinsのplugin)のバージョンアップが後回しになっており、本サーバーについては塩漬け状態でした。
いつかは対応が必要と思いつつも、サービス提供側の基盤構築/運用をどうしても優先してしまう状況が続いていたため、そもそも維持管理が不要な状態としたいと感じていました。

移行方針

前述の課題に対する対応方針を以下のように整理し、詳細な検討を進めていきました。

"課題1: 設備/HW面の懸念"への対応方針

弊社ではデータセンタは利用しておらず、クラウドサービス活用が第一の選択肢となりそうです。
複数のクラウドサービスを利用している状況ですが、AWSを主なクラウド基盤として利用しているため、AWSへ移行することで設備/HW面の課題については対応する方針としました。

"課題2: 維持管理の手間"への対応方針

色々な選択肢がありそうですが、Rails側では既にGitHub ActionsやAWS Codeサービス群を活用したCI/CDが実装されているので、Seasar2側もこのあたりを軸に構成決めていくことで維持管理の手間は最小化する方針としました。

移行対応の詳細

ここからは、どのように移行を進めていったかの詳細を記載します。

構成概要

細かな検討経緯などは一旦後回しにして、対応の前後でどのように構成変化したかを記載します。

着手前の構成

移行前の構成

  • CI/CDに係る主な要素

    • AWS上
      • ソースコード管理: AWS上にGitHub Enterprise Server(以下GHES)
    • 社内サーバー上
      • CI: Jenkinsジョブでテストや成果物作成を実行していました
      • CD: JenkinsジョブでTomcatへのデプロイ処理を実施していました
  • デプロイ処理の大まかな流れは以下の通りです

    • 1: Jenkinsジョブ実行時のパラメータ指定でデプロイ対象等挙動を調整する
    • 2: デプロイ対象サーバーをLBから切り離す
    • 3: デプロイ対象サーバーのTomcatを停止する
    • 4: デプロイ対象サーバーにビルド成果物を配置する
    • 5: デプロイ対象サーバーのTomcatを起動する
    • 6: デプロイ対象サーバーをLBに再接続する
    • 7: 冗長構成のため次のデプロイ対象サーバー向けに同様の処理を行う
  • デプロイ実装方法については本番/非本番環境間の構成差異や実装タイミングの違いに起因して若干の差異がある状況でした
    • シェルスクリプトによる実装
    • Fabric(デプロイに活用できるPythonのライブラリ)による実装

移行後の構成

移行後の構成

移行後のJavaアプリデプロイフロー

  • ビルド部分はGitHub Actionsに移行
    • ビルド成果物はS3に配置する
  • デプロイ処理は以下の組み合わせで実装
    • AWS CodePipeline(以下CodePipeline): パラメタ指定で処理を起動
    • AWS Step Functions(以下Step Functions): パラメタに応じて処理調整
    • AWS CodeBuild(以下CodeBuild): Step Functionsから呼び出され、各State向けに用意されたAnsible Playbookを実行することでデプロイを行う
  • その他、本記事の記載対象とはしていませんが、社内ライブラリを管理するために利用しているMavenリポジトリをAWS CodeArtifactへ移行しています

検討経緯

テスト/ビルド部分の移行先として、組織としてCIにGitHub Actionsを利用する方針だったのでGitHub Actionsにすんなり決まりました。

一方でデプロイ箇所については結構悩むことになりました。
もともとは CodePipeline & Deployを軸としたシンプルな構成で実装したいと思っていましたが、AWS CodeDeploy(以下CodeDeploy) への移行に課題があったのです。

  • 障壁1: 複雑な条件分岐や順序制御が難しい

    • 詳細は本記事では割愛するのですが、セッションレプリケーションの仕様上、同一サーバー上のTomcatを連続して停止してしまうとセッションが巻き戻る問題がありました
      • 詳細についてご興味あれば弊社の過去ブログも是非ご参照ください
      • こちらについてはCodeDeployのデプロイグループをサーバー単位で作成し、順次呼び出しを行うことで解決できそうです
    • 既存環境ではJenkinsジョブ実行時のパラメタで挙動を制御することで、デプロイ対象を制御するなどを行っており、移行後も引き続き同機能を提供したいと考えていました
      • 実現方法検討してみたのですが、CodeDeployだけでこのような制御を実装することは難しそうに思えました
  • 障壁2: CodeDeploy Agentを導入できない問題

    • そもそものデプロイ対象サーバーのOSが残念ながらCodeDeploy Agentに対応していませんでした
    • Apache License 2.0で公開されているCodeDeploy Agentの導入も検討したのですが、こちらもRubyのバージョンが条件を満たしていない状況でした
    • 前述の障壁のこともあり、Rubyのバージョンアップ検証を実施するくらいならいっそ代替案を検討してみようと考えました

代替案

当初考えていたCodeDeployによる実装が難しいため、次の選択肢としてAWS上のなんらかのコンピュートリソース上でデプロイ処理を実行するという方式を検討しました。

まず、検討したことはデプロイ処理の実装方法です。
既存ではシェルスクリプトとFabricを利用していたため、比較的複雑な処理を実装していたFabric側に併せて処理統一することも検討したのですが、ここでも懸念がありました。

  • Fabricのv1を利用しているが、現在Fabric v2以上の利用が推奨されている
  • Fabricのv1から v2移行に際しては一定コード書き換えが必要そう

現在SREチームではサーバーのOS以上のレイヤの管理は原則Ansibleを利用して管理する運用となっていたこともあり、どうせ書き換えが必要ならいっそのことすべてAnsibleで実装してしまう。という方針にしました。
もう一つの選定理由としてはAnsibleは普段から書き慣れており、かつ処理ロジックも既存処理から流用できるので、移行コストがさほどかからないのではないか?という今振り返るとやや楽観的な見積もりもありました(実際はサーバー間の実行順序制御などツール間の違いを埋める検討が必要で片手間で対応できるほど容易ではありませんでした。)

次にAnsibleを実行するコンピュートリソースの選定です。
極力管理対象OSは増やしたくないという思いがあり、EC2利用以外で検討しました。
ECSタスクとして実行する方法や、CodeBuildから呼び出す方法を検討しましたが、既にRails側でCI/CDで利用しておりNW環境が整っていたこともありCodeBuildを採用する方針としました。

あとはCodeBuildをどのように呼び出すかを検討します。
移行前のJenkinsではパラメタを使って柔軟にデプロイ処理対象を制御していたので、移行後も極力同等の機能は提供したいと思いました。
こちらについては、CodePipeline のv2タイプ パイプラインでは実行時に変数が指定できるようになっているので問題はなさそうです。
(余談ですが、Jenkinsの場合はパラメタに設定する値をドロップダウンメニューから選べたりして操作が一目瞭然だったのですが、CodePipeline側では現状パラメタ指定は文字入力のみのようです。手順書を用意したり、パラメタ説明に期待する入力値を記載するなどでお茶を濁しています)

次に受け取ったパラメタを使ってどのように処理を調整するかを検討しました。
AnsibleのPlaybook内でパラメタを使って制御するという方法は以下の理由で検討外としました。

  • Playbookが複雑になりすぎてしまう懸念がある
  • デプロイ処理の各所でWEBページの応答確認を行っている都合上、単一Playbook内で冪等性を保証することが難しい

その他にも、なんらかの理由でPlaybookが失敗した場合、問題を取り除いた後に気軽に処理を再スタートしたいという思いもありました。
(既存のJenkins上の処理は単一のジョブとして構成されており、かつ処理に冪等性がない状態でした。稀にジョブが失敗した場面ではジョブ再実行ができないためあたふたしながら手動で後続の作業を実施してたりしたので、この機会に改善したかったという思いもありました)

今回は以下の理由からStep Functionsで処理の分岐を行い、Step FunctionsからCodeBuildを呼び出す構成を取る方針としました。

  • Step Functions は社内で活用されており、一定知見が溜まっていた
  • Step Functionsにはredrive機能で失敗したStateから再実行が可能で活用できそう

実装

AWSの管理はTerraformを利用しているため、Step Functionsの実装もTerraformから実施しました。
直接 Terraform のコードを新規に作成するのではなくAWS管理コンソールから処理を組み立てた後、JSON ベースの構造化言語(Amazon States Language)をTerraform に取り込み、これをベースに適宜修正を行うことで比較的短期間で実装できました。

前述の通りCodePipelineやCodeBuild周りの環境整備についても、既に利用している環境だったので追加で検討した箇所はIAMやSGくらいで、概ねスムーズに実装ができました。

一点実装段階で検討不足が露呈したのは、GHESとの接続部分です。
設計当初はCode Connections(旧CodeStar Connections)をCodePipelineのソースに指定することでStepFuntions/CodeBuildにGHESリポジトリのデータを渡せると思っていました。
ただ、Step FunctionsへアーティファクトのS3パスをパラメタの一部として渡す方法はなさそうだったためこの方法が利用できないことが分かりました。
こちらの対応策としては、GHES側でトークンを用意しCodeBuildでこのトークンを登録することでアクセス許可させる方式としました。

導入/切り替え

今回の移行対象ジョブは複数あったのですが順次導入していくと、デプロイ処理を行う開発者の方がどのジョブがJenkinsにまだ残置していてどのジョブがAWSに移行されたかわからなくなる恐れがありました。
なので、導入自体は裏で粛々と進めておき移行対象すべてのテストが完了した段階で切り替え宣言/運用ドキュメント差し替えを行うという方針を取りました。
この方針自体は問題なかったと思っていますが、並行稼働期間が生じることで顕在化した問題もありました。

  • 既存ジョブ側で権限不足でデプロイ処理に失敗する
    • こちらは単純なミスではありましたが、移行後のデプロイ処理でファイル配置時に指定した所有者に誤りがあったため、既存ジョブが実行された際に権限不足でファイルの置き換えができない問題でした

まとめ

JenkinsにはわかりやすいWEB管理画面があり、私を含め愛着を持っている開発者も多かったのですが、今回は基盤運用の手間を削減するために思い切ってAWSのマネージドサービスを活用したリファクタリングを行う選択をしました。

移行後に若干のバグ修正などが発覚したものの現状は安定して稼働してくれています。今回移行を行ったことでSeasar2からRailsへの移行が完全に完了するその日まではHW障害に怯えることなく、CI/CDを安定して提供できるようになったのではないかと思っています。

かなりニッチな内容になってしまった気もしますが、本取り組みの内容がどなたかの役に立てれば幸いです。

シンクロ・フードのエンジニアが書籍購入補助制度を利用して2023年度に購入した書籍ランキングTOP5を紹介します

こんにちは、シンクロ・フードの越森です。

昨年、書籍購入補助制度を利用して2022年度に購入した書籍を紹介する記事を書いていますが、今年も2023年度(2023年4月〜2024年3月)に購入した書籍のランキングを紹介してみようと思います。

tech.synchro-food.co.jp

2023年度の購入書籍

2023年度の1年間で購入された書籍数は195冊で、エンジニアは35名在籍していることから1人当たり5.6冊購入している計算になります。

2023年度の購入書籍ランキングTOP5

次に2023年度に購入された書籍のランキングTOP5は以下になります。

順位 書籍名 購入冊数
1位 システム設計の面接試験 5
2位 実践Terraform AWSにおけるシステム設計とベスト プラクティス 4
3位 Good Code, Bad Code ~持続可能な開発のためのソフトウェアエンジニア的思考 3
3位 SRE サイトリライアビリティエンジニアリング 3
3位 パーフェクト Ruby on Rails 【増補改訂版】 3
3位 プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで 3
3位 世界一流エンジニアの思考法 3

集計してみて驚いたことは重複して購入している書籍が少ないことです。
昨年は116種類、218冊で多くの種類の書籍を購入していると思っていたのですが、今年は141種類、195冊と昨年以上に重複して購入する書籍が少なかったです。
色々な書籍を購入していて面白いと思う反面、読んでみて良いと思った書籍を社内で共有する仕組みを用意していないことから重複することもないのかなとも思いましたので、次は紹介するような取り組みも進めてみたいと思います。
(今は書籍を購入してシステムに登録するタイミングでSlackに通知していますが、誰がどの本を購入したということしか分からないようになっています).

1位の「システム設計の面接試験」は書籍のタイトルにあるように面接試験を想定していることもあり、システム要件の確認から概要設計、要件に合わせた設計の深掘りと進むような形でシステム設計の検討の流れを追体験できるような形になっています。
取り扱っているテーマも幅広く、いつも利用しているキーバリューストアの設計から通知システムの設計、果てはYouTubeの設計と自身の業務でも関連する内容からあまり意識しない内容まで様々なシステム設計について学ぶことができます。
あまり理解していないシステムについては考えることも多く読み進めるのに時間がかかったりしますが、確実に自身のシステム設計の引き出しが増える感覚があり充実感があります。 章の最後に必ず自分を褒めてあげましょうとあるのもとても良かったです。

2位の「実践Terraform AWSにおけるシステム設計とベスト プラクティス」は弊社ではインフラは Terraform でコード化して管理しているため、SREチームのメンバーが Terraform の基本を学ぶために購入することが多いのと、最近はWebエンジニアも Terraform を触る機会があったりするため多く購入されていると思います。
2019年に出版された書籍ということもあり、バージョンの違いで書籍通りでは動かないこともあるため調べながら対応する必要はありますが、Terraform について体系的に学ぶことができます。

最後に

あらためてまとめてみるとたくさんの種類の書籍が購入されていることに驚きますが、今年に入ってからも多くの書籍が出版されていてそれも納得です。
色々読みたいと思う書籍があるのですが、全然読むスピードが追いつかず積ん読が多くなっています。
今のところ以下の書籍を次以降に読んでいく予定ですが、皆さんはどのような書籍を読もうと思われているでしょうか?

求人@インテリアデザインのRailsリプレースが完了したのでリプレースの効率とバグの傾向を分析してみた

はじめまして、開発部の大沼です。
求人インテリア内装建築グルメバイトちゃんなどのサービスの開発に携わっています。

2019年から始まった求人@インテリアデザイン(以下求人インテリア)のRailsへの移行プロジェクト(以下Railsリプレース)が完了したので、それについてお話ししたいと思います。
Railsリプレースの報告と共に他サービスでリプレースを行っていくときの参考になればと思い効率とバグの傾向を分析することにしました。

求人インテリアとは

求人インテリアは2005年にサービスを開始しているインテリアデザイン業界に絞った専門求人サービスです。
以前はJavaとSeasar2を使用して開発していたのですが弊社で現在メインで使っているRubyとRailsへのリプレースを行うことになりました。

リプレースの方針と状況

リプレース初期のバージョンはRubyは2.6でRailsが6.1になります。
サービスの規模はCRUDごとの機能数が107 で画面数が169となっています。

以下の方針でリプレースを進めました。

  • CSSとJavaScriptは変更しない
  • HTML構造はそのままでviewを移し替える
  • バックエンドはクラス設計から見直している

今回のRailsリプレースによって技術的には以下の恩恵が期待されます。

  • Seasar2のサポートが切れているためセキュリティリスクがあるがRailsは継続的なアップデートが期待できる
  • 黎明期の技術的に未熟なコードが残っているが、それらを保守性や可読性の高いコードに書き直せる
  • ビルド環境やアプリケーションが稼働するサーバなどのインフラもレガシーであり置き換えることでデプロイフローの簡素化やセキュリティアップデートを受けやすくなる
  • JavaのリポジトリはCIが整備されていないのでRailsリプレースを行い自動テストが常に実行される環境にすることで保守性の向上が期待できる

リプレースは完了して全ての機能がRailsで動くようになり、今後の開発で全ての恩恵を受けれる状態になりました。
具体的な数値があげられるものでは自動テストのカバレッジは約95%と高い水準になっています。

次項からは今回のRailsリプレースの開発について振り返ろうと思います。

開発効率について分析

Railsリプレースでは担当チームが一度変更されました。まずは開発期間などの基礎情報をまとめました。

前チーム 後チーム
開発期間 2019年 11月 ~ 2020年 3月 2020年 4月 ~ 2024年 4月
開発人数 3人 15人
開発工数 832h 828h
開発状況 Railsリプレースが中心 機能開発の合間で開発

リプレース前の状況としては求人インテリアの開発チームの人数が少なく、機能開発が常に動いていたためリプレースがなかなかできずにいました。そこで機能開発をよく行う主要機能のみを先に集中してリプレースをする選択をしました。
前チームは短期間で少数のメンバーが開発して後チームは多数のメンバーが少しずつ開発したことから前チームの方が効率よく開発できたのではないかと考えました。

開発効率の算出の仕方と前チームと後チームの開発効率について分析していきます。

開発効率の分析方法

開発効率を出すためには1タスクごとの粒度を定義する必要がありました。タスクはRailsリプレースを細かく分割したものでデプロイすることで完了します。Railsリプレース全体で77タスクに分けられています。
粒度としてファイルチェンジ数が使えるのではないかと考えてタスクごとの ファイルチェンジ数 / 開発時間 を効率として相関があるのかを調べました。
相関があることが確認できればチーム別にファイルチェンジ数の合計 / 開発時間の合計で全体の効率が算出できます。
補足として主に新卒入社された方が開発を行っていて年次はほぼ一緒でスキルの差はあまりなかったと思います。

抽出の条件は以下になります。

  • ファイルチェンジ数は JavaScript・CSS・画像ファイルはそのまま移行するため無視してrbとerbファイルのみを対象にする
    • rbとerbファイルの割合はどちらのチームでも7割ほどで差はなかった
  • 影響の大きい開発時間が10h以上のタスクを対象にする
    • 前半は26タスク・後半は23タスクが該当して全体の9割ほどの開発時間を占めている
  • 外れ値を除外するために要件ごとのファイルチェンジ数 / 開発時間を対数にとり箱ひげ図を用いて四分位範囲(第1四分位数から第3四分位数までの範囲)の1.5倍を上下限として判定する
    • 5タスクが該当して計算から除外した

結果

結果は以下のようになりました。相関係数が0.7を超えていることからある程度はファイルチェンジ数をタスクごとの粒度として使用できると判断しました。

前チームの開発効率の散布図

後チームの開発効率の散布図

外れ値を除いた10h以上のタスクの効率 前チーム 後チーム
ファイルチェンジ数(rbとerbのみ) 1046 339
開発時間(h) 757.93 483.58
効率 1.38 0.70
相関係数 0.86 0.72

前チームの効率のほうが2倍程度高くなっていることがわかります。
全てのタスクとファイルチェンジを対象にした場合でも効率は前チーム2.1で後チームが1.0であり比はほとんど同じになります。バーンアップチャートにするとこのようになります。

総工数と総ファイルチェンジ数の推移

結論

以上の結果から多人数で分散して対応すると効率が悪くなることがデータでも確認できました。
長期間で少しずつ開発すると開発メンバーが入れ替わりになるため仕様理解にかかる時間が増えるのが主な原因だと考えました。
リソースが限られている状態では難しいかもしれませんがRailsリプレースを担当するメンバーをある程度固定して短期間で完全に置き換えるまで進めた方が良いと思いました。

Railsリプレースによって発生したバグの傾向の分析

他のアプリケーションでRailsリプレースを行う際の参考にするために発生したバグの傾向を見ていこうと思います。
バグの対応件数は少なくとも22件は確認できました。発生頻度としては 工数 80 hに対して1件ほどの割合になります。
1件ずつ分類したところテストケースが網羅されておらずに起こるバグが全体の半数ほどでした。それらの根本的な原因としては以下のいずれかに該当していました。

  • 仕様の理解不足
  • 片方の言語の知識不足
  • ディレクトリ構成が不適切なことによって影響範囲から漏らしてしまう
    • 例: 非同期処理の移行でJavaScriptのファイルが想定外のページから呼ばれていることでそれに気づかずに修正をしてしまいエラーが出る
  • ステートフルな実装
    • 例: 詳細ページから一覧ページに戻る際に検索条件を残すためにcookieを使っているときにcookieが存在しない場合かつ特定のパラメータがある場合にエラーが発生する
  • 入力なしの時の初期値が異なることによって発生するケース
    • 例: 空白を初期値としてDBに保存していたところにNULLを入れてしまうことでそのレコードを使う際にエラーが出る

移行元のコードの可読性が低かったり配置が不適切だとバグの発生を移行段階のみで防ぎ切るのは難しいです。またRailsリプレースは経験の浅いメンバーが開発するケースが多くどちらかの言語の知識が不足していてバグが発生しやすくなるのではないかと思います。

まとめ

求人インテリアのRailsリプレースについての分析を行いました。他のアプリケーションで進めているRailsリプレースや将来行うであろうRailsからの移行時に生かしていきたいです。Railsリプレースは完了したばかりなので今後の求人インテリアの開発にも注目していけたらと思います。