Serverless Framework から AWS Systems Manager パラメータストアを利用する

# はじめに

Serverless Framework でアプリケーションを作成する際、秘密情報をリポジトリ内に管理せず外部で管理して取得したい場合があります。
そこで Serverless Framework では、AWS Systems Manager パラメータストアに対応し、管理したパラメータを取得して展開できます。
今回は Go のサンプルアプリケーションを元に、 AWS Systems Manager パラメータストアと連携してどのように値を取得するのか記載します。

# 環境

- macOS High Sierra Version 10.13.6
- Node.js v10.15.3
- npm 6.9.0
- Go 1.12.1
- Serverless Framework 1.39.1
- AWS CLI 1.16.120
- github.com/aws/aws-lambda-go v1.9.0
- github.com/aws/aws-sdk-go v1.18.6

# Serverless Framework のインストール

Node.js・Go・AWS CLI がインストールされている前提で始めます。
インストールしていない方は各ランタイム、CLI のインストールから始めてください。
まずは、Serverless Framework をインストールします。

```bash
$ npm install -g serverless
```

すでにインストールされている方はアップデートしましょう。

```bash
$ npm update -g serverless
```

# サンプルアプリケーションの作成

任意のディレクトリで以下のコマンドからサンプルアプリケーションを作成します。
`sls` は、`serverless` コマンドの省略形です。

```bash
$ sls create --template aws-go --path sample
```

以下のようなテンプレートプロジェクトが作成されます。

```bash
$ tree sample
sample
├── Makefile
├── hello
│   └── main.go
├── serverless.yml
└── world
    └── main.go
```

作成したディレクトリ内で Go Modules を有効にします。

```bash
$ cd sample
$ export GO111MODULE=on
$ go mod init
$ go get github.com/aws/aws-lambda-go
```

サンプルアプリケーションを少し修正します。
`serverless.yml` を開き、以下のコメントアウトを外して region を `ap-northeast-1` に変更します。

```diff:serverless.yml
 provider:
   name: aws
   runtime: go1.x

 # you can overwrite defaults here
-#  stage: dev
-#  region: us-east-1
+  stage: dev
+  region: ap-northeast-1
```

また、Makefile に `undeploy` コマンドを追加しておきます。

```diff:Makefile
-.PHONY: build clean deploy
+.PHONY: build clean deploy undeploy

 build:
 	env GOOS=linux go build -ldflags="-s -w" -o bin/hello hello/main.go
 	env GOOS=linux go build -ldflags="-s -w" -o bin/world world/main.go

 clean:
 	rm -rf ./bin

 deploy: clean build
 	sls deploy --verbose
+
+undeploy:
+	sls remove --verbose
```

確認のためにサンプルアプリケーションをデプロイしてみます。

```bash
$ make deploy
```

デプロイ後以下のようなエンドポイントが払い出されます。

```bash
endpoints:
  GET - https://odf7wg3qii.execute-api.ap-northeast-1.amazonaws.com/dev/hello
  GET - https://odf7wg3qii.execute-api.ap-northeast-1.amazonaws.com/dev/world
```

それぞれのエンドポイントを確認すると以下のようなレスポンスが得られます。

```bash
$ curl https://odf7wg3qii.execute-api.ap-northeast-1.amazonaws.com/dev/hello
{"message":"Go Serverless v1.0! Your function executed successfully!"}
$ curl https://odf7wg3qii.execute-api.ap-northeast-1.amazonaws.com/dev/world
{"message":"Okay so your other function also executed successfully!"}
```

# パラメータを取得して注入する

