[re:Invent2018] セッションで紹介されたLambdaのtipsを試してみた[1] ~環境変数とParameter Storeによる設定の管理~

はじめに

こんにちは。データサイエンスチームのmotchieです。
この記事はNHN テコラス Advent Calendar 2018の11日目の記事です。

先日、ラスベガスで開催されたre:Invent2018に参加してきました。
イベントの規模の大きさと怒涛の新サービス発表、そしてホテルの朝食・昼食の美味しさに感動しました。ベネチアンのメキシコ料理、来年もぜひ食べに行きたいです……。

もちろん、ご飯だけでなく、セッションやワークショップにも参加してきました。
この記事では、 Optimizing Your Serverless Applications (SRV401-R2) のセッションの中から、特に使ってみたいと感じたLambdaのtipsをご紹介します。
今回は、AWS Systems ManagerのParameter Storeを使った設定情報の管理と、Lambdaの環境変数による環境の切り替えについて、実際の実装方法にも触れつつ、詳しく見ていきたいと思います。

セッションの資料

Optimizing Your Serverless Applications (SRV401-R2) のセッションは、スライドと動画が公開されています。
セッション全体の内容が気になる方は、ぜひこちらも見てみてください。

セッションでは、Lambda関数の実装や実行環境に関して、様々なtipsが挙げられていました。
Optimizing Your Serverless Applications (SRV401-R2)のスライドp71において、全体のtipsがまとめられています。
この記事では主に、Functionのtipsの上から3つ目、シークレット情報の扱いについて、詳しくご紹介したいと思います。

Your Function Recap:

  • アプリケーションの範囲によって、シークレット情報を共有しよう
    • 単一の関数:環境変数
    • 複数の関数/共有される環境:Parameter Store

この記事の内容

この記事では、セッションの内容を紹介しつつ、tipsにしたがって実際のコードを修正していく中で、

  • デプロイ環境ごとに、関数の設定を動的に切り替える
  • 設定情報やシークレットの値を一元管理し、複数の関数で値を共有する

といった方法を具体的にご紹介できればと思います。

記事の流れは以下の通りです。

  • LambdaのBlueprint:Slackへのアラート通知アプリについて
  • Lambdaの環境変数:デプロイ環境ごとに設定を切り替える
  • Parameter Store:複数の関数からアクセスする設定・シークレットの値を一元管理する

LambdaのBlueprint:Slackへのアラート通知アプリについて

Lambdaでは関数の作成支援のため、事前設定されたコードのテンプレートがBlueprintとして公開されています。
この記事では、Lambdaのcloudwatch-alarm-to-slack-python3というBlueprintをベースに、紹介するLambdaのtipsによって、このコードがどのように修正できるのか、見ていきたいと思います。

Blueprintを使ってCloudWatchのアラートをSlackに通知するには、以下の手順が必要になります。

  1. https://<your-team-domain>.slack.com/services/new をブラウザで開く
  2. Incoming WebHooks のアプリを検索する
  3. 通知を送るチャンネルを選択し、Add Incoming WebHooks Integration からアプリを作成する
  4. webhook URLの値は後で必要なのでコピーしておく。

この記事では、Lambda実装のtipsの紹介が目的なため、各リソースのセットアップ手順については詳しく触れません。詳細を知りたい場合には各自でご確認をお願いします。

それでは、セッションのtipsに従って、Lambda関数の中身を変更してみたいと思います。

Lambdaの環境変数:デプロイ環境ごとに設定を切り替える

Lambdaでは、key-valueの形式で関数ごとに環境変数を設定することが出来ます。
設定された値はローカル環境で環境変数を扱う際と同じ方法で取得できます(例えばPythonなら os.environ )。

セッションでは、Lambdaの環境変数を使って、デプロイ環境(Dev/Test/Prod/etc)ごとに異なる設定情報を指定するというtipsが紹介されていました。
DBの接続情報など、デプロイ環境によって異なる値を環境変数から取得することで、デプロイ環境ごとに関数のコードを書き換える必要がなくなります。

また、環境変数の値は、AWS Key Management Service(KMS)によって暗号化することが可能です。
例えば、Slack通知のLambdaBlueprintでは、WebhookURLの値をKMSによる暗号化を行った上で kmsEncryptedHookUrl として環境変数に登録して使用しています。
この KMSによる暗号化は、Lambdaのマネジメントコンソール上で簡単に行うことが出来ます(暗号化の設定->伝送中の暗号化のためのヘルパーの有効化->暗号化ボタンで環境変数を暗号化)。

