Medley Developer Blog

株式会社メドレーのエンジニア・デザイナーによるブログです

Terraform のテスト環境を Terraform Workspaces で構築した

株式会社メドレーのエンジニアの笹塚です。 主にジョブメドレーのインフラを担当しています。

  • 直近では、コンテナ化されていなかった環境の移行などをしていました。
  • 休日は主にゲームをやっています。今は、日本語版がリリースされたばかりの「レムナント:フロム・ジ・アッシュ」に夢中です。

今回は Terraform のテスト環境を、Terraform Workspaces を使用して構築した事例を紹介します。

背景

ジョブメドレーにはいくつかのサービスがあり、Terraform によるコード化が進んでいるサービスと、Ansible、 itamae などで部分的にコード化はされているものの、Terraformの使用が遅れているサービスが混在しており、これらのサービスについても Terraform への移行を進めています。

Terraform への移行とあわせて、メンテナンスを担当するメンバーを増やす必要があるのですが

【作業担当】

  • Terraform のコードから、実際に稼働させる環境を作るのは慣れていても難しい。

【レビュワー】

  • Terraform のコードの差分だけで、内容をすぐに把握するのは難しい。

などの理由から、作業担当、レビュワーともにハードルが低いとは言えず、Terraform によるインフラの変更内容を事前に確認できる環境構築が必要だと感じるようになりました。

検討内容

事前に確認できる環境を作るにあたり、まず最初に検討したのは、各ステージのコードを共通化することでした。 ジョブメドレーの関連サービスでは、大きくわけて3つのステージで構成しています。

Sandbox(個人の検証用) → QA(リリース前の検証用) → Production

この各ステージのコードを共通化できれば、Production には QA 環境までで確認済みのコードを apply することができます。

ですが、Sandbox 環境、QA 環境、Production 環境はそれぞれ似てはいるものの、全てが同じ構成ではありません。

f:id:medley_inc:20200731151334p:plain

Terraform の HCL では

などの制限があり、構造の差分をコードで吸収しにくく、共通化できたとしてもメンテナンス性を下げる可能性が高いです。 また、すでに Terraform でコード化されている状態からの移行作業も楽ではないでしょう。

そこで、ステージごとのコードを共通化するのではなく、AWS Organizations で別アカウントを作り、各ステージのコードを試せる環境を作ることにしました。

f:id:medley_inc:20200731151544p:plain

<目標とする>
Terraform のコードをプロダクションアカウントで実行する前に、テストアカウントで実行し、設定した内容を AWS マネジメントコンソール や AWS CLIで確認することできる。

  • ストアカウント上で設定を確認した後、プロダクションアカウントに同じコードを apply することができる。
  • コストを抑えるため、テストアカウント上のリソースは、確認が終了したら destory することができる。

<目標としない>

  • ストアカウント上でサービスが稼働することは目標としない。
  • 必要なデータセットの作成など用意するものが増えるので、目標には含めませんでした。

設定作業

必要な作業は大きくわけて2つです。

  1. Terraform Workspaces の設定
  2. アカウントごとに必要なコードの追加、修正

Terraform Workspaces の設定

Terraform と AWS provider の設定を変更することで、workspace を切り替えることができます。

以下が作業イメージです。

workspace の追加

$ terraform workspace new production
$ terraform workspace list
  default
* production

Terraform の環境変更

terraform {
 required_version = "= 0.12.28"
 backend "s3" {
   bucket               = "example-state"
   workspace_key_prefix = "workspace"
   dynamodb_table       = "terraform-state-lock"
   key                   = "example.tfstate"
   region               = "ap-northeast-1"
   profile              = "production"
 }
}

provider "aws" {
 region  = "ap-northeast-1"
 version = "= 2.70.0"
 profile = var.workspace_profile[terraform.workspace]
}

variable "workspace_profile" {
 type = map(string)
 default = {
   default    = "test"
   production = "production"
 }
}

この例では、default workspace はテスト環境、Production を実際にサービスが稼働する環境のアカウントになるように設定しています。 それぞれ default は、aws config の test profile、Production は production profile を参照しています。

s3://example-state/example.tfstate がテスト環境、s3://example-state/workspace/production/example.tfstate が、プロダクション環境の state ファイルになります。

Terraform のコード内からは、現在の workspace 名を terraform.workspace で参照することができます。

ここまでの設定で

$ terraform workspace select [workspace名]

で workspace を切り替えることができるようになりました。

あとは、アカウントごとに必要な変更をしていきます。

アカウントをまたいだ共通のリソースの定義

全アカウントでユニークにする必要があるリソース(S3 bucketDNS など)の場合は、名称の分岐処理が必要です。 テストアカウントでは、リソース名に prefix をつけて作成するようにしました。

# 定義
 variable "example_bucket_name" {
 type = map(string)
 default = {
   default    = "test-example-bucket"
   production = "example-bucket"
 }
}

# リソースでの参照時
resource "aws_s3_bucket" "example" {
 bucket = var.example_bucket_name[terraform.workspace]
 ..
}

ストアカウントで起動するインスタンスタイプや台数の変更

ストアカウントでは EC2 のインスタンスを起動させなくても良い場合には、台数を変更するようにしました。

resource "aws_autoscaling_group" "example_web" {
  name             = "example-web"
  max_size         = 12
  min_size         = 6 
  desired_capacity = terraform.workspace == "production" ? 6  : 0}

インスタンスの起動が必要な場合も、同様の分岐でインスタンスタイプの変更を行っています。

コード化をしないリソースの扱い

プロダクションアカウントで一旦コード化を保留したリソースについては data source で参照しますが、テストアカウントにはリソースが存在しないため作成しなければいけません。 テストアカウントのリソースは確認が終了した段階で destroy したいので

  • ステージ用の Terraform コードとは別に、必要なリソースを作成するコードを用意する
  • 必要なリソース用のコード → ステージ用のコードの順に apply する

ようにしました。 data source で参照できれば良い範囲でのコード化なので、この追加のリソースも最小限のコストになるようにしています。

以上の設定で、同じ Terraform のコードを workspace を切り替えて plan、apply ができるようになりました。

運用してみて

プロダクションアカウントに apply する前に、テストアカウントで事前に apply することができるようになったので、作業中の試行錯誤もしやすくなりました。

レビュワーも、テストアカウント上で実際に apply された結果を確認することができるようになり、apply の差分だけではわかりにくかった変更内容を確認できるようになりました。

これなら新しく担当するメンバーの不安を、少しかもしれませんが解消できそうです。

まとめ

今回は Terraform のテスト環境を、Terraform Workspaces を使用して構築した事例を紹介させていただきました。 テストアカウント上のリソースの自動 destroy や、 plan、apply の自動化については触れられていませんが、また別の機会に紹介できればと思います。

長らく運用しているサービスでは、サービスを稼働させたまま解決しなければいけない課題が数多くあります。 それらの課題を、一つずつ着実に解決していくことに楽しさを見いだせる方、ぜひメドレーで一緒に働きましょう!

www.medley.jp

PWA, PRPL Pattern の概要と採用状況の調査

こんにちは。メドレーにてジョブメドレー開発エンジニアをしています、矢野と申します。

  • ジョブメドレーでは、主にバックエンド ( Ruby on Rails ) の改修を担当してます
  • 直近では 「サイトパフォーマンス改善施策」 として、Rails コードのリファクタリングによる TTFB 高速化に取り組んでました
  • 「もう絶対にコケないのが分かってる」ビルドやテストを、手元のコンソールで何度も叩いて「わー。ちゃんと通る!」っていう時間が好きです

