はじめに

コマンドラインツールをインストールして Lambda で実行するため、ECR のコンテナイメージで動作する AWS Lambda を実装し、GitHub Actions からデプロイする機会がありましたので、その手順を記載します。

GitHub Actions から AWS SAM を実行して、AWS Lambda のデプロイを行います。

やること

AWS Lambda へデプロイするプログラムをローカルでテストする環境を構築

準備

準備するファイルとフォルダ構成
VSCode Dev Container を使ってローカルでテストする環境を作成します。

|--.devcontainer
| |--devcontainer.json
|--.vscode
| |--extensions.json
| |--launch.json
| |--settings.json
|--Dockerfile
|--lambda
| |--app.py
| |--requirements.txt

.devcontainer/devcontainer.json
コンテナ内で Python のデバッグができる様に VSCode の拡張機能をインストールする指定を入れています。
Lambda 実行プログラムの配置先である「/var/task」をカウントしています。

{
    "name": "lambda_ecr",
    "build": {
        "dockerfile": "${localWorkspaceFolder}/Dockerfile",
        "args": {}
    },
    "runArgs": [
        "--name=lambda_ecr",
        "--volume=${localWorkspaceFolder}/lambda:/var/task",
        "--volume=${localWorkspaceFolder}/.vscode:/var/task/.vscode"
    ],
    "workspaceFolder": "/var/task",
    "customizations": {
        "vscode": {
            "extensions": [
                "ms-python.autopep8",
                "ms-python.python",
                "ms-python.vscode-pylance",
            ]
        }
    }
}

.vscode/extensions.json
コンテナ外で使用する VSCode の拡張機能を定義しています。

{
    "recommendations": [
        "ms-python.autopep8",
        "ms-python.python",
        "ms-python.vscode-pylance",
        "ms-vscode-remote.remote-containers"
    ]
}

.vscode/launch.json
Docker 内でデバッグ起動する際に使用します。

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Lambda",
            "type": "debugpy",
            "request": "launch",
            "program": "${workspaceFolder}/app.py",
            "console": "integratedTerminal",
            "args": []
        }
    ]
}

.vscode/settings.json
フォーマット定義

{
    "editor.tabSize": 4,
    "editor.renderWhitespace": "all",
    "terminal.integrated.scrollback": 10000,
    "extensions.ignoreRecommendations": false,
    "editor.formatOnSave": true,
    "editor.formatOnPaste": true,
    "": {
        "editor.defaultFormatter": "ms-python.autopep8",
        "editor.formatOnSave": true
    },
}

lambda/app.py
Lambdaメイン処理
「python app.py」で実行できる様に、if __name__ == "__main__":を定義しています。

import json


def lambda_handler(event, context):
    """
    Lambdaメイン処理
    """
    print(json.dumps(event, ensure_ascii=False))
    return {
        'statusCode': 200,
        'body': 'Hello World!',
    }


if __name__ == "__main__":
    event = {}
    context = {}
    print(lambda_handler(event, context))

lambda/requirements.txt
ライブラリインポート用
空のファイルを準備します。

Dockerfile
tar と gzip は、コンテナ内で VSCode の拡張機能をインストールするために必要です。

FROM public.ecr.aws/lambda/python:3.12

RUN set -ex \
 && dnf -y update \
 && dnf install -y tar gzip

COPY lambda/ ${LAMBDA_TASK_ROOT} 

RUN pip3 install -r requirements.txt

ENV \
 TZ='Asia/Tokyo'

CMD [ "app.lambda_handler" ]

Dev Container起動

左下「><」アイコンをクリックして、「コンテナを再度開く」を選択します。
DevContainer起動
左メニューの拡張機能アイコンをクリック、「@recommended 」を入力して推奨される拡張機能の一覧を表示して、「ワークスペースのおすすめの拡張機能をインストール」を実行してインストールします。
ワークスペースのおすすめの拡張機能をインストール

デバッグ起動

左メニューのデバッグアイコンをクリック、ブレークポイントを設定してデバッグ起動するとブレークポイントで止まることを確認します。
デバッグ起動

Github Actions からデプロイする設定

GitHub Actions から AWS へアクセスするため GitHub ID プロバイダ作成

GitHub Actions で IAM ロールを利用して AWS へアクセスするために、ID プロバイダを登録します。一時的なクレデンシャルを利用して GitHub Actions から AWS へアクセスするためよりセキュアに接続することができます。
IAM から、アクセス管理 – プロバイダを追加を実行します。
プロバイダの URL:OpneID Connect
プロバイダーの URL: https://token.actions.githubusercontent.com
対象者: sts.amazonaws.com
ID-プロバイダを作成

