ジモティーエンジニアのビジネスとの関わり

お久しぶりです。 バックエンドのチームで活動している阿部です。

前回投稿からおよそ2年、前回の時にはAndroidエンジニアとして活動していましたが、1年ほど前にバックエンドのチームへコンバートし現在ではRuby on Rails を中心としてバックエンドの領域で活動しています。

思えば、iOSエンジニアとして入社してさまざまな領域での経験をさせてもらえていますね。

今回は、少し毛色を変えてジモティーエンジニアの現在のビジネスとの関わりについてお話ししていきたいと思います。

こちらの記事でビジネスと近い距離で要件や仕様に関われるというお話をしていました。

現在ではエンジニアもビジネス側としてプロダクトの成長のために企画のフェーズから担当するようになりました。

その様子を自分の経験談を交えながら話していければと思います。

エンジニアとビジネスの関わり方

先で記載した通り、現在ジモティーのエンジニアも企画フェーズからプロジェクトに入りプロダクトをどのような方向に開発・変更していくべきか、分析・議論し日々業務にあたっています。

以前の記事で弊社の開発フローは大きく次のような流れがあるとお伝えしました。

  1. ビジネス要件定義
  2. システム要件定義
  3. 開発
  4. テスト
  5. リリース

また、 分析のデータ出しはエンジニアの得意分野なので、ビジネス要件定義からエンジニアも関わっていることをお伝えしていました。

現在もそこは変わらないのですが、このビジネス要件も含めエンジニアが主担当として案件を進めていく様に変化してきました。

そのため、分析・議論だけでなく画面のワイヤーをFigmaで作成して、自分でディレクションを行い、実装するエンジニアも存在します。

このようにエンジニアの活躍できる場が広がっているのが現在のジモティーです。

ジモティーのビジネス領域

現在のジモティーではユーザーグロース(UG)とマネタイズでチームが分かれており、ここにエンジニアも半々で所属して各Gで企画から携わっています。

  • UG:サービス成長のため、ユーザーが使いやすいサービズにするためのグループ
  • マネタイズ:事業を推進するために売り上げを構築するために動いているグループ

私はマネタイズの方に所属しております。

マネタイズグループでは、ジモティーの売り上げ構成としては以下の4つになっておりそれぞれの売り上げを伸ばす施策の検討を行なっています。

  • 広告:アドネットワークを利用した広告をコンテンツ内に掲載。第三者配信型と自社広告型の2種類がある。
  • DB連携(成果報酬) :他メディアとデータを連携し送客。
  • 機能課金:投稿を目立たせる機能を販売
  • その他売上:ネット決済時に発生する手数料課金、ジモスポでの商品売上及び業務受託売上、HRでの応募課金等がある

売り上げ構成など詳しく知りたい方は決算説明資料にも記載されていますので参照ください。

この中で私はDB連携を担当しており、売り上げ向上のため日々試行錯誤中です。

私が担当していること

私が企画側も担当していることはお伝えしていますが、それだけではイメージしづらいと思いますので 具体的にどの様な働き方をしているかお伝えしようと思います。

ジモティーでは11カテゴリーの投稿が存在しております。

その中で5カテゴリーにおいて、様々な企業のデータを連携し、投稿を作成しております。

この連携のことを弊社では「DB連携」と呼んでいます。

この連携データのコンディションを確認し連携企業に対してできるだけ効果を返すために基盤の整理・ジモティーユーザーに人気のデータの特性を分析などを中心に私は業務にあたっています。

以下に普段の業務の一例を示しています。

業務 詳細
KPIのためのモニタリング基盤の作成 連携しているデータのコンディションを確認するためのモニタリングを整備してマーケティングでよく使われる指標であるimp*1 ,CT*2, CV*3をモニタリング可能にした
連携投稿の状況をモニタリング 実際にどんな連携データが存在しているのか、KPIに大きな変動はないのかをモニタリングする。悪化している場合は、各部署と連携しつつ調査を進める。
連携データ、CVデータ、ローデータを分析 連携データの中で実際にユーザーがCVしたデータから人気のデータの特性を調査し、 コンテンツの配置を検討する。
施策立案 分析したデータを元に課題になっている部分の仮説をたて、検証する。その中で効果がありそうなものに優先度を振り分ける
例) xxxカテゴリの連携投稿はimp数は十分に出ているはずだが、CTRが低いのでimpされてもユーザーマッチしない連携投稿が閲覧されやすくなっているのではないか?
要件定義 実際に課題は何か?システムで何を実現したいのか?を定めてKPIの目標値を決める。画面は改修するのであればモックを作成
例) CV数を向上させることを目標として、閲覧されやすい箇所にユーザ志向に合わせた案件を配信する。これにより、CTR *4が上昇することが見込まれるため、結果としてCV数向上を実現できる。実際に実施するかどうかは費用対効果を見積もってROIから判断する。
設計・システム開発 実際に施策を実現するためにシステムをどう改修すればいいのか検討・実装(開発者の皆様には馴染み深いフェーズ)
施策振り返り リリースしたロジックの効果を確認し、思った効果が出ていなければ切り戻し、原因を調査。主に作成したモニタリング基盤でKPIを確認する

