Lambda CI/CD アーキテクチャ - 3. 実装ガイドライン

ドキュメント構成

免責事項

本記事はAI(Claude)を活用して執筆しています。内容の正確性については保証いたしかねますので、重要な情報は必ず一次情報源をご確認ください。


この章で理解すること

実際に構築する際の具体的な手順とコード例を示します。

  • Terraform実装(インフラ、outputs)
  • Lambda Layer管理
  • Lambroll実装(function.jsonl、デプロイ)
  • GitHub Actions設定
  • バージョン削除の自動化
  • アーティファクト管理

Phase 1: Terraform実装

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
# layers/dependencies/build.sh

mkdir -p python
pip 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
}
}
}

Terraform outputs

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
// functions/user-auth/function.jsonl
{
"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
# functions/user-auth/index.py
import json
import os
import logging

# 環境変数から設定取得
DB_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
# 初回のみ実行: エイリアスを作成

# development
aws lambda create-alias \
--function-name user-auth-function \
--name development \
--function-version '$LATEST'

# staging(初期はVersion 1を参照)
aws lambda create-alias \
--function-name user-auth-function \
--name staging \
--function-version 1

# production(初期はVersion 1を参照)
aws lambda create-alias \
--function-name user-auth-function \
--name production \
--function-version 1

Phase 3: GitHub Actions設定

Terraform CI/CD

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
# terraform-infrastructure/.github/workflows/terraform-apply.yml

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
# lambda-applications/.github/workflows/deploy-staging.yml

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

# Terraform outputsを取得
- 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

# Layer ARNs
LAYERS=$(jq -r '.layer_arns.value | to_entries | map(.value) | @json' outputs.json)
echo "TF_LAYER_ARNS=$LAYERS" >> $GITHUB_ENV

# Lambrollインストール
- 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

# Versionを発行
- 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"

# staging エイリアス更新
- 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 }}"

# Slack通知
- 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
# lambda-applications/.github/workflows/deploy-production.yml

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 }}

# production エイリアス更新
- 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

# Slack通知
- 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
# scripts/cleanup-old-versions.py

import boto3
from datetime import datetime, timedelta
import sys

lambda_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
})

# 最新N個は保持(日付でソート)
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():
# すべてのLambda関数を取得
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
# lambda-applications/.github/workflows/cleanup-versions.yml

name: Cleanup Old Lambda Versions

on:
schedule:
- cron: '0 0 * * 0' # 毎週日曜日 0:00 UTC
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
# Staging デプロイ前にアーティファクトを保存
- name: Build and upload artifact
run: |
cd functions/user-auth

# zipファイル作成
zip -r function.zip index.py requirements.txt

# S3にアップロード(バージョニング有効)
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

# 展開してLambrollでデプロイ
unzip function.zip
lambroll deploy

まとめ

実装の流れ

  1. Phase 1: Terraform(インフラ、IAM、Layer、outputs)
  2. Phase 2: Lambroll(function.jsonl、コード)
  3. Phase 3: GitHub Actions(CI/CD、手動承認)
  4. Phase 4: バージョン削除(週次自動化)
  5. Phase 5: アーティファクト管理(S3保管)

チェックリスト

  • Terraform outputsが正しく設定されている
  • Lambrollがoutputsを環境変数で参照できる
  • エイリアス(development、staging、production)が作成済み
  • GitHub Actions環境変数・Secretsが設定済み
  • S3バケット(アーティファクト保管)が作成済み
  • バージョン削除スクリプトが動作する

次のステップ

実装方法を理解したら、運用上の考慮点を確認しましょう。

次へ: 4. トレードオフと運用上の考慮点