SwiftUIで画面内の各コンテンツの表示ログを送る

こんにちは、レシピサービス開発部の@miichan_ochaです。普段はiOS版クックパッドアプリの開発をしています。

クックパッドアプリでは開発した機能の評価を行うために、画面のPVログや画面内の各コンテンツの表示・タップログなどの様々な行動ログを送っています。

今回は、SwiftUIで新たに作った画面内の各コンテンツの表示ログを送る仕組み(ShowContentLog)についてご紹介します。この仕組みは昨年7月にリリースされたiOS版クックパッドアプリ「のせる」タブ開発時に作られたもので、現在約半年ほど運用しています。

ShowContentLogの仕組み

ログの要件

レシピサービス開発部では、iOS版クックパッドアプリの画面内の各コンテンツの表示ログを以下の要件で取っています。

  1. コンテンツが初めて画面に表示される時に、そのコンテンツの表示ログを送る
  2. 画面のスクロールによって、コンテンツが一度画面外に出てから再度画面内に表示された時には、そのコンテンツの表示ログは送らない
  3. 一定時間経過後の画面自動更新やPull to Refreshによる画面更新を行った時は、更新後の画面に表示されているコンテンツの表示ログを送る
  4. 別画面にプッシュ遷移した後、遷移先から戻ってきて画面が再度表示された時に、その画面に表示されているコンテンツの表示ログを送る
  5. タブの切り替えによって画面が再度表示された時に、その画面に表示されているコンテンツの表示ログを送る
  6. アプリがバックグラウンドからフォアグラウンドに復帰した時に、復帰時の画面に表示されているコンテンツの表示ログを送る

各要件に対応するデモ動画

1 2 3
要件1のデモ動画 要件2のデモ動画 要件3のデモ動画
4 5 6
要件4のデモ動画 要件5のデモ動画 要件6のデモ動画

なお、ここでいう各コンテンツとは「表示・タップ回数を計測したいViewのまとまり」のことを指しており、ある画面では画面内のセクション単位であったり、別の画面ではバナー・カルーセルなどViewのコンポーネント単位であったりと、その粒度は画面によって様々です。

UIKitで作られた画面では、ViewController内で表示された各コンテンツのIDを管理するSetをpropertyとして保持し、UICollectionViewで作られた画面であればUICollectionViewDelegatecollectionView(_:willDisplay:forItemAt:)をトリガーにログを送信することでこの要件を実現していました。

仕組みが必要になった背景

きっかけは前述の通り、レシピサービス開発部で「のせる」タブの画面をSwiftUIで作ったことです。

「のせる」タブでは上記の仕様で各コンテンツの表示ログを送る要件があったため、SwiftUIの画面でも各コンテンツの表示ログを送る必要が出てきました。加えて、UIKitでは画面ごとに都度表示ログを送る実装をしていましたが、SwiftUIでは仕組み化して簡単に送れるようにしたいという動機から、今回の仕組みが生まれました。

動作する環境について

今回の仕組みはiOS版クックパッドアプリ上で使用されており、以下の条件・環境で動作しています。

  • iOS版クックパッドアプリで採用しているVIPERアーキテクチャに適合したまま、View層のみでSwiftUIを使っている

  • 表示ログを送る各コンテンツは、LazyVStackListなど、遅延ロードを行うViewの中に配置されている必要がある

    • 各コンテンツが表示されたかどうかは onAppear/onDisappear で判定しているため
  • Markdownで書かれたログ定義から自動生成された行動ログ(以下「自動生成行動ログ」と呼びます)を送る前提で設計されている(追加で別のログを送ることも可能)

  • iOS Deployment Targetが 14.0 の時代に開発された

    • iOS13での動作は未検証となっています

ShowContentLogの使い方

まずは、仕組みの使い方について簡単に説明します。

*説明のためコードを簡略化しています。完成版のコードに関しては「完成版のコード」の章をご覧ください。

最初に、UIHostingControllerを保持しているViewController内でShowContentLogControllerというクラスのインスタンスを作成します。

// ViewController
private lazy var showContentLogController = ShowContentLogController(screenViewController: self)

次に、表示ログを送る各コンテンツを内包しているView(以下「大元のView」と呼びます)に ShowContentLogRootModifierというViewModiferを付与します。これにより、このViewModiferを付与したViewが表示されている時にだけ、各コンテンツのログが送られるようになります。

シンプルな画面ではUIHostingControllerの引数に渡すrootViewのViewにShowContentLogRootModifierを付与すれば良いですが、タブがある画面においては、各タブの中身のViewごとに付与します(各タブで表示状態が異なるため)。

ShowContentLogRootModifierの引数controllerには、先程作成したShowContentLogControllerのインスタンスを渡します。各タブの中身のViewごとにShowContentLogRootModifierを付与した場合は、タブの数だけShowContentLogControllerのインスタンスを作成し、タブごとに別々のインスタンスを渡してください。

*このViewModiferに対応するshowContentLogRootというメソッドをSwiftUI.Viewにextensionとして定義しています。

// ViewController
override func viewDidLoad() {
    super.viewDidLoad()

    let rootView = HogeView(delegate: self, dataSource: dataSource)
        .showContentLogRoot(controller: showContentLogController)

    let hostingVC = UIHostingController(rootView: rootView)
    ...
}

最後に、表示ログを送りたい各コンテンツのViewそれぞれにPostShowContentLogModifierというViewModiferを付与します。引数eventにはLogCategory protocolに準拠したログイベントを渡します(「自動生成行動ログ」の全てのログイベントはLogCategoryに準拠しています)。

RecipeView()
    .postShowContentLog(SampleCategory.showRecipe(recipeId: recipe.id))

以上で、ログの要件通りに各コンテンツの表示ログを送ることができるようになります。

ShowContentLogの設計

次に、このShowContentLogの設計・内部実装について詳しく見ていきます。

*ここでもコードは適宜簡略化しています。簡略化されていないものは「完成版のコード」の章をご覧ください。

ShowContentLogController

ShowContentLogControllerは、表示ログを送る「大元のView」の表示状態を、子Viewである各コンテンツのViewに通知する役割を担うクラスです。isRootViewAppearingというPublisherを持ち、各コンテンツのViewはこれを監視することで「大元のView」の表示状態を知ることができます*1

@MainActor
final class ShowContentLogController {
    private let isRootViewAppearingSubject = CurrentValueSubject<Bool, Never>(false)

    lazy var isRootViewAppearing: AnyPublisher<Bool, Never> = isRootViewAppearingSubject
        .removeDuplicates()
        .eraseToAnyPublisher()

    func setIsRootViewAppearing(_ appearing: Bool) {
        isRootViewAppearingSubject.send(appearing)
    }
}

ShowContentLogRootModifier

ShowContentLogRootModifierは、表示ログを送る「大元のView」に付与するViewModifierです。

このViewModifierは、

  • ShowContentLogControllerのインスタンスをEnvironmentValuesに設定する
  • ViewModiferが付けられた「大元のView」の表示状態をShowContentLogControllerに伝える

という2つの役割を担っています。

struct ShowContentLogRootModifier: ViewModifier {
    let controller: ShowContentLogController
    @State private var isAppearing: Bool = false

    func body(content: Content) -> some View {
      content
        .onAppear {
            isAppearing = true
        }
        .onDisappear {
            isAppearing = false
        }
        .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
            if didEnterBackground {
                isAppearing = true
                didEnterBackground = false
            }
        }
        .onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in
            if isAppearing {
                isAppearing = false
                didEnterBackground = true
            }
        }
        .onChange(of: isAppearing) { appearing in
            controller.setIsRootViewAppearing(appearing) // 表示状態を ShowContentLogController のインスタンスに伝える
        }
        .environment(\.showContentLogController, controller) // EnvironmentValues に設定する
    }

PostShowContentLogModifier

