Terraform ✖️ Lambda credential管理

こんにちは！
25卒で現在はスタートアップのSRE部署でインターンをしている大学生です。
先日TerraformでLambda環境を作成する際に、credential管理について色々検討し、自分の中での落とし所を見つけたのでアウトプットしたいと思います。
ローカル環境もある程度忠実に再現できますので、誰かの役に立てたら幸いです。

最初に具体的な方法を語り、後半になぜこのような方法に至ったかについて、別の方法と比較検討しつつ解説したいと思います。

## 前提知識

- Terraformの基礎 (コード例見れば知識なくてもわかるかも)
- Lambdaの基礎

## アプリケーション概要

一応今回解説対象として作成したアプリケーションの概要について記載しました。
スキップしてもあまり問題ありません。

### Github Link

ローカル環境の構築する際に参考になればと思います。
Readmeに手順書いています。

https://github.com/yuta-2001/go-no-commit-notify

### 構成図
![スクリーンショット 2024-08-24 午後3.08.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3470041/da50b690-27c2-8aa3-934c-ff829845b142.png)

### 使用技術

- golang
- aws (sam, eventbridge, ecr, lambda, iam, kms, parameter store)
- terraform
- docker (localstack)
- github actions
- line notify api, github api

### 管理対象のcredentail

- github token
- line notify token

## credential管理の具体的方法

今回のアプリケーションでは、外部APIを利用するためのcredential情報として、GitHub TokenとLINE Notify Tokenを扱っています。以下はそれらをTerraformとLambdaを使用して安全に管理する方法です。

### 1. **Parameter Store リソース (terraform)**

credential情報を保存するため、AWSのParameter Storeリソースを作成します。Parameter Storeは環境変数などの情報を管理できるサービスです。(https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/systems-manager-parameter-store.html)
ここでは`SecureString`を指定し、値は後でAWS CLIを使用して上書きするため、ダミー値を設定しています。

```terraform
resource "aws_ssm_parameter" "github_token" {
  name  = "/${var.project_prefix}/github_token"
  type  = "SecureString"
  value = "github_token"

  # valueはaws cliから上書きするため、変更を無視する
  lifecycle {
    ignore_changes = [value]
  }
}

resource "aws_ssm_parameter" "line_notify_token" {
  name  = "/${var.project_prefix}/line_notify_token"
  type  = "SecureString"
  value = "line_notify_token"

  # valueはaws cliから上書きするため、変更を無視する
  lifecycle {
    ignore_changes = [value]
  }
}
```

### 2. **Lambda リソース (terraform)**

Lambdaの環境変数には、Parameter Storeで保存しているcredentialのパラメーター名を指定し、値自体は含めません。

```terraform
locals {
  /// 省略 ///
  /// 以下lambdaの環境変数 ///
  /// SSM Parameter Storeのnameと一致させる ///
  enviroment_variables = {
    ENV                          = "${var.enviroment}"
    GITHUB_USER                  = "${var.user_github}"
    GITHUB_TOKEN_PARAM_NAME      = "/${var.project_prefix}/github_token"
    LINE_NOTIFY_TOKEN_PARAM_NAME = "/${var.project_prefix}/line_notify_token"
  }
}
```

### 3. **Parameter Storeからcredentialを取得 (go)**

Parameter Storeから暗号化された値を取得し、平文にデコードします。シングルトンパターンを用いて、Lambda内で一度だけParameter Storeへの接続を確立します。実際に値を取得しているのはGetParamValueメソッドで、引数にLambdaの環境変数に設定しているパラメーター名を指定すると値を取得できます。

```go
package ssm

import (
    "fmt"
    "os"

    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/ssm"
)

type SsmInstanceStruct struct {
    Client *ssm.SSM
}

var SsmInstance *SsmInstanceStruct

func GetSsmInstance(env string) (*SsmInstanceStruct, error) {
    if SsmInstance == nil {
        sess, err := session.NewSession()
        if env == "local" {
            localKmsEndpoint := os.Getenv("LOCAL_KMS_ENDPOINT")
            if localKmsEndpoint == "" {
                return nil, fmt.Errorf("LOCAL_KMS_ENDPOINT is empty")
            }

            sess, err = session.NewSession(&aws.Config{
                Endpoint: aws.String(localKmsEndpoint),
            })
        }
        if err != nil {
            return nil, err
        }
        svc := ssm.New(sess)
        SsmInstance = &SsmInstanceStruct{
            Client: svc,
        }
    }

    return SsmInstance, nil
}

func (SsmInstance *SsmInstanceStruct) GetParamValue(param string, decrypt bool) (string, error) {
    input := &ssm.GetParameterInput{
        Name:           aws.String(param),
        WithDecryption: aws.Bool(decrypt),
    }

    result, err := SsmInstance.Client.GetParameter(input)
    if err != nil {
        return "", err
    }

    return *result.Parameter.Value, nil
}
```

