Terraformで、AWS Lambda関数を登録して動かしてみる

# What's？

* はじめてのAWS Lambdaを試してみたい
* AWS LambdaのデプロイはTerraformで行いたい

というお題で。

やっておきたいこと、動かし方としては

* AWS Lambda関数は、Terraformでzip圧縮してアップロードする
* AWS Lambda関数からログを出力する
    * Amazon CloudWatch Logsへ出力するようにロググループを作成する
* AWS Lambda関数から環境変数を読み込む
    * 環境変数の暗号化にはAWS KMSのCMKを用いる
* 動作確認はAWS CLIで行う

というところで。


# 環境

今回の環境は、こちらです。

```shell
$ terraform version
Terraform v0.12.29
+ provider.archive v1.3.0
+ provider.aws v2.70.0


$ aws --version
aws-cli/2.0.36 Python/3.7.3 Linux/4.15.0-112-generic exe/x86_64.ubuntu.18
```

AWSのクレデンシャルは、環境変数で設定します。

```shell
$ export AWS_ACCESS_KEY_ID=...
$ export AWS_SECRET_ACCESS_KEY=...
$ export AWS_DEFAULT_REGION=ap-northeast-1
```


# AWS Lambda関数を作成する

最初に、AWS Lambda関数を作成します。

AWS Lambdaのラインタイムはいろいろ選ぶことができるわけですが、今回はPythonにしました。

[AWS Lambda ランタイム](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-runtimes.html)

[Python による Lambda 関数のビルド](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-python.html)

[Python の AWS Lambda 関数ハンドラー](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/python-handler.html)

Pythonのランタイムは、現時点の最新バージョンである3.8を使うことにします。

ソースコードは、こちら。

`app/lambda.py`  
※ `app`というディレクトリ配下に置いています

```python
import logging
import os

logger = logging.getLogger('lambda_logger')
logger.setLevel(logging.INFO)

def handler(event, context):
    logger.info('function = %s, version = %s, request_id = %s', context.function_name, context.function_version, context.aws_request_id)
    logger.info('event = %s', event)

    base_message = os.environ['BASE_MESSAGE']

    last_name = event['last_name']
    first_name = event['first_name']
    
    return { 'message': f'{base_message}, {first_name} {last_name}!!' }
```

環境変数とリクエストから値を読みつつ、メッセージにして返す、という簡単な関数です。

```python
    base_message = os.environ['BASE_MESSAGE']

    last_name = event['last_name']
    first_name = event['first_name']
    
    return { 'message': f'{base_message}, {first_name} {last_name}!!' }
```


ログの実装は、こちらを参考に。`print`でもAmazon CloudWatch Logsにログの保存は可能なようですが、今回ははPythonのログ出力機能を使用することにします。

[Python の AWS Lambda 関数ログ作成](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/python-logging.html)