ざっくりと上記を繰り返しながら連携してくださっている企業へ効果を返せるように改善を進めています。

面白さ or 苦労した

ビジネス側の企画から参加することで、今までのエンジニアリングとは異なるやりがいもみえるようになってきました。

  • 実際に売り上げに直結することで今まで以上にメディアのコンディションに敏感になる
  • 実際に自分が要件定義から実装まで上流から下流まで担当できる
    • スピード感のあるPCDAを回すことでき、企画に必要なスキルをエンジニアでも身につけることができる
    • ドメイン知識が深まることで設計の方向性が明確になり、エンジニアスキルの方も同時に成長できる
    • 今まで諦められていた機能もエンジニアが主導だからこそ、さっと実装してプロダクトの成長に貢献できる

一方で、エンジニアリングという領域から外に出ることでその分苦労したこともあります。

  • ビジネス知識0の状態からチャレンジ
    • そもそも売り上げを上げることをKGIとするときに、何に対して効果がある案件が良さそうなのか判断がつかないため効果がある案件を進めることができない
      • ドメイン知識が豊富な方に壁打ちをお願いすることで思考が整理されていき、自走することができるようになってきました。
  • まだまだ課題がたくさんある領域だった
    • 広告業界でよく目にするCTR、CVRをモニタリングできない状態だったので施策の効果がわかりづらい状態でした。実際に設計から入りモニタリング可能な状態にするという面白さを経験できました。

この苦労も一人ではなく伴走してくださったり、サポートしてくださる環境があったからこそ成長を感じられています。

最後に

このようにビジネス側にも密接に関われるエンジニアが多数在籍しており、日々プロダクトを成長させて行くために領域関係なく試行錯誤を繰り返しています。

もし、エンジニアの方でもビジネスと近い距離で仕事をしてみたいと思ってくださったり、熱量があるエンジニアを募集しています!

一緒に課題を解決してくれる仲間としてジョインしてくださる方を待っています!

また、自分が担当しているDB連携領域でも随時連携してくださる企業を募集しております。

直近3ヶ月でも興味を持っていただいていた企業様とも連携を行い、連携データを随時配信しております。

もし興味をお持ちの企業様がいらっしゃいましたら、ご連絡お待ちしております。


弊社では一緒にプロダクトを改善していただける仲間を探しています!

こちらでお気軽にお声がけください!

ネイティブアプリエンジニアの採用って難しいですよね。。。

ジモティーのウェブチームについてお話ししたいです

*1:Impressionの略で広告が表示された回数。ここではリスト形式で表示された回数としている

*2:Click Throughの略。広告が何回クリックされたかの回数

*3:Conversionの略で、コンバージョンした数。連携先へ送客できた数としている

*4:「Click Through Rate」の略。ユーザーに広告が表示された回数(impression数)のうち、広告がクリックされた回数の割合のこと。

Terraformプロジェクトの構造を考える

こんにちは! ジモティーにてインフラ開発・運用を担当している斎藤です。

ジモティーでは主にAWSを活用してシステムを作っているのですが、分析基盤としてBigQueryを利用するなど、一部の用途でGoogle Cloudも活用してきました。

今回、BigQueryにリアルタイムでログを蓄積していく必要があり、APIサーバなどのコンピューティングリソースもGoogle Cloudに構築する必要性が出てきました。

Google Cloudでインフラを管理するなら将来的な拡張も見据えてやはりTerraformか、、、? という考えに至り、実際にTerraformプロジェクトを1から立ち上げてみましたので、そのときに考えたことを共有したいと思います。

Terraformってなに?

パブリッククラウドのリソース(サーバ、ネットワーク、ストレージ等々)をコードで管理するためのIaCツールです。

Hashicorp社製で、HCLという独自言語によってインフラを宣言することができます。

例えば以下のような記述をすると、Google Cloud上にCloud Storageバケットをひとつ定義できます。

resource "google_storage_bucket" "static_contents_bucket" {
  name                        = "jmty-static-contents"
  location                    = "ap-northeast1"
  uniform_bucket_level_access = true
}

IaCツールにおいてはAWSだとCloud Formationが有名ですね。

最近はCloud Formationをより抽象化してより開発者がコーディングに集中できるようなAWS CDK(Cloud Development Kit)というものもあります。

TerraformがAWSのそれらと違うのは、プロバイダというライブラリを入手することで多種多様なインフラをコード化対象にできる点です。

AWS、Azure、Google Cloudなどの主要パブリッククラウドだけでなく、GitHubやHerokuなどのSaaS、PaaSに対してもプロバイダが提供されています。

提供されているプロバイダはこちらから一覧することが可能です。

プロジェクト構造

さて、Terraformでインフラを管理することを決めたのはいいのですが、まず何をすればいいのか。。。

Terraformプロジェクトの構成の仕方は基本的に自由です。拡張子を .tf にさえしておけば、どこでどんなリソースを定義することもできます。

ただしプラクティスは既に無数に存在します。

