PLAY DEVELOPERS BLOG

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

HuluやTVerなどの日本最大級の動画配信を支える株式会社PLAYが運営するテックブログです。

AWS SAMを使ったLambda自動デプロイ + Lambda関数URL化 + CF設定までの道のり

こんにちは、OTTサービス技術部の小渕です。

SmartTV(HTML5TV)向けのアプリ開発は、PCやスマホアプリ開発とは異なる独特の苦労があります。ブラウザエンジンの性能が限られているため、少しの重い処理がユーザー体験(UX)を著しく損なわせます。

私が担当したある案件では、以下の三重苦に直面しました。

  • 巨大なレスポンスを返す既存API
  • 厳しいリクエスト制限 があるAPI
  • 画像生成などの重い処理 をテレビ側で行うことによるパフォーマンス低下

これらを解決するために、AWS Lambdaを「BFF(Backend For Frontend)」兼「画像生成エンジン」として活用し、そのインフラ管理を AWS SAM で自動化する手法を採りました 。

今回は、そのAWS SAMを使ったLambda自動デプロイ + 関数URL化 + CloudFrontによるセキュアな構成まで実施した手順を備忘録としてまとめてみました。

AWS SAMとは

AWS SAM(Serverless Application Model)は、AWS CloudFormationの拡張機能であり、LambdaやAPI Gatewayなどのサーバーレスアプリケーションのデプロイに特化したフレームワークです 。

テンプレートファイル(template.yaml)を用いてインフラをコード化(IaC)でき、複雑なリソース構成を簡潔に定義できるのが特徴です 。

導入~自動デプロイ設定までの手順

導入

まずは開発環境にAWS SAM CLIをインストールします。

macOS(Appleシリコン)の場合

# PKGファイルをダウンロード
$ curl -L "https://github.com/aws/aws-sam-cli/releases/latest/download/aws-sam-cli-macos-arm64.pkg" -o "aws-sam-cli.pkg"

# インストール実行
$ sudo installer -pkg aws-sam-cli.pkg -target /

macOS(Intelプロセッサ)の場合

# PKGファイルをダウンロード
$ curl -L "https://github.com/aws/aws-sam-cli/releases/latest/download/aws-sam-cli-macos-x86_64.pkg" -o "aws-sam-cli.pkg"

# インストール実行
$ sudo installer -pkg aws-sam-cli.pkg -target /

WindowsOSの場合

公式GitHubからインストーラーをダウンロードしてください。

github.com

インストール完了後、バージョンが出力されることを確認します 。

$ sam --version

SAM CLI, version 1.154.0

事前準備

1. GitHubリポジトリの用意

デプロイ対象のソースコードを管理します。

2. AWS認証情報の用意

GitHubホステッドランナー を利用する場合、GitHubとAWSをOpenID Connect (OIDC) で連携させ、GitHub Actions専用のIAMロールを作成して認証情報を取得できるようにしておきます。

※ 従来ではIAMユーザーのアクセスキーIDとシークレットアクセスキーを発行し、GitHub Secretsに登録する方法が一般的でしたが、外部に長期的な認証情報を保存することは漏洩のリスクを伴うため、現在は推奨されていません。

セルフホストランナー を利用する場合、サーバー側にAWSプロファイルを作成しておくか、インスタンスプロファイル(IAMロール)を付与しておきます。

3. AWS SAMの新規リソースを作成

以下コマンドで、最小構成に必要なファイル一式を生成できます 。

$ sam init --runtime nodejs24.x --name github-actions-with-aws-sam

※ 実行時の対話形式オプション(X-Rayや監視設定など)については、今回は「1」か「No」を選択して進めて問題ありません 。

生成されるファイルはプロジェクトフォルダの直下に配置されます。

リポジトリ名/ (ルート)
├── events/
│   └── event.json
├── hello-world/
│   ├── app.mjs
│   ├── package.json
│   └── tests/
│       └── unit/
│           └── test-handler.mjs
├── .gitignore
├── README.md
├── samconfig.toml
└── template.yaml

ローカル動作テスト