今回は、上記の「サイトパフォーマンス改善施策」の文脈で調査した、PWA の実装パターンである PRPL Pattern という リソース提供の設計アーキテクチャ について紹介します。

PRPL Pattern とは

f:id:medley_inc:20200716154256p:plain

ref. Apply instant loading with the PRPL pattern - web.dev

PRPL Pattern は、Google I/O 2016 で提案された PWA - Progressive Web Application の構築・配信のための設計アーキテクチャです。

Web サイト・アプリケーションが、回線強度やスペックが高くないスマートフォンなどのデバイスでもストレスなく機能するよう、リソース配信とアプリ起動時のパフォーマンス ( = 高速化 ) に重点を置いています。

PRPL meanings

では具体的に「どうやって速くするの?」ということで、PRPL が提唱している 4 つのサイトレンダリング手法について見ていきます。

  • Push: <link preload> および HTTP/2 を使用して、初期 URL ルートの重要なリソースを Server Push する
  • Render: クライアントが初期ルートをなるべく早くレンダリングする
  • Pre-cache: 残りのルートをクライアントが Service Worker でプリキャッシュする
  • Lazy load: クライアントはオンデマンドで残りのルートを遅延読込みして作成する

PRPL は上記 4 つの頭文字をとったものですね。 PRPL は HTTP/2 の Server Push や、PWA の Service Worker など、Web プラットフォームの最新技術を駆使してサイトパフォーマンスをあげよう!というプラクティスです。

PWApps とは、最新の Web 技術を有効に活用し、漸進的 ( Progressive ) に高度なユーザー体験を提供しようとする概念です。この PWApps の概念を具体化する一つの手法として、「 PRPL 」 ( パープル ) と名付けられた開発・提供パターンが提案されました。

(中略)

Web Components や、 Service Worker、 HTTP/2 Server Push といった Web の最新技術をフルに活用し、レスポンス性の高いユーザー体験を提供しようというものです。

ref. Googleが新たに提唱する Progressive Web Apps の新たな開発パターン「 PRPL 」とは?

周辺知識 - HTTP/2

まずは、周辺知識からおさらいしていきます。PRPL は HTTP/2 の Server Push を利用する、という話でした。そもそも HTTP/2 とはどんなものでしょうか。

  • Hyper Text Transfer Protocol と呼ばれる TCP 上の通信プロトコルの次世代バージョン
  • 普段私たちが Web サイトを閲覧する際に利用しているプロトコル
  • HTTP/1.1 が 1997 年に策定され、2015 年にようやく /2 が標準化
  • Express, Apache, Nginx など各 Web サーバ、各ブラウザも対応してきている

ref. HTTP の進化

現行の一般的なバージョンは HTTP/1.1

HTTP は普段私たちが Web サイトを閲覧する際に利用する通信プロトコルです。HTTP/2 はその次世代バージョンになります。HTTP/1.1 には以下のような特徴があります。

  • ステートレスな通信
  • テキストベースで情報をやりとりする
  • 原則 1 リクエストに対して 1 レスポンスである
  • → 複数リソースを得るために何度もリクエストしてコネクションを貼り直す必要があり パフォーマンス上の課題がある

これに対して、1.1 の次期バージョンである HTTP/2 は以下のような特徴があります。

  • 通信時にヘッダを圧縮し使い回す省エネ設計 = 一部ステートがある
  • バイナリベースで情報をやりとりする
  • ストリームという概念で、1 コネクション中で Request / Response を多重化できる
  • 1 コネクションの中で複数リソースを並行して Request / Response できる!
  • リソースの Server Push が可能

HTTP/2 自体が HTTP/1.1 の課題であった通信のオーバヘッドを改善する規格であることがわかりますね。また「 request / response の多重化」により、1.1 と比較してどの程度「速く」なるのかについては、以下 Akamai 社のブログサイトが参考になります。

HTTP/1.1

f:id:medley_inc:20200716154352p:plain

HTTP/2 ( request / response の多重化 )

f:id:medley_inc:20200716154439p:plain

ref. HTTP/2を活用するパフォーマンス最適化 ADAPTIVE ACCELERATION

HTTP/2 の採用事例

f:id:medley_inc:20200716154502p:plain

ref. caniuse.com

上記対応状況からも分かる通り、モダンブラウザでは一通り対応しています。実際のプロダクションでも 日本ではメルカリさん、世界的なサービスだとTwitter, Facebook, Instagram などSNS サービスや、Slack, Dropboxなどが HTTP/2 に対応しているようです。

ジョブメドレーはまだ HTTP/1.1 でのサービス提供しか行っていませんが、ゆくゆくはバージョンアップ対応を行っていきたいと思っています。

周辺知識 - PWA

次に、PWA についておさらいします。PRPL は PWA の Service Worker を利用した実装パターンという話でしたが、そもそも PWA や Service Worker とはどんなものなのでしょうか。

PWA - Progressive Web Application

  • Web プラットフォームの新機能を使って「ネイティブアプリとウェブアプリのいいとこ取りした、UX の高いウェブアプリ」という概念
  • ウェブアプリの特性 ( Secure, Linkable, Indexable … ) を保ちつつ、ネイティブアプリの多機能さ ( インストール、プッシュ通知、オフライン動作 … ) を最新のブラウザ機能 = JavaScript API で実現する
  • 必ずしも SPA であったり、最新機能の全てを使っている必要はなく、「斬新的に Web 新 API でネイティブな機能を取り入れていける」というコンセプト

ref. プログレッシブウェブアプリの紹介 - MDN

PWA を構成する新機能たち

  • HTTPS: HyperText Transfer Protocol Secure 、 SSL / TLS による通信の暗号化
  • Service Worker: Web ページで動作するスクリプトから独立したイベント駆動型の worker
  • Cache API: Request / Response オブジェクトのストレージキャッシュ
  • Push API / Notifications API: サーバーからアプリへの通知送信
  • マニフェスト: アプリストアを通さず Web アプリをホーム画面にインストール可能

ref. プログレッシブウェブアプリ - MDN

上記以外にも、PWA には様々な API ・機能が存在します。その中でも PWA の、そして PRPL アーキテクチャの中核を成す重要な機能が Service Worker です。

Service Worker とは

  • ブラウザのバックグラウンドプロセスとして動作する Worker
  • Web ページで動作するスクリプトとは独立して動作する
  • サーバサイドでいうところの Worker プロセスと同じような使い方ができる

ref. Service Worker の紹介

Web ページの JavaScript プロセスとは切り離された文脈で、予め登録しておいた処理を、様々なイベントに応じて発火させることができるイメージですね。

プッシュ通知など、いわゆる「ネイティブアプリのような機能」は、この Service Worker を利用することで実現しています。

PWA の採用状況

Google からの提唱当初 ( PWA も Google のプロジェクトです ) こそ、先進的すぎてなかなか受け入れられなかった PWA ですが、2020 年現在は各ブラウザの対応状況も少しずつ向上されています。

日本では SUUMO、日経電子版、一休.com、世界的なサービスだと Instagram などが PWA による Web サイトを提供しているようです。