しかし、同じ設定情報を複数のLambdaで使う場合は、上記の方法では、Lambda関数の数だけ同じ環境変数を設定する必要があります。
変数の値を後から変更する場合、関数の数だけ修正が必要になり、管理が大変です。

そこで登場するのが、AWS Systems ManagerのParameter Storeです。セッションでは、複数の関数からアクセスする設定・シークレットの値は、Parameter Storeで管理することがオススメされていました。

そこで今回は、Blueprintでは環境変数として使われていた slackHookURLslackChannel の値をParameter Storeで一元管理する形に変更し、複数のLambda関数から設定情報にアクセスできるようにしてみたいと思います。

Parameter Storeとは

AWS ドキュメント: AWS Systems Manager パラメータストア

Parameter Storeは、設定データや機密データを安全に一元管理するためのストレージです。
パスワード、データベース文字列、ライセンスコードなど、設定情報・シークレットの値を、パラメータ値として保存することができます。

また、パラメータの値は、KMSを使って暗号化して保存することが出来ます。
更に、スラッシュ( / )を使用してパスを区切ることで、パラメータに階層構造を持たせることが出来ます
これらによって、パラメータのセキュアな管理とアクセス権限の制御が行いやすいという利点があります。

例えば、以下のように、デプロイ環境ごとに階層を分けて、設定情報・シークレットの値をParameter Storeで管理することが出来ます。

key value
/Prod/SlackTest/Lambda/ChannelName prod_app_channel
/Prod/SlackTest/Lambda/HookUrl hooks.slack.com/services/xxxxxxx
/Dev/SlackTest/Lambda/ChannelName dev_app_channel
/Dev/SlackTest/Lambda/HookUrl hooks.slack.com/services/yyyyyy

Parameter Storeへの値の登録は、AWS CLI、AWS Tools for Windows PowerShell、マネジメントコンソールを使って行うことが出来ます。
AWS CLIの場合、以下のようなコマンドになります。LambdaのBlueprintに従い、ChannelNameは平文で、HookUrlは暗号化して格納しています。--type SecureString の部分で暗号化を指定しています。

$ aws ssm put-parameter --name "/Prod/SlackTest/Lambda/ChannelName" --value "prod_app_channel" --type String
$ aws ssm put-parameter --name "/Prod/SlackTest/Lambda/HookUrl" --value "hooks.slack.com/services/xxxxxxx" --type SecureString

暗号化に用いる鍵(CMK)を指定しない場合、自動的に作成されるデフォルトのCMK(aws/ssm)が使用されます。

パラメータの作成・確認は、マネジメントコンソール上でも行うことが出来ます。

また、パラメータの取得・値の復号化のための権限をLambdaに付与する必要があります。新しくIAMロールを作成し、ポリシーを指定します。
下記の例は、パラメータを東京リージョン(ap-northeast-1)で作成し、暗号化にデフォルトのCMKを使い、本番環境(/Prod)のパラメータにアクセスする場合のIAMポリシーになります。<your aws account id> の値は自身のAWSアカウントIDの値に置き換えてください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ssm:GetParameters"
            ],
            "Resource": [
                "arn:aws:ssm:ap-northeast-1:<your aws account id>:parameter/Prod/SlackTest/Lambda/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "kms:Decrypt"
            ],
            "Resource": [
                "arn:aws:kms:ap-northeast-1:<your aws account id>:key/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "sts:AssumeRole"
            ],
            "Resource": [
                "*"
            ]
        }
    ]
}

ここでは、ssm:GetParameters のアクションで取得出来るパラメータを、名前が/Prod/SlackTest/Lambda/ で始まるリソースにだけに制限しています。
このように、パラメータ名に階層構造を持たせることで、デプロイ環境やアプリケーションによって、シークレットのアクセス権限を制御しやすいというメリットがあります。
注意点ですが、Parameter Storeにアクセスする際、sts:AssumeRoleの許可が必要になります。

Blueprintが作成した環境変数 slackChannelkmsEncryptedHookUrlは削除した上で、関数を作成します。

関数のコードの内容を以下のように変更します。
cloudwatch-alarm-to-slack-python3 のBlueprintを元に、環境変数からChannelName/HookUrlを取得している部分をコメントアウトし(11-16行目)、Parameter Storeからこれらの値を取得するようにしています(21-34行目、36-44行目)。

