閉じたサブネット内のESドメインとEC2踏み台で手動スナップショットを登録する

# やりたいこと

外部ネットワークへのルートがなく、従ってAWSのAPIエンドポイントへの疎通ができないサブネット（以降、Secured Subnet）に存在する踏み台EC2がある。
ここから、同じくSecured Subnet内にVPCアクセスとして構成したAmazon ESドメインに対し、手動スナップショットレポジトリの登録を行いたい。

[ここ](https://docs.aws.amazon.com/ja_jp/elasticsearch-service/latest/developerguide/es-managedomains-snapshots.html)に手順が書いてあるが、SigV4認証が必要とあり、インスタンスプロファイルから認証情報取ってくるから大丈夫だろうとは思いつつも一抹の不安がある。

# 環境
**Amazon ES**

- 7.10
- VPCアクセス（Secured Subnetに配置）
    - VPC外とは疎通できない
- IAM認証なし、Fine Grained Access Control（FGAC）なし
- Secured Subnet内の通信のみ許可

**踏み台EC2**

- Amazon Linux 2
- あらかじめpip3で以下をインストール済み
    - boto3
    - requests
    - requests_aws4auth
- Secured Subnetに配置
    - VPC外とは疎通できない

# 手順

## 1. S3バケットを作成する

今回は`my-bucket-4-es`として作成。

```bash
% aws s3 mb s3://my-bucket-4-es --region ap-northeast-1
```

## 2. S3バケットに読み書きできるポリシーを作成する

いくつかポリシーを作成していくのだが、これはESが使う用のポリシー。
手動スナップショットのデータをS3にPutするのに使用する。

```json
{
  "Version": "2012-10-17",
  "Statement": [{
      "Action": [
        "s3:ListBucket"
      ],
      "Effect": "Allow",
      "Resource": [
        "arn:aws:s3:::my-bucket-4-es"
      ]
    },
    {
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Effect": "Allow",
      "Resource": [
        "arn:aws:s3:::my-bucket-4-es/*"
      ]
    }
  ]
}
```

## 3. ESに渡すロールを作成し、上記ポリシーをアタッチする

ポリシーをESに渡すために、es.amazonaws.comをプリンシパルに指定したロールを作る。
ここでは`TestRoleForESManualSnapshot`として作成する。
ちなみに、マネジメントコンソールのIAMポリシージェネレーターから作るとESを指定できないので、EC2とかで適当に作成して後で修正する。もちろんCLIで作ってもよい。

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "es.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
```

なおこのロールは、マネジメントコンソールやCLIからはESには渡せない。
後続のPythonスクリプトでESドメインに手動スナップショットレポジトリを登録する際に、一緒に渡す建て付けとなっている。

## 4. iam:passRoleとes:ESHttpPut権限を付与したポリシーを作成する

上記で作成したロールをESに渡すには、`iam:passRole`という「ロールを渡すための権限」が必要になる。
今回、渡す側はEC2上のPythonスクリプトで、PythonスクリプトはEC2インスタンスプロファイルから一時認証情報を取ってくるので、`iam:passRole`を持たせる相手はこのインスタンスプロファイルに指定するロール、ということになる。

また、ESドメインに手動スナップショットレポジトリを登録するためにPUTを使うので、`es:ESHttpPut`も併せて付与する。

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "iam:PassRole",
      "Resource": "arn:aws:iam::123456789012:role/TestRoleForESManualSnapshot"
    },
    {
      "Effect": "Allow",
      "Action": "es:ESHttpPut",
      "Resource": "arn:aws:es:ap-northeast-1:123456789012:domain/<ESドメイン名>/*"
    }
  ]
}
```

## 5. EC2インスタンスプロファイルとして使うロールを作成し、上記ポリシーをアタッチする

上記ポリシーをEC2インスタンスプロファイルで使えるようにするために、プリンシパルをec2.amazonaws.comとしたロールを作成する。
ここでは`EC2RoleForESSnapshot`として作成し、マネジメントコンソールで踏み台EC2にアタッチする。

```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
```

## 6. 踏み台に接続

準備が整ったので、手動スナップショットレポジトリの作成に入る。

目指す踏み台には直接入れないので、IGWのいるサブネット（Public Subnet）上の踏み台を経由して入る。他にも手段はあるが今回はそこ主眼でないので、古の方法で。

```bash
% ssh -A ec2-user@"<Public Subnet上のEC2 FQDN>".ap-northeast-1.compute.amazonaws.com
Last login: Fri Jun  4 07:17:53 2021 from 27.0.3.146

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

https://aws.amazon.com/amazon-linux-2/
[ec2-user@ip-XXX-XXX-XXX-XXX ~]$ ssh "<Secure Subnet上のEC2 FQDN>".ap-northeast-1.compute.internal
Last login: Fri Jun  4 07:22:30 2021 from ip-XXX-XXX-XXX-XXX.ap-northeast-1.compute.internal

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

https://aws.amazon.com/amazon-linux-2/
[ec2-user@ip-YYY-YYY-YYY-YYY ~]$
```

## 7. 手動スナップショットレポジトリ登録用のPythonスクリプトを用意する

公式ドキュメントのサンプルを改修して作成。

```python
import boto3
import requests
from requests_aws4auth import AWS4Auth

host = 'https://<ESエンドポイント>.ap-northeast-1.es.amazonaws.com/'
region = 'ap-northeast-1'
s3bucket = 'my-bucket-4-es'
snapshotrole = 'arn:aws:iam::123456789012:role/TestRoleForESManualSnapshot'
service = 'es'
credentials = boto3.Session().get_credentials()
awsauth = AWS4Auth(credentials.access_key, credentials.secret_key, region, service, session_token=credentials.token)

# Register repository
path = '_snapshot/my-snapshot-repo' # the Elasticsearch API endpoint
url = host + path

payload = {
  "type": "s3",
  "settings": {
    "bucket": s3bucket,
    "region": region,
    "role_arn": snapshotrole
  }
}

headers = {"Content-Type": "application/json"}

r = requests.put(url, auth=awsauth, json=payload, headers=headers)

print(r.status_code)
print(r.text)
```

## 8. レポジトリの登録を実行

```bash
[ec2-user@ip-YYY-YYY-YYY-YYY ~]$ python3 ./manual-ss.py
200
{"acknowledged":true}
```

問題なく実行できた。
AWSのAPIエンドポイントへのアクセスは不要であるようだ。

## 9. レポジトリの確認

```bash
[ec2-user@ip-YYY-YYY-YYY-YYY ~]$ curl -XGET https://"<ESエンドポイント>".ap-northeast-1.es.amazonaws.com/_snapshot/my-snapshot-repo-name/my-first-snapshot?pretty
{
  "snapshots" : [ {
    "snapshot" : "my-first-snapshot",
    "uuid" : "lw31y3buT1iJyV-09Gy85Q",
    "version_id" : 7090199,
    "version" : "7.9.1",
    "indices" : [ "movies", ".kibana_1" ],
    "data_streams" : [ ],
    "include_global_state" : true,
    "state" : "SUCCESS",
    "start_time" : "2021-06-04T07:43:34.170Z",
    "start_time_in_millis" : 1622792614170,
    "end_time" : "2021-06-04T07:43:34.770Z",
    "end_time_in_millis" : 1622792614770,
    "duration_in_millis" : 600,
    "failures" : [ ],
    "shards" : {
      "total" : 6,
      "failed" : 0,
      "successful" : 6
    }
  } ]
}
```

## 10. 手動スナップショットの作成

```bash
% curl -XPUT https://<ESエンドポイント>.ap-northeast-1.es.amazonaws.com/_snapshot/my-snapshot-repo/my-first-snapshot
{"accepted":true}
```

## 11. 手動スナップショットの確認

```bash
% curl -XGET https://<ESエンドポイント>.ap-northeast-1.es.amazonaws.com/_snapshot/my-snapshot-repo/my-first-snapshot
```

S3上ではこんな感じで作られる。
通常の自動スナップショットはユーザーのS3側には現れないが、こちらはユーザー所有の（1.で作成した）バケット上に格納され、自由に扱える。

![スクリーンショット 2021-06-04 午後5.19.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/609876/eeb181dd-0169-4445-09d9-6576391db1c6.png)

# まとめ

AWSのAPIエンドポイントへのアクセスが一切ない環境でも、手動スナップショットレポジトリの登録とスナップショットの作成ができることが確認できた。

リストアについてはまたいずれ。