ドキュメント構成
免責事項 本記事はAI(Claude)を活用して執筆しています。内容の正確性については保証いたしかねますので、重要な情報は必ず一次情報源をご確認ください。
この章で理解すること 実際に構築する際の具体的な手順とコード例を示します。
Terraform実装(インフラ、outputs)
Lambda Layer管理
Lambroll実装(function.jsonl、デプロイ)
GitHub Actions設定
バージョン削除の自動化
アーティファクト管理
IAMロール作成 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 # iam/lambda-roles.tf resource "aws_iam_role" "lambda_execution" { name = "lambda-execution-role" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{ Effect = "Allow" Principal = { Service = "lambda.amazonaws.com" } Action = "sts:AssumeRole" }] }) } # 基本的な実行権限 resource "aws_iam_role_policy_attachment" "lambda_basic" { role = aws_iam_role.lambda_execution.name policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole" } # CloudWatch Logs resource "aws_iam_role_policy" "lambda_logging" { name = "lambda-logging" role = aws_iam_role.lambda_execution.id policy = jsonencode({ Version = "2012-10-17" Statement = [{ Effect = "Allow" Action = [ "logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents" ] Resource = "arn:aws:logs:*:*:*" }] }) } # RDSアクセス(必要に応じて) resource "aws_iam_role_policy" "lambda_rds" { name = "lambda-rds-access" role = aws_iam_role.lambda_execution.id policy = jsonencode({ Version = "2012-10-17" Statement = [{ Effect = "Allow" Action = [ "rds-db:connect" ] Resource = aws_db_instance.main.arn }] }) }
VPC設定 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 # vpc/main.tf resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" enable_dns_hostnames = true enable_dns_support = true tags = { Name = "lambda-vpc" } } resource "aws_subnet" "private" { count = 2 vpc_id = aws_vpc.main.id cidr_block = "10.0.${count.index + 1}.0/24" availability_zone = data.aws_availability_zones.available.names[count.index] tags = { Name = "lambda-private-${count.index + 1}" } } resource "aws_security_group" "lambda" { name = "lambda-sg" description = "Security group for Lambda functions" vpc_id = aws_vpc.main.id # アウトバウンドは全許可(インターネットアクセス用) egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } # インバウンドはRDSからのみ(必要に応じて) ingress { from_port = 0 to_port = 0 protocol = "-1" security_groups = [aws_security_group.rds.id] } tags = { Name = "lambda-sg" } }
Lambda Layer管理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 # layers/dependencies/layer.tf # 依存関係Layer(pip install等) resource "aws_lambda_layer_version" "dependencies" { layer_name = "common-dependencies" filename = "${path.module}/dependencies.zip" source_code_hash = filebase64sha256("${path.module}/dependencies.zip") compatible_runtimes = ["python3.11"] lifecycle { create_before_destroy = true } } # 社内共通ユーティリティLayer resource "aws_lambda_layer_version" "company_utils" { layer_name = "company-utils" filename = "${path.module}/company-utils.zip" source_code_hash = filebase64sha256("${path.module}/company-utils.zip") compatible_runtimes = ["python3.11"] lifecycle { create_before_destroy = true } }
Layer作成スクリプト:
1 2 3 4 5 6 #!/bin/bash mkdir -p pythonpip install -r requirements.txt -t python/ zip -r dependencies.zip python/
アーティファクト保管用S3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 # data-stores/s3.tf resource "aws_s3_bucket" "artifacts" { bucket = "company-lambda-artifacts" } resource "aws_s3_bucket_versioning" "artifacts" { bucket = aws_s3_bucket.artifacts.id versioning_configuration { status = "Enabled" } } # ライフサイクルポリシー(90日以上前のバージョンを削除) resource "aws_s3_bucket_lifecycle_configuration" "artifacts" { bucket = aws_s3_bucket.artifacts.id rule { id = "delete-old-versions" status = "Enabled" noncurrent_version_expiration { noncurrent_days = 90 } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 # outputs.tf output "lambda_role_arn" { description = "Lambda execution role ARN" value = aws_iam_role.lambda_execution.arn } output "private_subnet_ids" { description = "Private subnet IDs for Lambda" value = aws_subnet.private[*].id } output "lambda_security_group_ids" { description = "Security group IDs for Lambda" value = [aws_security_group.lambda.id] } output "rds_endpoint" { description = "RDS endpoint" value = aws_db_instance.main.endpoint sensitive = true } output "layer_arns" { description = "Lambda Layer ARNs" value = { dependencies = aws_lambda_layer_version.dependencies.arn company_utils = aws_lambda_layer_version.company_utils.arn } } output "artifact_bucket" { description = "S3 bucket for Lambda artifacts" value = aws_s3_bucket.artifacts.id }
Phase 2: Lambroll実装 function.jsonl作成 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 { "FunctionName" : "user-auth-function" , "Description" : "User authentication handler" , "Runtime" : "python3.11" , "Handler" : "index.handler" , "MemorySize" : 512 , "Timeout" : 30 , "Role" : "{{ must_env `TF_ROLE_ARN` }}" , "Layers" : { { env `TF_LAYER_ARNS` | json_array } } , "VpcConfig" : { "SubnetIds" : { { env `TF_SUBNET_IDS` | json_array } } , "SecurityGroupIds" : { { env `TF_SG_IDS` | json_array } } } , "Environment" : { "Variables" : { "DB_ENDPOINT" : "{{ must_env `TF_RDS_ENDPOINT` }}" , "LOG_LEVEL" : "INFO" , "ENVIRONMENT" : "{{ env `ENVIRONMENT` | default `development` }}" } } , "Tags" : { "ManagedBy" : "Lambroll" , "Team" : "Backend" } }
Lambda関数コード 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import jsonimport osimport loggingDB_ENDPOINT = os.environ['DB_ENDPOINT' ] LOG_LEVEL = os.environ.get('LOG_LEVEL' , 'INFO' ) logger = logging.getLogger() logger.setLevel(LOG_LEVEL) def handler (event, context ): """ ユーザー認証のハンドラー """ logger.info(f"Received event: {json.dumps(event)} " ) try : result = authenticate_user(event) return { 'statusCode' : 200 , 'body' : json.dumps(result) } except Exception as e: logger.error(f"Error: {str (e)} " , exc_info=True ) return { 'statusCode' : 500 , 'body' : json.dumps({'error' : 'Internal server error' }) } def authenticate_user (event ): return {'authenticated' : True }
エイリアス初期設定 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 aws lambda create-alias \ --function-name user-auth-function \ --name development \ --function-version '$LATEST' aws lambda create-alias \ --function-name user-auth-function \ --name staging \ --function-version 1 aws lambda create-alias \ --function-name user-auth-function \ --name production \ --function-version 1
Phase 3: GitHub Actions設定 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 name: Terraform Apply on: push: branches: [main ] paths: - '**.tf' - 'environments/**' jobs: terraform: runs-on: ubuntu-latest permissions: id-token: write contents: read steps: - uses: actions/checkout@v4 - uses: hashicorp/setup-terraform@v3 with: terraform_version: 1.6 .0 - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_TERRAFORM_ROLE_ARN }} aws-region: ap-northeast-1 - name: Terraform Init run: terraform init working-directory: environments/production - name: Terraform Plan run: terraform plan -out=tfplan working-directory: environments/production - name: Terraform Apply run: terraform apply tfplan working-directory: environments/production
Lambroll Staging自動デプロイ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 name: Deploy to Staging on: push: branches: [main ] paths: - 'functions/**' 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: ${{ secrets.AWS_LAMBDA_ROLE_ARN }} aws-region: ap-northeast-1 - name: Checkout infrastructure repo uses: actions/checkout@v4 with: repository: your-org/terraform-infrastructure path: terraform-infrastructure token: ${{ secrets.GH_TOKEN }} - name: Get Terraform outputs id: tf_outputs run: | cd terraform-infrastructure/environments/production terraform init -backend-config=backend.hcl terraform output -json > outputs.json echo "TF_ROLE_ARN=$(jq -r '.lambda_role_arn.value' outputs.json)" >> $GITHUB_ENV echo "TF_SUBNET_IDS=$(jq -r '.private_subnet_ids.value | @json' outputs.json)" >> $GITHUB_ENV echo "TF_SG_IDS=$(jq -r '.lambda_security_group_ids.value | @json' outputs.json)" >> $GITHUB_ENV echo "TF_RDS_ENDPOINT=$(jq -r '.rds_endpoint.value' outputs.json)" >> $GITHUB_ENV LAYERS=$(jq -r '.layer_arns.value | to_entries | map(.value) | @json' outputs.json) echo "TF_LAYER_ARNS=$LAYERS" >> $GITHUB_ENV - name: Setup Lambroll run: | VERSION="1.0.3" curl -L "https://github.com/fujiwara/lambroll/releases/download/v${VERSION}/lambroll_${VERSION}_linux_amd64.tar.gz" | tar xz sudo mv lambroll /usr/local/bin/ lambroll version - name: Deploy with Lambroll run: | cd functions/user-auth lambroll deploy --log-level info env: ENVIRONMENT: staging - name: Publish Version id: version run: | VERSION=$(aws lambda publish-version \ --function-name user-auth-function \ --description "Commit: ${{ github.sha }}, Actor: ${{ github.actor }}" \ --query 'Version' \ --output text) echo "version=$VERSION" >> $GITHUB_OUTPUT echo "Published version: $VERSION" - name: Update staging alias run: | aws lambda update-alias \ --function-name user-auth-function \ --name staging \ --function-version ${{ steps.version.outputs.version }} echo "Deployed to staging: version ${{ steps.version.outputs.version }}" - name: Notify Slack if: always() uses: slackapi/slack-github-action@v1 with: webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} payload: | { "text": "Staging Deployment", "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": "*Deployment Status:* ${{ job.status }}\n*Function:* user-auth-function\n*Version:* ${{ steps.version.outputs.version }}\n*Commit:* ${{ github.sha }}" } } ] }
Production手動リリース 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 name: Release to Production on: workflow_dispatch: inputs: function_name: description: 'Function name to release' required: true type: choice options: - user-auth-function - order-processing-function - payment-service-function version: description: 'Version number (from staging)' required: true jobs: release: runs-on: ubuntu-latest environment: production permissions: id-token: write contents: read steps: - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_LAMBDA_ROLE_ARN }} aws-region: ap-northeast-1 - name: Verify version exists run: | aws lambda get-function \ --function-name ${{ inputs.function_name }} \ --qualifier ${{ inputs.version }} - name: Update production alias run: | aws lambda update-alias \ --function-name ${{ inputs.function_name }} \ --name production \ --function-version ${{ inputs.version }} - name: Verify deployment run: | CURRENT=$(aws lambda get-alias \ --function-name ${{ inputs.function_name }} \ --name production \ --query 'FunctionVersion' \ --output text) if [ "$CURRENT" == "${{ inputs.version }} " ]; then echo "Successfully released version ${{ inputs.version }} " else echo "Release failed. Current: $CURRENT, Expected: ${{ inputs.version }} " exit 1 fi - name: Notify Slack uses: slackapi/slack-github-action@v1 with: webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }} payload: | { "text": "Production Release", "blocks": [ { "type": "section", "text": { "type": "mrkdwn", "text": "*Function:* ${{ inputs.function_name }}\n*Version:* ${{ inputs.version }}\n*Released by:* ${{ github.actor }}" } } ] }
Phase 4: バージョン削除の自動化 削除スクリプト 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 import boto3from datetime import datetime, timedeltaimport syslambda_client = boto3.client('lambda' ) def cleanup_old_versions ( function_name, keep_versions=10 , keep_days=90 ): """ 古いLambdaバージョンを削除 保持ルール: - エイリアスが参照しているバージョン - 最新N個のバージョン - N日以内に作成されたバージョン """ print (f"Processing {function_name} ..." ) try : aliases = lambda_client.list_aliases(FunctionName=function_name) protected_versions = set ( alias['FunctionVersion' ] for alias in aliases['Aliases' ] if alias['FunctionVersion' ] != '$LATEST' ) print (f" Protected versions (by aliases): {protected_versions} " ) except Exception as e: print (f" Error getting aliases: {e} " ) return try : versions = lambda_client.list_versions_by_function( FunctionName=function_name, MaxItems=100 ) except Exception as e: print (f" Error listing versions: {e} " ) return deletable = [] cutoff_date = datetime.now(datetime.timezone.utc) - timedelta(days=keep_days) for version in versions['Versions' ]: version_num = version['Version' ] if version_num == '$LATEST' : continue if version_num in protected_versions: continue last_modified = datetime.fromisoformat( version['LastModified' ].replace('Z' , '+00:00' ) ) if last_modified < cutoff_date: deletable.append({ 'version' : version_num, 'last_modified' : last_modified }) deletable.sort(key=lambda x: x['last_modified' ], reverse=True ) to_delete = deletable[keep_versions:] print (f" Total versions: {len (versions['Versions' ])} " ) print (f" Deletable (old): {len (deletable)} " ) print (f" Will delete: {len (to_delete)} " ) for item in to_delete: version_num = item['version' ] try : lambda_client.delete_function( FunctionName=function_name, Qualifier=version_num ) print (f" Deleted version {version_num} " ) except Exception as e: print (f" Failed to delete version {version_num} : {e} " ) def main (): paginator = lambda_client.get_paginator('list_functions' ) for page in paginator.paginate(): for func in page['Functions' ]: function_name = func['FunctionName' ] cleanup_old_versions( function_name, keep_versions=10 , keep_days=90 ) print () if __name__ == '__main__' : main()
週次実行ワークフロー 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 name: Cleanup Old Lambda Versions on: schedule: - cron: '0 0 * * 0' workflow_dispatch: jobs: cleanup: runs-on: ubuntu-latest permissions: id-token: write contents: read steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: '3.11' - name: Install dependencies run: pip install boto3 - uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_LAMBDA_ROLE_ARN }} aws-region: ap-northeast-1 - name: Run cleanup run: python scripts/cleanup-old-versions.py
Phase 5: アーティファクト管理 S3へのアップロード 1 2 3 4 5 6 7 8 9 10 11 12 13 - name: Build and upload artifact run: | cd functions/user-auth zip -r function.zip index.py requirements.txt ARTIFACT_KEY="user-auth-function/$(date +%Y%m%d-%H%M%S)-${GITHUB_SHA::7}.zip" aws s3 cp function.zip s3://${{ env.TF_ARTIFACT_BUCKET }}/${ARTIFACT_KEY} echo "artifact_key=$ARTIFACT_KEY" >> $GITHUB_OUTPUT
ロールバック時の利用 1 2 3 4 5 6 aws s3 cp s3://company-lambda-artifacts/user-auth-function/20250102-120000-abc1234.zip ./function.zip unzip function.zip lambroll deploy
まとめ 実装の流れ
Phase 1 : Terraform(インフラ、IAM、Layer、outputs)
Phase 2 : Lambroll(function.jsonl、コード)
Phase 3 : GitHub Actions(CI/CD、手動承認)
Phase 4 : バージョン削除(週次自動化)
Phase 5 : アーティファクト管理(S3保管)
チェックリスト
次のステップ 実装方法を理解したら、運用上の考慮点を確認しましょう。
次へ: 4. トレードオフと運用上の考慮点