特に、既にネイティブアプリで大成功している Instagram が、回線・端末スペックの低い新興国をターゲットとした PWA をリリースしているという点はプロダクト観点からもとても興味深いですね。

PRPL パターンの利点

さて、話を戻して PRPL パターンが HTTP/2 や Service Worker を使って、具体的にどのようにサイトパフォーマンスを向上するのか?という点を見ていきます。

Server Push + Service Worker による Pre-cache

PRPL の 4 要素を、改めてもう少しわかりやすく記載してみると以下のようになります。

  • Push
    • 初回コネクションで HTTP/2 Server Push で 必要リソースをまとめて Push
  • Render
    • 上記で受け取った HTML リソースを元に初期画面をレンダリングする
  • Pre-cache
    • 上記初期画面で利用されるリソースは、Server Push により非同期的に Service Worker が Pre-cache する
    • また、今後利用しそうな追加リソースについても、非同期・投機的に Service Worker が事前に DL 、キャッシュする
  • Lazy load
    • 初期画面以降で必要になった画像などのリソースを、画面スクロールなどを検知し、表示に必要になったタイミングで遅延読み込みする

上記の太字箇所を画像で説明すると、以下のようになります。

HTTP/2 ( Server Push )

f:id:medley_inc:20200716154542p:plain

HTTP/2を活用するパフォーマンス最適化 ADAPTIVE ACCELERATION

HTTP/2 の Request / Response の多重化だけを利用したケースと比較すると、「ブラウザがページを解析して、必要リソースを Request する」よりも前に「サーバが必要リソースを強制的に Push 」しているのがわかります。

この Push されたリソースを Service Worker が受け取り → キャッシュ化することで 「ページ解析が終わった時点では既に必要リソースがブラウザにキャッシュされている」 状態となり、アプリの初回起動が速くなる、というのが Push & Pre-Cache の速度改善の仕組みです。

Service Worker で必要になりそうなリソースの事前キャッシュ

また、初期画面に必ず必要なリソース以外については、「このあと必要になりそう・なるはずの追加リソース」ということで、Service Worker に投機的に事前 DL → Pre-cache されることも可能です。

このあたりのキャッシュ戦略・導入事例は以下の一休さんの記事が詳しいです。

一休.comにService Worker(Workbox)を導入しました

PRPL Pattern の採用状況

さて、そんなパフォーマンスに嬉しい PRPL Pattern ですが、HTTP/2、PWA 自体の普及率も高くなくまだまだプロダクションでの採用事例は少ない印象です。

  • Google I/Oで日経電子版が事例として紹介された話
    • PRPL パターンを参考にした Service Worker を使ったキャッシュ、HTTP/2 Push でのリソース配信などが採用されている
  • ライブラリでは有名どころだと Gatsby が標準対応、当たり前だが Google の Polymer ライブラリも PRPL パターンで実装されている

とはいえ、PWA 化している Web サイトであればパターンの適用はそこまで難しくありません。

また Next.js の PWA 化ライブラリ next-pwa では、Next.js 本体の Code Splitting 機能と連携した「 Service Worker での追加コード読み込み」をサポートするなど、このようなアーキテクチャパターンの潮流は今後も派生していくのかな?という気がしています。

まとめ - HTTP/2 + PWA + PRPL Pattern

まとめです。

  • PWA とは
    • Web プラットフォームの新機能を使った「ネイティブアプリとウェブアプリのいいとこ取りした UX の高いウェブアプリ」という概念
  • PRPL パターンとは
  • プロダクトで使えるのか
    • Web サイトの PWA をする/しているのであれば、サーバの HTTP/2 化をして、リソースの Push、Pre-cache を導入するのはパフォーマンス観点で十分検討できるのでは
    • 但し、SPA + SSR 構成のサイトでは、Next.js などフレームワークのコード分割に寄せるのが今の所無難そうではある

今回は調査のみで、プロダクトへの実践投入は行いませんでしたが、今後プロダクトの PWA 化が企画されるような場合は、ぜひ導入してみたい技術だなと感じました。

以上、ここまで読んでくださり、ありがとうございました。

ECSサービス間の通信をAmazon ECS サービスディスカバリで実現した話

株式会社メドレーのエンジニアの阪本です。

緊急事態宣言も開け、普段の生活を取り戻しつつあるこの時期、 皆さんはいかがお過ごしでしょうか?

私は野球観戦(虎党)を毎日の楽しみとしています。 今年はコロナ渦の影響で開幕予定が遅延したものの、自粛期間を経て6月中旬にめでたくシーズン開幕を迎えることができました。 ここまでの「長い冬」が明け、テレビをつけると野球が見られる。 これで私自身も2020年が開幕したなと実感しています。

今回は、私がインフラ開発時に直面した問題と解決までの事例について紹介させて頂きます。

背景

私はジョブメドレーのサービス開発を行っています。 このシステムは多くの機能で構成された大規模なもので、AWSのElastic Container Service(以下ECS)にて稼働しています。

このシステムに対し既存機能のリプレース案件に携わる機会がありました。 現在のシステムは多くの時間をかけて多くの機能を実装した結果、かなり大きなコードとなっています。 これにより、一つの変更が及ぼす影響が甚大なものになり得る状況だったため、リプレース対象の機能を新システムとして別アプリケーションに切り出して開発することにしました。

f:id:medley_inc:20200629201809p:plain

しかし、別システムとして一部の機能を切り出すものの、この機能は 既存システムとの連携が必要となります。 そのため、この連携をシステム間のAPIリクエストで実現することにしました。

課題

ここで1つの問題が発生しました。 システム間の通信が必要になりましたが、お互いECSサービスで分離した構成となるため このままではアドレスの解決が出来ないことに気づきました。

同一ECSサービス内でかつ、ネットワークモードがawsvpcモードであれば ポート番号を分ける事により相互でアクセスが可能であるものの 異なるサービスであればポート以前にアドレス解決ができません。

f:id:medley_inc:20200629201843p:plain

そのため、何らかの手段を持ってお互いの場所を認識できる状態にする必要があります。 そこで、これを実現できる幾つかの方法を検討しました。

アプローチ

その1 全て1つのECSのサービスにまとめる

f:id:medley_inc:20200629201919p:plain

上記の通り、既存システムが動いているECSサービス/タスクに新システム(のコンテナ)を全て混ぜる方法です。 この場合、全てのポートを個別に割り振ることで127.0.0.1:portによるアクセスが可能となるため 相互のリクエストも実現できることになります。

ただ、インフラ的には2つのシステムが1つの塊として構成される事になるため デプロイの単位やスケールの単位を常に双方共有することになります。

f:id:medley_inc:20200629201942p:plain

こうなると、せっかくアプリケーションを分けたにも関わらず 運用の部分では何もメリットを得られないどころか制約が増えただけのようになりそうなのが問題です。

その2 内部Application Load Balancerを経由する

f:id:medley_inc:20200629202007p:plain

VPC内部にinternalなApplication Load Balancer(以下ALB) を設置し、接続先となる既存システムをTargetGroupに登録します。 この方法であれば、ALBのエンドポイントに向けてリクエストすることで配下の既存システムにアクセスする経路が確保できます。

またECSサービスとTargetGroupが紐づくことにより既存システム側のデプロイやスケールが自動的にALB側にも連動することになるため、新システム側は既存システムのステータスを意識する必要は少なくなります。

これだと不自然な点も無くアプリケーション間の通信経路も確保できると期待しましたが・・・新たに問題が発生しました。

