エムスリーエンジニアリンググループ AI・機械学習チームでソフトウェアエンジニアをしている中村(po3rin) です。検索とGoが好きです。
今回はElasticsearchのバージョンをアップグレードする際に、Elastic Cloudのデプロイメント管理をTerraformに移行する話をします。
なぜTerraformに移行する必要があったか
Elastic CloudではコンソールからUpgradeボタンでローリングアップデートが可能です。とても楽ですね。
しかし、このボタンの問題はロールバックが直ぐにできない点にあります。デプロイメントで選べるElasticsearchのバージョンは最新含め3つしかないので、古いデプロイメントから移行すると、元のバージョンに直ぐに戻せません。また、AI・機械学習チームではSudachiを利用しているので、Sudachiのバージョンも上げる必要があります。そのため、単純にUpgradeボタンを押しただけではプラグインのバージョンエラーが出てしまいます。
そのため、弊社では移行先のデプロイメントを新しく立てて、reindexを行い、新しいデプロイメントが安定して動く動作確認をした後に、検索APIやデータ投入Batchの向き先であるURLを切り替える運用を行なっています。
このデプロイメントを立てる作業が職人技になっており、他の人が作業するのを困難にしていました。
例えば、弊社ではmappingを使ってindexを作成するのですが、デフォルトの設定だと、データ投入すると自動で簡易的なmapping設定のindexが作成されてしまいます。そのため、デプロイメントを立てたらクラスタの設定を変更するなどのトリッキーな作業が発生していました。
そこでデプロイメント作成時に何が必要なのかをコードで理解できるようにし、作業コストを減らすために、Terraformを導入することにしました。
Terraformへの移行
今回定義したTerraformは下記になります。下記はサンプルとしてQA用の最小構成を想定しています。
provider "ec" { timeout = "15m" verbose_file = "15m" } locals { sudachi_bundle_file_path = "es/sudachi/sudachi_bundles.zip" plugin_file_path = "es/sudachi/plugin/elasticsearch-8.8.1-analysis-sudachi-3.1.0.zip" dict_bundle_file_path = "es/dict/dict.zip" user_settings_override_file_path = "es/user_settings_yaml.yaml" } data "ec_stack" "latest" { version_regex = "8.8.1" region = "ap-northeast-1" } resource "ec_deployment_extension" "sudachi_dict_bundle" { name = "XXX-sudachi-dict-${local.env}-all-version" description = "Sudachi用バンドル。主にv8.8.1用に生成したが、明確なバージョンの縛りはない。" version = "*" extension_type = "bundle" file_path = local.sudachi_bundle_file_path file_hash = filebase64sha256(local.sudachi_bundle_file_path) } resource "ec_deployment_extension" "sudachi_plugin" { name = "XXX-sudachi-plugin-${local.env}-8.8.1" description = "Elasticsearch v8.8.1対応Sudachi Plugin" version = data.ec_stack.latest.version extension_type = "plugin" file_path = local.plugin_file_path file_hash = filebase64sha256(local.plugin_file_path) } resource "ec_deployment_extension" "XXX_dict_bundle" { name = "XXX-dict-bundle-${local.env}-all-version" description = "Sudachi以外のdict" version = "*" extension_type = "bundle" file_path = local.dict_bundle_file_path file_hash = filebase64sha256(local.dict_bundle_file_path) } resource "ec_deployment" "XXX-deployment" { name = local.deployment_name region = data.ec_stack.latest.region version = data.ec_stack.latest.version deployment_template_id = local.deployment_template_id elasticsearch = { config = { plugins = ["analysis-icu", "analysis-kuromoji"] user_settings_yaml = file(local.user_settings_override_file_path) }, hot = { autoscaling = {} size = local.memory_size zone_count = local.zone_count }, extension = [ { name = ec_deployment_extension.sudachi_dict_bundle.name type = "bundle" version = data.ec_stack.latest.version url = ec_deployment_extension.sudachi_dict_bundle.url }, { name = ec_deployment_extension.XXX_dict_bundle.name type = "bundle" version = data.ec_stack.latest.version url = ec_deployment_extension.XXX_dict_bundle.url }, { name = ec_deployment_extension.sudachi_plugin.name type = "plugin" version = data.ec_stack.latest.version url = ec_deployment_extension.sudachi_plugin.url }, ] } kibana = { size = "1g" zone_count = 1 } }
ポイントを紹介します。
ExtensionをTerraformを管理する
ExtensionもTerraformでアップロードするようにしています。
今まではExtensionは自分でzip圧縮してアップロードする運用を行なってきました。Extensionには辞書ファイルやSudachiなどのPluginが含まれます。
Extensionを自分達でアップロードしていくと、どのExtensionがどのデプロイメントで使われているかがぱっと見分かりません(デプロイメントを一個一個見ていって使われているかを確認する必要がある)。実際に古いExtensionがElastic Cloudに残ってしまっているという状況が発生し、Extensionの管理コストが上がっていました。
タイムアウトの定義
Terraform Providerの定義の中でtimeoutを定義できます。
provider "ec" { timeout = "15m" verbose_file = "15m" }
デプロイメントを立ち上げるのは時間のかかる処理なので、timeoutを設定しなくてはいけません。デフォルトだとtimeout
が1m
とだいぶ短いので安定して立ち上げたい場合はtimeoutは必須の設定になります。
verbose_file
は各API呼び出しのtimeoutの設定です。Extensionをアップロードする際にファイルが大きいと若干時間がかかるので、ここも長めに設定しておいた方が良いでしょう。
Cluster設定
config.user_settings_yaml
フィールドで、デフォルトの設定を任意の設定で上書きできます。弊社ではデータ投入時のindex自動作成を避けたいのでクラスタの設定であるaction.auto_create_index
をfalse
にしています。
action.auto_create_index: false
その他設定できる項目は下記をご覧ください。
output
パスワードなどのクレデンシャルは実行時に取得できるようにoutput.tf
に定義しておきます。後からコンソールでパスワードが取得できないので、ここでのアウトプット定義は必要です。
output "credential_name" { value = ec_deployment.myapp-deployment.elasticsearch_username } output "credential_password" { value = nonsensitive(ec_deployment.myapp-deployment.elasticsearch_password) } output "endpoint" { value = ec_deployment.myapp-deployment.elasticsearch.https_endpoint }
SudachiのバージョンアップとExtensionの更新
TerraformでアップロードするExtensionを更新します。
Sudachi Pluginの更新
SudachiのPluginはElasticsearchにバージョンを合わせる必要があります。そのため、Sudachiのリポジトリから対象バージョンのSudachi Pluginをダウンロードしておきます。もし、対象バージョンのPluginがまだリリースされていない場合は、コードから直接ビルドします。今回の移行では対象バージョンがリリースされていたのでそのまま利用しました。
詳細はSudachi Pluginのリポジトリをご覧ください。
カスタム辞書とシノニム辞書の更新
Sudachi Pluginを更新したらカスタム辞書も更新します。こちら、弊社では様々な独自ツールを使って以前使っていたkuromoji辞書からSudachi辞書を作成しています。詳しくは私が書いた下記のブログをご覧ください。
Reindex作業
Elastic CloudでDeploymentが立ち上がったらReindex作業です。
Snapshot Restoreという方法もありますが、こちらはindex構造ごとRestoreされるので、古いSudachiの分割結果が残ってしまい、検索に引っ掛からなくなってしまう単語が出てきます。そのため、Reindexでデータの移行をします。
まずは新しいDeploymentにindexを作成します。弊社ではeskeeperというツールでaliasやindexを管理しているのでそのファイルを変更してapplyすれば完了です。下記はeskeeperでindexを作成する例です。Aliasもeskeeperで切り替えられるようになっているので、新しいIndexを作成した場合は無停止で向き先を切り替えられるようになっています。
index: - name: docs-v0.1 mapping: es/mapping/docs.json status: close alias: - name: docs index: - docs-v0.1
eskeeperのリポジトリはこちらです。
Reindex対象の新しいDeploymentにはまだAPIが向いていない状態なので、本番影響が発生しうる負荷も気にせずにReindexできます。
今は愚直にローカルからAPIを叩く運用になっているので、ここも今後上手く属人化を排除していきたいと考えています。
まとめ
今回はElasticsearch Deployment管理をTerraformに移行したお話をしました。作業コストの削減、属人化の排除の観点でTerraform化はおすすめです。Pluginのバージョン更新も一緒に行う際にはTerraformでExtensionも管理できるとより便利です。
今後さらに検索エンジン運用改善を行なっていきたいと思います!
We are hiring !!
AI・機械学習チームでは情報検索/推薦が好きなエンジニアを募集中です。カジュアル面談でより詳しくお話ししましょう!