Pythonを一行一行、見ていく（Pythonでトークン取得・パラメータストア登録/更新・デプロイ後のテスト失敗〜成功まで）

どうも、こんにちは。お元気ですか？

ネットで昔の動画が配信されていると、
懐かしさで観てしまいますよね。
懐かしいというのは、それだけでエンターテイメントになるんですね。

[以前はServerless FrameworkやPythonについてなど
有識者の方々のを見たり、聞いたりして、教わりながら
色々と新しいことを体験しながら覚えていったので
それらの知見？ 感想？ をここに残しておきたいと思います。](https://qiita.com/han-aru/items/0f87c630147ecda5f629)

# まずは、handler.pyを一行一行、見ていこうと思います

こういう記述がありました、どういう中身なのでしょう。
Python（パイソン）と聞くと、ムキムキの黒人ボクサーを思い出してしまいますよね。
あの人って、海外版だとバルログっていう名前になってるんですよね。

```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の略で、慣例的にはプロジェクト内で使われる汎用的な関数などを定義したモジュールなどに対してつけられることの多い名前](https://teratail.com/questions/233152)だそうです
- この場合は `/layers/python/` の中に `util.py` が格納されています（util.pyの中身は割愛させていただきます）
- `util.py` は `handler.py` とは違う階層にあるのに、なぜファイルまでのパスを書かずに呼び出せているのでしょうか？
    - これは `AWS Lambda Layers` が関係しているようで、[AWS Lambda Layersを使用することで、ライブラリなどの共通コンテンツをレイヤーとして作成することで、パッケージにライブラリを含める必要がなくなりました](https://qiita.com/t_okkan/items/394a15577bd1aad46ec3)
    - [前回、わりとぼんやりと書いてしまったこれ `${self:custom.requirements_layer}`](https://qiita.com/han-aru/items/4fda3b471c0310edff1e#--selfcustomshared_layers)ですね
    - 共通したレイヤー（直訳で「層」「階層」）に、呼び出すライブラリが格納されていたわけですね
    - デプロイ（AWSに必要な設定やファイルをアップロード）する時って以外に時間がかかるので、こうやって細分化することで時短になるんですね
- [モジュール・パッケージ・ライブラリの違い](https://qiita.com/yutaro50/items/f93893a2d7b23cb05461)

### import app
- これは、下記に記述されている内容です。同階層にあります

### logger = get_logger('INFO')
- これは[ソフトウェアが実行されているときに起こったイベントを追跡するための手段](https://docs.python.org/ja/3/howto/logging.html)ということです
- このなかにある `INFO` とは `想定された通りのことが起こったことの確認` のようです

### @logging_decorator
- `@` とは `デコレータ（売り場内やショー・ケース、ショー・ウィンドウの装飾を担当する人のこと）` と言って、[すでにある関数に処理の追加や変更を行う為の機能](https://www.sejuku.net/blog/25130#:~:text=%E3%83%87%E3%82%B3%E3%83%AC%E3%83%BC%E3%82%BF%E3%81%A8%E3%81%AF%E3%80%81%E3%81%99%E3%81%A7%E3%81%AB%E3%81%82%E3%82%8B,%E5%87%BA%E6%9D%A5%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%AA%E3%82%8A%E3%81%BE%E3%81%99%E3%80%82)のようです
- [既存の関数の中身を直接変更することなく、それらに機能を追加したり変更したりすることが出来るように](https://www.sejuku.net/blog/25130#:~:text=%E3%83%87%E3%82%B3%E3%83%AC%E3%83%BC%E3%82%BF%E3%81%A8%E3%81%AF%E3%80%81%E3%81%99%E3%81%A7%E3%81%AB%E3%81%82%E3%82%8B,%E5%87%BA%E6%9D%A5%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB%E3%81%AA%E3%82%8A%E3%81%BE%E3%81%99%E3%80%82)なるそうです
- [関数の中の途中の処理を関数化してその関数を引数で指定することによって好きに変えることができるという点で便利です](https://teratail.com/questions/29223)ようです
    - リボンやハートの取外し可能なクリップ（@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()で関数を呼ぶ事が出来る](https://medium.com/lsc-psd/pythonic%E8%89%B2%E3%80%85-python%E3%81%AE%E3%82%A2%E3%83%B3%E3%83%80%E3%83%BC%E3%82%B9%E3%82%B3%E3%82%A2-%E3%82%92%E4%BD%BF%E3%81%84%E3%81%93%E3%81%AA%E3%81%9D%E3%81%86-3c132842eeef)そうです
    - [_single_leading_underscore: "内部でだけ使う" ことを示します。 たとえば from M import * は、アンダースコアで始まる名前のオブジェクトをimportしない](https://pep8-ja.readthedocs.io/ja/latest/#id42)ようです（ご指摘いただきました、ありがとうございます）

---

# 続いて、app.pyを一行一行、見ていこうと思います

こういう記述がありました。ServerlessFrameworkに使うPythonです。
あと結構伏せ字（xxx）にしたので、変数の流れが間違っていたらすみません。
処理内容は、`指定されたIDとパスワードを使用し、トークン情報（json形式）を指定されたところから取得して、
トークン情報のみを抽出し、パラメータストアに登録する` というものです。

```python: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
- [JSON `JavaScript Object Notationの略でデータフォーマットの一種` エンコーダおよびデコーダ](https://docs.python.org/ja/3/library/json.html)です

### import boto3
- ここで使われているbotoって[アマゾン川原産の淡水イルカにちなんで名付けられた](https://github.com/boto/boto3/issues/1023)そうです
- boto3は、[AWS SDK for Python (Boto3) を使用すると、AWS の使用を迅速に開始でき](https://aws.amazon.com/jp/sdk-for-python/)るそうです
- SDKとは、[少ない労力でアプリケーションを開発できるようにするために、プログラム、API、サンプルコードなどをパッケージにしたもの](https://techtarget.itmedia.co.jp/tt/news/1911/07/news06.html)だそうです


### import requests
- Requestsは、[Python の HTTP 通信ライブラリで、Webサイトの情報取得や画像の収集などを簡単に行うことができる](https://techacademy.jp/magazine/19195)そうです
- また、[Python には標準で urllib というライブラリが存在しますが、 Requests はそれよりもシンプルに、人が見て分かりやすくプログラムを記述することができる](https://techacademy.jp/magazine/19195)そうです。へー！ いいことづくしですね

### import os
- [主にファイルやディレクトリ操作が可能で、ファイルの一覧やpathを取得できたり、新規にファイル・ディレクトリを作成することができる](https://www.sejuku.net/blog/67787)そうです

### def _token(event):
- defは先ほど覚えましたね
- これは先ほどのhandler.pyが呼び出そうとしている関数にあたります
- eventが仮引数[関数を定義したときにカッコの中に入れる変数の呼び名](https://qiita.com/yuta-38/items/18e114df4a7bfe67db87)というものです
    
###     try:
- `try-except文` というのものがありまして、[「例外処理：文法的に正しいコードを書いても、実行時にエラーが発生することがあるときに処理」を行う](https://www.sejuku.net/blog/23044)そうです
- 実際には `try:` 記述のあとに「エラーハンドリング」として下記が追記されています（ここの中身はここでは割愛し、いつか…）

```python
# エラーハンドリング
    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ブロックに書いている処理が実行される](https://www.headboost.jp/python-try-except/#1_tryexcept)ということになるようです

###         client = boto3.client('ssm')
- ここの `boto3.client` というのは、[Client APIを使用する場合、最初に boto3.client('サービス名 = ssm：Amazon Simple Systems Manager') を呼び出す](https://dev.classmethod.jp/articles/boto3-client-api-and-resource-api/)
- boto3の公式ドキュメントに[A low-level client representing Amazon Simple Systems Manager (SSM):](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/ssm.html#client)とありました
- ちなみにここに出てくる `低レベルAPI` というのは、[低レベル→細かい事を指定できる(=細かい分岐も自分で制御する必要がある)、高レベル→やりたい事だけ指定すればいい(=細かい事を指定できない)、という意味で使われている](https://teratail.com/questions/190798)そうです

###         response_get = client.get_parameter(
- まずは事前にパラメーターストア画面に行って、あらかじめ頂いているキーを登録してきました
- ↓↓↓↓ここのオレンジのところですね

 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528622/f6a2e360-6641-11c8-97e4-a998e94d8a99.png)

- こういう画面で作成していきます（名前はわかりやすく、説明もわかりやすく、標準で安全な文字列でキーを値の欄に書き込みました）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528622/fd5195b9-eef7-524b-b5c1-3e4bc0682620.png)

- その後、登録したキー（JIjlksjdfaijklaIJ+IJ...こんな感じの文字列）をget_parameter関数で取ってきます


###             Name = '/xxx/xxxx/xxxx',
- 登録した名前をここに書きます
- ここに `/` が書いてあるのは、名前にあえて `/` を入れて、他に登録したキーなどと被らず、かつ、このキーはどこに所属しているものかを階層という表記にして、わかりやすくしています

###             WithDecryption = True
- ここは[WithDecryption=Trueにより複合化しパラメータストアに保存されているパラメータ名に対する値を返している](https://dev.classmethod.jp/articles/get-data-from-system-manager-parameter-store-using-boto3-ja/)ようです
- 登録したときに暗号化したので、ここで復号します[「復号」は操作そのものを指す言葉ですから、「復号化」とはしません。してはいけないのです](https://kei-sakaki.jp/2013/08/09/encryption-and-decryption/)

###         )
- カッコ閉じ

###         client_secret_get = response_get['Parameter']['Value']
- ['Parameter']['Value']とリストを分割するのはなぜでしょう？
    - 情報が少ないの（どういう単語で調べるんでしょう？）ですが、[['spam', 'ham']+['egg'] # リストの結合](https://pycamp.pycon.jp/textbook/4_collections.html#use-list)ということができるようです
    - 試していませんが `['Parameter', 'Value']` これでもいいと思っていましたが、ここでは `Parameterの中` の `Value` 部分を取得するという意味で `['Parameter']['Value']` とリストを分けているのかもしれません
    - 他にも `['Parameter'][0]['Value']` こういう記述を見つけたのですが、これは、[['spam', 'ham', 'egg'][0] # リストの0番目を取得する](https://pycamp.pycon.jp/textbook/4_collections.html)ということらしいです
    - [パラメータは階層的に編成されているため、辞書内の辞書のようにアクセスする方が私には合理的](https://medium.com/@nqbao/how-to-use-aws-ssm-parameter-store-easily-in-python-94fda04fea84)のようです
    - [section1のkeyの値を出力する](https://qiita.com/kikuchiTakuya/items/53990fca06fb9ba1d8a7#%E3%81%A8%E3%82%8A%E3%81%82%E3%81%88%E3%81%9A%E4%BD%95%E3%81%8B%E5%80%A4%E3%82%92%E5%8F%96%E3%82%8A%E5%87%BA%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B)などの場合はリストを分けるようです


###         CLIENT_SECRET = xxxxx_xxxx_get
- ここで、上で取得した、シークレットキーを `CLIENT_SECRET` という変数にします（文字なのに `数`）

###         CLIENT_ID = os.environ['XXXX']
- ここで最初にimportした `os` を使用します
- [os.environ()は環境変数を取得したり、書き込み・上書きするときに使われる](https://www.sejuku.net/blog/67787)そうです
- Lambda関数画面に、serverless.ymlでデプロイして書き込んでいる、環境変数に書かれたIDをここで取得しています（XXXXには実際には環境変数名が書いてあります）

###         XXXX_URL = "https://xxxxx/xxxx"
- トークンのある場所のURLを `XXXX_URL` という変数に入れました

###         xxxx_xxxx_key = {
             'xxxx_secret': XXXX_SECRET,
             'xxxx_id': XXXX_ID,
- ここではリストの辞書型を使い、キーをまとめて `xxxx_xxxx_key` という変数に入れます
- トークンを取得するために必要な、シークレットキーとIDを同じ場所に記述してはセキュリティ上よろしくない（ハードコーディングになる：[別の場所に分けて書いた方が良い処理や値をソースコードの中に直接書いちゃうことだよ](https://wa3.i-3-i.info/word12866.html)）ということで、シークレットキーはパラメータストアに、IDはLambdaの環境変数に分けて登録したので、それらを取得する記述が必要になっています

###         }
- }：波括弧（なみかっこ）、[現在では「中括弧」という呼び方よりも、「波括弧」の方が望ましいとされている](https://business-textbooks.com/type-of-parenthesis/#toc-7)ようです

###         response_xxxx = requests.post(XXXX_URL, data=xxxx_xxxx_key)
- [HTMLフォームのように、フォームでエンコードされたデータを送信します。これを行うには、単にdata引数に辞書を渡します。リクエストが行われると、データの辞書が自動的にフォームエンコードされる](https://requests.readthedocs.io/en/master/user/quickstart/#more-complicated-post-requests)ようです
- 辞書型にキーを複数入れて、それを `xxxx_xxxx_key` という変数にして、その変数を `data` の引数にして渡しています

###         xxxx_data = json.loads(response_xxxx.text)
- ここで実際にトークンを取得できるのですが、いわゆるJSON形式でした
- [json loadsとは文字列をデコード(扱いやすい型に変換)してくれる](https://www.sejuku.net/blog/78985)そうです
- なのでそれを見越してここでは `import json` の `json.loads` 関数を使って1行にして呼び出しています
- response_xxxxのあとに `.text` をつけるのはなぜでしょう？
    - [レスポンスに対して .text とすることで, レスポンスボディをテキスト形式で取得できる](https://qiita.com/sqrtxx/items/49beaa3795925e7de666)ようです
    - [レスポンスボディとは「相手が欲しがってたファイルの中身」が書かれている場所](https://wa3.i-3-i.info/word1848.html)のようです
- ちなみに驚いたのが[json.loadとjson.loadsは違います。json.dumpとjson.dumpsも違うもの](sejuku.net/blog/78985)なのです

###         xxxx_xxxx_data = xxxx_data.get('access_token')
- xxxx_dataの中から `access_token` と書かれた中身を取得します
- `.get` 部分ですが、[get()メソッドを使うと、キーが存在しない場合にエラーを発生させずに任意の値（デフォルト値）を取得できる](https://note.nkmk.me/python-dict-get/)ようです
- [key が辞書にあれば key に対する値を、そうでなければ default を返します。 default が与えられなかった場合、デフォルトでは None となります。そのため、このメソッドは KeyError を送出することはない](https://docs.python.org/ja/3/library/stdtypes.html#dict.get)そうです
    - なぜ `KeyError` は出てほしくないのでしょう？ エラーなので出てほしくはないんですが
        - [KeyErrorが出力されると強制的にプログラムが終了してしまう](https://tomoprog.hatenablog.com/entry/2016/05/18/222959)そうです、それは出てほしくないですね

###         response_put = client.put_parameter(
- 上の方でgetをした内容を今度はput（置く、貼る）をします
- 仕組みとしてはget_parameterと同じですね

###             Name = '/xxxx/xxxx',
- putしたいパラメータストアの名前です（/が入っていますが、URLではなくてこちらで決めたネーミングルールです）

###             Value = xxxx_xxxx_xxxx,
- わかりやすい説明をここで書きます

###             Type = 'SecureString',
- ここが面白くて `String` だとそのままの文字が送られますが、`SecureString` にすると `*****` となります
- セキュリティですね

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/528622/b5732e09-d84c-dc69-d384-58fe50d2f272.png)


###             Overwrite = True
- 今回は週一でトークンを書き直す処理を行っているので、上書きOKとしています

###         )
- ここは波括弧ではないのはなぜ？
    - [[ ] はリスト、( ) はタプル、{ } は辞書](https://teratail.com/questions/213790)
    - [リストを定義するには角括弧（[ ]）を使い、含める要素をカンマ（,）で区切ります `['spam', 'egg', 0.5]`](https://pycamp.pycon.jp/textbook/4_collections.html)
    - [タプルを定義するには括弧（( )）を使い、含める要素をカンマ（,）で区切りで並べます `('spam', 'ham', 4)`](https://pycamp.pycon.jp/textbook/4_collections.html)
        - [タプルはリストとほぼ同じように使用できますが、要素を変更できない点が異なる](http://www.tohoho-web.com/python/list.html)ようです
        - [タプルは、関数の戻り値や不変としたい設定用の値に使う](https://pycamp.pycon.jp/textbook/4_collections.html)ようです
    - [辞書{...} は、リストとは違い、各要素に順番を持ちません。代わりにキー（key）と、対応する値（value）を持つ](https://pycamp.pycon.jp/textbook/4_collections.html)ようです
        - [辞書を定義するには波括弧（{})で各要素を囲み、コロン（:）でキーと値を書きます。 値と次のキーの間はカンマ（,）で区切る `user_info = {'user_name': 'taro', 'last_name': 'Yamada'}`](https://pycamp.pycon.jp/textbook/4_collections.html)ようです
    - つまり、先ほどのは辞書形式で、ここはタプル形式（キーを必要とせず、変更不可能にしたい要素達）だから

---

## 終わりに `今回も長かった！`
本ページ内のリンクで引用、参照させていただいた、
公式ドキュメントや記事の筆者にお礼申し上げます。ありがとうございます。

さて、この段階に来て、次にこんなことも起こりました。
`何度やってもデプロイ後、Lambda画面でのテストに失敗した` んです！

## なぜLambda画面でのテストに失敗したのか、それは…

Lambda（つまりserverless.yml）で設定していたPythonのバージョンは3.8だったのですが、
ローカルで編集していたときのPython環境は3.7だったんです。
てっきりデプロイする（ローカルから離れる）タイミングで最新に切り替わると思っていたのですが、そうではなかった。
作業をしているローカル環境のPythonのバージョンを3.8にしてデプロイしたら正常に動きました。
みなさまもお気をつけくださいませ。