AWS Systems Manager パラメータストアを参照するような、AWS Lambda関数を登録して動かしてみる

# What's？

前にこんな記事を書いたのですが、環境変数がふつうに見えてしまうことに困っていました。

[Terraformで、AWS Lambda関数を登録して動かしてみる](https://qiita.com/charon/items/19ab5087f7036dafce4b)

「転送時の暗号化に使用するヘルパー」という話もあるのですが、そういえばと思い、設定をAWS Systems Managerのパラメータストアから取得する方法も試してみようかなと。

[AWS Systems Manager パラメータストア](https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/systems-manager-parameter-store.html)

AWS Systems Manager パラメータストアを使う場合は、AWS Lambdaの環境変数とは別に設定することになります。

[Lambda 関数のグループに共通の環境変数を設定できますか?](https://aws.amazon.com/jp/premiumsupport/knowledge-center/lambda-common-environment-variables/)

というわけで、こんなことをやってみます。

* AWS Lambda関数は、Terraformでzip圧縮してアップロードする
* AWS Lambda関数からログを出力する
* Amazon CloudWatch Logsへ出力するようにロググループを作成する
* AWS Lambda関数から、AWS Systems Manager パラメータストアを使って設定を読み込む
    * AWS Systems Manager パラメーターストアに格納する値は、AWS KMSのCMKで暗号化する
* 動作確認はAWS CLIで行う

前に書いた記事の内容から、AWS Systems Manager パラメータストアを使用するように変更しただけですね。


# AWS Lambda関数から、AWS Systems Manager パラメータストアを使う時の注意点

通常は、AWS Lambda関数からAWS Systems Manager パラメータストアにアクセスするには、権限を追加する以外の作業は不要です。

これが、VPC内で動作するAWS Lambda関数の場合は、話が変わってきます。

[VPC の Lambda 関数で Systems Manager パラメータストアにアクセスするにはどうしたらよいですか?](https://aws.amazon.com/jp/premiumsupport/knowledge-center/lambda-vpc-parameter-store/)

この場合は、AWS Lambda関数にインターネットアクセスが可能なように設定、またはVPCエンドポイントを使用する必要があります。


# 環境

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

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

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

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


# AWS Lambda関数を作成する

作成するAWS Lambda関数も、前回とほぼ同じ。リクエストで受け取った内容から、メッセージを作って返すものにします。

ここで、メッセージの一部を前回は環境変数から受け取っていたのですが、ここをAWS Systems Manager パラメータストア経由にします。

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

`app/lambda.py`

```python
import boto3
import logging

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

client = boto3.client('ssm')

base_message_parameter = client.get_parameter(
    Name = '/App/Lambda/Config/BASE_MESSAGE',
    WithDecryption = True
    )
base_message = base_message_parameter['Parameter']['Value']

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)

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

AWS LambdaのPythonランタイムには、Boto3が含まれているため、そのまま使えます。

```python
import boto3
```

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

今回は、起動時に値を取得してしまうことにしました。

```python
client = boto3.client('ssm')

base_message_parameter = client.get_parameter(
    Name = '/App/Lambda/Config/BASE_MESSAGE',
    WithDecryption = True
    )
base_message = base_message_parameter['Parameter']['Value']
```

[SSM / Client](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ssm.html#client)

[get_parameter](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ssm.html#SSM.Client.get_parameter)

今回はAWS Systems Manager パラメータストアから単一の値を取得していますが、ある程度一括で取得することも可能です。

[get_parameters](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ssm.html#SSM.Client.get_parameters)


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

基本的には、前の記事と同じです。先に、ポイントだけ絞って書いていきます。

ソースコード全体は、最後に載せることにします。

AWS Lambda関数のリソース定義は、こんな感じに。環境変数（`environment`）の定義はありません。

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

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

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

AWS KMSキーおよびエイリアスを作成すると共に、AWS Systems Manager パラメータストアに設定を追加します。

```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
}

resource "aws_ssm_parameter" "lambda_variable_message_base" {
  name   = "/App/Lambda/Config/BASE_MESSAGE"
  type   = "SecureString"
  value  = "Hello"
  key_id = aws_kms_key.lambda_key.id
}
```

前の記事では、AWS Lambda関数のリソース定義に、こんな感じで入っていました。

```go

  kms_key_arn = aws_kms_key.lambda_key.arn

  ...

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

あと、AWS Lambdaに付与するIAMロールに、AWS Systems Manager パラメータストアにアクセスできるように権限設定も必要ですね。`ssm:GetParameter`を追加します。

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

  statement {
    effect = "Allow"

    actions = [
      "kms:Decrypt",
      "ssm:GetParameter"
    ]

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


# 確認

これで、`apply`して

```shell
$ terraform apply
```

ログを`tail`しつつ

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

確認。

```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


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

OKですね。

これで、AWS Lambda関数からAWS Systems Manager パラメータストアから、値を取得できるようにTerraformで構成できました、と。


# オマケ

以下、Terraformの構成ファイル全体です。

`main.tf `

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

provider "aws" {
  version = "2.70.0"
}

provider "archive" {
  version = "1.3.0"
}

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"

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

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

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

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

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",
      "ssm:GetParameter"
    ]

    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
}

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

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
}

resource "aws_ssm_parameter" "lambda_variable_message_base" {
  name   = "/App/Lambda/Config/BASE_MESSAGE"
  type   = "SecureString"
  value  = "Hello"
  key_id = aws_kms_key.lambda_key.id
}
```