AWS(S3)

#S3

クラウド環境で使えるオブジェクトストレージ。検証しないとわからなそうなことを色々とメモ。


##結果整合性

- S3は結果整合性を採用しているため、PUTリクエストが完了しても最新の状態になっていない場合がある。なぜなら複数のAZにPUT後にオブジェクトをレプリカしており、全てにレプリカが完了するまで最新の状態が反映されない。結果的に時間が経てば整合性が取れている、これが結果整合性。
    - あくまでアトミックな更新になるので、PUT後に複数AZにレプリカが完了するまでは古いデータ、PUT後に複数AZにレプリカが完了したら最新のデータが参照できる形になる。
    - 新規のオブジェクトに関しては例外的に、キーを直接指定して参照した場合は、PUT後にすぐ読み込みできる。(LISTの場合はこの限りではなく、結果整合性)

`参考`
https://dev.classmethod.jp/cloud/amazon-s3-eventually-consistent-and-consistent-read/


##IAMポリシー、バケットポリシー、ACL
- 基本的に細かい制御が可能なIAMポリシー、バケットポリシーのいずれか、または場合によっては複数組み合わせでアクセス制御をするべきと個人的には考える。

- `awscliやsdkでアクセスする場合`。S3へのバケット、オブジェクトに対するアクセス制御を管理する仕組みは数多くある。どれで許可・拒否・未設定の状態になっていれば結果的にオブジェクトにawscli,sdkでアクセスできるのか。基本的なポリシーは以下でシンプル。
    - 最低限、対象バケット、オブジェクトにアクセスするためのIAMポリシーの明示的な許可が必要
    - どれか１つでもアクセス拒否されているものがあると他でアクセス許可されていてもアクセス不可。

```ruby:バケットポリシー例
###下記は指定したIAMロールやIAMユーザ以外のアクセスのみ許可するJSON。
###特定のIAMユーザtestuserに対して許可する場合は、１行目のみ必要。
###EC2に付与したIAMロールtestroleに対して許可する場合は,2,3行目両方が必要。
{
    "Version": "2012-10-17",
    "Id": "Test Bucket Policy",
    "Statement": [
        {
            "Sid": "Test Policy",
            "Effect": "Deny",
            "NotPrincipal": {
                "AWS": [
                    "arn:aws:iam::<AccountId>:user/testuser", 
                    "arn:aws:iam::<AccountId>:role/testrole",
                    "arn:aws:sts::<AccountId>:assumed-role/testrole/i-xxxxxxxxxx"
                ]
            },
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject",
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::<バケット名>",
                "arn:aws:s3:::<バケット名>/<対象のフォルダなど>/*"
            ]
        }
    ]
}
```

- `ブラウザやcurlでhttp経由でアクセスする場合`。
    - ブロックパブリックアクセスをオフにする。
    - バケットポリシーまたはACLで明示的に対象のオブジェクトに該当する部分を許可する。
    
```ruby:パブリックアクセス
###パブリックアクセスを許可する場合
###ポイントは、
{
    "Version": "2012-10-17",
    "Id": "Test Bucket Policy",
    "Statement": [
        {
            "Sid": "Test Policy",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:GetObject",
                "s3:PutObject",
                "s3:DeleteObject",
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::<バケット名>",
                "arn:aws:s3:::<バケット名>/<対象のフォルダなど>/*"
            ]
        }
    ]
}
```

##REST API

パブリックアクセスを許可した後にcurlコマンドでREST APIを再現してみる。

`リソース参照(CLI上では読み取れないので保存)`
curl -v -O -X GET https://[s3バケット名].s3.amazonaws.com/public/test.jpg
===>正常HTTPステータスコード： HTTP/1.1 200 OK

`リソース作成or更新`
curl -v -X PUT https://[s3バケット名].s3.amazonaws.com/public/test.jpg -T <アップロードファイル>
===>正常HTTPステータスコード： HTTP/1.1 200 OK

`リソース削除`
curl -v -X DELETE https://[s3バケット名].s3.amazonaws.com/public/test.jpg 
===>正常HTTPステータスコード： HTTP/1.1 204 No Content

