RAKSUL TechBlog

ラクスルグループのエンジニアが技術トピックを発信するブログです

Docker + Fastify + webpack-dev-server によるフロントエンド開発環境

こんにちは。ハコベル事業本部 ソリューションチームの丸山です。RAKSUL Advent Calendar 2021 17日目の記事になります。 現在はハコベルコネクトという物流における配車業務のデジタル化により、業務自動化・情報一元化を行い、顧客の業務コスト削減を図るサービスの開発をしています。

ハコベルコネクトでは11月にDocker + Fastify + webpack-dev-server(Vue CLI)という構成でフロントエンドの開発環境とシステム構成の刷新を行いました。 Nuxt.jsやNext.jsといったフレームワークを使用することも検討しましたが、永く運用してきたプロダクトなためいきなり乗り換えることは難しく、悩んだ結果上記のような構成にしています。 同じような悩みを持った方に少しでも参考になればと思い、どのような構成・実装を行なっているのか紹介します。

背景

まず初めにフロントエンド環境を刷新するに至った経緯についてですが、ハコベルコネクトはプロダクト立ち上げ時にはRuby on Railsによってフロントエンドからバックエンドまでを実装したモノリシックな構成となっていました。 その後、開発を続けるうちにコードベースも大きくなり、Railsのviewだけではフロントエンドの開発を継続するのが難しくなってきたためVue.jsを使用しRailsからは切り離した構成へと変更しました。 しかし、完全なSPA化などには至っておらず、またページアクセスの際のルーティングやアセットの配信などは引き続きRailsを使用しており、Gitリポジトリとしても1つのリポジトリに全てが入った状態となっていました。

永らくその状態で運用されていたのですが、フロントエンドとバックエンドが同じリポジトリにあることで、どちらかの変更しかしていなくてもデプロイ時に全体のビルドが行われてしまい時間がかかっていたことや、開発環境やライブラリの変更を行う際に必要以上に影響範囲を気にしなくてはならないなどの問題が発生していました。

そこで、リポジトリをフロントエンド用とバックエンド用に分け、ページのルーティングやアセットの配信もフロントエンド側で行うようにすることで、RailsはAPIサーバとしての責務に集中しお互いを疎結合にすることを目的としてフロントエンド開発環境の刷新を行いました。

構成

まず初めに考えたのはフロントエンド環境を完全にSPA化し、Nuxt.jsなどを使用してフロントエンドサーバを構築することでした。しかし、ページごとにエントリポイントがあり、それぞれを別ファイルとしてビルドしている現状から、限られた時間やリソースの中でその状態まで一気に対応することは難しいという結論に至りました。 そこで、まずはリポジトリを分割することとルーティングなどのフロントエンドの責務をバックエンドから引き剥がすことを第一として既存のVueのコードベースやビルドフローにはできる限り変更を加えない方法をとることにしました。

最終的な開発環境の構成は以下

  • Vue部分はこれまで通りページごとのエントリポイントを持ち、開発中はwebpack-dev-server(Vue CLI)を使用する
  • ページのルーティングにはFastifyを使用。本番環境ではアセットの配信も行うが開発中はwebpack-dev-serverが行う
  • ローカルでの開発環境はdocker-composeを用い、webpack-dev-server とFastifyへの振り分けにはnginx-proxyイメージを使用する
  • ローカルでhttpsでのアクセスが必要な場合には https-portalイメージを使用

docker-compose.ymlのサンプルは以下のようになります。(説明用のためいくつかの設定は省略しています)

version: '3'

services:
  https-portal:
    profiles:
      - https
    image: steveltn/https-portal:latest
    ports:
      - 8443:443
    environment:
      STAGE: local
      DOMAINS: 'localhost -> http://nginx-proxy:80'
  nginx-proxy:
    image: jwilder/nginx-proxy:0.9.0
    ports:
      - 8080:80
    volumes:
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - ./dev-proxy:/etc/nginx/vhost.d
    depends_on:
      - vue
      - fastify
  vue:
    build: ./vue
    hostname: vue
    environment:
      VIRTUAL_HOST: vue.local
      VIRTUAL_PORT: 8080
    volumes:
      - ./vue:/app:delegated
      -/app/node_modules
    command: "yarn dev"
  fastify:
    build: ./fastify
    environment:
      VIRTUAL_HOST: localhost
      VIRTUAL_PORT: 80
    volumes:
      - ./fastify:/app:delegated
      -/app/node_modules
    command: "dockerize -wait http://vue:8080/webpack-dev-server -timeout 10m -wait-retry-interval 5s yarn serve"

ポイント

上記構成のポイントをいくつかご紹介します。

nginx-proxy

Docker Hub にて公開されているDocker image です。 nginxのリバースプロキシを構成し、複数コンテナ間のアクセス振り分けなどを簡単に設定することができます。 各serviceのenvironmentに VIRTUAL_HOST VIRTUAL_PORT を指定するとその VIRTUAL_HOST でnginx-proxyコンテナにアクセスがあった際に各serviceの VIRTUAL_PORT で指定したportへアクセスを振り分けてくれます。