import boto3
import json
import logging
import os
 
from base64 import b64decode
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
 
 
# The base-64 encoded, encrypted key (CiphertextBlob) stored in the kmsEncryptedHookUrl environment variable
#ENCRYPTED_HOOK_URL = os.environ['kmsEncryptedHookUrl']
# The Slack channel to send a message to stored in the slackChannel environment variable
#SLACK_CHANNEL = os.environ['slackChannel']
 
#HOOK_URL = "https://" + boto3.client('kms').decrypt(CiphertextBlob=b64decode(ENCRYPTED_HOOK_URL))['Plaintext'].decode('utf-8')
 
logger = logging.getLogger()
logger.setLevel(logging.INFO)
 
ssm = boto3.client("ssm", region_name="ap-northeast-1") # parameterを作成したregion
 
PARAM_CHANNEL = os.environ["SlackChannelParam"]
PARAM_URL = os.environ["HookUrlParam"] 

def get_parameter(parameter_name, with_decryption):
    res = ssm.get_parameters(         
        Names=[
            parameter_name         
        ],
        WithDecryption=with_decryption
    )
    parameter_value = res["Parameters"][0]["Value"]
    return parameter_value

SLACK_CHANNEL = get_parameter(
  parameter_name=PARAM_CHANNEL,
  with_decryption=False
)
 
HOOK_URL = "https://" + get_parameter(
  parameter_name=PARAM_URL,
  with_decryption=True
)
 
 
def lambda_handler(event, context):
    logger.info("Event: " + str(event))
    message = json.loads(event['Records'][0]['Sns']['Message'])
    logger.info("Message: " + str(message))
 
    alarm_name = message['AlarmName']
    #old_state = message['OldStateValue']
    new_state = message['NewStateValue']
    reason = message['NewStateReason']
 
    slack_message = {
        'channel': SLACK_CHANNEL,
        'text': "%s state is now %s: %s" % (alarm_name, new_state, reason)
    }
 
    req = Request(HOOK_URL, json.dumps(slack_message).encode('utf-8'))
    try:
        response = urlopen(req)
        response.read()
        logger.info("Message posted to %s", slack_message['channel'])
    except HTTPError as e:
        logger.error("Request failed: %d %s", e.code, e.reason)
    except URLError as e:
        logger.error("Server connection failed: %s", e.reason)

このように、設定情報・シークレットの値は、Lambdaの環境変数ではなく、Parameter Storeで管理・実行時に取得して使う形にすることで、複数の関数に渡って設定情報を共有したり、設定情報の管理・変更をParameter Storeで一括して行うことが出来ます。

最後に、Lambdaの環境変数にParameter Storeのパラメータ名を登録しておきます。
これにより、実行時にデプロイ環境ごとに異なる設定値をParameter Storeから取得することができ、デプロイ環境ごとにコードを書き換える必要がなくなります

key value
SlackChannelParam /Prod/SlackTest/Lambda/ChannelName
HookUrlParam /Prod/SlackTest/Lambda/HookUrl

まとめ

  1. シークレットの値はParameter Storeにて、デプロイ環境ごとにパスを階層化し、KMSで暗号化して、作成・管理する。
  2. Parameter Storeのパラメータ名など、デプロイ環境ごとに異なる値は、Lambdaの環境変数で指定する
  3. 関数は実行時に、デプロイ環境ごとパラメータ名を環境変数から取得し、設定情報・シークレットの値をParameter Storeから取得し、処理に用いる。

という形にすることで、複数のLambda関数でのシークレットの値の共有、設定情報やシークレットの値の一元管理、デプロイ環境ごとの関数の設定切り替えを行うことが出来ます。

この記事では、Slack通知用のLambdaのBlueprintの修正を通じて、Lambda環境変数によるデプロイ環境ごとの設定切り替えと、Parameter Storeを使った設定情報・シークレット情報の一元管理の方法についてご紹介しました。

皆さんもぜひ試してみてください。

参考サイト

SlideShare: Optimizing Your Serverless Applications (SRV401-R2) – AWS re:Invent 2018
YouTube: AWS re:Invent 2018: [REPEAT 2] Optimizing Your Serverless Applications (SRV401-R2)

Developers.IO: Lambdaの「Blueprint」で簡単にSlackとCloudWatchを連携してみた(2017年版)
AWS ドキュメント: AWS Systems Manager パラメータストア

AWS移行支援キャンペーン

あなたにおすすめの記事