ALB + Lambda 構成でAPIキー認証を実装する方法を試してみた
製造ビジネステクノロジー部の小林です。
AWS で Web API を構築する際、「ALB + Lambda」 の構成は、タイムアウト制限(API Gatewayの29秒)を回避したい場合や、既存のALB環境を利用したい場合に有力な選択肢となります。
しかし、ALB には API Gateway のような「管理されたAPIキー機能」が標準ではありません。 そこで今回は AWS CDK を使って ALB + Lambda 環境を構築し、以下の2つの方法でAPIキー認証を実装・比較してみます。
- ALB リスナールール
- AWS WAF
前提条件
- AWS CDK v2(TypeScript)
- Node.js (v22系)
- express
- AWS CLI
事前準備:APIキーの登録
APIキーをコードにベタ書きするのは推奨されないため、AWS Systems Manager (SSM) Parameter Store を利用します。
ここでセキュリティを意識して暗号化された SecureString を使いたくなりますが、ALBのリスナールールは、 CloudFormation の SecureString 動的参照に対応していません。
もし SecureString を使ってCDKデプロイしようとすると、以下のエラーが発生します。
ToolkitError: ... SSM Secure reference is not supported in: [AWS::ElasticLoadBalancingV2::ListenerRule/Properties/Conditions]
そのため、ALBリスナールールで認証を行う場合は、String (平文) でパラメータストアに保存する必要があります。
APIキーの登録コマンド
以下のコマンドで、String 型としてキーを登録します。
# AWS CLIで登録
aws ssm put-parameter \
--name "/app/alb/api-key" \
--value "my-api-key" \
--type "String"
コンソールで確認すると String 型 でキーが登録されました。

CDKの実装
CDKプロジェクトを作成し、必要なライブラリをインストールします。
# CDKプロジェクトの作成
cdk init app --language typescript
# Lambda(Express)用
npm install express serverless-http
npm install --save-dev @types/express @types/aws-lambda esbuild
アプリケーションコード
lambda/src/index.ts を作成します。
import express from 'express';
import serverless from 'serverless-http';
const app = express();
app.get('/', (req, res) => {
res.json({
message: 'Authentication Successful!',
runtime: process.version,
});
});
export const handler = serverless(app);
ALBリスナールール
リスナールール を使った実装です。 ALBの機能で「特定のヘッダーを持たないリクエスト」を Lambda に届く前にブロックします。
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as nodejs from 'aws-cdk-lib/aws-lambda-nodejs';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as targets from 'aws-cdk-lib/aws-elasticloadbalancingv2-targets';
import * as ssm from 'aws-cdk-lib/aws-ssm'; // SSMモジュール
export class CdkAlbApikeyStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
/**
* APIキーの取得
* ALBリスナールール用に String (平文) パラメータとして取得
*/
const apiKey = ssm.StringParameter.valueForStringParameter(this, '/app/alb/api-key');
const HEADER_NAME = 'x-api-key';
/**
* VPC と Lambda 関数の構築
*/
const vpc = new ec2.Vpc(this, 'AppVpc', {
maxAzs: 2,
natGateways: 0,
subnetConfiguration: [
{ cidrMask: 24, name: 'Public', subnetType: ec2.SubnetType.PUBLIC },
],
});
const fn = new nodejs.NodejsFunction(this, 'ExpressFunc', {
entry: 'lambda/src/index.ts',
handler: 'handler',
runtime: lambda.Runtime.NODEJS_22_X,
});
/**
* ALB 構築
*/
const lb = new elbv2.ApplicationLoadBalancer(this, 'AppLB', {
vpc,
internetFacing: true,
});
const listener = lb.addListener('HttpListener', {
port: 80,
open: true,
});
/**
* ALB リスナールールの設定
* 認証ロジック (リスナールール版)
*/
listener.addAction('DefaultBlock', {
action: elbv2.ListenerAction.fixedResponse(403, {
contentType: 'application/json',
messageBody: JSON.stringify({ error: 'Forbidden: Invalid API Key' }),
}),
});
// 【許可ルール】APIキーが一致する場合のみ転送
listener.addTargets('AllowWithKey', {
priority: 10,
conditions: [
// SSMから取得した値を設定
elbv2.ListenerCondition.httpHeader(HEADER_NAME, [apiKey]),
],
targets: [new targets.LambdaTarget(fn)],
healthCheck: { enabled: true, path: '/' },
});
new cdk.CfnOutput(this, 'AlbDnsName', { value: lb.loadBalancerDnsName });
}
}
AWSコンソールでALBのリスナールールを確認してみます。 APIキーはコード上からは隠蔽しましたが、ALBの設定値として展開されるため、コンソール上ではこのように平文で見えてしまう点に注意が必要です。

