every Tech Blog

株式会社エブリーのTech Blogです。

Elasticsearchをゼロダウンタイムで再起動する

タイトル画像 - Elasticsearchをゼロダウンタイムで再起動する

こんにちは。MAMADAYSバックエンドチームのsa9sha9です。最近Diablo3にハマりました。

MAMADAYSでは検索基盤としてElasticsearch(以下ES)を利用していますが、時たま再起動を実施したいケースがあります。

本記事では、ゼロダウンタイムでのESの再起動を実現するための注意点を実際のフローに沿ってまとめたいと思います。

MAMADAYSのアーキテクチャについては以前のTechBlogをご参照ください。

tech.every.tv

おことわり

本記事でご紹介する手順については必ずしもご自身の環境とマッチするか保証しかねます。 バージョンごとの差異については、しっかりと公式ドキュメントにてご確認ください。

安直に再起動ができない理由

ESを利用する場合には複数台のクラスタ構成にするのが常かと思いますが、ESクラスタを安直に再起動してしまうとダウンタイムが発生してしまいます。

検索機能が主機能なサービスの場合には致命的な障害となってしまうでしょう。

MAMADAYSでは3台のノードでクラスタを構成していますが、それぞれのノードを1台ずつ再起動することでゼロダウンタイムでの再起動を目指します。

再起動の準備体操

1. ヘルスチェック

必ずはじめにヘルスチェックを行いましょう。

ここでステータスが yellow / red だった場合は必ず settings や shards の状態を確認し、 green ステータスにしてから再起動に臨みましょう。

  GET _cat/health

  // 結果
  epoch      timestamp cluster             status node.total node.data shards pri relo init unassign pending_tasks max_task_wait_time active_shards_percent
  1645172581 08:23:01  mamadays-es-cluster green           3         3    xxx xxx    0    0        0             0                  -                100.0%

2. ノード一覧の確認

現時点でどのノードが master node になっているかを確認しましょう。

そして再起動する際には、 master node は最後に再起動しましょう。

というのも master node が停止した場合、別のノードが master node に成り代わるわけですが、最初に master node を停止すると master node の移動が最低でも2回行われてしまいます。

master node の再起動を最後に行うことで master node の移動は必ず1回になるので、余計な移動を避けられます。

  GET _cat/nodes?v

  // 結果
  ip             heap.percent ram.percent cpu load_1m load_5m load_15m node.role master name
  192.168.1.95            37          88   1    0.11    0.07     0.02 dilm      -      ip-192-168-1-95
  192.168.2.238           27          89   1    0.00    0.00     0.00 dilm      -      ip-192-168-2-238
  192.168.3.72            24          89   1    0.04    0.01     0.00 dilm      *      ip-192-168-3-72

3. インデックスの状態を確認

再起動後にデータの欠落がないか確認するため、 docs.count を控えておきましょう。

  GET _cat/indices/index_01?v

  // 結果
  health status index    uuid                   pri rep docs.count docs.deleted store.size pri.store.size
  green  open   index_01 xxxxxxxxxxxxxxxxxxxxxx   3   1          x            0      x.xmb        xxx.xkb

4. 自動アロケーション機能を無効化

ESは replica shards の存在が確認できないと、別ノードに新たな replica shards が作成されます。ノードの再起動をかけた際に一時的に replica shards が確認できなくなるためこの処理が実行されます。

ただし、すぐに複製を開始するわけではなく、デフォルトでは1分間待機してから複製を開始します。

再起動だけなのでほぼ1分以内にノードは復旧するはずですが、何らかの理由で1分を超えてしまうと不要な複製処理が行われ膨大なI/Oが発生してしまうため、作業中は自動複製を停止しておきましょう。

ただし、primaries を指定して primary shards を他のノードへ再配置することは許可しておきましょう。

  PUT _cluster/settings
  {
    "persistent": {
      "cluster.routing.allocation.enable": "primaries"
    }
  }

  // 結果
  {
    "acknowledged" : true,
    "persistent" : {
      "cluster" : {
        "routing" : {
          "allocation" : {
            "enable" : "primaries"
          }
        }
      }
    },
    "transient" : { }
  }

5. 機械学習機能が有効になっている場合は停止

もし機械学習機能を使っているなら、一時的に停止しましょう。

MAMADAYSでは使っていないのでこの手順はスキップします。


これで再起動準備は整いました!

いざ、再起動

1. 各ノードに入ってプロセスを再起動

  sudo systemctl restart elasticsearch.service

必須ではありませんが、この時に他の生きているESノードに1sごとに _cat/shards APIでシャード状態を確認すると、 primary shard の再配置の動きが肌で感じられてとても良いです。

2. (1つ目のノードの再起動を行った後に) 試しにヘルスチェック