PostShowContentLogModifierは、表示ログを送りたい各コンテンツのViewに付与するViewModifierです。 このViewModifierは、ShowContentLogControllerのisRootViewAppearing Publisherを監視しつつ、適切なタイミングで表示ログを送る役割を担っています。

また、ViewModifierが付与されたViewが現在表示されているか(isAppearing)、既に表示ログを送ったか(didPostLog)の状態をStateとして保持していて、監視しているisRootViewAppearingがfalseになった時(「大元のView」が非表示になった時)に、既に表示ログを送ったか(didPostLog)の状態をリセットしています。

ShowContentLogControllerRequired(とAppEnvironmentRequired)については「実用段階にするまでに用意した仕組み」の章で詳しく説明します。

struct PostShowContentLogModifier<Category: LogCategory>: ViewModifier {
    let event: Category
    @State private var isRootViewAppearing: Bool = false
    @State private var isAppearing: Bool = false
    @State private var didPostLog: Bool = false

    func body(content: Content) -> some View {
        AppEnvironmentRequired { appEnvironment in
            ShowContentLogControllerRequired { showContentLogController in
                content
                    .onAppear {
                        isAppearing = true
                        if isRootViewAppearing && !didPostLog {
                            postLog(appEnvironment)
                        }
                    }
                    .onDisappear {
                        isAppearing = false
                    }
                    .onReceive(showContentLogController.isRootViewAppearing) { rootAppearing in
                        if rootAppearing {
                            isRootViewAppearing = true
                            if isAppearing && !didPostLog {
                                postLog(appEnvironment)
                            }
                        } else {
                            isRootViewAppearing = false
                            didPostLog = false // didPostLog の状態をリセット
                        }
                    }
            }
        }
    }

    private func postLog(_ appEnvironment: any AppEnvironment) {
        appEnvironment.activityLogger.post(event)
        didPostLog = true
    }
}

設計時に検討したこと

ログを送ったかどうかの管理をどこで行うか

UIKit時代は、表示したコンテンツのIDをViewControllerのSetで管理し、親Viewが中央集権的に各コンテンツのログ送信フラグの管理を行っていました。しかし、SwiftUIでは全てのViewがIdentityを持っており、ログを送るView自身がStateでログを送ったかどうかを管理する方がSwiftUI的に自然だと考えました(加えて子Viewから親Viewに自身の Identifierを伝えて管理させるやりとりも減らすことができて、実装もシンプルになります)。そのためShowContentLogでは、PostShowContentLogModifierを付与したView自身がログ送信フラグの管理を行う設計となっています。

実用段階にするまでに用意した仕組み

ShowContentLogController

isPresented

対応するログの要件

  1. アプリがバックグラウンドからフォアグラウンドに復帰した時に、復帰時の画面に表示されているコンテンツの表示ログを送る

開発中に気付いたのですが、ShowContentLogを使っている画面上でモーダルを表示し、そのモーダルが表示されている状態でバックグラウンド→フォアグラウンド復帰した時に表示ログが送られていました。原因は、モーダルが表示されている状態でバックグラウンド→フォアグラウンド復帰した時にShowContentLogRootModifierのwillEnterForegroundNotificationdidEnterBackgroundNotificationが発火していたことでした。

iOS版クックパッドアプリでは、モーダル表示を含めた画面遷移はUIKitで行われているため、ShowContentLogControllerの初期化時にSwiftUIのViewを表示しているViewControllerを渡して、モーダルを表示しているかどうかを取得するクロージャisPresented: () -> Boolを保持することにしました。

final class ShowContentLogController {
    ...
    let isPresented: () -> Bool

    init(screenViewController: UIViewController) {
        isPresented = { [weak screenViewController] in
            screenViewController?.presentedViewController != nil
        }
    }
}

これをShowContentLogRootModifier内でdidEnterBackgroundNotificationの通知を受け取った時に参照することで、モーダルが表示されている状態でバックグラウンド→フォアグラウンド復帰した時には表示ログを送らないようにしています。

// ShowContentLogRootModifier
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
    if didEnterBackground {
        isAppearing = true
        didEnterBackground = false
    }
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in
    // モーダルが表示されていない時だけ処理を行う
    if isAppearing && !controller.isPresented() {
        isAppearing = false
        didEnterBackground = true
    }
}

willRefreshOnForeground

対応するログの要件

  1. 一定時間経過後の画面自動更新やPull to Refreshによる画面更新を行った時は、更新後の画面に表示されているコンテンツの表示ログを送る
  2. アプリがバックグラウンドからフォアグラウンドに復帰した時に、復帰時の画面に表示されているコンテンツの表示ログを送る

iOS版クックパッドアプリの一部の画面では、一定時間経過後に再びタブ切り替えで戻ってきたりバックグラウンド→フォアグラウンド復帰したりすると、画面の自動更新が行われます(自動更新の判定はViewControllerで行われています)。この時UIApplication.willEnterForegroundNotificationが送られるタイミングが自動更新が走るタイミングより早いので、バックグラウンド→フォアグラウンド復帰時に自動更新が走る場合は、更新前後で古いコンテンツの表示ログと更新後のコンテンツの表示ログが2回送られてしまっていました。

この場合は更新後の表示ログのみを送りたいので、ShowContentLogControllerに画面の更新が予定されているかどうかを取得するwillRefreshOnForeground: () -> Boolというクロージャを保持し、バックグラウンド→フォアグラウンド復帰時に画面の更新が予定されている場合は更新が終わるまでisAppearingの変更を待つようにしました。

final class ShowContentLogController {
    ...
    let willRefreshOnForeground: () -> Bool

    init(screenViewController: UIViewController, willRefreshOnForeground: @escaping () -> Bool = { false }) {
        isPresented = ...
        self.willRefreshOnForeground = willRefreshOnForeground
    }
}
// ShowContentLogRootModifier
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
    if didEnterBackground {
        // 画面をリフレッシュする場合はリフレッシュを待ってから isAppearing を true にする
        if !controller.willRefreshOnForeground() {
            isAppearing = true
        }
            didEnterBackground = false
        }
    }
}

ShowContentLogRootModifier

forceDisappear

対応するログの要件

  1. タブの切り替えによって画面が再度表示された時に、その画面に表示されているコンテンツの表示ログを送る

forceDisappearをtrueにするとisAppearingの値を常にfalseにしてShowContentLogControllerに伝えることができます。

TabContentView(...)
    .showContentLogRoot(
        controller: tabState.showContentLogController,
        forceDisappear: selection != tabState.tabType // 違うタブが選択されている時は `isAppearing` の値を常に false にする
    )
// ShowContentLogRootModifier
.onChange(of: forceDisappear) { newForceDisappear in
    controller.setIsRootViewAppearing(!newForceDisappear && isAppearing)
}
.onChange(of: isAppearing) { appearing in
    controller.setIsRootViewAppearing(!forceDisappear && appearing)
}

元々はiOS14のTabViewで、選択されていないタブのonAppearが呼ばれることがあり、それを回避するために生まれました。それ以外にも、OSバージョン問わずタブの選択が完全に切り替わっていない時(スワイプで隣のタブが少しだけ見えている状態)にも隣のタブのonAppearが呼ばれていたので、iOS14のサポートを終了してからもこの指定は続けています。

ちなみにsetIsRootViewAppearing自体をスキップしてしまうと、各種イベントの発火タイミングによっては本来送られるべきログが送られなくなってしまう可能性があるため、このように値を上書きしてsetIsRootViewAppearingを呼ぶ方法を取っています。

isRefreshing

対応するログの要件

  1. 一定時間経過後の画面自動更新やPull to Refreshによる画面更新を行った時は、更新後の画面に表示されているコンテンツの表示ログを送る

更新時にdidPostLog(ログを送ったかどうか)をリセットするために用意されているpropertyです。

現在レシピサービス開発部でSwiftUIで新規画面を開発する時は、下記のScreenStateのようなものを使って画面の状態管理をしているので、このpropertyを使う必要はありません(画面更新時はScreenStateloadingloadedとなりloadedに対応するViewが再生成されるため)。

