[AWS]V4署名を使ってOpenSearchにアクセスする

この記事は [株式会社サイバー・バズ Advent calendar 2021](https://qiita.com/advent-calendar/2021/cyberbuzz) 16日目の記事です。

# 概要
`ECS`や`Local`環境から`AWSV4`認証を使用して`OpenSearch`にアクセスする機会がありました。

基本的には[こちら](https://dev.classmethod.jp/articles/typescript-connect-es/)の記事を参考にさせていただいています。

# 目次
1. [AWS V4署名](# AWS V4署名)
2. [Local環境から](# Local環境から)
3. [ECS環境から](# ECS環境から)


# AWS V4署名
`AWSV4`署名とは、`AWSAPI`リクエストに認証情報を付与するものです。
AWSへのリクエストは基本的に署名が必要です。

詳しくはドキュメントをご覧ください。
https://docs.aws.amazon.com/ja_jp/general/latest/gr/signature-version-4.html

本記事では`Node.js`から`SDK`を使用して`AWSV4`署名の後`OpenSearch`へアクセスを試みます。

まずは、必要なライブラリをインストールします。

```
npm install --save @aws-crypto/sha256-browser @aws-sdk/client-sts @aws-sdk/credential-providers @aws-sdk/node-http-handler @aws-sdk/protocol-http @aws-sdk/signature-v4
```

##### ※注釈
`SDK`には`V2`と`V3`の二つが存在します。

```
npm install --save aws-sdk
```
上記のコマンドでinstallするSDKは`V2`になり、`@aws-sdk/xxxxx`のように必要なmoduleのみをinstallする`V3`を使用することが推奨されています。
https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/index.html

# Local環境から
では早速Local環境から`OpenSearch`にアクセスしてみます。

## 署名リクエスト作成
まずは、署名を施す`createSignedRequest` です。

```typescript:signed.ts
import { Sha256 } from '@aws-crypto/sha256-browser'
import { STSClient, AssumeRoleCommand } from '@aws-sdk/client-sts'
import { fromIni } from '@aws-sdk/credential-providers'
import { NodeHttpHandler } from '@aws-sdk/node-http-handler'
import { HttpRequest } from '@aws-sdk/protocol-http'
import { SignatureV4 } from '@aws-sdk/signature-v4'

export const createSignedRequest = async ({ host, query }) => {
  const httpRequest = new HttpRequest({
    body: query,
    headers: {
      'content-type': 'application/json',
      host,
    },
    hostname: host,
    method: 'POST',
    path: '[index]/_search',
    port: 443,
  })

  const signer = new SignatureV4({
    credentials: fromIni({
      profile: '[profile-name]',
      roleAssumer: async (credentials, params) => {
        const sts = new STSClient({ credentials })
        const response = await sts.send(new AssumeRoleCommand(params))
        return {
          accessKeyId: response.Credentials.AccessKeyId,
          secretAccessKey: response.Credentials.SecretAccessKey,
          sessionToken: response.Credentials.SessionToken,
          expiration: response.Credentials.Expiration,
        }
      },
    }),
    region: 'ap-northeast-1',
    service: 'es',
    sha256: Sha256,
  })
  return signer.sign(httpRequest) as Promise<HttpRequest>
}
```

まず、`HttpRequest`を作成します。

次に、`SignatureV4`を使って`V4`署名のリクエストを作成します。

`[profile-name]`については、`~/.aws/credentials`で定義済の`profile`を使用します。

今回は、`OpenSearch`インスタンスが別`Role`に存在しているので、`roleAssumer`で`credentilas`情報を取得しています。

`AssumeRole`については以下の記事が参考になります。(特徴的な図があって分かりやすく、個人的に記憶に残っています。)
https://dev.classmethod.jp/articles/iam-role-passrole-assumerole/

署名を施し、returnします。


## 本リクエスト部分
続いて署名を使って実際に`OpenSearch`へリクエストします。

```typescript:index.ts
import { NodeHttpHandler } from '@aws-sdk/node-http-handler'
import { createSignedRequest } from './signed'

export const fetch = async <T>(): Promise<T> => {
  const host = 'search-xxxxxxxxxx.ap-northeast-1.es.amazonaws.com'
  const query = {
    query: {
      match_all: {}
    }
  }
  const signedRequest = await createSignedRequest({
    host,
    query,
  })

  const client = new NodeHttpHandler()
  const { response } = await client.handle(signedRequest).then((res) => res)
  const res: T = await new Promise((resolve) => {
    let responseBody = ''
    response.body.on('data', (chunk) => {
      responseBody += chunk
    })
    response.body.on('end', () => {
      resolve(JSON.parse(responseBody))
    })
  })
  return res
}
```

`host`は実際に使用する`OpenSearch`の`host`名に変換してください。

`OpenSearch`にまつわる詳しい`query`については公式ドキュメントを参考にしてください。

https://opensearch.org/docs/latest/

今回は上から10件分`document`を取得する`query`です。

そして先ほど作成した`createSignedRequest`から署名済みのリクエストを取得し、`NodeHttpHandler`を使用して`OpenSearch`にリクエストしています。

# ECS環境から
ECS環境の場合は、`createSignedRequest`の`SignatureV4`部分に少し修正を入れます。

```typescript:signed.ts
import { Sha256 } from '@aws-crypto/sha256-browser'
import { STSClient, AssumeRoleCommand } from '@aws-sdk/client-sts'
import { fromContainerMetadata } from '@aws-sdk/credential-providers'
import { NodeHttpHandler } from '@aws-sdk/node-http-handler'
import { HttpRequest } from '@aws-sdk/protocol-http'
import { SignatureV4 } from '@aws-sdk/signature-v4'

export const createSignedRequest = async ({ host, query }) => {
  const httpRequest = new HttpRequest({
    body: query,
    headers: {
      'content-type': 'application/json',
      host,
    },
    hostname: host,
    method: 'POST',
    path: '[index]/_search',
    port: 443,
  })

  const signer = new SignatureV4({
    credentials: fromContainerMetadata(),
    region: 'ap-northeast-1',
    service: 'es',
    sha256: Sha256,
  })
  return signer.sign(httpRequest) as Promise<HttpRequest>
}
```

上記で`ECS`コンテナ自体の`Role`を使用します。
そのままでは`OpenSearch`にアクセスできないので、`AWS`コンソールのセキュリティ設定→アクセスポリシーに`ECS`コンテナからのアクセス許可を追加します。

```OpenSerach/Accesspolicy
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": [
          "arn:aws:iam::000000000000:role/ecs-task"
        ]
      },
      "Action": "es:*",
      "Resource": "arn:aws:es:ap-northeast-1:000000000000:domain/[domainName]/*"
    }
  ]
}
```

`Role`や`domainName`に関しては適宜変更してください。

今回は`AWSV4`署名を使用して`OpenSearch`へアクセスを試みました。

他にも良いやり方があるかもしれませんが、一つの参考になれば幸いです。