なんならGoogle自身がTerraformでインフラを管理する際のプラクティスも提供してくれています。

重要な点は一つのディレクトリが「モジュール」として扱われる点です。

基本的にデプロイはモジュール単位で行われ、モジュールの中で別のモジュールを呼び出すこともできます。

このような特徴から、Terraformプロジェクトでは以下のようなプロジェクト構造を採用しました。

結果、プロジェクトは大きくわけて modules ディレクトリと environments ディレクトリを持つ形に落ち着きました。

.
├── README.md
├── environments → ★各環境ごとに設定値を格納したファイル群
│   ├── global → ●環境間共有リソース
│   │   ├── common
|   |   ...
│   ├── prod → ●本番環境
│   │   ├── api_server
|   |   ...
│   └── stg → ●検証環境
│       ├── api_server
|       ...
└── modules → ★各システムごとのリソース定義ファイル群(モジュール)
    ├── api_server
    │   ├── enable_apis.tf
    |   ...
   ...

以降で modules ディレクトリと environments ディレクトリについて説明します。

modulesディレクトリ

modules ディレクトリには各環境を構成するシステム単位毎に、Google Cloudリソース群がモジュールとして定義されています。

例えば api_server モジュールにはAPIサーバとそれが動くために必要なインフラ、 management_console モジュールには管理画面のインフラを定義するためのリソース群が定義されています。

木構造で表すと、以下のような構造になっています。

modules/
├── api_server
│   ├── enable_apis.tf
│   ├── main.tf
│   ├── outputs.tf
│   ├── provider.tf
│   └── variables.tf
├── management_console
│   ├── enable_apis.tf
│   ├── main.tf
│   ├── outputs.tf
│   ├── provider.tf
│   └── variables.tf
...
└── networking
    ├── enable_apis.tf
    ├── main.tf
    ├── outputs.tf
    ├── provider.tf
    └── variables.tf

各モジュールは以下のようなファイル群で構成するようにしました。(各モジュールによっては、利用しないファイルも存在します)

  • enable_apis.tf
    • モジュールの実行に必要なGoogle CloudのAPIの有効化を行う
  • main.tf
    • モジュールの実行によって作成したいGoogle Cloudリソースを定義する
  • outputs.tf
    • モジュールの出力を定義する。モジュール実行の戻り値(出力インタフェース)として機能する
  • provider.tf
    • モジュールで利用するTerraformプロバイダを宣言する
  • variables.tf
    • モジュールで利用する変数を定義する。モジュール実行の引数(入力インタフェース)として機能する

environmentsディレクトリ

modules ディレクトリでは環境固有の設定値を引数にしたモジュールを定義していました。environments ディレクトリでは modules で定義したモジュールに対して、各環境の設定値を入力として与えモジュールをインスタンス化します。

結果としてGoogle Cloudにリソースをデプロイすることができます。

stg 環境、つまり検証環境を例にとると以下のような構成になっており、各モジュール名のディレクトリで実際にモジュールがインスタンス化されるようになっています。

environments/stg/
├── api_server
│   ├── backend.tf
│   ├── data.tf
│   ├── main.tf
│   ├── outputs.tf
│   ├── terraform.tfvars
│   └── variables.tf
├── common
│   ├── backend.tf
│   ├── outputs.tf
│   ├── terraform.tfvars
│   └── variables.tf
├── management_console
│   ├── backend.tf
│   ├── data.tf
│   ├── main.tf
│   ├── outputs.tf
│   ├── terraform.tfvars
│   └── variables.tf
...
└── networking
    ├── backend.tf
    ├── data.tf
    ├── main.tf
    └── outputs.tf

各モジュールは以下のような構成を取るようにしました。

  • backend.tf
    • Terraformステートを格納するTerraformバックエンドを宣言する
  • data.tf
    • 依存する別モジュールを宣言する。例えばサーバをデプロイするためにnetwork名が必要になるため、 networking モジュールに依存するなど。ここで定義したモジュールの出力を使って main.tf でさらにインフラを定義する
  • main.tf
    • モジュールをインスタンス化する宣言をする
  • outputs.tf
    • モジュールの出力を all というキーに全て詰め込む宣言をする。これにより外部モジュール側の data.tf で利用宣言すると、もれなくこのモジュールの出力を利用させることができる
  • terraform.tfvars
    • variables.tf に実際に渡す値を書く
  • variables.tf
    • モジュールで利用されている変数を定義しておく
      • モジュール側と環境側で二重管理になってしまっているが、モジュール側の variables.tf はあくまで環境に依存せず必須にしたい変数を必須宣言しておき、環境によっては任意にしたい変数にはデフォルト値を付与して任意宣言としてある。これにより、モジュール側は必須パラメータだけを最低限定義すればよく、環境によるチューニングは任意パラメータで実施することが可能になる

commonモジュール

各環境には modules ディレクトリにはないモジュールである common モジュールが定義されています。

これは特殊なモジュールで main.tf を持たないモジュールです。

つまり common モジュールをインスタンス化することによってGoogle Cloud上にリソースがデプロイされることはなく、Terraformステートだけがデプロイされることになります。