GitHub Actions 用の IAM ポリシー、IAM ロール作成

IAM で、GitHub Actions 用のIAMポリシーを作成します。下記権限のポリシーをポリシー名「lambda-ecr-deploy-policy」で作成します。

  • CloudFormation テンプレートファイルを S3 へ格納するため S3 の権限が必要
  • CloudFormation を実行する権限が必要
  • Lambda に対する権限が必要
  • ECR プッシュする権限が必要
  • (この例では必要なかったが)CloudFormation テンプレートファイルで IAM に関する定義がある場合は権限が必要
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "0",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject"
            ],
            "Resource": "arn:aws:s3:::lambda-ecr-deploy-{AWSアカウントID}/*"
        },
        {
            "Sid": "1",
            "Effect": "Allow",
            "Action": "cloudformation:*",
            "Resource": [
                "arn:aws:cloudformation:ap-northeast-1:{AWSアカウントID}:stack/lambda-ecr-deploy-stack/*",
                "arn:aws:cloudformation:ap-northeast-1:aws:transform/Serverless-2016-10-31"
            ]
        },
        {
            "Sid": "2",
            "Effect": "Allow",
            "Action": "lambda:*",
            "Resource": [
                "arn:aws:lambda:ap-northeast-1:{AWSアカウントID}:function:lambda-ecr-deploy"
            ]
        },
        {
            "Sid": "3",
            "Effect": "Allow",
            "Action": [
                "ecr:GetAuthorizationToken",
                "ecr:BatchCheckLayerAvailability",
                "ecr:GetDownloadUrlForLayer",
                "ecr:GetRepositoryPolicy",
                "ecr:DescribeRepositories",
                "ecr:ListImages",
                "ecr:DescribeImages",
                "ecr:BatchGetImage",
                "ecr:GetLifecyclePolicy",
                "ecr:GetLifecyclePolicyPreview",
                "ecr:ListTagsForResource",
                "ecr:DescribeImageScanFindings"
            ],
            "Resource": [
                "*"
            ]
        },
        {
            "Sid": "4",
            "Effect": "Allow",
            "Action": [
                "ecr:BatchCheckLayerAvailability",
                "ecr:InitiateLayerUpload",
                "ecr:UploadLayerPart",
                "ecr:CompleteLayerUpload",
                "ecr:PutImage"
            ],
            "Resource": [
                "arn:aws:ecr:ap-northeast-1:{AWSアカウントID}:repository/lambda-ecr-deploy-ecr"
            ]
        },
        {
            "Sid": "5",
            "Effect": "Allow",
            "Action": [
                "iam:CreateRole",
                "iam:DetachRolePolicy",
                "iam:AttachRolePolicy",
                "iam:GetRole",
                "iam:DeleteRole",
                "iam:PassRole"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

IAM で、GitHub Actions 用の IAM ロールを作成します。下記権限のポリシーをポリシー名「lambda-ecr-deploy-role」で作成します。
信頼されたエンティティタイプ:カスタム信頼ポリシー
カスタム信頼ポリシー

  • IDプロバイダーを使用すること宣言
  • 接続するGitHubリポジトリを定義
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::{AWSアカウントID}:oidc-provider/token.actions.githubusercontent.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
                },
                "StringLike": {
                    "token.actions.githubusercontent.com:sub": "repo:{GitHubリポジトリ}:*"
                }
            }
        }
    ]
}

GitHubActions用IAMロール

Lambda 用のIAMポリシー、IAMロール作成

IAM で、Lambda 用のIAMポリシーを作成します。下記権限のポリシーをポリシー名「lambda-ecr-deploy-run-policy」で作成します。

  • LambdaからCloudWatchへログ出力するのに必要な権限のみ定義
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "0",
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}

IAM で、Lambda 用の IAM ロールを作成します。下記権限のポリシーをポリシー名「lambda-ecr-deploy-run-role」で作成します。
信頼されたエンティティタイプ:AWS のサービス
サービスまたはユースケース:Lambda
Lambda 用 IAM ロール

ECR リポジトリ作成

ECR 名「lambda-ecr-deploy-ecr」、プライベートな ECR 定義を作成します。

S3 バケット作成

バケット名「lambda-ecr-deploy-{AWSアカウントID}」で作成します。

Lambda 用の CloudFormation テンプレート作成

ファイルを追加して、Lambda 用の CloudFormation テンプレートを作成します。

|--.devcontainer
|  |--devcontainer.json
|--.vscode
|  |--extensions.json
|  |--launch.json
|  |--settings.json
|--Dockerfile
|--lambda
|  |--app.py
|  |--requirements.txt
|--lambda-stack.yaml ← ★追加★