ちなみにPOSTリクエストのレスポンスヘッダは以下。S3に対してはPOSTはサポートされていない模様。

```ruby:
< HTTP/1.1 405 Method Not Allowed
< x-amz-request-id: 36A3ABB56AF54D61
< x-amz-id-2: wgWlOntkIMkkTSwwQjzuNw+p1rmwHvr1RMHs2fGogj57Pv3SuBaxomOb20eGVPjMqQ0CPt+qoqQ=
< Allow: HEAD, DELETE, GET, PUT
< Content-Type: application/xml
< Transfer-Encoding: chunked
< Date: Thu, 20 Feb 2020 15:29:01 GMT
< Connection: close
< Server: AmazonS3
```


##ライフサイクルルール

- 端的にいうと、ログ改廃または移行の仕組み
    - 設定しておくと指定した期間(日)がタイムスタンプ的に経過したS3上のファイルを削除(Expiration)または他ストレージタイプへ移行(Transition)する仕組み
    - 対象はバージョニングで裏で保管されているもの、または最新版いずれに対しても設定可能
    - prefix単位でも指定可能
    - いつの何時何分に必ず処理が発行されていつまでに終わるという明確な基準はない（ベストエフォート型）。ただ大体の目安は決まっていて毎日UTC0時頃からライフサイクルルールの対象になっているファイルの最終更新時刻をチェックして保管期限が過ぎていたら消すという仕組み
https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/intro-lifecycle-rules.htmlhttps://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/intro-lifecycle-rules.html    

>Amazon S3 は、ルールに指定された日数をオブジェクトの次の新しいバージョンが作成された時間に加算し、得られた日時を翌日の午前 00:00 (UTC) に丸めることで、時間を算出します。たとえば、バケット内に 2014 年 1 月 1 日の午前 10 時半 (UTC) に作成されたオブジェクトの現行バージョンがあるとします。現行バージョンを置き換えるオブジェクトの新しいバージョンが 2014 年 1 月 15 日の午前 10 時半 (UTC) に作成され、3 日間の移行ルールを指定すると、オブジェクトの移行日は 2014 年 1 月 19 日の午前 0 時 (UTC) となります。

`参考:現時点のライフサイクルルール対象を確認するスクリプト`
タイムスタンプはデフォルトのままだとUTC時間で出てきてしまうため、JST時間で出るように手を加えている。

```ruby:S3バケット全体に対して指定した日数以前のファイルを出力(第一引数でバケット名、第二引数で日数を指定)
#!/usr/bin/python

import boto3
import sys
import datetime

args = sys.argv
bucket_name=args[1]
expired_day=int(args[2])

timestamp_now=datetime.datetime.now()
s3 = boto3.resource('s3')
bucket = s3.Bucket(bucket_name)

for obj_summary in bucket.objects.all():
        dt=obj_summary.last_modified.strftime("%Y-%m-%d %H:%M:%S")
        dp=datetime.datetime.strptime(dt,"%Y-%m-%d %H:%M:%S")
        timestamp_put=dp + datetime.timedelta(hours=9)
        timestamp_del=timestamp_now - datetime.timedelta(days=expired_day)

        if timestamp_put < timestamp_del:
            print (str(timestamp_put),"s3://" + bucket_name + "/" + obj_summary.key)
```
```ruby:S3バケットの特定のプレフィックスに対して指定した日数以前のファイルを出力(第一引数でバケット名、第二引数でプレフィックス名、第三引数で日数を指定)
#!/usr/bin/python

import boto3
import sys
import datetime

args = sys.argv
bucket_name=args[1]
prefix=args[2]
expired_day=int(args[3])

timestamp_now=datetime.datetime.now()
s3 = boto3.resource('s3')
bucket = s3.Bucket(bucket_name)

for obj_summary in bucket.objects.filter(Prefix=prefix):
        dt=obj_summary.last_modified.strftime("%Y-%m-%d %H:%M:%S")
        dp=datetime.datetime.strptime(dt,"%Y-%m-%d %H:%M:%S")
        timestamp_put=dp + datetime.timedelta(hours=9)
        timestamp_del=timestamp_now - datetime.timedelta(days=expired_day)

        if timestamp_put < timestamp_del:
            print (str(timestamp_put),"s3://" + bucket_name + "/" + obj_summary.key)
```
 

