piro_kitiの日記

IT技術ブログ

AWS Lamdbaを利用したゴミ出し通知サービス

まえがき

自分の地域では普段資源ごみのタイミングが第N曜日となってまして、
月に2回ほどビン・缶・雑誌など出すタイミングがありますが、
よく忘れてしまい缶などがたまって困っていました^^;
この課題を勉強がてら、AWSを活用して各サービスを組み合わせて解決してみようと思います!

目次

概要

EventBridgeを1時間ごとにスケジュール実行するよう設定し、EventBridgeからLamdbaが呼ばれます。
呼ばれたLamdbaの中でS3上に配置してあるschedule.csvから
通知条件を取得しcsvの中の条件が一致する場合はメッセージを
LineNotifyを通じて自身のLineに通知します。

LamdbaのデプロイはCodePiplineを利用して自動化しています。
Lamdbaはコンテナがサポートされたとのことだったので
ECS同様の方法でbuildspec.ymlにビルドからデプロイまで記述してみました。

前提条件

  • VSCodeUbuntu(WSL2)、Docker、Docker-compose設定済みであること
  • AWSCLIが設定済みであること
  • Bitbucket or Github or CodeCommitへpushが出来ること
  • LineNotifyは設定例がたくさんありますので対象外としてます

プログラム説明

ディレクトリ構成

/garbage
 /src
  index. py
 buildspec.yml
 docker-compose.yml
 Dockerfile
 requirements.txt
src配下に今回実行するindex.pyファイルを配置しています

buildspec.yml

version: 0.2
phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin "${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com"
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...       
      - docker build -t "${REPOSITORY_NAME}:latest" .
      - docker tag "${REPOSITORY_NAME}:latest" "${REPOSITORY_URI}"
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker image...
      - docker push "${REPOSITORY_URI}"
      - echo Update garbage lamdba ...      
      - aws lambda update-function-code --function-name "${LAMDBA_NAME}" --image-uri "${REPOSITORY_URI}:latest" --publish

buildspec.ymlではECRへのログイン、コンテナ形式のlamdbaのビルド、ECRへのpush 最後にpost_buildの中でlamdba関数のデプロイを実施しています。

Dockerfile

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

COPY ./src ${LAMBDA_TASK_ROOT}

COPY requirements.txt  .
RUN  pip3 install -r requirements.txt

CMD [ "index.handler" ]

lamdbaがコンテナをサポートしたためECRからlamdbaコンテナイメージ取得し、 ソースを含めてビルドします。

docker-compose.yml

version: "3"
services:
  lamdba:
    container_name: "garbage"
    build: .
    volumes:
      - ./src:/var/task
    ports:
      - "9000:8080"
    env_file:
      - variables.env
    command: index.handler

開発用にローカルのsrcディレクトリをlamdbaの実行ディレクトリにマウントしています。

#Lineトークン
ACCESS_TOKEN=xxxxxxxxxxxxxxxxxx
AWS_ACCESS_KEY_ID=xxxxxxxxxxxxxxxxxx
AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxx
TZ=Asia/Tokyo

開発環境用の環境変数は外だししvariables.envに記述しています。

index.py

import os
import io
import json
import requests
import boto3
import pandas as pd
import datetime

# Line Notify
ACCESS_TOKEN = os.environ["ACCESS_TOKEN"]
HEADERS = {"Authorization": "Bearer {}".format(ACCESS_TOKEN)}
URL = "https://notify-api.line.me/api/notify"

s3 = boto3.resource('s3')

