Elixir で EC2 インスタンスロールを使って Elasticsearch Service に接続する

## はじめに

Elixir 、 Phoenix で EC2 上に API を構築しているとき、 Elasticsearch Service に接続する必要が出てきました

アクセス管理には EC2 インスタンスロールを使っており、今更それ以外に認証キーなどを発行したくありません

が、先達も書いていた通り、一筋縄では接続できないようです、、、

https://qiita.com/poly_soft/items/8a82fc3cc83fdd5131f2

という訳で、自分なりの方法で接続してみました

## 何故接続できないのか

当然、誰でも接続できては悪いので、アクセスするためには認証情報が必要になります

通常、以下のような AWS の認証情報を持っていれば AWS に API 経由で簡単にアクセスできます

- AWS_ACCESS_KEY_ID
- AWS_SECRET_ACCESS_KEY

Elasticsearch Service の場合、 AWS の API にアクセスするのではなく、 Elasticsearch のエンドポイントにアクセスすることになります

単純に AWS の認証情報を Elasticsearch に送ったところで、 API が別物なので認証してくれません

Elasticsearch Service で認証するためには、 AWS の認証情報を使って署名付きのリクエストを出す必要があるのです

公式に以下の言語の実装例が出ています

- Java
- Python
- Ruby
- Node
- Go

https://docs.aws.amazon.com/ja_jp/elasticsearch-service/latest/developerguide/es-request-signing.html

Python は Boto3 が上手くやってくれるので簡単に実装できます

まあ、 Elixir はないですよね

しかし、 Elixir 用の Elasticsearch クライアントにはちゃんと AWS 接続用の機能があり、これで署名までしてくれます！

https://github.com/danielberkompas/elasticsearch-elixir

自分の開発用マシンからは、自分用の IAM ユーザーの認証情報で簡単に署名・アクセスできましたが、、、

なぜか EC2 からだとアクセスできない、、、なぜ？

Elixir 用の AWS クライアント ExAws の署名している箇所を見てみましょう

https://github.com/ex-aws/ex_aws/blob/master/lib/ex_aws/auth.ex

```elixir
    [
      {"X-Amz-Algorithm", "AWS4-HMAC-SHA256"},
      {"X-Amz-Credential", Credentials.generate_credential_v4(service, config, datetime)},
      {"X-Amz-Date", amz_date(datetime)},
      {"X-Amz-Expires", expires},
      {"X-Amz-SignedHeaders", signed_headers_value(signed_headers)}
    ] ++
      if config[:security_token] do
        [{"X-Amz-Security-Token", config[:security_token]}]
      else
        []
      end
```

ああ、 `security_token` があれば、ヘッダの `"X-Amz-Security-Token"` に追加するようになっています

インスタンスロールの場合、署名だけでなく、 `security_token` も付けないといけないのです

Elixir 用の Elasticsearch クライアントの README には、以下のように設定すれば良い、と書かれていますが、これでは `security_token` がヘッダに付与されないため、認証できないのです

```elixir
# config/prod.exs
config :my_app, MyApp.ElasticsearchCluster,
  api: Elasticsearch.API.AWS,
  default_options: [
    aws: [
      region: "us-east-1",
      service: "es",
      access_key: "aws_access_key_id",
      secret: "aws_secret_access_key"
    ]
  ]
```

## セキュリティトークンの取得

EC2　インスタンスロールでは、通常の認証情報に加え、一時的な認証情報としてセキュリティトークンを使うことで、機密性を高めています

セキュリティトークンは定期的に変化するため、毎回最新のセキュリティトークンを取得しないと認証できません

Elixir 用の AWS クライアント ExAws の以下のモジュールを覗いてみると、以下の URL に EC2 からアクセスすることで、認証情報を得ていました

```bash
$ curl http://169.254.169.254/latest/meta-data/iam/security-credentials/<EC2のインスタンスロール>
{
  "Code" : "Success",
  "LastUpdated" : "2021-07-01T06:54:33Z",
  "Type" : "AWS-HMAC",
  "AccessKeyId" : "ZZZ",
  "SecretAccessKey" : "zzz",
  "Token" : "xxx",
  "Expiration" : "2021-07-01T13:02:54Z"
}%
```

以下のように実装すれば、開発マシンでの実行時は自分の IAM ユーザー認証情報、 EC2 上ではインスタンスロール認証情報が取得できます

```elixir

alias ExAws.Config

def get_aws_credential do
  Application.get_all_env(:ex_aws)
  |> Map.new()
  |> Config.retrieve_runtime_config()
end

```

## セキュリティトークンの付与

あとは、リクエストにセキュリティトークンを付与すれば、 Elasticsearch にアクセスできます

```elixir

alias Elasticsearch.API.AWS

defp handle_temp_credentials(headers, %{security_token: token}) do
  headers
  |> Map.merge(%{"X-Amz-Security-Token": token})
end

defp handle_temp_credentials(headers, _), do: headers

def update_aws_credential do
  aws_config = get_aws_credential()
  default_headers = handle_temp_credentials(%{}, aws_config)

  opts = %{
    default_options: [
      aws: [
        region: System.get_env("AWS_REGION"),
        service: "es",
        access_key: aws_config[:access_key_id],
        secret: aws_config[:secret_access_key]
      ]
    ],
    default_headers: default_headers
  }

  Elasticsearch.Cluster.Config.get(__MODULE__)
  |> Map.merge(opts)
end

def request(method, url, body) do
  update_aws_credential()
  |> AWS.request(method, url, body, [])
end

```

## まとめ

色々なパッケージの中を覗くことで、勉強になることが多いです

EC2　インスタンスロールの仕組みはよくできていますね

かなりニッチな記事ですが、誰かのお役に立てれば