検証時にALB/TargetGroupを新規作成。 ECSサービスについては 後付けでTargetGroup脱着はできない 複数TargetGroupの付与はAWSコンソールでは対応していない といった理由のためにAWS CLIでの作成作業を行ったのですが、下記エラーが発生してしまいました。

An error occurred (InvalidParameterException) when calling the CreateService operation: load balancers can have at most 5 items.

これはAWSによる制限で、1つのECSサービスに関連付けることのできるTargetGroupは最大5つまでという事を表しています。 つまり既存システムは多くの機能やコンテナが同居しているECSサービスとなっていたため、既にTargetGroupが5つ存在しており上限に達していたのです。

サービスで使用するロードバランサーを表すロードバランサーオブジェクト。Application Load Balancer または Network Load Balancer を使用するサービスの場合、サービスにアタッチできる 5 つのターゲットグループの制限があります。

docs.aws.amazon.com

この方法を取るなら不要なTargetGroupを削るかまとめるかの手を打つ必要がありますが、 周辺環境に対する影響があまりにも大きい為に現実的ではありませんでした。 そのため、新たな選択肢を探すことにしました。

その3 Amazon ECS サービスディスカバリを使用する

そんな中、ECSの機能としてサービスディスカバリ(サービス検出)というものを見つけました。

aws.amazon.com

f:id:medley_inc:20200629202146p:plain

これはECSサービスとRoute53 内部ホスト空間を紐付ける機能です。 またECSサービスの起動・停止・スケールといった対象が変動した場合にも、自動的にホスト空間のレコードを登録/解除し最新の状態に追従してくれるものです。

この場合、別々のサービス間でもお互い相手の名称を把握することができる上、 TargetGroupも新規に必要としないため、今回のニーズに適した方法になりそうです。

f:id:medley_inc:20200629202201p:plain

導入してみた結果

試しにECSサービスにサービスディスカバリを導入し、疎通できるか試してみます。 既に存在するサービスに後付でのサービスディスカバリは出来ないため、新規にサービスを作成 することになります。

f:id:medley_inc:20200629202228p:plain

今回は初めてのサービスディスカバリとなるので、名前空間も同時に作成することになります。 ひとまず名前空間を「medley-blog.local」、サービス検出名を「service-discovery」としてみます。

f:id:medley_inc:20200629202502p:plain

このままECSサービスを作成すると、Route 53にて新たな名前空間medley-blog.local」が作成されていることが確認できます。

f:id:medley_inc:20200629202249p:plain

この状態でサービスのタスクを1つ起動ししばらく待つと、サービス検出名.名前空間となる service-discovery.medley-blog.local にAレコードが追加されています。 このレコードに紐づくIPアドレスこそ、ECSタスクに紐付けられているIPアドレスになります。

f:id:medley_inc:20200629202731p:plain

あとはこの名称で別環境から疎通できるか試してみます。 curlコマンドでservice-discovery.medley-blog.localで通信してみると、見事にレスポンスを受け取ることが出来ました。

f:id:medley_inc:20200629202751p:plain

※ここでは検証のため、接続先アプリケーションはNginxコンテナを配置しています

これにより、異なるECSサービス間での通信を実現することができました。

今後の予定

現段階では検証段階のため評価環境への導入のみとなりますが、今の所は大きな問題も発生せず順調に稼働しています。 このまま特に問題無く稼働できれば、本番環境への導入も目指したいと思います。

さいごに

メドレーのエンジニアは大小問わず課題に対して真摯に向き合い、試行錯誤し、突破口を開く取り組みを常に続けています。 そんな我々と一緒に働きたいと思った方、まずは下記リンクからご応募いただきカジュアルにお話しませんか?

www.medley.jp

デザイナーがデザインツールを使わずに、Reactを使ってデザインした話

メドレーのデザイナー酒井です。最近、JobMedleyからCLINICSに異動しました。 自分はデザインはもちろん、HTML/CSS/JS実装してプルリク送ったりしているちょっとフロントエンド実装領域に軸足が寄ったタイプのデザイナーです。

ここでは以前所属していたJobMedley事業部の話をさせていただきます。

当時、JobMedleyの社内システムのリニューアルプロジェクトにデザイナーとして参加していました。通常、デザイナーがデザインをするときにはSkecthやFigmaのようなデザインツールを利用するのが一般的かと思います。

弊社でも基本的にはデザインツールでデザインを行うことが多いのですが、プロジェクトによっては、よりリアルなモックアップが必要なため、デザイナー自身がコーディングでデザインを行い、ブラッシュアップしていくことがあります。その後、フロントエンド実装者が、デザイナーが作ったデザインを参考に、しっかりと設計されたもので作り直します。

今回のリニューアルプロジェクトのデザイン・モックアップの制作では、FigmaやSketchなどのデザインツールは使用せず、Reactでコーディング行い、デザインを制作しています。ここが特殊な制作フローになると思うので、このエントリではReactをデザインツールとして使ったときの流れとメリット・デメリット、ちょっとニッチなポイントに絞ってお話していきます。

デザインの流れ

1.まずはReactを勉強する

元々受託制作会社でデザイナーをやっていた時代にVue.jsでの開発経験はありましたが、Reactは今回が初めてでした。そのため、一旦公式チュートリアルを一通り読み、Googleなどで調べつつ、基礎知識のインプットすることからはじめました。

2.ワイヤーフレームを作る

さすがにワイヤーフレーム無しでコード実装には入れないので、ここではFigmaなどのツールを使います。ワイヤーフレームを作り、要件定義を進めていきますが、デザインに関してもある程度のあたりを付けていきます。

f:id:medley_inc:20200619172937p:plain

3.開発環境構築

今回はNext.jsで制作を行っています。 そもそも、プロダクトの実装でNext.jsを使用することが決まっていたので、デザイン側もNext.jsを採用しましたが、結果的に良かったなと感じています。

Next.jsで作っておけば、めんどくさいrouterの設定やデザイナーが苦手なwebpackのconfigを記述しなくてすみます。

また、next build && next exportで静的なHTMLファイルとして書き出されるため、サーバがあればすぐに共有が可能です。今回はエンジニアさんに、developブランチにmergeされると、next build && next exportが走り、自動でAmazon S3に静的ファイルをデプロイされるようにしてもらいました。

この時点でプロダクト側のフロントエンド設計も進んでいたため、ESLintやコンポーネント設計、ライブラリなどを流用し、デザインプロジェクト用に最適化させます。

4.Next.jsの設計

実際にエンジニアがプロダクトを実装する際には色々なことを検討する必要があると思いますが、デザインツールとして使用する際には、逆にそこまでガチガチにしてしまうと実装に工数がかかりすぎてしまうので「割り切り」が必要になります。私が実装した際の環境は以下のようなものです。

  • TypeScriptは入れない。要件が固まりきっていない状態で型定義をやり始めると修正に時間がかかりすぎるため。
  • 非同期通信処理も入れない。データがほしければローカルにjsonファイルを配置しておく。
  • デザイン側のソースコードの汚さはあまり気にしない。プロダクト版を作成するときにフロントエンドエンジニアがきれいに作り直してくれる。
  • コンポーネント設計を意識する。共通コンポーネントとして使用する汎用的なものは先に洗い出しておく。
  • テストコードは書かない。そこまでページ数が多くない上、上記のような割り切りをしているので複雑性も極端に上がらない。
  • Google Chrome最新版のみ対応とする。他のブラウザは無視する。

