AWS Secrets Managerを使ってLambdaのシークレットを管理する

新サービス、AWS Secrets Managerでシークレットに保存される値がどのように管理されているのか、AWS CLIを使って、その構成要素であるバージョン、ステージラベルを色々と操作しながら理解してみたいと思います。
2018.06.11

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

AWS Summit 2018 San Francisco でシークレットを簡単にローテーション、管理、取得するAWS Secrets Manager が発表されました。

AWS Lambda のシークレットを管理する場合、従来は AWS KMS で暗号化し、実行時に復号する方法などが採用されました。

KMSで認証情報を暗号化しLambda実行時に復号化する

AWS Lambda のブループリント(CloudWatchアラームをAmazon SNS経由でSlackに通知する cloudwatch-alarm-to-slack など)もこのアプローチを採用しています。

Lambdaの「Blueprint」で簡単にSlackとCloudWatchを連携してみた(2017年版)

本ブログでは、 このブループリントにおいて、AWS KMS で暗号・復号する代わりに、AWS Secrets Manager でシークレット管理する方法を紹介します。

作業手順

以下の流れで作業します。

  1. SlackのWebhookの準備
  2. AWS Secrets Manager の設定
  3. Lambda Function の作成
  4. 動作確認

1. SlackのWebhookの準備

Webhook URL の取得

次のページを参考にSlack のWebhook URL を取得してください。

Incoming Webhooks | Slack

https://hooks.slack.com/services/XXX/YYY/ZZZ のような形式の URL が発行されます。

cURL から動作確認

Webhook URL をつかってメッセージ投稿できることを確認します。

cURL からリクエストします。

$ URL=https://hooks.slack.com/services/XXX/YYY/ZZZ
$ curl -X POST -H 'Content-type: application/json' \
  --data '{"text":"This is a line of text.\nAnd this is another one."}' \
  $URL
ok

Slack で確認します。

投稿できています。

2. AWS Secrets Manager の設定

管理コンソールからSlackのシークレットを登録します。

secret type に "Other type of secrets" を選択し、

  • Webhook URL
  • メッセージ投稿したい Slack チャンネル

を登録します。

名前・概要を設定します。

名前は「シークレット名/環境」の命名規則で "slack/dev" で登録します。

シークレット情報を定期的に更新する automatic rotation はやらないため "Disable automatic rotation" を選択します。

シークレットを登録すると、このシークレットを取得するクライアントのサンプルコードがあります。

実際にAWS Secrets Manager からシークレットを取得すると、以下のような JSON がかえってきます。

{
  "ARN": "arn:aws:secretsmanager:REGION:123456789012:secret:slack/dev-XXX",
  "Name": "slack/dev",
  "VersionId": "DUMMY",
  "SecretString": "{\"SLACK_HOOK_URL\":\"https://hooks.slack.com/services/DUMMY\",\"SLACK_CHANNEL\":\"#general\"}",
  "VersionStages": [
    "AWSCURRENT"
  ],
  "CreatedDate": "...",
  "ResponseMetadata": {
    "RequestId": "DUMMY",
    "HTTPStatusCode": 200,
    "HTTPHeaders": {
      "date": "Sat, 09 Jun 2018 15:10:52 GMT",
      "content-type": "application/x-amz-json-1.1",
      "content-length": "381",
      "connection": "keep-alive",
      "x-amzn-requestid": "DUMMY"
    },
    "RetryAttempts": 0
  }
}

3. Lambda Function の作成

最後に AWS Secrets Manager からシークレットを取得し、Slack にメッセージ投稿する Lambda 関数を作成します。

Lambda のロール

いつもの Lambda 向け権限の他に、AWS Secrets Manager からシークレットを取得するためのポリシーを追加します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "secretsmanager:GetSecretValue",
            "Resource": "*"
        }
    ]
}

Python3 プログラム

Python3 で Lambda 関数を作成します。

import boto3
import json
import logging
import os
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError

from botocore.exceptions import ClientError

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

def lambda_handler(event, context):
    logger.info("Event: " + str(event))
    message = json.loads(event['Records'][0]['Sns']['Message'])

    alarm_name = message['AlarmName']
    #old_state = message['OldStateValue']
    new_state = message['NewStateValue']
    reason = message['NewStateReason']

    # AWS Secrets Manager からシークレット取得
    try:
        client = boto3.client(service_name='secretsmanager')
        SecretId = os.environ["SecretId"]  # Lambda の環境変数から Secrets Manager のシークレットIDを取得
        get_secret_value_response = client.get_secret_value(SecretId=SecretId)
    except ClientError as e:
        raise e
    else:
        secret = json.loads(get_secret_value_response['SecretString'])
        SLACK_HOOK_URL = secret['SLACK_HOOK_URL']
        SLACK_CHANNEL = secret['SLACK_CHANNEL']

    slack_message = {
        'channel': SLACK_CHANNEL,
        'text': "%s state is now %s: %s" % (alarm_name, new_state, reason)
    }

    req = Request(SLACK_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)