enum ScreenState<T, E: Error> {
    case initial
    case loading(T?)
    case loaded(T)
    case error(E)
}

例えば画面の表示切り替えをZStack内のViewのopacity変更で行っていて、画面更新時にViewが再生成されない場合にこのpropertyを使ってdidPostLogをリセットすることができます。

// ShowContentLogRootModifier
var isRefreshing: Bool
...
    .onChange(of: isRefreshing) { refreshing in
        isAppearing = !refreshing
    }

PostShowContentLogModifier

ShowContentLogControllerRequired

ShowContentLogControllerRequiredは、PostShowContentLogModifierを付けたViewよりも上の階層でShowContentLogControllerのインスタンスがEnvironmentValuesに設定されていない(つまりShowContentLogRootModifierを付け忘れている)時にassertionFailureを起こすためのViewです。

import SwiftUI

struct ShowContentLogControllerRequired<Content: View>: View {
    @Environment(\.showContentLogController) private var showContentLogController: ShowContentLogController?
    private let content: (ShowContentLogController) -> Content

    init(@ViewBuilder content: @escaping (_: ShowContentLogController) -> Content) {
        self.content = content
    }

    private func noEnvironment() -> some View {
        assertionFailure("You must pass the showContentLogController from a parent or ancestor view. If you use postShowContentLog modifier, add showContentLogRoot modifier to a parent or ancestor view.")
        return EmptyView()
    }

    var body: some View {
        if let showContentLogController = showContentLogController {
            content(showContentLogController)
        } else {
            noEnvironment()
        }
    }
}

AppEnvironmentRequiredも同じような実装となっています(むしろShowContentLogControllerRequiredが先に実装されたAppEnvironmentRequiredの実装を参考にしています)。

AppEnvironmentRequiredがクロージャの引数に渡しているappEnvironmentは、iOS版クックパッド上で用意されている依存関係を取り出すためのDIコンテナで、PostShowContentLogModifier内ではappEnvironmentを用いて行動ログを送るための依存にアクセスしています。

appEnvironmentについては下記に詳しい説明があります(記事内ではEnvironmentと呼ばれています)。 https://techlife.cookpad.com/entry/2021/06/16/110000

onPostLog

onPostLog は「自動生成行動ログ」が送られるタイミングで呼び出されるクロージャで、「自動生成行動ログ」とは別のログを追加で送るためのものです。

// PostShowContentLogModifier
private let onPostLog: ((any AppEnvironment) -> Void)?
...
private func postLog(_ appEnvironment: any AppEnvironment) {
    appEnvironment.activityLogger.post(event)
    onPostLog?(appEnvironment)
    didPostLog = true
}

完成版のコード

以下が、「実用段階にするまでに用意した仕組み」を踏まえた完成版のコードです。

*動作確認時の環境: Xcode 14.1、iOS Deployment Target 15.0

ShowContentLogController

import Combine
import SwiftUI
import UIKit

@MainActor
final class ShowContentLogController {
    private let isRootViewAppearingSubject = CurrentValueSubject<Bool, Never>(false)
    lazy var isRootViewAppearing: AnyPublisher<Bool, Never> = isRootViewAppearingSubject
        .removeDuplicates()
        .eraseToAnyPublisher()

    let isPresented: () -> Bool
    let willRefreshOnForeground: () -> Bool

    init(screenViewController: UIViewController, willRefreshOnForeground: @escaping () -> Bool = { false }) {
        isPresented = { [weak screenViewController] in
            screenViewController?.presentedViewController != nil
        }
        self.willRefreshOnForeground = willRefreshOnForeground
    }

    func setIsRootViewAppearing(_ appearing: Bool) {
        isRootViewAppearingSubject.send(appearing)
    }
}

private struct ShowContentLogControllerKey: EnvironmentKey {
    static let defaultValue: ShowContentLogController? = nil
}

extension EnvironmentValues {
    var showContentLogController: ShowContentLogController? {
        get { self[ShowContentLogControllerKey.self] }
        set { self[ShowContentLogControllerKey.self] = newValue }
    }
}

ShowContentLogRootModifier

import SwiftUI

private struct ShowContentLogRootModifier: ViewModifier {
    private let controller: ShowContentLogController
    private var isRefreshing: Bool
    private var forceDisappear: Bool
    @State private var isAppearing: Bool = false
    @State private var didEnterBackground: Bool = false

    init(controller: ShowContentLogController, isRefreshing: Bool, forceDisappear: Bool) {
        self.controller = controller
        self.isRefreshing = isRefreshing
        self.forceDisappear = forceDisappear
    }

    func body(content: Content) -> some View {
        content
            .onAppear {
                isAppearing = true
            }
            .onDisappear {
                isAppearing = false
            }
            .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
                if didEnterBackground {
                    // 画面をリフレッシュする場合はリフレッシュを待ってから isAppearing を true にする
                    if !controller.willRefreshOnForeground() {
                        isAppearing = true
                    }
                    didEnterBackground = false
                }
            }
            .onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in
                if isAppearing && !controller.isPresented() {
                    isAppearing = false
                    didEnterBackground = true
                }
            }
            .onChange(of: isRefreshing) { refreshing in
                isAppearing = !refreshing
            }
            .onChange(of: forceDisappear) { newForceDisappear in
                controller.setIsRootViewAppearing(!newForceDisappear && isAppearing)
            }
            .onChange(of: isAppearing) { appearing in
                controller.setIsRootViewAppearing(!forceDisappear && appearing)
            }
            .environment(\.showContentLogController, controller)
    }
}

extension View {
    func showContentLogRoot(controller: ShowContentLogController, isRefreshing: Bool = false, forceDisappear: Bool = false) -> some View {
        modifier(ShowContentLogRootModifier(controller: controller, isRefreshing: isRefreshing, forceDisappear: forceDisappear))
    }
}

PostShowContentLogModifier

import SwiftUI

private struct PostShowContentLogModifier<Category: LogCategory>: ViewModifier {
    private let event: Category
    private let onPostLog: ((any AppEnvironment) -> Void)?
    @State private var isRootViewAppearing: Bool = false
    @State private var isAppearing: Bool = false
    @State private var didPostLog: Bool = false

    init(event: Category, onPostLog: ((any AppEnvironment) -> Void)?) {
        self.event = event
        self.onPostLog = onPostLog
    }

    func body(content: Content) -> some View {
        AppEnvironmentRequired { appEnvironment in
            ShowContentLogControllerRequired { showContentLogController in
                content
                    .onAppear {
                        isAppearing = true
                        if isRootViewAppearing && !didPostLog {
                            postLog(appEnvironment)
                        }
                    }
                    .onDisappear {
                        isAppearing = false
                    }
                    .onReceive(showContentLogController.isRootViewAppearing) { rootAppearing in
                        if rootAppearing {
                            isRootViewAppearing = true
                            if isAppearing && !didPostLog {
                                postLog(appEnvironment)
                            }
                        } else {
                            isRootViewAppearing = false
                            didPostLog = false
                        }
                    }
            }
        }
    }

    private func postLog(_ appEnvironment: any AppEnvironment) {
        appEnvironment.activityLogger.post(event)
        onPostLog?(appEnvironment)
        didPostLog = true
    }
}

extension View {
    func postShowContentLog<Category: LogCategory>(_ event: Category, onPostLog: ((any AppEnvironment) -> Void)? = nil) -> some View {
        modifier(PostShowContentLogModifier(event: event, onPostLog: onPostLog))
    }
}

まとめ

今回ご紹介したShowContentLogによって、SwiftUIの画面でもUIKit同様に各コンテンツの表示ログを送ることができるようになりました。また、UIKit時代は画面を作る度に一から表示ログの実装が必要だったのですが、仕組みを作ったことでSwiftUIでは表示ログを簡単に送ることができるようにもなりました。

