Amazon S3の静的ウェブサイトホスティングをAmazon Route53のプライベートホストゾーンからエイリアスする

# はじめに
Amazon S3の静的ウェブサイトホスティングは便利。
でもエンドポイントが```http://バケット名.s3-website-ap-northeast-1.amazonaws.com```になってしまったり```https://s3-ap-northeast-1.amazonaws.com/バケット名.com```になってしまってイケてない。内部通信くらいはちゃんとしたサービス名をつけてあげたいけど内部通信のためにドメイン使用量は払いたくない。

そんなワガママなあなたに、良い感じの名前でアクセスするコツを書いておく。

さらに、今回の記事では、以下の構成で作った静的ウェブサイトホスティングに対してクロスアカウントでセキュアにアクセスする方法まで書いておく。

![構成図_r2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/025ade67-080a-816b-2d09-854d2c647e69.png)

# Amazon Route53のプライベートホストゾーンを使おう
良い感じの名前を付けるのであれば、Amazon Route53のプライベートホストゾーンを使えば良い。

プライベートホストゾーンを設定する方法は[以前の記事](https://qiita.com/neruneruo/items/70b6bbc07ad859a0d3ca)に書いている。

一つ注意しなければいけないのは、Amazon S3のエイリアスを作る場合は、***「ホスト名＝バケット名」になっている必要がある***ということだ。これをやっておかないと、Amazon Route53のホストゾーンでAレコードをS3に紐付けることができない。

Terraformで格納であればこんな感じになる。```name    = aws_s3_bucket.example.id```としているのがキモだ。
```lifecycle```については後述する。

```HCL
resource "aws_route53_zone" "example" {
  provider = aws.to_account

  name = local.zone_name

  vpc {
    vpc_id = data.aws_vpc.to.id
  }
}

resource "aws_route53_record" "example" {
  provider = aws.to_account

  zone_id = aws_route53_zone.example.zone_id
  name    = aws_s3_bucket.example.id
  type    = "A"

  alias {
    name                   = aws_s3_bucket.example.website_domain
    zone_id                = aws_s3_bucket.example.hosted_zone_id
    evaluate_target_health = true
  }
}
```

# 作ったエイリアスレコードを他のAWSアカウントと共有してクロスアカウントアクセスを可能にする
さて、せっかく作ったエイリアスレコードなのだから、複数のサービスで共通で使用したくなることがあるかもしれない。ないかもしれないしその可能性が高い。

だが、もしそういう事態が訪れた時には、[AWS公式がやり方を紹介してくれている](https://aws.amazon.com/jp/premiumsupport/knowledge-center/private-hosted-zone-different-account/)。
このリンクお貼っておくだけでは芸がないので、Terraformで書くとこうなる、を置いておく。

```HCL
resource "aws_route53_vpc_association_authorization" "example" {
  provider = aws.to_account

  vpc_id  = data.aws_vpc.from.id
  zone_id = aws_route53_zone.example.id
}

resource "aws_route53_zone_association" "example" {
  provider = aws.from_account

  vpc_id  = aws_route53_vpc_association_authorization.example.vpc_id
  zone_id = aws_route53_vpc_association_authorization.example.zone_id
}
```

これで```terraform apply```すると、aws_route53_zoneに指定したVPCが追加される。
しかし、VPCが追加されるということは、```aws_route53_zone.example```の.tfファイルとプロパティが合わなくなってしまうということだ。
すると、何が起こるかと言うと、次にapplyするタイミングで、実際のリソースとの差分を検知して、せっかく設定したVPCの設定が削除されてしまう。

これを避けるために、aws_route53_zone_associationでクロスアカウント設定する場合は、aws_route53_zoneに以下の設定を追加する。

```diff_HCL
resource "aws_route53_zone" "s3" {
  provider = aws.to_account

  name = local.zone_name

  vpc {
    vpc_id = data.aws_vpc.to.id
  }

+ lifecycle {
+   ignore_changes = [
+     vpc,
+   ]
+ }
}
```

これにより、vpcの変更は無視されるようになるため、次に関係のないリソースを```terraform apply```する際に差文検知しなくなる。

# クロスアカウントアクセスをセキュアにする
さて、静的Webサイトホスティングの用途は千差万別だが、内部のためにクロスアカウントアクセスを設定したのであれば、以下のようにVPCエンドポイント経由のアクセスになるようルーティングをしておこう。

```HCL
resource "aws_vpc_endpoint" "s3_gateway" {
  provider = aws.from_account

  vpc_id       = data.aws_vpc.from.id
  service_name = "com.amazonaws.ap-northeast-1.s3"
}

resource "aws_vpc_endpoint_route_table_association" "s3_gateway" {
  provider = aws.from_account

  route_table_id  = data.aws_route_table.from.id
  vpc_endpoint_id = aws_vpc_endpoint.s3.id
}
```

なお、コンテンツを一般公開するのであれば、バケットのパブリックアクセスを許容しないといけないが、VPCエンドポイント経由でプライベートアクセスする場合は、バケットポリシーでAllowするアクセス制御をしておけば、バケットの公開は不要だ。以下のように設定を行い、よりセキュアな状態にしておこう。

```diff_HCL
resource "aws_s3_bucket_public_access_block" "example" {
  provider = aws.to_account

  bucket = aws_s3_bucket.example.id

- block_public_acls       = false
+ block_public_acls       = true
- block_public_policy     = false
+ block_public_policy     = true
- ignore_public_acls      = false
+ ignore_public_acls      = true
- restrict_public_buckets = false
+ restrict_public_buckets = true
}

resource "aws_s3_bucket_policy" "example" {
  provider = aws.to_account

  bucket = aws_s3_bucket.example.id
  policy = data.aws_iam_policy_document.example_bucket_policy.json
}

data "aws_iam_policy_document" "example_bucket_policy" {
  statement {
    sid = "AllowVPCEndPointRead"

    effect = "Allow"

    principals {
      type        = "*"
      identifiers = ["*"]
    }

    actions = [
      "s3:GetObject",
    ]

    resources = [
      "arn:aws:s3:::${local.bucket_name}",
      "arn:aws:s3:::${local.bucket_name}/*",
    ]

+   condition {
+     test     = "StringEquals"
+     variable = "aws:sourceVpce"
+
+     values = [
+       "${aws_vpc_endpoint.s3_gateway.id}",
+     ]
+   }
+ }
}
```

# おまけ) インターフェース型エンドポイントで固定IPアドレスを使ってS3へのプライベートアクセスを行う
通常はゲートウェイ型のVPCエンドポイントを使えばプライベートアクセスの要件を満たせるが、たとえばオンプレミスからのアクセスでDNS参照ができないためにIPアドレスを固定したいケースがたまにある。この場合、AWS公式のブログやAWS re:Postでも紹介されている通り、ENIをアタッチするインターフェース型エンドポイントを用いると良い。

https://aws.amazon.com/jp/blogs/news/internal-static-web-hosting/

https://repost.aws/ja/knowledge-center/s3-bucket-access-direct-connect

:::note warn
インターフェース型エンドポイント(PrivateLink)でのS3へのアクセスはRESTAPIエンドポイント向けになるので、静的Webサイトホスティングは使用しない。この記事の趣旨とは多少異なるが、S3へのプライベート接続の一つの手法として紹介しておく。
:::

設定はそんなに難しいことはなく、以下のようにインターフェース型のVPCエンドポイントを作成すれば良い。
セキュリティグループは80番ポートを解放しておくようにしよう。

```HCL
resource "aws_vpc_endpoint" "s3_interface" {
  provider = aws.from_account

  vpc_id            = data.aws_vpc.from.id
  service_name      = "com.amazonaws.ap-northeast-1.s3"
  vpc_endpoint_type = "Interface"

  subnet_ids = data.aws_subnets.from.ids

  security_group_ids = [
    data.aws_security_group.from_http.id,
  ]

  private_dns_enabled = false
}
```

また、払い出されたエンドポイントIDをバケットポリシーにも追加するのを忘れないようにしよう。

```diff_HCL
data "aws_iam_policy_document" "example_bucket_policy" {
  statement {
    sid = "AllowVPCEndPointRead"

    effect = "Allow"

    principals {
      type        = "*"
      identifiers = ["*"]
    }

    actions = [
      "s3:GetObject",
    ]

    resources = [
      "arn:aws:s3:::${local.bucket_name}",
      "arn:aws:s3:::${local.bucket_name}/*",
    ]

    condition {
      test     = "StringEquals"
      variable = "aws:sourceVpce"

      values = [
        "${aws_vpc_endpoint.s3_gateway.id}",
+       "${aws_vpc_endpoint.s3_interface.id}",
      ]
    }
  }
}
```

これで、追加後に払い出されたエンドポイントかIPアドレスにアクセスすることで、Amazon S3のオブジェクトを取得できる。Amazon S3はHostヘッダで正しいリージョンにアクセスしているかの検証をしているようなので、IPアドレス指定の場合は以下のようにリスクエストヘッダを設定する必要がある(リクエストヘッダがないと301応答で正しくコンテンツの取得ができない)。

```bash
curl -H 'Host:[バケット名].s3.[リージョン名].amazonaws.com' http://XXX.XXX.XXX.XXX/index.html
```