この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
AWS Summit 2018 San Francisco でシークレットを簡単にローテーション、管理、取得するAWS Secrets Manager が発表されました。
AWS Lambda のシークレットを管理する場合、従来は AWS KMS で暗号化し、実行時に復号する方法などが採用されました。
AWS Lambda のブループリント(CloudWatchアラームをAmazon SNS経由でSlackに通知する cloudwatch-alarm-to-slack など)もこのアプローチを採用しています。
本ブログでは、 このブループリントにおいて、AWS KMS で暗号・復号する代わりに、AWS Secrets Manager でシークレット管理する方法を紹介します。
作業手順
以下の流れで作業します。
- SlackのWebhookの準備
- AWS Secrets Manager の設定
- Lambda Function の作成
- 動作確認
1. SlackのWebhookの準備
Webhook URL の取得
次のページを参考にSlack のWebhook URL を取得してください。
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 の管理するシークレットはバージョン管理されます。クライアントによって、利用するシークレットのバージョンを変えることも可能です。
さまざまなアプリケーションでシークレットを共有して運用ケースでは、Secrets Manager が向いていそうです。
最後に
Lambda 関数から Slack にメッセージ投稿するユースケースを例に、AWS KMS のかわりに AWS Secrets Manager でシークレット管理する方法を紹介しました。
AWS Secrets Manager でシークレットを一元管理し、AWS Secrets Manager からシークレット情報を取得する薄いラッパーを挟むことで、セキュアにシークレット管理しつつ、シークレットの変更にも柔軟に対応できるのではないでしょうか。