メドピア開発者ブログ

集合知により医療を再発明しようと邁進しているヘルステックカンパニーのエンジニアブログです。読者に有用な情報発信ができるよう心がけたいので応援のほどよろしくお願いします。

Feature Toggleを用いたRailsアプリの継続的なリリースと要注意事項

はじめに

皆様こんにちは、サーバーサイドエンジニアの草分です。

突然ですが、開発者の皆様、実装したソースコードはこまめにリリースしていますか? 「大きい機能なので開発に時間がかかる」「障害が起きないよう念入りにテストする必要がある」などの理由で、Featureブランチのままコミットグラフが伸びに伸びたりしていませんか?

大きな機能を作ること自体は悪いことではありませんが、大きすぎるFeatureブランチは、本流ブランチとの挙動の乖離やコードの衝突が発生しやすく、レビューやマージに多大な苦労を伴います。

この記事では、この問題の解決策の1つとなる「Feature Toggle」を、Ruby on Railsにおける実装方法と共にご紹介します。 Feature Toggle自体は開発手法の一種であるため、言語/フレームワークを問わず広く活用されています。

Feature Toggleとは

  • 「機能が動作する/動作しない を切り替える」機構です。
  • ソースコードは存在しますが、トグルを「ON」にするまで機能が動作しないよう制御します。

Feature Toggle 動作イメージ

効果

Featureブランチの早期マージが可能となる

通常、作りかけの機能を本流ブランチにマージしてしまうと、ユーザーに未完成の機能を提供することになるため、問題になってしまいます。

そこでFeature Toggleを用いて、開発中の機能はトグル「OFF」の場合動作しないように実装します。そうすることで、開発中の機能をユーザー環境に影響を出さずにマージできるようになります。

本番環境でのテストが可能となる

本番環境では、開発環境では見つからなかった問題が多々発生します。 例えば、既存データに予期せぬレコードが入っていたり、アプリではない別のレイヤーでトラブルが起きたり...

Feature Toggleで社内向けのテストユーザーのみ機能を利用できるよう制御すれば、実際のユーザー環境にリリースする前に、テストユーザーで機能の動作確認ができるようになります。

Railsでの実装例

この記事では、Feature Toggle実装の助けとなってくれる「Flipper」 gemをご紹介します。 このgemはフラグ管理と分岐制御の仕組みを提供しています。

github.com

Feature Toggleは独自に実装することもできますが、このgemを使えばフラグの動的切り替えやフラグの管理画面などを比較的簡単に導入することができます。

続きを読む

SwiftUI / UIKit (Storyboard) ハイブリッド対応、Needle + RIBs インスパイアな iOS アプリケーションデザイン

こんにちは、モバイルアプリを開発しています高橋です。交互に仕事場に猫二匹がやってきて監視されながら仕事しています。

先日リリースしたとある iOS アプリは、

  • 機能は機能ごとに分割して実装したい
  • 依存解決のコードは自動生成したい
  • ライトウェイトな設計としたい

というコンセプトの元、コンパイルセーフな DI フレームワークの uber/needle を使い、uber/RIBs のようなアプリケーションアーキテクチャでデザインすることで、各コンポーネントをコンパクトに分割することができました。

Needle や RIBs が前提知識となります。そのため本記事ではざっと Needle と RIBs を解説したのちに、具体的なコードを交えて SwiftUI + UIKit (Storyboard) ハイブリッド対応でかつ Needle + RIBs インスパイアなアプリケーションアーキテクチャの一端を解説してみます。

動作環境

  • Xcode 13.2
  • Swift 5.5
  • macOS 12 Monterey (macOS 11 BigSur でも確認済み)

Needle

まず、簡単に Needle を説明します。

Needle は各モジュールをコンポーネントとしてツリーで表現し DI することができるものです。

コードジェネレーターの cli ツール→①と、ジェネレートされたコードを引き回すライブラリ→②がセットになっており、①をビルド時に実行するようにして使います。

Swift Package Manager でも Carthage でも CocoaPods でも使うことができます。ここではサクッと①を Homebrew 、②を Xcode の Swift Package Manager で入れてみます。

①Needle コードジェネレータ

% brew install needle

次に、Build Phase で Compile Sources の前に以下を設定します。

mkdir "$TEMP_DIR"
touch "$TEMP_DIR/needle-lastrun"
SOURCEKIT_LOGGING=0 needle generate $SRCROOT/アプリフォルダ名/Needle.swift アプリフォルダ名

※Carthage で入れた場合はチェックアウトディレクトリの中にあるので、それ (Carthage/Checkouts/needle/Generator/bin/needle) を実行します。

ビルドしてみると、registerProviderFactories() という空のメソッドが生えている Needle.swift ファイルができます。このメソッドは後ほど使用します。

Needle.swift ファイルを .xcodeproj に入れたら、下準備完了です。

コラム: SOURCEKIT_LOGGING=0

SourceKit ロギングをサイレントモードにしています。 通常 Xcode はログを表示しますが、Xcode で作業している際のノイズを減らすために入れています。

②Needle ライブラリ