ハイライトしている 22-33 行目がキモです。

本番・開発など環境ごとにコードを共有したいため、シークレットIDは Lambda の環境変数から取得します。 そのため、 SecretId = os.environ["SecretId"] としています。

ほしいシークレット情報はレスポンス(JSON形式)の "SecretString" キーで取得できます。 バリューは JSON を文字列でダンプしたものです。そのため、 json.loads で JSON に戻して処理します。

secret = json.loads(get_secret_value_response['SecretString'])
SLACK_HOOK_URL = secret['SLACK_HOOK_URL']
SLACK_CHANNEL = secret['SLACK_CHANNEL']

Lambda 関数の環境変数

シークレットIDを Lambda の環境変数から取得しているため、環境変数 "SecretId" に登録したシークレットID(今回は "slack/dev")を設定します。

4. 動作確認

最後に動作確認します。

本来であれば、CloudWatch Alarm、SNS などを構築すベきでしょうが、省力化のために、Lambda のテストイベントを利用して、擬似的にトリガーを発生させます。

Lambda テストイベントの登録

CloudWatch Alarm->

{
  "Records": [
    {
      "EventVersion": "1.0",
      "EventSubscriptionArn": "arn:aws:sns:EXAMPLE",
      "EventSource": "aws:sns",
      "Sns": {
        "SignatureVersion": "1",
        "Timestamp": "1970-01-01T00:00:00.000Z",
        "Signature": "EXAMPLE",
        "SigningCertUrl": "EXAMPLE",
        "MessageId": "95df01b4-ee98-5cb9-9903-4c221d41eb5e",
        "Message": "Hello from SNS!",
        "MessageAttributes": {
          "Test": {
            "Type": "String",
            "Value": "TestString"
          },
          "TestBinary": {
            "Type": "Binary",
            "Value": "TestBinary"
          }
        },
        "Type": "Notification",
        "UnsubscribeUrl": "EXAMPLE",
        "TopicArn": "arn:aws:sns:EXAMPLE",
        "Subject": "TestInvoke"
      }
    }
  ]
}

Lambda の実行

登録したテストイベントを利用して Lambda 関数を「Test」ボタンから実行します。

正常終了すると、Secrets Managerで登録した Webhook/Channel に対応するチャンネルに CloudWatch Alarm の通知がメッセージ送信されます。

KMS 方式と Secrets Manager 方式の違い

最後に KMS 方式と Secrets Manager 方式の違いを考えてみます。

シークレットのアクセス権限

KMS 方式の場合、管理者が KMS:encrypt でシークレットを暗号化します。 シークレットの利用者は KMS:decrypt 権限だけが与えられ、都度、暗号化されたシークレット(ciphertext)を復号して(plaintext)利用します。

Secrets Manager 方式の場合、管理者がシークレットを Secrets Manager に登録します。 シークレットの利用者は secretsmanager:GetSecretValue 権限だけが与えられ、都度、平文のシークレットを取得して利用します。

どちらのケースも、利用者には最小の権限だけを付与すれば運用できます。

シークレット変更時の対応

KMS 方式の場合、 暗号化されたシークレット は Lambda の環境変数で定義されているため、各 Lambda 関数の環境変数を更新する必要があります。

Secrets Manager 方式の場合、シークレットは一元管理され、各Lambda関数はシークレットへのポインター情報を持っているだけです。 そのため、Secrets Manager のシークレットを変更するだけですみます。

また、Secrets Manager の管理するシークレットはバージョン管理されます。クライアントによって、利用するシークレットのバージョンを変えることも可能です。

AWS Documentation » AWS Secrets Manager » User Guide » Getting Started with AWS Secrets Manager » Key Terms and Concepts for AWS Secrets Manager

さまざまなアプリケーションでシークレットを共有して運用ケースでは、Secrets Manager が向いていそうです。

最後に

Lambda 関数から Slack にメッセージ投稿するユースケースを例に、AWS KMS のかわりに AWS Secrets Manager でシークレット管理する方法を紹介しました。

AWS Secrets Manager でシークレットを一元管理し、AWS Secrets Manager からシークレット情報を取得する薄いラッパーを挟むことで、セキュアにシークレット管理しつつ、シークレットの変更にも柔軟に対応できるのではないでしょうか。