AWS lambdaをTerraformとGitHub Actionsで自動デプロイする
2024-05-24今回の記事ではタイトルの通り、AWS lambdaに対して、TerraformとGitHub Actionsを使って、自動デプロイをしたので、やったことをまとめておきます。
一部正確でない可能性やベストプラクティスでない場合がございます。自己責任でお願いします。
目次
具体的にやること
AWS lambdaはPython3.11で作成し、毎日12時に定期実行されるように設定します。
外部モジュールを加えてlambdaを実行する方法を紹介します。ソースコードはS3に配置します。
また、lambdaのソースコードはGitHubで管理を行います。
その上でGitHubのmainブランチにpushされた場合、Actionsが動作しlambda上に新しいソースコードがデプロイされます。
環境
- macOS : Sonoma 14.2.1
- Python : 3.11
- Terraform: 1.5.1
ディレクトリ構成
├── .github/workflows/ # GitHub Actionsを定義するフォルダー
│ └── deploy_lambda.yml
├── lambda_function/ # lambdaのソースコードを入れるフォルダー
│ ├── mod/
│ │ └── mod1.py
│ ├── venv
│ ├── main_handler.py
│ └── requirements.txt
├── terraform/ # terraformのコードを入れるフォルダー
│ ├── main.tf
│ └── variables.tf
├── .gitignore
└── README.md
上記の通り今回のプロジェクトフォルダの中には3つのフォルダがあり、その中でコードを書いていきます。
- .github/workflows: GitHub Actionsの定義ファイルを格納するフォルダ(フォルダ名が同じである必要がある)
- lambda_function : lambdaのソースコードを入れるフォルダ
- terraform : Terraformのコードを入れるフォルダ
Pythonコードの作成
兎にも角にもまずはlambdaで実行するためのPythonコードを作成します。
requirements.txtの作成
外部モジュールを使ったlambdaを開発したいので、lambda_functionディレクトリの中にrequirements.txtを作成します。
pandas
Pythonコードの作成
次にPythonコードを書いていきます。
先に自作のモジュールを作っていきます。
lambda_functionディレクトリの中にmodディレクトリを作成し、中にmod1.pyを作ります。
内容としては、コンストラクタでメッセージを受け取り、get_messageメソッドで少し加工して文字列を返すというシンプルなクラスです。
class Mod1:
def __init__(self, message: str):
self.message = message
def get_message(self):
return 'This module message is ' + self.message
lambda_functionの中にmain_handler.pyを作成します。
基本的にやっていることは3つです。
- ログで現在日時の出力
- pandasのバージョンをログに出力
- 自作モジュールのget_messageをログに出力
import logging
import datetime
import pandas
from mod.mod1 import Mod1
# ロガーの設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def handler(event, context):
# 現在の日時を取得
now = datetime.datetime.now()
# ログに現在の日時を記録
logger.info(f'lambda実行日時: {now}')
# pandasのバージョンをログに出力
logger.info(pandas.__version__)
# 自作モジュールのインスタンスを作成し、メッセージをログ出力
module_1 = Mod1('Module message')
logger.info(module_1.get_message())
# 応答メッセージ
return {
'statusCode': 200,
'body': f'Hello from Lambda! Current time is {now}'
}
Terraformの作成
次にTerraformのコードを作成していきます。
tfstate格納用バケットの作成
Terraformを作成していく前に、tfstateを格納しておくS3バケットをAWS上に作成しておく必要があります。
AWS consoleから手動でS3バケットを作成しておいて下さい。
名前はなんでも良いです。
このバケットで、tfstateファイルを管理することで、GitHub Actionsでデプロイする際にリソースの状態を参照することが可能です。
逆にこれがないと、GitHub Actionを実行するたびに、リソースを新規作成しようとするためエラーになります。
Terraformコードの作成
terraformディレクトリの中にmain.tfを作成します。
少し長いですが、内容は次のとおりです。
細かい解説は行いませんが、コメントを記載しているので、そちらを参考にして下さい。
provider "aws" {
region = "ap-northeast-1"
}
# tfstateを保存するS3バケットを指定
# 事前に作成しておく必要あり
terraform {
backend "s3" {
bucket = "任意のバケット名1"
key = "terraform.tfstate"
region = "ap-northeast-1"
}
}
# Lambda関数のコードを格納するS3バケットを定義
resource "aws_s3_bucket" "lambda_bucket" {
bucket = "任意のバケット名2"
}
resource "aws_s3_bucket_ownership_controls" "lambda_bucket_ownership" {
bucket = aws_s3_bucket.lambda_bucket.id
rule {
object_ownership = "BucketOwnerPreferred"
}
}
# lambda関数をzip化する
data "archive_file" "lambda_zip" {
type = "zip"
source_dir = "${path.module}/../lambda_function"
output_path = "${path.module}/../lambda_function/lambda_package.zip"
}
# zipファイルをS3にアップロードする
resource "aws_s3_object" "lambda_code" {
bucket = aws_s3_bucket.lambda_bucket.bucket
key = var.lambda_zip_key
source = data.archive_file.lambda_zip.output_path
etag = filemd5(data.archive_file.lambda_zip.output_path)
}
# lambdaに付与するロールを定義
resource "aws_iam_role" "lambda_role" {
name = "任意のロール名"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "lambda.amazonaws.com"
}
},
]
})
}
# ロールにアタッチするポリシーを定義
resource "aws_iam_role_policy" "lambda_policy" {
role = aws_iam_role.lambda_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
Effect = "Allow",
Resource = "arn:aws:logs:*:*:*"
},
]
})
}
# lambda関数の設定を定義
resource "aws_lambda_function" "example_lambda" {
function_name = "任意のlambda関数名"
# S3バケットからソースコードを取得する
s3_bucket = aws_s3_bucket.lambda_bucket.bucket
s3_key = var.lambda_zip_key
handler = "main_handler.handler"
runtime = "python3.11"
role = aws_iam_role.lambda_role.arn
# S3のコードに変更があれば、再読み込み
source_code_hash = data.aws_s3_object.lambda_code_data.etag
}
# S3バケットのソースコード場所を定義
data "aws_s3_object" "lambda_code_data" {
key = var.lambda_zip_key
bucket = aws_s3_bucket.lambda_bucket.bucket
depends_on = [ aws_s3_object.lambda_code ]
}
# lambdaを実行するスケジュールを定義
resource "aws_cloudwatch_event_rule" "lambda_schedule" {
name = "任意のevent名"
schedule_expression = "cron(0 3 * * ? *)" # 毎日昼の12時に実行する
}
# スケジュールのターゲットをlambdaに設定
resource "aws_cloudwatch_event_target" "lambda_target" {
rule = aws_cloudwatch_event_rule.lambda_schedule.name
target_id = "LambdaTarget"
arn = aws_lambda_function.example_lambda.arn
}
# CloudWatch EventsからのLambda関数起動を許可する権限を設定
resource "aws_lambda_permission" "allow_cloudwatch" {
statement_id = "AllowExecutionFromCloudWatch"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.example_lambda.function_name
principal = "events.amazonaws.com"
source_arn = aws_cloudwatch_event_rule.lambda_schedule.arn
}
上記のmain.tfでは変数を使っています。その変数を宣言するために、terraformディレクトリにvariables.tfを作成します。
内容は次のとおりで、実際の値の注入はGitHub Actions実行時に環境変数として与えることにします。
加えて、今回はリソース名を直接記述していますが、必要に応じて変数で与えた方が良い場合があります。
variable "lambda_zip_key" {
description = "The S3 key for the uploaded Lambda zip file"
type = string
}
GitHub Actionsのワークフローを作成
最後にGitHub Actionsのワークフローを作成します。
IAMユーザーの作成
GitHub ActionsからTerraformを実行する際に利用するIAMユーザーをAWSで作成する必要があります。
IAMユーザーを作成したら、そのユーザーのアクセスキーとシークレットキーを生成します。
この2つのキーの取り扱いには十分注意して下さい。
またIAMユーザーには必要な権限を与える必要があります。理想を言うと、必要最低限の権限を与えるのが望ましいですが、今回は以下の権限を与えると実行できます。
- EventBridge: Full Access
- IAM : Full Access
- S3 : Full Access
- Lambda : Full Access
GitHubリポジトリにシークレット値の設定
生成したアクセスキーとシークレットキーをGitHubリポジトリのシークレット値に設定します。
リポジトリのSettingsタブへいき、Secrets and variables > Actionsから設定が可能です。
次の3つを設定します。
- LAMBDA_ZIP_KEY : lambda_package.zip (値は任意の値で大丈夫です)
- AWS_ACCESS_KEY_ID : IAMユーザーのアクセスキーを設定します
- AWS_SECRET_ACCESS_KEY: IAMユーザーのシークレットキーを設定します
deploy_lambda.ymlの作成
.github/workflowsディレクトリを作成し、その中にdeploy_lambda.ymlファイルを配置します。
name: Deploy Lambda Function via Terraform
on:
# mainブランチにプッシュされた時に起動する
push:
branches:
- main
paths:
- 'lambda_function/**'
- 'terraform/**'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: '3.11'
# requirements.txtから外部モジュールをインストールし、lambda_functionフォルダをzip化する
- name: Install Python dependencies
run: |
cd lambda_function
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt -t .
deactivate
zip -r ../lambda_package.zip .
# IAMユーザーの権限を設定する
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: 'ap-northeast-1'
# Terraformの初期化
- name: Initialize Terraform
run: |
cd terraform
terraform init
# AWSにTerraformの定義を反映する
- name: Apply Terraform
run: |
cd terraform
terraform apply -auto-approve
env:
TF_VAR_lambda_zip_key: ${{ secrets.LAMBDA_ZIP_KEY }}
- name: Cleanup
if: always()
run: |
rm lambda_function/lambda_package.zip
完成したらGitHubにプッシュするとActionsが実行され、AWSにリソースが配置されます。
まとめとNext Action
今回は定期実行するPythonのlambdaをTerraformとGitHub Actionsを使って、自動デプロイする方法をまとめました。
次はシークレット値の管理や自動デプロイ、リポジトリなどもAWS関連のサービスを使ってできないか試してみたいです。