(昨今のフロントエンド開発の流れと真逆、、、)

5.デザインルール設計

続いてデザインルールの設計を行います。

theme.jsファイルを作成し、以下を定義します。この時点で定義が完全にできていることは無いと思うので、分かる範囲で書いていきます。最終的に、不要なものを消したり、一緒にできるものは統合して定義を減らしていきます。

コンポーネントでpaddingやmarginの余白を数値で指定したり、色味のカラーコドをベタ打ちせず、必ずthemeファイルから値を引っ張ってくるようにします。そうすると、修正が容易になる上、あとからどこで何が使われているかを把握しやすくなります。

■theme内に記述する内容のイメージ

fontFamily// fontFamilyを定義
colors// カラーコードの一覧
spaces // 4/8/16など余白に使う数字を定義。
fontSizes // fontSizeを定義
lineHeight // lineHeightを定義
fontWeight // fontWeightを定義。
boxShadow // ドロップシャドウを使用するなら定義

6.コンポーネント設計

f:id:medley_inc:20200619173007p:plain

続いて、コンポーネントコンポーネントに必要なpropsを洗い出します。

世の中には優秀なUIフレームワークが複数存在していますのでそういったものを参考にします。そういったものはほとんど同じようなコンポーネント設計になっているのでコンポーネントの分け方や受け取るpropsなどを参考にすると作業が捗ります。

ここで気をつけたいのは、コンポーネントの数を極力減らすように努力することです。

同一機能の別コンポーネントが複数できてしまうと、今後の機能拡張時に、どのコンポーネントを使うか迷うことになります。ほとんどのエンジニアやデザイナーにとって、コンポーネント選びで迷うことは時間の無駄です。選択肢は極力減らす努力をし、減らせないようなら使用する場所を明確に定義する必要があります。

例えば

component/tab1/
component/tab2/
component/tab3/

のようにタブコンポーネントが複数あると、あとから見たときにどれを使えばいいかわからなくなりますよね。 これを許容してしまうと、知らぬ間に

component/tab4/

ができあがっていることでしょう。

7.実装

まずは大枠のページテンプレートを制作します。その後、小さいコンポーネントから順に作り上げていき、ページテンプレートに配置していきます。コンポーネント化ができていれば、見た目の変更はあとから行えるので、まずはpage内に配置し、機能することを目指します。

この時点で、デザインを細部まで作り込んだとしても、結局全体を見渡してから細かく調整をかけていくと思うので、そこまでセンシティブにならずに行っていきます。

8.確認・ヒアリング・修正の繰り返し

見た目が完全にできていない状態で、確認観点を絞って、PMや、ディレクター、実際に業務で使う方々にチェックしてもらいフィードバックを受けます。 徐々にブラッシュアップしていき、完成を目指します。

デザイン中には常に以下を意識して作業することで、無駄なものを介在させない設計を目指します。

  • コンポーネントを共通化できないか
  • 色数を減らせないか
  • fontSizeやlineheightなどの定義を減らせないか

9.各種資料の作成

作っているデザインではエラー処理などすべてを表現しきれないので、補足資料として別途スプレッドシートなどにまとめていきます。

また、実際のコンポーネントと想定しているprops、実装時の注意事項などを記載したUIコンポーネント集ページを制作し、フロントエンドエンジニアと共有します。

Storybookでの共有も検討しましたが、受け渡すpropsの想定、エンジニアへの指示、要件の補足を記載していくだけですので、Storybookほどの機能は不要と考え、Next.js内にシンプルなPageを作り、そこに記述する方法を選択しました。

また、デザインの設計ルールも資料化し、最終的には各種資料と、制作したコード、S3にアップされているURLを成果物としてエンジニアに実装をしていってもらいデザインのフェーズを完了させます。

f:id:medley_inc:20200619173035p:plain

Reactでデザインした場合のメリット

忠実度の高いモックアップが制作できる

プロトタイプツールは数多くあれど、実装にまさる忠実度の高さなし。 最近はFigmaのAutoLayoutやXDのスタックなど、便利な機能も登場してきていますし、プロトタイピングに関しても様々なアクションが設定できるので、非常に便利になってきました。ただ、それでもCSSやJSの表現力にはまだ届かない部分も多いかなと感じています。

ボタンを押したとき、特定の<select/><option/>が選択されたとき、複数条件下でのみ表示させるもの、ウインドウサイズが小さくなったときの表示など、デザインツールで実装するにはやや面倒な箇所も、コードベースなら対応可能で、デザインファイルと実装されたページでの印象差も起こりづらいです。

実際に操作できる環境を渡すことで高い精度のフィードバックをもらえる

様々なリテラシーのユーザーが使用するサービスであるからこそ、よりリアルなプロトタイプを使用し、ミスコミュニケーションを減らせました。

結果論ですが、新型コロナウィルスによって、コミュニケーションを取りにくい状況が続きましたが、挙動を忠実に再現しているので、精度の高い意見を聞けたのは良かったです。

プロダクト版のフロントエンドの実装時に、こちらの意図が伝わりやすい

fontSizeやspace、colorをtheme.jsファイルにまとめておき、そこからコンポーネント側ではtheme側から読み込むようにしておけば、デザインルールが把握しやすくなります。また、CSSがすでに記述されているので、プロダクト版への移植も可能となり、「デザインファイルと実装でデザインが違う」が起こりにくくなります。

Gitやエディタの恩恵をフルに受けられる

一旦別ブランチでデザイン案を作る、作ったブランチのcommitからcherry-pickする、過去のlogを見る、特定のcommitに戻るなど、Gitの恩恵をフルに受けられます。また、文字列の一括検索や置換が可能なので、色味を一気に置き換えたい、どこで使われているかを探したい場合などかなり重宝しました。

とはいえいいことばかりではなかった。。。

  • ちょっと要素の位置を変える、などはデザインファイルのほうが全然楽。コードベースだとCSSとHTML構造自体を書き換えないといけない
  • Reactを理解していないと、動かなくなったりした時に、修正で時間を余計に使ってしまう
  • 他のデザイナーに引き継ぎしずらい

など、通常のデザインファイルでは起こらないようなデメリットもありました。

結局の所プロジェクトによって向き不向きはあります。

個人的には社内の管理システム系のように、一般的なコンポーネントを組み合わせて、色々なデータを表示する必要があるサイト制作に向いているかなと思います。

逆に、特殊なコンポーネントを量産するケースや、LPのようにグラフィック要素が多くなってくるとデザインツールのほうがはかどります。

プロジェクトの性格に応じて最適なデザイン方法を選んでいきたいですね。

デザイナーだけどコードも書きたい方や、一緒にプルリク送ってくれるデザイナーの方いらっしゃれば、ぜひメドレーで一緒に働きましょう〜

www.medley.jp

メドレー知財担当がエンジニア・デザイナー向け社内勉強会で"特許"について発表しました

  • はじめに

こんにちは、メドレーのコーポレート本部法務コンプライアンス部で知的財産関連の業務を担当している鬼鞍です。コーポレート本部といっても、知財担当の仕事内容としてはエンジニア、デザイナーの方々としっかり協働することが一番大事なので、テックブログにも僭越ながら登場させていただきました。

 

先日、社内勉強会のテックランチにて、「特許」についてお話しする機会がありました。