def handler(event, context):
    print("==>> Start garbage lamdba")

    schedule_obj = s3.Object(
        "garbage-notice-master-bucket",
        "schedule.csv"
        )

    schedule_csv_string  = schedule_obj.get()['Body'].read().decode("utf-8")
    s_df = pd.read_csv(io.StringIO(schedule_csv_string))
    s_df['execdate'] = pd.to_datetime(s_df['execdate'])

    #現在の時間を日付型で取得
    date_now =datetime.datetime.now()

    #schedule.csv上にあるレコード分ループ
    for s_data in s_df.itertuples():
        #実行条件日を取得
        exec_date = s_data.execdate
        #事前に通知したい日数を加算して現在日時とする
        add_date_now = date_now + datetime.timedelta(days=s_data.ago)
        #実行条件日の年月日のみ現在日付にリプレイス
        exec_date = s_data.execdate.replace(year=add_date_now.year, month=add_date_now.month, day=add_date_now.day)
        #リプレイスした日付と、現在日時の差を計算
        date_diff = exec_date - add_date_now
        #実行条件日と加算した現在日時の差が180分以内かチェック
        if date_diff < datetime.timedelta(minutes=180) and exec_date > add_date_now:
            #CSV実行条件の曜日と、加算した現在日時の曜日が同じかチェック
            if s_data.execdate.isoweekday() == add_date_now.isoweekday():

                #加算した現在日時が第何週か算出 CSVの第何週条件と同じかチェック
                caloc_nthday = (add_date_now.day - 1) // 7 + 1
                if caloc_nthday == s_data.nthdayofweek:
                    #LINEに通知
                    send_data = {'message': "{} は資源ごみを出す日です!ビン・缶・雑誌など!".format('{0:%Y-%m-%d}'.format(add_date_now))}
                    requests.post(URL, headers=HEADERS, data=send_data)

    print("==>> End garbage lamdba")

    return {
        "statusCode": 200,
        "body": json.dumps(
            {
                "message": "Complete!",
            }
        ),
    }

メインの処理を記述しているPythonのコードになります。
ACCESS_TOKENはLineNotifyから取得した環境変数へセットしています。
またS3へ手動でアップロードしたschedule.csvを直接参照し、
条件を取得後pandasのdataframeへ入れて値を取得しています。
schedule.csvの中のexecdateが実行対象日情報
※今回は第何週の条件のため時刻を条件として利用
nthdayofweekが第何週かの情報、agoが何日前に通知するかの情報を保持しています。

schedule.csv

EventBrigeからキックされた現在時刻に事前通知条件を加算し、現在時刻として
現在時刻が取得した条件に一致するする場合通知を行うという仕組みです。
時刻をそのままにして実行対象日を現在の年、月、日で置き換えた日付を
実行条件日(exec_date)としています。

#リプレイスした日付と、現在日時の差を計算
date_diff = exec_date - add_date_now
#実行条件日と加算した現在日時の差が180分以内かチェック
if date_diff < datetime.timedelta(minutes=120) and exec_date > add_date_now:

実行条件日と現在の時刻(事前通知日数を加算したadd_date_now)と比較して3時間以内かチェック。

#CSV実行条件の曜日と、加算した現在日時の曜日が同じかチェック
if s_data.execdate.isoweekday() == add_date_now.isoweekday():

#加算した現在日時が第何週か算出 CSVの第何週条件と同じかチェック
caloc_nthday = (add_date_now.day - 1) // 7 + 1
if caloc_nthday == s_data.nthdayofweek:

あとは実行条件日と現在の時刻の曜日が同じであること、第N週を現在の時刻から算出し、
schedule.csvのnthdayofweekの値を同じであるか確認し同じであれば LineNotifyを 通じて通知をします。

AWS側の設定

■ElasticContainerRegistoryの設定


まずはECRへ手動でLamdbaイメージをpushします。

Amazon ECR>リポジトリリポジトリを作成を選択

プライベート&リポジトリを入力して作成します。

Amazon ECR>リポジトリ

作成したリポジトリを選択してプッシュコマンドを表示を確認してAWSCLIよりイメージをプッシュします。
※コマンドが表示されるためそれをそのまま入力便利!

■Lamdbaの設定


Lamdba関数を手動で先ほどのコンテナイメージを元に作成します。