仕組みを作る中で様々な意見・指摘をくれたチームの同僚に感謝します。この記事が、SwiftUIを使った画面で行動ログを送る際の参考に少しでもなれば幸いです。

*1:isRootViewAppearingを最初computed propertyにしていたのですが、それだとSwiftUIのViewのbodyが再実行される度にonReceiveで毎回新たなPublisherのインスタンスを購読するという挙動になりremoveDuplicatesが効かなくなってしまうので、lazy varで宣言しています。

【後編】企業所属のRubyコミッター対談! 〜Ruby開発の裏話と今後の取り組み〜

こんにちはCTO室の緑川です。今回はアンドバッドさんが主催しているPodcast「ANDPAD TECH TALK」のゲストに弊社の@mameが出演した記事の後半です。Podcastとしてお聞きしたい方は下記のアンドパッドさんの記事からお聴きください。

tech.andpad.co.jp

前編の記事はこちらです。

【前編】企業所属のRubyコミッター対談! 〜企業に所属するOSS開発者って何?〜 - クックパッド開発者ブログ

トーク本編

櫻井:皆さま、こんにちは。アンドパッドの開発本部でエンジニアリングマネージャーをしている櫻井です。

櫻井:13回目のANDPAD TECH TALKです。ANDPAD TECH TALKはアンドパッドの開発チームの中の人をゲストに招いて、あれやこれやお話しするカジュアルなテック系Podcastなのですが、今回は前回に引き続き社外ゲストをお招きしたスペシャル会の後編となっております。企業に所属するRubyコミッターであるお二人をお招きしています。

櫻井:アンドパッドからはフェローでありRubyコミッターの柴田さん。対談相手はクックパッド株式会社所属のRubyコミッターであるmameさんこと遠藤侑介さんをお呼びしています。前回はお二人の今までの経緯、Rubyコミッターが普段どんなことをしているのかをお話いただき、非常に良いところで後編となっていたところです。今回はRuby開発の裏話と今後の取り組みなど深振りをさせていただきたいと思っております。それでは後編をお聞きください。

Ruby開発の裏話

櫻井:まずは遠藤さんからお伺いしますが、開発裏話みたいなものはありますでしょうか?

遠藤:クックパッドでRubyをめちゃくちゃ現場で使っている職場に転職したから、現場感がつかめてなかったので、よくわからなかったところがうまくいくようになったのはちょっとあったりしますね。

遠藤:最初に述べたカバレッジの測定をする機能を担当しているんですけれども、カバレッジの測定を止めたり再開したりする機能が欲しいという要望が以前Rubyに来たことがあったんですけれどもその時は必要性がよくわからなかったんですね。実装するのも大変なので断っていたんですけども、クックパッドで働くようになって、現場で働いている人 から同じ問題で困っているという話を聞くことができて、どういうふうに困っているのか理解がちゃんとできたので今回対応することにし、oneshot coverageという機能を導入するようにした話があったりします。

柴田:その話で言うと、RubyコミッターRubyのコード書いていない問題みたいなものをよく言われたりしています。Cプログラマーの皆さんだから会社の中でRubyをどう使って何を書いているかとか、またRailsをメインに開発されている人とかRailsを使ってアプリケーションを開発している人とかが「Rubyでこういうことができるといいんだけどな」みたいな部分とあとRubyを開発しているRubyのコミッターの人たちが「こういうRubyのコードは書かないでしょ」って言った矢先に「書きますよ」みたいな話とか、逆にこう書けたら便利じゃないって「誰も書きませんよ、そういうこと」みたいな話は結構イベントとかSlackとかそういったところでよく散見されたりするのはあるあるネタですよね。

遠藤:そうですね。本当に現場感がない人がRubyを作っているっていうのはちょっと問題としてはあったりしますね。

柴田:ただ最近はShopifyのエンジニアであるとか、あとはよく喋る場みたいな部分が割と増えてきたような気はします。スタートアップ中心にRubyを採用している会社も結構な数があったりするので、「こういうことができるといいんだけどなー」みたいな部分とか拾ったりヒアリングしたりしやすくなったりしているのかなというのはありますよね。

遠藤:ありますね。

柴田:最近のライブラリというかプログラミング言語のGoとかRustみたいな言語はライブラリ自体がGoとRustで書いてあるというような言語なので、初学者の方とかが最初にGoとかRustをやりましょうみたいなときに割りかし開発を始めるまでのつまづくステップが比較的ないんですよね。それに比較するとRubyはC言語で書かれていて、C言語を動かすためにはコンパイラーとコンパイルした後の実行パイナリを実行する場所が必要になっていて、その辺の組み合わせで動かないとかビルドできないとか、何かプログラムを書こうと思ったんだけどプログラムを書くまでに1日とか何なら数日かかってしまう。Googleで検索しても何かエラーメッセージが出てこないみたいなことが増えていますと。

柴田:あとはAppleのmacOS、MacBookのアーキテクチャーがガラッと変わったことでいろいろビルドできない問題が引き続き多いとか、ARMのCPUの上では何かうまく動かないとかそういったいろいろな社会のコンピューティング環境の変化にともなって、自分が使いたいものがすぐ使えない、みたいな部分に散見されるなっていう問題があります。僕はいろいろやってはいるんですけど、その辺のプログラムを書こうと思ったときにすぐ書けるようになるみたいな部分の時間をとにかく小さくしたいと思っているので、その辺をいろいろやったり、前回遠藤さんの方から紹介があったkateinoigakukunさんがmacOSについてすごい詳しくて本当に助かったんですけれどもその辺のmacOSでビルドできない問題、動かない問題みたいな部分もいろいろ複数人で協力しながら解決していったりしているっていうのが現在進行系の話になってますね。

開発者に知ってもらいたいこと

櫻井:ありがとうございます。Ruby開発の裏側を聞いてきましたが、開発者に知ってもらいたいことなどがあれば、お二人からお伺いしたいんですけれどもいかがでしょうか?

柴田:はい、そうですね。昨今のプログラミング言語界隈の流れというか流行りみたいな部分の話をちょっとしたいんですけど、VS Codeと呼ばれるエディターが割とメインというかメジャーな存在となっていて、VS CodeはTypeScriptであるとか、Go言語であるとか、最近だったらRust言語みたいなもののサポートが非常に豊富なんですよね。

柴田:それもちろんMicrosoftが今すごい投資をしているんですけど、開発者体験という言葉があってデベロッパーエクスペリエンスというんですけど、開発者が何かをしようとした時に「うっ」てつまずかないように、なおかつ、こういうものを書きたいと思った時にスラスラっと書けます、テストも実行できます、不具合があった場所を見つけますみたいなものをできる限り提供していこうというのがどのプログラミング言語でも非常に重要視されています。

柴田:RubyもVS Codeのサポートであるとか、そういった型システムみたいな部分については少しずつ頑張っているんですけれども、やはり他の言語と相対的にはまだまだだなという部分があります。その中でも2022年にリリースするRubyのバージョン3.2でも今話したようなエラーを見つけましょうとか、そういう開発者にとってスラスラっとRubyのコードを書けるようにするための機能がいくつかあるので、その機能の開発を頑張っていた遠藤さんに詳細を聞くといいんじゃないかなと思います。

遠藤:僕がRuby3.2の中で新しくやったこととしてはRubyの例外が出たときに「このコードのせいでおそらく例外が出てるんじゃないの?」というのをエラーメッセージでサジェストをするという機能を少し拡充するのをやってみました。ErrorHighlightと呼ばれる機能なんですけども、それを拡充していました。また僕が作ったやつ以外にもSyntaxSuggestっていう機能がRuby3.2に増えまして、Rubyでコードを書いててありがちなのはendでステートメントを区切る言語なんで、endを書きすぎたり、逆にendが足りなかったりした時にどこにendが足りなかったのかというのはよくわからなくなりがちなんですよね。