ライブラリの方もプロジェクトに入れます。Xcode → File → Add Packages... より Needle のレポジトリ(https://github.com/uber/needle.git)を指定して入れます。

Needle を Swift Package Manager で入れる
Needle を Swift Package Manager で入れる

Needle の使い方の簡単な説明をするとこのようになります。

  • コンポーネントをツリー構造にします。ルートだけ BootstrapComponent のサブクラスにします。
  • 親コンポーネントで定義したコンポーネントを Dependency にするので、上位コンポーネント(ルートとか)で諸々注入します。
  • 子コンポーネントでは親で注入されたものを protocol Dependency で定義することで利用することができます。
  • 子コンポーネントは Component<***Dependency> のサブクラスにします。

Needle を使ったサンプル

雰囲気を掴むために、雑ですがコードを示します。

RootComponent.swift

import NeedleFoundation

final class RootComponent: BootstrapComponent {
    var point: Int {
        return 100
    }
    var featureABuilder: FeatureABuilder { FeatureAComponent(parent: self) }
}

FeatureAComponent.swift

import NeedleFoundation

protocol FeatureADependency: Dependency {
    var point: Int { get }
}
protocol FeatureABuilder {
    func showPoint()
}
class FeatureAComponent: Component<FeatureADependency> {
}
extension FeatureAComponent: FeatureABuilder {
    func showPoint() {
        print(dependency.point)
    }
}

これで子の FeatureAComponent の中で、親の RootComponent で定義した point にアクセスすることができています。Component<***Dependency> の中で dependency というプロパティが生えているので、これ経由でアクセスすることができます。

  • 注:***Builder という protocol を登場させています。 Component の protocol として定義しています。(後述)

AppDelegate あたりで先ほど触れた registerProviderFactories() を呼び出すことで Needle との接続を行います。

AppDelegate.swift

import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    private(set) var rootComponent: RootComponent!
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        registerProviderFactories() // Needle.swift で定義されている
        rootComponent = RootComponent()
        return true
    }
    // MARK: UISceneSession Lifecycle
    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }
    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {}
}

RootComponent を経由して featureAComponent を取り出し、そのメソッドを呼び出して挙動を確認します。

SceneDelegate.swift

import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let _ = (scene as? UIWindowScene) else { return }
        let rootComponent = (UIApplication.shared.delegate as! AppDelegate).rootComponent
        let featureABuilder = rootComponent!.featureABuilder
        featureABuilder.showMoney()
    }
}

実行結果:

100

雰囲気だけで申し訳ありませんが、 Needle は以上のような感じで (point が Component になった時とか、孫 Component を持った時なども同様に) 自動で DI することができます。

RIBs

今回のプロジェクトはあくまでインスパイアされているだけなので実際は使用していませんが、軽くどういうものが触れておくと、こちらは VIPER や MVC のようなアプリケーションアーキテクチャであり、フレームワークでもあります。

実際に使用する際は、uber/RIBs に則ってプロジェクトにインストールします。Xcode テンプレートも利用することができ、スクリプトを実行して Xcode にインストールすることで新規コンポーネントを作成するときに自動で必要なファイルが生成されるようになっているため、効率的に開発が進められるとのことです。

RIB

RIBs は Router-Interactor-Builder(RIB) が中心となったデザインになっており、RIB 単位でコンポーネントとして分割します。画面であればこれに View を付け足します。

  • Router: RIB 間のやりとりを担当し、画面系の RIB であれば画面遷移なども担当します
  • Interactor: ビジネスロジックを担当します
  • Builder: Router / Interactor / View を作ります
  • View: データを表示したり、ユーザーインタラクションを担当します

出典: https://github.com/uber/RIBs/wiki/iOS-Tutorial-1

ルール

大枠のルールとしてはこのような感じです。

  • 別 RIB にアクセスする場合は、Router から別 RIB の Builder を参照する
  • 直接 View から Router にアクセスせず、Interactor で Router のメソッドへアクセスしてもらう

今回のアプリケーションアーキテクチャ(Needle + RIBs インスパイア)

ようやく本題です。

前述した Needle を使用し、RIBs の設計とルールを取り入れたアプリケーションアーキテクチャデザインになっており、Needle + RIBs インスパイアとこの記事では命名しております。(現場では特に名前をつけていません)

Needle + RIBs inspired App Architecture Example
Needle + RIBs inspired App Architecture Example

ベース部分の実装イメージ

あくまでイメージですが、RootComponent.swift や SceneDelegate.swift は以下のようになっています。

RootComponent.swift

import NeedleFoundation

final class RootComponent: BootstrapComponent {
    // DI
    var apiClient: APIClient { APIClient() }
    var errorLogger: ErrorLogger { ErrorLogger() }
    
    // 子コンポーネント
    var featureABuilder: FeatureABuilder { FeatureAComponent(parent: self) }
    var featureBBuilder: FeatureBBuilder { FeatureBComponent(parent: self) }
}

SceneDelegate.swift

import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
        guard let rootComponent = appDelegate.rootComponent else { return }
        let featureABuilder = rootComponent.featureABuilder
        let window = UIWindow(windowScene: windowScene)
        self.window = window
        window.rootViewController = featureABuilder.viewController
        window.makeKeyAndVisible()
    }
}

