コネヒト開発者ブログ

コネヒト開発者ブログ

カスタムコンテナイメージを用いたデータ分析環境共通化Tips(ローカルPC&AWS SageMaker Studio)

みなさんこんにちは。たかぱい(@takapy0210)です。
気づけばもう6月ですね。2021年の半分が過ぎようとしています。もう半分.....もう...。

はじめに

みなさんデータ分析環境はどのように構築していますか?
Gunosyさんのブログ*1にもあるように、環境構築方法は様々あると思います。

本エントリでは、ローカルとクラウド(AWS SageMaker Studio)のデータ分析環境をコンテナで構築し、環境差分の無い快適なデータ分析ライフを過ごすTipsについてご紹介しようと思います。

これにより軽量な分析はローカル環境でサクッと、重めの分析はクラウド環境(AWS SageMaker Studio)で強いコンピューティングリソースを用いてじっくりとやる、といったことを環境差分を気にせず行うことができます。

ちなみに、ローカルで起動する際はdocker compose up -d jupyterlabコマンドを叩くだけ、SageMakerで起動する際はGUI上でポチポチっと数回クリックすると同じ環境が起動できたりします。

dockerなどの詳細には触れませんのでdockerドキュメントやgoogleなどで調べてみてください。


目次


AWS SageMaker Studioとは

まずAWS SageMaker Studioについて簡単にご紹介します。(以降の記載はSageMakerに省略します)