遠藤:シンタックスサジェストとはコードの構造を大きく抜粋して「おそらくここにendが多すぎるんじゃないか」とか「足りないんじゃないか」というのをエラーメッセージの中にヒント情報として出してくれるという拡張が行われていて、これもエラーがでた時に開発者がどこを直せばいいのかというのをサジェスチョンしてくれるという機能がちょっとずつ増えています。

柴田:今の遠藤さんのお話はRubyのコードを実行したときに「この辺がエラーではないか?」というのを開発者の方にすぐお知らせするというような機能だったりするんですけど、他にもRubyの開発会議とか開発のこういうふうな変更を加えてはどうかみたいな時にも割と新しい機能を入れたりこういうメソッドを入れて警告を出すとか、「この辺が良くないのでは?」みたいなのを教えた方がいいんじゃないっていう提案が来るたびにじゃあそのエラーメッセージなり警告メッセージをプログラマーが見て「なにかできることはあるの?」みたいな話をしたら「いや、ないかも」みたいな話とか結構開発の方針を決めたりするときはあるあるネタです。メッセージを出したりプログラミング言語の動きとして、それを使った人が、「じゃあそれ見てなんかできることはあるの?」とか逆に「こうすればもっと良くなる」みたいなことをすぐ適切に知らせるにはどうしたらいいのかみたいなのは本当にRubyコミッターの中でも熟考というか結構紛糾しがちなネタだったりします。

柴田:例えばワード1個をセーフって言い切ってしまっていいのかみたいな話とかでも、1時間とか2時間とか議論して「いやこれセーフって言うとダメでしょう」とか「いやじゃあなんて言えばいいの」みたいな話とかは結構あるあるネタだったりしますよね。

遠藤:名前はね、本当にデベロッパーエクスペリエンスに直結するところなので、だいぶ長く議論をしますね。その結果がやや不自然な結果になることがあるんですけど本当に熱意を持って設計されているところだと思います。

今後の計画

櫻井:なるほど。ありがとうございます。さまざまなお話を伺ってきまして、ぜひ今後のお話も伺いたいと思うんですけれども、今後お二人がやっていきたいRuby開発にはどんなことがあるでしょうか? もし計画などがあればぜひ教えていただきたいです。

柴田:はい。計画はないのでそれぞれが勝手にやりたいことをやっている、という話のあとに計画の話をするというのもあれなんですけど、僕が思っている部分としては、やっぱりRubyを開発する人が、開発をもっとしやすくなるようにできるといいなと思っている部分があるのでそこの部分の支援ですね。

柴田:具体的にはRubyがちゃんとサポートしている動く場所、コンピューターの上として10個とか20個とか、Linuxの上であるとかmacOSの上であるとかWindowsの上であるとか、そういったいろんな部分で動かせるようにしましょうというのをユーザーと約束していて、ちゃんとそこの上で動かせるようにするというのをやっているんですけど、何かの変更を入れた時にWindowsは動きませんでしたとか、Windows向けに入れた変更はmacOSではダメでしたみたいなことがよくあるんですね。

柴田:ただ、やはりRubyコミッター1人1人が持っているコンピューターは1個とか2個とかに限られているので、あるRubyコミッターがサポートするよって約束している10個とか20個とかの環境に即座に自分の目の前のコードを実行したりテストできるようにするみたいな部分を来年何かしら用意したいなというのが1個目の野望というか計画で、2つ目はリリース作業をもっと楽にしたいと思っていて現状は僕と遠藤さんとあとは3、4名のリリース担当のRubyコミッターと呼ばれる人たちが18時ぐらいから大体いつも22時から23時過ぎまでいつもリリース作業と呼ばれる作業をします。皆様に最新のRubyをご提供みたいな仕事をしているんですけど、もうなんかやるたびに(少しずつ良くはなっているんですけど)ほぼみんながもうやりたくないなっていう風に考えて、また次頑張るみたいな消耗戦を繰り広げているので、もう念じたらリリースできるというくらいまではちょっと頑張りたいなと思っているところです。だからいろいろ問題があるんですよね。

遠藤:不思議ですよね。リリースは本当にびっくりするぐらい何かしら必ずはまるという感じで。

柴田:なんか解決したはずなのに新しい問題がまた起きるみたいなことを本当に繰り返していて、誰かがサボリとか悪意を持ってやってしまったとか、そういう話ではなくて本当に新しい技術的な課題が毎回起きていて、そのたびにちゃんとこれはポストモーテムして対策を入れるみたいのを毎回少しずつ対策してるんですけど毎回新しい問題が起きるんですよね。

遠藤:普通にソースコードをtarballにまとめるだけでなんでこんなにハマるんだっていう風に思う人もいるかもしれませんけど、本当になぜかtarballに固めたバージョンだけで発生するバグとかが毎回ちょっとずつ混入するのでパッケージを作ってテストしてみたら失敗するとかっていうのが発生するんですよね。なので、いざ本当にリリースバージョンを作ったら新しい問題が分かるのでそこから慌てて直すとかっていうのが何かしら発生する感じで大変ですよね本当に。

柴田:ですよね。Webサービスの場合は自分たちが面倒を見て全責任を持っているコンピューターの上で動かすようにソフトウェアをリリースしますっていう感じなんですけど、Rubyとかプログラミング言語の場合は自分たちの外に向かってソフトウェアをリリースするみたいな部分です。なので、使う人の数だけそのソフトウェアが動く場所があって可能な限りそれを広くカバーしたいけどカバーしきれないこともあって「とあるソフトウェアがこういうバージョンだったらビルドできませんでした。これあかんみたい」な話とかを何回も繰り返しています。そこの大変さをちょっと広い目っていうか大きいスコープで捉えて解決するような仕組みを用意することでリリースとかはもっと毎月でもバンバンやっていいんじゃないのくらいのほうが、短いサイクルのほうがちょっとした不具合があっても「3ヶ月待たないとRubyは直らないんだよな」から「来月直るし」みたいなほうがユーザーにとってはおそらくいいことだろうし、開発する側にとっても何かミスっても来月直せばええやみたいな感じになって、みんなが楽になると思うので、少しずつがんばりたいなっていうのは僕の2023年の目標活動に入れています。

遠藤:ほんとリリース頻度上げたいですよね。定期に3、4ヶ月くらいしたら1回入れるかどうかっていう。

柴田:年3、4回ですもんね。

遠藤:nodeとかは実質的に2ヶ月に1回リリースしてるみたいなので、そういうのをまねしていきたいですよね。

柴田:そのくらいになりたい、、、隔月くらいになりたいですよね。

遠藤:隔月くらいになるとユーザーも「そろそろRubyの新しいバージョンが出るらしい」みたいな備えができるかなと思っていて、いいなと思ってます。

柴田:それに雑に壊れても、次回直せばいいなみたいな感じにもなれると思うので、やっぱり儀式化すると挑戦する意欲を高めるのにもすごい時間がかかるし、やってしまったときの「あー」みたいな気持ちもすごい高まるので、その辺の敷居はどんどん雑に下げていきたいですね。遠藤さんも何かあるんですか?

遠藤:そうですね。最初のほうに話したTypeProfっていうやつを今まで作ってきてるんですけど、今年はですね今の実装のアプローチ、バイトコードを解析するというベースのアプローチだとちょっと限界を感じてきていて、抽象構文木ベースで解析し直すように作り直すというのを考えています。そのためにまずParserをどうにかするという必要があって、その辺りをShopifyの人たちがやってくれているのがそろそろ形になってきているのでそれをベースに2023年に作り直したいなというふうに思っています。それによってVS codeでのRubyの対応が弱いとかって言われているのを改善するように1つの提案ができたらなというふうに思っております。

個人の開発者がRuby開発へ貢献できる方法

櫻井:では最後にですね、ここまで聞いてきたリスナーで自分もRuby開発に貢献したいと思ったエンジニアが結構いるんじゃないかなと思ってるんですが、どうやったら個人の開発者がRuby開発へ貢献できるのでしょうか?