AppDelegate.swift

import UIKit
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    private(set) var rootComponent: RootComponent!
    func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        registerProviderFactories()
        rootComponent = RootComponent()
        return true
    }
    // MARK: UISceneSession Lifecycle
    func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration {
        UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }
    func application(_: UIApplication,
                     didDiscardSceneSessions _: Set<UISceneSession>) {}
}

(Needle の紹介をしていたときに、 Builder という protocol を急に登場させていましたが)RIBs で言うところの Builder として Component は振る舞ってもらう、という発想に基づいて Builder と命名し、これを使って初期の画面を作成しています。

このとき、Builder はよく build() と命名されているメソッドではなく、エンドポイントの内容を示す意味で viewController を持つようにしています。

コンポーネント部分の実装イメージ

実際のコンポーネントは Builder、Interactor、Router (、View) で構成するので、コンポーネントごとにそれらの protocol と実装を作成し、ファイル分割します。(今回は雑ですが、コンポーネントごとにファイル分割して以下に掲載してしまいます)

FeatureA コンポーネントの View (SwiftUI) から FeatureB コンポーネントの View (UIKit) を画面遷移するサンプルを作ってみました。

ちなみに Interactor は今回は Router へのただの橋渡し役になっています。(API 通信やデータベースアクセスなどデータ処理・ドメインロジックが入るときはここで対応する)

FeatureAComponent.swift

import NeedleFoundation
import UIKit
import SwiftUI

// MARK: -
// MARK: Builder

protocol FeatureADependency: Dependency {
    // 説明用にここで注入。ただしこのサンプルであれば、FeatureAComponent の中で生成してもよい
    var featureBBuilder: FeatureBBuilder { get }
}
protocol FeatureABuilder {
    var viewController: UIViewController { get }
}
final class FeatureAComponent: Component<FeatureADependency> {}
extension FeatureAComponent: FeatureABuilder {
    var viewController: UIViewController {
        let navigationController = UINavigationController()
        let router = FeatureARouter(viewController: navigationController,
                                    featureBBuilder: dependency.featureBBuilder)
        let interactor = FeatureAInteractor(router: router)
        let viewController = FeatureAViewController(interactor: interactor)
        navigationController.viewControllers = [viewController]
        return navigationController
    }
}

// MARK: -
// MARK: Router

protocol FeatureARouting {
    func showFeatureB()
}
final class FeatureARouter: FeatureARouting {
    private weak var viewController: UIViewController?
    private let featureBBuilder: FeatureBBuilder
    init(viewController: UIViewController, featureBBuilder: FeatureBBuilder) {
        self.viewController = viewController
        self.featureBBuilder = featureBBuilder
    }

    func showFeatureB() {
        guard let viewController = viewController else { return }
        viewController.present(featureBBuilder.viewController, animated: true)
    }
}

// MARK: -
// MARK: Interactor

protocol FeatureAInteracting {
    func showFeatureB()
}
final class FeatureAInteractor: FeatureAInteracting {
    private let router: FeatureARouting
    init(router: FeatureARouting) {
        self.router = router
    }
    func showFeatureB() {
        router.showFeatureB()
    }
}

// MARK: -
// MARK: View

final class FeatureAViewController: UIHostingController<FeatureAView> {
    private let interactor: FeatureAInteracting
    init(interactor: FeatureAInteracting) {
        let view = FeatureAView(interactor: interactor)
        self.interactor = interactor
        super.init(rootView: view)
    }
    @MainActor @objc required dynamic init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
struct FeatureAView: View {
    let interactor: FeatureAInteracting?
    var body: some View {
        Button {
            guard let interactor = self.interactor else { return }
            interactor.showFeatureB()
        } label: {
            Text("B 画面開けるかな 🤔")
        }.padding()
    }
}

FeatureB では View を Storyboard で作成しています。Storyboard の説明は割愛します。

FeatureBComponent.swift

import NeedleFoundation
import UIKit

// MARK: -
// MARK: Builder

protocol FeatureBDependency: Dependency {
}
protocol FeatureBBuilder {
    var viewController: UIViewController { get }
}
final class FeatureBComponent: Component<FeatureBDependency> {
}
extension FeatureBComponent: FeatureBBuilder {
    var viewController: UIViewController {
        let router = FeatureBRouter()
        let interactor = FeatureBInteractor(router: router)
        let storyboard = UIStoryboard(name: "FeatureB", bundle: nil)
        guard let viewController = storyboard.instantiateInitialViewController() as? FeatureBViewController else { return UIViewController() }
        viewController.interactor = interactor
        router.viewController = viewController
        return viewController
    }
}

// MARK: -
// MARK: Router

protocol FeatureBRouting {
    func dismiss()
}
final class FeatureBRouter: FeatureBRouting {
    weak var viewController: UIViewController?
    init() {
    }
    func dismiss() {
        viewController?.dismiss(animated: true)
    }
}

// MARK: -
// MARK: Interactor

protocol FeatureBInteracting {
    func dismiss()
}
final class FeatureBInteractor: FeatureBInteracting {
    private let router: FeatureBRouting
    init(router: FeatureBRouting) {
        self.router = router
    }
    func dismiss() {
        router.dismiss()
    }
}

// MARK: -
// MARK: View

final class FeatureBViewController: UIViewController {
    var interactor: FeatureBInteracting?
    @IBAction func dismissButtonWasTapped(_ sender: UIButton) {
        interactor?.dismiss()
    }
}

Demo

Needle で SwiftUI の画面から UIKit (storyboard) の画面に遷移するデモの動画キャプチャ

SwiftUI 画面 → UIKit 画面への画面遷移ができました!

Demo ソースコード一式:

github.com

ちなみに

