kakakakakku blog

Weekly Tech Blog: Keep on Learning!

Lambda@Edge と CloudFront Functions に入門できるワークショップ「Handling Rewrites and Redirects using Edge Functions」

Lambda@Edge と CloudFront Functions を使って HTTP リクエストのリダイレクトとリライトを体験するワークショップ「Handling Rewrites and Redirects using Edge Functions」を実施してみた❗️

Lambda@Edge と CloudFront Functions に入門したいな〜という人に特におすすめ💡アーキテクチャ図も多く載っていてイメージしやすく構成されているのも良かった〜 \( 'ω')/

catalog.us-east-1.prod.workshops.aws

目次

大きく2部構成になっていて,Module 1 では Lambda@Edge を使ってキャッシュ可能なリダイレクトとリライトを体験して,Module 2 では CloudFront Functions を使って動的なリダイレクトとリライトを体験する📝

  • Getting Started
  • Overview
  • Module 1 - Cacheable Use Cases
    • URI based Redirects
    • Geo Location Redirects
    • Device Redirects
    • URI based Rewrites
  • Module 2 - Dynamic Use Cases
    • Dynamic Geo Location Redirects
    • Cookie Based Redirect
    • Bot Signatures Based Rewrite
  • Conclusion

所要時間はワークショップの冒頭に以下のように書いてあって妥当だと思う👌僕は計3ポモドーロ (90 minutes) で最後のクリーンアップまで完了できた.

This workshop takes about 90-120 minutes to complete all the labs covered.

Lambda@Edge と CloudFront Functions の比較

Lambda@Edge と CloudFront Functions の比較はワークショップ (Overview) で出てくるけど,ワークショップ環境を AWS CloudFormation で構築する待ち時間などに以下のドキュメントを軽く読んでおくと理解しやすくなって良いと思う💪

docs.aws.amazon.com

Module 1

Module 1 では Lambda@Edge を使って4種類のリダイレクトとリライトを体験する.Lambda@Edge のコード (Python 3.9) はコピペする.シンプルな実装で理解しやすいし,AWS Lambda 関数の実装に慣れているから特に詰まるところもなかった💡

Lambda@Edge に Amazon CloudFront のトリガーを設定して,AWS CloudShell から curl コマンドを実行しながらリダイレクト・リライトの挙動確認や X-Cache ヘッダーの結果を確認していく.

また2番目の Geo Location Redirects では Amazon CloudFront で CloudFront-Viewer-Country ヘッダーを追加したりする.勉強になる👌

docs.aws.amazon.com

そして4番目に試す URI based Rewrites もプロダクション運用でニーズがありそうで実践的だな〜と思った💡

Module 1 の実施ログ

最終的に4種類の Lambda@Edge を追加した.

Module 1 で追加した Lambda@Edge

Module 2

Module 2 では CloudFront Functions を使って3種類のリダイレクトとリライトを体験する.CloudFront Functions の言語は JavaScript なので TypeScript で実装してデプロイできたら良さそうだな〜なんて考えながら進めていた👀

3番目に試す Bot Signatures Based Rewrite は AWS WAF と組み合わせてボット対応を体験する.awswaf:managed:aws:bot-control:bot:category:social_media ラベルを使ったりして,実践的で良かった🛡️

Module 2 で設定した AWS WAF Web ACLs

最終的に3種類の CloudFront Functions を追加した.

Module 2 で追加した CloudFront Functions

まとめ

Lambda@Edge と CloudFront Functions に入門できるワークショップ「Handling Rewrites and Redirects using Edge Functions」を実施してみた❗️面白かった〜 \( 'ω')/

ちなみに朝活でワークショップを実施した🌅朝活だと頭もスッキリしてて集中して取り組めるからおすすめ.

ワークショップは AWS Workshops で探せるよ〜

awsworkshop.io

Powertools for AWS Lambda (Python) で Cognito User Pools の トークン生成前トリガーを実装しよう