1つ目のノードを再起動したタイミングでヘルスチェックを行うと、ステータスが yellow になっているかと思います。

これは replica shards が一時的に切り離されたことによるもので、検索機能に影響はありません。

  GET _cat/health

  // 結果
  epoch      timestamp cluster             status node.total node.data shards pri relo init unassign pending_tasks max_task_wait_time active_shards_percent
  1645170501 07:48:21  mamadays-es-cluster yellow          3         3    xxx xxx    0    0      xxx             0                  -                 66.7%

3. (全ノードの再起動を行った後に) ノードとインデックスの確認

master nodeの位置が変わっているはずです。

  GET _cat/nodes?v

  // 結果
  ip             heap.percent ram.percent cpu load_1m load_5m load_15m node.role master name
  192.168.1.95            37          88   1    0.11    0.07     0.02 dilm      *      ip-192-168-1-95
  192.168.2.238           27          89   1    0.00    0.00     0.00 dilm      -      ip-192-168-2-238
  192.168.3.72            24          89   1    0.04    0.01     0.00 dilm      -      ip-192-168-3-72
  GET _cat/indices/index_01?v

  // 結果
  health status index    uuid                   pri rep docs.count docs.deleted store.size pri.store.size
  green  open   index_01 xxxxxxxxxxxxxxxxxxxxxx   3   1          x            0      x.xmb        xxx.xkb

4. 後処理

自動アロケーション機能を有効化

  PUT _cluster/settings
  {
    "persistent": {
      "cluster.routing.allocation.enable": null
    }
  }

  // 結果
  {
    "acknowledged" : true,
    "persistent" : { },
    "transient" : { }
  }

5. (必要なら) 機械学習機能を有効化

MAMADAYSでは使っていないのでスキップ。

6. 各方面のチェック

WebやAppから検索機能が使えるかどうかなどを確認しましょう。


これにて、ESクラスタの再起動は完了です。

困った点

ロードバランサーが再起動中のESノードにも疎通させてしまい一定の確率で接続できなくなる

MAMADAYSではESの前面にロードバランサーを置いているのですが、ロードバランサーはESノードの死活状態を即時に検知しないため、ESノードの再起動の如何にかかわらず一定の確率で疎通させてしまいます。

ESノードの再起動中はもちろん応答ができないためエラーを返してしまい、結果としてダウンタイムが発生することになります。

そのため、ロードバランサーのターゲットグループから再起動させるESノードを予め除外し、再起動中は疎通させないようにしておく必要があります。

kibanaのアクセス先のESノードが落ちると、その間だけ状態確認ができなくなる

再起動中にも _nodes APIなどで状態を確認したい場合は、curl で他のESノードのAPIを呼びましょう。

理想はkibanaが自動で障害検知して他のESノードへ接続してくれると良いんですが、どうにもそれができなかったので今回はやむなく上記の方法で対処しました。

Sniffingがそういった機能を有しているらしいのですが、うまく動作せず今回は見送りました。詳しい方がいればぜひ入社して欲しいです。

切り戻しについて

万が一何らかの障害が発生して、インデックスデータなどを失ってしまった場合に備えてsnapshotを取っておきましょう。

本記事では詳しい説明は省略しますが、バージョン違いによる互換性などは必ず確認することをお勧めします。

Snapshot and restore | Elasticsearch Guide [8.0] | Elastic

最後に

本来はこのようなトイルは自動化すべきなのですが、本件が緊急対応ということもあって手動で行うことになりました。

今後は、誰でも簡単かつ迅速かつ安全に実施できるようにAnsible化を行おうと考えています。

参考文献

https://www.elastic.co/guide/en/elasticsearch/reference/current/restart-cluster.html#restart-cluster-rolling

2022年 エブリーの開発組織の抱負

f:id:nanakookada:20220124122335p:plain

少し遅くなりましたが、あけましておめでとうございます。エブリーのCTO今井です。
早速ですが、2021年の振り返りと今年の抱負についてお話しできればと思います。

2021年の振り返り

2021年は開発本部に属する全開発部の部長が入れ替わり、 僕自身もDELISH KITCHEN開発部長となり、そして10月にはCTOに就任することとなりました。
会社としてもPMV(パーパス・ミッション・バリュー)を定めるなど、次のステップに向けて足場を整え、
深くしゃがんだ1年だったように思います。
個人としては、未知の領域がさらに増えパンクしそうになりながらも、
チームの仲間のサポートもあり、過去一番成長した1年だったと思います。

今後の抱負

DataUtilization

全社をあげてDataUtilization(データ活用)を推進しています。
これはデータを活用できる人材を増やすことと、活用可能なデータを増やすことの両面が必要だと考えます。
開発本部ではこの両面を支援すべく尽力していきたいと思います。

