パラメータストアを使って安全なLambda関数を作成する

はじめに

こんばんは、菅野です。
Lambda関数を作っていると、外部に知られたくない文字列を使いたい事があります。
そんな時にパラメータストアを使うと、ソースに埋め込む必要があるのは「名前」だけで「秘密にしたい値」はプログラム側で一切保持しなくて済むようになります。
今回はパラメータストアに保存してあるAPIキーとRoomIDを使って、Chatworkへメッセージを送信する汎用的なLambda関数を作成してみました。

パラメータについて

今回必要なパラメータは以下の3つとなります。

  • ChatworkのURL
  • ChatworkのAPIキー
  • ChatworkのRoomID

この中で機密性の高いものはAPIキーとRoomIDの二つなので、これらは暗号化しておきます。
URLはソースに埋め込んでも問題無いのですが、他のLambda関数でも使えるのでパラメータ化しておきます。
そうすればURLが変わった時にパラメータだけ変更すれば全てのLambda関数の修正が完了します。

パラメータの準備

それではパラメータを作成します。
例として暗号化しておきたいAPIキーを作成してみましょう。名前を付けてタイプを「安全な文字列」にするだけです。

同様にRoomIDのパラメータも作成します。
ChatworkのURLは暗号化しなくてもいいので、タイプは「文字列」でOKです。

Lambda関数用のIAMポリシーとIAMロールの準備

今回作成するLambda関数に必要な権限はパラメータストアからのパラメータ取得です。
必要な権限は以下の二つとなります。

  • ssm:GetParameters
  • sts:AssumeRole

今回知ったのですが、AssumeRoleが必要なのです。

このポリシーを使う、Lambda関数用のIAMロールも作ります。

Lambda関数で使うライブラリの準備

今回のLambda関数はChatworkへhttpsでPOSTする必要があるので、requestsライブラリをzipで用意しておきます。

$ mkdir requests
$ cd requests
$ pip install requests -t .
$ zip -r requests.zip *

Lambda関数の作成

準備ができたので作成しましょう。

  • 「一から作成」を選択
  • 名前を付けます
  • ランタイムはPython3.6を選択
  • 先ほど作成したIAMロールを選択

  • コードエントリタイプは「.ZIPファイルをアップロード」を選択
  • 先ほど作成したrequests.zipをアップロード
  • 右上の「保存」ボタンをクリック


「lambda_function.py」ファイルが無いと注意されます。アップロードしたZIPファイルの中に含まれていないので当然です。

本来は「lambda_function.py」とライブラリをまとめてZIPファイルにしてアップロードするのですが、今回は「lambda_function.py」をマネジメントコンソール上で作成します。

  • 「File」をクリック
  • 「New File」をクリック
  • もう一度「File」をクリック
  • 「Save As」をクリック

ファイル保存ダイアログが表示されます

  • ファイル名に「lambda_function.py」と入力
  • 一番上のフォルダを選択
  • 「Save」ボタンをクリック

これで関数を作成できます。以下のコードをコピーして「lambda_function.py」に貼り付けて保存してください。

from __future__ import print_function
import json
import boto3
import requests


# 初期設定
ssm = boto3.client( 'ssm' )
chatwork_url_param_name = 'chatwork-url'
chatwork_room_param_name = ''
chatwork_key_param_name = ''


# メイン関数
def lambda_handler( event, context ):

    global chatwork_room_param_name
    global chatwork_key_param_name

    # 渡されたパラメータストアの名前を保存
    try:
        chatwork_room_param_name = event[ 'room' ]
        chatwork_key_param_name = event[ 'key' ]
    except KeyError as e:
        return {
            "error": "param_exists_error",
            "param_name": str( e ).replace( "'", '' )
        }

    # パラメータの名前から復号化したパラメータを取得
    ssm_response = ssm.get_parameters(
        Names = [
            chatwork_url_param_name,
            chatwork_room_param_name,
            chatwork_key_param_name
        ],
        WithDecryption = True
    )

    # パラメータを格納する配列を準備
    params = {}

    # 復号化したパラメータを配列に格納
    for param in ssm_response[ 'Parameters' ]:
        params[ param['Name'] ] = param['Value']

    # パラメータストアに存在しないパラメータ名を指定されていたら終了
    if len( ssm_response[ 'InvalidParameters' ] ) > 0:
        return {
            "error": "param_name_error",
            "param_name": ', '.join( ssm_response[ 'InvalidParameters' ] )
        }

    # chatwork へメッセージを送信
    return post_to_chatwork( event, params )


# chatwork へメッセージを送信
def post_to_chatwork( event, params ):

    # 送信する情報を準備
    post_params = { "body": event[ 'message' ] }
    token_header = { "X-ChatWorkToken": params[ chatwork_key_param_name ] }

    # chatwork へメッセージを送信
    try:
        https_response = requests.post(
            # 引数1:URL
            params[ chatwork_url_param_name ] + '/rooms/' + params[ chatwork_room_param_name ] + '/messages',
            # 引数2:POST パラメータ
            data=post_params,
            # 引数3:ヘッダ(key)
            headers=token_header
        )
        # 200 以外は例外を発生
        https_response.raise_for_status()
    except Exception as e:
        return {
            "error": "requests_post_error",
            "message": str( e )
        }

    # レスポンスを辞書型に変換
    https_response_dict = json.loads( https_response.text )

    # 終了
    return {
        "error": "",
        "message_id": https_response_dict[ 'message_id' ]
    }

パラメータを取得する部分の説明

今回、3つのパラメータをまとめて取得しています。

# パラメータの名前から復号化したパラメータを取得
ssm_response = ssm.get_parameters(
    Names = [
        chatwork_url_param_name,
        chatwork_room_param_name,
        chatwork_key_param_name
    ],
    WithDecryption = True
)
  • Namesというリストにパラメータ名をカンマ区切りで入れて渡しています。
  • APIキーとRoomIDは暗号化されていますので、復号化されたものを取得できるように「WithDecryption = True」を指定します。

テスト実行

今回作成したLambda関数はAPIキーとRoomIDのパラメータ名、Chatworkへ送る文字列をこの関数へ渡す事で「event」変数にそれらの値が入った状態で動作します。
右上の「テストイベントの選択」からテスト実行するのに必要なパラメータを作成しましょう。

「テストイベントの設定」をクリックすると以下のダイアログが表示されます。

  • 任意のイベント名を入力
  • パラメータに以下の内容を入力して下にある「作成」ボタンをクリック
{
  "room": "chatwork-room-test",
  "key": "chatwork-key-cm",
  "message": "aaa\nbbb"
}

右上で今作成したテストイベント「test」を選択して「テスト」ボタンをクリックしてみます。

Chatworkへのメッセージ送信が成功して、message_idが帰ってきました。

Chatworkでもメッセージが確認できます。

さいごに

今更感がありますが、パラメータストアを利用してみました。
今まではLambda関数ではなく、呼び出す方のプログラムに秘密の情報を埋めていましたが、パラメータストアを使えば呼び出し側もLambda関数側もどちらにも書かなくて済みますので安心してソースコードを管理できます。
皆さんもパラメータストアを活用して安全なプログラムを作成していきましょう!

参考ページ

以下のページを参考にしました。
ありがとうございました。
Boto 3 Docs 1.7.4 documentation
Systems Manager パラメータについて - AWS Systems Manager
【AWS】Lambdaでpipしたいと思ったときにすべきこと - Qiita
PythonモジュールRequestsのHTTPステータスコードについて
【Python入門】JSONをパースする方法 - Qiita