Amazon Cognito User Pools には AWS Lambda 関数を使って認証フローをカスタマイズできる「Lambda トリガー」という機能がある💡トークン生成前トリガー (Pre token generation Lambda trigger) を使うと,認証時に発行されるトークン (ID Token / Access Token) をカスタマイズできる👌

docs.aws.amazon.com

docs.aws.amazon.com

ちなみにトークン (Access Token) は2023年12月のリリースでカスタマイズできるようになっていたりする💡

aws.amazon.com

Powertools for AWS Lambda (Python)

Powertools for AWS Lambda (Python) の Event Source Data Classes を使うと,シンプルに Lambda トリガーを実装できるようになる.今回は PreTokenGenerationTriggerEvent を使ってトークン (ID Token) のペイロードにクレームを追加/削除するトークン生成前トリガーを実装する❗️

docs.powertools.aws.dev

準備

まずは Amazon Cognito User Pools を作っておく❗️今回はサクッと認証を試すためにアプリケーションクライアントの認証フローとして ALLOW_USER_PASSWORD_AUTH も有効化する.あとカスタム属性として X アカウント名を表す custom:x を追加しておく.その他はデフォルト設定で良いかなと思う👌

トークンを取得する(トークン生成前トリガーなし)

まずは「トークン生成前トリガーなし」でトークン (ID Token) を取得する.AWS CLI で aws cognito-idp initiate-auth コマンドを実行する💡

awscli.amazonaws.com

$ CLIENT_ID=xxx
$ EMAIL=xxx
$ PASSWORD=xxx

$ aws cognito-idp initiate-auth \
  --client-id ${CLIENT_ID} \
  --auth-flow USER_PASSWORD_AUTH \
  --auth-parameters USERNAME=${EMAIL},PASSWORD=${PASSWORD} | jq .
{
  "ChallengeParameters": {},
  "AuthenticationResult": {
    "AccessToken": "xxx",
    "ExpiresIn": 3600,
    "TokenType": "Bearer",
    "RefreshToken": "xxx",
    "IdToken": "xxx"
  }
}

取得した IdToken の値から JWT Payload を抜き出すと以下のようになっていた❗️追加したカスタム属性 custom:x にも @kakakakakku という値が入っている👌

{
  "sub": "f7a4da98-a041-70e4-7954-8bbc5d3ca764",
  "email_verified": true,
  "iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_xxxxxxxxx",
  "cognito:username": "f7a4da98-a041-70e4-7954-8bbc5d3ca764",
  "custom:x": "@kakakakakku",
  "origin_jti": "85ed5e84-1346-41fa-8269-1e5aa0a35bab",
  "aud": "xxx",
  "event_id": "525e5170-54f5-450c-a053-258fde4ea50f",
  "token_use": "id",
  "auth_time": 1726876453,
  "exp": 1726880053,
  "iat": 1726876453,
  "jti": "db5441ca-8710-4fe7-b0cc-4b31f6fc77e0",
  "email": "[email protected]"
}

トークン (ID Token) の詳細は以下のドキュメントに載っている📝

docs.aws.amazon.com

トークン生成前トリガー

次に AWS Lambda 関数を実装してトークン生成前トリガーを設定する.

AWS Lambda 関数の実装は Powertools for AWS Lambda (Python) の Event Source Data Classes を使って,my_key クレームの追加と custom:x クレームの削除をする.実装自体は簡単にできる👌

👾 app.py(サンプル1)

シンプルに実装する場合

from aws_lambda_powertools.utilities.data_classes.cognito_user_pool_event import PreTokenGenerationTriggerEvent


def lambda_handler(event, context):
    event: PreTokenGenerationTriggerEvent = PreTokenGenerationTriggerEvent(event)
    event.response.claims_override_details.claims_to_add_or_override = {'my_key': 'my_value'}
    event.response.claims_override_details.claims_to_suppress = ['custom:x']

    return event.raw_event