  • データモデル (Entity) などはこれらと別に struct を定義し Needle の管理下とは関係なくあちこちで利用します。

まとめ

  • ライトウェイトなアーキテクチャである
  • コンパイルセーフでバシバシ DI できる
  • SwiftUI も UIKit でも柔軟に対応できる

よかったらお試しください!


是非読者になってください


メドピアでは一緒に働く仲間を募集しています。 iOS / Android のモバイルエンジニアも募集しています。カジュアル面談からでも OK です! ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

Vue.js 公式ドキュメントのモブ翻訳をやりました!

こんにちは!
週一のサウナは欠かさない、フロントエンドエンジニアの土屋です。

先日、Vue.js の公式ドキュメントがリニューアルされました。
Vue.js 日本ユーザーグループ主導で翻訳プロジェクトが立ち上がっているのはご存知でしょうか?

メドピアでは、普段お世話になっている Vue.js に貢献したいという思いから、数回に渡って、Vue.js 公式ドキュメントのモブ翻訳を行ないました。

モブ翻訳の方法

  • Google Meet でファシリテーターの画面を共有しながら、Vue.js 公式ドキュメントの翻訳を行う
    • OSS へのコントリビュートに慣れていない方は、ファシリテーターが共有した画面を見ながら一緒に作業する。
    • 不明点がある場合、適宜ボイスやテキストチャットで相談しながら行う。
  • OSS へのコントリビュートに慣れてる方は、自分のペースでもくもくと作業する。

モブ翻訳の流れ

ここからは実際のモブ翻訳の流れとともに、その様子を紹介します。

まず、リポジトリの READMEVue.js 公式サイト日本語翻訳ガイドを確認します。
翻訳の方法や注意点が記載されているので、必ず目を通しましょう。

その後、翻訳するページを決めます。
Vue.js ビギナーの参加者が多かったので、Tutorial を翻訳することにしました。 翻訳を通して、Vue.js の知識を獲得するのが狙いです。

GitHub Issues で翻訳タスクが管理されているので、翻訳するページが決まったら、Issue に翻訳する旨をコメントします。
Tutorial を翻訳するので、Tutorial 翻訳まとめという Issue にコメントしました。

f:id:doyahiro:20220330165158p:plain
弊社のメンバーがこぞってコメントする様子

その後、リポジトリをフォークしてローカル開発環境を構築します。
ローカルで立ち上げることに成功したら翻訳開始です!

f:id:doyahiro:20220330165401p:plain
モブ翻訳中の様子。Google Meet で画面を共有しながらワイワイ。テキストチャットも盛んです。モブ翻訳だと、ここはこう訳した方が良いなど、色々な人の意見を聞けるのがありがたいですね。

訳し方に迷ったら Wiki をチェックしましょう。よくあるNGが記載されています。
また、Vue2 の公式ドキュメントではどのように訳されているか確認するのも有用でした。

訳し方に自信が持てない箇所は DeepL を使いました。

f:id:doyahiro:20220330165552p:plain
DeepL は強力ですが、意訳になりすぎたり、文中、文末の : (コロン) が削除されることがあるので注意が必要です。頼りきりにならないようにしましょう。

翻訳が完了したら、フォークした自分のリポジトリにプッシュします。
その後、フォーク元のリポジトリに Pull Request を出します。

f:id:doyahiro:20220330165703p:plain

PRを出すと、メンテナーの方がレビューしてくれます。
修正箇所がある場合は修正して再度コミットします。

f:id:doyahiro:20220330165735p:plain

問題がなければメンテナーの方がマージしてくれます。

f:id:doyahiro:20220330165813p:plain
晴れてマージされました。やったね!

これで一つの翻訳タスクが完了です。

f:id:doyahiro:20220330165902p:plain
初めてのOSSへのコントリビュートに喜びを隠しきれない様子

この流れで数回モブ翻訳を行い、弊社の社員で Tutorial を全て翻訳することができました!

まとめ

OSSにコントリビュートするのが初めての参加者が多かったので、リポジトリをフォークしてPRを出すといった、一般的なOSSへのコントリビュートの流れを体験できたのは良い経験になったと思います。

OSSにコントリビュートしてみたいけど、本体のコードに手を入れるのはハードルが高いと感じている方は、まずドキュメントの翻訳からトライしてみるのはいかがでしょうか。

普段使っているOSSには、今後も積極的にコントリビュートしていきたいですね。


是非読者になってください


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

Google ドキュメントを会議メモとして使う

こんにちは。サーバーサイドエンジニアのティーチです。最近の趣味は生後半年の娘にウケる動作の探求です。直近だと背筋が手応えありました。髪の毛がふわふわするのが面白かったようです。

本記事について

f:id:taichisato:20220304134701p:plain

■書く

