はじめに
Amazon S3の静的ウェブサイトホスティングは便利。
でもエンドポイントがhttp://バケット名.s3-website-ap-northeast-1.amazonaws.com
になってしまったりhttps://s3-ap-northeast-1.amazonaws.com/バケット名.com
になってしまってイケてない。内部通信くらいはちゃんとしたサービス名をつけてあげたいけど内部通信のためにドメイン使用量は払いたくない。
そんなワガママなあなたに、良い感じの名前でアクセスするコツを書いておく。
さらに、今回の記事では、以下の構成で作った静的ウェブサイトホスティングに対してクロスアカウントでセキュアにアクセスする方法まで書いておく。
Amazon Route53のプライベートホストゾーンを使おう
良い感じの名前を付けるのであれば、Amazon Route53のプライベートホストゾーンを使えば良い。
プライベートホストゾーンを設定する方法は以前の記事に書いている。
一つ注意しなければいけないのは、Amazon S3のエイリアスを作る場合は、「ホスト名=バケット名」になっている必要があるということだ。これをやっておかないと、Amazon Route53のホストゾーンでAレコードをS3に紐付けることができない。
Terraformで格納であればこんな感じになる。name = aws_s3_bucket.example.id
としているのがキモだ。
lifecycle
については後述する。
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公式がやり方を紹介してくれている。
このリンクお貼っておくだけでは芸がないので、Terraformで書くとこうなる、を置いておく。
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に以下の設定を追加する。
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エンドポイント経由のアクセスになるようルーティングをしておこう。
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するアクセス制御をしておけば、バケットの公開は不要だ。以下のように設定を行い、よりセキュアな状態にしておこう。
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をアタッチするインターフェース型エンドポイントを用いると良い。
インターフェース型エンドポイント(PrivateLink)でのS3へのアクセスはRESTAPIエンドポイント向けになるので、静的Webサイトホスティングは使用しない。この記事の趣旨とは多少異なるが、S3へのプライベート接続の一つの手法として紹介しておく。
設定はそんなに難しいことはなく、以下のようにインターフェース型のVPCエンドポイントを作成すれば良い。
セキュリティグループは80番ポートを解放しておくようにしよう。
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をバケットポリシーにも追加するのを忘れないようにしよう。
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応答で正しくコンテンツの取得ができない)。
curl -H 'Host:[バケット名].s3.[リージョン名].amazonaws.com' http://XXX.XXX.XXX.XXX/index.html