今回の勉強会は、コロナウイルスの影響で全員オンライン参加という状況下でうまく伝えられるか心配だったのですが、予想以上に質問を頂いてそれなりに反響がありましたので、ここで紹介させていただこうと思います(厳密には特許権のことですがここでは説明の便宜上、以下特許と称することにします)。

 

みなさん、特許と聞いてどのようなイメージをもたれるでしょうか? 

 

あなたが知財を仕事にしている人ではない限り、多くの方にとっては馴染みの薄い世界かもしれません。今回は、世間ではあまり表に出てこない特許という世界にスポットライトを当てつつ、少しでも特許の面白さみたいなものをお伝えできればと思います。

 

  • 特許とは陣地に敷かれた地雷でもあり、身を守る鎧でもある

さて、秒で特許のイメージをつかんでいただくためにここでは極端な例えをします。

 

他人が取得した特許は陣地に置かれた地雷のようなものであり、この地雷を踏んでしまうと特許権侵害というリスクで怪我をしてしまいます。あまりいい例えではないかもしれませんが、これは特許が技術を独占的に実施し、他社が実施した場合には排他できるという側面を表現しています。

企業の知財部門は、地雷を踏まないように日々特許調査をし、地雷のない安全な道を探る、いわば道先案内人のようなものなのです。

また逆に、自分たちが取得した特許があれば、地雷を踏んで権利を主張されてもカウンターパンチを当てることができるので、身を守るための鎧にもなるわけです。

f:id:medley_inc:20200522160511j:plain

 

  • 特許とはエンジニア、デザイナーの成果物である

ここで、特許について少し目線を変えて見てみましょう。

特許というものは、皆さんが努力・工夫した目に見えない技術的なアイディアが見える化された成果物である、という見方もできます。皆さんの頭の中にあるアイディアに知的財産権という形を与えることで、外部の人たちに明確に示すことができます。これによって、例えば投資家から高い評価を得たり、自分たちの技術力をPRするためのツールとして特許を用いることもできるのです。

f:id:medley_inc:20200522152632j:plain

 

  • 特許権は国から与えられるグッドアイディア賞みたいなもの

特許については知らなくてもグッドデザイン賞についてはご存知の方が多いかもしれません。


メドレーのプロダクトであるクラウド診療システム「CLINICS」はグッドデザイン賞を受賞しています。

クラウド診療支援システム [CLINICS] | 受賞対象一覧 | Good Design Award

また、ISMS情報セキュリティマネジメントシステム)についての認証も受けています。

全社で本気になってリーンにISMSの仕組みをつくった話

 

なぜ、こんな話をしているかというと、グッドデザイン賞ISMS特許権も1つの共通点があるからです。それはいずれも客観的な審査を経て合格したものにだけ与えられてるものだという点です。

そして合格したものは、対外的に一定の信用を示すことができる、という機能を備えていいます。日本の特許庁の審査レベルは世界的に見ても高水準であると言われています。特許権というのは、その企業が技術的なアイディアに優れているという一定の信用を示すためのツールとしての性質も兼ね備えているのです。

 

  • 特許に潜むリスク

先ほど特許は地雷であるという話をしました。より具体的に地雷を踏むとどんなリスクがあるのでしょうか?そのリスクは主に差し止め請求や損害賠償です。仮にこのような請求を受けてしまった場合は、大変な労力とコストが伴います。訴訟まで発展してしまうと、周囲の信用を失うことにもなりかねません。

このようなリスクを最小限にするために、知財担当の私が各プロダクトごとの開発定例に同席して自社の開発動向をキャッチアップし、他社権利の特許調査を行っています。

f:id:medley_inc:20200522152754j:plain

 

  • 特許的な思考プロセスとは本質を考えること

さて、ここで「特許的な思考プロセス」を使って皆さんに少し頭の体操をしていただこうと思います。

 

知財関連ではよく例に取り上げられるのですが、鉛筆の発明を題材にして考えてみます。

 

あなたは断面が円形の鉛筆を使っていましたが、机の上を転がって下へ落ちてしまうという問題点に気がつきました。あなたは、鉛筆の断面形状に着目し、断面を五角形にすることにより、この問題を解決したとしましょう。そして、「断面が五角形の鉛筆」という内容を特許請求の範囲(特許権の権利範囲部分を決めるもの)に記載して特許権を得ることができたとします。

f:id:medley_inc:20200522152846j:plain

 

しかし、その後、あなたの断面五角形の鉛筆を見て他の業者が断面を三角形や四角形にした鉛筆を思いつき、それが市場に多く流通し、あなたの断面五角形の鉛筆の売り上げが下がってしまいました。あなたは特許請求の範囲に何と記載すべきだったのでしょうか?

f:id:medley_inc:20200522152924j:plain

 

そう、あなたは「断面が五角形の鉛筆」ではなく「断面が多角形の鉛筆」と書いていれば良かったのですね。

そうすれば、三角形や四角形などの類似品の流通を防ぐことができました。これは簡単です。

その後、あなたは断面が楕円形でも、机の上を転がりにくいという効果を生み出すことに気がつきます。では、多角形も楕円形も含めるためにはどのような表現がいいのでしょうか?ちょっと考えてみてください。

f:id:medley_inc:20200522153019j:plain

あくまで答えの一例ですが、「断面が非円形の鉛筆」とか、「断面がその中心からの距離である長軸と短軸とを有する鉛筆」などの表現であれば上記の全ての形状をカバーすることができます。

 

このように、いくつかの具体例から、効果を生み出すために必要最低限な要素は何なのか、共通点は何なのかを探し出すことが、アイディアの本質を捉えることになります。具体と抽象の間を行ったり来たりするプロセスです。

 

アイディアの本質を捉えるためには多くの具体例を出す発想力が必要です。

もしあなたが発想力豊かなエンジニアやデザイナーであれば強力な特許権が取得できるかもしれません。実際にプロダクトに生かされていない技術的なアイディアというのはあなたの頭の中に埋もれていることが多いものです。

 

  • エンジニアやデザイナーの成果が報われる土壌作りに貢献したい

知的財産権の生みの親はエンジニアやデザイナーの皆さんです。皆さんはあまり意識していないかもしれませんが、技術的なアイディアは日々の開発業務で生み出されていて、それが形になるものもあればならないものもあります。

 

メドレーでは、これまで検討された技術アイディアのうち、プロダクトとして実現化されたものや、そうでないものにかかわらず将来性を見込めそうなものについては、実際に特許出願をしてきました。