柴田:一番簡単な方法はとにかくRubyを使うっていうのがあって、手元の仕事で開発しているソフトウェアであるとか仕事じゃないソフトウェアもいっぱい皆さんのお手元にあると思うんですけど、その辺のコードをとにかくRubyで実行するということがまず最初の一歩です。

柴田:2つ目はですね、ここがちょっとアクションが必要になってくるんですけど、その動かした結果をRubyの開発チームに伝えるっていうのがポイントだと思っていて、動いたっていうことも重要なんですよ実は。

柴田:Rubyの開発チームはPreviewバージョンとRCバージョンっていうのを1年間に2、3回リリースするんですけど、その2、3回リリースしたPreviewバージョンを使って自分の手元のコードを実行してみて動いたら動いたって言ってほしいですし、動かなかったらこういうエラーが出て動かなかったっていうのを教えてほしい。どちらも実は重要でまず実行してもらわないことには不具合なり、そのちゃんと正しいっていう動きも我々が知ることができませんし。で実行した後に動いた動かないっていう状況を伝えてもらわないことには我々はそれを知ることができないっという二段構えになってまして、本当に本当に大事で、ベータバージョンを出してみんながテストしてくれたから、大丈夫だったと思ってベータじゃない正式バージョンをリリースしたら全然動きませんでした。「なんでだ?」「ベータバージョンは誰も実行していなかったからだ」みたいなのは本当ソフトウェアあるあるな話なので、とにかくベータバージョンみたいなものを触ってみて「なんだこれ?」みたいなものがあったら動かなかったっていう報告であるとか、あとこの動きはちょっとおかしいんじゃないとかもぜひ教えてもらいたいなと思います。

柴田:それで、教えてもらう方法もできる限りいろんな窓口を用意していて、メインで使ってるのはRedmineっていう課題管理ソフトウェアなので、Redmineの方で報告してもらうっていうのが一番の王道というかメインの手段なんですけど、それ以外にも例えばSlackにRubyのグローバルなコミュニティーとかもあったりするんですけど、そこの部分で動かなかったとか動いたみたいなものを僕とか遠藤さんにお伝えてもらってもいいですし、なんかTwitterとかそういう類似のソーシャルネットワークのサービスでメンションして、このコードが動きませんでしたみたいなことを伝えてもらってもいいですし、何かしらの手段でとにかくRubyの開発をしている「Rubyコミッターです」って名乗っている人たちに伝えるっていうのが一番最初にまずできることだと思いますね。

柴田:で、第3ステップ目がちょっとハードルが上がるのかなと思うんですけど「なんだこれ」みたいな動きに対して「こうした方がいいんじゃないのか」っていうようなコードを書いて、それをGithubなりのプルリクエストでサブミットするとかRedmineの方にコードの断片をパッチとして貼り付けて投稿するとか、そういった部分を繰り返していくっていうのがRuby開発への貢献。Rubyのコミッターサイドとしてすごいありがたいなっていうような動きになるのかなと思います。

柴田:皆さんが会社で行われてるような陸続きだと思っていて、RubyコミッターはただRubyを開発してる人でしかないので、チーム開発とかサービスを開発するときに隣の人が作った機能を実行してみたら動かなかったんだけどみたいなことだったら、皆さんは多分すぐSlackとかGitHubとか何かしらのチャットツールとかで動かなかったよって伝えると思うんですね。本当それと同じようなノリでいいと思っていて、ほんとまつもとゆきひろさんにTwitterで「これ動かないんですけど」みたいなって言うくらいでもいいと思って、まつもとさんすごいフレンドリーなので、まあそういう形でこうどんどん伝えて一緒に作っていくっていうのがRubyの魅力だと思うのでぜひなんかやっていただけるといいのかなと思います。

遠藤:そうですね。本当に動かなかったときに誰にも言わないで諦めてしまうっていうのが一番残念で、誰かが困った問題では他の人が困るのでいったん声を上げるっていうのが重要だと思います。声を上げるのもできればTwitterで誰ともなしに語るだけではなくて、僕らのようにRubyの開発やってる人になんとか伝わる形で、一番簡単なメンションとか、より理想的にはやっぱりバグトラッカーに報告をするっていう形で伝えてもらえたら嬉しいなと思います。本当に時々あるんですけどTwitterで動かなかったというツイートを誰かコミッターが拾って直すっていう対応をすることもあるんですけども たぶんほとんどのやつは気が付かずに流れていってると思うので伝えてもらえると嬉しいなと思います。個人の開発者がRuby開発へ貢献できるかっていう話だと、RubyはCで書かれているのでちょっとハードルが高いっていう風におっしゃる方が時々見かけるんですけれども、そんなに難しく考えなくても(もちろんCが書けるに越したことはないと思うんですけども)そのようにバグ報告をするっていうのも重要な貢献ですし、特に機能提案に関してこういうユースケースでこの機能が欲しいとか、この機能提案だとこういうケースで問題があるだろうという議論に参加するという形の貢献もあると思います。実際にそのRubyのコードに手を動かして貢献したいっていう時も何かしら声をかけてもらえれば課題を紹介したりとか書こうとしているプログラムを手伝ったりとかもできると思いますので、やっぱりこれも声をかけてもらうというのがすごく重要かなと思います。

櫻井:ありがとうございます。思っているだけではなくて、アクションに起こすことでRuby開発への貢献もできるし、コミッターへの道も開けるのではないかと。皆さんぜひどうしても見構えてしまう方もいるのかなというところがあるので、そういったところはちょっと一旦置いておいて、ちょっとした勇気を持ってコミュニケーションを取ってみるとそこから先に進めるのかなという気がしました。

遠藤:そうですね。自分が実際にRubyを使っていて、こういう機能が欲しいっていう思いから貢献してもらうのがベストではあるんですけども、何かやりたいけれども特にアイデアがないっていう時には、時々Google Summer of CodeとかRubyのコミュニティからこういう課題があるっていうのを紹介することがあるので、それを参考にこれだったら自分ができるかもというのを選んでもらうというのもいいかなと思います。

遠藤:この間柴田さんが書いたブログの記事とかね。そういう感じでRubyの長い懸案になっている課題みたいなのを紹介することもあるのでそういうのを考えてみてもらえといいかなと思ったりします。

柴田:そうですよね。結構発信してなかったなと思ったんで、こういうことをやりたいとか。プログラミング言語業界の懸案事項って実は結構あって、どの言語でも実は今この問題があって、どの言語も解決できてないとか、ある言語だけこういうアプローチで解決できているとか、何なら無視しているとか、結構そういうのはRubyコミッターの中では議論として出たりしているんで、そういったものをできる限り開発ネタとして発信をして意欲のある人が「えいっ」て頑張って作るみたいな部分でネタを提供したりもできればいいかなと思ってますね。

遠藤:そうですね。バグトラッカー上で議論しているのを頑張っておりますが、ちゃんとまとまってないのでどういう課題があるかというのを一覧できないのがやっぱり難しいですね。発信していかないとですね。

櫻井:ありがとうございます。今回も本当にいろいろな裏のお話だったりとか、深いお話もお伺いできたかなと思うのですが、そろそろお時間となりますので、今回のANDPAD TECH TALKとしては以上で終了とさせていただきたいと思います。

さいごに

Rubyコミッターによる対談はいかがだったでしょうか?クックパッドではサーバーサイドやOSSに関わりたい仲間を募集しています。Rubyについてもう少し詳細を知りたい方はカジュアル面談も実施していますので、ご興味のある方はぜひ気軽にご連絡ください。

cookpad.careers

AWS re:Invent 2022 に参加してきました

SRE の @s4ichi です。年が明けてしまいましたが、昨年 11 月の終わりから 12 月の始めに開催された re:Invent 2022 へ現地参加してきたため、足を運んで得てきた体験をレポートとしてお届けします。今年もクックパッドから総勢5名の社員が参加してきました。

