この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
おはようございます、加藤です。 昔話から始まりますが、2018年7月のアップデートでAWS Application Load Balancer(以降、ALB)のリスナールールでリダイレクトを行えるようになりました。
当時は条件として以下をサポートしていました。
- ホストヘッダー
- パスパターン
[新機能]Webサーバでの実装不要!ALBだけでリダイレクト出来るようになりました! | Developers.IO
その後、2019年3月にアップデートがあり、下記も条件としてサポートされました。
- HTTPヘッダー
- HTTPリクエストメソッド
- クエリ文字列
- 送信元IPアドレス
[新機能] HTTPヘッダーやクエリ文字列などなどでルーティングができちゃう!!AWS ALBで高度なリクエストルーティングが可能になりました! | Developers.IO
条件としては、リダイレクトの際に下記を任意の固定値に指定する事が可能です。
- ホスト
- パス
- クエリ
さらに、予約済みキーワードとして下記を使用できます。
- protocol 元のプロトコルを保持。プロトコルとクエリのコンポーネントで使用できる。
- host 元のドメインを保持。ホスト名、パス、およびクエリのコンポーネントで使用できる。
- port 元のポートを保持。ポート、パス、およびクエリのコンポーネントで使用できる。
- path 元のパスを保持。パスとクエリのコンポーネントで使用できる。
- query 元のクエリパラメータを保持。クエリのコンポーネントで使用する。
これらによって、例えば以下のような処理をALBに行わせる事が可能でした。
- httpアクセスをhttpsにリダイレクト
- 一時的にルールを作成しメンテナンス中でもオフィス(特定IPアドレス)からはアクセス可能
- マイクロサービスの為にhttpメソッドに応じてルーティング
しかし、元々のパス等を書き換える様なリダイレクトは現状では行えません。
- OK: /before/path → /after/before/path
- NG: /before/path → /after/path
こういった要件は、2018年のre:Inventで発表されたALBに関連付くターゲットグループにAWS Lambda Function(以降、Lambda関数)を使用する事で満たすことが可能です。
ALBのバックエンドにLambdaを選択してみた! #reinvent | Developers.IO
要件の整理
行いたいリダイレクトは下記の通りです。
これを実現する為には、Lambda関数が関連付いたターゲットグループを追加し、リスナールールにパスが/before/*
の場合に、このターゲットグループへルーティングする用に設定すれば良いです。
やってみた
疑似環境の作成
検証する為に、コア部分以外の疑似環境を作成します。
マネージメントコンソールやCloud Formation(以降、CFn)から作成しても良いでの最近、AWS CDK(以降、CDK)がマイブームなのでCDKを使用します。CDKに慣れている方はCDKを、そうでない方はCDKで生成したCFnテンプレートも記載するので、そちらを使用して疑似環境を作成してください。
CDKのStack部分
lib/alb-stack.ts
import * as cdk from "@aws-cdk/core";
import {Peer, Port, SecurityGroup, SubnetType, Vpc} from "@aws-cdk/aws-ec2";
import {ApplicationLoadBalancer as Alb, ContentType} from "@aws-cdk/aws-elasticloadbalancingv2";
import {CfnOutput} from "@aws-cdk/core";
export class AlbStack extends cdk.Stack {
constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const vpc = new Vpc(this, 'PublicOnlyVpc', {
maxAzs: 2,
natGateways: 0,
subnetConfiguration: [{name: 'Public', subnetType: SubnetType.PUBLIC}]
});
const securityGroup = new SecurityGroup(this, 'Sg', {vpc, allowAllOutbound: true});
securityGroup.addIngressRule(Peer.anyIpv4(), Port.tcp(80), 'allow http access from any ipv4');
const vpcSubnets = {subnets: vpc.publicSubnets};
const alb = new Alb(this, 'Alb', {vpc, securityGroup, vpcSubnets, internetFacing: true});
const listener = alb.addListener('Listener', {port: 80, open: true});
listener.addFixedResponse('DefaultResponse', {
pathPattern: '/*',
priority: 0,
contentType: ContentType.TEXT_PLAIN,
messageBody: 'This is default page',
statusCode: '200'
});
listener.addFixedResponse('RedirectedResponse', {
pathPattern: '/after/*',
priority: 1,
contentType: ContentType.TEXT_PLAIN,
messageBody: 'Redirected!!',
statusCode: '200'
});
new CfnOutput(this, "AlbDnsName", {
value: alb.loadBalancerDnsName,
description: 'Application Load Balancer\'s dns name'
})
}
}
生成されたCFnテンプレート
generated_template.yaml
Resources:
PublicOnlyVpc70520523:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsHostnames: true
EnableDnsSupport: true
InstanceTenancy: default
Tags:
- Key: Name
Value: AlbStack/PublicOnlyVpc
Metadata:
aws:cdk:path: AlbStack/PublicOnlyVpc/Resource
PublicOnlyVpcPublicSubnet1Subnet95BA5FA7:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.0.0/17
VpcId:
Ref: PublicOnlyVpc70520523
AvailabilityZone: ap-northeast-1a
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: AlbStack/PublicOnlyVpc/PublicSubnet1
- Key: aws-cdk:subnet-name
Value: Public
- Key: aws-cdk:subnet-type
Value: Public
Metadata:
aws:cdk:path: AlbStack/PublicOnlyVpc/PublicSubnet1/Subnet
PublicOnlyVpcPublicSubnet1RouteTableCB078C72:
Type: AWS::EC2::RouteTable
Properties:
VpcId:
Ref: PublicOnlyVpc70520523
Tags:
- Key: Name
Value: AlbStack/PublicOnlyVpc/PublicSubnet1
Metadata:
aws:cdk:path: AlbStack/PublicOnlyVpc/PublicSubnet1/RouteTable
PublicOnlyVpcPublicSubnet1RouteTableAssociation87A06197:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId:
Ref: PublicOnlyVpcPublicSubnet1RouteTableCB078C72
SubnetId:
Ref: PublicOnlyVpcPublicSubnet1Subnet95BA5FA7
Metadata:
aws:cdk:path: AlbStack/PublicOnlyVpc/PublicSubnet1/RouteTableAssociation
PublicOnlyVpcPublicSubnet1DefaultRoute330AD2F8:
Type: AWS::EC2::Route
Properties:
RouteTableId:
Ref: PublicOnlyVpcPublicSubnet1RouteTableCB078C72
DestinationCidrBlock: 0.0.0.0/0
GatewayId:
Ref: PublicOnlyVpcIGW5FDE497C
DependsOn:
- PublicOnlyVpcVPCGW6D24F5B6
Metadata:
aws:cdk:path: AlbStack/PublicOnlyVpc/PublicSubnet1/DefaultRoute
PublicOnlyVpcPublicSubnet2Subnet14102853:
Type: AWS::EC2::Subnet
Properties:
CidrBlock: 10.0.128.0/17
VpcId:
Ref: PublicOnlyVpc70520523
AvailabilityZone: ap-northeast-1c
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: AlbStack/PublicOnlyVpc/PublicSubnet2
- Key: aws-cdk:subnet-name
Value: Public
- Key: aws-cdk:subnet-type
Value: Public
Metadata:
aws:cdk:path: AlbStack/PublicOnlyVpc/PublicSubnet2/Subnet
PublicOnlyVpcPublicSubnet2RouteTable26AC6139:
Type: AWS::EC2::RouteTable
Properties:
VpcId:
Ref: PublicOnlyVpc70520523
Tags:
- Key: Name
Value: AlbStack/PublicOnlyVpc/PublicSubnet2
Metadata:
aws:cdk:path: AlbStack/PublicOnlyVpc/PublicSubnet2/RouteTable
PublicOnlyVpcPublicSubnet2RouteTableAssociation1626B7EE:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
RouteTableId:
Ref: PublicOnlyVpcPublicSubnet2RouteTable26AC6139
SubnetId:
Ref: PublicOnlyVpcPublicSubnet2Subnet14102853
Metadata:
aws:cdk:path: AlbStack/PublicOnlyVpc/PublicSubnet2/RouteTableAssociation
PublicOnlyVpcPublicSubnet2DefaultRoute56ACD62D:
Type: AWS::EC2::Route
Properties:
RouteTableId:
Ref: PublicOnlyVpcPublicSubnet2RouteTable26AC6139
DestinationCidrBlock: 0.0.0.0/0
GatewayId:
Ref: PublicOnlyVpcIGW5FDE497C
DependsOn:
- PublicOnlyVpcVPCGW6D24F5B6
Metadata:
aws:cdk:path: AlbStack/PublicOnlyVpc/PublicSubnet2/DefaultRoute
PublicOnlyVpcIGW5FDE497C:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- Key: Name
Value: AlbStack/PublicOnlyVpc
Metadata:
aws:cdk:path: AlbStack/PublicOnlyVpc/IGW
PublicOnlyVpcVPCGW6D24F5B6:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId:
Ref: PublicOnlyVpc70520523
InternetGatewayId:
Ref: PublicOnlyVpcIGW5FDE497C
Metadata:
aws:cdk:path: AlbStack/PublicOnlyVpc/VPCGW
SgD4954771:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: AlbStack/Sg
SecurityGroupEgress:
- CidrIp: 0.0.0.0/0
Description: Allow all outbound traffic by default
IpProtocol: "-1"
SecurityGroupIngress:
- CidrIp: 0.0.0.0/0
Description: allow http access from any ipv4
FromPort: 80
IpProtocol: tcp
ToPort: 80
VpcId:
Ref: PublicOnlyVpc70520523
Metadata:
aws:cdk:path: AlbStack/Sg/Resource
Alb16C2F182:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Scheme: internet-facing
SecurityGroups:
- Fn::GetAtt:
- SgD4954771
- GroupId
Subnets:
- Ref: PublicOnlyVpcPublicSubnet1Subnet95BA5FA7
- Ref: PublicOnlyVpcPublicSubnet2Subnet14102853
Type: application
DependsOn:
- PublicOnlyVpcPublicSubnet1DefaultRoute330AD2F8
- PublicOnlyVpcPublicSubnet2DefaultRoute56ACD62D
Metadata:
aws:cdk:path: AlbStack/Alb/Resource
AlbListener86261768:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- FixedResponseConfig:
ContentType: text/plain
MessageBody: This is default page
StatusCode: "200"
Type: fixed-response
LoadBalancerArn:
Ref: Alb16C2F182
Port: 80
Protocol: HTTP
Metadata:
aws:cdk:path: AlbStack/Alb/Listener/Resource
AlbListenerRedirectedResponseRule8BCB50AE:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- FixedResponseConfig:
ContentType: text/plain
MessageBody: Redirected!!
StatusCode: "200"
Type: fixed-response
Conditions:
- Field: path-pattern
Values:
- /after/*
ListenerArn:
Ref: AlbListener86261768
Priority: 1
Metadata:
aws:cdk:path: AlbStack/Alb/Listener/RedirectedResponseRule/Resource
CDKMetadata:
Type: AWS::CDK::Metadata
Properties:
Modules: aws-cdk=1.20.0,@aws-cdk/aws-cloudwatch=1.20.0,@aws-cdk/aws-ec2=1.20.0,@aws-cdk/aws-elasticloadbalancingv2=1.20.0,@aws-cdk/aws-iam=1.20.0,@aws-cdk/aws-ssm=1.20.0,@aws-cdk/core=1.20.0,@aws-cdk/cx-api=1.20.0,@aws-cdk/region-info=1.20.0,jsii-runtime=node.js/v12.14.1
Outputs:
AlbDnsName:
Description: Application Load Balancer's dns name
Value:
Fn::GetAtt:
- Alb16C2F182
- DNSName
CFnスタックの作成が完了すると、ALBのDNS名がアウトプットされます。後ほど使用するのでメモしてください。
Lambda関数を使用して高度なリダイレクトを行う
Lambda関数を新規作成します。 関数名は任意、ランタイムはNode.js 12.xを選択し、関数の作成を選択します。
インラインでコードを編集します。下記をコピー&ペーストしてください。 また、処理内容にはコメントを確認してください。
index.js
const dnsName = process.env["DNS_NAME"];
exports.handler = async (event) => {
// パスを配列として取得し、先頭から2要素を削除する
// 例: "/before/path1/"" → [""", "before", "path1", ""] → ["path1", ""]
const paths = event.path.split("/");
paths.splice(0, 2);
// パス配列を"/"区切りで結合して文字列にする
const suffix = paths.join("/");
// Alb用にレスポンスを生成する
const response = {
statusCode: 301,
headers: {
"Location": `http://${dnsName}/after/${suffix}`
}
};
return response;
};
ちなみに、Google Chrome 79.0.3945.117からアクセスした場合、ALBからLambda関数は下記のイベントを受け取ります。(アカウント番号など一部置換しています)
sample-event.json
{
"requestContext": {
"elb": {
"targetGroupArn": "arn:aws:elasticloadbalancing:ap-northeast-1:${AWS_ACCOUNT_ID}:targetgroup/Redirect/01d9489ef8b930f7"
}
},
"httpMethod": "GET",
"path": "/before/path1/path2",
"queryStringParameters": {},
"headers": {
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
"accept-encoding": "gzip, deflate",
"accept-language": "ja,en-US;q=0.9,en;q=0.8",
"connection": "keep-alive",
"host": "${DNS_NAME}",
"upgrade-insecure-requests": "1",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36",
"x-amzn-trace-id": "Root=1-5e172729-9ba14e2060707d40b9abcca0",
"x-forwarded-for": "192.0.2.1",
"x-forwarded-port": "80",
"x-forwarded-proto": "http"
},
"body": "",
"isBase64Encoded": false
}
環境変数を1つ設定します、キーをDNS_NAMEとし、バリューはCFnスタック作成完了時に出力されたALBのDNS名に設定してください。
ターゲットグループを作成します。
ターゲットグループ名は任意、ターゲットの種類はLambda 関数で作成したLambda関数を選択します。 今回は、検証なのでバージョン:$LATESTを選択しています。 プロダクション環境で利用する場合は、Lambda関数作成時にバージョンの発行と発行したバージョンに対してエイリアスを設定し、それを選択する事を強くおすすめします。 バージョン:$LATESTを選択している場合、Lambda関数に対する変更が即座に適用されてしまいます。 さらに、一度ターゲットグループにLambda関数を関連付けると後から別の関数・バージョン指定・エイリアス指定をする事はできません。 変更する度にバージョンを発行し、例えばRELEASEとエイリアスを設定し、バージョンが発行される度に、エイリアスの参照バージョンを更新する事で安全に改修ができ、切り戻しも迅速に行えます。
ALBのリスナールールに作成したターゲットグループを関連付けます。 リスナータブを開き、デフォルトリスナーのルールの表示/編集を選択します。
+タブを開き、+ルールの挿入を選択すると、ルールが編集可能になります。 +条件の追加を選択しパス...を選び、/before/*の場合を条件に指定しチェックアイコンを選択して確定します。 +アクションの追加を選択し転送先...を選び、ターゲットグループに作成したターゲットグループを指定しチェックアイコンを選択して確定します。 最後に保存を選択します。
動作確認
まず、curl
コマンドを使って動作を確認します。
DNS_NAME=[ALBのDNS名]
curl -v http://${DNS_NAME}/before/path
* Trying 192.0.2.1...
* TCP_NODELAY set
* Connected to ${DNS_NAME} (192.0.2.1) port 80 (#0)
> GET /before/path HTTP/1.1
> Host: ${DNS_NAME}
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
< Server: awselb/2.0
< Date: Thu, 09 Jan 2020 15:16:26 GMT
< Content-Type: application/octet-stream
< Content-Length: 0
< Connection: keep-alive
< Location: http://${DNS_NAME}/after/path
<
* Connection #0 to host ${DNS_NAME} left intact
* Closing connection 0
意図した通り、http://${DNS_NAME}/after/path
にリダイレクトされています!
ブラウザでもhttp://${DNS_NAME}/before/path
にアクセスすると、http://${DNS_NAME}/after/path
にリダイレクトされる事を確認できました!
あとがき
想像力が足りないよと言われてしまいそうですが、ALBのターゲットグループでLambda関数が使用できる用にアップデートされた時は、「誰が喜ぶんだ...?」という感想でした。同じDNS名でALB配下のサービスとLambda関数を併用できますが、「サブドメイン作成して、API Gateway & Lambdaでやれば良いのでは?」と思っていました。 しかし、こういった高度なリダイレクトという用途だと、わざわざAPI Gatewayを作る程ではなく、かといってALBの機能では実現できませんね。 以上でした!!