👾 app.py(サンプル2)

ハンドラとロジックを分離してテストをしやすく実装する場合

import json

from aws_lambda_powertools.utilities.data_classes.cognito_user_pool_event import PreTokenGenerationTriggerEvent


def main(event):
    event: PreTokenGenerationTriggerEvent = PreTokenGenerationTriggerEvent(event)
    event.response.claims_override_details.claims_to_add_or_override = {'my_key': 'my_value'}
    event.response.claims_override_details.claims_to_suppress = ['custom:x']

    return event.raw_event


def lambda_handler(event, context):
    return main(event)


if __name__ == '__main__':
    with open('./events/event.json', 'r') as f:
        event = json.load(f)
    print(main(event))

トークンを取得する(トークン生成前トリガーあり)

もう一度 AWS CLI で aws cognito-idp initiate-auth コマンドを実行して,IdToken の値から JWT Payload を抜き出すと以下のようになっていた❗️期待通りに my_key が追加されていて,カスタム属性 custom:x は削除されていた👌

{
  "sub": "f7a4da98-a041-70e4-7954-8bbc5d3ca764",
  "email_verified": true,
  "iss": "https://cognito-idp.ap-northeast-1.amazonaws.com/ap-northeast-1_xxxxxxxxx",
  "cognito:username": "f7a4da98-a041-70e4-7954-8bbc5d3ca764",
  "origin_jti": "888ee4b8-4c3b-4a78-a34c-7e5f04c3788c",
  "aud": "xxx",
  "event_id": "bf25e9fe-beb5-4ad2-b1c7-188eda16ea75",
  "token_use": "id",
  "auth_time": 1726876789,
  "my_key": "my_value",
  "exp": 1726880389,
  "iat": 1726876789,
  "jti": "f75d7eaa-c826-4c40-9951-6dbe8bcc100a",
  "email": "[email protected]"
}

まとめ

Powertools for AWS Lambda (Python) の Event Source Data Classes を使って Amazon Cognito User Pools のトークン生成前トリガーをシンプルに実装するサンプルの紹介でした〜 \( 'ω')/

CloudFormation Git sync で変更セットの結果をプルリクエストにコメントする

2024年9月20日にリリースされたアップデートによって,AWS CloudFormation の「Git sync 機能」で GitHub リポジトリにプルリクエストを出したら自動的に変更セットの結果をコメントしてくれるようになった❗️

aws.amazon.com

試す

Git sync の設定画面で Enable comment on pull request を有効化にしておけば OK👌

今回は Amazon SQS キューの可視性タイムアウト (VisibilityTimeout) を 60秒 から 90秒 に更新する💡

Resources:
  Queue:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: sandbox-cfn-stack-git-sync
      ReceiveMessageWaitTimeSeconds: 20
      VisibilityTimeout: 90

GitHub リポジトリにプッシュをして main ブランチに対してプルリクエストを出したら,すぐにコメントが返ってきた❗️

そして Click here to view change details の表を右にスクロールすると

  • BeforeValue: 60
  • AfterValue: 90
  • AttributeChangeType: Modify

と書いてあって,変更セットの結果を確認できる❗️

確認してからプルリクエストをマージすると,期待通りにデプロイされてた〜

$ aws sqs get-queue-attributes \
    --queue-url https://sqs.ap-northeast-1.amazonaws.com/000000000000/sandbox-cfn-stack-git-sync \
    --attribute-names All | grep VisibilityTimeout
        "VisibilityTimeout": "90",

参考記事

AWS CloudFormation の Git sync 機能は既存の AWS CloudFormation スタックにも導入できることを前に確認したので合わせて読んでもらえればと〜📝

kakakakakku.hatenablog.com

LocalStack で Lambda の Errors メトリクスを SNS 経由でアラートする