commonモジュールの役割は、各システム間で値を共有することです。

commonモジュールの出力にあらかじめ環境内の各システム間で共有したい値(環境名など)をあたえておきます。

これにより各モジュールは common モジュールに依存すればそれらにアクセスすることができるようになり、先の役割を全うするようになっています。

global環境

environments ディレクトリには global 環境が定義されています。

これは各環境に直接属さない、環境間共有のリソースを定義する環境として利用されます。

例えばIAMを管理する project_members モジュールなどがこの環境でインスタンス化されます。

構成自体は他の環境と全く同様です。

最終的なデプロイ方法

結果として、以下のように modulesenvironments が宣言され、Google Cloudリソースが生成されることになります。

実際に terraform apply を実行するのはデプロイしたい環境のシステム単位ごとに行うようにします。

まとめ

この記事ではTerraformを使ってプロジェクトを構成する際に弊社で考えたことを書きました。

以下が肝だったのではないかと考えていますが、この考え方はデプロイ先がGoogle Cloudに限らず、適用できる考え方かと思います。

これからTerraformプロジェクトを立ち上げようとする方の参考になれば幸いです。

  • モジュールには環境固有の設定値を入れないことで、環境の複製を容易に、かつ環境差異が生じにくいようにした
  • モジュールの入力(variable) と出力(output) を意識しモジュールに対するインタフェースと捉えることで依存関係を明確にし、かつ一方向にのみ依存するように整理した

今回の共有は以上です。今後もテックブログを定期的に更新し、ジモティーの開発事例やプラクティスを共有していきたいと思います。

次回もまたお楽しみに!


弊社では一緒にプロダクトを改善していただける仲間を探しています!

こちらでお気軽にお声がけください!

ネイティブアプリエンジニアの採用って難しいですよね。。。

ジモティーのウェブチームについてお話ししたいです

Androidアプリのマルチモジュール化

はじめに

初めまして、ジモティーで Android アプリを担当している谷です。

最近マイクラにハマってしまい、やることが無限すぎて困っています。

今回は弊社の Android アプリをマルチモジュール化したのでそのお話をしたいと思います。

導入の背景

まずは弊社の Android アプリの現状の構成について説明します。

弊社の Android アプリは MVVM + CleanArchitecture を取り入れ、3層のレイヤードアーキテクチャで構成しています。

  • Presentation 層
  • Domain 層
  • Data 層 (Infra層)

上記を1つのモジュール内で管理しており、各層はパッケージで切り分けているため、参照の方向に強制力はありませんでした。

なので、実装担当者が参照の方向に気をつけながら実装する必要があり、本来の目的以外のところに注意を払う必要があり、厳しいさを感じてました。

そこで、マルチモジュールを取り入れることで、アーキテクチャをモジュールで表現することができるようになると考えて、上記課題を解決できるのではないかと思い導入を決めました。

また、各モジュールの依存関係を整理していくことで、ビルド時間の短縮の短縮も見込める可能性があるため、その点でも開発生産性の向上に期待できそうというのも決め手の一つとなりました。

分割方針

長い歴史のあるアプリで、コードベースも巨大なため、1回で理想的な形に変更できないと判断しました。

そこで、以下の点を考慮して方針を決めました。

  • 学習コストを低くする
  • 今後より改善しやすい構造であること

特に 学習コストを低くする という点を意識するために、現状のアーキテクチャ構造に近くなるようにモジュールを分割する形としました。

イメージ図

パッケージ構成

最終的なパッケージ構成は以下です。

モジュールには * をつけています。

(data パッケージ内の entity は CleanArchitecture 上の entity ではなく、 Gson によるレスポンスのマッピングを行うクラスを格納するモジュールとしての命名となっています。命名ミスっているので本当は変更したいところですが。。。)

├── app*
├── data
│   ├── repositoryimpl*
│   ├── data*
│   ├── entity*
│   ├── mapper*
│   ├── network*
│   └── database*
├── domain
│   ├── repository*
│   ├── model*
│   └── usecase*
└── utility*

解説

全てを説明するのは難しいため、ポイント部分の解説です。

  • 基本構成は app / domain / data としました。

  • app モジュールには Application クラスが入っており、全てのモジュールを参照する形になっています。

    • 本来、Application クラスを分離するべきですが、Application クラスを一時的なデータの保存場所として使っており、修正するには時間がかかりそうだったためこの形にしました。
  • utility モジュールは全モジュールが共通で使う、以下のようなクラスを格納しています。

    • util
    • helper
    • constant (定数などを定義しているクラス)

マルチモジュール化の結果

  • ⭕️アーキテクチャを仕組みで表現できるようになった

    • 適切な依存関係を設定することができるようになったため、アーキテクチャの強制力を働かせて開発を進めることができるようになり、実装者が意識しないといけない部分が減ったため、開発生産性が向上した。
  • ❌一方で、ビルド時間の短縮に対しては効果が薄かった。

    • フルビルド時のビルド時間が若干増加してしまいました。