##署名付きURL
S3ではパブリックアクセスを禁止しているバケットでも一時的にhttp経由で対象のオブジェクトにWebアクセスできるようにする署名つきURLを発行する方法がある。それをユーザに渡せば対象のユーザのみ期限付きで一時的にオブジェクトにアクセス可能。署名付きURL発行することでリクエストパラメータ付き(署名付き)のURLが発行され、それでアクセスすれば一時的にパブリックアクセス可能。デフォルトだと期限が１時間。

```ruby:署名付きURL発行
###デフォルト
aws s3 presign s3://[S3バケット名]/[S3キー名]
------------------
https://署名付きURL
------------------

###期限付き
aws s3 presign s3://[S3バケット名]/[S3キー名] --expires-in 秒数

```

`注意事項`

1.発行時に使った認証情報によって最大期限が決まっている。期限切れを以下の時間より大きくしても、最大期間が過ぎたら無効になる。

- AWS Identity and Access Management (IAM) インスタンスプロファイル: 最大 6 時間有効
- AWS Security Token Service (STS): 最大 36 時間有効 (AWS アカウントユーザーや IAM ユーザーの認証情報など、永続的認証情報を使用して署名した場合)
- IAM ユーザー: 最大 7 日間有効 (AWS 署名バージョン 4 を使用した場合)

https://aws.amazon.com/jp/premiumsupport/knowledge-center/presigned-url-s3-bucket-expiration/

2.巨大ファイルなどのダウンロード中に有効機嫌が過ぎたら？
結論的には、リクエストをAWSで受け取った時に切れていなければ問題ない。

3.IAMユーザまたはロールが有効期限内に削除されたら？
リクエストは即時無効になる。これはAWSでリクエストを受け取った時、署名に使った認証情報を内部で検証しているため。


## 静的ウェブサイトホスティング（ウェブサイトエンドポイント）

S3 の静的ウェブサイトホスティング機能を使うことにより静的コンテンツのより便利な公開が可能。
普通にパブリックアクセス許可して、バケットポリシーで対象のオブジェクトの許可をする(Rest API エンドポイント)だけでも静的コンテンツは公開できるが、以下の点で違いがある。

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/WebsiteEndpoints.html

- <strong>REST API エンドポイント</strong>
    - アクセスコントロール・・・パブリックコンテンツとプライベートコンテンツの両方をサポートします。
    - エラーメッセージの処理・・・XML 形式のエラーレスポンスを返します。
    - リダイレクトのサポート・・・不可能
    - サポートされるリクエスト・・・バケットおよびオブジェクトのすべてのオペレーションをサポートします。
    - バケットのルートでの GET リクエストと HEAD リクエストへのレスポンス・・・バケット内のオブジェクトキーのリストを返します。
    - Secure Sockets Layer (SSL) のサポート・・・可能

- <strong>ウェブサイトエンドポイント</strong>
    - アクセスコントロール・・・公開で読み取り可能なコンテンツのみをサポートします。
    - エラーメッセージの処理・・・HTML ドキュメントを返します。
    - リダイレクトのサポート・・・オブジェクトレベルとバケットレベルの両方のリダイレクトをサポートします。
    - サポートされるリクエスト・・・オブジェクトに対しては GET リクエストと HEAD リクエストのみをサポートします。
    - バケットのルートでの GET リクエストと HEAD リクエストへのレスポンス・・・ウェブサイト設定で指定されているインデックスドキュメントを返します。
    - Secure Sockets Layer (SSL) のサポート・・・不可能

個人的には、ウェブサイトエンドポイントがHTTPSに対応していないことはちょっと痛いが、エラーメッセージの処理やリダイレクトができることは非常によいと考える。
そのため、アクセス元のIPアドレスやエンドポイントを制御することで使用も検討できるかと。
ただし、これらは例えばCloudFrontで賄える可能性もあるので要件によって利用を考えるべきだろう。