LocalStack を使って AWS Lambda 関数の Errors メトリクスをモニタリングして Amazon CloudWatch Alarm と Amazon SNS でアラートするアーキテクチャを試してみた❗️LocalStack(無料版)ですべて試せる〜 \( 'ω')/

アーキテクチャ図

Webhook.site

今回は Amazon SNS からアラートを受け取るお手軽な環境として Webhook.site を使う.検証用途などに使える一時的な Webhook URL を発行できて,Amazon SNS の HTTPS エンドポイントとしてサブスクライブできる👌ウェブサイトにアクセスして Your unique URL に表示されている URL をコピーしておく.

webhook.site

デプロイ

まずは AWS SAM (AWS CloudFormation) で LocalStack 上に検証環境をデプロイする.

AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31

Parameters:
  WEBHOOK:
    Type: String
    AllowedPattern: https://webhook.site/.*

Resources:
  Function:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: sandbox
      CodeUri: ./src
      Handler: app.lambda_handler
      Runtime: python3.12
      Architectures:
        - x86_64
  Alarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: function-errors
      MetricName: Errors
      Namespace: AWS/Lambda
      ComparisonOperator: GreaterThanOrEqualToThreshold
      EvaluationPeriods: 1
      Period: 60
      Statistic: Sum
      Threshold: 1
      AlarmActions:
        - !Ref Topic
      Dimensions:
        - Name: FunctionName
          Value: !Ref Function
  Topic:
    Type: AWS::SNS::Topic
    Properties:
      DisplayName: alerts
      Subscription:
        - Protocol: https
          Endpoint: !Ref WEBHOOK

そして samlocal コマンドを使ってデプロイする.なお,デプロイ時の WEBHOOK パラメータには取得した Webhook.site の URL を設定しておく.

$ samlocal build
$ samlocal deploy --parameter-overrides WEBHOOK=https://webhook.site/64e8d8c6-3fa2-4d68-a2a2-a87409553fff

すると Webhook.site に SubscriptionConfirmation の通知が届くので,SubscribeURL にアクセスしてサブスクライブを承認しておく❗️これで準備 OK〜

SubscriptionConfirmation の通知が届いた

AWS Lambda 関数を実行する

今回はサンプルとして必ずエラーになる AWS Lambda 関数 (Python) をデプロイした💡

def lambda_handler(event, context):
    raise Exception('Always an error!')

さっそく awslocal コマンドを使って AWS Lambda 関数を実行する.

$ awslocal lambda invoke --function-name sandbox outputs.json
{
    "StatusCode": 200,
    "FunctionError": "Unhandled",
    "ExecutedVersion": "$LATEST"
}

すると Webhook.site にアラートが届いた👌

Webhook.site にアラートが届いた

LocalStack Resource Browser

LocalStack Resource Browser で Amazon CloudWatch の Alarms を開くと,期待通りに AWS Lambda 関数 sandbox の Errors メトリクスが 1 になっていた❗️

Errors メトリクス

お掃除

念のため Webhook.site の More メニューで Delete URL を実行しておくと安心かなと思う👌

Vercel Terraform Provider: Vercel の設定を Terraform で管理しよう

最近 Vercel を使っていて,Vercel コンソールでプロジェクト設定をしてしまったけど,継続的なプロダクション運用を考えたらコンソール操作は避けたいな〜と思って,Vercel Terraform Provider を試してみた❗️結論から言うと,期待した通りに Vercel の設定を宣言的に管理できて良かった👌 既に Terraform を使っているなら特にハマることなく導入できそう.

github.com

以下のドキュメントを読むと Next.js アプリケーションを Vercel Terraform Provider で Vercel にデプロイする流れを試せる📝 今回はこのドキュメントを参考にしつつ,個人的に気になる設定をデプロイしてみた \( 'ω')/

vercel.com

サンプルアプリ

Vercel にデプロイするアプリは何でも良くて,今回は2024年5月に React-Admin に入門するときに使った React-Admin Tutorial アプリ(GitHub プライベートリポジトリにある)を使うことにした.