工夫した点

  • マルチモジュール化によりどのくらい改善が図れたかを調べるため、gradle-profilerを使用して計測を行うようにしました。

    • マルチモジュール修正頻度よりも多く計測を行う必要はなさそうと判断したため、計測頻度は1週間に1回行うようにしました。
  • マルチモジュールの移行率を計測することでモチベーションを上げるようにしました。

    • 最初はちょっとずつそれぞれのモジュールへ移行していたのですが、数が膨大だったため、少しずつでも移行されている実感を得られないと続かないだろうと考えて計測を行うようにしました。 (チームの方にも手伝っていただき、移行が加速しました!)
  • ConventionPlugin導入

    • モジュールを作成する中で同じプラグインやライブラリを記述していく必要があり、それぞれのライブラリの管理に関してのオーバーヘッドが発生することが想像されたので、共通で使える形にしたいと考えて導入しました。
  • モジュールの作成手順の作成

    • 今回のマルチモジュール化では、学習コストを低くするというのも大事な要素だったので、モジュールの手順書を作成して、チームに展開後、作成した手順書を使ってハンズオンを行いました。

残課題

  • Application クラスを application モジュールとして切り出すことができなかったので分離を進める。
    • 画面上で生成したインスタンスの一時保存クラスとして Application クラスを使用してしまっているため、修正工数がかなりかかってしまうのと、安全にマルチモジュール化が行えないと判断して、app モジュールはそのままにするという形にしました。

最後に

今回はジモティーの Android アプリのマルチモジュール化について紹介させていただきました!

移行したばかりでまだまだこれからという状況ですが、日々改善していければと思います!


弊社では一緒にプロダクトを改善していただける仲間を探しています!

こちらでお気軽にお声がけください!

ネイティブアプリエンジニアの採用って難しいですよね。。。

ジモティーのウェブチームについてお話ししたいです

【iOS】広告の事前読み込みチャレンジ

はじめに

どうも。

ジモティーでiOSアプリ開発チームのマネージャーをやらせてもらっている、ていです。

前回の記事で弊社のiOSアプリにアーキテクチャを導入した過程の計画編をお届けしたので順番的には導入編をお届けするのが筋かと思いますが、気分が乗らないので少し前に広告の取得表示改善に取り組んだ話をしようと思います。

広告の課題

弊社の広告は所謂アドネットワークから配信される広告をSDKを通じてアプリに表示させるよくある手法を取ってるのですが、課題はたくさんあります。

私自身ジモティーに入社して7年目を迎えておりますが、この7年は広告(SDK)との戦いの歴史だと言っても過言ではないと自分では思ってます。

たくさんある課題の一つに広告の表示が遅く、ユーザさんが広告を目にする前にスクロールやページ遷移で広告表示画面から離れてしまうというものがありました。

そもそも見栄えが悪いのもありますし、弊社としても売上の機会損失につながっている状況です。

事前読み込み方式のチャレンジ

広告はリクエストを送った後、Biddingと呼ばれる入札が行われてその時点で収益効果の高い広告を返す仕組みになっている関係上リクエストからレスポンスまで一定の時間がかかってしまい、さらに(Admobの場合)カスタムイベントをたくさん設定しているとその分さらに時間がかかるということがあるので、どうしてもページ表示から広告表示までタイムラグが発生してしまいます。

弊社のアプリは特にそのタイムラグが大きく、レスポンスが返ってくるまでは広告表示枠が白板状態でぽっかりと空いてしまい非常に見栄えが悪い状態になっています。

そこで以下のような考え方で改修を行なってみることになりました。

1. 現在の広告取得後の表示タイミングで次に表示する予定の広告を取得しておいて、端末内に保存する

2. 次の広告表示タイミングでまず事前取得した広告を表示し、さらにその次に表示する予定の広告を取得しておいて端末内に保存する

3. こうしておけば初回リリース以降は(広告取得失敗がない限り)常にほぼ待ち時間0で広告を表示することが可能になる。

実際のソース(一部抜粋)

キャッシュクラス

class AdCacheHelper {
    private static var preloadAd: PreloadAd?

    static func getAdView() -> GADBannerView? {
        Self.checkAdGetTime()
        let localData = Self.preloadAd
        Self.removeAdView()
        return localData?.adView
    }

    static func setAdView(_ adView: GADBannerView) {
        Self.preloadAd = PreloadAd(createdAt: Date(), adView: adView)
    }

    // 取得から1時間経過した事前読み込み広告は破棄
    private static func checkAdGetTime() {
        if let preloadAd = self.preloadAd {
            let adLimitDate = Calendar.current.date(byAdding: .hour, value: 1, to: preloadAd.createdAt)!
            if adLimitDate < Date() {
                Self.removeAdView()
            }
        }
    }

    private static func removeAdView() {
        Self.preloadAd = nil
    }
}

struct PreloadAd {
    let createdAt: Date
    let adView: GADBannerView
}