デプロイする前に、ローカル環境で動作を確認します。実行にはDocker(または互換環境)が必要です。

# ビルド
$ sam build

# Lambda関数のローカル実行
$ sam local invoke

# ローカルサーバー(API Gatewayエミュレート)の起動
$ sam local start-api

# レスポンス確認
$ curl http://127.0.0.1:3000/hello

GtiHubActionsを使ってデプロイ

プロジェクトフォルダの直下にデプロイ用のYAMLファイルを作成します。

リポジトリ名/ (ルート)
└─ .github/
    └─ workflows/
        └─ deploy.yml

GitHubホステッドランナーの場合

name: Deploy

on:
  push:
    branches:
      - main
      - develop
jobs:
  build-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Install Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 20
      - name: Setup AWS SAM
        uses: aws-actions/setup-sam@v2
        with:
          use-installer: true
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v3
        with:
          role-to-assume: ${{ secrets.AWS_IAM_ROLE_ARN }}
          aws-region: ap-northeast-1
      - name: SAM Build
        run: sam build --use-container
      - name: sam deploy Prod
        if: ${{ github.ref_name == 'main' }}
        run: sam deploy --no-confirm-changeset --no-fail-on-empty-changeset --stack-name ${任意のスタック名} --capabilities CAPABILITY_IAM
      - name: sam deploy Dev
        if: ${{ github.ref_name == 'develop' }}
        run: sam deploy --no-confirm-changeset --no-fail-on-empty-changeset --stack-name ${任意のスタック名} --capabilities CAPABILITY_IAM

セルフホステッドランナーの場合

ランナー側にプロファイルが設定されている場合は、コマンドに --profile を追加します 。

name: Deploy

on:
  push:
    branches:
      - main
      - develop
jobs:
  build-deploy:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v3
      - name: Install Node.js
        uses: actions/setup-node@v3
        with:
          node-version: 20
      - uses: aws-actions/setup-sam@v2
        with:
          use-installer: true
      - uses: aws-actions/configure-aws-credentials@v3
        with:
          aws-region: ap-northeast-1
      - run: sam build  --profile ${プロファイル名}  --use-container
      - name: sam deploy Prod
        if: ${{ github.ref_name == 'main' }}
        run: sam deploy --profile ${プロファイル名} --no-confirm-changeset --no-fail-on-empty-changeset --stack-name ${任意のスタック名} --capabilities CAPABILITY_IAM
      - name: sam deploy Dev
        if: ${{ github.ref_name == 'develop' }}
        run: sam deploy --profile ${プロファイル名} --no-confirm-changeset --no-fail-on-empty-changeset --stack-name ${任意のスタック名} --capabilities CAPABILITY_IAM

SAMテンプレートの作成

sam initコマンドにより、そのまま扱えるHelloWorldのSAMテンプレートは存在しますが、自前で用意した関数を設定したい場合は以下の様に記述します。

Resources:
  HelloWorldFunction:
    ...
  TestFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: test-function/ ##作成した新規関数フォルダー名
      Handler: index.handler
      Runtime: nodejs20.x
      Architectures:
      - x86_64
      Events:
        TestFunctionApi:
          Type: Api
          Properties:
            Path: /test-function ##エンドポイントから新規fucntion叩く際のpath
            Method: get

関数URL設定

API Gatewayを使用してLambdaを叩く形式では、特有の制限により「処理時間」や「データサイズ」の面で扱いづらさがあります。

例えば、S3やStep Functionsなどと連携した重いバッチ処理を行う場合、API Gatewayの「29秒タイムアウト」の制約により、Lambdaの実行可能時間を十分に活かすことができなくなります。また、画像などのバイナリデータのアップロードなどにおいても、API Gatewayのペイロードサイズ制限(最大10MB)がボトルネックになるケースが挙げられます。

これらのうち、特にAPI Gateway特有の制限を回避する有力な選択肢として「Lambda関数URL」があります。これを利用すれば、API Gatewayを介さずHTTPSで直接Lambdaを実行でき、制限をLamba本来の仕様にでき、関数ごとの固有エンドポイントによってシンプルな構成で連携が可能になります。