services:
  nginx-proxy:
    ports:
      - 8080:80
  vue:
    environment:
      VIRTUAL_HOST: vue.local
      VIRTUAL_PORT: 8080
  fastify
    environment:
      VIRTUAL_HOST: localhost
      VIRTUAL_PORT: 80

これで http://localhost:8080 へアクセスすることで nginx-proxyコンテナがfastifyコンテナの80番ポートへとアクセスを振り分けてくれます。 ただしjsファイルなどのアセットはvueコンテナよりwebpack-dev-serverから配信を行うため、vueコンテナへと振り分けを行う必要があります。 そこで /dev-proxy ディレクトリ内に localhost というファイルを作り、nginxのlocation設定を記述します。

# docker-compose.yml
version: '3'

services:
  nginx-proxy:
    volumes:
      - ./dev-proxy:/etc/nginx/vhost.d # ./dev-proxy内のファイルをマウントする
# /dev-proxy/localhost

location ~ ^/(assets/.*) {
    proxy_pass http://vue.local/$1;
}

これで、 http://localhost:8080/assets 配下へのアクセスはvueコンテナへと振り分けられます。 また、以下のような記述も追加することで、 http://localhost:8080/_vue 配下へのアクセスはvueコンテナへと振り分けられるようになり、アセットファイルのパスや内容を調査したい時にデバッグがしやすくなり便利です。

# /dev-proxy/localhost

location ~ ^/_vue/(.*) {
    proxy_pass http://vue.local/$1;
}

https-portal

こちらも Docker Hub にて公開されているDocker imageです。 nginx, Let's Encrypt を使用してHTTPSサーバーを立ち上げることができます。 https環境を使用する必要がある場合のために使用しています。

# docker-compose.yml

services:
  https-portal:
    profiles:
      - https
    image: steveltn/https-portal:latest
    ports:
      - 8443:443
    environment:
      STAGE: local
      DOMAINS: 'localhost -> http://nginx-proxy:80'
  nginx-proxy:
    image: jwilder/nginx-proxy:0.9.0

このように設定することで、 https://localhost:8443 へのアクセスをnginx-proxyコンテナに振り分けることができ、自動でLet's EncryptよりSSL証明書を取得してくれます。 また、httpsでの挙動確認などが必要な場面は限られるため、常にhttps-portalコンテナを立ち上げておく必要はありません。 そのため profiles: https という値を設定しておくことで、docker-composeの立ち上げ時に --profile https というオプションを指定した時のみ起動するようにしています。

dockerize

https://github.com/jwilder/dockerize

他のコンテナの起動を待つために使用しているツールです。 vueコンテナ内ではビルド時にwebpack-manifest-plugin を使用しているため、fastifyコンテナの起動時に各ページで読み込むjsのエントリポイントを決める際にvueコンテナの manifest.json を参照する必要があります。 そのため、fastifyコンテナの起動はvueコンテナの起動を待ってから行う必要があり、それを実現するために dockerize を使用しました。

まずfastifyコンテナのDockerfile内で dockerize をインストールします。

# /fastify/Dockerfile

ENV DOCKERIZE_VERSION v0.6.1
RUN wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
    && tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
    && rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz

そしてdocker-compose.yml内のcommandでdockerizeを使用するようにします。

# docker-compose.yml

services:
  fastify:
    command: "dockerize -wait http://vue:8080/webpack-dev-server -timeout 10m -wait-retry-interval 5s yarn serve"

このように記述することでfastifyコンテナは http://vue:8080/webpack-dev-server へのアクセスを試み、成功すれば起動するようになります。 アクセスに失敗した場合には5秒おきにリトライを行い成功するまで待つことができるようになります。 実際に使用する際にはリトライ間隔はdocker-compose起動時に引数として指定できるようにしておくと良いかと思います。

最後に

このようにして、フロントエンドの開発環境を全てdocker-composeの中にまとめることでき、1つのコマンドで依存関係のダウンロードからサーバの起動までをローカル環境を汚さずに行えるようになりました。

気をつけたい点として、vueコンテナに関してはビルドのリソース消費が大きく、またコンテナにマウントするファイルの数も多くなりがちなので、マシンスペックやプロジェクトの規模によってはビルドに非常に時間がかかるようになってしまう場合もあるでしょう。 その場合はvueコンテナはDockerコンテナでのビルドは行わずにローカルで起動させ、ローカルサーバに向けてnginx-proxyの振り分けを設定した方がいいかもしれません。 我々も最終的にはVueについてはDockerを使用せずにローカルサーバを使用することもできるように変更を加えています。