  • Google ドキュメントを会議メモとして使うときの設定
  • Google ドキュメントを会議メモとして使う提案をする方法例

■書かない

会議メモを作る意義に触れると本記事の趣旨(Google ドキュメントの具体的な設定の仕方、使用の提案の仕方)以外の部分が長くなるので以下は割愛します。

  • 会議においてアジェンダ、議事録を作る意義
  • Google ドキュメントって何?

対象

  • 会議でアジェンダ、議事録を使いたいなと思ってる方
  • よその会議の仕方知りたいなという方

Google ドキュメントの強み

私のプロジェクトではオンライン会議をするためのツールとしてGoogle Meetを用いています。
そして、会議を入れる時はGoogle カレンダーに予定を入れるようになっています。

Google カレンダーには会議メモという機能があり、予定からGoogle ドキュメントを作成することが容易です。
作成後は予定にGoogle ドキュメントへのリンクが作られるので会議参加者への共有も容易です。

作成前 作成後
f:id:taichisato:20220304132153p:plain f:id:taichisato:20220304132217p:plain

f:id:taichisato:20220304132235p:plain

※2022/3/3時点 生成場所がマイドライブ固定のようなので共有ドライブに移動させるか、「共有」から編集権限を付与する必要があります。

作成、紐付けが容易な上
機能的には画像の挿入、見出し、同時編集機能ができるので良きです。

具体的な使い方

ファイル > ページ設定 から「ページ分けなし」に設定します。(この機能を紹介するためにこの記事書きました!) f:id:taichisato:20220304132252p:plain

ページ設定分けなしにすることで、なんと!例のあの空白をなくすことができます!ステキ! f:id:taichisato:20220304132303p:plain

f:id:taichisato:20220304132317p:plain

そして、1. 左上の灰色の部分をクリックしてテキストの幅を幅広にします。

あとは会議の内容に応じて構成を作る

f:id:taichisato:20220304132329p:plain

見出しは「見出し3」推しです。(MacならCommand + option + 3)

Google ドキュメントを会議メモとして使う提案をする方法例

私はすでに登録してある会議に対して上記のような会議メモを作って例えば以下のように共有しています。

前提

私はスギサポdeliというプロジェクトに携わっており、メンバーは本サービスを「deli」と呼称しています。

最近具だくさん食べるスープのBセットが出ました。私もまだ食べてないのですが、AセットはかなりおいしかったのでBセットもぜひ!(宣伝)

deliでは GitHubのプロジェクトボード を画面共有して会議していました。

Slackで投稿した文(ポイントがわかりやすいように修正しています)

Taichi Sato(teach)

deliの週次定例は内容充実してて良いのですが、
事前準備やissue外のトピックについての話し合いがやりづらいので、  .. (1)
マイブームの会議メモを作りました。.. (2)

[会議メモのURL]

deli projectとの2画面を画面共有するのはやりにくい部分あるかもしれませんが、
とりあえずやってみたいです! ..(3)

ポイント

(1) 会議メモがあると良い理由を一行で書く。

長いと読みづらいため。提案する相手によって詳細度は変えても良いと思います。

(2) 提案する人が会議メモのたたき台を作る。

理想的には、会議を招集する人が会議メモ、アジェンダ、構成全部つくるのが一番早いと思います。
しかし、登録された会議に対して
「アジェンダ作ってもらっていいですか?」
よりも
「会議メモとアジェンダ作りました!XXとYYはわからないので追記お願いしたいです!」
といった一緒に会議スタイルを作り上げていく方が現実的には効率よいのではないかなと私は思います。

(3) とりあえず一回!

「習うより慣れよ」
実際に会議メモを使って会議してみて、よかったら継続、合わなかったらやめれば良い。 そのくらいライトに提案した方が、提案された側も「とりあえずなら..」ってなる気がします。

deliでは今後も継続してGoogle ドキュメントを使用して会議をすることになりました。

まとめ

ページ分けなし設定ステキなのでぜひ広めて欲しい..!
Google ドキュメントの使用の提案方法が参考になれば幸いです!


是非読者になってください


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

ECR拡張スキャンでRailsアプリを診断した際の脆弱性警告(偽陽性)への対策

皆様こんにちは、サーバーサイドエンジニアの草分です。 最近ポケモン最新作を買ってしまったのでひたすら野原でボールを投げ続ける日々を送っています。

さて本題に入りましょう。

Amazon ECRには、pushしたコンテナイメージへのイメージスキャン(脆弱性診断)機能があります。

Image scanning - Amazon ECR

メドピアではこれを利用して全社横断的にアプリの脆弱性の検知および可視化を行っています。

f:id:yuyakusawake:20220131191845p:plain
脆弱性が検知された場合の表示

この記事では、Railsアプリをイメージスキャンした際の【偽陽性】警告の問題と、その解決策について紹介いたします。

問題

ECRのイメージスキャンには「基本スキャン」「拡張スキャン」の2種類があり、この内の拡張スキャン(Enhanced scanning)では、Rubyのgemやnpmのパッケージなども診断の対象となります。
Gemfile.lockに脆弱性のあるライブラリの記載があった場合でも、拡張スキャンを使えば自動検知&一覧化することができます。

ただし現時点(2022/01)ではアプリが使用するGemfile.lockだけでなく、インストールしたgemのディレクトリまで無差別に診断してしまうようです。

f:id:yuyakusawake:20220130162016p:plain

Rubyのgemは.gemspecファイルの記載に従って動作するため、gemパッケージ内にGemfile.lockが含まれていても通常は使われることはありません。

しかしそんなことはお構いなしに脆弱性警告を発してくるため、使っているgemによっては身に覚えのない警告を大量に受け取ることになるでしょう。困りましたね。

Inspector側でなんとかして欲しいという思いもありますが、この問題に対して以下の対策を実施しました。

続きを読む

The Complete Guide to Rails Performance読書会をしました

こんにちは。メドピアのお手伝いをしています@willnetです。最近車を買いました。これまでペーパードライバーだったので自信を持って運転できるように運転の練習を頑張っています。

今日は社内読書分科会で読んだThe Complete Guide to Rails Performanceという本の話を書きたいと思います。社内読書分科会って何?という人はこちらのエントリを参考にしてください。

tech.medpeer.co.jp

The Complete Guide to Rails Performanceとは

タイトルの通り、Railsアプリケーションのパフォーマンスを向上させるための知識やテクニックについて書かれている書籍です。書いている人は Nate Berkopecさんで、パフォーマンスに関するコンサルやワークショップをしているようです。パフォーマンスに関するメールマガジン*1の発行もしています*2

目次

こちらのgistから引用します。

