こんにちは、ソフトウェアエンジニアの @ajfAfg です。
弊社には複数のヘルプサイトが存在しますが、その一部を半年ほどで刷新しました。刷新と呼んでいますが、WOVN という多言語化用 SaaS の導入に加え、ヘルプサイトのコンテンツを作成するテクニカルライターの生産性向上を狙った取り組みも含まれていました。本稿では、刷新プロジェクトの中で私が担当した取り組みを紹介します。
なお、本稿では特に断りがない場合、旧ヘルプサイトは刷新前のヘルプサイトを指し、新ヘルプサイトは刷新後のヘルプサイトを指すものとします。文脈から明らかな場合は単にヘルプサイトと書く場合もあります。
目次
- 目次
- 背景
- 事前調査および技術選定
- インフラ構築
- テクニカルライターの生産性向上
- 新ヘルプサイトの品質保証
- マイグレーション戦略
- そして事件は起こった
- 今後の展望
- まとめ
背景
弊社のヘルプサイトは 10 年以上開発・運用されているため、後述する技術的負債が多く残っていました。そのため、ヘルプサイトの開発者のオンボーディングコストや、ヘルプサイトのコンテンツ管理にかかるコストが嵩んでいました。そこで、ちょうど諸々のタイミングが良かったのもあり、今回のヘルプサイト刷新プロジェクトが始まりました。
なお、ソフトウェアエンジニアとして刷新プロジェクトに参加したのは、初期は私と @mugi_uno の 2 名でした。(途中から週 1 で @chihiro_adachi が加わりました。)@mugi_uno はフロントエンドを担当し、私はそれ以外を担当しました。
刷新プロジェクトのスコープ
旧ヘルプサイトは jp.cybozu.help から配信されており、弊社で開発・販売している各製品のヘルプサイトがここに集約されています:

このうち、「cybozu.com 共通管理(/general)」、「kintone(/k)」、「cybozu.com の購入(/s)」が今回の刷新対象です。
刷新プロジェクトにおけるインフラのゴール
議論の結果、これらの刷新対象は別ドメイン(jp.kintone.help)から配信することになったため、刷新プロジェクトにおけるインフラの主なゴールは概ね以下の通りでした:
- 「cybozu.com 共通管理」、「kintone」、「cybozu.com の購入」が
jp.kintone.helpから配信されている。 - 「cybozu.com 共通管理」、「kintone」、「cybozu.com の購入」に関する旧ヘルプサイトのリンクが生きている。
- リダイレクトを挟んでもよい。
- 旧ヘルプサイトの技術的負債が存在しない。
なお、弊社の製品は海外にも展開されているのですが、日本向け、US 向け1、中国向けで製品内容や販売形態がやや異なります。そのため、ヘルプサイトも日本向け、US 向け、中国向けで 3 つ存在します。この区分をリージョンと呼んでおり、それぞれ JP/US/CN リージョンです。リージョンごとにドメインも異なるため、これら 3 つのリージョンについて上記のゴールを達成する必要がありました。
旧ヘルプサイトのインフラ
旧ヘルプサイトでは、各製品のヘルプサイトがそれぞれ個別に Amplify2 で配信されていました。これらを単一のドメインから配信するため、JP/CN リージョンでは Amplify を用いて各ヘルプサイトをリバースプロキシし、US リージョンでは CloudFront3 を用いてリバースプロキシしていました。なお、US リージョンでは、CloudFront のエッジサーバーとして Lambda 関数4が動いていました。以下に図示します:

旧ヘルプサイトのインフラに関する技術的負債
旧ヘルプサイトのインフラに関する技術的負債は、大きく分けて 3 つあります。
ほぼ全てのインフラが手動で構築されていた
ほぼ全て手動でインフラが構築されていたので、旧ヘルプサイトで使われているリソースの関係性やインフラの状態が分かりづらかったです。Amplify や Lambda 関数のデプロイは自動化されていましたが、自作スクリプトを用いたデプロイだったので保守・運用に難がありました。
テストや監視がなかった
テストや監視がなかったので、特にチームに新しく入った開発者にとってはリソースの変更に高いハードルがありました。開発環境は存在していたのですが、本番環境との差分が多く信用できませんでした。
リージョン間の意図しない差分が多かった
旧ヘルプサイトには、昔 Netlify で配信されていた名残で Netlify のリダイレクトの仕組み(以下、redirects)に似る仕組みがあり、例えばテクニカルライターがヘルプサイトのページの構成を変える場合(e.g. /news/foo → /blog/foo)などで活用されていました。しかし、この仕組みは JP/CN リージョンと US リージョンで仕様が大きく異なっており、この仕組みを主に利用するテクニカルライターの工数がかなり割かれていました。さらに、この仕組みは本家の redirects の仕様とは異なる独自実装が多く、特に製品ごとの条件分岐が多数存在し保守が困難でした。
他にも、節「旧ヘルプサイトのインフラ」で示した通りリージョンごとにアーキテクチャが異なるなど、意図しない差分がいくつかありました。
事前調査および技術選定
ヘルプサイトを刷新するにあたって、刷新コストや今後の運用コストが最も低い手段を選びたいです。そこで事前にいくつか調査しました。
コンテンツ管理システム
まず、諸々のコストが最も低いと期待できるコンテンツ管理システム(CMS)をいくつか調査しました。しかし、調べた限りでは要件を満たす CMS がなく断念しました。一番ネックだったのは、旧ヘルプサイトでは独自の Markdown 記法や機能が多用されている点でした。
静的サイトジェネレーター
スクラッチ開発を決断したので、次は静的サイトジェネレーターを調査しました。私もいくつか調べたのですが、最終的には @mugi_uno が調査した Astro を採用しました。Astro はカスタマイズ性に富んでいる点が決め手でした。
ホスティングサービス
次はスクラッチ開発したサイトを配信するインフラを決める必要があります。ホスティングサービスが利用できると一番楽なので主要なサービスは一通り調べたのですが、どれも要件を満たさなかったのでインフラもスクラッチ開発することにしました。一番ネックだったのは、旧ヘルプサイトでは 1,000 個を超えるリダイレクトのルールが存在していた点でした。Firebase Hosting はその点優秀で、設定可能なリダイレクトの数に上限がなく魅力的でした(実験したところ、少なくとも 2,000 個は設定可能でした)。しかし、redirects と同等の表現力、特にリダイレクト元のパスの一部をキャプチャーしてリダイレクト先に展開する機能がなかった(2025 年 6 月時点)ので採用しませんでした。
クラウドベンダー
ここはほぼ比較検討せず AWS を採用しました。理由としては、旧ヘルプサイトのインフラが AWS だったので、旧ヘルプサイトのインフラの良いところ・悪いところを参考にしつつアーキテクチャを決定できるからです。他にも、弊社には AWS を便利に使う仕組みが存在していたり、私が使い慣れているのも採用理由として大きかったです。
▶︎ ひとくち振り返り
この辺りの意思決定は全く後悔していないので良い選択だったと思います。刷新プロジェクトなので当初は把握できていなかった要件が後から見つかる可能性が高かった(し、実際いくつかあった)のですが、スクラッチ開発はそのような要件にも柔軟に対応できるだろうと思える点も精神衛生上良かったです。
インフラ構築
事前調査と技術選定の結果、新ヘルプサイトのインフラは AWS を用いてスクラッチ開発することになりました。本節では、新ヘルプサイトのインフラがどのように構築されているのか紹介します。
WOVN 導入
WOVN とは Web サイトを多言語化する SaaS です。今回の刷新プロジェクトは、WOVN の導入も実は含まれていました。
説明の都合上、WOVN の仕組みを先に説明できると嬉しいので、まず初めに WOVN 導入のモチベーションや WOVN の導入方法を紹介します。
WOVN 導入のモチベーション
いくつかありますが、最も大きいのは人手による翻訳が追いついていない箇所を機械翻訳できる点です。他にも、弊社のローカライズチームの翻訳プロセスと合致していたのも大きかったです。
WOVN の導入方法
WOVN の導入方法はいくつかありますが、今回はプロキシ方式(CDN パターン)により導入しています5。この方法の場合、WOVN から提供されるプロキシに対して、我々の CDN からリバースプロキシすればよいです。「プロキシ方式の導入パターン – WOVN.io HelpCenter」より、システム図を以下に引用します:
新ヘルプサイトでは、日本語ページのパス(e.g. /k/ja)へのリクエストはオリジンに流し、多言語ページのパス(e.g. /k/en)へのリクエストは WOVN のプロキシに流すよう設定しています。こうすることで、多言語ページのパスにリクエストを送ると翻訳済みの HTML がレスポンスされます6。
▶︎ ひとくち振り返り
他社の方と協力してプロジェクトを進める経験が積めて良かったです。
静的サイトの配信基盤の構築
静的サイトの配信基盤は比較的シンプルな作りになっていて、主な登場人物は Route 53、WAF、CloudFront、Lambda 関数、DynamoDB、S3、GitHub Actions です。Route 53 は DNS で、WAF はファイアウォール、CloudFront は CDN として機能し、Lambda 関数は主にエッジサーバーとして働きます(エッジサーバーとして働く Lambda 関数は Lambda@Edge と呼ばれます)。オリジンは Lambda 関数と S3 から成る配信サーバーで、配信サーバーが配信するファイルは GitHub Actions からアップロードします。DynamoDB の役割やオリジンが S3 ではない理由は後述します。今のところ全リージョン共通して同じアーキテクチャです。アーキテクチャ図を以下に示します:

日本語コンテンツは配信サーバーから配信され、多言語コンテンツは WOVN プロキシから配信されます。なお、日本語コンテンツの利用者の多くが日本在住だと想定されるため、配信サーバーはマルチリージョン展開しませんでした。
Netlify のリダイレクトの仕組み(redirects)の模倣
節「旧ヘルプサイトのインフラに関する技術的負債」で述べた通り、旧ヘルプサイトでは Netlify のリダイレクトの仕組み(redirects)に似る仕組みがサポートされていたため新ヘルプサイトでも実装しました。本節では redirects の実装や活用方法について説明します。
redirects 速習
脇道に逸れますが、redirects について理解していると今後の話を理解しやすい思うので軽く説明します。理解していなくても雰囲気で読めるとは思うので、節「新ヘルプサイトにおける redirects の実装」まで飛ばしていただいても構いません。
Netlify では redirects を用いてリダイレクト・リライトのルール(以下、リダイレクトルール)を設定できます。redirects と呼んでいますが、リダイレクトだけでなくリライト(i.e. リバースプロキシ)も可能な点に注意してください。リダイレクトルールは _redirects ファイルに記述します。
リダイレクトの基本
リダイレクト元のパスとリダイレクト先のパスを行ごとに記述します:
/home / /foo/bar /hoge/fuga 302 # これはコメント
上記のルールを記述している場合、例えば /home というパスへのリクエストは / にリダイレクトされます。
リダイレクト先のパスの後に HTTP レスポンスステータスコードを記述可能で、例えば上記のルールの場合は /foo/bar にアクセスすると 302 番で /hoge/fuga にリダイレクトされます。HTTP レスポンスステータスコードを省略した場合は 301 番でリダイレクトされます。
# から始まる行はコメントです。
リダイレクトを強制する
デフォルトでは、リダイレクト元のパスにリソースが存在する場合、そのリダイレクトルールは無視されます。例えば、リダイレクトルールが全く記述されていない状態で /home にリクエストすると 200 番で返ってくる場合、リダイレクト元として /home を書いてもそのルールは無視されます。
リソースが存在しても強制的にリダイレクトしたい場合は、HTTP レスポンスステータスコードの後ろに ! を記述します:
/home / 301!
404 リライト
逆に言うと、デフォルトでは、リダイレクトルールはリダイレクト元のパスにリソースが存在しない場合にマッチするので、redirects を利用して 404 ページのレスポンスも可能です。例えば以下のリダイレクトルールを書くと、404 番で 404 ページをレスポンスできます(* の意味は後述しますが、ここではワイルドカードみたいなものだと思ってもらえたら問題ありません):
/* /404.html 404
200 リライト
以下のようなリダイレクトルールでリライト(i.e. リバースプロキシ)できます:
/ https://example.invalid 200
パスを展開する
例えばページの構造を変えたいとき、リダイレクトルールを一つずつ記述するのは大変です。そのような場合は、:splat とプレースホルダーという二つの機能を利用すると楽に記述できる可能性があります。
まず、以下のルールを記述している場合、:splat を /news 配下の任意のパス(* に相当する部分)に束縛し、リダイレクト先の :splat に展開できます。例えば /news/foo/bar にアクセスすると /blog/foo/bar にリダイレクトされます。
/news/* /blog/:splat
注意点として、* はパスの末尾にのみ記述できます。つまり、例えば /news/*.html /blog/:splat は記述できません。
:splat を用いる方法はパスの一部分だけを展開できませんが、プレースホルダーを用いると可能です。Netlify の公式ドキュメントより、例を以下に引用します:
/news/:month/:date/:year/:slug /blog/:year/:month/:date/:slug
上記のルールを記述している場合、例えば /news/02/12/2004/my-story にアクセスすると /blog/2004/02/12/my-story にリダイレクトされます。
新ヘルプサイトにおける redirects の実装
新ヘルプサイトでは、CloudFront のエッジサーバーである Lambda@Edge として redirects を実装しました。旧ヘルプサイトではいくつかの仕様が実装されていなかったり仕様が異なっていたりしたのですが、新ヘルプサイトでは「Redirect by role」と「Signed proxy redirects」、「Redirect by cookie presence」を除いて全ての仕様を正確に実装しています。結果としてリダイレクトルールの表現力が上がっており、特にプレースホルダーを利用可能な点が効いて _redirects ファイルのサイズを削減できました。具体的には、旧ヘルプサイトの _redirects ファイルのサイズは約 90 KB だったところ、新ヘルプサイトでは約 30 KB まで削減できました。
新ヘルプサイトにおける独自仕様
一方で、この仕組みを利用してブラウザでの表示言語に適した 404 ページにリダイレクトするには表現力が足りませんでした7。そこで、「Redirect by country or language」と「Redirect by cookie presence」周りにあった仕様の余白を活用し、独自の仕様を追加しました。具体的には、リダイレクトルールとして
/ /ja 302 Language=ja
のように書くと HTTP ヘッダー Accept-Language をもとにルーティング可能なのですが、Language/Country/Cookie 以外の仕様が何も明記されていなかったので、ここを活用して任意の HTTP ヘッダーをもとにルーティング可能にしました。例えば
/ /hoge 302 foo=bar
というリダイレクトルールは、HTTP ヘッダー foo: bar を持つ、パス / へのリクエストとマッチします。WOVN は多言語ページ(e.g. /en/path/to)が存在しない場合、オリジナル言語のページ(e.g. /ja/path/to)にリライトします。リライト時に x-wovn-language: en といった HTTP ヘッダーが乗るので、この独自仕様を用いると表示言語に適した 404 ページ(e.g. /en/404.html)にリダイレクトできる訳です。
WOVN プロキシへのリライト
なお、WOVN プロキシへのリライトもまた、この仕組みで実現しています。リライト時に特定の HTTP ヘッダーを載せる必要があるのですが、それが可能な点が嬉しいです。また、WOVN プロキシのリライトに関する設定はリージョンごとにやや異なるのですが、その差分の表現は単に _redirects ファイルを分けるだけで良い点も嬉しいです。新ヘルプサイトで一番大事な仕組みと言っても過言ではありません。
redirects を模倣する仕組みのパフォーマンス
そんなに大事な仕組みですが、キャッシュミスした全てのリクエストに対して動作するのでパフォーマンスも気になるところです。そこは気を遣って実装しており、コアロジックはリダイレクトルールの列の長さについておよそ線形時間で動作します。あまり真面目に測っていませんが、1,000 個のリダイレクトルールに対して約 5 ms で動作します。
登場人物がやや多い理由
CloudFront のオリジンとして S3 を設定しない理由や、DynamoDB の役割について述べます。
CloudFront のオリジンとして S3 を設定しない理由
CloudFront のオリジンとして直接 S3 を設定せず Lambda 関数を挟んでいるのは、WOVN に対応するためです。まず前提として、新ヘルプサイトでは index document 機能(/path/to へのリクエストを /path/to/index.html にリライトする機能)をサポートしています。WOVN では基本的にエンドユーザーがアクセスする URL(e.g. /path/to)に対して翻訳データを登録するため、もし Lambda@Edge 上で URL 末尾に /index.html を付与すると WOVN の翻訳データが存在しないため困ります。そのため、Lambda@Edge ではなく、Lambda 関数を用いた配信サーバーで index document 機能をサポートしています。
ちなみに S3 の静的ウェブサイトホスティングは index document 機能をサポートしているようですが、HTTPS 通信をサポートしていないため採用しませんでした。また、Lambda 関数を配信サーバーとして利用する場合は、レスポンスサイズの上限が 6 MB である点が気になりますが、レスポンスをストリーミングして上限を 200 MB まで引き上げています。
DynamoDB の役割
DynamoDB の役割は、Lambda@Edge が読む _redirects ファイル(約 30 KB)の保存です。DynamoDB のテーブルはグローバルテーブルという機能を使ってマルチリージョンにデプロイしており、Lambda@Edge はエンドユーザーから最も近いリージョンのテーブルから _redirects ファイルを読んでいます8。他の方法もいくつか考えたのですが、以下に示す通り要件を満たさなかったり、DynamoDB の嬉しさを超えなかったので採用しませんでした:
- Lambda 関数と一緒に
_redirectsファイルをデプロイする方法- メリット: 実行速度が最も高速。料金も無料。
- デメリット:
_redirectsファイルは主にテクニカルライターチームが編集するので、インフラのライフサイクルと異なる。また、CloudFront は Lambda@Edge として利用する Lambda 関数のバージョンを参照する必要があるため、もし Lambda 関数のデプロイとバージョン参照を自動化するなら比較的重厚な CI/CD を組む必要がある。
- Lambda 関数内で
_redirectsファイルをキャッシュする方法- メリット: キャッシュヒット時は実行速度が最も高速。料金も無料。
- デメリット: キャッシュ無効化に工夫が必要。
- SSM Parameter Store に
_redirectsファイルを保存する方法- メリット: 料金が安い。
- デメリット: 保存できるデータのサイズが 8 KB と小さい。
- Secrets Manager に
_redirectsファイルを保存する方法- メリット: 約 66 KB のファイルを保存可能。
- デメリット: 今回扱うデータは機密性が高くはないことを踏まえると、料金が高い。
- S3
- メリット: 料金が安い。
- デメリット: DynamoDB と比較してやや read が遅い(再現実験はしていない)。
DynamoDB を利用する嬉しさとして、今回のデータサイズだったらデータ保存にかかる料金が無料利用枠(25 GB/月)で賄える点もあります。クリティカルな嬉しさではないため、S3 と DynamoDB はどちらを採用しても良かったです。
▶︎ ひとくち振り返り
redirects の模倣はかなり上手くいきました。WOVN プロキシへのリライトをはじめとして、他にも多くのリダイレクト・リライトの要件を実現できるので丁寧に実装した甲斐がありました。明文化されている仕様への影響がないとはいえ、独自仕様の追加は今後悩みの種にならないか逡巡しましたが、結果として筋良く 404 ページを多言語化できて良かったです。
index document 機能のサポートは良くなかったかもと思います。旧ヘルプサイトでは基本的に .html 付きの URL を扱う(index document 機能もサポートはされているが積極的に利用されない)のに対して新ヘルプサイトではそうしなかったのは、元々のモチベーションは「世の中の多くのサイトで採用されている仕様に寄せたい」でした。しかし、先述した問題をはじめとして色々と苦労があったので、そこまでして取り組む内容じゃなかったと思います。幸い Lambda@Edge 上にて、旧ヘルプサイトのリンクを切らさないために、URL 末尾に .html が付いてたら消してリダイレクトする処理を行なっており、その逆を行うことは難しくはないので今後扱いに困ったら元に戻せそうです。
検索基盤の構築
旧ヘルプサイトではサイト内検索が用意されていたので、新ヘルプサイトでも実装しました。旧ヘルプサイトにおけるサイト内検索は、JP/US リージョンでは Google 検索を利用し、CN リージョンではある SaaS を利用していました。しかし、リージョンごとに検索エンジンが異なることで運用の負荷が高まっていたり、ある SaaS と WOVN の相性があまりよくないと想定されたため、新ヘルプサイトでは全リージョンで共通して利用可能な検索基盤を構築しました。
検索基盤の仕組み
検索基盤と言っても大層なことは行なっておらず、OpenSearch Serverless を使って検索する仕組みを作っているだけです。ヘルプサイトのサイト内検索は 2 種類あり、1 つ目はよくある全文検索です。2 つ目は、記事ごとに振られる ID——記事番号——を用いた検索で、存在する記事番号を入力すると対応する記事が出力されます。それぞれの検索方法に合わせてインデックスを作っています。あとは検索サーバーとしての Lambda@Edge を用意して、そこから OpenSearch Serverless にクエリを投げています。
インデックスの運用もシンプルで、定期的に新ヘルプサイトをクロールするたびにフルインデクシングしています9。フルインデクシングの後、新旧インデックスをアトミックに追加・削除しています。同じタイミングでインデックスにエイリアスを張り、検索サーバーからはエイリアスを参照しています。毎回フルインデクシングせず差分を計算してドキュメントを追加・変更・削除する方法も考えたのですが、その仕組みの保守にかかる開発者の人件費よりシンプルなフルインデクシングの方が安いと想定されたのでこうしています。
Google 検索に似るシンタックス・セマンティクスで検索可能にする仕組み
旧ヘルプサイトでは検索エンジンとして Google 検索を使っていたので、Google 検索の記法で検索できました。例えば、"<string>" のように二重引用符で文字列を囲むと、完全一致検索ができました。同様の検索を新ヘルプサイトでも行いたいとの要望があったので作りました。
今回対応した機能は以下の通りです:
- 曖昧検索
<string>のように書く。
- 完全一致検索
"<string>"のように書く。
- AND 検索
<string> <string>のように書く。
- OR 検索
<string> OR <string>のように書く。
- キーワード除外
-<string>のように書く。
- 入れ子
(<string>)のように書く。
▶︎ 完全な構文定義
<string> は他のトークンを含まない任意の文字列です。
<syntax> ::= <syntax'> <syntax> // AND 検索
| <syntax'> OR <syntax> // OR 検索
| <syntax'>
<syntax'> ::= <string> // 曖昧検索
| "<string>" // 完全一致検索
| -<syntax'> // NOT 検索
| (<syntax>) // グルーピング
仕組みとしては、検索文字列を教科書的に字句解析・構文解析し、出てきた抽象構文木をクエリに変換して OpenSearch に投げる、という形になっています。一種のインタプリターと考えてもらえばよいです。
簡単な構文なのでパーサーは手書きしています。今回扱う構文は観察が間違っていなければ LL(1) なので、手書きでも十分に実装できますし、検索文字列の長さについて線形に動いてくれます。この手のライブラリの学習コストや追従コストは意外と馬鹿にならないので、このくらいだったら手書きのほうが楽だろうとの判断です。
簡単に負荷試験も行いましたが、バリデーションを通過した検索文字列については高々数秒で動作してくれました。ヘルプサイトでの検索の流量的にも問題なさそうだったのでスムーズにリリースできました。
▶︎ ひとくち振り返り
検索基盤に関する意思決定の良し悪しはまだわからないです。私はこれまで検索と向き合った経験が全くなく初めてだらけだったので、これから答え合わせしていくところです。有識者からレビューしてはもらっていますが不安は尽きないですね。
Google 検索に似るシンタックス・セマンティクスで検索可能にする仕組みづくりは、私の興味とマッチしており純粋に楽しかったです。
プレビュー環境の構築
旧ヘルプサイトでは、pull request の変更内容を確認可能な環境、いわゆるプレビュー環境が用意されていました。旧ヘルプサイトでは Amplify の Web プレビュー機能を利用していましたが、新ヘルプサイトは Amplify でホスティングしていないのでスクラッチ開発しました。
プレビュー環境の要件
プレビュー環境の要件は概ね以下の通りです:
- 任意の pull request について、その pull request ブランチのフロントエンドを配信するサイト(以下、プレビューサイト)を用意できる。
- プレビューサイトには弊社の社員のみアクセスできる。
- 本番のヘルプサイトと同様のリダイレクトルールをプレビューサイトに適用できる。
- 言い換えると、例えば
/ /fooというリダイレクトルールが本番環境にあったとして、プレビューサイトでは/pr-xxx /pr-xxx/fooのように手直しする必要がない。
- 言い換えると、例えば
- プレビューサイトごとに異なるリダイレクトルールを適用できる。
上記の要件 1--3 は Amplify の Web プレビュー機能でも満たせますが、要件 4 はそうではなかった10ので、これを機に解決を試みました。新ヘルプサイトでは配信サーバーとして Amplify を使っていないのは、実はこの要件 4 を満たすためでもあります。
プレビュー環境の仕組み
とはいえスクラッチでプレビュー環境を構築するなら、要件 4 を満たすのはあまり難しくないです。今回のプレビューサイトの仕組みは単純で、まず pull request ごとにサブドメイン(e.g. pr-xxx)を発行し、pr-xxx.example.invalid のようなドメインからプレビューサイトを配信可能にします。続いて、発行されたサブドメインと S3 のフォルダを対応付け、リクエストが来たらサブドメインに対応するファイルをレスポンスしています。 _redirects ファイルもまた、サブドメインに対応付くキーで DynamoDB に置けばよいです。「Pull Request ごとに S3 + CloudFront へ SPA のプレビュー環境をデプロイする - Classi 開発者ブログ」で紹介されている仕組みを、redirects を模倣する仕組みに合わせてアレンジしています。
プレビュー環境の認証
要件 2「プレビューサイトには弊社の社員のみアクセスできる」を満たすため、弊社では社員の情報を Microsoft Entra ID で管理している点に目をつけ、Entra ID で SSO できるようにしました。仕組みとしては、Entra ID と Cognito を SAML 連携し、npm パッケージ Cognito@Edge を利用して Lambda@Edge 上で認証しています。仕組み自体はシンプルなのですが、やや躓きポイントがあるのにも関わらずネット上で解決策が見つからなかったので、気が向いたら Zenn にて投稿するかもしれません。
▶︎ ひとくち振り返り
プレビュー環境の構築に関しても概ね良い意思決定ができたと思います。プレビュー環境は社内の人間しか使わないので気を遣って isolate する必要が特になく、今回のようなシンプルな構成で今のところ十分です。Entra ID による SSO に関しても、テクニカルライターチームはチーム外の人(e.g. 製品の開発者)にプレビューサイトを共有するタイミングがままあるので、URL のみ共有すればよい新ヘルプサイトのプレビュー環境は生産性向上に寄与できると期待しています。
一方で、今のプレビュー環境には大きな懸念点があり、それは同時に存在可能なプレビューサイトが高々 100 個である点です。1 つの pull request に対して 3 つのリージョンのプレビューサイトが作成されるので、プレビューサイトを作成可能な pull request は高々 33 個です。原因としては、Cognito には認証後にコールバック可能な URL を登録する必要があるのですが、その登録可能な数が 100 だからです。ワイルドカードのような機能はなく、サブドメインごとに上記の URL を登録する必要があるのですぐ枯渇してしまうわけです。流石に懸念が大きすぎるのでなんとかしたいところですが、良いアイディアを思いついておらず手がつけられていません。JP リージョン以外のプレビューサイトは利用頻度が少ないようなので、実は JP リージョンのプレビューサイトのみデプロイする方針で回避可能かもしれません。今後にご期待ください。
監視基盤の構築
新ヘルプサイトの障害に気づけないと怖いため、新ヘルプサイトを監視する簡易的な仕組みを作りました。可能な限り料金を抑えたかったので、今回は Route 53 ヘルスチェックを用いて外形監視しています。監視する URL ごとに 0.50 USD/月な点が魅力的(HTTPS で監視する場合は +1.00 USD/月)です。
なお、プレビュー環境は認証をかけている都合上 Route 53 ヘルスチェックによる外形監視が簡単ではなく、今のところ監視していません。今後の課題です。
▶︎ ひとくち振り返り
Route 53 ヘルスチェック使い勝手がよくて素晴らしいですね。
Terragrunt を用いた Terraform コード設計
旧ヘルプサイトのインフラはコード化されていなかったため、問題がいくつか発生していました。そこで、新ヘルプサイトではごく一部のリソース(e.g. 旧 Chatbot の Slack クライアント)を除き、すべて Terraform でコード化しました。本節では、新ヘルプサイトの Terraform コードをどのように設計したか紹介し、設計の良し悪しを振り返ります。
インフラの要求
新ヘルプサイトでは、開発環境、本番環境、プレビュー環境の 3 つの環境が要求されました。開発環境はインフラの変更を確かめるための環境で、本番環境はエンドユーザーが利用する環境、プレビュー環境はフロントエンドやヘルプサイトのコンテンツの変更を確かめるための環境です。なお、各環境について、さらに JP/US/CN リージョンが要求されます。3 つの環境と 3 つのリージョンで 9 通りの組み合わせとなります。
それぞれの環境にはいくつか差分が想定されました:
- 開発環境、プレビュー環境のドメインは新ヘルプサイトの開発者が用意するが、本番環境のドメインは情シスが用意する。
- 開発環境、プレビュー環境のスペックより、本番環境のスペックの方が高い(e.g. Lambda 関数のメモリサイズ)。
- プレビュー環境には複数のサブドメインが存在する。
- プレビュー環境の Lambda@Edge では認証をかけていたり、配信するリソースをサブドメインごとに出し分けている。
また、旧ヘルプサイトではリージョンごとに差分が生じていた(e.g. JP/US リージョンと CN リージョンで使用する検索エンジンが異なる)ので、新ヘルプサイトでも今後リージョンごとに差分が生じる可能性がありました。よって、上記の差分を表現できる仕組みもまた要求されました。
Terraform コード設計
上記の要求から、新ヘルプサイトでは Terraform モジュールを多用する設計を採用しました。ある意味の単位でモジュールを分け、環境 × リージョンからモジュールを選んで利用する形です。また、私は過去に Terraform State の肥大化で困った経験があるので、モジュールごとに Terraform State を作り、Terragrunt でオーケストレーションすることにしました。「詳解 Terraform 第 3 版」から強い影響を受けています。以下にディレクトリ構造の例を示します:
▶︎ ディレクトリ構造の例
. ├── live │ ├── preview │ │ ├── cn │ │ │ ├── hosting │ │ │ │ ├── cloudfront │ │ │ │ │ └── terragrunt.hcl │ │ │ │ └── route53 │ │ │ │ └── terragrunt.hcl │ │ │ ├── lambda │ │ │ │ ├── package.json │ │ │ │ ├── packages │ │ │ │ │ ├── redirects-in-preview │ │ │ │ │ │ ├── build.mjs │ │ │ │ │ │ ├── package.json │ │ │ │ │ │ ├── src │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ └── terragrunt.hcl │ │ │ │ │ └── ... │ │ │ │ ├── pnpm-workspace.yaml │ │ │ │ └── ... │ │ │ └── ... │ │ ├── global │ │ │ ├── cognito │ │ │ │ └── terragrunt.hcl │ │ │ └── ... │ │ ├── jp │ │ │ └── ... │ │ ├── mise.toml │ │ ├── root.hcl │ │ └── us │ │ └── ... │ ├── prod │ │ ├── cn │ │ │ ├── hosting │ │ │ │ └── cloudfront │ │ │ │ └── terragrunt.hcl │ │ │ └── lambda │ │ │ ├── package.json │ │ │ ├── packages │ │ │ │ ├── redirects │ │ │ │ │ ├── build.mjs │ │ │ │ │ ├── package.json │ │ │ │ │ ├── src │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── terragrunt.hcl │ │ │ │ └── ... │ │ │ ├── pnpm-workspace.yaml │ │ │ └── ... │ │ └── ... │ └── stage │ └── ... └── modules ├── cognito │ ├── main.tf │ └── ... ├── hosting │ └── cloudfront │ ├── main.tf │ └── ... ├── lambda │ ├── mise.toml │ ├── package.json │ ├── packages │ │ ├── redirects │ │ │ ├── main.tf │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── index.ts │ │ │ │ └── ... │ │ │ └── ... │ │ ├── redirects-in-preview │ │ │ ├── main.tf │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── index.ts │ │ │ │ └── ... │ │ │ └── ... │ │ └── ... │ ├── pnpm-workspace.yaml │ └── ... ├── route53 │ ├── main.tf │ └── ... └── ...
modules ディレクトリ配下で Terraform モジュールを定義し、live ディレクトリ配下から呼び出す形になっています。terragrunt.hcl ごとに Terraform State が作られます。
▶︎ Terragrunt の Tips
各 Terraform State は S3 に置いているのですが、キーの計算を自動化したかったので、各環境のルートに置いている root.hcl にて自動化しています:
locals { path_from_root = "${basename(dirname(find_in_parent_folders("root.hcl")))}/${path_relative_to_include()}" default_name = "helpsite-${replace(local.path_from_root, "/", "-")}" } remote_state { backend = "s3" generate = { path = "backend.tf" if_exists = "overwrite_terragrunt" } config = { bucket = "foo" key = "${local.path_from_root}/terraform.tfstate" region = "us-east-1" encrypt = true use_lockfile = true } }
ついでにリソースにつける名前も計算して、変数 default_name に束縛させています 。各 terragrunt.hcl にて、以下のように root.hcl を include すれば default_name が利用できます。大事なのは expose = true です。
include "root" { path = find_in_parent_folders("root.hcl") expose = true }
Lambda 関数のコード設計
Lambda 関数も同様の思想で実装しており、modules 配下では配布可能な npm パッケージとして定義し、live 配下ではパッケージをインストールして利用する形にしています。
このような作りなので、Lambda 関数は他の Lambda 関数からも再利用できます。例えば、modules/lambda/packages/redirects では redirects を模倣する Lambda@Edge を実装していますが、modules/lambda/packages/redirects-in-preview ではそのパッケージをインストールし、プレビュー環境用にチューニングしています(サブドメインに適した _redirects ファイルを読むようにしています)。
一つのパッケージでは単一の責任のみ満たすようにし、複数の責任を持たせたい場合は複数のパッケージをインストールして組み合わせる形にすることで、かなり見通しよく Lambda@Edge を実装できると考えています。
今回の設計で良かったこと・悪かったこと
今回の設計で良かったことは、やはり各環境の差分を表現しやすい点です。例えば、プレビュー環境ではヘルプサイトの開発者が Route 53 のホストゾーンを作るので、そのためのモジュールを呼び出しています(live/preview/cn/hosting/route53)が、本番環境では情シスがホストゾーンを作るので呼び出す必要がないです。実はホストゾーンの作成者が開発・本番環境で異なると分かったのは刷新プロジェクトの途中だった11のですが、今回の設計のおかげで不測の差分も手戻りなく表現できました。こういった差分の表現はモジュール内で条件分岐する方法も考えられますが、利用側の都合をモジュールが知るのは基本的に筋悪だと考えているので避けました。Terraform State が細かく分かれているので、一部のモジュールだけ terraform apply すればよい場合は高速に動作する点も嬉しいです。
悪かったことはリファクタリングが大変な点です。「このリソースは別のモジュールで定義した方が良かった」と判明しても、Terraform State が分かれているので移動が難しいです。手動ローリングアップデートしたり terraform import したりすればよい話ではありますが少し大変です。リファクタリングが大変なのは継続して開発する上で結構な痛手なので、モジュールは多用するけれども、Terraform State は環境 × リージョンごとに 1 つにしてもよいかもしれません。
また、今後発生しそうな困り事としては、開発環境と本番環境の乖離です。今回の設計は差分の表現力が高すぎるので、逆に意図しない差分を作りかねないと懸念しています。開発環境と本番環境はほぼ同一であると期待されるので、一部(e.g. Route 53)を除くモジュールを集約するモジュールを使い回しても良かったかもと思っています。
▶︎ ひとくち振り返り
Terraform の設計難しいですね。Terraform State を分割するのは困ってからでも遅くはないとの知見が得られて勉強になりました。一方で困ってから分割するのも大変なので、ちょうどいい塩梅を見つけていきたいです。
Lambda 関数の設計に関してはかなり上手くいったと思います。
テクニカルライターの生産性向上
ヘルプサイト開発のユーザーは製品のユーザーだけでなく、ヘルプサイトのコンテンツを作るテクニカルライターも含まれます。そのテクニカルライターの生産性向上を狙った取り組みもいくつか実施しました。
textlint の導入
textlint とは文書の校正ツールです。textlint 自体は旧ヘルプサイトでも導入されていたのですが、膨大な数のエラーが無視されていました。そこで、新ヘルプサイトでは一旦いくつかのルールを無効化したり、いくつかのファイルを検査対象外にし、textlint が正常終了する状態を作ってから導入しました。今後は無効化したルールや検査対象外にしたファイルを段階的に元に戻していく予定です。
また、今後同じ状況が起こったら困るので、textlint のエラーとの付き合い方をドキュメントにまとめたり、失敗したジョブが存在する場合に pull request のマージを禁止する仕組みを作りました。後者の仕組みは「モノレポで不要な Github Actions 実行を最小にしつつマージブロック機能(マージ前必須チェックステータス)を使う方法の検討」で紹介されている方法を少しアレンジして作っています。
▶︎ ひとくち振り返り
textlint に限らず、膨大な数のエラーが無視されていたのも旧ヘルプサイトの技術的負債だったので、解決できてよかったです。
textlint によるリンク切れチェックの仕組み
旧ヘルプサイトに存在していたリンク切れチェックの仕組みを textlint で再現しました。旧ヘルプサイトでは独自のリンク切れチェックのプログラムが VM 上で動いていたのですが、プログラムは既存のものを流用できると嬉しいですし、GitHub Actions 上で動かせると楽な面が大きいと判断したためです。方法は「textlint と GitHub の Code Scanning を組み合わせてリンク切れをチェックする | Web Scratch」とほぼ同じです。取り組みを進める中でコントリビュートチャンスをいくつか発見したので、textlint-rule/textlint-rule-no-dead-link に pull request を出したりもしました。
▶︎ ひとくち振り返り
今回のリンク切れチェックは訳あってビルド成果物の HTML ファイルを対象にしているため、数万のリンクを検査する必要がありました。その影響でこの取り組みは想像以上に大変でした。副産物として、textlint-rule-no-dead-link は膨大な量のリンクを捌けるようになったはずなので、皆さんもぜひ利用していただけると嬉しいです。
textlint のプラグイン開発
旧ヘルプサイトではテクニカルライター向けのリンターが Go で実装されていました。新ヘルプサイトではフロントエンド・バックエンドともに TypeScript を利用しており、これまで見てきた通り textlint を導入していたので、そのリンターを textlint のカスタムルールとして再実装しました。ルールの内容は以下の通りです:
- 記事番号の重複を禁止するルール
- 登録した単語の使用を禁止するルール
- このルールのモチベーションとしては、弊社は例えば Garoon という製品を US 向けに販売していないので、US リージョンのヘルプサイトでは「Garoon」という単語の使用を禁止したい、というものです。
- 記事番号の登録を強制するルール
- 副詞としての「始めて」の使用を禁止するルール
- 不適切な長音符の使用を禁止するルール
- 例えば「‐」(U+2010)。
- 記事番号によるサイト内ページの参照を強制するルール
- 並列表記「または」の直前に「、」を強制するルール
この辺りは、私は方針決めのみ行い、コーディングはほぼ全て生成 AI に書いてもらっていた記憶があります。特に「並列表記「または」の直前に「、」を強制するルール」は筋良く実装するのが難しそうだったので潔く諦めて、エッジケースが見つかるたびにテストを追加し、テストが通過するよう生成 AI にほぼ全て書き直してもらう展開が度々ありました。
▶︎ ひとくち振り返り
すべて textlint に寄せられたので、かなり保守・運用が楽になりました。
感想ですが、仕様(あるいはテスト)が変わるたびに、生成 AI がプログラムを一から作り直す世界が来るかもと感じていたので、その一端を垣間見られて面白かったです。特に「並列表記「または」の直前に「、」を強制するルール」に関しては、テクニカルライターがテストを追加したら生成 AI がプログラムを書き換えてくれると、テクニカルライター主導でルールを育てていけて嬉しそうだと思いを馳せています。(何らかのガードレールがないと怖い仕組みでもありますが。)
ヘルプサイトを GitHub Actions 上で PDF 化
お客様からの要望があり、旧ヘルプサイトではヘルプサイトの各ページを組版した PDF を生成していました。新ヘルプサイトで同様の PDF を生成する仕組み自体は @mugi_uno が作ったので、私はそれを GitHub Actions 上で動かせるようにしました。私が取り組んだことは少なく、@mugi_uno が用意した Dockerfile を GitHub Actions 上でビルドして利用しただけではあります。旧ヘルプサイトでは SSH ログインした VM 上で PDF 化していたので、新ヘルプサイトではブラウザ上で PDF 化を完結できる点が嬉しいと考えています。
▶︎ ひとくち振り返り
ヘルプサイトの PDF 化に関する刷新は @mugi_uno の偉業の一つ。
GitHub Actions の Playbook 作成
テクニカルライターが作成した pull request に対するジョブが落ちたとき、テクニカルライターはどのように対処すればよいか迷うと感じたため、対応方法をまとめた Playbook を作りました。仕組み自体は「GitHub Actions の Job が落ちたときに何をするべきかを記述する Playbook の仕組みを作って運用している話 - newmo 技術ブログ」をそのまま参考にしており、ジョブが落ちたら GitHub Summaries に Playbook(Markdown 形式のテキスト)を投稿しています。
▶︎ ひとくち振り返り
個人で判断できる量が多いほど高速に進めると考えているので、Playbook がテクニカルライターの役に立ってくれると嬉しいです。
新ヘルプサイトの品質保証
新旧ヘルプサイトで意図しない仕様の差分が存在していたら困るので調べました。各種プログラムのテストは書いていたので、ここではエンドユーザー目線で仕様がデグレードしていないか調査しました。
旧ヘルプサイトのページが新ヘルプサイトに全て存在することの確認
まずは旧ヘルプサイトのページが抜け落ちていないか確認しました。旧ヘルプサイトのサイトマップに存在するパスを対象に、新ヘルプサイトにて HEAD リクエストして最終的に 200 番が返ってきたら OK、途中でリダイレクトが発生しても OK、というプログラムで確認しました。
▶︎ ひとくち振り返り
これはコスパ良かったです。
新旧ヘルプサイトの見た目がほぼ同一であることの確認
次に見た目がほぼ同一であることを確認しました。完全一致は労力に見合わないと判断し、新旧ヘルプサイトの見た目の差分が閾値を下回ればよしとしました。
このアイディアで一番大事な点は二つの画像の差分の数値化ですが、これは reg-viz/reg-suit というツールで実現しました。差分の数値化だけでなく、差分が生じている箇所を画像で示してくれるのでとても便利でした。
▶︎ ひとくち振り返り
この取り組みはあまり効果的ではなかったです。意外と細かい差分があったので閾値を高めてしまい、重要な差分を見逃すことがよくありました。QA はテクニカルライターチームやローカライズチームに手伝ってもらっており、彼ら・彼女らのおかげでバグが見つかることの方が多かった印象です。差分検出の方法をより探究したかったですね。
新旧ヘルプサイトにおけるリダイレクト結果が同じであることの確認
節「Netlify のリダイレクトの仕組みの模倣」が期待通りに動作するか確認するため、新旧ヘルプサイトにおけるリダイレクト結果が同じであることを確認しました。
▶︎ ひとくち振り返り
これもコスパ良かったです。
マイグレーション戦略
ここまで長かったので、刷新プロジェクトのゴールをおさらいします。刷新プロジェクトのゴールは以下のとおりです。
- 「cybozu.com 共通管理」、「kintone」、「cybozu.com の購入」が
jp.kintone.helpから配信されている。 - 「cybozu.com 共通管理」、「kintone」、「cybozu.com の購入」に関する旧ヘルプサイトのリンクが生きている。
- リダイレクトを挟んでもよい。
- 旧ヘルプサイトの技術的負債が存在しない。
上記のゴールは JP リージョンにのみ言及していますが、US/CN リージョンも同様です。なお、旧ヘルプサイトは別ドメイン(e.g. jp.cybozu.help)から配信されていました。
先に述べた通り、新旧ヘルプサイトでドメインを分けられたので、マイグレーション戦略としては、刷新対象のヘルプサイトのパスについて、旧ヘルプサイトから新ヘルプサイトにリダイレクトするだけです。
▶︎ ひとくち振り返り
ドキドキ
そして事件は起こった
US リージョンのヘルプサイトを 1 時間ほどダウンさせてしまいました。JP/CN リージョンは無事でした。障害は早期にわかっていたのですが、旧ヘルプサイトにはビルド中に本番環境を参照する処理が存在しており、revert しようにも本番環境が落ちているのでビルドに失敗してしまい、色々と試行錯誤していたら時間が過ぎていました。最終的に旧ヘルプサイトのリダイレクトの設定を AWS コンソール上から暫定的に変更し復旧しました。不幸中の幸いだったのは、ダウンしていた時間はアメリカにおける日曜深夜だったことでしょうか。
事件の原因
障害は些細なミスが原因で、旧ヘルプサイトにのみ設定すべきリダイレクトルール、具体的には旧ヘルプサイトから新ヘルプサイトにリダイレクトするルールが誤って新ヘルプサイトに適用されていたことでした。新ヘルプサイトから新ヘルプサイトへのリダイレクトループが起こっていたわけです。
なぜこの事件が起きたかというと、まず前提として旧ヘルプサイトの _redirects ファイルを新ヘルプサイトに移行するツールを作っていたのですが、先述のルールを旧ヘルプサイトに設定した後にそのツールで新ヘルプサイトに移行してしまったからです。諸事情で旧ヘルプサイトに新しく設定されたルールを移行する目的だったのですが、移行後の設定から不要なルールを削除し忘れていました。
なお復旧した後、その日中に無事マイグレーションは完了しました。
反省会
色々と反省点はあるのですが、リダイレクトルールのサイクルを検出するツールを作っておくと良かったなと思います。誤ってサイクルを作ってしまうリスクは認知していて issue も積んでいたのですが、そのような設定を施すリスクは低いと考え優先度を下げてしまっていました。
リダイレクトルールの完璧なサイクル検出は無理だと示せてしまったので、現在はよくあるサイクルを検出するツールを作って運用しています。
▶︎ ひとくち振り返り
反省
今後の展望
色々あります。一つ目は検索に関するもので、社内から AI 検索機能の要望が上がっているので検討してみたいと考えています。検索に関するメトリクスを可視化してほしいとの要望もあります。二つ目は監視の拡充で、現在は最低限の監視しか用意できていないので、もう少しなんとかしたいです。三つ目はインフラにかかる料金の削減で、特に OpenSearch Serverless にかかる料金を削減したいです。最後に、textlint を導入するためにいくつかのルールを無効化したので、それらを段階的に有効化する宿題もあります。
まとめ
本稿では、ヘルプサイトの一部を刷新するプロジェクトの中で私が取り組んだ内容を紹介しました。紹介した取り組みとしては、AWS × Terragrunt によるインフラ再構築や、テクニカルライターの生産性向上を狙った仕組みづくり、新ヘルプサイトの品質保証、新旧ヘルプサイトのマイグレーションがありました。
- 「US 向け」と呼んでいますが、実態は「日本と中国以外向け」です。US 向けと呼んでいるのは昔の名残です。↩
- Amplify とは、ここではホスティングサービスと考えてもらえばよいです。(実際はより高機能です。)↩
- CloudFront とは、コンテンツデリバリネットワーク(CDN)です。↩
- Lambda 関数とは、サーバーレスコンピューティングサービスです。↩
- プロキシ方式(CDN パターン)による WOVN 導入は、特に SEO 対策の観点で嬉しいです。↩
- ブラウザ上の UI で言語を切り替えるときなど、動的に翻訳される場合もあります。↩
- 404 ページはリダイレクトせずに 404 番でリライトした方が良いのはそうなのですが、WOVN の仕組み的にリダイレクトしないと 404 ページを翻訳できないのでリダイレクトしています。WOVN は事前に登録した URL のみ翻訳しますが、リソースが存在しない任意の URL を事前に登録できないので、404 番でリライトしても翻訳されません。↩
-
エンドユーザーから最も近いリージョンの計算は簡単で、Lambda@Edge は HTTP ヘッダー
cloudfront-viewer-latitude/cloudfront-viewer-longitudeからエンドユーザーのおおよその緯度・経度を取得できるので、あとは何かしらのアルゴリズムで最も距離が短いリージョンを求めるだけです。二地点間の距離を求めるアルゴリズムはいくつか知られていますが、今回は npm パッケージ geographiclib-geodesic を使って楽しています。(ただし、まれに上記の方法で緯度・経度が取得できない場合があるので、その場合は特定のリージョンの_redirectsファイルを読んでいます。この影響で、有効期限が十分に短いローカルキャッシュも存在します。)↩ - ビルド成果物を利用しないのは、WOVN で翻訳する多言語ページをインデクシングするためです。↩
- Amplify ではアプリケーションごとにリダイレクトルールを設定する必要があります。言い換えると、プレビューサイトごとにリダイレクトルールを設定できません。なので、期待通りにリダイレクトルールが設定されていると確認する際、旧ヘルプサイトでは新しくアプリケーションを作って対応していました。↩
-
新ヘルプサイトのドメインは
{jp,us,cn}.kintone.helpですが、これは最初から決まっていたわけではなく刷新プロジェクトを進める中で決まりました。なので、刷新プロジェクトの初期は Route 53 のホストゾーンを誰が作るのかわからなかったわけです。↩