冒頭でも触れましたが、現在ではNuxt.jsやNext.jsなどWebサーバ機能を内包したメジャーなフレームワークも多く存在するためそういった手段を取った方がシンプルな構成にはなるかと思います。 しかし、実際に運用しているプロダクトでは必ずしもそういったフレームワークへの乗り換えが簡単に行えるとは限らず、悩んでいる方も多いのではないかと感じています。

この記事がそういった方に少しでも参考になれば幸いです。

 


ハコベルでは一緒に働くエンジニアを募集中です!

興味ある方はぜひこちらからご応募ください!

物流業界のDXを加速させる!ハコベル事業を牽引するテックリード候補を募集! | ラクスル株式会社

ラクスルのアドベントカレンダー全編はこちらから

Calendar for RAKSUL Advent Calendar 2021 | Advent Calendar 2021 - Qiita

Working in an IN-JA unit

Introduction

Today's article comes from the Josys product team. This article is about the makeup of the product team, the interworking of people from different countries, some of the challenges and how we address them to create a winning product!

About Josys

Josys is the newest addition to RAKSUL's business lines. It is an integrated IT device and SaaS management cloud platform for companies to simplify and optimise their corporate IT tasks as they pertain to their employee lifecycle management. It includes functionalities such as the selection and procurement of IT devices, the maintenance and interplay of the IT user-identify ledger, IT device ledger and software application ledger. The launch of Josys in Japan was on September 1st, 2021.

For RAKSUL, this is the first endeavour where the product development team is predominantly based outside of Japan, in the RAKSUL's India entity called RAKSUL India Private Limited (RIPL). While existing teams in Raksul Printing, Hacobell, and Novasell businesses already cooperate with engineers in Vietnam and India, Josys is the first business unit where the product development team is almost entirely international.

Josys Organization Structure

The entire Josys unit currently consists of about 25 members comprising developers (server-side, front-end and QA automation engineers), business development members, sales interns, operations personnel, product manager, bilingual coordinators and the leadership. The organisation has a flat structure and highly interconnected with no impediments for information flow and knowledge exchange.

The sales and the business development team, including product manager are located in Tokyo, Japan, and so are UX designer and a key developer. The product development team, development contractors, the Chief Architect and the Data Analyst/Scientist are located in Raksul India. The leadership team is based in both Japan and India.

Most of the day-to-day communication across the global Josys team is over Slack (mostly in English with the help of inline translation apps) and regular Agile meetings. The weekly cooperation meeting is where all the teams in the Josys unit participate, and is facilitated by a live translator to ensure everyone's common understanding and participation.

Product Development

The product development flow originates with a business development team's insights gleaned from various customer meetings and interactions in Japan. Internally, these insights will then take the form of hypotheses that require customer validation via mocks, POCs and working prototypes. The cross-functional team discussions will transform these requirements to epics and stories for the product development team to implement incrementally in short sprints (1-2 weeks long). Clarifications and refinements of requirements occur over the duration of the sprint in daily standups and other regular meetings on topics such as error handling, UI implementation, refactoring, etc. Once the feature is released at the end of the sprint, it is demo'd to the entire cross-functional team and the business development team will promote it with the customers. Questions, issues, improvements, bugs and further brush-ups are addressed by the product development team with the business development team playing the role of an effective conduit.

Constraints of a Global Team

While our unit has good cooperation between members in the Japan office and RIPL, there are some minor challenges we face. The first is the dependency resolution due to non-overlapping holidays in the two countries. Sometimes either of the teams can be on holiday while members of the other country are not. Therefore, when some task or query is dependent on a member, it might have to wait for its resolution. However every member in the team is very diligent and takes ownership in absence of any member.

The second challenge is the different time zones. Japan is 3.5 hours ahead of India, hence our "morning" standup meeting starts at 2 p.m. Japan time. We are all always considerate of others' time zone but there are times when a Raksul India member is required to start their day really early, or a Japan team member needs to respond to questions late at night.

Bridging the Language Gap

With the customer base for Josys being only in Japan today, all insights, feedback and requirements need to be gathered and translated to the common language, English, for the product development team to work on them. Customer meeting minutes, documents describing usage trends, issues encountered, insights and user experience design all need to be translated without too much lag. For this, we have bilingual coordinators who act as bridge professionals that facilitate all cross-language coordinations. They help with translation of documents, designs and also conduct real-time translations during meetings. The business background needs to be translated contextually as well as linguistically for the development team. This is where our bilingual team members, our product manager and an engineer in Japan excel and facilitate clear understanding of the business needs by the product development team. Lastly, we work very closely with our CTO in Japan who is also very fluent in english and helps us at every step.

Conclusion

Josys, Raksul's latest business unit and SaaS platform, is powered by a global team of spirited professionals across Japan and India, to bring digital transformation of corporate IT to businesses. Raksul is breaking new ground with this initiative to develop a product from across the globe to serve the Japan market, and every member of the team is displaying amazing adaptability and cooperation to handle the natural challenges of such an endeavour. The success of Josys as a product is an example of great coordination between people that transcends geographical boundaries.