さて本題ですが、AWS からパラメータを取得する方法として大きく 2 つの方法があります。
AWS Systems Manager パラメータストアと AWS Secrets Manager です。
AWS Systems Manager パラメータストアは、無料でパラメータを管理するサービスです。AWS KMS の料金が別途掛かりますが、AWS KMS による暗号化にも対応しています。
AWS Secrets Manager は、有料ですが AWS KMS で暗号化し、RDS などの DB 接続情報を自動更新したい場合に特に有効なサービスです。
今回は AWS Systems Manager パラメータストアを使用します。
AWS Secrets Manager を用いる場合には、以下のドキュメントを参考にしてください。
[Reference Variables using AWS Secrets Manager - Serverless Framework Documentation](https://serverless.com/framework/docs/providers/aws/guide/variables#reference-variables-using-aws-secrets-manager)

## パラメータストアに文字列として保存した値を取得する

まずは AWS Systems Manager パラメータストアへ暗号化せずに、パラメータを保存して取得します。
以下のコマンドで文字列のパラメータを作成します。

```bash
$ aws ssm put-parameter --name "MY_SECRET" \
    --description "文字列パラメータ" \
    --type "String" \
    --value "String parameter"
```

作成した `MY_SECRET` を環境変数から取得するように `serverless.yml` を修正します。

```diff:serverless.yml
 provider:
   name: aws
   runtime: go1.x

   # you can overwrite defaults here
   stage: dev
   region: ap-northeast-1
+  environment:
+    MY_SECRET: ${ssm:MY_SECRET}
```

`environment` に環境変数名を指定し、`${ssm:パラメータ名}` とすることでパラメータストアから取得できます。

hello の main 関数を修正して追記した環境変数を取得します。

```diff:hello/main.go
 package main

 import (
     "bytes"
     "context"
     "encoding/json"
+    "os"

     "github.com/aws/aws-lambda-go/events"
     "github.com/aws/aws-lambda-go/lambda"
 )

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

 // Handler is our lambda handler invoked by the `lambda.Start` function call
 func Handler(ctx context.Context) (Response, error) {
     var buf bytes.Buffer

+    var mySecret = os.Getenv("MY_SECRET")
+
+    var parameter = "MY_SECRET: " + mySecret
+
     body, err := json.Marshal(map[string]interface{}{
-        "message": "Go Serverless v1.0! Your function executed successfully!",
+        "message": parameter,
     })
     if err != nil {
         return Response{StatusCode: 404}, err
     }
```

再度デプロイします。

```bash
$ make deploy
```

hello のエンドポイントを確認すると `MY_SECRET` の環境変数にパラメータストアへ保存した値が取得できていることが分かります。

```bash
$ curl https://odf7wg3qii.execute-api.ap-northeast-1.amazonaws.com/dev/hello
{"message":"MY_SECRET: String parameter"}
```

AWS Lambda の画面からも `MY_SECRET` の環境変数が展開されていることが分かります。
![LambdaからMY_SECRETを参照](https://qiita-image-store.s3.amazonaws.com/0/129299/a59ed3ea-f03e-b5ed-b2d1-36d5162f4d6b.png "LambdaからMY_SECRETを参照")

## パラメータストアに安全な文字列として保存した値を取得する

次に AWS KMS で値を暗号化してパラメータストアから取得してみます。

まずは、AWS KMS の暗号化キーを作成します。
特に細かいポリシーは作成していませんが、必要に応じて指定してください。

```bash
$ aws kms create-key --description "Sample key"
```

作成した暗号化キーに分かりやすい名前を付けておきます。
KeyId には、`aws kms create-key` で作成した際に表示された KeyId を指定します。

```bash
$ aws kms create-alias --alias-name alias/sample-key --target-key-id ${KeyId}
```

作成した暗号化キーを用いてパラメータを作成します。

```bash
$ aws ssm put-parameter --name "MY_SECRET2" \
    --description "安全な文字列パラメータ" \
    --type "SecureString" \
    --value "Secure string parameter" \
    --key-id "alias/sample-key"
```

作成した `MY_SECRET2` を環境変数から取得するように `serverless.yml` を修正します。

```diff:serverless.yml
 provider:
   name: aws
   runtime: go1.x

   # you can overwrite defaults here
   stage: dev
   region: ap-northeast-1
   environment:
     MY_SECRET: ${ssm:MY_SECRET}
+    MY_SECRET2: ${ssm:MY_SECRET2~true}
```

`~true` を付与することで暗号化したパラメータを復号化した状態で取得できます。

hello の main 関数を修正して追記した環境変数を取得します。

```diff:hello/main.go
 // Handler is our lambda handler invoked by the `lambda.Start` function call
 func Handler(ctx context.Context) (Response, error) {
     var buf bytes.Buffer

     var mySecret = os.Getenv("MY_SECRET")
+    var mySecret2 = os.Getenv("MY_SECRET2")

-    var parameter = "MY_SECRET: " + mySecret
+    var parameter = "MY_SECRET: " + mySecret + ", MY_SECRET2: " + mySecret2

     body, err := json.Marshal(map[string]interface{}{
         "message": parameter,
     })
     if err != nil {
         return Response{StatusCode: 404}, err
     }
```

再度デプロイします。

```bash
$ make deploy
```

再度 hello のエンドポイントを確認すると `MY_SECRET2` の環境変数にパラメータストアへ暗号化して保存した値が取得できていることが分かります。

```bash
$ curl https://odf7wg3qii.execute-api.ap-northeast-1.amazonaws.com/dev/hello
{"message":"MY_SECRET: String parameter, MY_SECRET2: Secure string parameter"}
```

AWS Lambda の画面からも `MY_SECRET2` の環境変数が展開されていることが分かります。
![LambdaからMY_SECRET2を参照](https://qiita-image-store.s3.amazonaws.com/0/129299/04d32ad7-4f48-23a2-811f-891f9a8cecc0.png "LambdaからMY_SECRET2を参照")
しかしここで課題に挙げるとすれば、AWS Lambda の画面上で暗号化したパラメータが平文として見えてしまうことです。
AWS Lambda の画面上での復号化について問題とする場合には次の方法で対策が可能です。

## AWS Lambda 内からパラメータストアに安全な文字列として保存した値を取得する

AWS Lambda 内で復号化まで完結したい場合には、AWS Lambda 内から AWS SDK for Go を利用して復号化する必要があります。

`serverless.yml` を修正します。
`MY_SECRET2` は、AWS Lambda 内から復号化するため削除し、AWS SDK for Go から復号するためのロールを付与します。
対象のリソースは復号するパラメータのリソースに限定するとより良いですが、今回は全体で指定しています。
適宜調整してください。

```diff:serverless.yml
 provider:
   name: aws
   runtime: go1.x

   # you can overwrite defaults here
   stage: dev
   region: ap-northeast-1
+  iamRoleStatements:
+    - Effect: "Allow"
+      Action:
+        - "ssm:GetParameter"
+      Resource: "*"
+    - Effect: "Allow"
+      Action:
+        - "kms:Decrypt"
+      Resource: "*"
   environment:
     MY_SECRET: ${ssm:MY_SECRET}
-    MY_SECRET2: ${ssm:MY_SECRET2~true}
```

Go Modules 有効になった状態で AWS SDK for Go をインストールします。

```bash
$ go get github.com/aws/aws-sdk-go
```

AWS SDK for Go を利用してパラメータ取得するように hello の main 関数を修正します。

```diff:hello/main.go
 package main

 import (
     "bytes"
     "context"
     "encoding/json"
     "os"

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

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

 // Handler is our lambda handler invoked by the `lambda.Start` function call
 func Handler(ctx context.Context) (Response, error) {
     var buf bytes.Buffer

     var mySecret = os.Getenv("MY_SECRET")
-    var mySecret2 = os.Getenv("MY_SECRET2")
+
+   // Session の作成
+   sess, err := session.NewSession(&aws.Config{
+   	Region: aws.String("ap-northeast-1")},
+   )
+
+   // KMS サービスクライアントの作成
+   svc := ssm.New(sess)
+
+   // 復号化してパラメータを取得
+   result, err := svc.GetParameter(&ssm.GetParameterInput{
+   	Name:           aws.String("MY_SECRET2"),
+   	WithDecryption: aws.Bool(true),
+   })
+   if err != nil {
+   	return Response{StatusCode: 500}, err
+   }

-    var parameter = "MY_SECRET: " + mySecret + ", MY_SECRET2: " + mySecret2
+    var parameter = "MY_SECRET: " + mySecret + ", MY_SECRET2: " + *result.Parameter.Value

     body, err := json.Marshal(map[string]interface{}{
         "message": parameter,
     })
     if err != nil {
         return Response{StatusCode: 404}, err
     }
```

再度デプロイします。

```bash
$ make deploy
```

再度 hello のエンドポイントを確認すると `MY_SECRET2` の環境変数を指定せずにパラメータストアへ暗号化して保存した値が取得できていることが分かります。

```bash
$ curl https://odf7wg3qii.execute-api.ap-northeast-1.amazonaws.com/dev/hello
{"message":"MY_SECRET: String parameter, MY_SECRET2: Secure string parameter"}
```

AWS Lambda の画面からも `MY_SECRET` の環境変数のみが展開されていることが分かります。
![Lambdaから環境変数を参照](https://qiita-image-store.s3.amazonaws.com/0/129299/4a3083b1-85fa-330c-eac4-193bdfd0e98c.png "Lambdaから環境変数を参照")


## お掃除

サンプルアプリケーションをアンデプロイします。

```bash
$ make undeploy
```

作成したパラメータを削除します。

```bash
$ aws ssm delete-parameter --name "MY_SECRET"
$ aws ssm delete-parameter --name "MY_SECRET2"
```

作成した暗号化キーを削除します。
暗号化キーはすぐには削除されず、削除の待機期間が設けられます。
待機期間は、7 から 30 日の範囲で指定できます。
今回は最短の 7 日を指定します。

KeyId には、暗号化キー作成した際に表示された KeyId を指定します。
分からない場合は、`aws kms list-aliases` コマンドを実行して alias に対応する `TargetKeyId` を確認してください。

```bash
$ aws kms schedule-key-deletion --key-id ${KeyId} --pending-window-in-days 7
```

# さいごに

Serverless Framework から AWS Systems Manager パラメータストアを利用して管理したパラメータを取得できることが分かりました。
Serverless Framework が提供する機能を利用して、パラメータストアから必要に応じて AWS KMS と連携して暗号化したパラメータも取得できるのは非常に便利です。
そして、`~true` を指定して復号化すると AWS Lambda から平文で見えてしまうという課題を挙げました。
試した方法では、内部でパラメータストアから直接復号化してしまうと環境変数として取得する意義が薄れると感じました。
今回は試していませんが、`~true` を指定しなければ暗号化されたテキストが取得できるので、暗号化されたテキストを AWS SDK の AWS KMS の機能を利用して復号化する方が多少の自由度は上がる可能性があります。

# 参考

[AWS の Parameter Store と Secrets Manager、結局どちらを使えばいいのか？比較 - Qiita](https://qiita.com/tomoya_oka/items/a3dd44879eea0d1e3ef5)
[Lambda(Golang)から AWS KMS を復号化する方法 · TechTeco](https://techte.co/2018/01/30/lambda-go-kms/)
[Serverless Framework - Serverless - AWS Guide](https://serverless.com/framework/docs/providers/aws/guide/)
[kms — AWS CLI 1.16.128 Command Reference - Amazon.com](https://docs.aws.amazon.com/cli/latest/reference/kms/index.html)
[ssm — AWS CLI 1.16.128 Command Reference - Amazon.com](https://docs.aws.amazon.com/cli/latest/reference/ssm/index.html)
[ssm - Amazon Web Services - Go SDK - AWS Documentation](https://docs.aws.amazon.com/sdk-for-go/api/service/ssm/)
[カスタマーマスターキーを削除する - AWS Key Management Service](https://docs.aws.amazon.com/ja_jp/kms/latest/developerguide/deleting-keys.html)