
こんにちは、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からインストーラーをダウンロードしてください。
インストール完了後、バージョンが出力されることを確認します 。
$ 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を被せるのが一番バランスの良い構成になります。キャッシュによるレスポンスの高速化も実現できるため、設定しておくに越したことはないものです。
以下、設定手順になります。
- AWS CloudFrontにアクセスする
- Lambda関数を設定したいサービスのディストリビューションを選択
- 「オリジン」のタブに遷移し、「オリジンを作成する」ボタンを押下して新規作成を開始する
- 以下の項目を設定
- Origin Domain: 作成したLambdaの関数URLを指定
- プロトコル: HTTPSのみ
- HTTPSポート: 443
- Minimum Origin SSL protocol: TLSv1.2
- OriginPath: 設定不要
- Origin access control: 「Create New OAC」を押下し、作成したOACを設定
- 名前のみ適当に設定、後はデフォルトのままOACを作成
- 作成が完了するとAWSのCLIコマンドが表示されるため、自分のローカルでコマンドを叩く
- カスタムヘッダーを追加: 設定不要
- Enable Origin Shield: なし
- 追加設定: デフォルトでOK
- オリジン作成
- オリジンの作成完了後はプロジェクトにあったビヘイビア設定を実施
- ビヘイビアの設定が完了したら、サービスのURLにビヘイビアで設定したパスにアクセスし設定したLambda関数のレスポンスが帰って来ることを確認する
- オリジナルの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がリソースを操作するための権限が必要です 。最小権限を攻める場合は、以下のポリシーの付与が必要になります。
- AWSCloudFormationFullAccess
- IAMFullAccess (※フルアクセスがNGであれば最低限以下のポリシーを許可する)
- iam:CreateRole
- iam:AttachRolePolicy
- iam:PutRolePolicy
- iam:TagRole
- iam:PassRole
- AWSLambda_FullAccess
- AmazonAPIGatewayAdministrator
- AmazonS3FullAccess (※フルアクセスがNGであれば最低限以下のポリシーを許可する)
- PutObject
- GetObject
- 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側から正しくコンテナランタイムを認識できるようになります。


SAMテンプレートの命名規則
SAMテンプレートをカスタマイズする際、CloudFormationの命名規則従う必要があり、次の様なSAM特有の慣習が存在します。
下記の命名規則に従わなかった場合、単純にデプロイエラーや、既存のFunctionの論理IDをリネームにでAWS上の古い関数が削除され、期待しないリソースの再作成などが起きるため注意が必要です。
- 論理ID(Resources/Outputsのキー)
- ルール: 英数字のみ、先頭は英字、PascalCaseが慣習
- 例: TestFunction, ApplicationResourceGroup, TestApiFunctionUrlEndpoint
- Functionの論理ID
- ルール: 末尾にFunctionを付けるのが一般的
- 例: TestFunction, HelloWorldFunction
- イベント名(Events内)
- ルール: 目的 + 種別でわかりやすく
- 例: TestApi, GetUserApi, PostOrderApi
- Outputs名
- ルール: 対象 + 種別で明確に
- 例: TestFunctionArn, TestFunctionUrl, TestApiEndpoint
- 物理名(FunctionNameなど)
- ルール: 明示しない場合は自動生成。明示するならAWSの制約に従う
- 例: 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を使ってみてはいかかでしょうか。