Raksul is hiring in India!

https://www.linkedin.com/company/raksul-india/jobs/

See here for all Advent Calendar articles by Raksul

https://qiita.com/advent-calendar/2021/raksul

Protocol Buffersを使ってSQSのメッセージを構造化してみた

はじめに

こんにちは。RAKSUL Advent Calendar 2021 15日目を担当するハコベルの吉岡です。 本日はメッセージキューを使う際にProtocol Buffersを使って構造化されたメッセージのやりとりができないかのPoC(Proof of Concept)を紹介させていただきます。

背景

今取り組んでいるプロジェクト内でAmazon SQSを使うことになったのですが、現段階でSQSにメッセージをエンキューするのがGoのサービスで、メッセージを受け取るのがPythonのサービスということが想定されています。SQSの技術検証を進める中で「構造化されたデータをメッセージとしてやりとりできないか」とふと思い、「Protobufをうまいこと使えないか」という考えが浮かびました。 事例がないかリサーチしていると、自分がやりたいことと同じことをやっている方の記事があったのでこちらを参考に進めてみました。

今回はメッセージのプロデューサーをGo、コンシューマーをPythonで書いた場合の例を紹介したいと思います。 なお、ある程度Protocol Buffer, Message Queue, Docker, Go, Pythonの知識があることを前提に書いています。ご了承ください。

サンプルコード

いきなりですがサンプルコードはこちらです。

あくまでPoCなのでコードが少し雑なのはお許しください。こちらを見ながら読み進めていただけると理解しやすいと思います。それでは解説していきます。

.protoファイルを用意する

まずはprotoフォルダを作り、その中に.protoファイルを作成します。今回はPoC用のシンプルなメッセージを定義します。

message Message {
  string title = 1;
  repeated Sentence sentences = 2;
}

message Sentence {
  int64 id = 1;
  string text = 2;
}

https://github.com/no7wataru/proto-sqs-poc/blob/main/proto/message.proto

コードを生成する

.protoファイルが用意できたら、続いてはGoとPythonのコードを生成します。protoc実行用のDockerイメージを作り、それを使ってコードを生成してみます。Dockerfileはこんな感じ。

FROM golang:1.17-buster

ARG VERSION=3.19.1

RUN apt update -y && apt install -y curl unzip
RUN curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v${VERSION}/protoc-${VERSION}-linux-x86_64.zip
RUN unzip protoc-${VERSION}-linux-x86_64.zip -d /usr/local
RUN rm protoc-${VERSION}-linux-x86_64.zip

RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
RUN export PATH="$PATH:$(go env GOPATH)/bin"

ENTRYPOINT ["protoc"]

https://github.com/no7wataru/proto-sqs-poc/blob/main/proto/Dockerfile

イメージをビルド。

$ docker build -f proto/Dockerfile -t protoc .

ビルドしたイメージを使って使ってコードを生成します。

$ docker run --rm -v (eval pwd)/proto:/proto protoc --proto_path=/proto --go_out=/proto --go_opt=paths=source_relative --python_out=/proto message.proto

実行するとprotoフォルダ内にmessage.pb.gomessage_pb2.pyというファイルが生成されているのが確認できます。

メッセージキューを立ち上げる

実際の環境ではAmazon SQSを使う予定ですが、今回はSQSと互換性のあるElasticMQを使ってローカルにメッセージキューのサービスを立ち上げて使っていきます。

mqというフォルダを作成してその中にelasticmq.confというファイルを作ります。こんな感じに書いておくとproto-test-mqというキューが起動時に作成されます。

queues {
    proto-test-mq {
    defaultVisibilityTimeout = 10 minutes
    delay = 0 seconds
    receiveMessageWait = 20 seconds
    fifo = false
    contentBasedDeduplication = false
  }
}

https://github.com/no7wataru/proto-sqs-poc/blob/main/mq/elasticmq.conf

あとは以下のようにdocker runを実行するとメッセージキューが立ち上がります。

$ docker run --rm -p 9324:9324 -p 9325:9325 -v (eval pwd)/mq/elasticmq.conf:/opt/elasticmq.conf softwaremill/elasticmq-native

ElasticMQは簡易的なUIも用意されるのでそちらでキューの情報を確認することもできます。9325のポートが簡易UIになります。 http://localhost:9325

プロデューサーを作成する

続いてメッセージを送信するプロデューサーをGoで書きます。出来上がったコードはこんな感じです。

package main

import (
  "encoding/base64"

  "github.com/aws/aws-sdk-go/aws"
  "github.com/aws/aws-sdk-go/aws/session"
  "github.com/aws/aws-sdk-go/service/sqs"
  pb "github.com/no7wataru/proto-sqs-poc/proto"
  "google.golang.org/protobuf/proto"
)

