AWS Lambdaの書き方まとめ

AWS Lambda使って○○がしたいときってありますよね。

そんなときLambdaやプログラミング言語のスキルが低い私はけっこう苦戦したので、攻略方法を自分のためにも世のためにもまとめます。

なおこの記事で扱う言語はPythonのみです。

# 基本
## 何をしたいのかを整理する

まずLambdaで何をどうしたいのかを整理します。
（他のサービスで実現できないかを考えておくことが前提）

以下、例を挙げていきます。

- **RDSのスナップショットを2時間おきに取りたい**

自動スナップショットは1日1回しかバックアップを取得してくれないため作成する。必要なことは、RDSインスタンスの一覧を取得して、タグのついた対象のRDSインスタンス名を取得、スナップショットを実行する。もしくはRDS名を指定してスナップショットを実行する。

- **EBSをスナップショットしたら別リージョンにコピーしたい**

EBSのスナップショットはAmazon Data Lifecycle Manager (DLM)を用いれば自動化できるが、リージョンコピーまではできないため作成する。必要なことは、CloudWatchイベントでEBSのスナップショット完了を選択し、ターゲットをLambdaに設定すること、それをトリガーに対象のEBS名を取得しDRコピーを実行する。

このような形でやりたいことを整理していきましょう。Lambdaを利用してやりたいことの多くは自動化やサービス間連携が目的かと思われます。自動化の場合はコンソール上の操作のどこの部分を自動化したいのか、サービス間連携の場合は始まりと終わりを考えてみてください。AWSのコンソール上のすべての操作はAPIを利用して操作できますし、サービス間連携はCloudWatchやSNSなどを利用すればうまく繋げることができるはずですので、わりと整理しやすいですよ。

## やりたい動作をboto3のドキュメントで探す

> https://boto3.amazonaws.com/v1/documentation/api/latest/index.html

Lambdaでやりたいことが決まったら、boto3のドキュメントを見てやりたいことを探します。

例えば、SNSで通知を行いたい場合は、以下のURLを参考にします。

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sns.html#SNS.Client.publish

**Request Syntax** が構文で、**Returns** が結果です。

探し方は簡単、boto3のトップページから「API reference」の中で対象のサービスを選んで、やりたい動作を探すだけ。それでは構文にならってPythonを記述しましょう。

```python

import boto3

client = boto3.client('sns')

response = client.publish(
    TopicArn='xxxxxx',
    Message='Hello',
    Subject='This is test'
)
```

各パラメータの説明もドキュメントに書いてあります。上記の場合、「TopicArn」がSNSトピックのARNで、「Message」が本文、「Subject」が件名です。

また、boto3を利用する際は必ずインスタンスオブジェクトの生成が必要となりますので、お忘れなく。ドキュメントを見ると目次があると思いますが、それを選択すると一番始めに記載されているかと思います。そのあとに各動作の一覧がありますので、使いたいものに必要なインスタンスオブジェクトを生成することを忘れないでください（なんでもかんでも  **boto3.client** じゃないんだぜ）。ちなみに、上記でも利用しているSNSの「 publish() 」の場合は以下を参考にしています。

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sns.html#client

## 環境変数を使う

環境変数を使うことで、汎用的な関数にしましょう。なるべくコード内に情報は記述しない方向でいくと管理が楽です。

先ほどのSNSへの通知するLambdaの場合は以下のようにできます。

```python

import boto3
import os

client = boto3.client('sns')
arn_sns = os.environ['ARN']

response = client.publish(
    TopicArn=arn_sns,
    Message='Hello',
    Subject='This is test'
)
```

こうすることで対象のSNSトピックを変えたいときは、環境変数を修正するだけで済みますので、コードに触れることはありません。よって人的ミスを減らすことにつながります。ぜひ活用しましょう。

# よく使う技
## CloudWatchイベントの値を受け取る

例えば、CloudWatchをトリガーにしているとき、CloudWatchから渡される値を使いたい場合は、event変数に値が入っております。以下は、EBSのスナップショット完了をトリガーにしたものになります。

```python

import boto3

def lambda_handler(event,context):
    snaphost_create_time = event['time']
    print(snaphost_create_time)
```

「cloudwatach event ec2 snapshot」とかでググると、AWSドキュメントがヒットします。渡される内容が確認できますので、自分の取得したいものを確認しましょう。ほかのCloudWatchイベントも基本「cloudwatch event xxx」で検索すればAWSドキュメントが検索結果にでますので、どんな値が渡されるか事前に見ておくといいですね。

https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-cloud-watch-events.html#create-snapshot-complete

ちなみに、CloudWatchイベントのターゲットの設定で、「定数(JSONテキスト)」を選択してLambdaの動作をコントロールすることにより無駄な関数を作成しなくてもよいケースがあります。以下は、EC2の起動停止を行うLambdaです。

> 定数
> {"Action": "start", "Instances": ["************"]}


```python

import boto3

ec2 = boto3.client('ec2')
 
def lambda_handler(event, context):
    instances = event['Instances']
    if event['Action'] == 'start':
        ec2.start_instances(InstanceIds=instances)
        print ('started your instances: ' + ", ".join(instances))
    elif event['Action'] == 'stop':
        ec2.stop_instances(InstanceIds=instances)
        print ('stopped your instances: ' + ", ".join(instances))
```

