Help us understand the problem. What is going on with this article?

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 をインストールします。

$ npm install -g serverless

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

$ npm update -g serverless

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

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

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

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

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

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

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

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

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 コマンドを追加しておきます。

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

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

$ make deploy

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

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

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

$ 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

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

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

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

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

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 関数を修正して追記した環境変数を取得します。

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
     }

再度デプロイします。

$ make deploy

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

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

AWS Lambda の画面からも MY_SECRET の環境変数が展開されていることが分かります。
LambdaからMY_SECRETを参照

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

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

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

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

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

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

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

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

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

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 関数を修正して追記した環境変数を取得します。

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
     }

再度デプロイします。

$ make deploy

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

$ 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を参照
しかしここで課題に挙げるとすれば、AWS Lambda の画面上で暗号化したパラメータが平文として見えてしまうことです。
AWS Lambda の画面上での復号化について問題とする場合には次の方法で対策が可能です。

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

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

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

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 をインストールします。

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

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

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
     }

再度デプロイします。

$ make deploy

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

$ 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から環境変数を参照

お掃除

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

$ make undeploy

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

$ 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 を確認してください。

$ 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
Lambda(Golang)から AWS KMS を復号化する方法 · TechTeco
Serverless Framework - Serverless - AWS Guide
kms — AWS CLI 1.16.128 Command Reference - Amazon.com
ssm — AWS CLI 1.16.128 Command Reference - Amazon.com
ssm - Amazon Web Services - Go SDK - AWS Documentation
カスタマーマスターキーを削除する - AWS Key Management Service

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away