const (
  endpoint  = "http://localhost:9324"
  region    = "ap-northeast-1"
  queueName = "proto-test-mq"
)

func main() {
  sess, err := session.NewSession()
  if err != nil {
    panic(err)
  }

  client := sqs.New(sess, &aws.Config{
    Endpoint: aws.String(endpoint),
    Region:   aws.String(region),
  })

  output, err := client.GetQueueUrl(&sqs.GetQueueUrlInput{
    QueueName: aws.String(queueName),
  })
  if err != nil {
    panic(err)
  }
  println(*output.QueueUrl)

  m := &pb.Message{
    Title: "This is the title of the message",
    Sentences: []*pb.Sentence{
      {
        Id:   1,
        Text: "This is the first sentence.",
      },
      {
        Id:   2,
        Text: "This is the second sentence.",
      },
    },
  }
  data, _ := proto.Marshal(m)
  message := base64.StdEncoding.EncodeToString(data)

  res, err := client.SendMessage(&sqs.SendMessageInput{
    QueueUrl:    output.QueueUrl,
    MessageBody: aws.String(message),
  })
  if err != nil {
    panic(err)
  }

  println(*res.MessageId)
}

https://github.com/no7wataru/proto-sqs-poc/blob/main/producer/main.go

実際にSQSを使う場合ははじめのsessionを作る部分を少し変更する必要があるはずです。ローカルのElasticMQで試す分にはこれで十分です。

メッセージを作成したらproto.Marshal()でシリアライズ → base64で文字列に変換 → エンキュー という流れになります。実際に実行してみます。

$ go run producer/main.go

http://localhost:9324/012345678901/proto-test-mq
a765e3fd-1c39-48f5-b320-54e71a901900

ElasticMQのダッシュボードからもメッセージがうまくエンキューされているのが確認できます。

コンシューマーを作成する

続いてはメッセージを受信するコンシューマーのコードになります。こちらはPythonです。

import os
import sys
import base64
import boto3

sys.path.append(os.path.normpath(os.path.join(
    os.path.dirname(os.path.abspath(__file__)), '..')))
from proto.message_pb2 import Message


endpoint = 'http://localhost:9324'
region = 'ap-northeast-1'
queue_name = 'proto-test-mq'
access_key = 'x'
secret_key = 'x'

client = boto3.resource('sqs',
                        endpoint_url=endpoint,
                        region_name=region,
                        aws_secret_access_key=access_key,
                        aws_access_key_id=secret_key,
                        use_ssl=False)
queue = client.get_queue_by_name(QueueName=queue_name)

messages = queue.receive_messages(
    AttributeNames=[
        'All'
    ],
    MaxNumberOfMessages=1,
    MessageAttributeNames=[
        'All'
    ],
    VisibilityTimeout=0,
    WaitTimeSeconds=0
)

for message in messages:
    data = base64.b64decode(message.body)
    msg = Message()
    msg.ParseFromString(data)
    print(msg.title)
    for sentence in msg.sentences:
        print(sentence.id)
        print(sentence.text)
    message.delete()

https://github.com/no7wataru/proto-sqs-poc/blob/main/consumer/consumer.py

今度はプロデューサーで行ったのと逆の処理をし、データをパースします。

コードを実行すると無事に結果が出力されます。

$ python consumer/consumer.py

This is the title of the message
1
This is the first sentence.
2
This is the second sentence.

まとめ

いかがだったでしょうか?Protocol Buffersを使うことで構造化されたデータとしてメッセージを送受信することができました。メッセージの構造に変更があった際も.protoファイルを更新してコードを再生成するだけなので簡単ですね。 また、サンプルコード内では省略しましたが、SQSにメッセージを送信する際に一緒にメタデータを渡すことができます。.protoのバージョン等をメッセージと共に送信することでデータ構造に変更があった際も柔軟に対応できると思います。 Amazon SQSなどのメッセージキューを使う際には是非検討してみてください。

ハコベルチームでは一緒に働くメンバーを募集しています! 興味ある方はぜひこちらからご応募ください!

ラクスルのアドベントカレンダー全編はこちらから。

ハコベルソリューションチームでのモブプログラミングについて

はじめに

はじめまして!2021年に新卒で入社した横山です!現在はハコベル事業本部 ソリューションチームでハコベルコネクトというサービスの開発を行っています。

ハコベルコネクトは一般貨物の荷主と運送会社との間の配送依頼をWebで行えるようにしたサービスです。車両や依頼先運送会社の管理、請求管理などの配車にまつわる業務をデジタル化することで業務工数を削減することができます。

我々のチームではさらにユーザーの配車工数を削減することをミッションとして日々開発に取り組んでいます。その中で日常的にモブプログラミング(以下モブプロ)やペアプログラミング(以下ペアプロ)を行っているのですが、実際に行っている背景であったり、良かったこと、改善していきたいことなどを紹介します!

