AWS CDK + GitHub Actionsで作るLambdaコンテナCI/CDパイプライン(2/4)

1.はじめに

1.1.まえがき

本ブログは以下項目の4つに分かれており、本ブログはその2つ目になります。

※ 本ハンズオンの全コードはこちらのGitHubリポジトリで参照できます。

項番 ブログタイトル 概要
1 概要とAWS事前準備編 Lambdaコンテナの仕組みと環境準備
2 アプリケーションとCDK実装編(本ブログ) LambdaアプリとCDKコードの解説
3 GitHub Actions設定とデプロイ・挙動確認編 CI/CDパイプライン構築
4 トラブルシューティング集 ハマりポイントと解決方法まとめ

1.2.今回の内容

事前準備ができたので、今回はアプリケーションとインフラコードを作成します。

今回でやること:

  1. Lambda関数コードの作成:シンプルなHTTP APIエンドポイントのコード作成
  2. Dockerfileの作成:コンテナベースLambdaの環境構築コード作成
  3. スタック定義の実装:全インフラリソースをコードで定義

前提条件:

以下が完了していることを前提とします:

2.ハンズオン

2.1.前提

2.1.1.実行環境

2.1.2.全体のファイル構成

  • 以下のファイル構成をAmazo SageMaker Studio Code Editor(IDE)で構築する

■ 本手順の最終的なファイル構成

your-app/
├── .gitignore              #【前回作成】
├── .github/
│   └── workflows/
│       └── deploy.yml      # 【次回作成】GitHub Actions ワークフロー定義
├── app/
│   ├── app.py              #【★今回作成★】
│   └── Dockerfile          #【★今回作成★】
└── cdk/                    # AWS CDK アプリケーション (Python)
    ├── app.py              #【★今回作成★】CDK エントリーポイント
    ├── cdk.json            #【★今回作成★】CDK 設定ファイル
    ├── requirements.txt    #【★今回作成★】CDK 依存関係
    ├── .venv/              # Python 仮想環境
    └── pipeline_stack/     # スタック定義を置くディレクトリ
        ├── __init__.py     #【★今回作成★】
        └── pipeline_stack.py #【★今回作成★】 CDK スタック定義 (Python)

2.1.3.アプリケーションの説明

  • 機能: HTTP GETリクエストを受け取ると、事前に設定されたメッセージと、現在実行されているLambda関数のバージョン情報、リクエストIDをJSON形式で返却する。
  • CI/CDパイプライン構築が目的のため、アプリケーション自体のロジックは最小限にしている。

2.1.4.インフラの説明

  • AWS上にサーバーレスなAPIバックエンドと、そのCI/CDパイプラインを構築する。

■ 利用するリソース

項番 提供元 リソース名 概要
1 AWS AWS Lambda アプリケーションコード(コンテナイメージ)を実行するコンピューティングサービス。
2 AWS Amazon ECR Lambda関数で利用するDockerコンテナイメージを保存・管理するためのプライベートリポジトリです。GitHub Actionsでビルドしたイメージがここにプッシュされる。
3 AWS Amazon API Gateway Lambda関数を外部(インターネット)からHTTPリクエストで呼び出すためのエンドポイントを作成
4 AWS AWS CodeDeploy Lambda関数の新しいバージョンを安全にデプロイするためのサービスです。今回 カナリアリリース戦略を採用する。
5 AWS IAM GitHub ActionsがAWSリソース(ECRへのプッシュ、CDKによるデプロイなど)を操作するため、またLambda関数やCodeDeployが必要な権限で動作するためのロールとポリシーを作成・管理。OIDC (OpenID Connect) を利用してGitHub ActionsとAWS間の安全な認証連携を実現する。
6 AWS AWS CDK (Cloud Development Kit) 上記のAWSリソース構成をPythonコードで定義し、CloudFormationスタックとしてプロビジョニング(作成・更新)する。
7 GitHub GitHub Actions GitHubリポジトリへのコードプッシュをトリガーとして、アプリケーションのビルド(Dockerイメージ作成)、ECRへのプッシュ、CDKによるAWS環境へのデプロイを自動化するCI/CDパイプラインを構築。

2.1.5.本項目での構築箇所

file

2.2.gitignoreファイル作成

  • Amazo SageMaker Studio Code Editor(IDE)以下コマンドを実行していく作業
  • ファイル作成の目的としては、Gitに追跡させたくないファイルを指定する。
# フォルダ作成
mkdir your-app

# ファイル作成
cat > your-app/.gitignore << EOF
# Python関連(コンパイル済みファイルや一時ファイル)
*.py[cod]        # コンパイル済みPythonファイル(*.pyc, *.pyo, *.pyd)
*.pyo            # Python最適化ファイル  
__pycache__/     # Pythonキャッシュフォルダ
.env             # 環境変数ファイル(シークレット含むかも!)
*.egg-info/      # パッケージ情報フォルダ
dist/            # ビルド成果物
build/           # ビルド一時フォルダ
wheels/          # パッケージ配布用ファイル
*.egg            # 古い形式のパッケージ
.venv/           # Python仮想環境(ローカル用)
venv/            # 別名の仮想環境フォルダ

# AWS CDK関連(ビルド時に自動生成される)
cdk.out/         # CDKが生成するCloudFormationテンプレート置き場
cdk.context.json # CDKが保存する設定キャッシュ
.cdk.staging/    # CDKステージング用

# IDE/エディタ固有(ローカル設定)
.vscode/         # VSCode設定フォルダ
.idea/           # IntelliJ/PyCharm設定
*.swp            # Vimのスワップファイル
*~               # バックアップファイル

# OS固有(無駄ファイル)
.DS_Store        # macOSが作るサムネ情報
Thumbs.db        # Windowsが作るサムネ情報
EOF

2.3.アプリケーション作成

  • アプリケーションレイヤにあたる部分を作成。
  • コンテナベースのLambdaのため、Dockerfileで環境を制御できるという特徴がある。

2.3.1.Lambda関数作成

  • HTTP GETを受け取ると、事前に設定されたメッセージをJSON形式で返却する。
# フォルダ作成
mkdir -p your-app/app

# ファイル作成
cat > your-app/app/app.py << EOF
import json
import os

def handler(event, context):
    # Lambda実行時の環境変数 'MESSAGE' からメッセージを取得
    # 取得できない場合はデフォルトメッセージを使用
    message = os.environ.get('MESSAGE', 'Hello from Lambda Container!')
    print("Received event: " + json.dumps(event, indent=2))
    return {
        'statusCode': 200,
        'body': json.dumps({
            'message': message,
            'version_id': context.function_version,
            'aws_request_id': context.aws_request_id
        })
    }
EOF

2.3.2.Dockerfile作成

  • LambdaコンテナイメージをビルドするためのDockerfileを作成。
  • AWS が提供する公式ベースイメージを使うことで、必要な環境が含まれている。
# ファイル作成
cat > your-app/app/Dockerfile << EOF
# AWS公式提供のベースイメージを使用
FROM public.ecr.aws/lambda/python:3.11

# ハンドラコードをコピー
COPY app.py ./

# Lambdaが呼び出すハンドラを指定
CMD [ "app.handler" ]
EOF

2.4.CDK 作成

  • CDK(AWS Cloud Development Kit)を使ってインフラストラクチャをコードで定義していく。

2.4.1.app.py 作成

  • CDKアプリケーションのエントリーポイント。ここで必要なスタックを読み込み、AWS環境に対してデプロイを実行する。
  • CDK_DEFAULT_ACCOUNTCDK_DEFAULT_REGIONはCDK実行時に自動設定される環境変数。
  • app.synth()により CloudFormationテンプレートが生成される。
# フォルダ作成
mkdir -p your-app/cdk

# CDKアプリケーションファイル作成
cat > your-app/cdk/app.py << EOF
import os
import aws_cdk as cdk

# 実際のインフラ定義(スタック)をインポート
from pipeline_stack.pipeline_stack import PipelineStack

# CDKアプリケーションのインスタンス作成
app = cdk.App()

# スタックをアプリに追加(インフラの実体)
PipelineStack(app, "LambdaPipelineStackPy", # CFnスタック名
    # デプロイ先のAWS環境を指定
    env=cdk.Environment(
        account=os.getenv('CDK_DEFAULT_ACCOUNT'),
        region=os.getenv('CDK_DEFAULT_REGION')
    )
)

# CloudFormationテンプレートを生成(cdk.out/ディレクトリに出力)
app.synth()
EOF

2.4.2.requirements.txt 作成

  • CDKで利用するライブラリを定義するファイル
# requirementsファイルの作成
cat > your-app/cdk/requirements.txt << EOF
# 基本ライブラリ
aws-cdk-lib>=2.194.0,<3.0.0 # CDKコアライブラリ(V2)
constructs>=10.0.0,<11.0.0 # コンストラクトベースライブラリ

# Alpha モジュールのため、APIが変更される可能性がある
aws-cdk.aws-apigatewayv2-alpha
aws-cdk.aws-apigatewayv2-integrations-alpha
EOF

2.4.3.cdk.json 作成

  • CDKアプリケーションの設定ファイル。CDK CLIがどのようにアプリを実行するかを指定する。
  • context配下に設定されてるものが1つずつの機能フラグ
     - "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true であれば、APIGatewayの機能(APIキーの順序を気にしない)という設定
cat > your-app/cdk/cdk.json << EOF
{
  "app": "python3 app.py",
  "context": {
   "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true,
   "@aws-cdk/core:stackRelativeExports": true,
   "@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
   "@aws-cdk/aws-lambda:recognizeVersionProps": true,
   "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true,
   "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
   "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
   "@aws-cdk/aws-iam:minimizePolicies": true,
   "aws-cdk:enableDiffNoFail": "true",
   "aws-cdk:cx-api": "true"
  }
}
EOF

2.5.スタック定義部分の作成

2.5.1.スタック定義の意味

  • app.pyから呼び出されて実行される、実際のAWSリソースを定義する重要なファイル
  • 本アプリでは以下のような役割を設計している
    1. 設定値の一元管理:リソースの設定値を上部にまとめて定義
    2. リソース依存関係の管理:AWS各サービスを適切な順序で定義
    3. CodeDeployによるデプロイ:カナリアリリースで段階的デプロイを定義

2.5.2.本アプリでの AWSリソース設定値

項番 リソース名 設定項目 デフォルト値 説明
1 Lambda メモリ 512MB 十分な動作速度と適度なコスト
タイムアウト 30秒 API応答には充分
メッセージ Hello from CDK! 環境変数として注入
2 API Gateway API名 LambdaContainerApiPy 識別しやすい名前
APIパス /hello シンプルなGETエンドポイント
3 CodeDeploy アプリ名 MyLambdaApplicationPy デプロイアプリ識別用
エイリアス名 live 本番トラフィックを表す
4 ECRイメージ タグ latest/カスタム GitHub Actionsから渡される

2.5.3.Qualifierの取得

スタック定義で必要なECRリポジトリ名にはQualifierが含まれます。
以下のコマンドをCloudShell環境で実行して、自身の環境の値を取得しコード内 # ECRリポジトリ名 (Bootstrapで作成されたもの)を修正

# CDKが作成したS3バケット名を取得
BUCKET_NAME=$(aws cloudformation describe-stack-resources --stack-name CDKToolkit --query "StackResources[?ResourceType=='AWS::S3::Bucket'].PhysicalResourceId" --output text --region us-east-1)

# Qualifierを抽出(バケット名のパターン:cdk-{QUALIFIER}-...)
YOUR_QUALIFIER=$(echo "$BUCKET_NAME" | cut -d'-' -f2)

# 確認出力
echo $YOUR_QUALIFIER
2.5.4.ハンズオン開始
# フォルダ作成
mkdir -p your-app/cdk/pipeline_stack

# パッケージ初期化ファイル作成
touch your-app/cdk/pipeline_stack/__init__.py

# ファイル作成
cat > your-app/cdk/pipeline_stack/pipeline_stack.py << EOF
import os
import aws_cdk as cdk
from constructs import Construct
from aws_cdk import (
    aws_ecr as ecr,
    aws_lambda as lambda_,
    aws_iam as iam,
    aws_codedeploy as codedeploy,
    aws_apigatewayv2_alpha as apigwv2,
    aws_apigatewayv2_integrations_alpha as apigw_integrations,
    Duration,
    RemovalPolicy,
    Stack,
)