[logging --- Python 用ロギング機能](https://docs.python.org/ja/3.8/library/logging.html)

動かしてみてハマったのですが、ログレベルを`INFO`にしないと、今回のコードではログが出力されませんでした…。

```python
logger = logging.getLogger('lambda_logger')
logger.setLevel(logging.INFO)
```


# Terraformの構成ファイルを書く

それでは、Terraformの構成ファイルを書いていきます。次のファイルに。

`main.tf`

## TerraformやProviderのバージョン指定

```go
terraform {
  required_version = "0.12.29"
}

provider "aws" {
  version = "2.70.0"
}

provider "archive" {
  version = "1.3.0"
}
```


## AWS Lambda関数をアップロードする定義

先ほど作成した、Pythonコードをアップロードする定義を書きます。

```python
locals {
  function_name = "my_lambda_function"
}

data "archive_file" "function_source" {
  type        = "zip"
  source_dir  = "app"
  output_path = "archive/my_lambda_function.zip"
}

resource "aws_lambda_function" "function" {
  function_name = local.function_name
  handler       = "lambda.handler"
  role          = aws_iam_role.lambda_role.arn
  runtime       = "python3.8"

  kms_key_arn = aws_kms_key.lambda_key.arn

  filename         = data.archive_file.function_source.output_path
  source_code_hash = data.archive_file.function_source.output_base64sha256

  environment {
    variables = {
      BASE_MESSAGE = "Hello"
    }
  }

  depends_on = [aws_iam_role_policy_attachment.lambda_policy, aws_cloudwatch_log_group.lambda_log_group]
}
```

こちらで、先ほど作成した`app`ディレクトリ配下にあるAWS Lambda関数のファイルをzipファイルにします。

```python
data "archive_file" "function_source" {
  type        = "zip"
  source_dir  = "app"
  output_path = "archive/my_lambda_function.zip"
}
```

で、こちらを使ってAWS Lambda関数をリソース定義。

[Resource: aws_lambda_function](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function)

```python
resource "aws_lambda_function" "function" {
  function_name = local.function_name
  handler       = "lambda.handler"
  role          = aws_iam_role.lambda_role.arn
  runtime       = "python3.8"

  kms_key_arn = aws_kms_key.lambda_key.arn

  filename         = data.archive_file.function_source.output_path
  source_code_hash = data.archive_file.function_source.output_base64sha256

  environment {
    variables = {
      BASE_MESSAGE = "Hello"
    }
  }

  depends_on = [aws_iam_role_policy_attachment.lambda_policy, aws_cloudwatch_log_group.lambda_log_group]
}
```

`handler`は、`[ファイル名（拡張子なし].[関数名]`で指定ですね。

AWS Lambdaに与えるロール、AWS KMSキーは、あとで定義します。この部分ですね。

```go
  role          = aws_iam_role.lambda_role.arn

  kms_key_arn = aws_kms_key.lambda_key.arn
```

環境変数は、こちら。先ほどのPythonコードで、環境変数として読み込んでいた変数名ですね。

```python
  environment {
    variables = {
      BASE_MESSAGE = "Hello"
    }
  }
```

`depends_on`には、このあとに作成するIAMロール、Amazon CloudWatch Logsを設定しています。

```python
  depends_on = [aws_iam_role_policy_attachment.lambda_policy, aws_cloudwatch_log_group.lambda_log_group]
```

この`depends_on`については、Terraformのドキュメントを参考に設定しています。

[Resource: aws_lambda_function](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function)


## IAMロールを定義する

次に、IAMロールを定義します。今回は、`AWSLambdaBasicExecutionRole`ポリシーをベースに、AWS KMSに関する権限を与えることにします。

AWS Lambdaに関するポリシーは、こちらに記載があります。

[AWS Lambda 実行ロール / Lambda 機能の管理ポリシー](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-intro-execution-role.html#permissions-executionrole-features)

AWS CLIで、定義を見てみましょう。

```shell
$ aws iam get-policy --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
{
    "Policy": {
        "PolicyName": "AWSLambdaBasicExecutionRole",
        "PolicyId": "ANPAJNCQGXC42545SKXIK",
        "Arn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
        "Path": "/service-role/",
        "DefaultVersionId": "v1",
        "AttachmentCount": 0,
        "PermissionsBoundaryUsageCount": 0,
        "IsAttachable": true,
        "Description": "Provides write permissions to CloudWatch Logs.",
        "CreateDate": "2015-04-09T15:03:43+00:00",
        "UpdateDate": "2015-04-09T15:03:43+00:00"
    }
}
```

`AWSLambdaBasicExecutionRole`ポリシーに付与されている権限は、こんな感じですね。Amazon CloudWatch Logsに関するものです。

```shell
$ aws iam get-policy-version --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole --version-id v1
{
    "PolicyVersion": {
        "Document": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Action": [
                        "logs:CreateLogGroup",
                        "logs:CreateLogStream",
                        "logs:PutLogEvents"
                    ],
                    "Resource": "*"
                }
            ]
        },
        "VersionId": "v1",
        "IsDefaultVersion": true,
        "CreateDate": "2015-04-09T15:03:43+00:00"
    }
}
```

Assume Roleしつつ、

[AWS Lambda 実行ロール / IAM API によるロールの管理](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-intro-execution-role.html#permissions-executionrole-api)

```go
data "aws_iam_policy_document" "assume_role" {
  statement {
    actions = ["sts:AssumeRole"]
    effect  = "Allow"

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

`AWSLambdaBasicExecutionRole`を拡張する感じにします。

```go
data "aws_iam_policy" "lambda_basic_execution" {
  arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

data "aws_iam_policy_document" "lambda_policy" {
  source_json = data.aws_iam_policy.lambda_basic_execution.policy

  statement {
    effect = "Allow"

    actions = [
      "kms:Decrypt"
    ]

    resources = ["*"]
  }
}

resource "aws_iam_policy" "lambda_policy" {
  name   = "MyLambdaPolicy"
  policy = data.aws_iam_policy_document.lambda_policy.json
}

resource "aws_iam_role_policy_attachment" "lambda_policy" {
  role       = aws_iam_role.lambda_role.name
  policy_arn = aws_iam_policy.lambda_policy.arn
}

resource "aws_iam_role" "lambda_role" {
  name               = "MyLambdaRole"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}
```

具体的には、AWS KMSに関する権限を追加します。

```go
data "aws_iam_policy_document" "lambda_policy" {
  source_json = data.aws_iam_policy.lambda_basic_execution.policy

  statement {
    effect = "Allow"

    actions = [
      "kms:Decrypt"
    ]

    resources = ["*"]
  }
}
```


## Amazon CloudWatch Logsグループを定義する

AWS Lambda関数のログを格納する、Amazon CloudWatch Logsグループを定義します。

```go
resource "aws_cloudwatch_log_group" "lambda_log_group" {
  name = "/aws/lambda/${local.function_name}"
}
```

ロググループ名は、`/aws/lambda/[AWS Lambda関数名]`で作成します。

[Python の AWS Lambda 関数ログ作成 / AWS マネジメントコンソール でログを表示する](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/python-logging.html#python-logging-console)


## AWS KMSキーを定義する

環境変数の暗号化で使用する、AWS KMSキー（CMK）を作成します。

[AWS Lambda 環境変数の使用 / 環境変数の保護](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-envvars.html#configuration-envvars-encryption)

```go
resource "aws_kms_key" "lambda_key" {
  description             = "My Lambda Function Customer Master Key"
  enable_key_rotation     = true
  deletion_window_in_days = 7
}

resource "aws_kms_alias" "lambda_key_alias" {
  name          = "alias/my-lambda-key"
  target_key_id = aws_kms_key.lambda_key.id
}
```


## リソースを作成する

ここまで準備したので、あとはTerraformを実行してリソースを作成します。

```shell
$ terraform apply
```


# 動作確認する

では、動作確認してみます。

Amazon CloudWatch Logsでログをtailしつつ

```shell
$ aws logs tail /aws/lambda/my_lambda_function --follow
```

こちらを参考にして

[同期呼び出し](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/invocation-sync.html)

AWS Lambda関数を呼び出してみます。

```shell
$ aws lambda invoke --function-name my_lambda_function --payload '{"first_name": "Taro", "last_name": "Tanaka"}' --log-type Tail output.txt

Invalid base64: "{"first_name": "Taro", "last_name": "Tanaka"}"
```

が、エラーになります…。

今回使っているAWS CLI v2では、与えるパラメーターの扱いが変わっているようです。

[AWS CLI バージョン 2 はデフォルトで base64 エンコードされた文字列としてバイナリパラメータを渡すようになりました](https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cliv2-migration.html#cliv2-migration-binaryparam)

というわけで、`--cli-binary-format raw-in-base64-out`オプションをつけて実行

```shell
$ aws lambda invoke --function-name my_lambda_function \
 --payload '{"first_name": "Taro", "last_name": "Tanaka"}' \
 --cli-binary-format raw-in-base64-out \
 --log-type Tail output.txt
{
    "StatusCode": 200,
    "LogResult": "U1RBUlQgUmVxdWVzdElkOiBlNTZhYTI5Ny1jYTQyLTQ2NWEtOTNiZC1iZTZhMWQxZTQ5ZjAgVmVyc2lvbjogJExBVEVTVApbSU5GT10JMjAyMC0wNy0zMVQxNjozNDowMi41ODJaCWU1NmFhMjk3LWNhNDItNDY1YS05M2JkLWJlNmExZDFlNDlmMAlmdW5jdGlvbiA9IG15X2xhbWJkYV9mdW5jdGlvbiwgdmVyc2lvbiA9ICRMQVRFU1QsIHJlcXVlc3RfaWQgPSBlNTZhYTI5Ny1jYTQyLTQ2NWEtOTNiZC1iZTZhMWQxZTQ5ZjAKW0lORk9dCTIwMjAtMDctMzFUMTY6MzQ6MDIuNTgzWgllNTZhYTI5Ny1jYTQyLTQ2NWEtOTNiZC1iZTZhMWQxZTQ5ZjAJZXZlbnQgPSB7J2ZpcnN0X25hbWUnOiAnVGFybycsICdsYXN0X25hbWUnOiAnVGFuYWthJ30KRU5EIFJlcXVlc3RJZDogZTU2YWEyOTctY2E0Mi00NjVhLTkzYmQtYmU2YTFkMWU0OWYwClJFUE9SVCBSZXF1ZXN0SWQ6IGU1NmFhMjk3LWNhNDItNDY1YS05M2JkLWJlNmExZDFlNDlmMAlEdXJhdGlvbjogMS4zNiBtcwlCaWxsZWQgRHVyYXRpb246IDEwMCBtcwlNZW1vcnkgU2l6ZTogMTI4IE1CCU1heCBNZW1vcnkgVXNlZDogNTEgTUIJSW5pdCBEdXJhdGlvbjogMTEzLjA1IG1zCQo=",
    "ExecutedVersion": "$LATEST"
}
```

今度は、呼び出せたようです。結果を確認。

```shell
$ cat output.txt
{"message": "Hello, Taro Tanaka!!"}
```

OKです。

Amazon CloudWatch Logsの方は、こんな感じの出力になります。

```shell
2020-07-31T16:34:02.582000+00:00 2020/07/31/[$LATEST]31685a31fb3045d49d3585154de2b0dd START RequestId: e56aa297-ca42-465a-93bd-be6a1d1e49f0 Version: $LATEST
2020-07-31T16:34:02.583000+00:00 2020/07/31/[$LATEST]31685a31fb3045d49d3585154de2b0dd [INFO]	2020-07-31T16:34:02.582Z	e56aa297-ca42-465a-93bd-be6a1d1e49f0	function = my_lambda_function, version = $LATEST, request_id = e56aa297-ca42-465a-93bd-be6a1d1e49f0
2020-07-31T16:34:02.583000+00:00 2020/07/31/[$LATEST]31685a31fb3045d49d3585154de2b0dd [INFO]	2020-07-31T16:34:02.583Z	e56aa297-ca42-465a-93bd-be6a1d1e49f0	event = {'first_name': 'Taro', 'last_name': 'Tanaka'}
2020-07-31T16:34:02.584000+00:00 2020/07/31/[$LATEST]31685a31fb3045d49d3585154de2b0dd END RequestId: e56aa297-ca42-465a-93bd-be6a1d1e49f0
2020-07-31T16:34:02.584000+00:00 2020/07/31/[$LATEST]31685a31fb3045d49d3585154de2b0dd REPORT RequestId: e56aa297-ca42-465a-93bd-be6a1d1e49f0	Duration: 1.36 ms	Billed Duration: 100 ms	Memory Size: 128 MB	Max Memory Used: 51 MB	Init Duration: 113.05 ms
```

うまくいったようですね。


# 環境変数音暗号化について

AWS Lambdaの環境変数の暗号化についてですが、Terraformで指定できるのは`kms_key_arn`で、こちらは「保存時に暗号化するAWS KMSキー」を指しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309669/d4f3dd33-6cd6-92bf-b811-d5de765bd7b9.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309669/2525f068-c121-f42c-1a28-5c08ac8e7c45.png)

環境変数の値自体は、ふつうに見えています。

マネージメントコンソール上で暗号化した状態にするには、「転送時の暗号化」を行うことになりますが、これはTerraformではできなさそうです…。

AWS CLIでも、見れますね。

```shell
$ aws lambda get-function-configuration --function-name my_lambda_function
{
    "FunctionName": "my_lambda_function",
    "FunctionArn": "arn:aws:lambda:ap-northeast-1:[AWSアカウントID]:function:my_lambda_function",
    "Runtime": "python3.8",
    "Role": "arn:aws:iam::[AWSアカウントID]:role/MyLambdaRole",
    "Handler": "lambda.handler",
    "CodeSize": 394,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2020-07-31T16:48:37.579+0000",
    "CodeSha256": "R3089h8mTmE2HmcSK1p5w0qUMdpuSBao2kne6aZw8hU=",
    "Version": "$LATEST",
    "Environment": {
        "Variables": {
            "BASE_MESSAGE": "Hello"
        }
    },
    "KMSKeyArn": "arn:aws:kms:ap-northeast-1:[AWSアカウントID]:key/bf526778-7e70-4a9f-a28f-f1e4ee911c66",
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "038d76ab-1769-445f-845d-56df8720ffc0",
    "State": "Active",
    "LastUpdateStatus": "Successful"
}
```

このあたりは、また時間があったら調べることにしましょう。

ちなみに、`kms_key_arn`を指定しない場合は、環境変数の暗号化にはデフォルトの暗号化キー（AWS管理のCMK）が使用されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309669/72b48282-3e8e-8b3c-039c-857346e766b9.png)