> モブプログラミングとは複数人で集まって1つの画面を見ながら書く人と指示を出す人に別れてプログラミングを行> う開発手法です。詳しくは以下の記事が参考になると思います!

モブプログラミングへのいざない | Raksul ENGINEERING

 

背景

ソリューションチームでモブプロを活用しはじめた背景は大まかに2つあります。

1つ目は僕自身が入社してからのキャッチアップに苦労したことです。入社してのオンボードでドメインモデルや既存機能の説明をしていただいたのですが、長年作り上げてきているので複雑になってきており、古くからある機能はドキュメントも少なかったのでキャッチアップがとても大変でした。最近は新しいメンバーが増えてきて、今度は自分がオンボードしないといけない立場にもなってきているので、オンボードフローを改善できないかと考えました。

2つ目は特定の人しか把握していない仕様があることです。初期のサービス開発に携わったメンバーが少なくなってきており、既存機能の実装の背景やドメインモデルに対するドキュメントがなく、知識が属人化していました。新メンバーが加入する頻度も増えてきて、さらに知識や暗黙知の平準化が必要になってきています。

以上のような背景があり、どうすれば改善できるかチームで話し合った結果、普段の開発やオンボードにモブプロを組み込もうとなりました。

 

やってること

サーバーサイドエンジニア同士、フロントエンドエンジニア同士はもちろんのこと、サーバーサイドエンジニアとフロントエンドエンジニア、サーバーサイドエンジニアとQAエンジニアでも行います。

また、新メンバーのオンボードにもモブプロを組み込んでいます。

モブプログラミングの様子

 

サーバーサイドエンジニア同士、フロントエンドエンジニア同士

一番頻度多く行われています。新機能の実装時にはその機能におけるコアの部分を中心にモブプロで実装します。また、新機能でなくても実装が複雑で共有やレビューに時間がかかりそうなものに関してはモブプロで行います。

サーバーサイドエンジニアとフロントエンドエンジニア

フロントエンドとサーバーサイドのエンジニアは基本的にはそれぞれの領域をメインに開発を行いますが、軽微だけどお互いの領域に被って変更しないといけないようなものなどは一緒にペアプロで修正したりします。

サーバーサイドエンジニアとQAエンジニア

RSpec などのテストを対象にモブプロを行っています。古い機能でテストがなくQAエンジニアの手動テストで品質が担保されているような機能を自動でテストできるようにモブプロでテストを追加していっています。

新メンバーオンボード用のモブプロ

新規に入ってきたメンバーにはまずサービス画面を見ながら全体の機能のオンボードと、ドメインモデルに対するオンボードを行います。さらにその後に、オンボード用のタスクをペアプロ・モブプロで行ったり、加入時点でチームで開発している機能のペアプロ・モブプロに参加してもらいコード全体のオンボードを行っています。

良かったこと

新機能のコアとなる部分に対する共通認識ができる

機能のコアとなる部分を議論しながら作るのでこの時点で機能に対する解像度や、実装方針に対する共通認識が生まれます。その結果、後に控えている細々して量の多い作業を同じ認識を持って分担して進めることができるので作業速度も上がりました。また、都度チームでの認識すり合わせを行うので大きな手戻りも少なくなっています。

仕様が複雑な既存機能に対する知見の共有が行えている

仕様が複雑だけど、実装時の背景や実装方針などのドキュメントが残っていない機能に対する機能追加やリファクタリングは読み解くのもつらいですが、得られた知識を他のメンバーに再分配するのも大変です。そのような機能の改修をモブプロで行うことで、互いに理解度を高めながら実装することができ、同時に共有にもなります。

QAエンジニアしか知らなかった仕様などの共有ができた

QAエンジニアの手動テストによって品質が担保されてきたような機能はサーバーサイドエンジニアも把握できていないようなものが多く、テスト追加のモブプロを行うことで仕様の共有になっています。

テストケースの考え方などQAとサーバーサイドの知見が共有できた

「こうゆう場合もテストしておいた方が良いのではないか」とか「この場合のテストはそこまで重要ではない」という議論が、サーバーサイドエンジニアとQAエンジニア両方の視点からの提案で行われるので、テストケースの考え方の知見共有にもなっています。

担当領域を超えた変更も行えるようになった

フロントエンドエンジニアがサーバーサイドのコードを変更したりなど、お互いが書ける、読める範囲が広がることでちょっとした変更などをさくっと実装したりできるようになりました。

新メンバーの立ち上がりが早い

新メンバーのオンボードをモブプロで行うことによって、コード全体の大まかな流れをざっと追うことができたり、分からない事はすぐその場で解決できたりします。また、チームの開発スタイルの共有にもなっており新メンバーの立ち上がりに大きく貢献できていると感じます。

改善していきたいこと

モブプロに参加しなかった人、今後新たに入ってくる人への共有