具体的には非エンジニアのSQLの習得、より利用しやすいデータプラットフォームの構築、
AI・機械学習の推進などはその第一歩になると考えています。

事業にこだわる開発組織へ

僕が大切にしている価値観の1つに エンジニアが事業を引っ張る というのがあります。
当たり前ですが、事業が求める成果を最短・最速で実現することが、もちろんエンジニアにも求められます。
これに愚直にこだわれる組織にしていきたいと思います。

開発をしていると目の前の機能実装に追われ、この機能が事業を伸ばす上で本当に必要なのか、
もっと良い手段がないのか、それを考えるのをおろそかにしてしまうこともあるかもしれません。
時には最新の技術よりも既に涸れてる技術を選択することもあるし、
コストを払ってスピードを優先することもコスト削減のために地道な開発をすることもあると思います。
その時々で最適な選択は事業に一番貢献することだということに立ち返って、
開発を進められるようになって欲しいと思います。

技術的な挑戦の推進

事業にこだわる一方で、新しい技術やよりチャレンジングな挑戦をしていかないと衰退していくのがエンジニアだと思います。
僕個人も打算的である程度想定できる範囲で開発をすることよりも、
今まで使ったことない、未知の技術に触れながら開発する方がワクワクします。
それはエンジニアの働きがいなどにつながることもあれば、
長い目で見て生産性の向上や、採用にもつながることも大きいと考えています。

目の前の事業貢献と天秤にかけた時に、どちらを取るのかは大変難しい判断になることもありますが、
日々事業貢献を考えているエンジニアが必要と判断した場合に、積極的に挑戦できるよう、
周囲へ理解してもらうための説明やそのための予算取りなどで推進していけたらと思います。

まとめ

上記では足りないことも多々あるとは思いますし、
自分自身、CTOになったばかりでこの考えが数ヶ月後には変わっていることがあるかもしれませんが、
それも成長だと思いますし、日々変化の多いこの業界だからこそだとも思います。

エブリーでは常に仲間を募集しています。
少しでも共感する部分があった方、または自分ならもっとこう推進できるのにと思った方、
まずはカジュアルに面談からでもお待ちしております!

コーポレートサイト リクルートページ

エブリー公式オウンドメディア

Datadog APM を試してみた話

f:id:nanakookada:20211213104750p:plain

はじめに

はじめまして。DELISH KITCHEN 開発部でバックエンド開発を担当している池と申します。2021 年 9 月にエブリーに転職してバックエンドエンジニアとして働いています。入社して 3 ヶ月ですがサーバーサイド、フロントエンド、クラウド、CI/CD など多岐に渡る技術領域を触ることができ、とても有意義な毎日を送っています。

今回はこれまでに触ってきた技術の中から Datadog APM を試した際の内容についてご紹介したいと思います。

Datadog APM とは

ご存知の方も多いとは思いますが、Datadog は SaaS 型運用監視サービスです。様々なプラットフォームにおけるホストの監視、アプリケーション監視、ログ蓄積などシステム監視全般を Datadog 一つで行うことができます。その中で APM(Application Performance Management)は、名前の通りアプリケーションのパフォーマンスを監視する機能になります。

詳細は後述しますが、Datadog APM は分散トレーシングという監視の手法を用いており、マイクロサービスのような分散したアーキテクチャのフロントエンドからデータベースまで、エンドツーエンドのアプリケーション監視を行うことができます。

下記は具体的に計測できるメトリクスの一例です。

  • 時系列でのリクエスト数・レイテンシー、エラー数
  • エンドポイント毎のリクエスト数・レイテンシー、エラー率
  • 各リクエストの処理時間・ボトルネック

分散トレーシングについて

分散トレーシングは分散されたアーキテクチャのアプリケーションを監視するための手法です。 マイクロサービスのような複数システムから構成されるアーキテクチャでは、複数のサービスをまたいで処理が動くため、全体の振る舞いを把握することが難しいという課題があります。 また、障害発生時に原因を特定することも困難です。 分散トレーシングの手法を用いることで、サービス間をまたいだ処理の計測や可視化が可能になり、それら課題の解決に繋がります。

簡単に用語を説明します。分散トレーシングの主要な用語としてトレース(Trace)とスパン(Span)があります。

※ 公式のAPM 用語集から抜粋

用語 意味
トレース(Trace) トレースは、アプリケーションがリクエストを処理するのに費やした時間と、このリクエストのステータスを追跡するために使用されます。各トレースは、一つまたは複数のスパンで構成されます。
スパン(Span) スパンは、特定の期間における分散システムの論理的な作業単位を表します。複数のスパンでトレースが構成されます。

