見出し画像

Terraformを使ってLambda関連のリソースをAWSにデプロイしました

Lambdaを活用したツールを開発しており、今回はTerraformを使ってLambda関連のリソースをAWSにデプロイしました。

そもそもの開発の目的としては、個人開発を通していろいろな技術を触りたいとうことでして、せっかくなら自分自身が便利に使えそうなものから作ろうと思い「雨が降りそうだったらメールで通知されるツール」から開発しています。
最近天気が変わりやすくて外に干してた洗濯物が雨に濡れることが多かったので、それを解決したいです。(探せば既存の何らかのサービスでも解決できそうですが勉強も兼ねて自分で作ろうかと)

維持費を最低限に抑えられるように構成を考え、Lambdaをスケジュール実行できるように進めています。今のところ業務ではバックエンドの実装がメインということもあり、そこまでAWS利用費のことは深く考えていないので、個人開発ではコスト面にもこだわって作りたいところです。

1. TerraformにLambda関連のリソースを追加

以下のAWSリソースをTerraformのコードに追加しました。

  • Lambda

  • IAM(role、policy)

  • CloudWatch Log Group

  • ECR repository

今はTerraformのコードはmain.tfにまとめており、以下のようになっています。

# main.tf

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.62.0"
    }
  }

  backend "s3" {
    bucket  = "terraform-backend-state-for-weather-checker"
    key     = "terraform.tfstate"
    region  = "ap-northeast-1"
    encrypt = true
    profile = "aws_sso"
  }
}

provider "aws" {
  region  = "ap-northeast-1"
  profile = "aws_sso"
}

resource "aws_cloudwatch_log_group" "lambda_log_group" {
  name              = "/aws/lambda/weather_checker"
  retention_in_days = 14
}

resource "aws_ecr_repository" "weather_checker" {
  name                 = "weather-checker"
  image_tag_mutability = "MUTABLE"

  image_scanning_configuration {
    scan_on_push = true
  }

  tags = {
    Environment = "Production"
    Project     = "WeatherChecker"
  }
}

data "aws_caller_identity" "current" {}

data "aws_iam_policy_document" "lambda_policy" {
  statement {
    actions = [
      "ssm:GetParameter"
    ]

    resources = [
      "arn:aws:ssm:ap-northeast-1:${data.aws_caller_identity.current.account_id}:parameter/weather-checker/api-key"
    ]
  }

  statement {
    actions = [
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ]

    resources = ["arn:aws:logs:ap-northeast-1:${data.aws_caller_identity.current.account_id}:log-group:/aws/lambda/weather_checker:*"]

  }

  statement {
    actions = [
      "ecr:GetDownloadUrlForLayer",
      "ecr:BatchGetImage",
      "ecr:BatchCheckLayerAvailability"
    ]

    resources = [
      "arn:aws:ecr:ap-northeast-1:${data.aws_caller_identity.current.account_id}:repository/weather-checker"
    ]
  }
}

resource "aws_iam_role" "lambda_role" {
  name               = "lambda_weather_checker_role"
  assume_role_policy = data.aws_iam_policy_document.lambda_assume_role_policy.json
}

data "aws_iam_policy_document" "lambda_assume_role_policy" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }
  }
}

resource "aws_iam_role_policy" "lambda_policy_attachment" {
  name   = "lambda_weather_checker_policy"
  role   = aws_iam_role.lambda_role.name
  policy = data.aws_iam_policy_document.lambda_policy.json
}

resource "aws_lambda_function" "weather_checker" {
  function_name = "weather_checker"
  role          = aws_iam_role.lambda_role.arn
  package_type  = "Image"
  image_uri     = "${aws_ecr_repository.weather_checker.repository_url}:latest"
  environment {
    variables = {
      LOG_GROUP_NAME = aws_cloudwatch_log_group.lambda_log_group.name
    }
  }

  depends_on = [aws_cloudwatch_log_group.lambda_log_group]
}

2. Dockerfileの作成

Lambdaで使用するPythonコードをコンテナ化するために、以下のDockerfileを作成しました。

# Dockerfile

FROM public.ecr.aws/lambda/python:3.9

COPY handler.py ${LAMBDA_TASK_ROOT}

RUN pip install --no-cache-dir requests boto3 mypy_boto3_ssm

CMD ["handler.lambda_handler"]

Pythonのコードは以下のようになっています。

# handler.py

import json
import requests
import boto3
from mypy_boto3_ssm import SSMClient
import os
import logging

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def get_weather(api_key, location):
    url = f'https://api.openweathermap.org/data/2.5/weather?q={location}&appid={api_key}&units=metric'

    try:
        response = requests.get(url)
        response.raise_for_status()
        weather_data = response.json()

        temperature = weather_data['main']['temp']
        weather_description = weather_data['weather'][0]['description']

        return {
            'temperature': temperature,
            'description': weather_description
        }

    except requests.exceptions.RequestException as e:
        return {'error': str(e)}
    except Exception as e:
        return {'error': str(e)}