SageMakerは、機械学習のための統合開発環境 (IDE) と謳われており、慣れ親しんだnotebookを起動してデータ分析・モデル構築ができるのはもちろん、ホストされたエンドポイントにモデルのデプロイするといったことも可能なマネージドサービスです。(公式ドキュメント

デフォルトではAWSが用意してくれているコンテナイメージをベースにnotebookを起動できます。

コンピューティングリソースに関しても様々な種類のインスタンスが用意されており、使いたいインスタンスをGUI上で選択し、数分待つとnotebookが起動できたりと、手軽に環境構築することができます。

しかし、用意されているコンテナイメージでは使いたいライブラリがインストールされていなかったり、違うpythonのバージョンを使いたい、といった要望もあると思います。
(SageMakerを起動する時に毎回pip installしても良いのですが、なかなか煩雑ですよね...)

 そこで、カスタムコンテナイメージを用いることで上記のような課題を解決できます。

カスタムコンテナイメージを用いるメリット

上記でも述べましたが、AWSが用意しているコンテナイメージに存在しないライブラリだったり、使いたいpythonのバージョンをSageMaker上で使用することができます。

また、複数人のMLエンジニアやデータサイエンティストがいる場合においては、dockerfile群を共有しdocker compose buildするだけで手間なく同じ分析環境が構築でき、「Aさんの環境とBさんの環境で分析結果や挙動が異なる・・・」みたいなことを防ぐこともできます。

使用するDockerfile群について

まずはローカル環境を構築するために、以下3つのファイルを用意します。

  • Dockerfile
  • docker-compose.yml
  • jupyter_notebook_config.py

以降でそれぞれのファイルの詳細についてご紹介します。
(掲載しているコードはサンプルとして適宜省略しています)

Dockerfile

ポイントは以下2点です。

  • jupyter_notebook_config.pyファイルを用いることでシンプルなコマンドでjupyterが起動できるようにする(jupyter notebook --port 8888 --ip="0.0.0.0" --allow-rootみたいな長ったらしいコマンド叩くの嫌ですよね...)

  • python3 -m ipykernel install --sys-prefixコマンドでSageMakerから認識できるようにする。

FROM python:3.8

LABEL hoge <hoge@fuga.com>

RUN apt-get -y update && apt-get install -y --no-install-recommends \
        mecab \
        libmecab-dev \
        mecab-ipadic \
        mecab-ipadic-utf8 \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*
    
# jupyter lab
RUN pip3 install -U pip && \
    pip3 install jupyterlab && \
    pip3 install jupyterlab-git && \
    mkdir ~/.jupyter
COPY ./jupyter_notebook_config.py /root/.jupyter/jupyter_notebook_config.py

# requirements.lockにインストールしたいライブラリを記載する
# python3 -m ipykernel install --sys-prefix がないと、SageMakerがpython3.8を認識してくれないので注意
COPY requirements.lock /tmp/requirements.lock
RUN python3 -m pip install -r /tmp/requirements.lock && \
    python3 -m ipykernel install --sys-prefix && \
    rm /tmp/requirements.lock && \
    rm -rf /root/.cache

COPY working /opt/program/working
WORKDIR /opt/program/working

docker-compose.yml

下記のようなイメージです。

version: '3.4'

x-template: &template
  build:
    context: .
  volumes:
    - ./working:/opt/program/working:cached

services:
  jupyterlab:
    container_name: 'jupyterlab'
    image: jupyterlab:latest
    user: root
    ports:
      - "8999:8999"
    command: jupyter lab --allow-root
    <<: *template

jupyter_notebook_config.py

下記のようなイメージです。
c.NotebookApp.passwordにはjupyterlab起動時に入力するパスワードを設定できます。(本サンプルでは「password」となっています)

c = get_config()
c.NotebookApp.ip = '0.0.0.0'
c.NotebookApp.open_browser = False
c.NotebookApp.port = 8999
c.NotebookApp.notebook_dir = '/opt/program/working/'
c.LabApp.user_settings_dir = '/opt/program/working/jupyterlab/user-settings'
c.LabApp.workspaces_dir = '/opt/program/jupyterlab/workspaces'
c.NotebookApp.password = u'sha1:63cae364b3cd:c4319cba1eeb1bcf011a7d3fabd6448f95ae18c5'

ここまで準備ができたら、あとはdocker compose up -d jupyterlabコマンドを実行した後、ローカルのブラウザでhttp://127.0.0.1:8999/labにアクセスすればjupyterLabが使えます。

次に、このコンテナイメージをSageMaker上で使う方法についてご紹介します。

SageMaker でカスタムコンテナイメージを起動する方法

SageMakerで使用するためには、追加で以下2つのファイルが必要になります。

  • app-image-config-input.json
  • update-domain-input.json

それぞれの役割について簡単にご紹介します。

app-image-config-input.json

このconfigファイルには、SageMakerで使用する際のカーネル名やマウントディレクトリを定義します。
KernelSpecsNameに設定する値は、コンテナイメージをローカルで起動した後jupyter-kernelspec listを実行した際に表示されるカーネルの中から選択する必要があります。(詳しい手順はこちらをご参照ください)

{
    "AppImageConfigName": "ml-image-name",
    "KernelGatewayImageConfig": {
        "KernelSpecs": [
            {
                "Name": "python3",
                "DisplayName": "Python 3"
            }
        ],
        "FileSystemConfig": {
            "MountPath": "/root/data",
            "DefaultUid": 0,
            "DefaultGid": 0
        }
    }
}

update-domain-input.json

これはコンテナイメージをドメイン(≒ Sagamaker Studio)にアタッチするために必要なファイルです。

DomainIdには、AWSコンソール上で表示されるStudio IDを設定してください。

また、AppImageConfigName には、app-image-config-input.jsonで定義したものと同値を設定する必要があります。

{
    "DomainId": "d-hogehoge",
    "DefaultUserSettings": {
        "KernelGatewayAppSettings": {
            "CustomImages": [
                {
                    "ImageName": "ml-analysis",
                    "AppImageConfigName": "ml-image-name"
                }
            ]
        }
    }
}

SageMakerへ登録

上記2つのファイルを作成できたら、あとは以下のようなスクリプトを実行して、コンテナのビルド→ECR push→SageMakerへのアタッチを行います。

#!/bin/bash

REGION="AWSのリージョン"
ACCOUNT_ID="AWSのアカウントID"
REPOSITORY_NAME="ml-analysis"
TAG_NAME="ml-analysis"
ROLE_ARN="arn:aws:iam::${ACCOUNT_ID}:role/service-role/hoge"

# ビルド
docker build -t ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${REPOSITORY_NAME}:${TAG_NAME} .

# ログイン
aws ecr get-login-password --region ${REGION} | docker login --username AWS --password-stdin ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com

# push
docker push ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${REPOSITORY_NAME}:${TAG_NAME}

# コンテナイメージを Amazon SageMaker に登録
aws --region ${REGION} sagemaker create-image \
    --image-name ${TAG_NAME} \
    --role-arn ${ROLE_ARN}
aws --region ${REGION} sagemaker create-image-version \
    --image-name ${TAG_NAME} \
    --base-image "${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${REPOSITORY_NAME}:${TAG_NAME}"

# AppImageConfigの作成(「file:」以降にファイルパスを指定)
aws --region ${REGION} sagemaker create-app-image-config --cli-input-json file://sagemaker/app-image-config-input.json

# コンテナイメージをSagamakerにアタッチ(「file:」以降にファイルパスを指定)
aws --region ${REGION} sagemaker update-domain --cli-input-json file://sagemaker/update-domain-input.json

ここまでで、下記のようにSageMakerコンソール上からイメージがアタッチされていることを確認できます。

あとはSageMaker Studioを起動して、アタッチされたイメージを選択してnotebookを起動するだけで、ローカルと同一環境がSageMaker上で実現できます 🎉🎉🎉

SageMakerを使う際に気をつけたいこと

前述してきたように、SageMakerを使う際のメリットはいくつかあります。

  • 豊富なコンピューティングリソースを用いて分析・モデリングできる
  • notebookが動く環境が既に用意されているので環境構築が比較的容易
  • 付随するマネージドサービスが豊富にある(本記事では触れませんが、FeatureStore, Experiments, Pipelineなどがあります)

しかし、注意点もあるので最後にまとめておこうと思います。

油断すると結構お金がかかる

はい。油断すると結構お金がかかります(汗)

SageMakerは従量課金のマネージドサービスで、主に以下2点で課金されます。

  • notebookなどのインスタンス起動時間
  • マウントされているEFSの容量

notebookなどのインスタンス起動時間

使用していないインスタンスは停止させたり、業務終了時にインスタンスを落とすなどの対策すれば大丈夫だと思います。
例えば、ml.t3.2xlarge(8vCUP/メモリ32GiB)のインスタンスは0.522USD/hなので、1ヶ月(約720h)起動させたままにすると375USD(約4万円)コストがかかります。

「深夜に終わる想定の学習回しちゃったら朝までお金かかっちゃうのか...」と思われる方もいると思いますが、SageMakerには ProcessingJobTrainingJobといった機能もあり、これらはJobが終了すると自動的にインスタンスが落ちてくれるので、併せて使ってみると良いと思います。(この辺りのことも後日記事にできればと思っています)

(詳細な料金は公式ドキュメントを参照してください)

マウントされているEFSの容量

こちらは容量単位で課金されます。

ここで注意したいのが、SageMaker notebook上からファイルを削除すると、notebook上からは削除されているように見えるのですがTrashに残っていたりします。

特に機械学習やデータ分析においては容量の大きいファイルを多用することになると思うので、これを放置しておくと無駄に課金されてしまうリスクがあります。

SageMaker上でターミナルを起動しdf -hコマンドを実行すると現在どのくらいの容量を使っているかが把握できるので、定期的にチェックすることをオススメします。
(下記例だと、936MBのEFSボリュームを使っていることになります)

$ df -h
Filesystem         Size  Used Avail Use% Mounted on
overlay             17G   52K   17G   1% /
tmpfs               64M     0   64M   0% /dev
tmpfs              1.9G     0  1.9G   0% /sys/fs/cgroup
shm                 64M     0   64M   0% /dev/shm
127.0.0.1:/200005  8.0E  936M  8.0E   1% /home/sagemaker-user  # ここがnotebookにマウントされているEFSの容量
/dev/nvme0n1p1      83G  8.8G   75G  11% /etc/hosts
devtmpfs           1.9G     0  1.9G   0% /dev/tty
tmpfs              1.9G     0  1.9G   0% /proc/acpi
tmpfs              1.9G     0  1.9G   0% /sys/firmware

上記で表示される容量にはTrash(ゴミ箱)にあるファイルも含まれているので、notebook上から削除したファイルはrmコマンドで完全に削除する必要があります。
なので、不要なファイルをSageMaker notebook上から削除した際にはrmコマンドも実行するように意識しておくと良いです。

# ゴミ箱のファイル一覧
$ ls -al -h  ~/.local/share/Trash/files

# ゴミ箱のファイルを全て削除
rm -rf ~/.local/share/Trash/files/*

(EFSの詳細な料金については公式ドキュメントを参照してください)

最後に

最近はAWSからも機械学習系のサービスが頻繁にローンチ・アップデートされており、SageMakerもここ数年でかなり機能強化されています。 とはいえ、まだまだ公開されている事例が少ないとも感じているので、業務で得た知見などは積極的にアウトプットしていければと思っています。

みなさんも良い分析ライフをお楽しみください!

家族ノートを支えるBigQuery+StepFunctionsで作るデータレイク

こんにちは。インフラエンジニアの永井(shnagai)です。

最近、家族ノートという「ママリ」内の検索データとQ&Aデータ(現在開発中)を可視化したデータ分析サービスの立ち上げに携わっています。

info-kazokunote.mamari.jp

今回は、家族ノートで使っているデータ基盤の一部であるBigQuery+StepFunctionsで作ったデータレイクの仕組みについてご紹介します。

内容は、ざっくりとこんな話を書こうと思います。

  • データ基盤作りに至った経緯
  • AWS→BigQueryにデータ移送するアーキテクチャのpros&cons
  • StepFunctions+Embulk(Fargate)を利用したデータレイクの仕組み

データ基盤作りに至った経緯

コネヒトには大きく分けると2つのデータセットがあります。

  • DB(Aurora)にあるアプリケーションのデータ(業務データやマスターデータ)
  • BigQueryにある行動ログや集計データ

今回家族ノートで使うデータとしてこの2つのデータセットをかけ合わせる必要があり、 データ結合の手段としては、下記2つの方法を検討しました。

  • ①バッチ処理を使いアプリケーション側でデータの結合を行い新しいデータセットを作るパターン
  • ②データマートを用意しアプリケーションはデータ結合は意識せずにデータを利用するパターン

MLプロダクト等で、①のパターンの処理がいくつか動いているのですが、同じようなデータセットを作るバッチ処理が各所で動くのは望ましい状態ではないという課題があり、これを機にデータ基盤を作ることにしました。

と言っても、いきなり完璧なデータ基盤を作るのは現実的ではないのでスコープを絞り小さくデータ基盤を作ることから始めました。 こんな要件を定義しています。

  • 家族ノートのMVPリリースに必要なデータマートを用意するのがゴール
    • BigQuery上にデータレイク/データウェアハウス/データマートを用意
    • データマートとしてはBigQueryのViewを利用しアプリケーションから参照する
  • データマートに必要なデータレイクを最小限でBigQuery上に整えていく

この構成を考えるにあたり下記の書籍が非常に参考になりました。

データマネジメントが30分でわかる本 | ゆずたそ, はせりょ, ゆずたそ | 経営情報システム | Kindleストア | Amazon

Viewでデータマートを作るには、BigQuery上に必要なデータが揃っている(データレイク)必要があります。 以降は、Aurora(AWS上)にある業務データをBigQueryに移送するデータレイク作成の仕組みについて検討及び実装したの内容を紹介していきます。

AWS→BigQueryにデータ移送するアーキテクチャのpros&cons

AWS(Aurora)からBigQueryにデータを移送する手段ですが、AWS純正のものがあればそれを使うのが一番筋がいいだろうと考え、BigQueryにインテグレートできるような機能を探しましたが今のところはなさそうでした。

そこで、データの抽出/変換/移送を行うETLツールとそれを動かすワークフローについて、AWSで構成を作るにはどんなパターンがあるかをざっくりpros&cons形式で出し比較検討しました。

※個人の主観が入ってますので参考程度に

目指す状態

アーキテクチャ選定においては、下記観点を満たせる構成をゴールに設定しました。

  • 開発者もしくは必要な人が誰でも新しいETL処理を追加・削除出来る
    • コード化されておりかつ簡単なDSLで記載出来る方がよりよい
    • インフラ or データエンジニアしか管理・更新出来ない状態にはしない
  • できるだけワークフローの運用コストが低い
    • マネージドサービスもしくはそれに親しい運用コストが理想
    • 専属で面倒見る人まだいないので、オレオレの運用が発生しないという部分のウエイトを上げる

※構成比較の全体像 f:id:nagais:20210517095316p:plain

ETLツール

コード管理の容易さとプラガブルな構成による柔軟性からEmbulkを採用しました。

Embulk 

  • pros
    • 豊富なプラグインで柔軟な処理
      • フィルタである程度整形も出来る
    • 社外での活用事例も多数あり信頼出来る
    • yamlで簡単な記述
    • 処理をembulk内で完結出来る
    • 使い慣れてるのはある(うちでも既に運用実績はあり)
  • cons
    • Embulkの運用(バージョン管理,エラー時は調査が必要)
      • 重要な意味を持ち出した時にメンテ続けるのは結構大変ではある
    • Embulk実行リソース(Fargateにすることでサーバレスには出来る)

Glue+S3+DataTransferService(BigQuery)

  • pros
    • 全てマネージドサービスなので運用コストが低い
      • どんな処理をするかを定義すれば後はおまかせ
  • cons
    • 各マネージドサービスのコード管理が必要(terraformでやれそう)
    • Glue, DataTransferServiceそれぞれの制約と向き合う必要
    • DataTransferServiceの知見がなく事例も薄い(ちょい検証して実地で知見をためてくアプローチ)
    • DataTransferServiceはスケジュールベースでしか動かないかも(リトライ考えるとトリガ形式にしたい)

Glue+S3+Lambda+GCS+CloudFunctions

  • pros
    • GCSにcsvを置くとBQにデータ流すという構図が作れる
    • s3に置くまでとGCS後の世界が分けれるので応用は効く
      • s3にファイルを置けばデータソースが何であれ同じ仕組みでBQに取り込めるみたいな
  • cons
    • ピタゴラスイッチになるので、構成を把握するのが大変
    • 扱う要素が多くなるので管理コストは高い
      • 全部terraformで賄わないと変更箇所多数で運用破綻するかもな

ワークフロー

運用コストの低さをワークフローエンジンに求めたかったのが大きくStepFunctionsを採用しました。 DigDagでもFargateをキックする構成で近い構成を取ることはできそうだったのですが。DigDag自体の運用が残る点がネックとなりました。

DigDag

  • pros
    • 全てDigDagのDSLでコード管理
    • Embulkと好相性(Fargateキックパターンもあり)
    • BigQueryのジョブも管理出来る可能性ある(やってみないとわからないけど)
    • 管理画面もある
  • cons
    • 学習コスト
    • DigDag自体は常に存在している必要がある(ECSサービス化)
    • DigDagデプロイするときには注意が必要なのかな??(検証してみる必要がある)
  • 検討の中で追記
  • デプロイにしても何にしてもワークフロー自体をうまく回すことを意識して運用しないといけないよな・・・
  • コンテナでやる場合には、セッションやデータを管理する必要がある(イミュータブル対応)
  • 永続ディスクが必要(ECSでボリュームマウント)
  • PostgresSQLをRDSで立ててそこにデータを保存するようにする

StepFunctions

  • pros
    • ワークフローの運用は不要
    • 使い慣れている
    • AWSコンソール上からだがリトライやジョブ可視化可能
    • AWSリソースとフレンドリ
      • AWSサービス使うとかなった際に楽
  • cons
    • コード管理しないと運用破綻する
      • DWH構想用のコード化
  • 検討の中で追記
  • AWS SAMでStepFunctionsとEventBridgeを管理すればコード管理出来る
  • 共通Dockerイメージ作って、SFで Iterator を使うことで、呼び出し時にテーブル名を指定する形にすれば * 取得の時に簡素に書くことも可能かもを検証

StepFunctions+Embulk(ECS×Fargate)を利用したデータレイクの仕組み

f:id:nagais:20210517102541p:plain

コード管理

誰でも簡単にデータレイクに手をいれれる構成にこだわっていたので、データレイクに関わる必要なリソースはすべて1リポジトリで管理する構成にしました。

  • AWSリソース(StepFunctions,EventBridge等)はSAM(AWS サーバーレスアプリケーションモデル)を使って管理
  • データレイク処理追加時に1リポジトリの修正で済むようにembulkのコードと同じGitHubリポジトリで管理しています。
  • 基本的に新しいテーブルをデータレイクに追加したい時は、コピペベースでPR作ることで追加ができるようにしています。
  • デプロイも自動化しており、embulkのDockerイメージのbuild&pushとSAMのデプロイがmainマージ時に実行されます。

ディレクトリ構成はこんな感じです。

embulk(embulkの定義)
 |--conf/ (embulkの定義)★
sam(AWS SAMの定義)
 |--statemachine/ (StepFunctionsの定義)★
 |--functions/ (Lambdaの定義)
 |--env
   |--dev(dev環境用の定義)
     |--samconfig.toml (SAMデプロイ時の設定)
     |--template.yaml (SAMの定義 ※CloudFormationにジェネレートされる)
   |--prd(prd環境用の定義)
     |--samconfig.toml (SAMデプロイ時の設定)
     |--template.yaml (SAMの定義 ※CloudFormationにジェネレートされる)

StepFunctions

できるだけ運用コストを低くするために、

【ポイント】

  • StepFunctionsのステートマシンはデータソース単位で用意
    • 並列(Parallel)でFargateタスク(embulkプロセス)を呼び出すことでエラー時の他への影響を極力小さくする
  • コスト削減のためにFargate Spotを100%利用するクラスタで動かす
  • Fargateタスクなので、処理の干渉が起きない
    • 夜間なので今の所問題になっていないがデータソースに一気に繋ぐとデータソース側がパンクする可能性があるが、その時はステートマシン内のフローを組み替えることで対応予定
  • StepFunctions側でリトライ制御もいれており、単純リトライでもエラーになった際はエラー通知

参考までにSAMで管理している実際のステートマシンのコードの一部をお見せします。 VSCodeのプラグインを使うことでステートマシンを描画できるのはめちゃくちゃ便利でした。

{
    "Comment": "Definition of Embulk StateMachine",
    "StartAt": "Parallel",
    "States": {
      "Parallel": {
        "Type": "Parallel",
        "Next": "Final",
        "Catch": [
          {
            "ErrorEquals": [
              "States.ALL"
            ],
            "Next": "NotifySlackFailure"
          }
        ],
        "Branches": [
          {
            "StartAt": "users",
            "States": {
              "users": {
                "Type": "Task",
                "Resource": "arn:aws:states:::ecs:runTask.sync",
                "Parameters": {
                  "LaunchType": "FARGATE",
                  "Cluster": "${EcsCluster}",
                  "TaskDefinition": "${TaskDefinition}",
                  "NetworkConfiguration": {
                    "AwsvpcConfiguration": {
                      "Subnets": [
                        "${Subnets}"
                      ],
                      "SecurityGroups": [
                        "${SecurityGroups}"
                      ],
                      "AssignPublicIp": "ENABLED"
                    }
                  },
                  "Overrides": {
                    "ContainerOverrides": [
                      {
                        "Name": "${ContainerName}",
                        "Command": [
                          "/embulk/bin/embulk",
                          "run",
                          "conf/hoge/users_bigquery.yml.liquid"
                        ]
                      }
                    ]
                  }
                },
                "End": true,
                "Retry": [
                  {
                    "ErrorEquals": [
                      "States.ALL"
                    ],
                    "IntervalSeconds": 3,
                    "BackoffRate": 2,
                    "MaxAttempts": 1
                  }
                ]
              }
            }
          },
          {
            "StartAt": "children",
            "States": {
              "children": {
                "Type": "Task",
                "Resource": "arn:aws:states:::ecs:runTask.sync",
                "Parameters": {
                  "LaunchType": "FARGATE",
                  "Cluster": "${EcsCluster}",
                  "TaskDefinition": "${TaskDefinition}",
                  "NetworkConfiguration": {
                    "AwsvpcConfiguration": {
                      "Subnets": [
                        "${Subnets}"
                      ],
                      "SecurityGroups": [
                        "${SecurityGroups}"
                      ],
                      "AssignPublicIp": "ENABLED"
                    }
                  },
                  "Overrides": {
                    "ContainerOverrides": [
                      {
                        "Name": "${ContainerName}",
                        "Command": [
                          "/embulk/bin/embulk",
                          "run",
                          "conf/hoge/children_bigquery.yml.liquid"
                        ]
                      }
                    ]
                  }
                },
                "End": true,
                "Retry": [
                  {
                    "ErrorEquals": [
                      "States.ALL"
                    ],
                    "IntervalSeconds": 3,
                    "BackoffRate": 2,
                    "MaxAttempts": 1
                  }
                ]
              }
            }
          },

※embulkについては、すでにwebにも様々な事例があり特殊なことはしていないので詳細は省略します。

おわりに

この構成を作ってから半年以上経過していますが、今のところ特段大きな問題が起きておらず、意図した通りに低い運用コストで毎日動いています。 ※Fargateのエフェメラルストレージが200GBになったのも追い風になっています(20GBだったのでいつか限界に達したら手を加えなければと思っていた..)

StepFunctionsは他の用途でも活用しているのですが、AWSでワークフローを動かす時の選択肢としてかなり優秀だなと感じています。 AWSでワークフローを組む必要がある際には、一番の選択肢として考えていいのではないでしょうか。

最後に、今回紹介したデータを元にして開発している家族ノートを一緒に育てていってくれるエンジニアを募集しています。 少しでも興味をもたれた方は、ぜひ一度お話させてもらえるとうれしいです。

hrmos.co

Storybookを利用した開発フローの設計

こんにちは、リードエンジニアの @dachi_023 です。最近書きたいことが多くて今月3本目の記事です。今回はレビュー時にStorybookを使っている話です。チームによっては使うのが当たり前くらいになってきているツールでもありますが今一度「なぜ入れているのか」「どう役に立つのか」といった観点で振り返ってもらえればいいなと思っています。

新規開発時の課題

これまでUIの実装を始める時はボタンやラベルといった粒度のコンポーネントを実装して後からレイアウトを組んだりコンポーネントを埋め込んだりする、という流れで進めていました。しかし、新規アプリケーションで先にコンポーネントを実装してしまうと実際に動作しているページがなく、レビュー時に確認しづらいという課題がありました。

レビューのためのStorybook

この問題を解決するためコンポーネント実装時にStorybookへコンポーネントを追加しておき、描画されたコンポーネントを使って動作を見てもらうのはどうか?と思いやってみることにしました。また、コネヒトでのStorybookはUIカタログとして参照するのが主で、テストやレビューで使うこともほとんどなかったのでもっと活用できたらいいなという想いもありました。

ちなみに私の場合はコンポーネントを実装したらローカルで適当なページを用意してそこに描画して試す、みたいなことをやっていました。こういう非効率なやり方はどんどん改善されるべきだと思います(自戒)。

開発フローを改善する

UI開発時のフロー

  1. コンポーネントの実装(テスト、UI実装)
  2. Storybookにコンポーネントを追加
  3. Pull Requestを作成・レビュー依頼

これまでコードを書いてすぐレビューを依頼していたところをStorybookへの追加までをセットでPull Requestを作成するルールにしてみました。その結果、当初想定していた以上にいい効果がありました。

レビュー時の指摘がしやすい

今回の大目的であった部分は無事改善されました。Storybookを見ながらUIに対する指摘ができるのがレビュー時にとても便利で、Propsをその場で編集することで想定外の挙動をしないかなどを確認できるようになりました。他にも @storybook/addon-a11y を使うことでコンポーネントごとのWebアクセシビリティが担保されているかの確認ができるので、機械的に判定できる部分の指摘を減らせました。

Propsの編集 a11yチェック
Props の操作 @storybook/addon-a11y

Storybookのアドオンは他にもあるので目を通してみて「うちのチームはこれ入れると便利そう」といったものを見つけてみるといいかもしれません。また、他社事例なども非常に参考になるので「Storybook 運用」などで検索するのもおすすめです。

Addons | Storybook

UIのパターン漏れに気づきやすい

「エラー時に色が変わる対応が漏れていた」といったミスにレビューを出す前からある程度気付けるようになりました。過去にこういったことが何度かあったのですが、Storybookを使うようにしてからは Story を追加するにあたってどういうパターンがあるかを事前に洗い出すようにしたのでミスを大幅に減らせました。

通常時のUI エラー時のUI
通常時 エラー時

Storybookの運用が安定する

必ずStorybookに追加するようにしたのでこれまで起きていた追加・修正漏れがなくなりました。開発フローの改善をするつもりでしたが結果としてStorybookの運用フローも整いました。カタログ用途として運用しているとCIが失敗するわけでもないので追加し忘れがちだったのですが解消されました。

まとめ

スナップショットテストやカタログなどチームによって用途は異なりますが、コンポーネント単位でのレビュー時に動作確認をする場としても活用できるよという話でした。また、Storybookの追加漏れ防止など運用面でもいい効果があったのでちゃんと運用されずに困っている人にもオススメです。

この記事はShodoで執筆されました

KDDIグループでテックカンファレンスを開催しました

こんにちは、リードエンジニアの @dachi_023 です。2021/03/17(水)にKDDIグループ6社合同でテックカンファレンスを開催したので弊社から登壇したメンバーの発表内容を掲載しています。また、私が運営メンバーとして参加させてもらったのでその感想も少し載せています。

https://kgdc.connpass.com/event/203487/

KGDC Tech Conference

KDDI Group Developer Community

KGDCは「KDDI Group Developer Community」の略です。

KDDIグループ各社のエンジニアが集って技術的な発信をしていこう!という取り組みです。今回が初開催だったのですが、当日は多くの方が視聴してくださりとてもうれしかったです。視聴いただいた皆さんありがとうございました。

パネルディスカッション

パネルディスカッションは2テーマでの開催となっており、それぞれ技術・開発組織に関するものとなっています。弊社からはCTOの @itosho が技術枠で登壇しました。技術関連の意思決定で重要にしていることなど、会社や事業によって大きく変わってくるんだなというのを強く感じました。

セッション・LT

弊社メンバーのスライド一覧です。connpassに会全体の発表資料も一部ですが下記より閲覧できます。

https://kgdc.connpass.com/event/203487/presentation/

運営メンバーに入ってみての感想

今回は企画メンバーの1人として参加させてもらったのですが、大人数で運営するような会に参加したことがなかったので新鮮な気持ちでした。また、会全体のテーマ・コンテンツを同じチームの皆さんと一緒に考えたりするのも楽しかったです。KDDIグループ同士ではあったのですが交流する機会がほとんどなかったので、新しい取り組みを通して新たな機会が生まれたというのも非常に良かったです。

さいごに

初めてのカンファレンス運営でしたがとても楽しく取り組むことができました。今後も続けていく予定ですのでぜひconnpassグループのメンバーになって開催通知を受け取ってもらえるとありがたいです!よろしくお願いします。

https://kgdc.connpass.com/

この記事はShodoで執筆されました。

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などのコメントを拾うことができなかったのが個人的に悔しかったです)

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