以下Lambda関数URLの設定方法になります。

1. 既存のSAMのtemplate.yamlのリソースに以下の設定を加える

HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello-world/
      Handler: index.handler
      Runtime: nodejs20.x
      Architectures:
      - x86_64
      Events:
        HelloWorldFunctionApi:
          Type: Api
          Properties:
            Path: /hello
            Method: get
      FunctionUrlConfig:  # 追記: 関数URL化設定
        AuthType: NONE   # 追記: URLのパブリック設定

deploy完了後に設定した関数URLを確認できるよう、既存template.yamlにOutputsを追加しておくと楽です。

Outputs:
  HelloWorldFunctionUrlEndpoint:
    Description: "My Lambda Function URL Endpoint"
    Value: !GetAtt HelloWorldFunctionUrl.FunctionUrl

Lambda関数URLのCloudFront設定

設定したLambdaURLは、そのままだとURLを知っていれば誰でも叩けてしまい、かつリクエストが多ければLambdaの利用コストが上がってしまいます。そのため、CloudFrontを被せるのが一番バランスの良い構成になります。キャッシュによるレスポンスの高速化も実現できるため、設定しておくに越したことはないものです。

以下、設定手順になります。

  1. AWS CloudFrontにアクセスする
  2. Lambda関数を設定したいサービスのディストリビューションを選択
  3. 「オリジン」のタブに遷移し、「オリジンを作成する」ボタンを押下して新規作成を開始する
  4. 以下の項目を設定
    1. Origin Domain: 作成したLambdaの関数URLを指定
    2. プロトコル: HTTPSのみ
      1. HTTPSポート: 443
      2. Minimum Origin SSL protocol: TLSv1.2
    3. OriginPath: 設定不要
    4. Origin access control: 「Create New OAC」を押下し、作成したOACを設定
      1. 名前のみ適当に設定、後はデフォルトのままOACを作成
      2. 作成が完了するとAWSのCLIコマンドが表示されるため、自分のローカルでコマンドを叩く
      3. カスタムヘッダーを追加: 設定不要
      4. Enable Origin Shield: なし
      5. 追加設定: デフォルトでOK
      6. オリジン作成
  5. オリジンの作成完了後はプロジェクトにあったビヘイビア設定を実施
  6. ビヘイビアの設定が完了したら、サービスのURLにビヘイビアで設定したパスにアクセスし設定したLambda関数のレスポンスが帰って来ることを確認する
  7. オリジナルのLambda関数URLを無闇に叩かれないよう、SAMテンプレート側にアクセス制御の設定を有効にする
  HelloWorldFunction:
      Type: AWS::Serverless::Function
      Properties:
        CodeUri: hello-world/
        Handler: index.handler
        Runtime: nodejs20.x
        Architectures:
        - x86_64
        Events:
          HelloWorldFunctionApi:
            Type: Api
            Properties:
              Path: /hello
              Method: get
        FunctionUrlConfig:
          AuthType: AWS_IAM   # 変更: NONE → AWS_IAM、認証情報を持つクライアントのみアクセス可にする

SAMは更新したらコミット/プッシュして再度デプロイを実施するのを忘れないでください。

もし余力があれば、CloudFrontを前段にWAFを適用することをオススメします。Lambda関数URL単体ではWAFを直接設定することができないため、悪意のあるリクエストやDDoS攻撃を関数レベルで防ぐことが困難です。そのため、CloudFrontを設定している今回のケースではWAFを使うことで、よりセキュアな環境を構築することができます。

ハマりポイント

IAMのポリシー設定

SAMでのデプロイには、CloudFormationがリソースを操作するための権限が必要です 。最小権限を攻める場合は、以下のポリシーの付与が必要になります。

  1. AWSCloudFormationFullAccess
  2. IAMFullAccess (※フルアクセスがNGであれば最低限以下のポリシーを許可する)
    1. iam:CreateRole
    2. iam:AttachRolePolicy
    3. iam:PutRolePolicy
    4. iam:TagRole
    5. iam:PassRole
  3. AWSLambda_FullAccess
  4. AmazonAPIGatewayAdministrator
  5. AmazonS3FullAccess (※フルアクセスがNGであれば最低限以下のポリシーを許可する)
    1. PutObject
    2. GetObject
    3. ListBucket

