⛳
Pythonを一行一行見ていく(Pythonでトークン取得・パラメータストア登録/更新・デプロイ後のテスト失敗〜成功まで
どうも、こんにちは。お元気ですか?
ネットで昔の動画が配信されていると、
懐かしさで観てしまいますよね。
懐かしいというのは、それだけでエンターテイメントになるんですね。
まずは、handler.pyを一行一行、見ていこうと思います
こういう記述がありました、どういう中身なのでしょう。
Python(パイソン)と聞くと、ムキムキの黒人ボクサーを思い出してしまいますよね。
あの人って、海外版だとバルログっていう名前になってるんですよね。
handler.py
from util import logging_decorator, build_response_decorator, get_logger
import app
logger = get_logger('INFO')
@logging_decorator
def xxxx_handler(event, context):
return app._token(event)
from util import logging_decorator, build_response_decorator, get_logger
- まずは、
util
という名前のモジュール(直訳で組み立てユニット:ここではutil.pyというものが事前に作られており)その中から、logging_decorator, build_response_decorator, get_logger
の3つの関数を使います、と宣言しています - utilというのはutilityの略で、慣例的にはプロジェクト内で使われる汎用的な関数などを定義したモジュールなどに対してつけられることの多い名前だそうです
- この場合は
/layers/python/
の中にutil.py
が格納されています(util.pyの中身は割愛させていただきます) -
util.py
はhandler.py
とは違う階層にあるのに、なぜファイルまでのパスを書かずに呼び出せているのでしょうか?- これは
AWS Lambda Layers
が関係しているようで、AWS Lambda Layersを使用することで、ライブラリなどの共通コンテンツをレイヤーとして作成することで、パッケージにライブラリを含める必要がなくなりました -
前回、わりとぼんやりと書いてしまったこれ
${self:custom.requirements_layer}
ですね - 共通したレイヤー(直訳で「層」「階層」)に、呼び出すライブラリが格納されていたわけですね
- デプロイ(AWSに必要な設定やファイルをアップロード)する時って以外に時間がかかるので、こうやって細分化することで時短になるんですね
- これは
- モジュール・パッケージ・ライブラリの違い
import app
- これは、下記に記述されている内容です。同階層にあります
logger = get_logger('INFO')
- これはソフトウェアが実行されているときに起こったイベントを追跡するための手段ということです
- このなかにある
INFO
とは想定された通りのことが起こったことの確認
のようです
@logging_decorator
-
@
とはデコレータ(売り場内やショー・ケース、ショー・ウィンドウの装飾を担当する人のこと)
と言って、すでにある関数に処理の追加や変更を行う為の機能のようです - 既存の関数の中身を直接変更することなく、それらに機能を追加したり変更したりすることが出来るようになるそうです
-
関数の中の途中の処理を関数化してその関数を引数で指定することによって好きに変えることができるという点で便利ですようです
- リボンやハートの取外し可能なクリップ(@decorator)を最初に作っておいて、ある服にリボンやハートの取外し可能なクリップ(@decorator)をつけることで、パッと見で女性向けの服とわからせることができる(普段は「これは女性向けの服です」と言わなければならない)ということでしょうか? 違いますか、ややこしくなりましたか、普段は「これは女性向けの服です」と言わなければならない服って、どんな服でしょうか
def xxxx_handler(event, context):
- defとは
英語のdefine(定義する)の略からきている
そうです(Defaultと間違えていました) - xxxx_handlerと、定義された関数の中で、event, contextという引数を呼び出しています
return app._token(event)
- return文は、エンターキーをターンと押すイメージです
- app.pyの中の_token関数でeventを引数にしてエンターキーをターン!(ちょっと調べ疲れました)
-
_token
これはなぜ最初に_
が付いているのでしょう- _function(x): #関数前に一つアンダースコアを付ける事により、関数を”内部用”に定義できます。他のプログラミング言語のPrivate属性と似たような物ですが、Pythonには事実上のPrivate属性がありません。PEP8の説明ではweak internal useと説明されていて、from M import * の時、では一つのアンダースコアで始まる関数はインポートされませんが、class内の関数だとclassx._func()で関数を呼ぶ事が出来るそうです
- _single_leading_underscore: "内部でだけ使う" ことを示します。 たとえば from M import * は、アンダースコアで始まる名前のオブジェクトをimportしないようです(ご指摘いただきました、ありがとうございます)
続いて、app.pyを一行一行、見ていこうと思います
こういう記述がありました。ServerlessFrameworkに使うPythonです。
あと結構伏せ字(xxx)にしたので、変数の流れが間違っていたらすみません。
処理内容は、指定されたIDとパスワードを使用し、トークン情報(json形式)を指定されたところから取得して、 トークン情報のみを抽出し、パラメータストアに登録する
というものです。
app.py
import json
import boto3
import requests
import os
def _token(event):
try:
# ===================================================================
# トークン取得処理
# ===================================================================
# boto3を使い、パラメータストアを利用する
client = boto3.client('ssm')
# シークレットキー取得
# get_parameterでパラメータの値を取得
response_get = client.get_parameter(
Name = '/xxx/xxxx/xxxx',
WithDecryption = True
)
client_secret_get = response_get['Parameter']['Value']
# トークン取得に必要なキー一覧
CLIENT_SECRET = xxxxx_xxxx_get
CLIENT_ID = os.environ['XXXX']
XXXX_URL = "https://xxxxx/xxxx"
# あの夏、ボクらはキーを手にしたんだ
xxxx_xxxx_key = {
'xxxx_secret': XXXX_SECRET,
'xxxx_id': XXXX_ID,
}
# トークン情報にアクセスし必要な情報を入力
response_xxxx = requests.post(XXXX_URL, data=xxxx_xxxx_key)
# トークン情報を出力
# 結果はJSON形式なのでデコードする
xxxx_data = json.loads(response_xxxx.text)
# トークン情報の中からアクセストークンの値のみ抽出
xxxx_xxxx_data = xxxx_data.get('access_token')
# ===================================================================
# 上で取得したトークン情報を - AWS Systems Manager - パラメータストアに送る
# ===================================================================
# put_parameterでパラメータの値を更新
response_put = client.put_parameter(
Name = '/xxxx/xxxx',
Value = xxxx_xxxx_xxxx,
Type = 'SecureString',
Overwrite = True
)
import json
import boto3
- ここで使われているbotoってアマゾン川原産の淡水イルカにちなんで名付けられたそうです
- boto3は、AWS SDK for Python (Boto3) を使用すると、AWS の使用を迅速に開始できるそうです
- SDKとは、少ない労力でアプリケーションを開発できるようにするために、プログラム、API、サンプルコードなどをパッケージにしたものだそうです
import requests
- Requestsは、Python の HTTP 通信ライブラリで、Webサイトの情報取得や画像の収集などを簡単に行うことができるそうです
- また、Python には標準で urllib というライブラリが存在しますが、 Requests はそれよりもシンプルに、人が見て分かりやすくプログラムを記述することができるそうです。へー! いいことづくしですね
import os
def _token(event):
- defは先ほど覚えましたね
- これは先ほどのhandler.pyが呼び出そうとしている関数にあたります
- eventが仮引数関数を定義したときにカッコの中に入れる変数の呼び名というものです
try:
-
try-except文
というのものがありまして、「例外処理:文法的に正しいコードを書いても、実行時にエラーが発生することがあるときに処理」を行うそうです - 実際には
try:
記述のあとに「エラーハンドリング」として下記が追記されています(ここの中身はここでは割愛し、いつか…)
# エラーハンドリング
except Exception as error:
data = {'request': event}
logger.exception(error, extra=dict(data))
return_params = {
'message': 'エラーが発生しました。もう一度操作を実行してください。 {}'.format(str(error))
}
return {
'statusCode': 500,
'body': return_params
}
- つまり、tryブロックに書いているコードで例外が発生したら、exceptブロックに書いている処理が実行されるということになるようです
client = boto3.client('ssm')
- ここの
boto3.client
というのは、Client APIを使用する場合、最初に boto3.client('サービス名 = ssm:Amazon Simple Systems Manager') を呼び出す - boto3の公式ドキュメントにA low-level client representing Amazon Simple Systems Manager (SSM):とありました
- ちなみにここに出てくる
低レベルAPI
というのは、低レベル→細かい事を指定できる(=細かい分岐も自分で制御する必要がある)、高レベル→やりたい事だけ指定すればいい(=細かい事を指定できない)、という意味で使われているそうです
response_get = client.get_parameter(
- まずは事前にパラメーターストア画面に行って、あらかじめ頂いているキーを登録してきました
- ↓↓↓↓ここのオレンジのところですね
- こういう画面で作成していきます(名前はわかりやすく、説明もわかりやすく、標準で安全な文字列でキーを値の欄に書き込みました)
- その後、登録したキー(JIjlksjdfaijklaIJ+IJ...こんな感じの文字列)をget_parameter関数で取ってきます
Name = '/xxx/xxxx/xxxx',
- 登録した名前をここに書きます
- ここに
/
が書いてあるのは、名前にあえて/
を入れて、他に登録したキーなどと被らず、かつ、このキーはどこに所属しているものかを階層という表記にして、わかりやすくしています
WithDecryption = True
- ここはWithDecryption=Trueにより複合化しパラメータストアに保存されているパラメータ名に対する値を返しているようです
- 登録したときに暗号化したので、ここで復号します「復号」は操作そのものを指す言葉ですから、「復号化」とはしません。してはいけないのです
)
- カッコ閉じ
client_secret_get = response_get['Parameter']['Value']
- ['Parameter']['Value']とリストを分割するのはなぜでしょう?
- 情報が少ないの(どういう単語で調べるんでしょう?)ですが、['spam', 'ham']+['egg'] # リストの結合ということができるようです
- 試していませんが
['Parameter', 'Value']
これでもいいと思っていましたが、ここではParameterの中
のValue
部分を取得するという意味で['Parameter']['Value']
とリストを分けているのかもしれません - 他にも
['Parameter'][0]['Value']
こういう記述を見つけたのですが、これは、['spam', 'ham', 'egg'][0] # リストの0番目を取得するということらしいです - パラメータは階層的に編成されているため、辞書内の辞書のようにアクセスする方が私には合理的のようです
- section1のkeyの値を出力するなどの場合はリストを分けるようです
CLIENT_SECRET = xxxxx_xxxx_get
- ここで、上で取得した、シークレットキーを
CLIENT_SECRET
という変数にします(文字なのに数
)
CLIENT_ID = os.environ['XXXX']
- ここで最初にimportした
os
を使用します - os.environ()は環境変数を取得したり、書き込み・上書きするときに使われるそうです
- Lambda関数画面に、serverless.ymlでデプロイして書き込んでいる、環境変数に書かれたIDをここで取得しています(XXXXには実際には環境変数名が書いてあります)
https://xxxxx/xxxx"
XXXX_URL = "- トークンのある場所のURLを
XXXX_URL
という変数に入れました
xxxx_xxxx_key = {
'xxxx_secret': XXXX_SECRET,
'xxxx_id': XXXX_ID,
- ここではリストの辞書型を使い、キーをまとめて
xxxx_xxxx_key
という変数に入れます - トークンを取得するために必要な、シークレットキーとIDを同じ場所に記述してはセキュリティ上よろしくない(ハードコーディングになる:別の場所に分けて書いた方が良い処理や値をソースコードの中に直接書いちゃうことだよ)ということで、シークレットキーはパラメータストアに、IDはLambdaの環境変数に分けて登録したので、それらを取得する記述が必要になっています
}
- }:波括弧(なみかっこ)、現在では「中括弧」という呼び方よりも、「波括弧」の方が望ましいとされているようです
response_xxxx = requests.post(XXXX_URL, data=xxxx_xxxx_key)
- HTMLフォームのように、フォームでエンコードされたデータを送信します。これを行うには、単にdata引数に辞書を渡します。リクエストが行われると、データの辞書が自動的にフォームエンコードされるようです
- 辞書型にキーを複数入れて、それを
xxxx_xxxx_key
という変数にして、その変数をdata
の引数にして渡しています
xxxx_data = json.loads(response_xxxx.text)
- ここで実際にトークンを取得できるのですが、いわゆるJSON形式でした
- json loadsとは文字列をデコード(扱いやすい型に変換)してくれるそうです
- なのでそれを見越してここでは
import json
のjson.loads
関数を使って1行にして呼び出しています - response_xxxxのあとに
.text
をつけるのはなぜでしょう? - ちなみに驚いたのがjson.loadとjson.loadsは違います。json.dumpとjson.dumpsも違うものなのです
xxxx_xxxx_data = xxxx_data.get('access_token')
- xxxx_dataの中から
access_token
と書かれた中身を取得します -
.get
部分ですが、get()メソッドを使うと、キーが存在しない場合にエラーを発生させずに任意の値(デフォルト値)を取得できるようです -
key が辞書にあれば key に対する値を、そうでなければ default を返します。 default が与えられなかった場合、デフォルトでは None となります。そのため、このメソッドは KeyError を送出することはないそうです
- なぜ
KeyError
は出てほしくないのでしょう? エラーなので出てほしくはないんですが- KeyErrorが出力されると強制的にプログラムが終了してしまうそうです、それは出てほしくないですね
- なぜ
response_put = client.put_parameter(
- 上の方でgetをした内容を今度はput(置く、貼る)をします
- 仕組みとしてはget_parameterと同じですね
Name = '/xxxx/xxxx',
- putしたいパラメータストアの名前です(/が入っていますが、URLではなくてこちらで決めたネーミングルールです)
Value = xxxx_xxxx_xxxx,
- わかりやすい説明をここで書きます
Type = 'SecureString',
- ここが面白くて
String
だとそのままの文字が送られますが、SecureString
にすると*****
となります - セキュリティですね
Overwrite = True
- 今回は週一でトークンを書き直す処理を行っているので、上書きOKとしています
)
- ここは波括弧ではないのはなぜ?
- [ ] はリスト、( ) はタプル、{ } は辞書
- リストを定義するには角括弧([ ])を使い、含める要素をカンマ(,)で区切ります
['spam', 'egg', 0.5]
-
タプルを定義するには括弧(( ))を使い、含める要素をカンマ(,)で区切りで並べます
('spam', 'ham', 4)
- 辞書{...} は、リストとは違い、各要素に順番を持ちません。代わりにキー(key)と、対応する値(value)を持つようです
- つまり、先ほどのは辞書形式で、ここはタプル形式(キーを必要とせず、変更不可能にしたい要素達)だから
今回も長かった!
終わりに 本ページ内のリンクで引用、参照させていただいた、
公式ドキュメントや記事の筆者にお礼申し上げます。ありがとうございます。
さて、この段階に来て、次にこんなことも起こりました。
何度やってもデプロイ後、Lambda画面でのテストに失敗した
んです!
なぜLambda画面でのテストに失敗したのか、それは…
Lambda(つまりserverless.yml)で設定していたPythonのバージョンは3.8だったのですが、
ローカルで編集していたときのPython環境は3.7だったんです。
てっきりデプロイする(ローカルから離れる)タイミングで最新に切り替わると思っていたのですが、そうではなかった。
作業をしているローカル環境のPythonのバージョンを3.8にしてデプロイしたら正常に動きました。
みなさまもお気をつけくださいませ。
Discussion