AWS lambdaをTerraformとGitHub Actionsで自動デプロイする

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つのフォルダがあり、その中でコードを書いていきます。

  1. .github/workflows: GitHub Actionsの定義ファイルを格納するフォルダ(フォルダ名が同じである必要がある)
  2. lambda_function : lambdaのソースコードを入れるフォルダ
  3. 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つを設定します。

  1. LAMBDA_ZIP_KEY : lambda_package.zip (値は任意の値で大丈夫です)
  2. AWS_ACCESS_KEY_ID : IAMユーザーのアクセスキーを設定します
  3. 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関連のサービスを使ってできないか試してみたいです。