権限の設定が漏れていたりすると以下の様なエラーが発生します。エラーには大抵対象ポリシーの権限不足を指摘してくれているため、順に対応していけば解消可能です。

An error occurred (AccessDenied) when calling the AssumeRole operation: User: arn:aws:sts::************:assumed-role/Github-Runner-Policy/i-***************** is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::************:role/************

ローカル起動

事業部内ではDockerではなくRancher Desktopを利用しているため、そのままではうまく行きませんでした。 標準では /var/run/docker.sock が参照できず、エラーが発生します。

$ sam local invoke
No current session found, using default AWS::AccountId
Error: Running AWS SAM projects locally requires a container runtime. Do you have Docker or Finch installed and running?

解決策:

Rancher Desktopの設定画面で 「Administrative Access」を有効化 し、コンテナエンジンを 「dockerd(moby)」 に変更してください。 これにより、SAM側から正しくコンテナランタイムを認識できるようになります。

Rancher Desktop 設定1

rancher desktop 設定2

SAMテンプレートの命名規則

SAMテンプレートをカスタマイズする際、CloudFormationの命名規則従う必要があり、次の様なSAM特有の慣習が存在します。

下記の命名規則に従わなかった場合、単純にデプロイエラーや、既存のFunctionの論理IDをリネームにでAWS上の古い関数が削除され、期待しないリソースの再作成などが起きるため注意が必要です。

  1. 論理ID(Resources/Outputsのキー)
    1. ルール: 英数字のみ、先頭は英字、PascalCaseが慣習
    2. 例: TestFunction, ApplicationResourceGroup, TestApiFunctionUrlEndpoint
  2. Functionの論理ID
    1. ルール: 末尾にFunctionを付けるのが一般的
    2. 例: TestFunction, HelloWorldFunction
  3. イベント名(Events内)
    1. ルール: 目的 + 種別でわかりやすく
    2. 例: TestApi, GetUserApi, PostOrderApi
  4. Outputs名
    1. ルール: 対象 + 種別で明確に
    2. 例: TestFunctionArn, TestFunctionUrl, TestApiEndpoint
  5. 物理名(FunctionNameなど)
    1. ルール: 明示しない場合は自動生成。明示するならAWSの制約に従う
    2. 例: FunctionName: test-code-dev

コストについて

AWS SAM自体の利用に追加料金はかかりません 。ただし、SAMによってデプロイされたLambdaやCloudFrontなどのリソース使用量に応じて、AWSの従量課金が発生します 。

今回の構成では、API Gatewayを介さず「関数URL」を直接利用しています 。これにより、API Gatewayのリクエスト数やデータ処理量に応じて発生する課金を完全にゼロに抑えることができ、インフラ費用をLambdaの実行時間とCloudFrontのデータ転送量のみに集約できるのが最大のコストメリットとなっています。

終わりに

今回のSAMを使った対応により「アプリ全体の動作が劇的に軽くなったか?」と問われれば、正直なところ、現時点での答えは 「NO」 です。

理由は単純で、まだ適用している箇所が局所的な部分に留まっているからになります。しかし、今回の構築によって、将来的に重い処理を次々とクラウド側へ逃がしていくための「土台」は整いました。今後のパフォーマンス改善における期待度は高いと感じています。

何より大きな収穫は、「お手軽に作成したNode.jsの関数をPushするだけで、すぐにLambdaアプリとして公開できる体制」 ができたことです。

これにより、開発を進める中で出てくる「ちょっとここだけ処理を切り出したい」「APIのレスポンスを少しだけ整形したい」といった、いわゆる痒いところに手が届くような対応が、インフラ構成を意識せず迅速に行えるようになりました 。今回のように制約の多い環境で開発されている方がいれば、このSAMを使ってみてはいかかでしょうか。