こちらの方法は以下の記事を参考にしております。

https://www.simpline.co.jp/tech/awslambda%E3%81%A7ec2%E3%81%AE%E8%B5%B7%E5%8B%95%E3%83%BB%E5%81%9C%E6%AD%A2%E3%82%92%E5%88%B6%E5%BE%A1%E3%81%99%E3%82%8B/

## SNSをトリガーにする

トリガーをSNSトピックに選択することでいろんなサービスと連携できます。

例えば、RDSイベントサブスクリプションのスナップショット完了通知をトリガーにDRコピーをしたい場合、RDSイベントサブスクリプションをLambdaのトリガーに設定することはできませんが、一度SNSトピックに通知し、そのSNSトピックをトリガーにLambdaを実行することはできます。

つまり**SNSトピックを間に置いておく**ことで、各サービスとLambdaを連携することができるわけです。SNSはいろんなサービスと連携ができるので、Lambdaと連携できないなと思ったらSNSと連携できるかどうかを確認してみるといいかもです。

## S3からファイルを読み込む

S3からファイルを読み込みたいときってたまにあります。

そんなときは以下のようにやってみてください。

```python

import boto3

s3 = boto3.client('s3')
bucket_name = os.environ['BUCKET']

def lambda_handler(event, context):
    file_name = 'syukujitsu.csv' 
    
    response = s3.get_object(Bucket=bucket_name, Key=file_name)
    body = response['Body'].read()

    bodystr = body.decode('utf-8')
    lines = bodystr.split('\r\n')
    
    print(lines)
```

上記は日本の祝日がリスト化されているCSVファイルを読み込んでいます。

こちらの方法は以下の記事を参考にしております。

https://www.kabegiwablog.com/entry/2018/01/17/090000

## 一時的なファイルを利用する

Lambda関数内でファイルを生成してS3などに保存したいとき、一時的なファイルを利用すると便利です。

```python

def lambda_handler(event, context):
    # ready to tmpfile
    filepath = '/tmp/tmp.csv'

    savepath = test.csv
    header = ['name', 'age']
    data = [['judy','18'],['mary','20']]

    # create tmpfile
    with open(filepath, 'w', newline='', encoding='utf-16', errors='ignore') as f:
        writer = csv.writer(f, dialect='excel-tab', quoting=csv.QUOTE_ALL)
        writer.writerow(header)
        writer.writerows(data)
    
    # upload
    s3 = boto3.resource('s3')
    s3.meta.client.upload_file(filepath, bucket, savepath)
    
    # delete tmpfile
    os.remove('/tmp/data.csv')
```

上記のように一時ファイルにデータを書き込むことで、S3にLambda関数内で生成したファイルをアップロードできます。

こちらの方法は以下の記事を参考にしております。

https://qiita.com/nemutas/items/c3346a866fa7fe6f7d60

## Lambda関数の失敗検知

Lambdaのコンソールにて、「送信先を追加」を選択肢すると、失敗したときの通知設定が可能です。SNSトピックを指定すれば失敗したとき、登録しているメールアドレスに通知がいくようになります。

正常時の場合でも送信が可能ですので、このLambdaが成功したのをトリガーに○○をしたいと思ったときに使ってみるといいかもです。送信先は、SNSトピック、SQSキュー、Lambda関数、EventBridgeイベントパス、を指定することが可能です。



# 注意点

- グローバル変数の再利用

Lambdaは一定の時間を過ぎると初期化されますが、そうでない場合は（頻繁にLambdaが実行される、例えば30秒以内に2回とか）、グローバル変数が再利用されます。

例えば以下のようにコードを書いている場合、

```python
from datetime import datetime

now = datetime.now()

def lambda_handler(event, context):
    # code.....
```

現在時刻が正しく取得できず、同じ時刻が繰り返し利用されてしまいます。対処方法は簡単で、内容が変化する変数は関数内に記述すると影響を受けません。

こちらの方法は以下の記事を参考にしております。

https://qiita.com/aj2727/items/3987d840047cef8f0cb5

# 個人的にやっていること

- **インスタンスオブジェクトの書き方、リージョン名は基本いれない**

```python

region_name = "ap-northeast-1"
ec2 = boto3.client('ec2', region_name=region)
```

上記のようにリージョン名書いたりするのをよく見かけますが、別リージョン使わないならいらないと思いますので、私は記述しません。

- **環境変数は関数の外に置いておく**

```python

import boto3

s3 = boto3.client('s3')
bucket_name = os.environ['BUCKET']

def lambda_handler(event, context):
    response = s3.get_object(Bucket=bucket_name, Key=file_name)
    print(response['Body'])
```

定数となるものは関数の外に置いておきます。
これは単純に、間違えて代入しないようにです。

- **外部ライブラリをなるべく使わない**

外部ライブラリを使うとzipでまとめてアップロードしなきゃいけなくなるので、なるべく標準ライブラリで実装します。Lambdaのテストを行うときに、いちいちアップロードするのがめんどくさいだけです。

---------------------------------------

以上、ご参考になれば幸いです。

**Thanks.**