ブロックチェーンを用いた電子処方箋の管理方法(特願2018-078928

症状チェッカー(特願2017-011444

・患者統合基盤(特願2019-233247)(出願中につき概念図をIR資料より抜粋)

f:id:medley_inc:20200522153158j:plain

 

また、メドレーでは、エンジニア、デザイナーの方々の成果物がプロダクトだけでなく、知的財産としてもきちんと社内外で認められ、それが名誉となるような土壌作りをしていこうと考えています。

 

  • さいごに

メドレーは、テクノロジーを使って医療分野のデジタル化を推進し、医療ヘルスケアの未来をつくる会社です。そんなメドレーで生み出される素晴らしい技術やデザインを知的財産という分野で可能な限りバックアップしてきたいと思っています。少しでも特許の世界を感じて頂ければ幸いです。最後までお付き合い頂きありがとうございました!

Kotlin/Native を検証してみた

こんにちは、インキュベーション本部でエンジニアをしています世嘉良です。

インキュベーション本部は2020年2月から新規事業の開拓などを目的に新設されたのですが、その中でも若手の部類として日々頑張っています。 CTO 平山のインタビューとともにインキュベーションチームの紹介記事が、コーポレートサイトに掲載されています。こちらもぜひご覧ください。 https://www.medley.jp/team/creator-story-incubation.html

さて、今回は社内で行っている勉強会:テックランチの中で「Kotlin/Native」について発表する機会があったので紹介させていただきます。

Kotlin/Native について

Kotlin は JetBrains 社によって 2011 年に発表されたプログラミング言語です。

現在では Android 開発などで主流の言語となっており、多くの人に利用されていると思います。 Kotlin/Native とはそんな Kotlin を使って様々なプラットフォーム上で実行可能なファイルを生成しようというプロジェクトです。

Kotlin/Native - Kotlin Programming Language

仕組みとしては Kotlin のコードをもとに LLVM を生成し、それを様々なプラットフォームで利用可能なネイティブバイナリにコンパイルすることで追加のランタイムや JVM なしで動作するようにしています。

サポートしているプラットフォームは下記のとおりです。

コードの共通化について

Kotlin/Native のチュートリアルを参考に、Android / iOS で共通化の流れを追ってみます。

作成されるプロジェクトは Android / iOS でそれぞれ "Hello, World!" を出力するシンプルなプログラムです。

Kotlin Playground: Edit, Run, Share Kotlin Code Online

通常のプロジェクトとは別に SharedCode というプロジェクトを作成し、この中に Kotlin/Native (Android / iOS 共通のコード) を記述していきます。 それぞれのコードのレイアウトは以下の通りです。 ※ SharedCode を読み込むためのマルチプラットフォームのための gradle ファイルも必要で、先程のウェブサイトに記載されています。

f:id:medley_inc:20200508170713p:plain

commonMain の中に 「expect」 というキーワードを使って抽象型のようなものを宣言し、 androidMain / iOSMain といった各プラットフォームの実装の中で 「actual」 というキーワードを使用してその内容を記述します。

// commonMain
expect fun platformName(): String

// androidMain
actual fun platformName(): String {
    return "Android"
}

// iOSMain
actual fun platformName(): String {
    return UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}

このチュートリアルには記載ありませんが、「expect」 というキーワードはクラスや関数・アノテーションといったすべてのものに定義することができます。

Platform-Specific Declarations - Kotlin Programming Language

また iOSMain 内であれば iOS の Foundation フレームワークといったように、各プラットフォームごとのフレームワークもそれぞれ利用することができます。

こうしてできた、SharedCode のライブラリは Android/iOS のプロジェクトから使い慣れたライブラリと同じようにインポートすることができます。

f:id:medley_inc:20200508170931p:plain

f:id:medley_inc:20200508170949p:plain

f:id:medley_inc:20200508171007p:plain

Kotlin/Native の制約について

JVM 上で Kotlin を動かしていた時には標準ライブラリやメモリ管理には Java の資産を利用することができましたが、Kotlin/Native ではそれを利用することができないという制約があります。

具体的にアプリを作成する際には下記の点に注意が必要でした。

  • Java の標準ライブラリを使用しているライブラリを利用できないため Kotlin で記述されたライブラリのみが使用する
  • iOS アプリを開発する際には memScope, alloc / free などを利用してメモリ管理を自分で行う必要がある

Java の資産が利用できない Kotlin に対して心配することもありましたが、Kotlin/Native 向けに様々なライブラリが 提供されておりそれらを利用することで問題を解決しました。

f:id:medley_inc:20200508171035p:plain

現状は日付管理ですらサードパーティ製のライブラリで操作する必要があるのが不満ですが、Kotlin/Native の発展によってこういった不便さは解決されるように願っています。

  • 「suspend」 関数を iOS 側から直接呼び出すことができない (コールバック関数やなどに変換して呼び出す必要がある)
  • Kotlin で定義した宣言や型の一部が Swift だと別名になっている (「 objc_runtime_name」 や 「swift_name」 を参照する必要がある)
  • 「companion object」 や 「object」 などシングルトンの宣言に対してオブジェクトをフリーズするか 「ThreadLocal / SharedImmutable」 のアノテーションを付与する必要がある

こちらは iOS で利用する場合の注意ですが、いくつかの Kotlin の機能が利用できなかったり、追加の記述が必要なものがあります。 詳しくは下記の資料を参照ください。

Kotlin/Native as an Apple Framework - Kotlin Programming Language

kotlin-native/IMMUTABILITY.md

Kotlin/Native の利用例

まだまだ利用例が少ないため参考になる資料などがあまりないのですが、以下の実装がオープンソースとして公開されているため構成や雰囲気を掴むためにはちょうど良い資料となりました。

まとめ

弊社では 「CLINICS」 というオンライン診察を行うことができるアプリをリリースしています。

CLINICS を長期的に運用していくための技術調査の一環として、近年の Android 界隈で話題にあがっていた「Kotlin/Native」について検証し発表してみました。

まだまだ発展途上感のある技術ですが、実用性のあるプロジェクトだと思いますのでご興味のある方はぜひ一度お試しください!

社内勉強会TechLunchでジョブメドレーでのCircleCIの活用と改善について発表しました

こんにちは、メドレープロダクト開発室 エンジニアの岸田です。

先日、社内勉強会TechLunchにて、弊社の提供する医療介護分野の人材プラットフォーム「ジョブメドレー」の開発で利用しているCircleCIでのCI/CDについての取り組みを発表しましたので、紹介させていただきたいと思います。

ジョブメドレーの開発でCircleCIをどのように利用しているか

ジョブメドレーの開発では、主に次の2つをCircleCIを用いて行なっています。

デプロイに関しては、ECS環境とEC2環境への2通りのデプロイをCircleCIを利用して行なっています。

そのためCircleCIのWorkflowは「ユニットテスト・構文チェック」「EC2へのデプロイ」「ECSへのデプロイ」の3つに分かれています。

3つのWorkflowを大まかに説明させていただきます。

ユニットテスト・構文チェック

ジョブメドレーは主にサーバサイドをRuby on Rails、フロントエンドをReactを使って開発をしています。 そのためユニットテストにはRSpec・Jestを、構文チェックにはRubocopとESLint利用しています。

このWorkflowは3つのジョブで構成されています。

  1. ライブラリのインストール
    • bundle installyarn installなどを行い、ユニットテスト・構文チェックの実行に必要な依存ライブラリをインストールし、CircleCIのキャッシュ機能を用いてキャッシュを行なっています
  2. RSpecの実行
    • 2コンテナを利用しての並列実行を行なっています
  3. 構文チェックとJestの実行
    • RSpecと比較してJestでのテストコードはボリュームが少なく・実行時間が短いため、構文チェックと一緒に1つのジョブにしています

f:id:medley_inc:20200430190805p:plain

このWorkflowはどのブランチでもGitHubへPushが行われるたびに実行されるようにしています。

EC2へのデプロイ

このWorkflowではデプロイを行うための準備と、文字通りEC2へのデプロイを行なっています。 デプロイにはRuby gemのCapistranoを利用しています。

下記のような2つのジョブで構成しています。

  1. ライブラリのインストール
  2. ビルドとデプロイ
    • Railsのassets:precompileやyarn buildなどの処理と、Capistranoを用いたデプロイを行なっています

f:id:medley_inc:20200430190826p:plain

このWorkflowは特定のブランチでしか実行されないようになっています。

ECSへのデプロイ

このWorkflowではECSへデプロイするためのDocker image作成とそのためのビルドなどを行なっています。

ジョブは下記のように4つで構成されています。

  1. npm packageのインストール
  2. フロントエンドのビルド
  3. GemのインストールとRailsのassets:precompile
  4. Docker imageの作成とPush

f:id:medley_inc:20200430190429p:plain

こちらのWorkflowも特定のブランチでしか実行されないようになっています。

抱えていた課題とassets:precompile

ジョブメドレーの開発ではCircleCIの各ジョブが全て正常に完了することをPRをマージする条件の1つにしています。 しかし各Workflow・ジョブの実行時間が長く、ジョブの実行待ちをしなければいけないという状況がよく起こってしまっていました。

特に時間がかかっていたのが、下記の3つでした。

  1. 「EC2へのデプロイ」Workflowの「デプロイ」ジョブ
  2. 「ECSへのデプロイ」Workflowの「GemのインストールとRailsのasset_precompile」ジョブ
  3. RSpecの実行」ジョブ
    • RSpecの書き方などを改善することで短縮できるため、割愛させていただきます

これらについて調査したところ、1・2は、assets:precompileが主に時間を使っていることが分かりました。 この点について原因と行なった改善を説明をさせていただこうと思います。

assets:precompileに時間がかかる

ジョブメドレーではRailsのアセットパイプラインを利用して、アセットファイルのコンパイル・最小化などを行なっています。 これを実行する際に assets:precompile コマンドを利用しています。 また、同コマンド実行時にコンパイルしたファイルをAWS S3バケットにアップロードするために asset_sync gemを利用しています。

このコマンドの実行には9分から10分ほどの時間がかかっており、下記の2つの原因で遅くなっていました。

  1. 毎回1からコンパイルを行なっていた
  2. コンパイル後のファイルをアップロードするS3バケットに大量のファイルが存在する

毎回1からコンパイルを行なっていた

こちらについては読んで字のごとくですが、デプロイWorkflowが実行されるたびにアセットファイルの変更有無に関わらず、毎回3分ほどを費やして全てのアセットファイルをコンパイル・最小化していました。 こちらの解決策としては、CircleCIの公式ドキュメントでも例が載せられていますが、 assets:precompile コマンドで生成されるファイルが置かれるディレクトリ(public/assets)をCircleCIでキャッシュさせることで対応しました。 キャッシュさせることで、毎回3分ほどかかっていた処理を1分ほどに短縮することができました。

コンパイル後のファイルをアップロードするS3バケットに大量のファイルが存在する

こちらついてはもう少し背景が複雑かつ、まだ解決まではできていません。

まず asset_sync を利用したファイルのアップロード処理ですが、毎回6分以上の時間がかかっていました。 CircleCIのログをよくよく見てみるとアップロード自体に時間がかかっているのではなく、「どのファイルをアップロードすべきか」を判断する処理に多くの時間を費やしていることが分かりました。

f:id:medley_inc:20200430190719p:plain

(1ファイルもアップロードしていないが6分20秒かかっている)

そこで、asset_sync gemのソースコードを読んでみると、「アップロード先のS3バケットにあるファイルを全て取得し、 assets:precompile コマンドが生成したファイルのファイル名と比較する」という処理がありました。 この処理が怪しいのではないかと思い、S3バケットを確認してみたところ数十万件以上のファイルが存在することが分かりました。

数十万件以上のファイルの情報を取得していることを考えると6分以上時間がかかるのも納得です。

この問題の解決策は下記の2つが考え得ると考えました。

  1. 不要なファイルをS3バケットから削除する
  2. 前回のデプロイとの比較をしてS3にアクセスせずにアップロードすべきファイルかどうかを判断するようにする

1つ目の方法は正攻法で、数十万ファイルを全て使っているわけではないため利用されていないファイルを削除してしまう方法です。

asset_syncも早くなり、S3の利用料も少なくなるためこの方法を取れるのであれば、この方法で解決するのが良いように思います。 ジョブメドレーでもこの方法を取れないかと検討しましたが、ジョブメドレーから配信しているHTMLメールなどでも利用しているファイルがあるため一概にアクセスされていないからといって削除することができず、この方法での解決は一旦断念しました。

2つ目の方法はassets:precompileコマンドが生成するmanifestファイル(生成したファイルのリストなどが記述されている)とCircleCIのキャッシュ機能を使って短縮する方法です。

manifestファイルは、コンパイル後のアセットファイルが出力されるディレクトリ(public/assets/)に同じように出力され、また、上記でpublic/assetsをCircleCIのキャッシュ機能でキャッシュするようにしたため、manifestファイルも一緒にキャッシュされるようになっています。

assets:precompileの実行により今回作成されたmanifestファイルと、キャッシュされていたmanifestファイルを比較して差分が出たファイルだけをS3にアップロードするようにしようという試みです。 この処理はasset_sync gemではできそうになかったので、シェルスクリプトとRakeタスクを作成して実行してみたところ、「アップロードすべきファイルかどうか」を判断するための時間がほぼなくなり、6分以上の短縮をすることができました。

ただし検証が不十分なため、実運用に乗せることはまだできていません。

CircleCIのジョブ実行時間を短縮する小さな改善事項

上述の通り、各Workflowで大きく時間を費やしているのはassets:precompileRSpecの実行でしたが、細かな点としては他にも小さい改善をしたことがあります。

コードのチェックアウト

CircleCIでは組み込みのコマンドとして checkout コマンドがあります。 これは対象のリポジトリのブランチをクローンしてくれるコマンドですが、ジョブメドレーはモノレポに近い構成になっており、コードベースのサイズや履歴が大きいためクローンするだけである程度の時間がかかってしまっています。

そこでまずは、.gitディレクトリをCircleCIのキャッシュ機能を利用してキャッシュするようにしてみました。 すると、checkoutコマンド自体は大きく短縮しましたが、キャッシュのsave/restoreに短縮した以上の時間がかかるようになってしまいました。

別の方法としてcheckoutコマンドを利用せずに、GitのShallow cloneやSparse cloneを駆使して必要なファイルや履歴だけをクローンするということができます。 現在はSparse cloneを一部導入してみており、Shallow cloneも導入したいと考えています。

Docker imageの作成

ジョブメドレーではECSへのデプロイの際にCircleCI上でDocker imageをビルドしているため、docker buildコマンドの実行時間も可能な限り抑えたいと考えています。

短縮する方法は様々あるかと思いますが、CircleCI上でもDokcer Buildkitを利用することがきますので、それを利用してビルドすることで簡単に短縮することができます。

詳しくはDockerのガイドに記載の通りではありますが、DOCKER_BUILDKIT=1を指定してdocker buildを実行するだけで利用することができ、ジョブメドレーでは2分ほどかかっていたビルドを40秒ほど短縮することができました。

まとめ

今回は、TechLunchで発表したジョブメドレーにおけるCircleCIの活用と改善の取り組みについて紹介させていただきました。 今回紹介させていただいた以外にも様々な用途があり、今後もさらにうまく活用していきたいと思っています。

ご覧いただきありがとうございました。