def get_api_key():
    profile_name = os.getenv('AWS_PROFILE')
    session = boto3.Session(profile_name=profile_name)

    ssm: SSMClient = session.client('ssm', region_name='ap-northeast-1')

    response = ssm.get_parameter(
        Name='/weather-checker/api-key',
        WithDecryption=True
    )
    return response['Parameter']['Value']

def lambda_handler(event, context):
    api_key = get_api_key()
    location = 'Naha,Okinawa,JP' # 天気情報を取得したい場所。値は外部から受け取れるように変更予定

    weather_info = get_weather(api_key, location)

    if 'error' in weather_info:
        logger.error(f"Failed to fetch weather data: {weather_info}")
        return {
            'statusCode': 500,
            'body': json.dumps(weather_info)
        }

    logger.info(f"Successfully fetched weather data: {weather_info}")


    return {
        'statusCode': 200,
        'body': json.dumps({
            'location': location,
            'temperature': weather_info['temperature'],
            'description': weather_info['description']
        })
    }

if __name__ == "__main__":
    result = lambda_handler(None, None)
    print(result)


3. Makefileの作成

ECRへのログイン、Dockerイメージのビルド、タグ付け、プッシュ、そしてLambda関数の更新処理を自動化したいと考え、いくつか方法を検討した結果、今回はMakefileを使用することにしました。
以下がMakefileの内容です。

# Makefile

# 変数の設定
AWS_REGION=ap-northeast-1
AWS_ACCOUNT_ID ?= $(shell echo $$AWS_ACCOUNT_ID)
ECR_REPOSITORY_NAME=weather-checker
IMAGE_NAME=$(AWS_ACCOUNT_ID).dkr.ecr.$(AWS_REGION).amazonaws.com/$(ECR_REPOSITORY_NAME):latest
AWS_PROFILE ?= aws_sso

# ECRへのログイン
login:
	@echo "Logging into ECR..."
	aws ecr get-login-password --region $(AWS_REGION) --profile $(AWS_PROFILE) | docker login --username AWS --password-stdin $(AWS_ACCOUNT_ID).dkr.ecr.$(AWS_REGION).amazonaws.com

# Dockerイメージのビルド
build:
	@echo "Building Docker image..."
	docker build -t $(ECR_REPOSITORY_NAME) .

# Dockerイメージのタグ付け
tag:
	@echo "Tagging Docker image..."
	docker tag $(ECR_REPOSITORY_NAME):latest $(IMAGE_NAME)

# DockerイメージをECRにプッシュ
push:
	@echo "Pushing Docker image to ECR..."
	docker push $(IMAGE_NAME)

# Lambda関数を最新のイメージに更新
update-lambda-function:
	@echo "Updating Lambda function..."
	aws lambda update-function-code --function-name weather_checker --image-uri $(IMAGE_NAME) --region $(AWS_REGION) --profile $(AWS_PROFILE)

# 一連のコマンドを実行
deploy: login build tag push update-lambda-function
	@echo "Docker image successfully pushed to ECR."

メモ: `update-lambda-function`を入れ忘れてエラーが発生した

当初、`deploy` コマンドの中に `update-lambda-function` を入れていなかったため、Lambda関数が最新のDockerイメージを使用していないというエラーに直面しました。
具体的には、以下の現象が発生していました。

  • Dockerfileでmypy_boto3_ssmのinstallを追加し忘れていてLambdaが「No module named 'mypy_boto3_ssm'」エラーで失敗いていた

  • Dockerfileにmypy_boto3_ssmのinstallを追加した

  • 再度Makefileの`deploy`を実行(この時点ではupdate-lambda-functionは入れていない)

  • ECRに正しくDockerイメージがプッシュされる

  • 新しいDockerイメージはECRに存在するが、Lambdaが最新のDockerイメージを使っていないためAWS Lambdaの管理コンソールでテストしたところまだ「No module named 'mypy_boto3_ssm'」のエラーになる

このエラーを解決するために、`deploy` コマンドに `update-lambda-function` を追加し、MakefileでLambda関数を更新するようにしたところ、問題が解決しました。

4. デプロイ手順の実行

修正後、以下のコマンドを実行することで、ECRへのログイン、Dockerイメージのビルド・プッシュ、そしてLambda関数の更新が自動で行われるようになりました。

make deploy

これで、AWS Lambdaが常に最新のDockerイメージを使用するようになり、エラーも発生せず正常に動作するようになりました。

5. AWSマネジメントコンソールでLambdaをテスト

AWSのコンソールでLambdaをテストすると、以下のように処理が成功し、ログに天気情報が出力されることが確認できました。

まだトリガーはない状態ですが、処理は成功するようになりました

まとめ

AWSコンソールで手動ならLambdaを実行することができたので、次はLambdaの実行結果をAmazon SNSを使ってメール通知と、CloudWatchEventを使ったスケジュール実行を追加していきたいと思います。

いいなと思ったら応援しよう!