kakakakakku.hatenablog.com

1. Vercel GitHub App に権限を付与する

まずは Vercel GitHub App で連携する GitHub リポジトリに対する権限を付与しておく👌

2. Vercel Token を発行する

次に Vercel Token を発行して環境変数 VERCEL_API_TOKEN に設定しておく🔑

$ export VERCEL_API_TOKEN=xxx

vercel.com

3. terraform init コマンドを実行する

providers.tf を実装して Vercel Terraform Provider を取得する.今回は最新 v1.13.0 を使う.

👾 providers.tf

terraform {
  required_providers {
    vercel = {
      source  = "vercel/vercel"
      version = "~> 1.13"
    }
  }
}

4. Project: terraform plan コマンドを実行する

次に projects.tf を実装する.まずは Vercel Project を vercel_project リソースを使って追加する.React-Admin は Vite を使ってビルドをしているため frameworkvite にした👌

👾 projects.tf

resource "vercel_project" "react_admin_tutorial" {
  name      = "react-admin-tutorial"
  framework = "vite"

  git_repository = {
    type = "github"
    repo = "kakakakakku/react-admin-tutorial"
  }
}

そして terraform plan コマンドを実行する.問題なさそう❗️ちなみに次に設定を変更する予定の vercel_authentication.deployment_type はデフォルトでは standard_protection になっていた💡