### 4. **Parameter Storeの値を上書き**

Terraformではダミーの値を設定しているため、AWS CLIを使用して実際のcredential情報に上書きします。`--overwrite`オプションを使用します。

```bash
aws ssm put-parameter --endpoint "http://localhost:4566" --name "/no-commit-notify/github_token" --type SecureString --value "***" --overwrite
aws ssm put-parameter --endpoint "http://localhost:4566" --name "/no-commit-notify/line_notify_token" --type SecureString --value "***" --overwrite
```

## 考慮点・この方法にした背景

LambdaとTerraformでcredential管理をする際に、以下のポイントを考慮しました。

### 1. Lambdaの環境変数にcredential情報を含めないようにする

Lambda関数内でAPI Keyのようなcredentialを使用したい場合に、Lambdaの環境変数に直接ベタガキするといった手法を散見します。(昔は自分も知らずにやっていました笑)
以下記事にまとまっていますが、AWS APIの[GetFunction](https://docs.aws.amazon.com/lambda/latest/api/API_GetFunction.html)を叩かれた場合、環境変数の内容も返されてしまいます。
また、多くの場合関数の更新などにGithubActionsからupdate-lambdaをすると思いますが、その際に環境変数がリポジトリのGithub Actions履歴に平文で出てきてしまいます。結構な被害があるようなので気をつけなければいけないなと思いました。

https://blog.flatt.tech/entry/lambda_secret_security

https://zenn.dev/k_saito7/articles/github-actions-lambda-env-kms

### 2. Terraform経由で設定した場合、tfstateに情報が残る

以下の記事ではterraformのsecret管理のbest practiceとして、variableを外部から渡すことでcredentialを設定しています。
ただ、tfstateには平文で残ってしまうのであまり良いとは言えません。
もし万が一流出した場合に、暗号化されていないと危険ですのでこれも避けるべきかなと思います。

https://blog.gitguardian.com/how-to-handle-secrets-in-terraform/

### 3. 別の方法との比較・検討

**AWS KMSをそのまま使う**
Parameter StoreのSecureStringは内部的にKMSを使用して暗号化しているため、直接KMSを使用するのも一つの選択肢です。ただし、長い暗号化された文字列を扱うより、Parameter Storeのパラメーター名を使う方がシンプルで分かりやすいと考えました。

**Secret Managerを使う**

Secret ManagerはParameter Storeと同様にcredential管理として使用されています。
Secret ManagerとParameter Storeの大きな違いは、Secret Managerはcredential情報の自動更新があるのと料金がかかる、あたりかなと思います。(Parameter Storeは無料ですがSecureStringは内部的にKMSを使用しているので、KMSの使用量はかかります。)
AWSの内部サービスのkeyを管理するにはこちらの方がセキュアかもしれませんが、今回は外部APIの管理なのでParameter Storeで良いかなという判断です。

https://qiita.com/tomoya_oka/items/a3dd44879eea0d1e3ef5

## 終わりに

この方法がベストなのかは全くわかりません笑
ただ、一般的な脅威に対する防御はできていると思いますしそこまで複雑ではないので、このくらいのアプリケーションではある程度妥当な方法ではないのかなと思っています。
ひよっこなのでTerraform、Lambdaのcredential管理に詳しい方いたらコメントいただけると嬉しいです。

## 参考記事

- https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/systems-manager-parameter-store.html
- https://blog.flatt.tech/entry/lambda_secret_security
- https://zenn.dev/k_saito7/articles/github-actions-lambda-env-kms
- https://blog.gitguardian.com/how-to-handle-secrets-in-terraform/