コネヒト開発者ブログ

コネヒト開発者ブログ

GitHub Actions & ecspressoによるデプロイフロー構築

こんにちは、リードエンジニアの @dachi_023 です。今回はGitHub Actionsとecspressoでデプロイフローの構築をしたのでそれについて書いていきます。先に言っておくと簡単にセットアップできるし設定もシンプルなのでかなりおすすめです。

これまでのデプロイ

コネヒトではECS環境へのデプロイに silinternational/ecs-deploy を採用しています。CodeBuildもしくはTravis CI上からecs-deployを利用してECS環境にアプリケーションをデプロイする構成です。

CI/CDツールの乗り換え検討

これまでずっとTravis CIを利用してきました。しかし 料金体系の変更 があったり、CodeBuildでデプロイの改善 をしたり、アプリ開発ではBitriseを利用 し始めたりするなどTravis CIを選択する理由が減ってきました。それと、デプロイ起点が複数あることで各CI/CDサービスの挙動理解や、メンテ時の面倒さを生み出してしまっているかもなと今この記事を書きながら思いました。

そこで今回はGitHub Actionsが良いのでは、という判断をしました。ecspressoを導入したリポジトリでは乗り換え先の検証としてGitHub Actionsを使ってCIを実行しており、どうせやるなら1つのCI/CDサービスでやろうと思ったからです。これまでTravis CIで実行してきた処理はすべて移行可能ですし、CodeBuildで行っている各家庭の回線に依存しない手動デプロイも引き続き行うことができます。

あとはGitHubの各種イベントに連動させたり、HTTPリクエストで起動したりするのが簡単なのも良いです。機能面以外だと請求がGitHubにまとめられるというのも良いのではないでしょうか。(コード管理をGitHubでしている前提)

ECS設定をコード化したい

アプリケーションをECS環境にデプロイするという用途だけならecs-deployで十分です。しかし、サービス定義やタスク定義を管理する仕組みがないためアプリケーション側の都合でタスク定義を更新したい場合などにECSコンソールから更新をかけるかインフラチームへ依頼するという手順が必要でした。コード管理していないためPull Requestを使ったレビューを行うことができないという欠点もあります。

これらの課題を解決するため、ecs-deployをやめてecspressoを使ってみることにしました。

ecspresso

ecspressoはECSのサービス定義・タスク定義も更新可能で、その設定内容はJSONで管理できます。なのでリポジトリ内で管理して変更する際にはPull Requestを通してレビューすることも可能です。ecspressoがやってくれることや、その思想についての解説は以下の記事が非常に分かりやすかったです。

ecspresso advent calendar 2020 day 21 - やること、やらないこと

デプロイまでの手順

ここから先は既存のECS環境に対してecspressoを利用し設定ファイルの作成、デプロイフローの構築をするための手順になります。

ecspressoのインストール

Homebrew経由でインストール可能です。

$ brew install kayac/tap/ecspresso

定義ファイルを作成

以下コマンドを実行して設定ファイルを作成します。

$ ecspresso init \
  --region my-ecs-region \
  --cluster my-ecs-cluster \
  --service my-ecs-service \
  --config config.yaml

実行すると3つファイルが作成されるのでそれらをコミットしておきます。複数環境へのデプロイが想定される場合は ecspresso/{env}/ のような形でディレクトリを作成して格納しておくと管理しやすいです。

  • config.yaml : ecspressoの設定ファイル
  • ecs-service-def.json : サービス定義
  • ecs-task-def.json : タスク定義

Workflowの作成

デプロイフローなどは各開発チームによって手順が異なるのであくまで参考程度にですが、私のいる開発チームでは以下のような設定になっています。

# .github/workflows/push-main.yml
# main ブランチにマージされたら development 環境へデプロイする
name: Push main branch

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      IMAGE_TAG: latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: my-aws-region
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1
      - name: Build, tag, and push image to Amazon ECR
        env:
          DOCKER_BUILDKIT: 1
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: my-repository
        run: |
          docker build . -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
      - uses: kayac/ecspresso@v0
        with:
          version: v1.1.3
      - name: Deploy to Amazon ECS
        run: |
          ecspresso deploy --config .ecspresso/development/config.yaml
  1. 最新コードのチェックアウト
  2. ECRへのログイン
  3. イメージのビルド・プッシュ
  4. ecspressoを利用してECSタスク定義の更新・デプロイ

という順で実行されていきます。実行される処理のほとんどは既存のGitHub Actionを利用・組み合わせただけなので非常に簡単ですし、シンプルなのでメンテも楽になります。

より便利にするために

ここまででデプロイに必要な最低限を設定してきました。ここからは必要ならやってみてね、というものになります。

Slackへのデプロイ結果の通知

Slack通知用のGitHub Actionを入れます。ざっと調べただけでも3〜4個あるのですが今回は 8398a7/action-slack を使うことにしました。以下の設定をジョブの一番最後に入れます。

# .github/workflows/push-main.yml
- name: Notify slack of deployment result
  uses: 8398a7/action-slack@v3
  with:
    status: ${{ job.status }}
    # このへんはドキュメントを読みながらお好みで設定してください
    fields: repo,commit,ref,workflow,message,took
    # 誰がデプロイ作業をしているのかが分かるように GitHub ユーザー名を設定しています
    author_name: ${{ github.actor }}
    text: |
      Deployment has ${{
        (job.status == 'success' && 'succeeded') ||
        (job.status == 'failure' && 'failed') ||
        'cancelled'
      }} to my-app.
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
  if: always()