クックパッドと AWS re:Invent

クックパッドでは 2011 年ごろから AWS の各種サービスを利用しています。当時のサービスを AWS に移行するところから始め、かれこれ 10 年以上 AWS をコアに利用してきました。 re:Invent へは 2012 年から社員が参加しています。コアに利用するサービスのアップデートをキャッチアップするのはもちろんのこと、普段利用しないサービスの Workshop へ参加して知見を更新したり、AWS の開発者と直接議論やフィードバックをすることで、普段利用するだけでは見えてこない視点を養う機会になっています。

参加レポート

さて本題です。 re:Invent への参加は、参加した社員の知見更新だけでなく、帰国後に社内で実施する報告会や個々人の社内ブログ投稿を通して社内のエンジニアへ広められます。今回はそんな社内ブログに投稿されたレポートから取り上げた内容をいくつかピックアップして紹介します。

参加者それぞれ毛色の異なるセッションを聞いてきたので、それぞれの視点から書いたレポートをお届けします。今回レポートを書いたのは @s4ichi, id:sora_h, @hilinker, そして @naoki_shigehisa の4名です。

EBC (Enterprise Business Conference)

id:sora_h です。クックパッドは例年、AWS の担当アカウントチームに re:Invent を含めいろいろな機会で AWS のサービスチームとミーティング (EBC や Customer Meeting と呼ばれるもの) を組んでもらっています。re:Invent 2022 でも、複数のチームとディスカッションをすることができました。

詳細はここには書けませんが、基本的にやることは自分たちがどうサービスを使っていて、何に困っているかを持っていきます。現地ではそれに対して自分たちで workaround できる方法はあるか、サービスに機能を入れるとしたらどういう方向がいいのか、といった内容を議論することが多いです。内容は当然英語なので、基本的には事前にざっくりとした資料を作ったりして楽になるように、また限られた時間を有意義に活用できるようにしています。

担当者個人としては、re:Invent のような巨大カンファレンスはアーカイブに残らないコンテンツや機会を中心に時間を使うべきと考えていて、EBC は AWS との渉外を担当する自分にとっても、またマネージドサービスをしっかり利用している会社としても、重要なミッションです。今回も楽しく有意義な議論ができて満足しています (内容は書けないのですが)。

[NEW LAUNCH!] Using policies to manage permissions with Amazon Verified Permissions [Breakout Session]

@s4ichi です。このセッションは Amazon Verified Permissions という新機能に関するもので、その機能の説明からの技術の概要までを深堀りするセッションでした。Amazon Verified Permissions 自体も機能がかなり面白くて、簡単に言えば IAM Policy に似た記述を用いて任意のドメインに関する認証・認可を代替してくれるマネージドサービスになっています。 つまり、「ユーザーがレシピAを見るのに必要な権限があるか -> YES or NO」みたいなのを検証してくれる機能がサービスとして提供され、API を経由して利用できます。

そんな機能紹介はさておき、記述言語が独自言語であるため、その言語についての紹介が半分の時間を占めていました。プログラム言語に詳しい開発者の方が出てきて、作った言語の思想を紹介していく姿は、学会にでも来たのか?という状況が味わえる良いセッションでした。あとで調べたんですが、発表者の方はプログラム言語に関するエキスパートで、POPL や PLDI などでソフトウェアの Synthesis や Verification を専門とするアカデミアの方だそうです。そんな方が AWS の Scientist として登壇されていたので幅広い分野の方々が AWS を支えているのを実感しました。

記述言語は Cedar という名前です。Rust で実装されていて、公式サイトに Playground があります。

www.cedarpolicy.com

詳細には解説されていませんでしたが、この言語自体に対する仕様の検証や Policy の同値性証明などの話もありました。世紀の発明、というほどではないんですが、ニーズや要件を満たす最小限の言語がドンと出て機能の中核を担っているの、アツいものがありますよね。

セッションは動画も公開されています。

www.youtube.com

The evolution of chaos engineering at Netflix

@s4ichi です。Netflix はセッションの種別に NFX という Prefix があるくらい特別視されているらしく、このセッションはそんな NFX 系の Chaos Engineering についての発表です。

Chaos Monkey で一部のリソースを勝手に停止するような文字通り "Chaos" な取り組みから、今は FIT(Failure injection Technology)を構築している様子についての発表でした。 microservices 上の様々な要素を観測するための分散トレーシング基盤やサービスメッシュが Netflix のインフラに構築されており、その上で Failure injection を柔軟に扱う仕組みがあるそうです。Chaos Monkey のようなランダム性のある取り組みは方向性は良かったのですが、計算資源単位ではなくドメインやアーキテクチャを考慮した失敗、というのをシミュレートしたいというモチベーション。特定のサービス間で耐障害性が低くなる部分があったことが原因の障害があり、シナリオベースで障害を注入できる必要があると学んだ、とのことでした。

Netflix のサービスはソフトウェアエンジニアなら誰でも FIT を使った障害の注入が可能になっています(専用コンソールがありそうな雰囲気でした)。Injection Point や Scope、Treatment(回復手段)にその Scenario を定義することができて、それをリアルタイムに実行できます。

具体的には、リクエストヘッダに対して(例では x-ntfx-fit-fail でした)コンテキストを付与することができます。その内容は、各サービスやその横にある Envoy が解釈して Failure injection をシミュレートするためのコンテキストになるそうです。 例えば、「サービスAからBに通信するクライアント側で Failure する。Canary リリースされたユーザーのグループCのみに適用。障害時はリクエストを Delay して失敗させる」ぐらいの粒度で指定が可能です。シナリオを用意しつつ、それを Canary や Production 環境と組み合わせてシミュレートできるため、非常に機能的でした。

「Chaos is dead, Long live...」言っていたのが印象的です。可観測性を上げ、FIT によって特定の箇所を狙って障害をエミュレートできるようにし、リリースの戦略と合わせてテストをできるようにすることで、カオスはたった一つの道具にすぎないと話していました。例えば当時からある Chaos Monkey はそれ自体は良いけど、今は殆ど使われていないそうです。

セッションの動画も公開されています。

www.youtube.com

Deep dive into Amazon Aurora and its innovations [Breakout Session]

@hilinker です。このセッションは、前半で Amazon Aurora の仕組みを解説しつつ、後半では直近リリースされた機能の紹介をするものでした。クックパッドでは Amazon Aurora を非常に多くのアプリケーションで採用しています。Aurora の細かい利点については AWS の公式ドキュメントに譲りますが、かなり様々な恩恵を受けています。

さて、特にここでは直近で発表された Aurora の新規機能のうち、特に以下2つについて触れておきたいと思います。

1つ目は、Amazon Aurora zero-ETL integration with Amazon Redshift です。

aws.amazon.com

これは Aurora のデータをほとんどリアルタイムに Amazon Redshift 上に同期することができるというものです。 クックパッドではアプリケーションのデータベースとしては Aurora を利用し、分析などに用いる DWH として Amazon Redshift を採用しています。分析などの際には Aurora にあるアプリケーションのデータを Redshift 側にコピーして用いるのですが、このコピーは日次バッチなどを実行することで行っているケースがほとんどで、どうしてもデータの同期に時差が生じてしまいます。特に、最近は Redshift 上のデータを利用して組み上げたデータを元にして、リアルタイムにユーザーへコンテンツを表示したいという要求が社内で高まっていることもあり、Aurora、Redshift 間のデータの同期というのは悩みのタネでした。苦肉の策でバッチの実行間隔を狭めたりはしていましたが、あまりクールな解決とは言えませんでした。 そんな中で登場したこの新機能。いやぁ、待望と言わざるを得ないですね。これが入ると上述したような悩みのタネが一気に解決されます。 ただ、まだプレビュー版なのと、おそらく Aurora MySQL v3 からしか利用できないっぽいので、社内にある Aurora クラスタたちを v2 から v3 に上げていかなければならなそうなのはかなり大変かも。