また、Route53 に登録するドメイン名と全く同一名のS3バケットをウェブサイトエンドポイント化することで、Route53のエイリアスレコードでウェブサイトエンドポイントを参照先として指定できるので、S3へのアクセス時のホスト部分=ドメイン名にしたい場合は非常に有用。この時はゾーンのドメイン名＝S3バケット名にしないとS3のエイリアス先として出ないため注意。Amazon S3 はホスト名を使用してバケット名を決定するので、ホストヘッダー部分がS3バケット名と一致する必要あり。

## S3 アクセス元制御

S3 へのアクセス元を制御する方法としては、IPアドレスやエンドポイントで制御する方法が参考例のようにある。
これを実装する場合のバケットポリシー例。パブリックアクセスに対しては以下でアクセス元の制御可能
https://aws.amazon.com/jp/premiumsupport/knowledge-center/block-s3-traffic-vpc-ip/

```
以下例はホワイトリスト形式での制御。
-PublicReadForGetBucketObjects1=アクセス元のs3エンドポイントで制御。aws:sourceVpce 複数ある場合はリスト形式で指定可能。
-PublicReadForGetBucketObjects2=アクセス元のIPアドレスで制御。aws:SourceIp 複数ある場合はリスト形式で指定可能。

<------------------------------------------------------------------------
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadForGetBucketObjects1",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::[s3bucketname]/*",
            "Condition": {
                "StringEquals": {
                    "aws:sourceVpce": "vpce-xxxxxxxxxxxx"
                }
            }
        },
        {
            "Sid": "PublicReadForGetBucketObjects2",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::[s3bucketname]/*",
            "Condition": {
                "IpAddress": {
                    "aws:SourceIp": "xxx.xxx.xxx.xxx/xx"
                }
            }
        }
    ]
}
------------------------------------------------------------------------>
```



##レプリケーション

- S3バケットのオブジェクトを他のバケット（同一リージョン/他リージョン)にレプリケーションできる仕組み
    - 仕組みとしては以下。レプリケーション用のIAMロールがソースBucketのオブジェクトを宛先BucketにPUTする仕組み。
    　https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/setting-repl-config-perm-overview.html
    - レプリケーションルールはソースBucketへ設定する。ルールは複数設定可能
    - 宛先Bukcetとロールは、1ソースBucketあたり1つしか選べない。
    - レプリケーションは非同期で行われる。容量が大きいほど、完了時間は伸びる。RTCを使うと15分以内にレプリケーションを完了させられる、かつCloudwatchのレプリケーションメトリクスを利用できるが追加料金あり。


## Amazon Linux/Amazon Linux2のレポジトリ

Amazon Linux/Amazon Linux2のレポジトリは実はS3バケット上に存在する。(/etc/yum.repos.d/amzn2-*repo に定義があるレポジトリ！)
基本的にS3のエンドポイント含めAWSのサービスエンドポイントはデフォルトだとアクセス先がインターネットにあるためIGWやNAT GWでインターネットにアウトバウンド通信してアクセスする必要がある。そのため、yum レポジトリにアクセスするためにIGWやNAT GWをつける必要があるかと思いきやそこまでできない場合もある。
その場合は、S3のVPCエンドポイント経由でレポジトリアクセスしてyum installすることが可能。

S3のVPCエンドポイントで定義されるプレフィックスIDからS3のエンドポイントのIPアドレス帯を調べる例。
これらのIPアドレス帯＝S3バケットのIPアドレス帯で、これにアクセスする際はVPCエンドポイントを経由するようになる。
S3 のVPCエンドポイントは、ゲートウェイ型のためルートテーブルにアクセス経路として指定する

```
# aws ec2 describe-prefix-lists --prefix-list-ids=pl-xxxxxxxx --region=ap-northeast-1
{
    "PrefixLists": [
        {
            "PrefixListName": "com.amazonaws.ap-northeast-1.s3", 
            "Cidrs": [
                "52.219.0.0/20", 
                "52.219.136.0/22", 
                "52.219.16.0/22", 
                "52.219.68.0/22"
            ], 
            "PrefixListId": "pl-xxxxxxxx"
        }
    ]
}
```

`参考`
https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/vpc-endpoints-s3.html
例: Amazon Linux AMI リポジトリへのアクセスの有効化