タスク定義の変更がなければ処理をスキップする

タスク定義にdiffが出てなかったらタスク定義の更新処理をスキップしたくなりました。ecspressoのドキュメントを読んでいたら --skip-task-definition --force-new-deployment という2つのオプションを見つけたのでこれをdiffがなければ付けるようにします*1。差分チェックは dorny/paths-filter というGitHub Actionを使っています。

# .github/workflows/push-main.yml
- name: Check if file has changed
  uses: dorny/paths-filter@v2
  id: changes
  with:
    filters: .github/file-filters.yml
# 〜 中略 〜
- name: Deploy to Amazon ECS
  run: |
    ecspresso deploy --config .ecspresso/development/config.yaml ${{
      (steps.changes.outputs.task-def == 'false' && '--skip-task-definition --force-new-deployment') || ''
    }}
# .github/file-filters.yml
task-def:
  - .ecspresso/development/ecs-task-def.json

さいごに

ECS Scheduled Taskの管理をecscheduleでGitOps化しました などを推進してくれている @laugh_k に協力してもらいながら無事デプロイできるようになりました。また、ECSのようなインフラエンジニア以外も触れる機会の多い部分のコード化は非常に重要だなということを学びました。

参考リンク

PR

エンジニア募集しています!

hrmos.co

この記事はShodo (https://shodo.ink) で執筆されました。

*1:ecspresso advent calendar 2020 day 4 - deploy を読むとデプロイ時の挙動に詳しくなれます。

React.lazy を使うと初回表示時のパフォーマンスが落ちた事例の紹介

こんにちは! フロントエンドエンジニアのもりやです。

今回はママリのアプリ内で使われている WebView に React.lazy を導入した結果、初回表示時のパフォーマンスが落ちてしまった事例を紹介します。

React.lazy を入れようと思った動機

ママリでは、アプリ内の一部の画面をA/Bテストしやすくするなどの目的で、リリースが容易な WebView を使っています。

ただ WebView の初回表示が遅いという課題があり、今回パフォーマンス改善に取り組んでいました。

その改善策の1つとして React.lazy を導入して効果を検証してみました。

React.lazy とは

詳しくは公式ドキュメントの コード分割 – React に書かれていますが、ざっくりいうとファイルを分割して遅延読み込みができる機能です。

ママリで使っている WebView は react-router でパスごとに表示するページを切り替えていますが、ビルド自体は1つのファイルで出力しています。 (正確には外部ライブラリ用にもう1つ出力しています)

そのため本来表示したいページ以外のデータも含めて全部ダウンロードしないと表示ができない状態でした。

ファイルを分割してダウンロード量を減らせば、初回ロード時のパフォーマンスが上がるのでは、という意図で導入してみました。

初回表示時のパフォーマンス計測方法

Google Chrome の DevTools の Performance タブで計測しました。

また以下の設定もしています。

  1. Fast 3G にする
    • 計測時のインターネット回線の混雑状況などによる影響を減らすため
    • ある程度遅い方が変化を確認しやすいため
  2. Disable Cache にチェックを入れる
    • 初回表示時時のパフォーマンスを改善したいので、キャッシュによる変化をなくすため

image.png

React.lazy 導入前

Before performance

bundle.js(と外部ライブラリをまとめた vendor.bundle.js ) の読み込みが特に時間がかかっているので、ここを削減しようと考えました。

今回 React.lazy を導入して bundle.js を複数のファイルに分割するようにしました。

React.lazy 導入後

結論から言うと、ロードにかかる時間がかえって増加してしまい、初回表示時のパフォーマンスが下がる結果になりました。

After performance

bundle.js 自体のサイズは 157KB51.4KB に削減され、新たに読み込まれるようになったチャンクデータも合計 14KB なので読み込む容量自体は減らすことに成功しています。

しかし bundle.jsvender.bundle.js の読み込みが終わってからチャンクデータの読み込みが開始されるので、その分遅くなってしまうという結果になってしまいました。

いったんパフォーマンスが落ちていることが確認できた時点で、Revertとリリースして React.lazy の対応を消しました。

考察

React.lazy を使った場合、必要な全てのスクリプトが読み込まれ評価された後に、チャンクデータが読み込まれるようです。

なので vendor.bundle.js の読み込み時間が変わらないと、チャンクしたデータの読み込み分遅くなってしまうという結果になりました。

そもそも vendor.bundle.js の読み込みにも時間がかかってパフォーマンスが低下しているので、ここの改善の方が重要になりそうです。

また vendor.bundle.js が十分に小さければ、全体のロード時間が短縮され React.lazy による改善効果があったかもしれません。 なるべく外部ライブラリを導入しない、というのもパフォーマンスに有効かもしれませんね。

まとめ

React.lazy を入れることでロード時間が削減できる時間が、どれだけパフォーマンスに良い影響を及ぼすのかを考えて導入する必要がある、という学びを得ました。

今回の外部ライブラリのサイズによる影響のように、環境によって React.lazy を入れることによる影響は変わってきます。 一概に導入する・しない方が良いとは言えないのが難しいところですが、面白ところでもあると思いました。

最後に、コネヒトではフロントエンドを一緒に改善していただけるエンジニアを募集しています!

hrmos.co

付録: ChankLoadError

パフォーマンスと直接関係はないのですが、React.lazy を使うように変更してリリースすると ChunkLoadError というエラーが発生するようになりました。 文字通り React.lazy を使って分割したファイルが読み込めないという感じのようです。

件数もユーザー数に対する割合としては非常に少なく、再現は難しい状況でした。 一応、ブラウザの DevTools でネットワークを切断したりすると再現はできるのですが、なぜユーザーの環境で起きているのかは不明でした。

検索しても自動でリロードする対応をすると良いよ、ぐらいの記事しか見つかりませんでした。 結局 React.lazy を使わないように戻したので、解決はしていません。

想像ではありますが、クライアントのネットワーク環境とかも影響あるかもなので、完全に解決するのは難しいかもしれません。

この記事はShodo (https://shodo.ink) で執筆されました。

コネヒトマルシェオンライン「機械学習・データ分析」を開催しました!

f:id:taxa_program:20210315150933p:plain

こんにちは。MLエンジニアの野澤(@takapy0210)です。

最近自宅にスマートロック*1を導入しました。 ちょっとコンビニに行く〜などの際に、最小限の持ち物(≒スマホのみ)で出かけられることもあり、QOL爆上がり中です。

さて今回は、2月25日に開催したコネヒトマルシェオンラインの様子を簡単にご紹介できればと思います!
嬉しいことにLT枠もオーディエンス枠も多くの方に参加していただき、大盛況で終えることができました!

connehito.connpass.com


目次


LT内容

takapy

hiroto0227 san

usagisan2020 san

www.slideshare.net

ground0state san

kae_takahashi san

shnagai san

tada_infra san

オンライン懇親会

今回は試験的にRemoを用いてオンライン懇親会を開催しました。
社内では「誰も来なかったら、その時はその時ですね〜笑」みたいなことを言っていましたが、いざ開催してみると15名前後の方に参加していただき、とても嬉しかったです!

remo.co

以下、主催者側としてRemoを使ってみた所感を残しておきます。

良かった点

  • ホストがプレゼンモードを使って全体に対して声がけすることができるので、進行がしやすい
  • 強制的に座席シャッフルができる(多くの人との交流機会を作ることができる)
  • 参加者の人が自由に座席移動できるので、ホスト側でどのようにテーブル分けするか、をあまり考えなくても良い
    • 逆にいうと「どのテーブルに行っても大丈夫」という環境を作るために、各テーブルに最低1人はコネヒトの社員を配置する、ということをルールにしていました

次回やるときに改善したい点

  • 登壇者の方に関しては、Remo上の表示名を分かりやすくする
    • 登壇者を判別できるようにしておくことで、登壇した内容について質問しやすい環境が作れたと思いました
  • テーブルごとにどんな話題を喋っているのか、テーブル名で表現する
    • こうすることで、自分の興味ありそうな話題を話しているテーブルに入りやすいかも?と思いました

最後に

拙い司会・進行でしたが、最後までお付き合い頂きありがとうございました!
zoom上のチャットやTwitterで積極的に呟いてくださったこともあり、私自身もとても楽しく参加させていただきました!
(オンラインでの配信と司会を同時にやると意外に余裕がないことが分かり、Twitterなどのコメントを拾うことができなかったのが個人的に悔しかったです)

参加していただいた皆さま、改めてありがとうございました!
また次回のマルシェでお会いできたら嬉しいです!

ECS Scheduled Taskの管理をecscheduleでGitOps化しました

こんにちは。コネヒトのテクノロジー推進グループでインフラエンジニアをしている laughk です。 今回は定期実行バッチで利用しているECS Schedule Taskの管理に Songmu/ecschedule を導入し、GitOps化した話をまとめます。

サマリ

  • ecscheduleを導入する前の定期実行バッチの管理状況と課題
  • 技術選定 - ecshceduleを選定した理由
  • 導入プロセス
  • GitOps化
  • 導入後どうなったか

ecscheduleを導入する前の定期実行バッチの管理状況と課題

コネヒトでは提供するサービスのWeb基盤にAmazon ECSをフル活用しており、定期実行バッチにおいてもECS Scheduled Taskを利用しています。ECS Scheduled TaskはECS Taskを cron のように定期実行でき、とても便利なものです。一方でその管理においては利用する ECS Task の定義、EventBridge のルールなどの複数のAWSリソースを組み合わせる必要があり、煩雑になりがちです。

ECS Scheduled Taskの導入に関しましては別途記事がありますので、興味がある方はそちらも参照いただければうれしいです。

tech.connehito.com

ecschedule導入以前もECS Scheduled Taskのインフラの一部をTerrafrormによって管理をしている状況ではありました。管理対象はバッチ実行に利用するTask Definitionのみで、1つのバッチに対し1つのTask Definitionを用意し、スケジュール部分は手動で設定を行うという運用です。

しかしながらこの方法では次のような問題がありました

  • 各バッチで利用するコンテナイメージは同一のものであり、違いは実行コマンドのみであるのにそれぞれ別のリソースとして定義する必要があった
  • Task Definitionの container_definitions は JSON を文字列で記載する必要があるため、似たようなコピペが量産されている状況だった
  • スケジュールはコメントアウトでメモ程度に書かれているだけで、実際に適用する際は手作業で行っていた

特にTask Definitionのコードは次のような形で、container_definitioncommand 以外ほぼ同じもののコピペがほとんどを占めている状況で、開発者にとってもわかりやすいものではありませんでした。

image

技術選定 - ecshceduleを選定した理由

この状況を打開する手段はいくつか考えられましたが最終的に次の理由でecscheduleを採用しました。

github.com

  • ECS Scheduled Taskの管理に特化しているので、余計な機能がなくシンプル
  • ECSクラスタ名を指定し、その中のTask Definitionを指定するという思想は今のコネヒトのECSクラスタの状況と相性がよかった
  • タスクの単発実行にも対応している ( ecschedule run -conf conf.yaml batchName1 みたいな実行)
  • containerOverrides も含め、一気通貫YAMLで書ける

ほかの手段として Ansible の AWSモジュール を利用することも検討しました。

docs.ansible.com

こちらでもかなり効率的にECS Scheduled Taskの管理が行えるとみていました。しかしながらcontainerOverrideはJSON文字列を渡す必要がある点や、社内でAnsibleがバリバリ使われている状況ではありませんでした。開発者も巻き込んで使えるツールとしてはシンプルさでecshceduleに劣ると判断し、今回は利用を見送りました。

導入プロセス

ecscheduleにはECSクラスタ名を指定して、既存のECS Scheduled Taskの状態をYAML形式で出力するdump機能があります。そのため使い始めること自体はとても簡単にできました。次のようにコマンドを実行するだけでOKでした。

$ ecschedule dump -cluster cluster1  --region ap-northeast-1 > cluster1/event-rules.yaml

ですが、コネヒトの場合は既存のECS Scheduled Taskを管理するだけではTask DefinitionのTerrafrorm管理からは解放されませんでした。そのためバッチを実行する専用のTask DefinitionをECSクラスタにつき1つ用意し、EventBridge RuleのContainerOverrideでコマンドを指定するように構成を変更しました。

before after
image image

コネヒトではdev環境(開発環境)とprd環境(本番環境)でそれぞれ3つのECSクラスタでECS Scheduled Taskを利用している状況で、最終的に次のようなファイル構成に落ち着きました。

.
|-- cluster1/
|   |-- dev-events-rules.yaml
|   |-- dev-task-definition.json
|   |-- prd-events-rules.yaml
|   `-- prd-task-definition.json
|-- cluster2/
|   |-- dev-events-rules.yaml
|   |-- dev-task-definition.json
|   |-- prd-events-rules.yaml
|   `-- prd-task-definition.json
`-- cluster3/
    |-- dev-events-rules.yaml
    |-- dev-task-definition-ec2.json
    |-- dev-task-definition-fargate.json
    |-- prd-events-rules.yaml
    |-- prd-task-definition-ec2.json
    `-- prd-task-definition-fargate.json

ecschedule向けのYAMLファイルを (クラスタ名)/(環境名)-event-rules.yamlとしています。一方で、バッチで利用するTask Definitionの管理はecscheduleではできません。そのため執筆時点ではaws cliで適用可能なJSONを(クラスタ名)/(環境名)-task-definition.jsonにおいています。一つだけEC2とFargateを併用しているクラスタがあるため、そちらはEC2向けとFargate向けで別々に用意しています。

これらのファイルをGitHubリポジトリに乗せ、PRベースでECS Scheduled Task上のバッチ追加、変更をコード管理できるようにしました。PRがApproveされ、マージされたらEventBridge Ruleについてはecscheduleのapplyを実行するだけで変更の適用のほとんどが完結するようになり、運用負荷を大幅に減らすことができました。1

GitHub ActionsでGitOps化

ここまででもGitHubリポジトリ上のYAMLでバッチの管理が可能になり、とても便利になりました。しかしながら、変更が発生した際に人の手でYAMLの内容を適用する必要があることに変わりはなく、どのタイミングで変更が適用したのかということ人が意識しておく必要がありました。

そこでecscheduleのYAMLを編集する際に発生する実行コマンドをMakefileを用意してTask化し、その内容をもとにGithub Actions経由で自動適用できるところまで対応しました。

Makefileは次のような内容です。

ENV=dev

.PHONY: diff-all
diff-all: diff-cluster1-api diff-cluster2 diff-cluster3

.PHONY: apply-all
apply-all: apply-cluster1-api apply-cluster2 apply-cluster3

.PHONY: diff-cluster1-api
diff-cluster1-api:
  ecschedule diff -conf ./cluster1-api/$(ENV)-events-rules.yaml -all

.PHONY: diff-cluster2
diff-cluster2:
  ecschedule diff -conf ./cluster2/$(ENV)-events-rules.yaml -all

.PHONY: diff-cluster3
diff-cluster3:
  ecschedule diff -conf ./cluster3/$(ENV)-events-rules.yaml -all

.PHONY: apply-cluster1-api
apply-cluster1-api:
  ecschedule apply -conf ./cluster1-api/$(ENV)-events-rules.yaml -all

.PHONY: apply-cluster2
apply-cluster2:
  ecschedule apply -conf ./cluster2/$(ENV)-events-rules.yaml -all

.PHONY: apply-cluster3
apply-cluster3:
  ecschedule apply -conf ./cluster3/$(ENV)-events-rules.yaml -all

このMakefileを利用することを前提に、次のような .github/workflow/apply.yaml を用意しました。

name: apply
on:
  push:
    branches:
      - main

jobs:
  apply:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: setup ecschedule
        env:
          ECSCHEDULE_VERSION: 0.3.1
          ECSCHEDULE_MD5_HASH:  28bdd174e4f012e0345d701ac46f1038
        run: |
          sudo apt-get install -y make
          curl -L https://github.com/Songmu/ecschedule/releases/download/v${ECSCHEDULE_VERSION}/ecschedule_v${ECSCHEDULE_VERSION}_linux_amd64.tar.gz -o ecschedule.tgz
          echo "${ECSCHEDULE_MD5_HASH}  ecschedule.tgz" | md5sum -c -
          tar vxf ecschedule.tgz
          sudo mv -v ecschedule_v${ECSCHEDULE_VERSION}_linux_amd64/ecschedule /usr/local/bin/.

      - name: apply (dev)
        env:
          AWS_ACCESS_KEY_ID:  ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: |
          make apply-all ENV=dev

      - name: apply (prd)
        env:
          AWS_ACCESS_KEY_ID:  ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: |
          make apply-all ENV=prd

AWSを利用するためのsecretsはあらかじめ登録しておくことが前提です。applyはmainブランチの内容は毎回適用しても問題ないだろうと判断しているため、細かい条件は指定せずすべて適用するようにしています。

最後にApproveなしのコードがmainブランチに入ることを防ぐため、リポジトリの「Require pull request reviews before merging」を有効にします。執筆時点ではインフラエンジニアのレビューを必須にするため「Require review from Code Owners」も有効にしています。

image

これで人の手を介さずコードの内容の適用が可能になり、バッチ追加・変更のほとんどはGitHubのPullRequestで完結するようになりました。

導入後どうなったか

ecscheduleによるGitOps化ができてからは、開発者がローカルにecscheduleの実行環境を持つ必要もなくなり、バッチの更新・追加についてはGitHubのPullRequest上のやり取りで完結するようになりました。ecscheduleの仕様上自動削除には対応できてはいませんが、それでもほとんどの運用がYAMLの編集のみで完結するようになりました。

image

また計画停止が発生する際にバッチごとに影響範囲をPullRquest上で確認しながらYAMLに落とし込むことができ、とても効率的かつ、チームの議論の過程を残しながら対応ができるようになりました。参考までに、最近実際にPullRequest上で確認しながら計画停止を行った様子を紹介します。

image

image

image

合意をとった状態で対象のブランチをローカルに持ってきてecscheduleのapplyコマンドを実行すれば、その通りに確実にバッチを停止させられるため確実にオペミスを避けることができ、とても安心して計画停止を実施できるようにもなりました。

さいごに

ecscheduleを導入しGitOpsの実現によってECS Scheduled Task上の定期実行バッチの運用負荷を下げた取り組みを紹介しました。

今回紹介した話は来月3/17(水)13:30~18:00に開催される「KGDC Tech Conference #0 通信インフラだけじゃないKDDIグループの多彩な技術」というオンラインイベントでもお話いたします。ぜひ興味ある方、「直接話を聞いてみたい!」という方はご参加いただけるとうれしいです。

kgdc.connpass.com

2021.03.17 追記

この話を 「KGDC Tech Conference #0 通信インフラだけじゃないKDDIグループの多彩な技術」でも発表しました。当日利用したスライドもあわせてみていただけると嬉しいです。

この記事はShodo (https://shodo.ink) で執筆されました。


  1. Task Definitionについては、latestタグのDockerイメージを利用している関係で更新はほとんど発生していません。

レコメンデーション機能を実装するまでの "not 技術的" な取り組みについて

こんにちは。テクノロジー推進グループで機械学習周辺を担当しているたかぱい(@takapy0210)です。

最近観葉植物を購入しました。デスクに緑があると仕事が捗る気がし(ry

f:id:taxa_program:20210224093540j:plain:w600
仕事を捗らせてくれる魔法の植物

今回は質問のレコメンデーションをアプリに実装*1するまでの中で行ってきた、チームビルディングやプロジェクトの進め方についてお話しようと思います。
(技術的な話は、2021年02月25日に開催するイベント*2でお話しできればと思います!)


目次


はじめに

2020年11月頃から、テクノロジー推進グループはデザイングループと共創し、アプリ内にレコメンデーション機能を導入するプロジェクトを始めました。

本エントリでは、

  • 初めて一緒に仕事をする(デザイナーと一緒に仕事をするのが初めてだった)
  • ステークホルダーが多い(アプリに新しい機能を実装するので、CTO, PdM, デザイナー, エンジニアなどが関係している)

というプロジェクトの中で、どのようなことに取り組みながらプロジェクトを進めていったのかについてご紹介できればと思います。

取り組んだこと

以下、時系列に実践してきたことを紹介していきます。

ドラッカー風エクササイズ

冒頭で述べた通り、テクノロジー推進グループとデザイングループで1つのプロジェクトを行うのは初めてでした。
もちろんランチなどで話をしたことはあるものの、知っているようで知らないことも多いのでは?という思いもありました。

そこで、まずは仲間のことを知る時間を作ろうということで、ドラッカー風エクササイズを行いました。

ドラッカー風エクササイズとは?

  • チームビルディングの手法
  • チーム全員がそれぞれお題の質問に答え、共有し合うワーク
  • 相互理解の促進と期待のすりあわせという効果がある
  • 特にプロジェクトの開始時や新メンバーを迎えるときに効果的

と言われています。

今回は以下のような効果を見込んでワークを行いました。

  • メンバーの得意なことを確認・発見し、チームの力を最大限引き出す
  • 期待値を擦り合わせて、円滑なコミュニケーションとチームの成長を促す
  • その期待は重い…という場合はベビーステップを考えられる状態になる

f:id:taxa_program:20210223230136p:plain
エクササイズの様子。Miroで行いました。

このワークを通じて、「何が得意なのか」ということはもちろん、「私はこんな仕事の進め方は苦手」などといった共通認識を取ることもできました。
また、「〇〇さんにはこんなことを期待しているけど、どう?」といった議論がカジュアルにできたりと、非常に有用なワークだったと思います。

我々はなぜここにいるのか(インセプションデッキの作成)

インセプションデッキ*3とは、アジャイル開発のプラクティスの1つで「プロジェクトを始める前に明らかにしておくべき大切なことを知るための活動」と言われています。

インセプションデッキは10個の質問と答えから構成されていますが、今回は「我々はなぜここにいるのか」とう1つの質問に焦点を当ててワークを行いました。
この頃はコロナが一時的に落ち着いていたので、日程調整してオフィスでワークをしていました。懐かしい。

f:id:taxa_program:20210223232209p:plain
インセプションデッキ「なぜ我々はここにいるのか?」についてアイディアを出している様子

このワークではチームの北極星となるような、何かあったら立ち返る場所を作ることを目的として行いましたが、別の観点から「バケーションの写真」としての効果もあるようです。(弊社のエンジニアが教えてくれました)

バケーションの写真とは、旅行で撮った1枚の写真を見れば、その旅行で何があったか/どんな感情だったか、ということが頭の中に蘇ってくるように、全員で決めた「我々はなぜここにいるのか」の標語を見れば、そこにたどり着くためにどんな議論があったか、ということを全員が思い出せる状態になる、という効果のことのようです。

実際、ここで決まった標語は毎朝slackに流れる(毎日目に入る)ようにしたのですが、「なんでこれやってるんだっけ?」「誰のためにやってるんだっけ?」などを毎日思い返すことができ、チームの士気も上がっていたように感じます。

サービスの要求理解

バケーションの写真を撮るとの同時に、PdMの人とサービスの要求についても整理しました。
具体的には、下記のような図に対してそれぞれの項目を埋めることで、お互いの齟齬を減らしつつ、「なぜやるの?」を明文化しました。

f:id:taxa_program:20210223234101p:plain
要求理解に使用した図

UIの検討

ママリの世界観を壊さないように、また、体験が大きく変わりすぎないように、デザイナー主導でいろんなアイディアを出しながらUIを決めていきました。

個人的には、UIってこんな感じに決めていくんだなぁ〜という体験ができたとの同時に、「このUIでレコメンドされたら、ユーザーはどう思うだろう?」といった思考を巡らす機会にもなり、とても楽しかったし勉強になりました。

レコメンデーションは裏側のロジックも大切ですが、「それをどのようにユーザーに届けるのか」といった部分でも結果が大きく左右されると思っています。
ロジックとUI・UXを掛け合わせて、どうすれば効果を最大化できるのか、ということを改めて考える良い機会になりました。

f:id:taxa_program:20210224002352p:plain
MiroでUIのアイディア出しを行っている様子

先行指標やA/Bテストの進め方をPdMと握る

これは今回に限った話ではありませんが、開発を始める前に施策のメトリクスを定義しました。
具体的には下記項目について叩き台を作り、PdMと議論しながら最終的なメトリクスを決定しました。

  • Optimizing for OEC metrics: 施策のゴールとしておく指標
  • Protecting Guardrail metrics: サービスとして最低限維持したい指標
  • Understanding the Why. Local feature, debug, and operational metrics: 施策に関係しそうなユーザー行動をみる指標

また、A/Bテストの割合や実施期間についても予め決めておきました。
開発を始める前に上記の事柄を定義しておくことで、A/Bテストの前にドタバタすることもありませんし、明確な指標をイメージして開発することができるので、よりコトに向かって開発することができました。

反省点

取り組んだ中で「もっとこうした方が良かったな、どうにかできたかもな」と思う点もいくつかありました。

PdMからの権限移譲

今回の施策では、ユーザー体験が大きく変わるかもしれないということで、意思決定の多くはPdMを巻き込みながら行いました。
しかし、PdMとの日程調整は結構難しく、モノによってはこの意思決定がボトルネックとなり予定が遅れてしまう、といったこともありました。

これに対しては、「メンバー層でどこまで意思決定をしても良いか、線引きを明確にする」といったことがポイントだったかな、と感じています。
といっても、いきなり明確な線引きをするのは難しいと思うので、まずは意思決定を下す上で大切・不安に思っているポイントをヒアリングしたりなど、PdMの頭の中を徐々に理解しながら、自分たちで意思決定できる幅(信頼残高)を増やしていければ良いのでは、と思っています。

こういった信頼形成は一朝一夕ではできないことなので、日頃から積み重ねを意識していこうと改めて思いました。

深いコラボレーション

今回はデザイナーと初めての共創ということで、自分自身もワクワクしていましたが、当初想定していたよりもうまくコラボレーションできなかった感覚がありました。
もちろん、インセプションデッキの作成や、UIの検討といった部分では、ママリの世界観に精通しているデザイナーと議論することで得るものは大きかったです。しかし、当初は「サービスの体験設計など一歩踏み込んだ部分でコラボレーションしていけるのでは?」と思っていたので、少し物足りなさを感じていました。
(このあたりはリソースや他業務の優先度などとの兼ね合いもあり、ゴリゴリ推進することができませんでした)

ここで感じたモヤモヤは「当初イメージしていたものとのギャップ」も影響しているとは思うので、最初の期待値調整・リソース配分などをもう少し明確にしておけば結果は違ったかもしれません。

まとめ

本日は機械学習プロジェクトを進める上で、0→1の機能を作る際にやったことをピックアップしてお伝えしました。

機械学習に限らず、他プロジェクトでも流用できる取り組みが多いと感じているので、本エントリが同じような状況にある人の一助になれば幸いです。

最後に

コネヒトでは、プロダクトを成長させたいMLエンジニアを募集しています!!

  • ライフイベント,ライフスタイルの課題解決をするサービスに興味がある方
  • 機械学習の社会実装,プロダクト開発に興味のある方

是非お話できれば嬉しいです!
カジュアル面談では答えられる範囲でなんでも答えます!(特に準備はいりません!)

自分のTwitter宛てにDM送っていただいてもOKですし、下記リンクから5分で申し込むこともできます!

sokumenkun.com

選考はこちらから応募できます。

hrmos.co

iOS/Android 開発Tips共有会 potatotips #72 をオンライン開催しました

本年もよろしくお願い致します。コネヒトのテクノロジー推進グループというところでアプリケーションエンジニアをしているaboyです。

2021年もコネヒトは技術コミュニティに貢献していくぞ、ということで、昨年12月ではありますが iOS/Android 開発Tips共有会 potatotips #72 を主催させていただきました。

potatotips.connpass.com

12月は師走、お忙しい中参加してくださった方々、本当にありがとうございました。

ちなみに、コネヒトにとって12月は再出発の月でした。新しいビジョンを掲げ、コーポレートサイトもリニューアルしました。また、コネヒトにおけるテクノロジーへの「態度」を表明した羅針盤 Connehito Tech Vision を公開したタイミングでもあったため、その紹介もさせていただきつつ。

connehito.com

以降は発表スライドのまとめと、簡単な感想を書いています。

なお当日の様子は togetter してくれた方がいました。ありがとうございます!

togetter.com

発表スライドと一言感想

僕は CI サービスを Travis CI から Bitrise へ移行したことについて発表しました。元々 fastlane で自動化されていたところをそのまま活用し、ワークフローは fastlane ステップを中心に組み立てるだけ。移行コストは低かったです。


同僚の @tommykw さんは Kotlin Compiler Plugin の作り方を解説してくれました。ドキュメントが無いそうですが代わりとなりうるオススメの動画を紹介してくれました。

www.droidcon.com


iOS 14 における Universal Links の変更点について。 Apple CDN 経由に変わり、クローラーからアクセスできるサイトで app-site-association を配布する必要があるみたいです。


はじめての発表ということでしたがとてもスムーズでした! あとで書く


HorizonCalendarというカレンダー UI ライブラリの tips を共有してくれました。僕はこのライブラリを知らなかったのですが、自前で実装すると色々と面倒な UI を実現できるので便利そうでした。メソッドをチェーンさせて書けるのが Flutter や SwiftUI ぽくて個人的に好きです(Flutterは Dart のカスケード記法のことです)。

github.com


Flutter や Kotlin Multiplatform Mobile 等のマルチプラットフォーム開発フレームワークを使うにしても、プラットフォームごとの知識はあるに越したことはないので、入門する際の参考にしたいと思いました。


Twillio 使ったことないので新鮮でした。主題から逸れますが CMTime と TimeInterval のインターフェースが用意されていますがどう使い分けるのがいいのでしょう?コネヒトで動画再生まわりを実装したときは、CMTime を使って再生時間を取得していました。


Flutter における状態管理手法が、僕の中では「Google も使ってるし BLoC でおk」のイメージからアップデートできていなかったので、久しぶりに状態管理について聞けて良かったです。アーキテクチャの選定/命名理由の言語化も丁寧だなーと思って聞いてました。


下から出てくるメニューに限らず、いわゆるアコーディオン UI も簡単に実装できたりするんですかね。この方の著書『1人でアプリを作る人を支えるSwiftUI開発レシピ』も読みつつ SwiftUI 勉強したいと思いました!


Android は全くわかりませんが広く捉えるとプログラミングにおいて重要なエラーハンドリングの話だ...と思いながら聞いていました。スライドの最後に書いてある「Lint は無視しない」は大事です。

参加者の皆さんありがとうございました!

最後に、イベント中流していたハワイの BGM はこちらです。

www.youtube.com


コネヒトでは Android エンジニアなど募集していますので少しでも気になった方は一緒にお話しましょう!

hrmos.co

RxSwift 6.0の主な変更点

こんにちは!コネヒトでiOSエンジニアをやっていますyanamuraです。

RxSwiftの6.0が公開されました。

公式のWhat's new in RxSwift6はこちらです。

What's new in RxSwift 6 ? - DEV Community

細かいdiffはこちらをご覧ください。 https://github.com/ReactiveX/RxSwift/compare/5.1.1...6.0.0

------------------------------✂--------------------------------------

ここでは、RxSwift6.0にアップデートすると影響受けそうなところから見ていきたいと思います。

Breaking Changes

deprecatedだったものが消されました

変更はこちら

Deprecated

以下のものの命名が変更されました。コンパイルは通りますがwarningになります。

  • Rename catchError(:) to catch(:).
  • Rename catchErrorJustReturn(:) to catchAndReturn(:).
  • Rename elementAt(_:) to element(at:).
  • Rename retryWhen(_:) to retry(when:).
  • Rename takeUntil(:) to take(until:) and takeUntil(behavior::) to take(until:behavior:).
  • Rename takeWhile(:) to take(while:) and takeWhile(behavior::) to take(while:behavior:).
  • Rename take(_:) duration overload to take(for:) (e.g. take(for: .seconds(3))).
  • Rename skipWhile(_:) to skip(while:).
  • Rename takeUntil(_:) to take(until:).
  • Rename observeOn and subscribeOn to observe(on:) and subscribe(on:).

Single

RxSwift5まではSingleはsubscribeするとSingleEventという独自のResultみたいなものを返していましたが、これがResultに変わりました。

// RxSwift 5

public enum SingleEvent<Element> {   
    /// One and only sequence element is produced. (underlying observable sequence emits: `.next(Element)`, `.completed`)  
    case success(Element) 

    /// Sequence terminated with an error. (underlying observable sequence emits: `.error(Error)`) 
    case error(Swift.Error)   
}
// RxSwift 6

public typealias SingleEvent<Element> = Result<Element, Swift.Error>

これまでは .error, onError としていたところを .failure, onFailure に変更しないとwarningになります。

変更のコミットはこちら

New

全てのReactiveCompatibleなオブジェクトのプロパティでBinderの実装が不要に

これまではclassのプロパティを

class MyView: UIView { 
    var title: String
}

このようにbind(to:)できるようにするには

viewModel.title.bind(to: myView.rx.title)

下記のような記述を書く必要がありました。

// RxSwift 5

extension Reactive where Base: MyView {
    var title: Binder<String> {
       Binder(base) { base, title in 
           base.title = title
       }
    }

RxSwift6ではReactiveCompatibleなオブジェクト(.rxの使えるオブジェクト)ではこの記述は不要になります。

変更のコミットはこちら

withUnretained

ObservableTypeに withUnretained が追加されました。

これまでは[weak self]をいちいち書く必要がありましたが

viewModel.importantInfo
    .subscribe(onNext: { [weak self] info in 
        guard let self = self else { return }
        self.doImportantTask(with: info)
    })
    .disposed(on: disposeBag)

withUnretainedを使うことでoptional bindingが不要になります。

viewModel.importantInfo
  .withUnretained(self)
  .subscribe(onNext: { owner, info in 
    owner.doImportantTask(with: info)
  })
  .disposed(by: disposeBag)

変更のコミットはこちら

decode(type:decoder:)

Observabledecode(type:decoder:) が追加されました。

decodeがメソッドチェーンですっきりとかけるようになりました。

func test() {
    let rawJSON = """
        [
          {"id": 1, "name": "Foo", "country": "Foo"},
          {"id": 2, "name": "Bar"}
        ]
        """.data(using: .utf8)!

    Observable
              .just(rawJSON)
              .decode(type: [FakeObject].self, decoder: JSONDecoder())
}

private struct FakeObject: Equatable, Decodable {
  let id: Int
  let name: String
  let country: String?
}

変更のコミットはこちら

driveとemitで複数のobserverにbinding

driveとemitでは一つにしかbindingできませんでしたが、複数できるようになりました。

viewModel.string.drive(input1, input2, input3)
viewModel.number.emit(input4, input5)

変更はこちらこちら

Infallible

RxSwiftにInfallibleが追加されました。

errorを流さないObservableです。似たようなものにRxCocoaのDriverやSignalがありますが、これらと違うのはメインスレッドは保証されないのとresourceをshareしたりreplayしない点です。

実装はこちら

ReplayRelay

ReplayRelayが追加されました。

PublishRelay, BehaviorRelayと同様にReplaySubjectのwrapperでerrorやcompleteを流しません。

変更のコミットはこちら

distinctUntilChanged(at:)

distinctUntilChanged(at:) が追加されdistinctUntilChangedにkeyPathを使うことができるようになりました。

        struct TestObject: Equatable {
            let value: Int
            let other = ""
        }

        Observable<TestObject>
            .create { observer in
                observer.onNext(TestObject(value: 1))
                observer.onNext(TestObject(value: 1))
                observer.onNext(TestObject(value: 2))
                return Disposables.create()
            }
            .distinctUntilChanged(at: \.value)
            .subscribe(onNext: {
                print($0.value)
            })

変更のコミットはこちら

UIApplication.rx

UIApplicationのLife cycle Eventなどが追加されました。

追加されたものはこちらです。

  • didEnterBackground
  • willEnterForeground
  • didFinishLaunching
  • didBecomeActive
  • willResignActive
  • didReceiveMemoryWarning
  • willTerminate
  • significantTimeChange
  • backgroundRefreshStatusDidChange
  • protectedDataWillBecomeUnavailable
  • protectedDataDidBecomeAvailable -userDidTakeScreenshot

変更のコミットはこちら