2つ目は、Fully Managed Blue/Green Deployments です。

aws.amazon.com

これは、Aurora クラスタのバージョン更新作業などのメンテナンス時に、Blue/Green デプロイメントを AWS 側の用意してくれたスキームで実施できるというものです。これは re:Invent の keynote とかで発表されたわけではなく、初日ぐらいにさらっとニュースが出ただけなのですが、マジで待望の機能でした。

軽く説明すると、Aurora にはエンジンバージョンがあります。で、バージョンがあるということは当然 EOL もあり、EOL までにアップグレードをかけなければならないのですが、このアップグレードの際にどうしても DB にダウンタイムが発生してしまいます。in-place upgrade というボタンをポチッとするだけで上げられる方法を使うとアップグレード自体は簡単にできるんですが、read も write も平気で10分以上止まるので認証サーバーなどのクリティカルな DB では採用できません。

では、そいつらどうやってアップグレードするかというと、もう1個クラスタを用意してそっちにデータをレプリケーションしつつ、アプリケーションからアクセスする writer をえいやで切り替える方法を取ります。この方法だとアプリケーションから接続するエンドポイントを切り替えるだけなので、まず read のダウンタイムは発生せず、かつ write のダウンタイムも数分で済みます。

じゃあ全部レプリケーション方式でやればいいじゃん!と思うかもしれませんが、このレプリケーション方式、マジで準備が面倒くさいんですよね。いや、本当に作業と確認しなきゃいけない項目が多すぎて時間とメンタルが溶けます。

ということでここらへんの面倒な準備を AWS 側でいい感じにやってくれて、ぽちぽちすると切り替えができるようにしてくれる(らしい)のがこの Blue/Green Deployment 機能になります。早口でいっぱい喋っちゃうぐらいには嬉しい。

Build your first project with Amazon CodeCatalyst [Workshop]

@hilinker です。今回発表された新サービスの1つである Amazon CodeCatalyst を触ってきました。最初は、「CI/CD をいい感じにしてくれるぐらいかな〜」と思っていたらそれだけではなく、なんか開発に必要なサービス全部入ってますみたいな超巨大サービスでした。

まず、Git レポジトリの機能があります。そして、当然 issue/PR が作成できます。この時点で結構面白いんですが、さらにファイルを Cloud9 で開けて当然コードの編集ができます。レポジトリ内に manifest ファイルを定義するだけで PR の CI や CD の workflow を CodePipeline 上に作成できます。当然、実行や結果の確認、ログ出力なども CodeCatalyst 上でできます。さらに CloudFormation との連携も提供されているのでインフラ管理もお手のもの……

未だかつてなくオールインワンのサービスで、来るところまで来たなという感じがして笑っていました。難しいこと考えずにとりあえず AWS に乗っかれば大丈夫というのはだいぶ面白いですね。

ただ、こんだけオールインワンでやっていながら結構他からの移植性みたいなのは気にしていて、たとえば IDE で言えば Cloud9 だけでなく普通に VSCode や IntelliJ も使えたり、GitHub をレポジトリソースにできたり、Jenkins などと連携できたりと部分的に既存のものを使えるようにしているのは割と気を遣っているなという印象でした。

個人的にちょっと面白かったのは、CodeCatalyst のアカウントは AWS アカウント(やその中のユーザー)とは別のものということですかね。1人は1つの AWS Builder アカウントを持っており、それが複数の AWS アカウントと結びつくという関係らしいです。

[NEW LAUNCH!] Provision and scale OpenSearch resources with serverless [Breakout Session]

@naoki_shigehisa です。このセッションは新機能の OpenSearch Serverless についての Breakout session でした。名前の通りですが、 OpenSearch Service のServerless モードがプレビューとしてリリースされたという内容です。

クックパッドの検索では主にsolrが使われていますが、 OpenSearch Service もどこかで活用できないかなと思っていたところ keynote で Serverless モードが発表され、興味津々でセッションを聞きにいきました。

驚いたのは Serveless モードでサポートしている auto scaling の機能を実現するための構成が、クックパッドの solr を使った検索システムの構成とかなり似ていたことです。

techlife.cookpad.com

具体的には indexing 用の unit と search 用の unit が別に存在し、indexing 用の unit は index を作成して s3 に保存し、 search 用の unit は s3 に保存された index を使って起動するという構成です。時代がクックパッドに追いつきましたね。 現状ちょっと値段が高かったのですぐに活用するのは難しいかもしれませんが、せっかくなのでどこかで試しに使ってみたいなと思っています。

こちらのセッションは録画も公開されていたので、リンクを貼っておきます。

www.youtube.com

Improve search relevance with ML in Amazon OpenSearch Service [Workshop]

@naoki_shigehisa です。このセッションは SageMaker と OpenSearch Service を使って、単語の embedding を使ったテキスト検索を実現する workshop でした。

SageMaker 内で起動した notebook から OpenSearch へ embedding を含めたドキュメントを登録しておき、実際に検索する時にはSageMaker を使って作成したエンドポイントに対して検索クエリを投げて取得した embedding を使って OpenSearch に向けて検索リクエストを投げる形式で実装を行いました。これによって、単語の完全一致ではなくある程度意味を考慮してドキュメントの検索を行うことができます。

workshop用のコードの大半は予め用意されていてほとんど notebook 上でコードを実行するだけではありましたが、実装に必要な作業を大まかに把握できました。

機械学習の用途としては割とありがちなものだなと思いましたが、ほとんどの作業が SageMaker 内で起動した notebook 上で済むのはとても便利だなと感じました。モデルのデプロイやエンドポイントの作成まで notebook 上でコードを実行しているだけで完了するのは嬉しいですね。ぜひ活用してみたいです。

AWS GameDay: The New Frontier

id:sora_h です。先に述べたようなインタラクティブでアーカイブがないセッションとして、今年もGameDay に参加してきました (月曜午後の回)。当然、ランダムなチーム構成を希望して現地にいる他の知らない参加者とチームを組んで挑戦します。

最初に結果だけ書いておくと、2 度目の優勝を遂げて 2019年ぶりに Keynote の特等席を得たりしていました。

今年、というか近年の GameDay は他イベントでの再利用など横展開がしやすいようにするためか、初期の競技性があるもの — 全てをデプロイしてそれをちゃんと動かし続けてポイントを得る、ではなくクエストでポイントを稼いでいく形式、実質的に AWS コンソール操作 RTA レースみたいになっているのはちょっと残念と言えます。今回も多分に漏れずその傾向で、難易度が高いクエストから処理していけば上位にいけるので、悲しいところです。

いちおう Learning Opportunity の一環としてのゲームであるため、他のメンバーのサポートなどに回ったりしています。他の会社どころか他の国や出身の人達と同じ目的に対して頑張るという意味では貴重な機会なので、ここはたいへん楽しいところで、そしてたいへん運ゲーなところですね。

また頻繁に上位にいるせいで PM に顔を覚えられていて、またお前か…のような反応を得つつ公式からツイートもされる始末です。うかつな結果を残すわけにはいかないので、引き続き精進しようと思います。

思い出

参加メンバーで会期後にグランドキャニオン含むツアーに弾丸参加したときの写真です。(id:sora_h は Re:ステージ! のライブに行くために一足早く帰ったため、5人での集合写真が存在しませんでした)

さいごに

クックパッドでは AWS re:Invent の参加を通して、実際のサービス・基盤開発に活かせることを発見したり、より効率よく AWS を利用できないか研鑽する取り組みを続けています。こうしたブログでの発信に留まらない発見や知見更新のチャンスが社内に溢れているためです。

クックパッドには経験年数問わず、熱量のあるエンジニアがこうしたカンファレンスに参加できる環境があります。興味を持っていただけた方は下記のリンクより採用情報を参考にしてみてください。

cookpad.careers