class PipelineStack(Stack):
    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # ======== 設定変数(ここにまとめる) ========

        # Lambda関連
        LAMBDA_MEMORY = 512  # Lambda関数のメモリサイズ(MB)
        LAMBDA_TIMEOUT = 30  # Lambda関数のタイムアウト(秒)
        LAMBDA_MESSAGE = "Hello from CDK !"  # 出力させるメッセージ

        # API Gateway関連
        API_NAME = "LambdaContainerApiPy"  # API名
        API_PATH = "/hello"  # APIパス

        # CodeDeploy関連
        APP_NAME = "MyLambdaApplicationPy"  # CodeDeployアプリケーション名
        ALIAS_NAME = "live"  # Lambdaエイリアス名

        # ECRリポジトリ名 (Bootstrapで作成されたもの)
        ECR_REPO_NAME = f"cdk-★★自身の Qualifier★★-container-assets-{cdk.Aws.ACCOUNT_ID}-{cdk.Aws.REGION}"

        # GitHub Actionsから渡されるイメージタグ (Contextから取得)
        IMAGE_TAG = self.node.try_get_context("image_tag")
        if not IMAGE_TAG:
            # フォールバックまたはエラー処理
            print("Warning: image_tag context not found. Using 'latest'.")
            IMAGE_TAG = "latest"
            # raise ValueError("Context variable 'image_tag' is required.")
        print(f"--- Using Image Tag from context: {IMAGE_TAG} ---") # 確認用プリント

        # ======== リソース定義 ========
        # --- ECR リポジトリの参照 ---
        repository = ecr.Repository.from_repository_name( # リポジトリを参照
            self, "LambdaEcrRepo",
            repository_name=ECR_REPO_NAME
        )

        # --- Lambda 関数の IAM ロール ---
        lambda_role = iam.Role(
            self, "LambdaExecutionRole",
            assumed_by=iam.ServicePrincipal("lambda.amazonaws.com"),
            managed_policies=[
                iam.ManagedPolicy.from_aws_managed_policy_name("service-role/AWSLambdaBasicExecutionRole")
            ],
        )

        # --- Lambda 関数 (ECRイメージを参照) ---
        my_function = lambda_.Function(
            self, "MyLambdaFunction",
            code=lambda_.Code.from_ecr_image(
                repository=repository, # リポジトリオブジェクトを渡す
                tag_or_digest=IMAGE_TAG # タグを渡す
                # image_uri は使わない
            ),
            handler=lambda_.Handler.FROM_IMAGE,
            runtime=lambda_.Runtime.FROM_IMAGE,
            role=lambda_role,
            environment={"MESSAGE": LAMBDA_MESSAGE},
            memory_size=LAMBDA_MEMORY,
            timeout=Duration.seconds(LAMBDA_TIMEOUT),
            current_version_options=lambda_.VersionOptions(
                removal_policy=RemovalPolicy.RETAIN,
            )
        )

        # --- Lambda エイリアス (CodeDeployが管理) ---
        alias = lambda_.Alias(
            self, "LiveAlias",
            alias_name=ALIAS_NAME,
            version=my_function.current_version,  # 初期状態では最新バージョンを指す
        )

        # --- CodeDeploy アプリケーション & デプロイグループ ---
        application = codedeploy.LambdaApplication(
            self, "CodeDeployApplication",
            application_name=APP_NAME,
        )

        codedeploy.LambdaDeploymentGroup(
            self, "DeploymentGroup",
            application=application,
            alias=alias,
            deployment_config=codedeploy.LambdaDeploymentConfig.CANARY_10_PERCENT_5_MINUTES,  # カナリアリリース設定
            auto_rollback=codedeploy.AutoRollbackConfig(
                # 必要に応じて自動ロールバック設定
                failed_deployment=True,  # デプロイ失敗時にロールバック
                # deployment_in_alarm=True, # アラーム発報時にロールバック (別途CloudWatch Alarmの設定が必要)
            )
        )

        # --- API Gateway (HTTP API) ---
        http_api = apigwv2.HttpApi(
            self, "MyHttpApi",
            api_name=API_NAME,
            description="API Gateway for Lambda Container (Python CDK)",
            # CORS設定など必要に応じて追加
        )

        http_api.add_routes(
            path=API_PATH,
            methods=[apigwv2.HttpMethod.GET],
            integration=apigw_integrations.HttpLambdaIntegration("LambdaIntegration", alias)
        )

        # --- Outputs ---
        cdk.CfnOutput(self, "LambdaFunctionName", value=my_function.function_name)
        cdk.CfnOutput(self, "LambdaFunctionArn", value=my_function.function_arn)
        cdk.CfnOutput(self, "LambdaLiveAliasArn", value=alias.function_arn)
        # API Gateway の URL の末尾スラッシュを削除してからパスを結合
        api_endpoint_url = http_api.url.rstrip('/') if http_api.url else None
        cdk.CfnOutput(
            self, "ApiEndpoint",
            value=f"{api_endpoint_url}{API_PATH}" if api_endpoint_url else "NoAPIGateway"
        )
EOF

3.おわりに

今回は第2回 アプリケーションとCDK実装編を記載しました。

今回実施したことサマリ:

  • シンプルなLambdaアプリケーションファイル作成
  • Dockerfileで公式イメージをベースにLambda環境を構築するファイル作成
  • CDKでインフラをコード化
    • ECR・Lambda・API Gateway・CodeDeployをPythonで定義
    • カナリアリリース設定でデプロイの安全性を確保

次回は実際のGitHub Actions設定とデプロイ・挙動確認編で続きの構築をしていきたいと思います。

Last modified: 2025-05-04

Author