  • An Economist, A Physicist, and a Linguist Walk Into a Bar...
  • Little's Law (+ screencast)
  • The Business Case for Performance
  • Performance Testing (+ screencast)
  • Profiling (+ screencast)
  • Memory - How to Measure (+ screencast)
  • Rack Mini Profiler (+ screencast)
  • New Relic
  • Skylight
  • Optimizing the Front-end
  • Chrome Timeline (+ screencast)
  • The Optimal Head Tag (+ screencast)
  • Resource Hints (+ screencast)
  • Turbolinks and View-Over-The-Wire
  • Webfonts (+ screencast)
  • HTTP/2 (+ screencast)
  • JavaScript
  • HTTP Caching (+ screencast)
  • Memory Bloat (+ screencast)
  • Memory Leaks (+ screencast)
  • ActiveRecord
  • Background Jobs
  • Caching
  • Slimming Down Your Framework (+ screencast)
  • Exceptions as Flow Control
  • Webserver Choice
  • Idioms - writing faster Ruby
  • Streaming
  • ActionCable
  • CDNs (+ screencast)
  • Databases
  • JRuby
  • Memory Allocators
  • SSL (+ screencast)
  • Easy Mode Stack - "What stack should I choose?"
  • The Complete Checklist - a 75+ item checklist for Ruby/Rails apps.

良かったポイント

Railsでアプリケーションを開発するときに知っておくと良い知識や気をつけるべきポイントについて網羅的にまとめられています。特にRailsアプリケーションだけの話にとどまらず、フロントエンドやCDNなどのwebアプリケーションを構成する要素全体について記述されているのが良いところだと感じています。ユーザが感じるwebアプリケーションの速度はサーバサイドのレスポンス速度だけではなくレイテンシやフロントエンドも含めた総合的な体験であり、通常の構成のwebアプリケーションではアセット類を取得したりパース、実行する時間のほうがユーザが待つ時間の大きい比率を占めるのでまずそちらを見てボトルネックを見つけると良い、というのは言われるとそうですね、となるのですがRailsエンジニアとしてはまずスロークエリとかN+1とかに目がいってしまうのですよね…。

一定規模以上のRailsアプリケーションを運用していると、アプリケーションサーバやワーカのメモリが急に増える現象に遭遇することがあります。そうなったときになにが原因なのかよくわからないのでとりあえず再起動しよう、とpuma_worker_killersidekiq-worker-killerなどのツールを使ってお茶を濁すことも多いです。この本ではお茶を濁さずに、原因をメモリリークなのかメモリ肥大化なのかを切り分けるところから、具体的にどうやって調査、解決していったり良いのか方針を提示してくれています。

最近 メドピア内でjemallocを採用した記事が公開されましたが、この本ではjemallocとそれ以外のメモリアロケータ (tcmallocHoardについてベンチマークが取られておりjemalloc以外の選択肢について考えさせられました(とはいえ採用事例の多いjemalloc側に寄せておくのがいいのかな、というのが現時点での個人的な見解です)。

などなど。もちろんActiveRecordで効率的なクエリを書くのはどうしたらよいか、とかキャッシュの使い方など基本的なトピックも抑えています。洋書を事前に予習しておくというのがハードル高く大変でしたが、それを超えたメリットがある本です。

良くなかったポイント

良いことばかり書くと信憑性に欠けそうなので、良くなかったポイントについても書いておきます。

何度か改訂はされていますが初出が2016年なので、当時の状況と現在の状況を読み替えないといけない箇所があります。具体的にはHTTP/1.1が広く使わている前提になっていてjsを結合して配信しましょう、という文章があちこちにあります。2022年現在ではHTTP/2が広く使われており、jsを結合する必要は薄れているのでそこを意識しつつ読み進める必要があります。

あとはとても細かい点です。HTML, PDF, epub, mobi, txt など複数のフォーマットで本が読めるのは良いのですが、PDF, epub, mobiで読むと横に長いソースコードが全部掲載されていない(右側が切れてしまう)ことがあってtxtなどを参照しにいかないとソースコードを確認できなくてつらい、となりました。あとは編集ミスのような文章の構成(2章分の全然別のトピックが1章にまとめられてる)があるので注意が必要です。

まとめ

The Complete Guide to Rails Performanceの紹介でした。Railsエンジニアでパフォーマンスに興味のある人であれば広くおすすめできる本ですのでよければ読んでみてください!


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

*1: ここ から購読できます

*2:参考になる知見が多いので、僕はずっと購読しています

Ruby v3.1.0のSegmentation faultに遭遇した話

こんにちは。サーバーサイドエンジニアの三村です。

保険薬局と患者さまを繋ぐ「かかりつけ薬局」化支援アプリ kakariやその姉妹サービスである患者接点を資産化する診療予約システム かかりつけクリニック支援サービス kakari for Clinicの開発を担当しています。

目次

はじめに

kakariをRuby v3.1.0にアップグレードする作業をしていたところSegmentation faultに遭遇したので、bugs.ruby への報告や再現コード作成などの経緯をまとめました。

賢い方法では無い部分も多々ありますが、温かい目で見守ってください。

bugs.rubyのissueはこちらです。

bugs.ruby-lang.org

f:id:t_mimura:20220119185723p:plain
bugs.ruby issue

※ Ruby本体の実装の話は出てきません。

現象

kakariでRuby v3.1にアップデートをしたら、CircleCIで実行しているRSpecでたまにSegmentation faultが発生するようになりました。

f:id:t_mimura:20220119185413p:plain
CircleCIログ

以下のような状態でした。

  • ActiveDecoratorの内部処理で発生
  • 複数回発生
  • エラー箇所は1箇所ではない
  • 再実行すると直る

bugs.rubyに報告

何はともあれ、Segmentation faultはRubyのバグなので報告します。 が、原因分からず再現コードも全然整理出来ていなかったので報告を躊躇いました。 そんなときに便利なruby-jp Slack

f:id:t_mimura:20220117233548p:plain
ruby-jp Slackでの相談の様子

温かく受け入れてくれそうだったので、安心してissueを作成することが出来ました。https://bugs.ruby-lang.org/issues/18489

issue起票当時は再現コードを整理する余裕がなかったので少しでも情報量を多くしようと思い、遭遇したSegmentation faultのログを複数個添付しました。

原因究明までの道のり

環境依存の問題かどうかを切り分け

これまでCircleCIでのみ再現していたので別の環境でも発生するのかをまず切り分けました。

ローカル開発環境(Docker Desktop for Mac)で複数回RSpecを全件実行し、無事に?再現することが確認出来ました。 Docker Image( cimg/ruby:3.1.0-browsers を利用 )依存ならまだしもCircleCI環境依存だった場合は原因究明までの試行錯誤が恐ろしく大変だったので一安心。

エラー発生ファイルの特定

特定のrequest specで再現することがすぐに判明出来ました。 が、このrequest specを単体で何回も実行しても再現しないことからファイルの読み込み順依存の問題であることも同時に分かり悲しみ。(ファイルの読み込み順問題よくありますよね)

specの実行順をランダムから定義順に変更

specは config.order = :random が指定されランダム順に実行されるようになっていたので、これを defined に変更し読み込み順を定義順にしました。 この状態でも本問題が再現することが確認できたのはかなりラッキーでした。 「randomの場合でのみたまに遭遇」みたいな状態だったらseed固定などもう一工夫必要になり面倒で諦めていたかもしれません。

因果関係のあるテストを特定

ここからは地道な試行錯誤の繰り返しです。

以下のspec群から本問題に関係のあるものを特定していきます。

$ tree -L 1 spec/
spec/
├── controllers
├── decorators
├── helpers
├── lib
├── mailers
├── models
├── push_notifiers
├── requests
├── serializers
├── system
├── uploaders
├── validators
└── workers

まずは試行時間の短縮の為に重たいspecを除外を試みました。 特にテスト数の多いmodelsと一つ一つが重たいsystemsを除外しました。 幸いこれらを除外しても変わらずSegmentation faultは再現できたのもラッキー。

脱Docker Desktop for Mac

この後もひたすらspecを実行しまくることが想像できたので少しでもspec実行時間を短くするように先に工夫しました。

kakariではDocker Desktop for Macを利用してローカル開発環境を構築しています。 が、これは色々な要因で重いことが有名ですね(詳細は割愛)。

ということで、脱Docker Desktop for Macを試みました*1。 (恒例の)mysql2のbuildでエラーになるなどちょいハマりポイントはあったものの、すんなり対応出来ました。 真面目に計測したわけではないですが、1.5~2倍くらい早くなった気がします。(かなりうろ覚えなので気になる方はご自身でお確かめください。)

MySQL -> SQLiteに変更

次にMySQLからSQLiteに変更をしました。

本事象は十中八九データベースは無関係だろうと予想していました。 MySQLが重たいわけではないですがSQLiteにすることでインメモリーなデータベースを利用することができる ( database: ':memory:' こんなやつ ) のでspecの並列実行が容易になりました。

基本的にDBの差異はActiveRecordが吸収しているのでadapterを切り替えるだけで済みました。 一部、外部キー制約やindex周りの挙動の違いはあったものの取り上げるほどのものはありませんでした。 (というか本事象と無関係だろうと思い深く考えずコメントアウトしたりして対応していました。)

↓はイメージですが、こんな感じの頭の悪い方法でspecを並列実行することが出来ました。

f:id:t_mimura:20220117234303p:plain
spec並列実行の様子
※ iTerm2の画面分割と「Broadcast Input to All Panes in Current Tab」を利用

再現コードの特定

上記の工夫のおかげもあり、因果関係のあるファイルを数個に特定することができたのでミニマムな再現コードの調査に切り替えました。 特に「request specでのみ再現」という状況が色々と面倒だったので何とかシンプルなRubyスクリプトコードを書けないかを模索。

数行のRubyスクリプトコードで再現したりしなかったりする状態までたどり着いたものの、再現有無の条件が全く分かりませんでした。 が、唐突に「GCか?」と思いついたため試しに GC.start を差し込んでみたところめでたく再現しました。

ここから先は簡単で、ActiveDecoratorを利用して書かれた再現コードをplainなRubyコードに変換するだけです。 ActiveDecoratorのコードは予め目を通していたお陰でここは数分で出来ました。

その結果出来た再現コードがこちらです。

M = Module.new
Object.new.extend(M)
GC.start
M.include(Module.new)

二日くらい費やした結果、美しく短い再現コードを作れた時は若干の感動がありました。

再現コード報告

早速「再現コード作れたよ」と追加報告です。

「ActiveDecoratorでSegmentation fault発生」という状況から一気にシンプルな再現コードに飛躍してしまったので「ActiveDecoratorをこんな感じに使ったら再現するよ」というのを添えてコメントしたのは我ながら親切ポイントです。

そして、再現コードをコメントしてから僅か2時間足らずでn0kadaさんが修正PRを作成してくれました。(流石だ)

修正確認

その後、issue上で 「Does https://github.com/ruby/ruby/pull/5455 fix it?」と聞かれてしまったので確認するしかありません。

Rubyをcloneして自前でビルドするのは初めてだったので不安がありましたが、実際にはruby/rubyのREADME手順通りにコマンド打つだけで大きなハマりどころもなくすんなり出来ました。 強いて言えばopensslのパス設定が必要だったかな程度です。あまり覚えていないくらい些細な問題でした。

上記の「脱Docker Desktop for mac」をしておいたのも地味に良かったです。

ruby-jpで相談していたところ、 k0kubunさんのブログ を紹介してもらいとても参考になりました(ありがとうございます!!) ※ 特に「ビルドしたrubyをrbenvから使うには」の所は便利

というわけで、大した苦労もなく無事に修正確認できました。

work around

無事に修正PRもマージされたとは言え流石にheadなrubyコードを利用するわけにはいかないので別途回避する方法を模索します。

これに関しては再現コードの特定が既に出来ているため簡単な話で、以下のような(不適切な形で)ActiveDecoratorを利用している箇所の修正するだけで済みました。

let!(:user) do
-  build(:user).extend UserDecorator
+  ActiveDecorator::Decorator.instance.decorate(build(:user))
end

本事象が発生していたのはspecのみで、実際のプロダクトコードでは同様の事象はなさそうでした。

まとめ

  • Rubyを触り始めて6年近く経ちますが、初めて Ruby本体 にまともな貢献が出来たかなと実感できとても良い経験でした。
  • ruby-jp Slackで気軽に相談できたのがとても有り難かったです。感謝感謝です。
  • 自分のコミットではないにしろ、ほぼ同等のものが「テストコード」としてRubyに入ったの嬉しい。
  • 褒められたの嬉しい。
    f:id:t_mimura:20220117235023p:plain
    ruby-jp Slackで褒められた様子

おまけ

無事にkakariはRuby v3.1.0にアップグレードすることができました。 本件は色々とありましたが、それ以外に必要な対応は殆どなくRubyの後方互換性の高さに感謝です。

以上です。 もし「Segmentation faultに遭遇して困った」なんて時に本記事が参考になれば幸いです。


メドピアでは一緒に働く仲間を募集しています。 ご応募をお待ちしております!

■募集ポジションはこちら

https://medpeer.co.jp/recruit/entry/

■開発環境はこちら

https://medpeer.co.jp/recruit/workplace/development.html

*1:今回の調査中だけの話です