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

パラメータストアを利用すればLambda関数やaws-cliに秘密の情報を埋める必要がありません。サンプルとしてLambda関数を作成しましたのでご利用ください。
2018.04.16

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

はじめに

こんばんは、菅野です。 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