ALBとLambdaを使ってS3に配置された静的ページを表示してみた
ALBを使用してメンテナンス画面を表示する際に固定レスポンスをご利用いただく方法があります。
ただし、こちらの方法はレスポンスとして返せる文字数が1024文字となっているためCSSやJavaScriptなどで装飾されているような画面だと使用するのが難しいものとなります。
今回は固定レスポンスの制限を回避するためALBのターゲットにLambdaを設定してS3に配置されたHTMLファイルを配信する設定を試してみました。
構成
今回作成する構成は以下のようになります。
通常時のリクエストは全てEC2 (Webサーバー) へ送り、メンテナンスを行う際はALBのリスナールールの優先順位を変更して全てのパスへのリクエストをLambdaへ送るようにします。
設定
リソースの作成は以下のCloudFormationテンプレートを使用します。
AWSTemplateFormatVersion: "2010-09-09"
Description: ALB,EC2,Lambda,S3
Metadata:
# ------------------------------------------------------------#
# Metadata
# ------------------------------------------------------------#
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: Parameters for env Name
Parameters:
- env
- Label:
default: Parameters for Network
Parameters:
- VPCCIDR
- PublicSubnet01CIDR
- PublicSubnet02CIDR
- PrivateSubnet01CIDR
- PrivateSubnet02CIDR
- Label:
default: Parameters for EC2
Parameters:
- EC2VolumeSize
- EC2VolumeIOPS
- EC2AMI
- EC2InstanceType
Parameters:
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
env:
Type: String
Default: dev
AllowedValues:
- dev
VPCCIDR:
Default: 192.168.0.0/16
Type: String
PublicSubnet01CIDR:
Default: 192.168.0.0/24
Type: String
PublicSubnet02CIDR:
Default: 192.168.1.0/24
Type: String
PrivateSubnet01CIDR:
Default: 192.168.2.0/24
Type: String
PrivateSubnet02CIDR:
Default: 192.168.3.0/24
Type: String
EC2VolumeSize:
Default: 32
Type: Number
EC2VolumeIOPS:
Default: 3000
Type: Number
EC2AMI:
Default: '/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-x86_64'
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
EC2InstanceType:
Default: t3.micro
Type: String
Resources:
# ------------------------------------------------------------#
# IAM
# ------------------------------------------------------------#
EC2IAMRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service:
- ec2.amazonaws.com
Action:
- 'sts:AssumeRole'
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
RoleName: !Sub iam-${env}-ec2-web-role
Tags:
- Key: Name
Value: !Sub iam-${env}-ec2-web-role
EC2IAMInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
InstanceProfileName: !Sub iam-${env}-ec2-web-instanceprofile
Roles:
- !Ref EC2IAMRole
# ------------------------------------------------------------#
# VPC
# ------------------------------------------------------------#
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: !Ref VPCCIDR
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Sub vpc-${env}
# ------------------------------------------------------------#
# InternetGateway
# ------------------------------------------------------------#
InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: !Sub igw-${env}
InternetGatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
InternetGatewayId: !Ref InternetGateway
VpcId: !Ref VPC
# ------------------------------------------------------------#
# Subnet
# ------------------------------------------------------------#
PublicSubnet01:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-1a
CidrBlock: !Ref PublicSubnet01CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub subnet-${env}-pub1
VpcId: !Ref VPC
PublicSubnet02:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-1c
CidrBlock: !Ref PublicSubnet02CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub subnet-${env}-pub2
VpcId: !Ref VPC
PrivateSubnet01:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-1a
CidrBlock: !Ref PrivateSubnet01CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub subnet-${env}-prv1
VpcId: !Ref VPC
PrivateSubnet02:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: ap-northeast-1c
CidrBlock: !Ref PrivateSubnet02CIDR
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub subnet-${env}-prv2
VpcId: !Ref VPC
PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: rtb-${env}-pub
PublicRouteTableRoute:
Type: AWS::EC2::Route
Properties:
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway
RouteTableId: !Ref PublicRouteTable
PublicRtAssociation1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnet01
PublicRtAssociation2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PublicRouteTable
SubnetId: !Ref PublicSubnet02
PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC
Tags:
- Key: Name
Value: rtb-${env}-prv
PrivateRtAssociation1:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTable
SubnetId: !Ref PrivateSubnet01
PrivateRtAssociation2:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId: !Ref PrivateRouteTable
SubnetId: !Ref PrivateSubnet02
# ------------------------------------------------------------#
# SecurityGroup
# ------------------------------------------------------------#
ALBSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: for alb
GroupName: !Sub securitygroup-${env}-alb
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
FromPort: -1
IpProtocol: -1
ToPort: -1
SecurityGroupIngress:
- FromPort: 80
IpProtocol: tcp
CidrIp: 0.0.0.0/0
ToPort: 80
Tags:
- Key: Name
Value: !Sub securitygroup-${env}-alb
VpcId: !Ref VPC
EC2SG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: for ec2
GroupName: !Sub securitygroup-${env}-ec2
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
FromPort: -1
IpProtocol: -1
ToPort: -1
SecurityGroupIngress:
- FromPort: 80
IpProtocol: tcp
SourceSecurityGroupId: !Ref ALBSG
ToPort: 80
Tags:
- Key: Name
Value: !Sub securitygroup-${env}-ec2
VpcId: !Ref VPC
VPCEndpointSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: for vpc endpoint
GroupName: !Sub securitygroup-${env}-vpc-endpoint
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
FromPort: -1
IpProtocol: -1
ToPort: -1
SecurityGroupIngress:
- FromPort: 443
IpProtocol: tcp
SourceSecurityGroupId: !Ref EC2SG
ToPort: 443
Tags:
- Key: Name
Value: !Sub securitygroup-${env}-vpc-endpoint
VpcId: !Ref VPC
# ------------------------------------------------------------#
# VPC Endpoint
# ------------------------------------------------------------#
S3Endpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
RouteTableIds:
- !Ref PrivateRouteTable
ServiceName: !Sub com.amazonaws.${AWS::Region}.s3
VpcEndpointType: Gateway
VpcId: !Ref VPC
SystemsManagerEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: true
ServiceName: !Sub com.amazonaws.${AWS::Region}.ssm
VpcId: !Ref VPC
SubnetIds:
- !Ref PrivateSubnet01
SecurityGroupIds:
- !Ref VPCEndpointSG
SystemsManagerMessageEndpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcEndpointType: Interface
PrivateDnsEnabled: true
ServiceName: !Sub com.amazonaws.${AWS::Region}.ssmmessages
VpcId: !Ref VPC
SubnetIds:
- !Ref PrivateSubnet01
SecurityGroupIds:
- !Ref VPCEndpointSG
# ------------------------------------------------------------#
# EC2
# ------------------------------------------------------------#
EC2:
Type: AWS::EC2::Instance
Properties:
BlockDeviceMappings:
- DeviceName: /dev/xvda
Ebs:
DeleteOnTermination: true
Encrypted: true
Iops: !Ref EC2VolumeIOPS
VolumeSize: !Ref EC2VolumeSize
VolumeType: gp3
DisableApiTermination: false
IamInstanceProfile: !Ref EC2IAMInstanceProfile
ImageId: !Ref EC2AMI
InstanceType: !Ref EC2InstanceType
NetworkInterfaces:
- AssociatePublicIpAddress: false
DeleteOnTermination: true
DeviceIndex: 0
GroupSet:
- !Ref EC2SG
SubnetId: !Ref PrivateSubnet01
Tags:
- Key: Name
Value: !Sub ec2-${env}-web
UserData: !Base64 |
#!/bin/bash
dnf install httpd -y
echo "Web Test" > /var/www/html/index.html
systemctl start httpd
systemctl enable httpd
# ------------------------------------------------------------#
# S3
# ------------------------------------------------------------#
S3:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub s3-${env}-html-${AWS::AccountId}-${AWS::Region}-001
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
BucketKeyEnabled: false
OwnershipControls:
Rules:
- ObjectOwnership: BucketOwnerEnforced
# ------------------------------------------------------------#
# Lambda
# ------------------------------------------------------------#
LambdaIAMRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: !Sub policy-${env}-lambda
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- s3:GetObject
Resource: "*"
Lambda:
Type: AWS::Lambda::Function
Properties:
Code:
ZipFile: |
import boto3
import os
def lambda_handler(event, context):
# S3設定
bucket_name = os.environ['BUCKET_NAME']
html_key = "503.html"
# S3からHTMLファイルを取得
s3_client = boto3.client('s3')
response = s3_client.get_object(Bucket=bucket_name, Key=html_key)
html_content = response['Body'].read().decode('utf-8')
# 503ステータスでHTMLレスポンスを返す
return {
"statusCode": 503,
"statusDescription": "503 Service Unavailable",
"headers": {
"Content-Type": "text/html;",
},
"body": html_content,
"isBase64Encoded": False
}
Environment:
Variables:
BUCKET_NAME: !Ref S3
FunctionName: !Sub lambda-${env}-s3-download
Handler: index.lambda_handler
Role: !GetAtt LambdaIAMRole.Arn
Runtime: python3.13
Timeout: 30
LambdaPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !GetAtt Lambda.Arn
Principal: elasticloadbalancing.amazonaws.com
# ------------------------------------------------------------#
# ALB
# ------------------------------------------------------------#
ALB:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
IpAddressType: ipv4
LoadBalancerAttributes:
- Key: deletion_protection.enabled
Value: false
Name: !Sub alb-${env}-ec2
Scheme: internet-facing
SecurityGroups:
- !Ref ALBSG
Subnets:
- !Ref PublicSubnet01
- !Ref PublicSubnet02
Tags:
- Key: Name
Value: !Sub alb-${env}-ec2
Type: application
TargetGroupEC2:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckEnabled: true
HealthCheckIntervalSeconds: 30
HealthCheckPath: /
HealthCheckPort: traffic-port
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 5
HealthyThresholdCount: 5
IpAddressType: ipv4
Matcher:
HttpCode: 200
Name: !Sub tg-${env}-ec2
Port: 80
Protocol: HTTP
ProtocolVersion: HTTP1
Tags:
- Key: Name
Value: !Sub tg-${env}-ec2
Targets:
- Id: !Ref EC2
Port: 80
TargetType: instance
UnhealthyThresholdCount: 2
VpcId: !Ref VPC
TargetGroupLambda:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
Matcher:
HttpCode: 200
Name: !Sub tg-${env}-lambda
Tags:
- Key: Name
Value: !Sub tg-${env}-lambda
Targets:
- Id: !GetAtt Lambda.Arn
TargetType: lambda
ALBHTTPListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- FixedResponseConfig:
ContentType: text/plain
MessageBody: "Service Unavailable"
StatusCode: "503"
Type: fixed-response
LoadBalancerArn: !Ref ALB
Port: 80
Protocol: HTTP
ALBHTTPEC2ListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- TargetGroupArn: !Ref TargetGroupEC2
Type: forward
Conditions:
- Field: path-pattern
Values:
- /*
ListenerArn: !Ref ALBHTTPListener
Priority: 1
ALBHTTPLambdaListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- TargetGroupArn: !Ref TargetGroupLambda
Type: forward
Conditions:
- Field: path-pattern
Values:
- /*
ListenerArn: !Ref ALBHTTPListener
Priority: 2
上記のCloudFormationテンプレートではIAMロール、S3、ALB、EC2、Lambdaが作成されます。
以下のAWS CLIコマンドでデプロイを行います。
aws cloudformation create-stack --stack-name スタック名 --template-body file://CloudFormationテンプレートファイル名 --capabilities CAPABILITY_NAMED_IAM
リソースの作成ができたらS3バケット (s3-dev-html-アカウントID-ap-northeast-1-001) に503.htmlというファイル名でHTMLファイルをアップロードしてください。
今回はClaudeにサンプルのHTMLファイルを作成してもらいました。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>メンテナンス中</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: #333;
}
.maintenance-container {
background: white;
padding: 3rem 2rem;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
text-align: center;
max-width: 500px;
width: 90%;
}
.maintenance-icon {
font-size: 4rem;
margin-bottom: 1.5rem;
animation: bounce 2s infinite;
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-10px);
}
60% {
transform: translateY(-5px);
}
}
h1 {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 1rem;
color: #2d3748;
}
.message {
font-size: 1.1rem;
line-height: 1.6;
color: #4a5568;
margin-bottom: 2rem;
}
.date-info {
font-size: 1rem;
color: #4a5568;
margin-bottom: 1.5rem;
font-weight: 500;
}
.contact-info {
font-size: 0.9rem;
color: #666;
margin-top: 1rem;
}
@media (max-width: 480px) {
.maintenance-container {
padding: 2rem 1.5rem;
}
h1 {
font-size: 2rem;
}
.maintenance-icon {
font-size: 3rem;
}
}
</style>
</head>
<body>
<div class="maintenance-container">
<div class="maintenance-icon">🔧</div>
<h1>メンテナンス中</h1>
<p class="message">
現在システムのメンテナンスを実施しております。
</p>
<div class="date-info" id="current-date"></div>
</div>
<script>
function formatCurrentDate() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
return `${year}年${month}月${day}日`;
}
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('current-date').textContent = formatCurrentDate();
});
</script>
</body>
</html>
Lambdaの説明
今回作成したLambdaのコードは以下になります。
ALBからLambdaへのアクセスが発生したら503.htmlをS3バケットからget_objectで取得してきます。
その後、HTTPステータスコード503でHTMLファイルをレスポンスとして返すようにしています。
import boto3
import os
def lambda_handler(event, context):
# S3設定
bucket_name = os.environ['BUCKET_NAME']
html_key = "503.html"
# S3からHTMLファイルを取得
s3_client = boto3.client('s3')
response = s3_client.get_object(Bucket=bucket_name, Key=html_key)
html_content = response['Body'].read().decode('utf-8')
# 503ステータスでHTMLレスポンスを返す
return {
"statusCode": 503,
"statusDescription": "503 Service Unavailable",
"headers": {
"Content-Type": "text/html;",
},
"body": html_content,
"isBase64Encoded": False
}
動作確認
まずは、ALBのリスナールールを何も変更していない状態でブラウザからアクセスします。
何も変更をしていなければEC2へアクセスするため以下のように「Web Test」というシンプルなレスポンスが確認できます。
次にリスナールールを編集してLambdaがターゲットになっているルールの優先順位を上げてからアクセスしてみます。
ロードバランサーの一覧画面にアクセスして「alb-dev-ec2」を選択後、リスナーとルールのタブをクリックします。
クリック後、リスナールールから「HTTP:80」をクリックします。
「HTTP:80」のリスナールールを開いたらアクションからルールの優先順位を再設定をクリックします。
クリックしたら、tg-dev-lambdaが転送先になっているリスナールールの優先順位を1に変更してください。
変更したら変更内容の保存をクリックします。
変更が完了したら再度ALBのDNS名にブラウザからアクセスすると作成したメンテナンスページが表示されます。
さいごに
今回はALBからメンテナンスページを表示する方法を試してみました。
ALBの固定レスポンスの制限を超えるような装飾されたページを配信したい時はLambdaを使用する方法もあるというのを頭の片隅に入れておいてもよいと思いました。