AWS Cloud Development Kit (AWS CDK)を使いこなすため、勉強したことを残していきます。
今回はCloudFrontで配信する静的サイトにAWS LambdaとAmazon API Gatewayで機能を追加する実装を行います。
CloudFrontでの静的サイト配信に関してはこちらの記事を参照。
Lambda + API Gateway追加
以前に作成したCloudFrontでS3にある静的サイトを配信する構成に、簡単な出力をするLambda関数を作成して、API GatewayでLambda関数を実行する機能を追加します。Stackの実装は以下の通り。
from aws_cdk import (
Stack,
RemovalPolicy,
CfnOutput,
aws_s3 as s3,
aws_s3_deployment as s3deploy,
aws_cloudfront as cloudfront,
aws_cloudfront_origins as origins,
aws_lambda as _lambda,
aws_apigateway as apigw,
)
from constructs import Construct
class AddApiStack(Stack):
def __init__(self, scope: Construct, construct_id: str, **kwargs):
super().__init__(scope, construct_id, **kwargs)
# S3 Bucket (private)
bucket = s3.Bucket(
self,
"PrivateBucket",
block_public_access=s3.BlockPublicAccess.BLOCK_ALL,
removal_policy=RemovalPolicy.DESTROY,
auto_delete_objects=True,
)
# Lambda (API backend)
used_lambda = _lambda.Function(
self,
"TestLambdaFunction",
runtime=_lambda.Runtime.PYTHON_3_12,
handler="index.handler",
code=_lambda.InlineCode(
"""
import json
import datetime
def handler(event, context):
now = datetime.datetime.now(datetime.timezone.utc).astimezone()
msg = f"あなたは{now.strftime('%Y-%m-%d %H:%M:%S %Z')}に、API経由でLambdaを使いました"
return {
"statusCode": 200,
"headers": {
"content-type": "application/json; charset=utf-8",
},
"body": json.dumps({"message": msg}, ensure_ascii=False),
}
"""
),
)
# API Gateway (REST API)
api = apigw.RestApi(
self,
"RestApi",
endpoint_configuration=apigw.EndpointConfiguration(
types=[apigw.EndpointType.REGIONAL] # CloudFrontがあるのでREGIONAL
),
deploy_options=apigw.StageOptions(
stage_name="prod"
),
)
# /api/used (GET) 作成
api_res = api.root.add_resource("api")
used_res = api_res.add_resource("used")
used_res.add_method(
"GET",
apigw.LambdaIntegration(used_lambda),
)
# CloudFront Distribution
distribution = cloudfront.Distribution(
self,
"Distribution",
default_behavior=cloudfront.BehaviorOptions(
origin=origins.S3BucketOrigin.with_origin_access_control(bucket),
viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
),
# API Gateway設定
additional_behaviors={
"api/*": cloudfront.BehaviorOptions(
origin=origins.RestApiOrigin(api),
allowed_methods=cloudfront.AllowedMethods.ALLOW_ALL, # GET/POSTなどすべて許可
cache_policy=cloudfront.CachePolicy.CACHING_DISABLED, # APIなのでキャッシュはOFF
origin_request_policy=cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER, # リクエスト情報
viewer_protocol_policy=cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, # HTTPはHTTPSに変換
)
},
default_root_object="index.html",
)
# S3にアップロード
s3deploy.BucketDeployment(
self,
"DeployWebsite",
sources=[s3deploy.Source.asset("assets")],
destination_bucket=bucket,
distribution=distribution,
distribution_paths=["/*"],
)
# Outputs
CfnOutput(
self,
"CloudFrontURL",
value=f"https://{distribution.domain_name}",
)
REST APIの設定について
上記StackでのAPIの設定について詳細確認。
まず、EndpointConfigurationの部分について。
エンドポイントタイプとしては以下の種類があるようです。
| タイプ | 特徴 |
|---|---|
| REGIONAL | 特定Regionでの限定配信 |
| EDGE | Global配信 (内部でCloudFrontを設定) |
| PRIVATE | VPC内限定 |
今回は実装部分でCloudFrontを使っているので、REGIONALを選択しました。
次にStageOptionsの部分について、
今回は簡単な検証なので作りこんでないですが、実際には環境ごとに設定を切り替える方が良さそうです。
| 環境 | 一般的な設定方針 |
|---|---|
| dev (開発) | デバッグ最優先, 全てのログを取る, 壊して問題なし |
| stg (検証) | 本番環境再現, 動作部分のログだけ取る |
| prod (本番) | コスト最適, ログはエラーだけ (不必要に情報を出さない) |
切り替える場合は、cdk deploy -c env=prodみたいな形で環境の情報を渡して、Stack内かConfig用のディレクトリにある環境ごとの設定を読み込んで運用/管理。
CONFIG = {
"dev": {
"stage_name": "dev",
"logging_level": apigw.MethodLoggingLevel.INFO,
"data_trace_enabled": True,
"metrics_enabled": True,
},
"stg": {
...省略
}
↑で設定したStageOptionsの中身は下表のもの、他にも色々あります。
(本番ならリクエスト数などを考慮すべき)
詳細は公式ドキュメント参照。
| オプション名 | 内容 |
|---|---|
| logging_level | どこまでログを取るか, [OFF, ERROR, INFO]の3種 |
| data_trace_enabled | リクエストとレスポンスの中身をログに出すか |
| metrics_enabled | CloudWatchメトリクスの有効/無効化 |
CloudFront Distributionの設定について
今回はcloudfront.Distributionの設定でAPIを叩けるようにしています。
api/*に関するURLは、S3のindex.htmlではなくREST API側にして、以下の設定をしています。
-
AllowedMethods.ALLOW_ALL: 各メソッドを許可 -
CachePolicy.CACHING_DISABLED: キャッシュ無効化 -
OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER: リクエスト情報保持 -
ViewerProtocolPolicy.REDIRECT_TO_HTTPS: HTTPをHTTPSに変換
【補足】
今回はadditional_behaviorsにAPIを追加する形を取っています。
URLを知っていればAPIを直接叩けることになるので、APIの内容によっては不適切です。
一方で、この機能を使うとパスごとでキャッシュ方針を変えたり、同一オリジンと見なすことができるため、CORS関連の面倒を減らすことができるようです。
Lambdaの別管理
上記Stackだと、Lambda関数の内容をインラインで入れることになるので少し不便です。
index.htmlのようにStackの外に出して、別で管理することにします。
以下のようなディレクトリの構成にします。
AddAPI/
├── app.py
├── add_api/
│ └── add_api_stack.py ← これを編集
├── assets/
│ └── index.html
├── lambda/
│ └── app.py ← ここにLambda関数を格納
lambda/app.pyの中身は上記のインラインで書いた内容をコピペ。
Stack側のLambdaの部分を以下のように修正します。
# Lambda (API backend)
used_lambda = _lambda.Function(
self,
"TestLambdaFunction",
runtime=_lambda.Runtime.PYTHON_3_12,
code=_lambda.Code.from_asset("lambda"),
handler="app.handler",
)
これでStackからLanbda関数の部分を分離することができました。
改善点
目的である静的サイトからAPIを叩くことは達成しましたが、問題も残っています。
特にAPI GatewayをCloudFront経由に制限できていないのは要改善なので、認証関係の勉強と併せて直したいと思います。
一方で、additional_behaviorsを使うとCORSの面倒を減らせるとあるので、機能に合わせて使い分けた方が良さそう?
分からない点が多いので、別途まとめたいと思います。
まとめ
静的サイトにAPIを追加しました。
- APIは構成によってエンドポイントタイプを選ぶ
- 環境によって
StageOptionsを切り替えられるようにする - CloudFront Distributionに
additional_behaviorsを追加すればパスごとの切り替えが可能 (同一オリジン扱いになる?) - Lambda関数はインラインではなく、別管理にした方が良さそう
改善点にも記載した通り、実際のサービスを考えると認証関係が必要になりそうなので、次は認証部分に関して勉強したいと思います。