デプロイと動作確認
ここで一度デプロイして、ALBのリスナールールが正しく機能しているか確認します。
出力された AlbDnsName (例: http://CdkAl-AppLB-xxxx.ap-northeast-1.elb.amazonaws.com) に対して curl でリクエストを送ります。
成功パターン
正しいAPIキーをヘッダーにセットします。
curl -H "x-api-key: my-api-key" http://<ALB-DNS-NAME>/
結果
{"message":"Authentication Successful!","runtime":"v22.x.x"}
Lambdaからのレスポンスが返ってきました。
失敗パターン(ブロック確認)
キーを設定しない、または間違ったキーで送ります。
curl http://<ALB-DNS-NAME>/
結果
{"error":"Forbidden: Invalid API Key"}
Lambdaには到達せず、ALBのリスナールールで 403 Forbidden が返されました。成功です!
AWS WAF を使うパターン
ALBリスナールールの代わりに AWS WAF を使用します。 WAF を使うと、IP制限やレートリミット(Dos攻撃対策)なども合わせて実装できます。CDK で WAF を実装する場合、ALB 側のルールは単純な転送に戻し、WAF の WebACL を作成して ALB に関連付けます。
lib/cdk-alb-apikey-stack.ts を以下のように書き換えます。
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as nodejs from 'aws-cdk-lib/aws-lambda-nodejs';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as targets from 'aws-cdk-lib/aws-elasticloadbalancingv2-targets';
import * as ssm from 'aws-cdk-lib/aws-ssm';
import * as wafv2 from 'aws-cdk-lib/aws-wafv2';
export class CdkAlbApikeyStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
/**
* APIキーの取得
* ALBリスナールール用に String (平文) パラメータとして取得
*/
const apiKey = ssm.StringParameter.valueForStringParameter(this, '/app/alb/api-key');
const HEADER_NAME = 'x-api-key';
/**
* VPC と Lambda 関数の構築
*/
const vpc = new ec2.Vpc(this, 'AppVpc', {
maxAzs: 2,
natGateways: 0,
subnetConfiguration: [
{ cidrMask: 24, name: 'Public', subnetType: ec2.SubnetType.PUBLIC },
],
});
const fn = new nodejs.NodejsFunction(this, 'ExpressFunc', {
entry: 'lambda/src/index.ts',
handler: 'handler',
runtime: lambda.Runtime.NODEJS_22_X,
});
/**
* ALB 構築
*/
const lb = new elbv2.ApplicationLoadBalancer(this, 'AppLB', {
vpc,
internetFacing: true,
});
const listener = lb.addListener('HttpListener', {
port: 80,
open: true,
});
// /**
// * ALB リスナールールの設定
// * 認証ロジック (リスナールール版)
// */
// listener.addAction('DefaultBlock', {
// action: elbv2.ListenerAction.fixedResponse(403, {
// contentType: 'application/json',
// messageBody: JSON.stringify({ error: 'Forbidden: Invalid API Key' }),
// }),
// });
// // 【許可ルール】APIキーが一致する場合のみ転送
// listener.addTargets('AllowWithKey', {
// priority: 10,
// conditions: [
// // SSMから取得した値を設定
// elbv2.ListenerCondition.httpHeader(HEADER_NAME, [apiKey]),
// ],
// targets: [new targets.LambdaTarget(fn)],
// healthCheck: { enabled: true, path: '/' },
// });
/**
* ALB リスナールールの設定
* WAF導入版: ALBは条件なしでLambdaへ転送(DefaultTarget)のみ設定
*/
listener.addTargets('DefaultTarget', {
targets: [new targets.LambdaTarget(fn)],
healthCheck: { enabled: true, path: '/' },
});
new cdk.CfnOutput(this, 'AlbDnsName', { value: lb.loadBalancerDnsName });
/**
* WAF Web ACLの定義
*/
const webAcl = new wafv2.CfnWebACL(this, 'ApiAuthWebACL', {
defaultAction: { block: {} },
scope: 'REGIONAL',
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: 'ApiAuthWebACL',
sampledRequestsEnabled: true,
},
rules: [
{
name: 'ApiKeyRule',
priority: 1,
action: { allow: {} }, // 一致したら許可
statement: {
byteMatchStatement: {
searchString: apiKey, // SSMの値
fieldToMatch: { singleHeader: { name: 'x-api-key' } },
positionalConstraint: 'EXACTLY',
textTransformations: [{ priority: 0, type: 'NONE' }]
}
},
visibilityConfig: {
cloudWatchMetricsEnabled: true,
metricName: 'ApiKeyRule', // ルールごとに一意な名前
sampledRequestsEnabled: true,
}
}
]
});
/**
* WAF Web ACL を ALB に関連付け
*/
new wafv2.CfnWebACLAssociation(this, 'WebAclAssoc', {
resourceArn: lb.loadBalancerArn,
webAclArn: webAcl.attrArn,
});
}
}
visibilityConfig を設定することで、CloudWatchメトリクスで「どれくらいブロックされたか」「どのルールに引っかかったか」を確認できるようになります。
WAF版のデプロイと動作確認
コードを修正したらデプロイします。デプロイ完了後、先ほどと同じ AlbDnsName に対してリクエストを送ってみましょう。
成功パターンの確認
正しいAPIキーがある場合は、これまで通り Lambda からレスポンスが返ります。
curl -H "x-api-key: my-api-key" http://<ALB-DNS-NAME>/
結果
{"message":"Authentication Successful!",...}
失敗パターンの確認(ここが変わります!)
キーなし、または間違ったキーでリクエストを送ります。
curl -v http://<ALB-DNS-NAME>/
結果
これまでALBリスナールールで返していた 403 Forbidden ({"error":...}) というJSONレスポンスではなく、WAFによるデフォルトの 403 HTMLページ(または403エラー)が返ってくるようになります。
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
</body>
</html>
AWSマネジメントコンソールで確認
AWSコンソールの 「WAF & Shield」 > 「保護パック (ウェブ ACL)」 を開き、作成された WebACL をクリックします。

「ダッシュボード、ログ、サンプルリクエストを表示」タブで確認すると、許可されたリクエスト(Allow)とブロックされたリクエスト(Block)の数がグラフで視覚的に確認できます。


「一致する文字列」の欄に APIキーの値「my-api-key」 が表示されています。 WAF もリクエストの中身を検査するために「比較対象の文字列」を保持する必要があるため、設定画面上では平文となります。

おわりに
今回は、API Gatewayを使わずに ALB + Lambda 構成でAPIキー認証を実装する方法を、CDK を用いて解説しました。ALBリスナールール と AWS WAF の2つのアプローチを試してみましたが、それぞれに特徴があります。
ALBリスナールール
シンプルで追加コストなし。ヘッダーベースの認証には十分
AWS WAF
より高度なセキュリティ機能(レート制限、IP制限など)を追加可能。ただし別途WAFの料金が発生
どちらの方法も、SSM Parameter Store から取得したAPIキーは設定画面上で平文表示される点に注意が必要です。より厳格なセキュリティが求められる場合は、Lambda Authorizer を使った認証や、Cognitoとの連携なども検討してください。
今回の実装が、皆さんの参考になれば幸いです。







