インフラ管理不要なコンテナ環境のAWS FargateでKeycloakを動かしてみる

[Keycloak by OpenStandia](https://qiita.com/advent-calendar/2017/keycloak-by-openstandia)の21日目は、Keycloakをコンテナ環境で動作させてみようと思う。ただ、単体のDockerコンテナとして動作させるのは非常に簡単で、[Docker で始める Keycloak](https://qiita.com/tadayosi/items/7ad82652809b00abff59)などの記事で既に紹介されている。そこで今回は、

* **先月行われた [AWS re:Invent 2017](https://reinvent.awsevents.com/) にて発表された [AWS Fargate](https://aws.amazon.com/jp/blogs/news/aws-fargate/) 上で動かす**
* **さらに、Keycloak はクラスター構成 (HA構成)とする**
* **ついでにCloudFormationでAWS環境の構築を自動化しちゃう**

をやってみました！

# AWS Fargateとは?

最初にAWS Fargateについて簡単に紹介しておく。FargateはEC2インスタンス管理が不要なAWSの新しいコンテナ実行環境サービスである。従来、AWSではコンテナ環境として[Amazon ECS(Elastic Container Service)](https://aws.amazon.com/jp/ecs/)を提供してた。しかし、ECSの実行基盤としてEC2インスタンスが必要であり、インスタンスのプロビジョニングやその運用維持は利用者自身で行う必要があった(**なので完全なフルマネージドなサービスではなかった** :cry: )。一方、Fargateでは利用者は下回りとなるEC2インスタンスを管理する必要が完全になくなる。コンテナを用意してデプロイするだけでよくなり、利用者がEC2インスタンスのプロビジョニングや設定などが不要となる。構築・運用範囲がコンテナだけとシンプルになるので、うまく活用すればコストをさらにおさえることができると思う。

AWS Fargateそのものについては、[AWS Fargate Advent Calendar 2017](https://qiita.com/advent-calendar/2017/aws-fargate)がちょうどあり、リリースされたばかりだというのに既に多くの記事が書かれているので、興味を持たれた方はそちらの記事も読むと理解が深まると思う。


# 前提環境

* Keycloak 3.4.1.Final
* [オフィシャルのKeycloak Dockerイメージ](https://hub.docker.com/r/jboss/keycloak/)をベースイメージとして利用
* AWSはバージニア北部(us-east-1)リージョンを利用 (AWS Fargateが2017/12/21時点で唯一使えるリージョンのため)

# やったこと

ざっくりまとめると以下のような感じ。

1. KeycloakをAWS向けにDockerコンテナ化する
    1. AWS向けにクラスター構成設定の変更
    2. HTTPS終端となるロードバランサ (今回はALBを利用) をフロントに配置するための設定追加
    3. コンテナビルド
    4. DockerコンテナのリポジトリをECRで作成
    5. コンテナのpush
2. AWS Fargate用のAWS環境を構築する
    1. VPC、ELB、RDS(MySQL)、ECSクラスターの作成とFargateによるコンテナデプロイ(ECSタスク定義、サービス定義)を行うCloudFormationテンプレートの作成
    2. 踏台SSHサーバ (Bastion Host) ログイン用のキーペアの登録
    3. HTTPS用の自己証明書の作成[^1]
    4. AWS Certificate Manager (ACM) に証明書を登録
    5. CloudFormationを実行
    6. 踏台SSHからRDSに接続し、Keycloak用データベースを作成

[^1]: 認証局のものが使えるならもちろん不要。今回は検証用なのでオレオレ証明書でやった。

以下、詳細を書いていく。なお、作ったもの (スクリプト、Dockerfile、CloudFormationテンプレート) は [github.com/wadahiro/keycloak-ecs-fargate](https://github.com/wadahiro/keycloak-ecs-fargate)に置いておいた。

# KeycloakをAWS向けにDockerコンテナ化する

オフィシャルのDockerイメージでは、以下の機能には対応している。

* 共有データベースを組み込みDB(H2 Database)からMySQL, PostgreSQLに変更
* UDPマルチキャストを利用したHA構成
* フロントにロードバランサーを配置するための設定(環境変数`PROXY_ADDRESS_FORWARDING`を`true`に設定する)

しかし、AWSではVPC内ではマルチキャストは使えない仕様のため、クラスター構成の設定変更が必要となる。他にも設定変更が必要なため、オフィシャルのイメージをベースとし、追加設定を行う[Dockerfile](https://github.com/wadahiro/keycloak-ecs-fargate/blob/master/Dockerfile)を書き、AWS環境用にイメージをビルドするようにした。


## AWS向けにクラスター構成設定の変更

Keycloak(というか下回りのWildFly)では、[JGroups](http://www.jgroups.org/)を使って各ノードのディスカバリを行う仕組みになっている。組み込まれているデフォルト設定ではUDPを使うようになっているため、AWSでも利用可能な方式に変更する。例えば、[TCPでJDBC_PING](http://www.jgroups.org/manual/html/protlist.html#d0e5196)を利用する方式がある。今回はこれを利用している。

この設定については、[Keycloakを冗長構成で動かしてみる > AWS EC2でのKeycloak冗長構成](https://qiita.com/yoonis/items/4f4a9df0f6f8e858bd4a#aws-ec2%E3%81%A7%E3%81%AEkeycloak%E5%86%97%E9%95%B7%E6%A7%8B%E6%88%90)で既に書かれている話だが、Fargate環境・Dockerコンテナ環境における注意点があるのでそこだけ補足しておく。

### Fargate環境の注意点
Fargateのコンテナ内からはEC2インスタンスメタデータは参照できないため、自分のIPアドレス取得は別の方法で行う必要がある。今回はOSの`ip`コマンドで取得した。実際のコマンドは[起動シェルのここを参照](https://github.com/wadahiro/keycloak-ecs-fargate/blob/f2bbfea717875ac88f9eba41b05b5f8f1748ee8f/docker-entrypoint-aws.sh#L7)。

### Dockerコンテナ環境の注意点
JGroupsのバインドアドレスに他ノードから到達可能なIPアドレスを設定する必要がある。なぜかというと、JGroupsは稼働サーバのデフォルトのネットワークインタフェースにバインドする仕様のため、Dockerコンテナ内で起動した場合、Dockerのネットワーク設定によってはプライベートなIPにバインドしてしまう。そうするとそのIPがクラスター内で伝わっても到達不可能なため、クラスターが機能しなくなる。そうならないように、明示的にIPの設定が必要となる (参考: [JBoss Cache > Key JGroups Configuration Tips](https://docs.jboss.org/jbossclustering/cluster_guide/5.1/html/jgroups.chapt.html#d0e5284))。

設定は簡単で、Keycloakの起動スクリプトの引数で、`-Djgroups.bind_addr=IPアドレス`を渡せば良い。IPアドレスは前述の`ip`コマンドで取得したものを使う。


## HTTPS終端となるロードバランサをフロントに配置するための設定追加
Keycloakのマニュアルに記載の設定追加が必要になる。詳細は、[Server Installation > 8.3.2. リバースプロキシでのHTTPS/SSLの有効化](http://keycloak-documentation.openstandia.jp/master/ja_JP/server_installation/index.html#リバースプロキシでのhttps-sslの有効化)を参照。

## コンテナビルド
ここまでの[設定変更内容を含めたDockerfileがこれ](https://github.com/wadahiro/keycloak-ecs-fargate/blob/master/Dockerfile)。あとは普通に`docker build`でビルドすればOK。なお、今回はKeycloak(WildFly部分)の設定変更は、オフィシャルのイメージで採用されている`jboss-cli.sh`を使ったコマンドでの設定変更を試しに使ってみている。これを使うと、XMLファイルを直接編集せずにコマンドで設定変更が可能となっている。例えば、JGroupsの設定変更は[`cli/jgroups.cli`ファイルにコマンドとして書いている](https://github.com/wadahiro/keycloak-ecs-fargate/blob/master/cli/jgroups.cli)。

## DockerコンテナのリポジトリをECRで作成
[AWS管理コンソール](https://console.aws.amazon.com/ecs/home?region=us-east-1#/repositories)からECRのリポジトリを先に作成しておく[^2]。リポジトリ名は `keycloak-ha-mysql` とする。

[^2]: CloudFormationで合わせて作成できるが、検証終了時にCloudFormationのスタック削除でエラーになるので手動で先に作成しておく。検証終了後、手動でpushしたイメージとリポジトリの破棄を忘れないように :warning: 

## コンテナのpush
作成したECRのリポジトリに、ビルドしたコンテナイメージを`docker push`しておく。これでコンテナの準備は完了である。後は実行環境を用意するだけ。

なお、ECRへのpush方法は[作成したリポジトリのページ](https://console.aws.amazon.com/ecs/home?region=us-east-1#/repositories/keycloak-ha-mysql#images;tagStatus=ALL)からコマンド参照することができる。

# AWS Fargate用のAWS環境を構築する
AWS環境の構築は管理コンソールでGUIでできるが、CloudFormationのテンプレートで書いてさくっと作ってさくっと消せるようにしておく。

## CloudFormationテンプレートの作成

下図の構成とした。AZ障害を考慮しつつ、Public/Privateサブネット構成のよくある構成。ただし、NATゲートウェイは置いていない。Fargateの場合、DockerコンテナのpullやCloudWatch logsへのpushにインターネットアクセスが必要となる。Privateサブネットにコンテナを配置する場合は、NATゲートウェイを経由してインターネットに出るのが一般的な方式である(参考: [Deep Dive into AWS Fargate - CON333 - re:Invent 2017 の16ページ目](https://www.slideshare.net/AmazonWebServices/deep-dive-into-aws-fargate-con333-reinvent-2017))。ただし、Fargateの場合はENIでPublicアドレスも付与することができるので、そこからインターネットに出れるようにしている(NATゲートウェイ2台置くとそこそこお金かかるので...)。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/35233/0e3b63bd-417f-eda7-c24c-0076db5cb29f.png)

今回、1つのYAMLファイルにVPCの作成からELB、RDS、ECSクラスターの作成とFargateによるKeycloakコンテナのデプロイ定義まで書いている。CloudFormationテンプレートの詳細内容については、これだけで1つの記事がかけるので割愛。[こちらでソース公開](https://github.com/wadahiro/keycloak-ecs-fargate/blob/master/environment.yml)しているので見ていただければと思う。また、FargateをCloudFormationで構築する [AWS CloudFormationを使ってAWS Fargateの環境を作成してみる](https://qiita.com/isleys/items/eaebbffb24be2c7cbcd3) という記事が既にあるので、そちらを参照するとよいと思う。

1点、Keycloakならではの補足事項として、クラスター構成の場合はJGroupが`7600`ポートを使ってノード間で通信を行うことになる。この場合、コンテナとしてそのポートが外部公開されている必要がある。よって、WebのHTTP用ポート`8080`だけでなく、`7600`についても[ECSのタスク定義でポートマッピング設定](https://github.com/wadahiro/keycloak-ecs-fargate/blob/f4ae53f9724d0590d4e80667b091884add255704/environment.yml#L426-L428)しておく必要がある。加えて、セキュリティグループをコンテナにアタッチされるENIに設定する場合は、[同セキュリティグループ内で`7600`通信を許可するための設定](https://github.com/wadahiro/keycloak-ecs-fargate/blob/f4ae53f9724d0590d4e80667b091884add255704/environment.yml#L203-L210)も必要なので注意。


## 踏台SSHサーバ (Bastion Host) ログイン用のキーペアの登録
RDS(MySQL)にログインしてKeycloak用の初期設定が必要なため、踏台SSHサーバをCloudFormationにて合わせて構築するようにしている。が、SSHログインするためのキーペアの作成は、CloudFormation実行前に行っておく必要がある。[AWS管理コンソールのキーペア](https://console.aws.amazon.com/ec2/v2/home?region=us-east-1#KeyPairs:)から作成しておく。

## HTTPS用の自己証明書の作成

`openssl`でサクッと作成。有効期間はお好みで。

```sh
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr
openssl x509 -in server.csr -days 365 -req -signkey server.key -out server.crt
```

## AWS Certificate Manager (ACM) に証明書を登録

ELBで使えるように、[AWS管理コンソールのACM](https://console.aws.amazon.com/acm/home?region=us-east-1#/)からインポートしておく。

* **証明書本文\*** には `server.crt` の内容を貼り付け
* **証明書のプライベートキー\*** には `server.key` の内容を貼り付け
* **証明書チェーン** は空のまま

でインポートすればOK。インポート後、登録された証明書の **識別子** を控えておくこと。次のCloudFormationの実行時に渡すパラメータとして使用する。

## CloudFormationを実行

事前に、[environment.yml](https://raw.githubusercontent.com/wadahiro/keycloak-ecs-fargate/master/environment.yml)をローカルにダウンロードしておく。あとは[AWS管理コンソールのCloudFormation](https://console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks?filter=active)を開き、スタックの作成を行い一気に環境を作る。手順としては以下。

1. **テンプレートを Amazon S3 にアップロード** で ダウンロードしたファイルを選択して次へ進む。
1. スタックの名前には任意の名前を入力。パラメータは下記の通りに入力して次へ進む。
    * **BastionHostKeyPair**: 事前に登録しておいた踏台SSH用のキーペアを選択する
    * **CertificateIdentifier**: 事前に登録しておいた証明書の**識別子**を入力する
    * **VpcCIDRPrefix**: 既存のVPCと被らなければデフォルトでOK。被る場合は変更する。
1. オプション画面はなにもせずそのまま次へ。
1. **AWS CloudFormation によってカスタム名のついた IAM リソースが作成される場合があることを承認します。** にチェックを付けて、作成を開始する。

RDSのプロビジョニングにそこそこ時間がかかるのでその間に:coffee: でも飲んで待ちましょう。

無事に終わると、CloudFormationの**出力**で、ELBのDNS名と踏台SSHのIPアドレスを参照できるので、これを控えておくこと。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/35233/9bdff28d-e66a-c5ab-c57e-3da2a5364735.png)


## 踏台SSHからRDSに接続し、Keycloak用データベースを作成

CloudFormationの実行が終わるとKeycloakコンテナが立ち上がるが、CloudWatch logsをみるとたくさんのエラーが出ているかと思う。これは、Keycloakで外部の共有データベースを使う場合は初期構築設定が必要なのだが、まだ何も行っていないのでDB接続でエラーとなってしまう。そこで、踏台SSHサーバにSSH接続を行い、RDS(MySQL)にログインして初期設定を行う。

```sh
$ ssh -i <作成したキーペアの秘密鍵> <控えておいた踏台SSHサーバのIP>
Last login: Wed Dec 20 06:20:17 2017 from xxx.xxx.xxx.xxx

       __|  __|_  )
       _|  (     /   Amazon Linux AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-ami/2017.09-release-notes/
No packages needed for security; 1 packages available
Run "sudo yum update" to apply all updates.
[ec2-user@ip-10-181-9-2 ~]$
```

今回、CloudFormationでRDSにはRoute53で`db.keycloak.local`をCNAMEで設定しているので、

```sh
[ec2-user@ip-10-181-9-2 ~]$ mysql -u root -prootpass -h db.keycloak.local
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 780
Server version: 5.7.16-log MySQL Community Server (GPL)

Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql>
```

のようにしてログインできる。後は、

```
mysql> CREATE DATABASE keycloak;
Query OK, 1 row affected (0.00 sec)

mysql> CREATE USER 'keycloak'@'%' IDENTIFIED BY 'keycloak';
Query OK, 0 rows affected (0.00 sec)

mysql> GRANT ALL ON `keycloak`.* TO 'keycloak'@'%';
Query OK, 0 rows affected (0.00 sec)

mysql> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.00 sec)
```

でKeycloak用のデータベースと接続ユーザを作成すれば完了。DB設定後、そのうちKeycloakコンテナが正常稼働状態になるはず(正常稼働状態になるまで、ELBのヘルスチェックエラーで落とされ自動再起動を繰り返す)。


# 動作確認
今回、SSO保護アプリは特にデプロイしていないので、Keycloakに付属するログインユーザのプロフィール画面を利用して動作確認を行う。

1. 作成されたELBのDNS名に対してHTTPSで`/auth/realms/master/account/`にアクセスし、ログイン画面を表示
2. `admin` でログイン(パスワードも同じ)
3. プロフィール画面が表示される
4. AWS管理コンソールにて、ECSの実行中タスクを1つ強制ストップさせる
5. 再ログインなしでプロフィール画面内を遷移できることを確認
6. ECSにより自動的に再度コンテナが起動しきるのを待つ(CloudWatch logsでチェック)
7. 最初からあったもう1つの実行中タスクを強制ストップさせる
8. 再ログインなしでプロフィール画面内を遷移できることを確認

上記のように、コンテナを片方ずつ落としても再ログインなしでプロフィール画面にアクセスし続けることができれば成功 :clap:

# AWS FargateはKeycloakと相性が良い？

今回試してみて、コンテナ単位にENIがアタッチされるため、ホストポートの競合が起きないのが良いと感じた。クラスター構成の場合、ホスト側のポート`7600`ポートにマッピングする必要があるため、従来のECSだと1台のEC2インスタンスに複数のKeycloakコンテナを同居させることは難しかった。この問題を回避するために、[ECSのタスク配置の制約事項](http://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task-placement-constraints.html)を設定して同一インスタンス上で動作しないようにする、ワークアラウンド的な方法を取っていたかと思う。

なお、このコンテナ単位にENIがアタッチされるのは、[2017/11にリリースされた **コンテナ用の AWSVPC ネットワーキングモード**](https://aws.amazon.com/jp/about-aws/whats-new/2017/11/amazon-ecs-introduces-awsvpc-networking-mode-for-containers-to-support-full-networking-capabilities/)によるものなので、Fargateを使わない従来のECSでも実はホストポートを意識しない構成をいつの間にかとれるようになっているようだ。

# おわりに

ざっと、AWS Fargate上でKeycloakをHA構成で稼働させるためにやったことを書いたが、細かい設定については[github.com/wadahiro/keycloak-ecs-fargate](https://github.com/wadahiro/keycloak-ecs-fargate)のソースを参照してもらえればと思う。今後追加で調べてみたいこととしては、

* Fargateでオートスケール。残念ながらまだ試していない。
* Amazon Elastic Container Service for Kubernetes (Amazon EKS)上でも試す (プレビュー招待こないかなぁ...)

あたりでしょうか。それでは！ :wave_tone2: