概要
S3にアップロードしたJSファイルのContent-Typeを変更するスクリプトを、Lambdaを使って実装しました。
この記事では、その方法を共有します。
動機
静的サイトをAWS S3でホスティングしようとした際、
フロントエンジニアにIAMのアクセスキーを渡してS3にサイトのソースコード(HTML, CSS, JSファイルなど)をアップロードしてもらいましたが、
JSファイルのContent-Typeがbinary/octet-stream
に設定されてしまい、サイトが正常に動作しないという問題が発生しました。
原因
コマンドラインからS3にファイルをアップロードする際に、Content-Type を明示的に指定しないと、AWSがファイルの拡張子に基づいて自動的にContent-Typeを推測します。
この推測が正しく行われない場合があり、特にJSファイルでこの問題が発生するようです。
対応
本来であれば、S3にファイルをアップロードする際に正しいContent-Typeを指定すべきですが、フロントエンジニア側の設定変更ができない状況だったため、サーバサイドで対応することにしました。
S3へのファイルアップロードをトリガーに、AWS Lambda関数を使ってJSファイルのContent-Typeを書き換える処理を実装してます。
また、このLambda関数の管理はTerraformを使用しています。
注意点
この実装では、S3にファイルがアップロードされるたびにLambda関数がトリガーされるため、大量のファイルをアップロードするとLambdaの実行コストが増加する可能性があるので注意が必要です。
コード
lambda
# lambda-replace-content_type.py
import boto3
s3 = boto3.client('s3')
def lambda_handler(event, context):
# S3イベントからバケット名とオブジェクトキーを取得
bucket_name = event['Records'][0]['s3']['bucket']['name']
object_key = event['Records'][0]['s3']['object']['key']
# Content-Typeを変更
replace_content_type(bucket_name, object_key)
# S3にアップロードされたオブジェクトのContent-Typeを変更する関数
def replace_content_type(bucket_name, object_key):
try:
# 現在のオブジェクトのメタデータを取得
response = s3.head_object(Bucket=bucket_name, Key=object_key)
metadata = response['Metadata']
# content-typeがbinary/octet-stream以外だった場合、正しい設定がされているとして終了
if response['ContentType'] != 'binary/octet-stream':
return {}
# 拡張子によってContent-Typeを判定
ext = os.path.splitext(object_key)[1][1:]
if ext == 'js':
# 今の所JSファイルのみ問題になっているので、JSファイルの場合のみContent-Typeを変更
content_type = 'text/javascript'
else:
# jsファイル以外はoctet-streamのままにしておく
content_type = 'binary/octet-stream'
# オブジェクトの置き換え(Content-Typeを変更)
s3.copy_object(
Bucket=bucket_name,
CopySource={'Bucket': bucket_name, 'Key': object_key},
Key=object_key,
ContentType=content_type,
Metadata=metadata,
MetadataDirective='REPLACE' # メタデータの置き換えを指示
)
except Exception as e:
print(e)
terraform
#####
# Lambda関数
#####
# terraform実行時にsource_fileのスクリプトをzip化する。
data "archive_file" "lambda-src-zip" {
type = "zip"
source_file = "src/lambda-replace-content_type.py"
output_path = "src/lambda-replace-content_type.zip"
}
resource "aws_lambda_function" "replace_content_type" {
function_name = "replace_content_type"
role = aws_iam_role.lambda_s3_role.arn
handler = "lambda-replace-content_type.lambda_handler"
runtime = "python3.9"
filename = data.archive_file.lambda-src-zip.output_path
source_code_hash = data.archive_file.lambda-src-zip.output_base64sha256
}
#####
# ポリシー周り
#####
# lambdaのロール
resource "aws_iam_role" "lambda_s3_role" {
name = "lambda_s3_role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
}
]
})
}
# lambdaのロールにs3のアクセスを制御するポリシーをアタッチ
resource "aws_iam_role_policy_attachment" "lambda_s3_policy_attachment" {
policy_arn = aws_iam_policy.lambda_s3_policy.arn
role = aws_iam_role.lambda_s3_role.name
}
resource "aws_iam_policy" "lambda_s3_policy" {
name = "lambda_s3_policy"
description = "Policy to allow Lambda to perform GetObject, PutObject, and CopyObject actions on S3"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:PutObject",
"s3:CopyObject"
]
Resource = [
"arn:aws:s3:::my-bucket/*", # ここで具体的なARNを指定
]
}
]
})
}
#####
# Lambdaのトリガー設定
#####
# トリガー対象のイベントを定義
resource "aws_s3_bucket_notification" "s3-notification" {
bucket = module.static-hosting.website-bucket.name
lambda_function {
lambda_function_arn = aws_lambda_function.replace_content_type.arn
# s3:ObjectCreated:CopyはLambda側で実行するため、ここでは指定しない(ループするかもしれないから)
events = [
"s3:ObjectCreated:Put",
"s3:ObjectCreated:Post",
"s3:ObjectCreated:CompleteMultipartUpload"
]
}
depends_on = [aws_lambda_permission.allow-s3-invoke]
}
# Lambdaが特定サービス(S3とか)によって実行される際の権限を定義
resource "aws_lambda_permission" "allow-s3-invoke" {
statement_id = "AllowS3InvokeProd"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.replace_content_type.function_name
principal = "s3.amazonaws.com"
source_arn = "arn:aws:s3:::my-bucket" # ここで具体的なARNを指定
}