TerraformでAWS上にFessを起動する(Qiita検索サイトを作ってみた)

# はじめに

[Fess](https://fess.codelibs.org/ja/)というOSSの全文検索サーバーがあります。

個人的には約５年前に初めて使って、職場で担当製品のマニュアル検索サイトを作成し、社内のサーバで稼働させていました。
社内には、マニュアル検索するサイトは既にあったのですが、Fessを導入して自作したものの方が使いやすかったので、周りのメンバーからも喜ばれました。
また、私が使い始めた頃はまだ誰も周りに使っている人はいませんでしたが、徐々に周囲にも使う人が増えたり、社内の他のシステムにも採用されているのを見かけるようになりました。

Fess自体が5分で簡単に構築可能とされているだけあって、導入はそれほど難しくありません。
AWS等のクラウド上に導入すればもちろんWebサイトとして公開もできます。
本記事では、Terraformを使用してAWS上にFessを使用した全文検索サイトを構築する手順をコードサンプルを交えながら解説したいと思います。

# 完成品のサンプル

Fessを使用し、Qiitaの記事を検索するサイトをAWS上に作成したので使ってみてください。

- Qiita Search  
https://qiita-search.com (現在停止中)

![2021-06-07-07-43-55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80955/25628c8f-c02c-ffee-5be9-98fd26906a0a.png)

キーワードで過去の人気記事を検索できます。
直近の人気記事(ストック数10以上)と過去5年間くらいの人気記事(ストック数100以上)を検索対象としています。
後述するように検索対象記事は毎日自動で更新しています。
Qiitaの標準検索では見つかりにくい良記事に出会えるかもしれません。

なお、検索結果画面の`期間`は、Qiita Search側に記事を登録した日なので、現状数年前の記事でも1ヶ月以内のフィルタに入ったりしています。(今後しばらく運用すると変わってくるはずですが)
`サイズ`はおよそ記事の文字数に対応しているはずなので、`サクッと読める人気記事`や`気合いの入った読み応えのある記事`が見つかるかもしれません。

![2021-09-17-09-53-40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80955/37fcf3a7-dd41-f250-8463-e18711deb20d.png)

また、`検索オプション`の`ソート`で`日付(降順)`で検索すると、だいたいQiitaへの登録日が新しい順に記事が表示されるはずです。

![2021-09-16-11-28-30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80955/0cc28a92-1fe4-054a-5994-4f2af3963683.png)

# ローカル環境でFessを起動する方法

Fessについて理解するため、まずローカル環境で起動する方法を紹介します。
環境は、Mac(macOS Big Sur 11.5)です。

Docker(Docker Desktop等)をインストール済みであれば、以下だけで起動できます。

```sh
git clone https://github.com/codelibs/docker-fess.git
cd docker-fess/compose
docker compose -f docker-compose.yml -f docker-compose.standalone.yml up -d
```

最近のFessはDocker単体ではなくDocker composeで起動するのが標準的手順となったようです。(おそらくElasticsearchがFessのコンテナから分離されたため)
注意点として、Docker Desktopの`Resources`の設定でメモリを4GB程度にしておくと安定して起動できると思います。(デフォルトの2GBだとElasticsearchのコンテナが停止してしまったので）

`http://localhost:8080`にアクセスすることでFessのトップページを表示できます。
ログインをクリックしてadmin/adminで認証し、Fessの管理画面にアクセスできます。

![2021-07-26-23-33-28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80955/feb8794f-66b2-1cb1-11df-c7de029d661f.png)

![2021-07-26-23-34-57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80955/c80840ad-6202-9390-42ea-14c787f95a3c.png)



## 簡単にクロール設定してみる

検索対象の記事を登録するため、クロール設定をします。
Fessの管理画面からウェブのクロール設定で、たとえばQiita記事のURLを検索対象とする場合は、以下のように設定します。
`深さ`はURLに設定した記事内のリンクをたどる深さですが、URLに設定した記事のみをクロール対象にしたい場合は、深さを0に設定します。
`間隔`はクロールの時間間隔ですが、デフォルトは10秒と少し遅いので1秒(1000ミリ秒)にしています。
その他はデフォルト値のままです。

![2021-07-26-22-24-42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80955/c24294f3-a8a5-fc27-259d-eaa0d7c53abc.png)

## 実際にクロールして動作確認する

スケジューラからクローラを起動します。
`Default Crawler`で`今すぐ開始`をクリックするとクロールが始まります。

![2021-07-26-13-03-29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80955/79edc66c-0918-0202-b5cf-ec584b489695.png)

しばらくするとクローラが停止します。
検索してみると検索結果が表示されることがわかると思います。

![2021-07-26-22-35-32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80955/f0e44162-846c-3540-d828-1041a0254e1f.png)


## 画面を変更する方法

ロゴなどもFessの管理画面から変更できます。
まず、`ファイルマネージャー`からファイルをダウンロードするなどして変更箇所のファイル名を特定します。
`アップロードするファイル`で特定したファイル名を指定し、手元のファイルを選択してアップロードすることで変更できます。
なお、JavaScriptやCSSも変更できるため、Qiita Searchでは利用規約等のダイアログを表示するように修正も加えています。(FessにはjQueryも同梱されているのでjQueryのAPI使用可)

![2021-07-26-22-43-29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80955/a00b45d2-c4f0-5931-19b8-566bd4e4b761.png)

![2021-07-26-22-52-02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80955/edecca59-f86d-adb3-b8d3-4e4b1ac17229.png)

## イメージ化してECRに登録する

AWS上でもFessの設定は可能ですが、ローカルで試行錯誤して設定したものをAWS上にそのまま持ち出せたら便利です。
画面を編集したものをDockerイメージ化すると、そのイメージをAWSのコンテナレジストリであるECRに登録することで、AWS上ではその編集後のイメージから起動できるようになります。
たとえば、以下のようにDockerイメージを作成してECRに登録します。(AWS上で事前にECRのリポジトリを作成する必要あり。以下は私のリポジトリを使用した例。)

```sh
# 編集後のFessのコンテナ(ID:08dd4aa61fcb)をイメージ化
docker commit 08dd4aa61fcb inayuky/fess:13.12.1

# AWS CLIを使用し、ECRに対してDockerクライアントを認証
aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws/c4k4h8g1

# 作成したイメージにECRのURLをタグ付けする
docker tag inayuky/fess:13.12.1 public.ecr.aws/c4k4h8g1/inayuky/fess:13.12.1

# タグ付けしたイメージをECRに登録
docker push public.ecr.aws/c4k4h8g1/inayuky/fess:13.12.1
```

なお、動作確認した限り、Fessのシステム全般の設定やページのデザインはFessコンテナ内に保持されるため上記の方法で持ち出せますが、クロールの設定やクロールされたデータは別の場所(おそらくDockerボリューム内)に保存されるため、それらも持ち出したい場合は別途手順が必要になります。

# AWS上でFessを起動する方法

こちらもDockerを使う方が簡単に構築できたので、その方法を紹介します。
EC2(AmazonLinux2)を起動後、以下を管理者権限でそのまま実行するとFessを起動できます。
EC2のスペックは`t2.medium`くらいあった方が安心です。
`http://EC2のPublicIP:8080`でアクセスでき、前述のようにローカル環境で実施したクロール設定等ができます。

```sh
# install docker
amazon-linux-extras install -y docker
systemctl enable docker
systemctl start docker

# install docker-compose
curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose

# change parameter for Elasticsearch
echo "vm.max_map_count=262144" >> /etc/sysctl.conf
sysctl -q -w vm.max_map_count=262144

# start fess
yum install -y git
git clone https://github.com/codelibs/docker-fess.git
cd docker-fess/compose
docker-compose -f docker-compose.yml -f docker-compose.standalone.yml up -d
```

なお、前述のECRに登録されたイメージから起動する場合は、以下のように`docker-compose.yml`内の`image`でECRのURLを指定してから`docker-compose`を実行します。

```yml
version: "3"
services:
  fess01:
    image: public.ecr.aws/c4k4h8g1/inayuky/fess:13.12.1
    container_name: fess01
  .
  .
  .
```

# TerraformからAWS上でFessを起動するサンプル

Terraformを使用することで、AWSリソースをコード化して簡単に環境を作成または削除できます。
今回のようなシンプルな構成を単に作成するだけなら手動構築の方が早かったりしますが、一度構築できると別の目的で少し構成を変更して使いたいとき等に使い回せて便利ですね。

書籍[実践Terraform](https://www.amazon.co.jp/dp/B07XT7LJLC/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1)を参考にしつつサンプルコードを作成し、[GitHub](https://github.com/inayuky/terraform-fess-sample)に公開しました。

使い方はREADMEに記載した通りですが、TerraformとAWS CLIをセットアップできていれば、作成/削除が簡単にできるはずです。
EC2起動時に前述のFessをインストールするスクリプトを実行しているだけで、それ以外は特別なことはしていないため、Terraform入門用のサンプルコードとしても使えるかもしれません。

# Qiita Searchの構成図

以下の通り、シンプルな構成にしています。

![2021-08-14-22-13-37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80955/53781bc0-75d3-290f-55e4-79bfcddfc512.png)


以下、構成についての補足です。

- Route53でドメイン(qiita-search.com)を取得してACMでSSLの証明書を作成しました。
- EC2は1台(AZ間で冗長化しない）だが、証明書関連設定の簡易さからALBは採用することにしました。
- EC2内のFessからQiitaの人気記事をクロールするため、S3にQiitaの記事のURLを格納してEC2から取得しています。(Qiita人気記事の抽出方法は後述)
- CloudFrontとS3を使用することでSorryページ(サービス停止時に表示されるページ)を作成しました。[こちらの記事](https://ceblog.mediba.jp/post/140328272267/aws-の-dns-フェイルオーバーでサーバレス高可用性-sorry-ページを構築する)を参考にしました。(ALBにも静的ページを表示する機能はあるが、ALB削除時にもSorryページを表示したかったのでこの構成にした)
- ALBがPublicサブネットにあるため、EC2はPrivateサブネットに配置したかったが、Qiita記事をクロールするにはEC2からインターネットアクセスが必要になり、そのためだけにNATゲートウェイ等を作成するのは費用面からして避けたかったので、Publicサブネットに配置することにしました。ただ、セキュリティグループの設定でEC2のアクセス元をALBに限定することで、インスタンスのPublic IPには直接アクセスできないようにしました。(これにより、ALB経由以外からのパブリックアクセスは防ぎながらも、NATゲートウェイやPrivateLinkなしでSSMやCloudWatchAgentも使えるようになり、シンプルで比較的安価な構成にすることが可能なので、あまり一般的ではないと思うが小規模サービスなら意外とアリな構成ではないかと感じました)

# Qiita SearchのTerraformサンプル

[GitHub](https://github.com/inayuky/terraform-qiita-search)に実際のサービス構築に使用しているコードを公開しているため、参考にしてください。

以下、ポイントについてまとめておきます。

- SSL証明書は手動作成しているので、Terraformで作成/削除をしても証明書の再発行はされない。(Terraformによる証明書の再発行自体は可能だが上限回数があるためTerraformとは分離するのがベターと感じた)
- `terraform destroy -target=module.alb.aws_lb.this`を実行することでALB周辺のリソースだけ削除できるし、再度applyすればいつでもサービス復旧できる。ALBは常時起動が原則のコンポーネントという認識だが、今回のように試しに作ってみた程度のサービスとしては料金が高め(約2000円/月)になるので、不要なときは削除が気軽にできるといい。ALBを使用していればEIPの費用は不要になるので、あとはインスタンス停止さえすれば、いつでも1コマンドでサービス再開できる状態を、限りなく費用を抑えた状態で維持できる。これだけでもTerraformによって管理する意義を感じた。(なお、Terraformの`-target`オプションは[日常的な運用での使用は推奨されていない](https://www.terraform.io/docs/cli/commands/plan.html#resource-targeting)ので扱いには注意が必要)
- CloudFrontとS3を使用することでSorryページを作成しているが(手動作成)、Route53にはファイルオーバーのルーティングポリシーをもったレコードを作成し、セカンダリのレコード(手動作成)からCloudFrontのURLを指定することで転送している。前述のTerraformによるALB削除/作成コマンドと連動してプライマリのレコード方は削除/作成されることになり、Sorryページへの切り替えと復旧が自動化される。
- SSMのポリシーはsshを使わずにEC2へコンソール接続するためにアタッチしている。
- CloudWatchAgentのポリシーはEC2からメモリやディスク容量のメトリクスを取得するためにアタッチしている。(別途CloudWatchAgentをEC2にインストールする必要はある)

# Qiitaの人気記事のURLを取得して自動更新する方法

クロール対象のURLはFessの管理画面から入力できるのですが、入力文字数には制限があります。
sitemap形式でURLを記載して、そのsitemap自体のURLをクロール設定で指定することで、文字数制限以上のURLをクロールできます。
また、sitemap形式にすれば、sitemapを更新するスクリプトを定期実行することで、Fessの設定を変更しなくてもクロール対象のURLを自動更新することもできます。

Qiitaの人気記事については、QiitaAPIを使用すればストック数が一定以上の記事のURL一覧を取得できます。
今回は簡単な[Rubyスクリプト](https://github.com/inayuky/terraform-qiita-search/tree/main/scripts)を作成して、[CircleCIから定期実行](https://github.com/inayuky/terraform-qiita-search/blob/main/.circleci/config.yml)して人気記事のURL一覧を記載したsitemapを更新することにしました。
なお、Rubyには[Qiita公式のQiitaAPIクライアント](https://github.com/increments/qiita-rb)が存在するので、せっかくなので使ってみました。(しばらく更新されていないようですが現在でも使えました)

以下が、Fessのクロール設定でsitemapを指定する例です。
sitemapはS3に配置しているので、そのURLを指定しています。
動作確認したところ、sitemapを指定する場合、クロール設定での深さは1以上にする必要があるようです。

![2021-08-15-21-23-17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/80955/f3d1069b-d5af-c2d1-1a25-c8f08c10796f.png)

# まとめ

- ローカル環境とAWS環境について、簡単にFessを起動して使用する方法をまとめた。
- Terraformを活用してAWS上にFessを使用した全文検索サイトを構築できた。今回は個人的に思い入れのあるFessのイメージを使用して構築したが、Dockerイメージで提供されている似たタイプのOSS(CMSとか？)であれば、今回まとめた手順は応用できるだろう。
- 今回のようなシンプルな構成では費用面を考慮するとALBの選択は避けたかったが、Terraformのおかげで試しにALBを選択して作成してみる気になった。Terraformで管理すること自体がAWS側の構成を決定する要因になり得ると感じた。
- sitemap形式でQiita記事のURLを記載してS3にアップロードするスクリプトを作成することで、Fessから自動的にクロール対象を更新することができるようになった。