呼び出しがわ

    public func bannerViewDidReceiveAd(_ bannerView: GADBannerView) {
        if let adCacheableViewController = self.rootViewController as? AdLoadable {
            switch self.loadStatus {
            // 事前取得表示済みの場合と再取得後はキャッシュ保存してリフレッシュ待機状態にする
            case .preLoaded, .reRequested:
                AdCacheHelper.setAdView(bannerView)
                self.loadStatus = .refreshing
            // 初回起動時 or 破棄されてる場合 or 異常として広告がない場合は取得した広告を表示して事前読み込み用の広告を再取得
            case .firstLoading:
                adCacheableViewController.loadAd(bannerView: bannerView, admobUnitId: self.admobUnitId)
                self.loadStatus = .reRequested
                self.requestAds()
            // リフレッシュはそのまま表示するのみ
            case .refreshing:
                adCacheableViewController.loadAd(bannerView: bannerView, admobUnitId: self.admobUnitId)
            }
        }
    }

※1 実装に関する指摘コメント歓迎です!

※2 ただしあまりに厳しい言葉だと実装者のメンタルに響きますのでお手柔らかにお願いします。

※3 実装に関してzaimさんのブログを勝手に参考にさせていただきました。

結果

結果としては見栄えの部分ではある程度改善されたのですが、数字上の効果はあまり改善が見られませんでした。

仮説としてはそもそも広告の取得が遅すぎて、事前読み込み完了前にページ遷移などで広告リクエストが破棄されてしまいいつまで経っても事前広告がキャッシュされないパターンが多いのでは?というものが有力そうです。

最後に

この経験を踏まえ今後は事前読み込み方式を一度に複数読み込んでおくとか、そもそもBiddingの速度を上げられないか?など多岐にわたる改善方法を検討して進めていく予定です。

広告を扱われていて速度改善で良い成果を挙げられた方などいらっしゃいましたら、是非お知恵をお貸しいただけると幸いです。

またはジョインしていただき一緒にジモティープロダクトを改善していただけるとよりハッピーなので、よろしくお願いします。


弊社では一緒にプロダクトを改善していただける仲間を探しています!

こちらでお気軽にお声がけください!

ネイティブアプリエンジニアの採用って難しいですよね。。。

ジモティーのウェブチームについてお話ししたいです

CodeBuild始めました

ジモティーでインフラとバックエンドを担当している鈴木です。最近は貝出汁ラーメンをよく食べてます。美味しい。

ジモティーにCodeBuildを導入しましたので、背景や工夫した点などを紹介します。

CodeBuildとは

AWS CodeBuildは、AWSが提供するフルマネージド型のビルドサービスです。このサービスを利用することで、ソースコードのビルド、テストの実行など、ビルドプロセスを自動化することができます。 サーバーレスなサービスであり、ビルド実行の度に新たな環境が自動的に作成されるため、ユーザーは事前にビルドサーバーを準備しておく必要がありません。

背景

ジモティーでは、Rails、Next.jsといったアプリケーションサーバーがECS Fargate上で稼働しており、FargateにデプロイするDockerイメージは、元々はJenkinsを使用してビルドしていました。

しかし、Jenkins上で同時に複数のビルドを行うと、メモリ不足に陥るケースがありました。

さらに、新たな問題として、RailsやNext.jsのコンテナをARMアーキテクチャに置き換えることを計画していました。これにはARM向けのイメージをビルドする必要がありますが、イメージのビルドは一般的にホスト環境のCPUアーキテクチャに依存します(※)。弊社のJenkinsサーバーはx86アーキテクチャのインスタンスで構築されているため、ARM向けのイメージをビルドすることができませんでした。

こういった一連の課題を解決するため、ジモティーではCodeBuildの導入を決定しました。これにより、ホスト環境に依存せずに、各アーキテクチャのイメージをビルドできるようになります。また、CodeBuildはビルドごとに独立した環境で実行されるため、ビルドの同時実行によるメモリ不足といったリソースの問題も解決できます。

※ Docker Buildxプラグインを使うことでマルチアーキテクチャでのビルドが可能ですが、今回はリソース問題も解決できるCodeBuildの導入を選択しました。

工夫した点

Jenkins環境でDockerイメージをビルドしていた際には、Dockerビルドの結果がキャッシュとしてローカルに保存され、次に同じイメージをビルドする際にビルド時間を短縮することができていました。

一方、AWS CodeBuildはビルドごとに新しい環境を作成し、ビルドが完了するとその環境は破棄されます。そのため、ビルド間でイメージキャッシュを共有することができません。

キャッシュによるビルド時間の短縮はデプロイプロセスの効率において重要だったため、ECRに保存している前回のイメージを --cache-from オプションで指定することでキャッシュを有効にしました。

# 前回のイメージをECRからpull
- docker pull XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/${REPOSITORY_NAME}:latest

# 前回のイメージを --cache-from オプションで指定
- docker build -t ${REPOSITORY_NAME} . --cache-from XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/${REPOSITORY_NAME}:latest 

この方法では、前回のイメージをECRからpullするため、通信のオーバーヘッドは発生しますが、このユースケースではビルド時間の短縮幅に比べるとはるかに小さいものだっため許容しました。