例えば、API サーバが一つのリクエストを受け取ってからレスポンスを返却する一連の処理を計測するとします。よくある API では、クライアントからリクエストが送られてきた後、DB や外部サーバなど複数のサービスと連携してデータを処理し、ビジネスロジックを経由して最終的にレスポンスが返却されると思います。

それらをトレースとスパンを用いて表すと次の図のようになります。各サービスにおける処理の単位がスパンで表され、複数のスパンで構成される一連の全体処理がトレースで表されます。

f:id:yutaike:20211210162937p:plain

実装方法

前提

今回は下記の前提のもと、 Datadog APM の導入および、一つの API リクエストを受け取ってからレスポンスを返却するまでのトレース計測の実装を行います。

  • Go で実装されている API サーバでトレース処理を実装する
  • Web フレームワークには echo を利用
  • API サーバは AWS ECS で管理

次の図は今回取り扱う全体のイメージ図です。Server へ Datadog Agent を導入し、API アプリケーション本体へトレース計測の処理を実装します。

f:id:yutaike:20211210163049p:plain

datadog-agent の導入

datadog-agent は計測対象の API が動作しているサーバーで起動される必要があります。公式の datadog-agent コンテナを AWS ECS のタスク定義から起動することで、簡単に導入することができます。

次のようにコンテナの定義をタスク定義に設定します。詳細なセットアップ方法は公式ページを参照ください。

f:id:yutaike:20211210163116p:plain f:id:yutaike:20211210163128p:plain:w400

トレースの実装

Datadog APM の trace ライブラリを導入します。

go get gopkg.in/DataDog/dd-trace-go.v1/...

トレースエージェントにホスト設定を伝えます。

import(
    "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" // traceライブラリ追加
    ...

)