$ terraform plan
Terraform will perform the following actions:

  # vercel_project.react_admin_tutorial will be created
  + resource "vercel_project" "react_admin_tutorial" {
      + auto_assign_custom_domains                        = true
      + automatically_expose_system_environment_variables = (known after apply)
      + customer_success_code_visibility                  = (known after apply)
      + directory_listing                                 = (known after apply)
      + framework                                         = "vite"
      + function_failover                                 = (known after apply)
      + git_fork_protection                               = true
      + git_lfs                                           = (known after apply)
      + git_repository                                    = {
          + production_branch = (known after apply)
          + repo              = "kakakakakku/react-admin-tutorial"
          + type              = "github"
        }
      + id                                                = (known after apply)
      + name                                              = "react-admin-tutorial"
      + oidc_token_config                                 = {
          + enabled = false
        }
      + prioritise_production_builds                      = (known after apply)
      + protection_bypass_for_automation_secret           = (known after apply)
      + serverless_function_region                        = (known after apply)
      + team_id                                           = (known after apply)
      + vercel_authentication                             = {
          + deployment_type = "standard_protection"
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

5. Project: terraform apply コマンドを実行する

最後は terraform apply コマンドを実行する.

$ terraform apply

vercel_project.react_admin_tutorial: Creating...
vercel_project.react_admin_tutorial: Creation complete after 2s [id=prj_xxxxx]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

できた❗️最高 \( 'ω')/

Vercel Project を追加できた

6. Deployments: terraform plan コマンドと terraform apply コマンドを実行する

そして Vercel Project をデプロイするために vercel_deployment リソースを使う.今回はシンプルに GitHub の main ブランチをデプロイする設定にした👌

👾 deployments.tf

resource "vercel_deployment" "react_admin_tutorial" {
  project_id = vercel_project.react_admin_tutorial.id
  ref        = "main"
}

同じく terraform plan コマンドと terraform apply コマンドを実行する.

$ terraform plan
Terraform will perform the following actions:

  # vercel_deployment.react_admin_tutorial will be created
  + resource "vercel_deployment" "react_admin_tutorial" {
      + domains    = (known after apply)
      + id         = (known after apply)
      + production = (known after apply)
      + project_id = "prj_xxxxx"
      + ref        = "main"
      + team_id    = (known after apply)
      + url        = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

$ terraform apply
vercel_deployment.react_admin_tutorial: Creating...
vercel_deployment.react_admin_tutorial: Still creating... [10s elapsed]
vercel_deployment.react_admin_tutorial: Still creating... [20s elapsed]
vercel_deployment.react_admin_tutorial: Still creating... [30s elapsed]
vercel_deployment.react_admin_tutorial: Still creating... [40s elapsed]
vercel_deployment.react_admin_tutorial: Creation complete after 49s [id=dpl_xxx]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

少し待っているとデプロイされた \( 'ω')/

Vercel に React-Admin Tutorial アプリをデプロイできた 1

Vercel に React-Admin Tutorial アプリをデプロイできた 2

7. Project: terraform plan コマンドと terraform apply コマンドを実行する

Vercel Project では Deployment Protection のデフォルト設定として Vercel Authentication が有効化されている🛡

vercel.com

よって,Vercel へのアクセス権限がないとサイトにアクセスできず,例えば Google Chrome のシークレットウィンドウでアクセスしようとすると拒否される🛑

Vercel Authentication でアクセスできなかった

そこで projects.tf を修正して vercel_authentication.deployment_type をデフォルトの standard_protection から none に修正する💡

👾 projects.tf

resource "vercel_project" "react_admin_tutorial" {
  name      = "react-admin-tutorial"
  framework = "vite"

  vercel_authentication = {
    deployment_type = "none"
  }

  git_repository = {
    type = "github"
    repo = "kakakakakku/react-admin-tutorial"
  }
}

terraform plan コマンドと terraform apply コマンドを実行する.

$ terraform plan
Terraform will perform the following actions:

  # vercel_project.react_admin_tutorial will be updated in-place
  ~ resource "vercel_project" "react_admin_tutorial" {
        id                                                = "prj_xxxxx"
        name                                              = "react-admin-tutorial"
      + protection_bypass_for_automation_secret           = (known after apply)
      + team_id                                           = (known after apply)
      ~ vercel_authentication                             = {
          ~ deployment_type = "standard_protection" -> "none"
        }
        # (12 unchanged attributes hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

$ terraform apply
vercel_project.react_admin_tutorial: Modifying... [id=prj_xxxxx]
vercel_project.react_admin_tutorial: Modifications complete after 1s [id=prj_xxxxx]

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

Vercel Authentication を無効化できて Google Chrome のシークレットウィンドウでもアクセスできるようになった👌

Vercel Authentication を無効化できた

8. Domains: terraform plan コマンドと terraform apply コマンドを実行する

最後は Vercel Project のデフォルトドメイン *.vercel.app にカスタムドメインを割り当てる❗️カスタムドメインは vercel_project_domain リソースを使って割り当てられる.なお前提として,以下のドキュメントを参考に Amazon Route 53 で取得したドメインのホストゾーンに cname.vercel-dns.com. という CNAME レコードを登録しておく👌

vercel.com

👾 domains.tf

resource "vercel_project_domain" "react_admin_tutorial" {
  project_id = vercel_project.react_admin_tutorial.id
  domain     = "react-admin-tutorial.xxxxx.com"
}

同じく terraform plan コマンドと terraform apply コマンドを実行する.

$ terraform plan
Terraform will perform the following actions:

  # vercel_project_domain.react_admin_tutorial will be created
  + resource "vercel_project_domain" "react_admin_tutorial" {
      + domain     = "react-admin-tutorial.xxxxx.com"
      + id         = (known after apply)
      + project_id = "prj_xxx"
      + team_id    = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

$ terraform apply
vercel_project_domain.react_admin_tutorial: Creating...
vercel_project_domain.react_admin_tutorial: Creation complete after 1s [id=react-admin-tutorial.xxxxx.com]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

カスタムドメインを設定できた❗️そして https://react-admin-tutorial.xxxxx.com でアクセスできるようになった \( 'ω')/

カスタムドメインを設定できた

まとめ

Vercel Terraform Provider を使って Vercel の設定を Terraform で宣言的に管理しよう❗️

関連記事

kakakakakku.hatenablog.com