【AWS】Secrets Manager でクレデンシャルを管理する

qiita への投稿を取得して twitter に投稿するプログラムを作ったのですが、 ``access key`` や ``token`` の管理に[AWS Secrets Manager](https://aws.amazon.com/jp/secrets-manager/)を使用したので、シークレットキーの作成から python での実装までを記載します。

## シークレットキーの作成

AWS CLI で Secrets Manager で管理するキーと値のペアを登録してみます。キーと値のペアは json ファイルに記載して、 cli 実行時に指定するようにします。

```json:creds.json
{
    "CONSUMER_KEY": "xxx",
    "CONSUMER_SECRET": "xxx",
    "ACCESS_TOKEN": "xxx",
    "ACCESS_TOKEN_SECRET": "xxx",
    "QIITA_TOKEN": "xxx"
}
```

``create-secret`` コマンドで secret を作成します。

```
> aws secretsmanager create-secret --name bot-credential --secret-string file://creds.json
```

**出力:**

```json
{
    "ARN": "arn:aws:secretsmanager:ap-northeast-1:xxx:secret:bot-credential-xxx",
    "Name": "bot-credential",
    "VersionId": "xxx-xxx-xxx-xxx-xxx"
}
```

作成した内容を確認してみます。

```
> aws secretsmanager get-secret-value --secret-id bot-credential
```

**出力:**

```json
{
    "ARN": "arn:aws:secretsmanager:ap-northeast-1:xxx:secret:bot-credential-xxx",
    "Name": "bot-credential",
    "VersionId": "xxx-xxx-xxx-xxx-xxx",
    "SecretString": "{\n    \"CONSUMER_KEY\": \"xxx\",\n    \"CONSUMER_SECRET\": \"xxx\",\n    \"ACCESS_TOKEN\": \"xxx-xxx\",\n    \"ACCESS_TOKEN_SECRET\": \"xxx\",\n    \"QIITA_TOKEN\": \"xxx\"\n}",
    "VersionStages": [
        "AWSCURRENT"
    ],
    "CreatedDate": "2021-08-08T01:28:20.349000+09:00"
}
```

シークレットキーが作成されています。
コンソール画面からも確認できます。
![secret-key.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/780549/16651a0d-eb6f-19b8-b143-cbe0062dea62.png)


secret を作成すると、コンソール画面でサンプルソースコードを確認することができます。
![code-sample-python3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/780549/e487827e-69a8-fa4b-2e05-53998b814bb0.png)

今回はサンプルコードをそのまま利用します。

```python:secrets_manager.py
# Use this code snippet in your app.
# If you need more information about configurations or implementing the sample code, visit the AWS docs:   
# https://aws.amazon.com/developers/getting-started/python/

import boto3
import base64
from botocore.exceptions import ClientError


def get_secret():

    secret_name = "bot-credential"
    region_name = "ap-northeast-1"

    # Create a Secrets Manager client
    session = boto3.session.Session()
    client = session.client(
        service_name='secretsmanager',
        region_name=region_name
    )

    # In this sample we only handle the specific exceptions for the 'GetSecretValue' API.
    # See https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html
    # We rethrow the exception by default.

    try:
        get_secret_value_response = client.get_secret_value(
            SecretId=secret_name
        )
    except ClientError as e:
        if e.response['Error']['Code'] == 'DecryptionFailureException':
            # Secrets Manager can't decrypt the protected secret text using the provided KMS key.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise e
        elif e.response['Error']['Code'] == 'InternalServiceErrorException':
            # An error occurred on the server side.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise e
        elif e.response['Error']['Code'] == 'InvalidParameterException':
            # You provided an invalid value for a parameter.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise e
        elif e.response['Error']['Code'] == 'InvalidRequestException':
            # You provided a parameter value that is not valid for the current state of the resource.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise e
        elif e.response['Error']['Code'] == 'ResourceNotFoundException':
            # We can't find the resource that you asked for.
            # Deal with the exception here, and/or rethrow at your discretion.
            raise e
    else:
        # Decrypts secret using the associated KMS CMK.
        # Depending on whether the secret is a string or binary, one of these fields will be populated.
        if 'SecretString' in get_secret_value_response:
            secret = get_secret_value_response['SecretString']
        else:
            decoded_binary_secret = base64.b64decode(get_secret_value_response['SecretBinary'])
            
    # Your code goes here. 
    return secret
```

## 実装

qiita から記事を取得して twitter に投稿するプログラムは別記事に書きましたのそちらをご覧ください。

https://qiita.com/kiyo27/items/3db780252adcfd563a5f

環境変数で取得していた ``secret key`` や ``token`` を Secrets Manager から取得するように修正します。``secrets_manager.py`` の ``get_secret_value_response['SecretString']`` の値は辞書型では文字列型なので、文字列を辞書型に変換する ast モジュールの ``literal_eval`` を使用します。
https://dev.classmethod.jp/articles/secrets_manager_tips_get_api_key/

```diff_python:tweet.py
+import ast
 import tweepy
 import qiita
 import os

+import secrets_manager

-consumer_key = os.environ['CONSUMER_KEY']
-consumer_secret = os.environ['CONSUMER_SECRET']
-access_token = os.environ['ACCESS_TOKEN']
-access_token_secret = os.environ['ACCESS_TOKEN_SECRET']

+secret = ast.literal_eval(secrets_manager.get_secret())

+consumer_key = secret['CONSUMER_KEY']
+consumer_secret = secret['CONSUMER_SECRET']
+access_token = secret['ACCESS_TOKEN']
+access_token_secret = secret['ACCESS_TOKEN_SECRET']

 auth = tweepy.OAuthHandler(consumer_key, consumer_secret)
 auth.set_access_token(access_token, access_token_secret)

 api = tweepy.API(auth)
...
```