こちらはJapan AWS Ambassadors Advent Calendar 2024 の15日目の記事です。
こんにちは。皆さん、re:Invent2024は楽しめましたか?
今年も、生成AIや機械学習、強力なDRの機能を持った新しいデータベースのアセットを中心に、まさに「顧客の課題を中心にとらえた」AWSらしい新サービスやアップデートがたくさん紹介されましたね。私も現地でKeyNoteを聞きながら盛り上がっていました。
一方、re:Inventの数週間前から怒涛のアップデートが発表されたpre:Inventについても魅力的なアップデートがたくさん来ています。
本記事では、pre:Inventの中で発表されたAWS Transfer Family web appsをお手軽に構築できるようTerraformのコードに書き起こしました。
AWS Transfer Family web appsは、まだAWS Providerからは提供されていないため、ここだけCLIで作りますが、それ以外の周辺リソースを一気に作り上げられるようにしています。
さらに、簡易的なコンテンツ管理システムを実装できるのではないかと利用ケースを考察しています。
AWS Transfer Family web apps って何?
一言で言うと「開発者ではない運用者やサービス主幹の人がお手軽かつセキュアにシステムにファイルアップロードするためのWebインタフェースのマネージドサービスです。
システム開発のよくある要件として「運用者がファイルをアップロードできること。専用の認証と専用のWeb画面を具備すること」と書かれていたりしますよね?バックエンドの機能にコストをあまりかけたくないので「マネージメントコンソールからのアップロードさせてもらえないですかね……?」と相談しつつも利用部門から「ダメ」と言われて泣く泣くCognitoと簡易的なお手製の画面とAPIを用意した覚えがある人が多くいるかと思いますが、今後はそんなものは不要になります、
料金は、ユニット数(同時接続数)に対する1時間単位の従量課金です。
東京リージョンでは0.5USD/時間・ユニットで、ずっと起動していると激安とは言えない金額になります。
今回の構築対象の構成
それでは、コードを書いていきましょう。
一見するとシンプルですが、S3のアクセス制御にS3 Access Grantsを使ったりしていて、少し癖があります。
なお、以降に記載するHCLのコード例ではセキュリティ関連はあまり重視した記載になっていないため、実運用をする際はそのあたりを充分に強化したうえで利用してください。
なお、構成を作るにあたっては、AWS Transfer Familyのユーザーガイドを参考にしているため、あわせてご確認ください。2024/12/15現在、まだ日本語のドキュメントは提供されていません。
事前準備(IAM Identity Centerの有効化)
まずは、IAM Identity Centerを有効しておきましょう。
アカウント共通の設定のため?なのか、この部分はTerraformでは触ることができません。
有効化後、以下のデータソースを使うことで参照ができます。
data "aws_ssoadmin_instances" "example" {}
Terraformのコードを書いていく
IAMロール
AWS Transfer Familyと、S3 Access Grants用のIAMロールをそれぞれ用意します。
resource "aws_iam_role" "transfer" {
name = local.iam_transfer_role_name
assume_role_policy = data.aws_iam_policy_document.transfer_assume.json
}
data "aws_iam_policy_document" "transfer_assume" {
statement {
effect = "Allow"
principals {
type = "Service"
identifiers = [
"transfer.amazonaws.com",
]
}
actions = [
"sts:AssumeRole",
"sts:SetContext",
]
}
}
resource "aws_iam_role_policy" "transfer" {
name = local.iam_transfer_policy_name
role = aws_iam_role.transfer.id
policy = data.aws_iam_policy_document.transfer_custom.json
}
data "aws_iam_policy_document" "transfer_custom" {
statement {
effect = "Allow"
actions = [
"s3:GetDataAccess",
"s3:ListCallerAccessGrants",
"s3:ListAccessGrantsInstances",
]
resources = [
"*",
]
}
}
S3 Access Grantsのロールは、Amazon S3のユーザーガイドを参考にします。
resource "aws_iam_role" "access_grants_s3" {
name = local.iam_access_grants_s3_role_name
assume_role_policy = data.aws_iam_policy_document.access_grants_s3_assume.json
}
data "aws_iam_policy_document" "access_grants_s3_assume" {
statement {
effect = "Allow"
principals {
type = "Service"
identifiers = [
"access-grants.s3.amazonaws.com",
]
}
actions = [
"sts:AssumeRole",
"sts:SetContext",
]
}
}
resource "aws_iam_role_policy" "access_grants_s3" {
name = local.iam_access_grants_s3_policy_name
role = aws_iam_role.access_grants_s3.id
policy = data.aws_iam_policy_document.access_grants_s3_custom.json
}
data "aws_iam_policy_document" "access_grants_s3_custom" {
statement {
sid = "BucketLevelReadPermissions"
effect = "Allow"
actions = [
"s3:ListBucket",
]
resources = [
"${aws_s3_bucket.example.arn}",
]
condition {
test = "StringEquals"
variable = "aws:ResourceAccount"
values = [data.aws_caller_identity.self.id]
}
condition {
test = "ArnEquals"
variable = "s3:AccessGrantsInstanceArn"
values = [aws_s3control_access_grants_instance.example.access_grants_instance_arn]
}
}
statement {
sid = "ObjectLevelReadPermissions"
effect = "Allow"
actions = [
"s3:GetObject",
"s3:GetObjectVersion",
"s3:GetObjectAcl",
"s3:GetObjectVersionAcl",
"s3:ListMultipartUploadParts",
]
resources = [
"${aws_s3_bucket.example.arn}/*",
]
condition {
test = "StringEquals"
variable = "aws:ResourceAccount"
values = [data.aws_caller_identity.self.id]
}
condition {
test = "ArnEquals"
variable = "s3:AccessGrantsInstanceArn"
values = [aws_s3control_access_grants_instance.example.access_grants_instance_arn]
}
}
statement {
sid = "ObjectLevelWritePermissions"
effect = "Allow"
actions = [
"s3:PutObject",
"s3:PutObjectAcl",
"s3:PutObjectVersionAcl",
"s3:DeleteObject",
"s3:DeleteObjectVersion",
"s3:AbortMultipartUpload",
]
resources = [
"${aws_s3_bucket.example.arn}/*",
]
condition {
test = "StringEquals"
variable = "aws:ResourceAccount"
values = [data.aws_caller_identity.self.id]
}
condition {
test = "ArnEquals"
variable = "s3:AccessGrantsInstanceArn"
values = [aws_s3control_access_grants_instance.example.access_grants_instance_arn]
}
}
}
IAM Identity Center
IAM Identity Cneterでユーザを作成します。
identity_store_id
は、先に作成したIAM Identity Centerのデータソースを参照します。
resource "aws_identitystore_user" "example" {
identity_store_id = tolist(data.aws_ssoadmin_instances.example.identity_store_ids)[0]
display_name = "foo bar"
user_name = "example"
name {
given_name = "foo"
family_name = "bar"
}
emails {
value = "hoge@example.com"
}
}
resource "aws_ssoadmin_application_assignment" "example" {
depends_on = [ terraform_data.sso_admin_list_applications ]
application_arn = file("../output/application-arn.txt")
principal_id = aws_identitystore_user.example.user_id
principal_type = "USER"
}
S3バケット
今回、上記したように、コンテンツ管理システム的な扱いができないかを考察するために、Amazon S3の静的ウェブサイトホスティング機能を有効化します。
以下の例では、静的ウェブサイトホスティングを簡易に実装するためにaws_s3_bucket_public_access_block
をすべてfalse
にしていますが、セキュリティ面を考慮する場合、本来は推奨されない設定方法です。Amazon CloudFrontを前段に置いて、OACの設定でS3バケットへの直接アクセスをさせないようにする方が良いです。
resource "aws_s3_bucket" "example" {
bucket = local.s3_bucket_name
}
resource "aws_s3_bucket_ownership_controls" "example" {
bucket = aws_s3_bucket.example.id
rule {
object_ownership = "BucketOwnerEnforced"
}
}
resource "aws_s3_bucket_public_access_block" "example" {
bucket = aws_s3_bucket.example.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
resource "aws_s3_bucket_policy" "example" {
bucket = aws_s3_bucket.example.id
policy = data.aws_iam_policy_document.s3_bucket_policy.json
}
data "aws_iam_policy_document" "s3_bucket_policy" {
statement {
sid = "PublicReadGetObject"
effect = "Allow"
principals {
type = "AWS"
identifiers = ["*"]
}
actions = [
"s3:GetObject",
]
resources = [
"${aws_s3_bucket.example.arn}/*",
]
}
}
resource "aws_s3_bucket_versioning" "example" {
bucket = aws_s3_bucket.example.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_website_configuration" "example" {
bucket = aws_s3_bucket.example.id
index_document {
suffix = "index.html"
}
error_document {
key = "error.html"
}
}
S3 Access Grantsの設定
以下のようにS3 Access Grantsを有効化します。
IAM Identity Centerに対して権限を付与するために、作成時にaws_ssoadmin_instances
のデータソースを参照してください。
resource "aws_s3control_access_grants_instance" "example" {
identity_center_arn = tolist(data.aws_ssoadmin_instances.example.arns)[0]
}
resource "aws_s3control_access_grants_location" "example" {
depends_on = [aws_s3control_access_grants_instance.example]
iam_role_arn = aws_iam_role.access_grants_s3.arn
location_scope = "s3://${aws_s3_bucket.example.bucket}"
}
resource "aws_s3control_access_grant" "example" {
access_grants_location_id = aws_s3control_access_grants_location.example.access_grants_location_id
permission = "READWRITE"
access_grants_location_configuration {
s3_sub_prefix = "*"
}
grantee {
grantee_type = "DIRECTORY_USER"
grantee_identifier = aws_identitystore_user.example.user_id
}
}
AWS Transfer Family web apps
AWS Transfer Family web appsをCLIで作成します。
ただし、データソースの参照を行えるようにするために、terraform_data
(旧null_resource
)を使います。
IdentityCenterConfig
には、IAM Identity Centerのデータソースへの参照と、先に作成した、AWS Transfer用のIAMロールへの参照を行います。
なお、aws transfer create-web-app
は以下の出力をします。
{
"WebAppId": "webapp-xxxxxxxxxxxxxxxxx"
}
WebAppIdは後で使用するため、値のみ抜き出すようjqで編集してファイルに出力しておきます。
resource "terraform_data" "transfer_webapps" {
provisioner "local-exec" {
command = <<-EOT
aws transfer create-web-app \
--identity-provider-details IdentityCenterConfig={InstanceArn=${tolist(data.aws_ssoadmin_instances.example.arns)[0]},Role=${aws_iam_role.transfer.arn}} \
--web-app-units Provisioned=1 \
--tags Key=Name,Value=r-kubota \
| jq -r ".WebAppId" \
| head -c -1 \
> ../output/web-app-id.txt
EOT
}
}
また、AWS Transfer Family web appsを作成すると、IAM Identity Centerのアプリケーションとして自動で登録がされます。後で、IAM Identity Centerで作成したユーザに紐づける必要があるため、このアプリケーションのARNを取得しておきます。
AWS Transfer Family web appsは"TransferWebApp-[WebAppId]"
という一意な命名をされるため、aws sso-admin list-applications
の--filter
で絞り込みを行いたいのですが、残念ながらName属性での絞り込みは機能として存在しないので、jqのselectを使って自前フィルタを作ります。
Terraformのfile()
関数は、ファイルが存在しない状態でApplyしようとするとエラーになってしまうので、空ファイルをあらかじめ用意しておきましょう。中身が正しく設定されてから実行するよう、depends_on
を使ってリソース作成順を制御します。
resource "terraform_data" "sso_admin_list_applications" {
depends_on = [ terraform_data.transfer_webapps ]
provisioner "local-exec" {
command = <<-EOT
aws sso-admin list-applications \
--instance-arn ${tolist(data.aws_ssoadmin_instances.example.arns)[0]} \
| jq -r '.Applications[] | select(.Name == "TransferWebApp-${file("../output/web-app-id.txt")}") | .ApplicationArn' \
| head -c -1 \
> ../output/application-arn.txt
EOT
}
}
S3バケットのCORSの設定
先ほど作ったAWS Transfer Family web appsのエンドポイントを、S3バケットのCORSとして設定する必要があるため、以下のように設定します。
resource "aws_s3_bucket_cors_configuration" "example" {
depends_on = [ terraform_data.transfer_webapps ]
bucket = aws_s3_bucket.example.id
cors_rule {
allowed_headers = ["*"]
allowed_methods = ["GET", "PUT", "POST", "DELETE", "HEAD"]
allowed_origins = [ "https://${file("../output/web-app-id.txt")}.transfer-webapp.ap-northeast-1.on.aws" ]
expose_headers = [
"last-modified",
"content-length",
"etag",
"x-amz-version-id",
"content-type",
"x-amz-request-id",
"x-amz-id-2",
"date",
"x-amz-cf-id",
"x-amz-storage-class",
"access-control-expose-headers",
]
max_age_seconds = 3000
}
}
IAM Identity Centerのユーザとアプリケーションの紐づけ
IAM Identity Centerのユーザとアプリケーションをaws_ssoadmin_application_assignment
のリソースで紐づけます。
resource "aws_ssoadmin_application_assignment" "example" {
depends_on = [ terraform_data.sso_admin_list_applications ]
application_arn = file("../output/application-arn.txt")
principal_id = aws_identitystore_user.example.user_id
principal_type = "USER"
}
これで、準備は完了です。
いざ、構築する!
上記の設定まで完了したらterraform apply
をしましょう。
applyを実行した瞬間と、実際にリソースを作る瞬間で、ファイルで無理矢理参照しているデータのファイルタイムスタンプが違うためにエラーが発生して実行が停止する可能性がありますが、しつこく何回か実行すると全リソースの構築が完了します。
作成したAWS Transfer Family web appsのエンドポイントにアクセスすると、以下のようにバケット選択画面が表示されます。
ここから、アップロードをしてみましょう。
良い感じです!
これで、ファイアップローダを簡単に実装できるようになりました!
IAM Identity Centerでは、アイデンティティソースとして外部IDプロバイダを使うこともできます。
既にOktaやAuth0でのSSOをしている場合は、そこと統合したアップローダにすることも簡単にできます。
「よくある要件」をマネージドサービスに寄せることができる素晴らしいサービスだと思います。
コンテンツ管理はできそうか?
さて、最後に、AWS Transfer Family web appsをコンテンツ管理システムとして扱えるかについての考察です。
結論から言うと、素のままでは扱えません。
いわゆるCMSにあるような予約・プレビュー・ロールバックという機能がないことは(そもそもの目的が違うため)仕方がないとして、AWS Transfer Family web appsでアップロードしたオブジェクトは、Content-Typeがapplication/octet-stream固定になってしまいます。
マネージメントコンソール用にファジーに判断してくれるわけではなさそうです。
このため、格納用バケットと公開用をバケットを分けてAmazon S3トリガーを使ってLambdaでContent-Typeを指定してオブジェクトをコピーするか、CloudFront FunctionsでレスポンスヘッダーのContent-Typeを書き換える(未検証)かが必要になります。
メタデータをアップロード時に指定できるようになると、さらにユースケースが拡がるかもしれません。