モブプロに参加した人同士での共通認識や知見の共有は行えています。基本的に共有が必要とされるメンバー全員で行っているので現状では問題はないのですが、得られた情報を永続化はできておらず、その場限りの共有になってしまっています。今後入ってくるメンバーやモブプロに参加してなかった人へも共有する方法などは模索する必要があります。

人によってペースが異なるので深掘りしにくい

特にオンボードにおいてこの問題は発生しました。モブのペースで開発が進むので、途中途中で深ぼってコードを読み込むことができないという意見がありました。モブプロの時間と1人で作業をする時間のバランスやモブプロそのものの進め方を改善していかないといけません。

時間がとられて進捗が遅いように感じる

モブプロを多く行った週は複数人で1つの作業を行うので、リソース効率的には悪くなりベロシティも下がってしまいます。しかし、共有やレビューの時間を短縮できていると考えて許容しています。

まとめ

今回はソリューションチームで行っているペアプログラミング・モブプログラミングの取り組みについて紹介させていただきました。課題に感じていた部分を解決できチーム内の知見共有にはとても役立っています!

ハコベルではエンジニアを絶賛募集中です!

物流業界のDXを加速させる!ハコベル事業を牽引するテックリード候補を募集! | ラクスル株式会社

Datadogを活用したRailsアプリケーションのパフォーマンス改善のススメ

はじめに

こんにちは、ハコベル事業本部 ソリューションスクラムチームの池松です。RAKSUL Advent Calendar 2021 13日目の記事になります。

まだ入社4ヶ月目ですがこういった記事をかかせていただき感謝。

本日は、直近で実施したDatadogを活用したシステムのパフォーマンス改善に関するお話をしたいと思います。

 

背景

ソリューションスクラムチームで開発するハコベルコネクトは、物流における配車業務のデジタル化により、業務自動化・情報一元化を行い、顧客の業務コスト削減を図るサービスです。

ありがたいことに、認知度ランキングでも一位を獲得させていただいており、導入社数も順調に増えてきています。

また、運送業界では12月はクリスマスやお正月などイベントが集中しており、1年で最も忙しい時期で、ハコベルコネクトへのアクセスもが多いときで通常の2〜3倍になることがあります。

こうしたユーザ数の増加や繁忙期の影響で、これまで違和感なく利用できていた機能も徐々にパフォーマンスが悪くなり、UXを著しく下げてしまう問題がありました。

そこでソリューションスクラムチームでは、ハイピークを迎える前にパフォーマンス改善を行うべく、既存の処理のパフォーマンス劣化を引き起こす原因の特定から行うことにしました。

ただ、原因の特定といっても、アクセスが集中するタイミングなどでたまたま重くなるパターンなどもあり、単一のアクセスログなどから調査・改善を進めても、全体への効果としてはイマイチなことが想像されたため、今回はDatadogを活用しました。

初めて利用しましたが想像以上に便利で、もはやDatadogなしでは生きられないぐらいになったので、今回はその機能や使い方について紹介させていただければと思います。

 

要約

  • Datadog APMでは、エンドポイント単位/クエリ単位のパフォーマンス監視が可能
  • それぞれの監視において、合計消費時間の上位から調査・改善していくことで、効率的なパフォーマンス改善が可能
  • 様々な統計情報がわかりやすく可視化されており、パフォーマンスの劣化を引き起こす原因を素早く特定できる
    • エンドポイント単位の監視においては、各リクエストの処理内容・処理時間が可視化されていて、支配的な処理を容易に確認可能
    • クエリ単位の監視では、各クエリがどのエンドポイントから実行されたものかという確認も可能
  • Datadog LogsやMonitorなど他の機能とも連携ができ、運用コストの削減にも効果的

 

 

Datadogとは

SaaS向けのデータ分析プラットフォームで、詳細については公式サイトをご確認いただければと思いますが、今回のパフォーマンス改善という目的では、以下のような機能を提供してくれます。

  • ログ集約・検索
    • すべてのサービス、アプリケーション、プラットフォームからログを自動収集
    • ログ、メトリクス、リクエストトレース間をシームレスにナビゲート
    • ログデータを可視化、アラート表示
  • パフォーマンスの可視化
    • 分散したシステム全体のリクエストをトレース
    • エラー率やレイテンシーのパーセンタイル統計 (p95、p99 など) をグラフ化およびアラート

本記事の解説内容

実際に分析を行う前のインフラ設定やアプリケーション側の設定など、必要な作業はいくつかありますが、本記事ではあくまで導入したときの作業イメージや効果にフォーカスしてお伝えできればと思います

改善の流れ

Datadogの機能の一つであるDatadog APM (Application Performance Monitoring) では名前の通りアプリケーションのパフォーマンス監視を行えます。

Railsインスツルメンテーションを有効にした場合、リクエスト、データベース呼び出し、テンプレートのレンダリング、およびキャッシュの読み取り/書き込み/削除操作をトレースすることができます。

この状態で行える監視は主に2つあります。