Lambda>関数>関数の作成

  • コンテナイメージを選択
  • 関数名を入力
  • イメージを参照より先ほどpushしたイメージを選択
  • アーキテクチャはデフォルト
  • 実行ロール※デフォルトで作成されたロールに
    S3へのアクセスが必要なためS3への読み取りポリシーを付与します

関数を作成を押下して作成されていることを確認します。

Lambda>関数>作成された関数名を選択
- 設定
- 環境変数
- ACCESS_TOKEN=LineNotifyで取得したトーク
- TZ=Asia/Tokyo

■EventBrigeの設定


Lamdbaを定期実行するスケジュールを設定します。

Amazon EventBridge>ルール>ルールを作成

ルールの詳細を定義
- 名前を入力します
- イベントバスはdefaultを選択
- ルールタイプはスケジュールを選択

スケジュールを定義

ターゲットを選択

  • AWSサービスを選択
  • Lamdba関数を選択
  • 機能 ⇒ 作成したlamdbaの関数を選択します。

これで手動でLamdbaをコンテナ形式でデプロイし、定期的に実行できる仕組みができました。
次はCodePiplineを使って先ほど手動で実施した作業を自動でデプロイできるようにします。

■CodePiplineの設定


デベロッパー用ツール>CodePipeline>パイプライン>新規のパイプラインを作成する

パイプラインの設定を選択する

ソースステージを追加する

  • ソースプロバイダ⇒Bicbucket
  • 接続⇒Bicbucketへの接続を作成してセット
  • リポジトリ名を選択
  • ブランチ名を選択

※変更時にパイプラインの実行はしたくなかったためチェックを外しました。

ビルドステージを追加する
- プロバイダーを構築する⇒AWSCodeBuildを選択
- リージョン⇒アジアパシフィック
- プロジェクトを作成をクリックしてCodeBuild作成画面へ

デベロッパー用ツール>CodeBuild>ビルドプロジェクト>ビルドプロジェクトを作成する

プロジェクトの設定
- プロジェクト名を入力

環境
- 環境イメージ⇒マネージド型
- オペレーティングシステム⇒AmazonLinux2
- ランタイム⇒スタンダード
- イメージ⇒aws/codebuild/amazonlinux2-aarch63-standard:2.0
- イメージのバージョン⇒常に最新
- 特権付与⇒チェック
- サービスロールは新規作成
- AmazonEC2ContainerRegistryPowerUser、AWSLambda_FullAccess2つポリシーを追加

Buildspec
- buildspec ファイルを使用する
※buildspecファイルを使用するを選択し作成したbuildspec.ymlを参照してもらいます。

ログ
- CloudWatch Logsチェック
※CloudWatch Logsが有効になっていることを確認します。

デベロッパー用ツール>CodePipeline>パイプライン>新規のパイプラインを作成する

ビルドステージを追加する
※CodeBuildの設定を完了し、CodePipline側へ作成されたプロジェクトがセットされます。

lamdbaへのデプロイはbuildspec.ymlの中で実施してしまうため、
ビルドステージをスキップして完了します。

■CodePiplineの実行


デベロッパー用ツール>CodePipeline>パイプライン

より作成したパイプラインを選択し「変更をリリース」を押下デプロイが成功すれば完了です。

まとめ

身近な課題を題材に設定してみましたが、Python,S3,EventBridge,CodePipline,CodeBuild,Lamdba,
コンテナ,Git,LineNotifyと基本的ですが意外に多くのプロダクトを扱ったことで
基礎理解がとても深まったと思います。 ゴミ忘れも解決でき一石二鳥で有意義でした。
少しでもどなたかの参考になれば幸いです。

参考URL

https://dev.classmethod.jp/articles/get-s3-object-with-python-in-lambda/#toc-3
https://dev.classmethod.jp/articles/tried-using-line-notify-with-lambda-to-notify-the-appointment/