まとめ

  • ジモティーのアプリケーションサーバーはFargateで構築されており、デプロイするDockerイメージをJenkins上でビルドしていた
  • 以下の課題をCodeBuild導入により解決した
    • ビルドの同時実行によるリソースの枯渇
    • ビルドがホスト環境のCPUアーキテクチャに依存するためARM用のイメージビルドが難しい
  • CodeBuildではローカルのイメージキャッシュを使えないというデメリットがあるがECRからpullしたイメージをキャッシュ指定することで解決できた(pullの通信によるオーバーヘッドは発生する)

この記事では、ジモティーにおけるCodeBuild導入の背景やそのメリット、そして工夫した点について解説しました。CodeBuildを活用することで、様々なアーキテクチャに対応したビルド環境を効率的に構築することができます。今後もテックブログでジモティーの開発事例や取り組みをご紹介していきますので、どうぞお楽しみに!


弊社では一緒にプロダクトを改善していただける仲間を探しています!

こちらでお気軽にお声がけください!

ネイティブアプリエンジニアの採用って難しいですよね。。。

ジモティーのウェブチームについてお話ししたいです

openapi.ymlのコンフリクト解消術

ジモティーでサーバサイドとインフラを担当している熊谷です。

今回はエンジニアグループ内で問題視されていた、openapi.ymlファイル競合(コンフリクト)問題を解決した事例についてご紹介します。

ジモティーのAPI開発

ジモティーのAPI開発では、仕様の共通化を目的としてOpenAPI を導入しています。

OpenAPI は公式ドキュメントREST APIのためのAPI記述形式として説明されており、定義ファイルをYAMLやJSONで記述することができるものです。

定義ファイル編集時はOpenAPI の GUI 定義エディタとして提供されているStoplight Studio ( バージョン2.10.0 )を使用することで、仕様の把握にかかる時間を短縮し、直感的な記述を行えるようにしています。

課題

OpenAPIの定義はyamlを1ファイル用意し、gitで管理していました。

しかし同時に編集する開発メンバーが増えたことの影響で、ファイルのコンフリクトが頻繁に発生するようになってきました。

# 開発メンバーの声
Aさん
OpenAPI のファイルはコンフリクトしやすいのが課題ですね。。。

Bさん
openapiのymlのコンフリクト解消したら関係ないpathの定義が壊れてるの腹立つ、、

このような課題感から解決策検討を行いました。

問題解消のために試してだめだった方法

ウェブでopenapi.ymlファイルの分割方法を検索すると、ファイル分割によるコンフリクト解決案がいくつかヒットします。

解決案の1つとして、以下のようなフォルダ構成を用意し分割を試しました。

Path とSchemaを分割する

root
│  openapi.yaml
│  
├─build
│      openapi.yaml
├─paths
│      users.yaml
│      user_settings.yaml
│      
└─schemas
        User.yaml
        Post.yaml
        Error.yaml

OpenAPIの用語としてpaths とschemasは以下のように説明されています。参考

paths
パスはAPIが公開する/usersや/reports/summary/などのエンドポイントのことで、これらのパスを操作するためのHTTPメソッドと一緒に記述します。

schemas
API で使用されるデータ構造を記述します。

各ファイルが分割された状態だとStoplight Studioで仕様の確認が行えないため、swagger-cli を使うことで、分割したファイルを結合します。

※ 結合ファイルは、build/openapi.yaml を指定し生成しています。

分割されたopenapi.yml を直接編集することでAPIの仕様を記載する場合は、こちらの方法でコンフリクト問題を解決できます。

しかし前述の通りジモティーのAPI開発では、openapi.ymlの編集にStoplight Studioを使用しています。

Stoplight Studioはopenapi.ymlが分割された状態では一部正しく編集を行えないためファイルは結合する必要があり、結合したファイルはStoplight Studioで編集後にコンフリクトを防ぐために分割する必要があります。

ファイルを結合するツールはswagger-cli が用意されていますが、結合されたファイルを分割するツールは見つからず、準備に時間がかかる可能性もあったため他の方法も模索することにしました。

Path 順、Schema順にぞれぞれソートする

そもそもopenapi.ymlファイルがコンフリクトする原因はPathやSchemaの最終行付近で編集が集中するからであり、PathやSchemaをそれぞれソートすることができればコンフリクトを防げる可能性は高いです。

これを踏まえ、pre-commit を導入し、commit 直前にPathとSchemaをそれぞれ確認し、ソートする方法を試しました。

pre-commit はコミットごとにスクリプトを実行し、コード上の問題を自動で指摘できるようにするツールでgit commit 時にPath とSchemaをソートするために使用します。

大まかな処理の流れ

1. 開発者がopenapi.ymlファイルの内容を追加・編集する
2. 変更内容を git commit する
3. pre-commit により.git/pre-commit ファイルに記載の処理が実行される
4. .git/pre-commit の中でソート用の 処理ファイルを実行する ( ジモティーでは ruby ファイルでソートしている)
5. 処理ファイルの中でopenapi.ymlファイルに記載されている内容をPathごと、Schemaごとに格納し、ruby のsort メソッドで処理を行う 

この方法であればopenapi.ymlファイルを分割しないため、Stoplight Studioを使用しつつコンフリクトを抑えることができます。