1つは、エンドポイント単位のパフォーマンス監視、

もう1つは、クエリ単位のパフォーマンス監視です。

エンドポイント単位のパフォーマンス監視

こちらが画面の全体像です。

主要な可視化機能としては、以下になります。

  • 時間帯毎のリクエスト数
  • 時間帯毎のエラー数
  • 時間帯毎のレイテンシ
  • 時間帯毎の消費時間比率
  • レイテンシの分布
  • 指定期間内のエンドポイント別の統計情報
    • リクエスト数
    • 合計時間
    • P50, P99
    • エラー数・エラーレート

どれも有用な指標ですが、今回特に利用したのが指定期間内のエンドポイント別の統計情報です。

パフォーマンス改善は、やればやるほど効果はでるけど、基本的にはキリがなく、より少ない労力で高い効果を得られることが求められると思います。

そこで、エンドポイント単位でトータル時間の降順に並べ、上位に来るものの中でリクエスト回数が少ないものが、パフォーマンス改善すべき要素が多い可能性が高く、かつ効果が高いと判断して、それらを優先して見直すことにしました。

上記の指定期間内のエンドポイント別の統計情報から特定のエンドポイントを選択すると下記のようなエンドポイント別の統計情報ページに遷移します。

こちらでも時間帯ごとの各種指標やレイテンシ分布が見れ、かつ、以下のような情報も確認できます。

  • エンドポイント内の各種処理ごとの実行時間と処理全体における割合
    • DB/Webなどでの絞り込みも可能
  • 対象のエンドポイントの各リクエストの発生時間帯とそれらのレスポンスタイム等
    • レスポンスタイム順でソートすることができ、最も時間のかかったリクエストも確認可能

上記からエンドポイント全体で時間のかかった処理を統計的に判断できるとともに、局所的に重くなっているリクエストの詳細を確認することなどもできます。

更にリクエストを一つ選択すると、下記のようなリクエスト単位で視覚的にどの処理にどれくらいかかったかわかるような画面が表示されます。

実際みてみるとあるモデルのActiveRecordのインスタンス生成で44%ほど使っていることがわかります。

(モザイクしているので見にくいですがなんとインスタンス数まで表示されます)

例えばこのパターンだと不要なインスタンス生成を行っていないかといった調査につなげることが可能です。

クエリ単位でのパフォーマンス監視

これだけでも十分なのですが、またクエリの観点から改善対象を調査することができます。

こちらがその画面の全体像です。

基本的にはエンドポイント単位でのパフォーマンス監視と同様ですが、クエリ単位での統計情報がこちらには表示されます。

こちらも同様にTOTAL TIMEで降順にならべて、上位の中でリクエスト数が少ないものに注目して調査を行いました。

実際に見てみると、対象のクエリがパルス的に発生していることがわかり、

また下記のようにこのクエリがどのエンドポイントからどれくらい呼ばれているか確認できる機能があるのですが、ある特定のエンドポイントからだけ発生するクエリだということがわかります。

結果として、このクエリ自体重かったので別途処理の改善は行うとともに、別サーバから定期的にAPIが叩れていることが判明し、それらの連携方法を改善することでパフォーマンスの向上につなげることができました。

余談ですが、パフォーマンス改善が行われるとDatadogからお祝いしてもらえたりします。

(watchdogという機能があります)

その他Datadogの活用

今回はAPMに関する説明をメインでしましたが、Datadogには他にもLogsやMonitorなど様々な機能が提供されています。

軽くだけ紹介させていただくと、Datadog Logsはログの集約・検索を行うもので、ログのパース設定などを適切に行うことで検索したいログへのアクセスと統計情報の表示を簡単にできるようにしてくれるものです。

なんとこのLogsとAPMは相互連携可能で、先程のAPMのリクエスト詳細画面からLogsのリンクをたどることで、具体的にどんなリクエストパラメータが付与されていたかなどの確認ができるようになっています。逆にLogsからAPM側にたどることもできるので問い合わせ対応などをより効率的に行うことができます。

Datadog Monitorは、事前に設定したルールに基づき、その条件を満たした場合にアラートを出すことができる機能です。アラートはslack通知することも可能です。例えばですが、私達は5分おきに全エンドポイントに対して特定の秒数を超えるリクエストが発生した場合にアラートを出し、slack通知を送るようにすることでパフォーマンスの劣化を検知できるようにしています。

今後導入を検討している機能としてはDatadog Databasesがあり、こちらはDBと直接agentをつなげることで、クエリの実行計画なども収集することができるものになります。

ほんとDatadogすごいです...

ありがとう...Datadog...

また機会があればこのあたりも紹介させていただければと思います。

おわりに

ハコベルチームでは一緒に働くメンバーを募集しています!

興味ある方はぜひこちらからご応募ください!

物流業界のDXを加速させる!ハコベル事業を牽引するテックリード候補を募集! | ラクスル株式会社