func main() {
    ...

    resp, err := http.Get("http://169.254.169.254/latest/meta-data/local-ipv4")
    bodyBytes, err := ioutil.ReadAll(resp.Body)
    host := string(bodyBytes)
    if err == nil {
        //set the output of the curl command to the DD_Agent_host env
        os.Setenv("DD_AGENT_HOST", host)

        // tell the trace agent the host setting
        tracer.Start(tracer.WithAgentAddr(host))
        defer tracer.Stop()
    }

次にスパンを実装します。

スパンの実装には Datadog が公式でサポートしている専用の統合ライブラリを利用します。この専用の統合ライブラリは、一般的に広く用いられている Go の Web フレームワークや、データストア、ライブラリを Datadog APM に統合するために作られているライブラリで、それら Web フレームワークやライブラリと互換性を持っています。(一覧はこちらを参照ください。)

今回の例では、echo と olivere/elastic ライブラリを専用の統合ライブラリに置き換えます。

echo を専用の統合ライブラリに置き換える

import(
    ddEcho "gopkg.in/DataDog/dd-trace-go.v1/contrib/labstack/echo" //追加
    "github.com/labstack/echo"
    ...
)

func main() {
    ...

    e := echo.New()
    e.Use(ddEcho.Middleware(ddEcho.WithServiceName("echo-service-name-test")))

専用の統合ライブラリを import し、datadog echo で準備されている Middleware をecho.Useすることで統合することができます。

統合することで、datadog echo によって API リクエスト処理のスパンが計測されて datadog-agent に送られ、Datadog の UI 上で可視化されるようになります。

f:id:yutaike:20211210163900p:plain
datadog echoで計測したスパンのFlameGraph

olivere/elastic を専用の統合ライブラリに置き換える

次に olivere/elastic を専用の統合ライブラリに置き換えます。

import()
    elastictrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/olivere/elastic"
    ...
)

func NewElasticSample() *ElasticSampleImpl {
    tc := elastictrace.NewHTTPClient(elastictrace.WithServiceName("my-es-service-test"))
    cli, _ := elastic.NewSimpleClient(
        ...
        elastic.SetHttpClient(tc),
    )

echo と olivere/elastic を同じトレースとして認識させるためには、同じ Context を利用する必要があります。

今回は簡易的に特定の elastic の呼び出し時に echo の Context を渡すように実装して動作を確かめました。

// SampleMethod
func (s *ElasticSampleImpl) SampleMethod(c echo.Context) {
    svc := s.client.Search().Index("Sample")
    ctx = c.Request().Context()
    result, err := svc.Do(ctx)
    ...

以上の実装により、各サービス(echo、elasticsearch)の処理時間が計測されるようになりました。

f:id:yutaike:20211210163451p:plain
サービス毎の処理時間

また、ここに示すのは一部ですが、様々なパフォーマンスデータを UI 上で確認することができ、それらのデータに対してアラート設定や、リアルタイム検索の機能なども実行可能です。

  • エンドポイント毎のリクエスト数
  • 時系列毎のリクエスト数・レイテンシー
  • レイテンシー分布

f:id:yutaike:20211210163512p:plain
エンドポイント毎の計測結果

f:id:yutaike:20211210172722p:plain
時系列でのリクエスト数・レイテンシー

f:id:yutaike:20211210172737p:plain
レイテンシー分布

Continuous Profiler の導入

次に Datadog Continuous Profiler という機能を試します。この機能はアプリケーションの性能をプロファイリングする機能です。

APM と Continuous Profiler の双方を有効化することで、Code Hotspots という機能によりコードベースでパフォーマンスのボトルネックを特定することができると記載があったため、試してみました。

APM 分散型トレーシングと Continuous Profiler の双方が有効化されたアプリケーションプロセスは自動的にリンクされるため、Code Hotspots タブでスパン情報からプロファイリングデータを直接開き、パフォーマンスの問題に関連する特定のコード行を見つけることができます。

※ 引用元:https://docs.datadoghq.com/ja/tracing/profiler/connect_traces_and_profiles/

Go では下記のプロファイルタイプがサポートされています。(詳細はこちらを参照ください。)

  • CPU Time
  • Allocations
  • Allocated Memory
  • Heap Live Objects
  • Heap Live Size
  • Mutex
  • Block
  • Goroutines

プロファイリングの有効化は、次のように数行で実装できます。

import(
    "gopkg.in/DataDog/dd-trace-go.v1/profiler"
    ...
)

func main() {
    ...

    if err := profiler.Start(
        profiler.WithService("profiler-service-name"),
        profiler.WithEnv("profiler-env-test"),
        profiler.WithProfileTypes(
            profiler.CPUProfile,
            profiler.HeapProfile,

            // The profiles below are disabled by
            // default to keep overhead low, but
            // can be enabled as needed.

            // profiler.BlockProfile,
            // profiler.MutexProfile,
            // profiler.GoroutineProfile,
        ),
    ); err != nil {
        log.Fatal(err)
    }
    defer profiler.Stop()

上記の実装を行うだけで、プロファイリング結果が Datadog の UI 上で可視化されます。

f:id:yutaike:20211210172802p:plain
時系列での CPU Time 上位 10 件

f:id:yutaike:20211210172814p:plain
CPU Time の FrameGraph

Code Hotspots

残念ながら Code Hotspots 機能はまだ Go に対応していませんでした。

f:id:yutaike:20211210172836p:plain
Code Hotspots 未対応(2021/10時点)

dd-trace-go の github を見ると Code Hotspots に関する PR が出ているので開発中であることがわかります。 アップデートに期待します。

料金 / サンプリング

最後に料金とサンプリングについてご紹介します。 (※2021/10 時点での料金体系です。)

料金

公式の料金ページから最小料金と加算費用を抜粋しました。

年払い
最小料金(1 ホスト、1 か月あたり) $31(オンデマンド払いは$36)
プラス料金:スパンの取り込み APM ホストあたり 150GB (すべての APM ホスト平均)無料、その後 $0.10 /GB
プラス料金:スパンの保存 APM ホストあたり Indexed Span 100 万件 (すべての APM ホスト平均)、その後
・保存期間 7 日、$1.27 / 100 万スパン / 月 (年払いまたはオンデマンド払いで $1.91)
・保存期間 15 日、$1.70 / 100 万スパン / 月 (年払いまたはオンデマンド払いで $2.55)
・保存期間 30 日、$2.50 / 100 万スパン / 月 (年払いまたはオンデマンド払いで $3.75)
プラス料金:アドオン AWS Fargate $2 / タスク

簡単にですが、以下の仮定をもとにざっくりと料金を試算します。

仮定

  • 月の総リクエスト数:1 億リクエスト(1 日あたり約 333 万リクエスト)
  • ホスト数:10 ホスト
  • 1 リクエスト:3 スパン
  • スパンの保存期間:7 日
  • 年払い
  • スパンの取り込みは試算が難しいため除く
  • AWS Fargate アドオンのプラス料金は掛からない
  • $1 = 113 円換算

料金試算

  • 最小料金:$31 × 10 ホスト = $310
  • スパン数:1 億リクエスト × 3 スパン = 3 億スパン
  • スパンの保存料金:(3 億スパン - 100 万スパン) ÷ 100 万スパン × $1.27 ≒ $380
  • 総料金:$310 + $380 = $690 ≒ 77,970 円

本来はここに、スパンの取り込みに応じたプラス料金と、もし利用があれば AWS Fargate アドオンのプラス料金が加算されます。 この試算のように全トレースデータを取り込んで保存すると大きな費用がかかるため、実際には一部のデータを取り込むようにサンプリングすることで費用を抑えつつ運用する形が現実的だと考えられます。

サンプリング

サンプリングは次の図の「Your instrumented applications」と「Intelligent retention & custum filters」の 2 箇所で設定することができます。前者はサーバで設定する Datadog に送るトレースのサンプリング設定です。後者は送られてきたトレースに対して Datadog の UI 上で設定するトレース保存のフィルター設定になります。

f:id:yutaike:20211210174109p:plain
Datadog にトレースが保存されるまでのフロー
※引用元:https://docs.datadoghq.com/ja/tracing/trace_retention_and_ingestion

本記事ではサーバ側のサンプリング設定についてご紹介します。

サーバ側のサンプリングは、デフォルト設定で 50 トレース / 1 秒まで 100%のトレースを取り込まれる設定になっています。超えた分は Datadog Agent によって自動的に選択/削除されて Datadog へ送られます。

このデフォルトのサンプリング率は、下記の環境変数を設定することで変更できます。

DD_TRACE_SAMPLE_RATE = 1.0

基本的にはDD_TRACE_SAMPLE_RATEに基づいて Datadog Agent が自動的にサンプリングしてくれるのですが、Span にMANUAL_KEEPMANUAL_DROP タグを追加することで、優先的に 100%保持・削除するように設定できます。

// 100%削除
span.SetTag(ext.ManualDrop, true)

// 100%保持
span.SetTag(ext.ManualKeep, true)

ここまでがサーバ側で設定できるサンプリング設定になります。

最終的には Datadog の UI 上で設定するフィルターを通して保存されるトレースが決定されるので、そちらの詳細はこちらを参照ください。

まとめ

今回 Datadog APM を触ってみて、公式のドキュメントが豊富だったため、大きく迷わずに導入を進めることができました。一方、既に運用されている複数のサービスにおいて導入・整備を進める作業にコストがかかることもわかりました。特に、サービス間のスパンを紐付けるために同一の Context を用いる設計にする必要があることや、専用の統合ライブラリに未対応のライブラリも多くあることから、既存システムへの導入の難しさを感じました。

しかし、本記事では紹介できていない機能もとても数多くあり、導入して環境整備さえできてしまえば、Datadog 一つにシステム監視ツールを統一でき、相応のメリットがあると思います。

また、CodeHotspots が Go に対応されれば、コードベースのパフォーマンス分析が可能になってメリットも増えるため、アップデート情報を待ちたいと思います。

最後まで閲覧いただきありがとうございました。少しでも参考になれば幸いです。

Jetpack Composeのチュートリアルをやってみた

f:id:nanakookada:20211126110305p:plain

はじめに

こんにちは。MAMADAYSでAndroidアプリの開発を担当している高野です。

UIはXMLで作成したほうが楽なのではないかと思い、まだ Jetpack Composeを触ったことがなかったのでチュートリアルに沿って進めながら体験してみたいと思います。

※Jetpack Compose は Android の UI を構築するための新しいツールキットです。2021年7月にバージョン 1.0 をリリースし、チュートリアルのページも用意されました。

チュートリアルでやること

チュートリアルでは4つのレッスンを通して次のことを学ぶことができるようです。

  1. 要素の追加
  2. プレビュー方法
  3. 複数要素のレイアウト
  4. デザインテーマの適用
  5. リストの実装
  6. アニメーションの実装

環境の準備

現在使っているAndroid Studioをそのまま使用します。

  • Android Studio Arctic Fox 2020.3.1 Patch 3

プロジェクトは新規で作成し、テンプレートは Empty Activity を使用します。

f:id:itsmynote:20211124185126p:plain:w380

Empty Activity のプロジェクトでJetpack Composeを使用できるようにセットアップの例に合わせてgradleファイルを修正します。

build.gradle

@@ -6,7 +6,7 @@ buildscript {
     }
     dependencies {
         classpath "com.android.tools.build:gradle:7.0.3"
-        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31"
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.21"

         // NOTE: Do not place your application dependencies here; they belong
         // in the individual module build.gradle files

app/build.gradle

@@ -15,7 +15,9 @@ android {

         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
     }
-
+    buildFeatures {
+        compose true
+    }
     buildTypes {
         release {
             minifyEnabled false
@@ -28,6 +30,11 @@ android {
     }
     kotlinOptions {
         jvmTarget = '1.8'
+        useIR = true
+    }
+    composeOptions {
+        kotlinCompilerVersion '1.4.21'
+        kotlinCompilerExtensionVersion '1.0.0-alpha10'
     }
 }

@@ -37,7 +44,17 @@ dependencies {
     implementation 'androidx.appcompat:appcompat:1.3.1'
     implementation 'com.google.android.material:material:1.4.0'
     implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
+
+    implementation 'androidx.compose.ui:ui:1.0.0-alpha10'
+    implementation 'androidx.compose.ui:ui-tooling:1.0.0-alpha10'
+    implementation 'androidx.compose.foundation:foundation:1.0.0-alpha10'
+    implementation 'androidx.compose.material:material:1.0.0-alpha10'
+    implementation 'androidx.compose.material:material-icons-core:1.0.0-alpha10'
+    implementation 'androidx.compose.material:material-icons-extended:1.0.0-alpha10'
+    implementation 'androidx.compose.runtime:runtime-livedata:1.0.0-alpha10'
+
     testImplementation 'junit:junit:4.+'
     androidTestImplementation 'androidx.test.ext:junit:1.1.3'
     androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
+    androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.0.0-alpha10'
 }

この後の流れはチュートリアルの内容をなぞるだけなので実装については割愛します。

チュートリアルを終えて

チュートリアルはとてもよくできていて、つまずくポイントはほとんどありませんでした。強いて言えば、Lesson3のマテリアルデザインを使用するためのComposeTutorialThemeって何だろう…?という部分ぐらいです。一通り終えるため、今回はThemeと入力した時にサジェストされた MaterialThemeという記述で代用しました。

Lesson1からLesson4までを通して次のような成果物ができました。

f:id:itsmynote:20211124185131g:plain

少しカスタマイズしてみる

チュートリアルのサンプルはチャット画面のようなレイアウトになっています。サンプルのままだと一人でチャットしているようで寂しいので、登場人物を二人にしてみたいと思います。

イメージとしてはこんな感じです。

f:id:itsmynote:20211124185205p:plain:w380

Message の修正

データクラスMessageをsealed classにしてMe/Youを追加します。

sealed class Message {
    abstract val author: String
    abstract val body: String

    data class Me(override val author: String, override val body: String) : Message()
    data class You(override val author: String, override val body: String) : Message()
}

サンプルデータ

サンプルデータは好みで修正します。

object SampleData {
    // Sample conversation data
    val conversationSample = listOf(
        Message.You(
            "Colleague",
            "Test...Test...Test..."
        ),
        Message.Me(
            "Me",
            "List of Android versions:\n" +
...

幅一杯に広げる

ModifierクラスにfillMaxWidthという関数があるので追加します。

@Composable
fun MessageCard(msg: Message) {
    Row(
        modifier = Modifier
            .padding(all = 8.dp)
            .fillMaxWidth()

位置の調整

MessageCardを含むRowの定義を見てみます。

@Composable
inline fun Row(
    modifier: Modifier = Modifier,
    horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
    verticalAlignment: Alignment.Vertical = Alignment.Top,
    content: @Composable RowScope.() -> Unit
) {
        ...

horizontalArrangementという引数があります。この引数にArrangementの値を渡すことで位置の調整ができるようです。horizontalArrangementを指定する際に引数msgの型を判定して位置を調整します。

@Composable
fun MessageCard(msg: Message) {
    Row(
        modifier = Modifier
            .padding(all = 8.dp)
            .fillMaxWidth(),
        horizontalArrangement = when (msg) {
            is Message.Me -> Arrangement.End
            is Message.You -> Arrangement.Start
        }

合わせて、アイコンや名前・メッセージも修正を加えます。名前とメッセージはColumnの中なので、Columnの定義をみてみます。

inline fun Column(
    modifier: Modifier = Modifier,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    content: @Composable ColumnScope.() -> Unit
) {
        ...

Columnの場合はhorizontalAlignmentAlignmentを指定するようですね。ついでにsurfaceColorも修正しています。

if (msg is Message.You) {
    Image(
        painter = painterResource(R.drawable.ic_launcher_foreground),
        modifier = Modifier
            .size(40.dp)
            .clip(CircleShape)
            .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape)
    )

    Spacer(modifier = Modifier.width(8.dp))
}

...
val surfaceColor: Color by animateAsState(
    if (isExpanded) MaterialTheme.colors.primary else when (msg) {
        is Message.Me -> MaterialTheme.colors.surface
        is Message.You -> MaterialTheme.colors.secondary
    },
)

Column(
    modifier = Modifier.clickable { isExpanded = !isExpanded },
    horizontalAlignment = when (msg) {
        is Message.Me -> Alignment.End
        is Message.You -> Alignment.Start
    }
) {
        ...
}

if (msg is Message.Me) {
    Spacer(modifier = Modifier.width(8.dp))

    Image(
        painter = painterResource(R.drawable.ic_launcher_foreground),
        modifier = Modifier
            .size(40.dp)
            .clip(CircleShape)
            .border(1.5.dp, MaterialTheme.colors.secondary, CircleShape)
    )
}

エミュレータで確認

チャット画面らしくなりましたね。

f:id:itsmynote:20211124185207p:plain:w380

最後に

Composeでの実装は想像していたよりずっと簡単でした。特に、リストがたった5行で実装できることに驚きました。RecyclerViewと比べるととても簡単ですよね。

まだComposeに慣れていないためもどかしさは感じましたが、簡単なアプリをいくつか作っていくうちに感覚的につかめそうだなといった印象です。

@angular/flex-layoutをtailwindcssで置き換える

f:id:nanakookada:20211116163840p:plain

はじめに

こんにちは、DELISH KITCHEN開発部でバックエンド開発を担当している高木です。 DELISH KITCHENのリテールソリューションズ事業部(以下、RS事業部)が、小売向けに展開している店頭サイネージの管理画面等の開発をしています。

DELISH KITCHEN RS事業部で提供している管理画面は、AngularというWebフレームワークを用いて開発されています1。 画面のレイアウト(Flexbox, Grid CSS + mediaQuery)には、Angular公式である@angular/flex-layoutというライブラリを使用していたのですが、 開発が活発ではないのと、ずっとベータの状態というのもあり、他の方法を模索していました。 そんな時に最近、tailwindcssというCSSフレームワークが話題になっていたのと、@angular/flex-layoutと同じようにHTML上でレイアウトが記述できることから、試しに置き換えた話をします。

tailwindcssとは

flex、pt-4、text-center、rotate-90などのCSSクラスが集まったutility-first CSSフレームワークです。下記のように、用意されているutilityクラスをHTML上で組み合わせることによってデザインしていくことが出来ます。

<div class="flex flex-row gap-2 p-4">
  <div class="text-blue-600 text-xl">Hello,</div>
  <div class="text-green-600 text-2xl">World</div>
</div>

導入

Angular CLI v11.2からtailwindcssが公式サポートされたので、導入は簡単です。 まずはtailwindcssをプロジェクトに追加します。

npm install tailwindcss

インストールが終わったら、設定ファイルを生成します。

npx tailwindcss init

初期設定のままでは不都合が多いので、下記の項目を設定しています。

  • purge
    • tailwindcssで用意されている全クラスが含まれてCSSのサイズが肥大化してしまので、使用しているクラス以外は除外する設定
  • prefix
    • クラス名が他のCSSフレームワークやアプリ固有のクラスと被らないための設定
  • screens
    • レスポンシブ対応のための設定で、ここではモバイルとそれ以外で分けている
  • important
    • tailwindcssのクラスを常に優先させるための設定
module.exports = {
  prefix: 'tw-',
  purge: {
    content: ['./apps/**/*.{html,ts,css,scss}', './libs/**/*.{html,ts,css,scss}']
  },
  darkMode: false, // or 'media' or 'class'
  theme: {
    extend: {},
    screens: {
      xs: { max: '599px' },
      'gt-xs': { min: '600px' }
    }
  },
  variants: {
    extend: {}
  },
  plugins: [],
  important: true
};

最後にstyles.scssに下記を追加します。

@import 'tailwindcss/components';
@import 'tailwindcss/utilities';

公式ドキュメントではtailwindcss/baseも追加していますが、これは標準タグのスタイルに変更が入ってしまうので、抜いています。

以上で導入は完了したので、次は実際に@angular/flex-layoutの部分をtailwindcssに置き換えていきます。

変換表

@angular/flex-layoutは、tailwindcssと同じくHTML上でレイアウトを記述するので移行は簡単でした。 プロジェクトで使用していたAPIのみ、下記に変換表を載せておきます。

@angular/flex-layout tailwindcss
<div fxLayout="column"> <div class="tw-flex tw-flex-col">
<div fxLayout="row"> <div class="tw-flex tw-flex-row">
<div fxLayout.xs="column" fxLayout.gt-xs="row"> <div class="tw-flex xs:tw-flex-col gt-xs:tw-flex-row">
<div fxLayout="row wrap"> <div class="tw-flex tw-flex-wrap">
<div ... fxLayoutGap="16px"> <div class="... tw-gap-4">
<div ... fxLayoutAlign="start center"> <div class="... tw-items-center">
<div fxFlexFill> <div class="tw-w-full">

まとめ

@angular/flex-layoutからtailwindcssにレイアウト部分の記述を移行しました。Angularが公式にtailwindcssをサポートしているのと、 どちらも似たような書き方で記述できることから、移行は比較的に簡単に出来ました。

今回tailwindcssを使用してみて、用意されているクラス名を組み合わせていくことで、クラス名を考える必要がなくなり、 CSSサイズの削減にも繋がるメリットを感じられたので、レイアウト以外のスタイルもtailwindcssに乗り換えていくことを検討中です。