似た名前のPath が同時に作成された場合にまたコンフリクトが発生する可能性もあるとは思いますが、コンフリクトは発生しなくなりました。

まとめ

今回はOpenAPIのymlファイル コンフリクト防止案検討についてご紹介しました。

openapi.ymlファイルのコンフリクト問題の解決策の一つとして、この記事が少しでも参考になれば幸いです。


弊社では一緒にプロダクトを改善していただける仲間を探しています!

こちらでお気軽にお声がけください!

ネイティブアプリエンジニアの採用って難しいですよね。。。

ジモティーのウェブチームについてお話ししたいです

Google Search Consoleでsitemap.xmlの検出URL件数が0件になる問題を解決した話

こんにちは、ジモティーエンジニアチームの山口です。

主にフロントエンド面を担当しております。

現在はジモティーWeb版のフロントをNext.jsに移行する開発を日々進めています。

今回は、フロントエンドとは直接関係ない話ですが、SEO対策の一環として行なっていた作業の中で遭遇した問題についてご紹介します。

問題の概要

ジモティーではGoogle Search Consoleから、Railsで生成したsitemap.xmlファイルとそのindexファイルをいくつか登録しています。

sitemap_index.xml
├── sitemap_1.xml
├── sitemap_2.xml
└── sitemap_3.xml

しかし、ある時から下記画像のように、読み込みには正常に成功するものの検出URL件数が0件となってしまうファイルが発生するようになりました。

Search Consoleから再読み込みをかけても変わらず0件のままであり続けたため、新規ページを作成してもサイトマップからインデックスさせられず、施策が行いにくい状況となってしまいました。

問題解消のために試してダメだったこと

このサイトマップが0件になる問題自体はネット上でも同様の症状が報告されており、その回避方法の情報が散見されました。

情報を元に試してみて改善に至らなかったことを4つ紹介します。

path名を短くしてみる

一定文字数以上(20文字以上?)のファイルやpathになると、この現象が発生する可能性があるらしいとのことで、試してみました。

弊社のsitemap.xmlは種類が多いため、50~100文字程度のpath名が設定されているものがほとんどでした。

これらをシンプルなよくあるsitemap.xmlなどの命名に短縮し、20文字以下ぐらいのpath名に収まるようにしました。

path名に特定の文字を使わない

Search Consoleに登録しているsitemap.xmlのpath内に"-"(ハイフン)を使うと、この問題が発生することがあるとの記事を見かけたため、シンプルにアルファベット以外の文字列が一切入らないpath名にしてみました。

生成していたxmlの形式が適切であるかを確認

sitemap.xmlのファイルの形式が間違っている可能性も考え、sitemap.xmlの形式をチェックできるツールにも通してみました。

残念ながらこれも問題なしという結果でした。

生成したxml内の不要な改行を削除

タグとタグの間など一部に改行が入っている場合にもこのバグを引き起こす可能性があるとの記事をみて試してみました。

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
...

<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
...

などにしてなるべく改行が入らないようにしてみましたがこれも効果がありませんでした。

問題解消に至った方法

ネットの情報で改善に至らず困っていたのですが、同じjmty.jpドメインで登録しているsitemap.xmlのうち、RailsからではなくWordpressから登録していたものだけは正常に読み込みできていることに気づき、そのファイルとの違いを比較することで原因に当たりをつけることができました。

弊社のsitemap.xmlはAWSのS3にアップロードして運用を行なっていたため、Google Search Consoleへの登録自体はhttps://jmty.jp/sitemap.xmlhttps://jmty.jp/sitemap_index.xml のURLでしていたものの、このURLにアクセスした際にはCloudFrontドメインのURL https://〇〇.cloudfront.net/sitemap.xmlhttps://〇〇.cloudfront.net/sitemap_index.xml などのURLへリダイレクトされるように設定されていました。

この設定自体はこれまで正常に機能していたため問題を引き起こす原因とは考えていませんでしたが、試しにこのリダイレクト設定を外して直接S3上のファイルを Google Search Consoleへ登録していたURLhttps://jmty.jp/sitemap.xmlhttps://jmty.jp/sitemap_index.xmlのURLでアクセスできるように設定したところ、検出URL件数が正常にカウントされるようになりました。

まとめ

今回は、SEO対策の一環として行なっていた作業の中で遭遇した問題とその解決策についてご紹介しました。

この問題は以前sitemapxmlの調整を担当していた前任者の方の時から発生しており半年ほど解決できずに匙を投げかけていた問題でしたが今回ようやく改善に至れて一安心しました。

ファイル自体に問題があるかのように見えて、実際は環境起因の問題だったということで原因が分からずに手詰まりになったときは、視野を広げる意識が大事だと理解させられる一件となりました。

この記事が同じ問題を抱えている方の手助けになれば幸いです。


弊社では一緒にプロダクトを改善していただける仲間を探しています!

こちらでお気軽にお声がけください!

ネイティブアプリエンジニアの採用って難しいですよね。。。

ジモティーのウェブチームについてお話ししたいです