lambda-stack.yaml
Lambda 本体の定義のみで、作成済みの IAM ロールと ECR リポジトリを指定しています。ImageConfig\Command で実行する Lambda 関数を指定しています。

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

Parameters:
  Project:
    Type: String
  AccountId:
    Type: String

Resources:
  EcrFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub ${Project}
      CodeUri: lambda/
      Role: !Sub arn:aws:iam::${AWS::AccountId}:role/${Project}-run-role
      PackageType: Image
      ImageUri: !Sub ${AWS::AccountId}.dkr.ecr.ap-northeast-1.amazonaws.com/${Project}-ecr:latest
      ImageConfig:
        Command:
          - "app.lambda_handler"

GitHub Actions 定義作成

ファイルを追加して、GitHub Actions 用のワークフローファイルを作成します。

|--.devcontainer
|  |--devcontainer.json
|--.github
|  |--workflows
|  |  |--lambda_deploy_dev.yaml ← ★追加★
|--.vscode
|  |--extensions.json
|  |--launch.json
|  |--settings.json
|--Dockerfile
|--lambda
|  |--app.py
|  |--requirements.txt
|--lambda-stack.yaml

lambda_deploy_dev.yaml
処理の流れは

  • GitHub 資源チェックアウト
  • GitHub 用 IAM ロールのセッション受入
  • ECR ログイン
  • Docker ビルド
  • ECR へDocker イメージプッシュ
  • AWS SAM コマンドで Lambda デプロイ
  • Lambda の新しいイメージをデプロイ実行(ECR へ Docker イメージプッシュしただけでは、Lambda の参照 ECR イメージは最新化されないため、最新化するコマンドを実行する)
name: Lambda Deploy Development
on:
  workflow_dispatch:

env:
  AWS_ACCOUNT_ID: {AWSアカウントID}
  AWS_IAM_ROLE_ARN: arn:aws:iam::{AWSアカウントID}:role/lambda-ecr-deploy-role
  AWS_DEFAULT_REGION: ap-northeast-1
  PROJECT: lambda-ecr-deploy
  SAM_CLI_TELEMETRY: 1

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read

    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ env.AWS_IAM_ROLE_ARN }}
          role-session-name: github-actions-${{ github.run_id }}
          aws-region: ${{ env.AWS_DEFAULT_REGION }}

      - run: aws sts get-caller-identity

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build Docker
        working-directory: ./
        run: |
          docker build -t ${{ env.PROJECT }}-ecr -f ./Dockerfile .
          docker tag ${{ env.PROJECT }}-ecr:latest ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${{ env.PROJECT }}-ecr:latest

      - name: Push Image to Amazon ECR
        working-directory: ./
        run: |
          docker push ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${{ env.PROJECT }}-ecr:latest

      - name: ECR Lambda Stack
        env:
          STACK_NAME: ${{ env.PROJECT }}-stack
          TEMPLATE_NAME: lambda-stack.yaml
        working-directory: ./
        run: |
          sam build --use-container \
            --template-file ${TEMPLATE_NAME}
          sam deploy --no-confirm-changeset \
            --stack-name ${STACK_NAME} \
            --template-file ${TEMPLATE_NAME} \
            --s3-bucket ${{ env.PROJECT }}-${AWS_ACCOUNT_ID} \
            --image-repository ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${{ env.PROJECT }}-ecr \
            --parameter-overrides Project=${{ env.PROJECT }} AccountId=${AWS_ACCOUNT_ID} \
            --capabilities CAPABILITY_IAM \
            --no-fail-on-empty-changeset

      - name: Lambda Renew Image
        run: |
          aws lambda update-function-code \
            --function-name ${{ env.PROJECT }} \
            --image-uri ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${{ env.PROJECT }}-ecr:latest

GitHub Actions 実行

GiHub Actions のリポジトリで、「Actions」タブ押下、左メニューのワークフロー名押下、「Run workflow」押下、該当の Branch を選択して、「Run workflow」押下して、GitHub Actions を実行し、
GiHub Actions が正常終了することを確認します。
GitHubActions実行

Lambda テスト実行

GiHub Actions 正常終了後、Lambda「lambda-ecr-deploy」がデプロイされていることを確認した後に、
イベント名:任意の値
イベントJSON:{}(任意のJSON値)
を入力して「テスト」ボタン押下してテスト実行し、正常に下記が出力されていることを確認します。

{
  "statusCode": 200,
  "body": "Hello World!"
}

Lambdaテスト実行

ECR のコンテナイメージで動作する AWS Lambda を GitHub Actions からデプロイし、テスト実行できるまでできました!