PythonでBase64エンコードとデコード

Base64というのは、データ(バイト列)を、64種類の文字(と、パディング用の”=”を含めた65文字)で表現するエンコード方法です。
64種類の文字の内訳はアルファベット小文字(a-z)26種類、大文字(A-Z)26種類、数値(0-9)10種類、記号(+,/)2種類です。
文字データしかやり取りのできないプロトコルで一般のデータを送受したりするために使われます。

詳しくはWikipediaをご参照ください。
参考: Base64 – Wikipedia

今回の記事は、このBase64のエンコードをPythonで実装する方法の紹介です。

Pythonには標準ライブラリに専用のモジュールが用意されています。
参考: base64 — Base16, Base32, Base64, Base85 データの符号化 — Python 3.9.4 ドキュメント

まず、文字列をbase64エンコーディングしてみましょう。
使い方は簡単で、base64.b64encodeに、バイトデータを渡してあげるだけです。
string型のデータはそのままでは受け取れない(エラー:a bytes-like object is required, not ‘str’ が発生する)ので、
元のテキストをencode()メソッドを使ってbyteデータに変換するのがポイントです。


import base64


text = "ハローワールド!"
print(base64.b64encode(text.encode()))
# b'44OP44Ot44O844Ov44O844Or44OJ77yB'

出力結果が b’〜’ となっているのでわかる通り、結果はbyte型で得られます。
base64の結果を文字列で欲しい時は、decode()する必要があります。


print(base64.b64encode(text.encode()).decode())
# 44OP44Ot44O844Ov44O844Or44OJ77yB

逆に、Base64のデータを、元のデータに戻したい場合は、base64.b64decode()を使います。
不思議なことに、このメソッドは、string型のデータもbyte型のデータも両方受け取ってくれます。
結果はbyte型で受け取ることになるので、元の文字列型のデータとして結果を得たい場合は、改めてdecode()する必要があります。


# byteを渡した場合
print(base64.b64decode(b'44OP44Ot44O844Ov44O844Or44OJ77yB'))
# b'\xe3\x83\x8f\xe3\x83\xad\xe3\x83\xbc\xe3\x83\xaf\xe3\x83\xbc\xe3\x83\xab\xe3\x83\x89\xef\xbc\x81'
print(base64.b64decode(b'44OP44Ot44O844Ov44O844Or44OJ77yB').decode())
# ハローワールド!

# stringを渡した場合
print(base64.b64decode('44OP44Ot44O844Ov44O844Or44OJ77yB'))
# b'\xe3\x83\x8f\xe3\x83\xad\xe3\x83\xbc\xe3\x83\xaf\xe3\x83\xbc\xe3\x83\xab\xe3\x83\x89\xef\xbc\x81'
print(base64.b64decode('44OP44Ot44O844Ov44O844Or44OJ77yB').decode())
# ハローワールド!

以上で、文字列データをBase64エンコード/デコードできました。
ただ、Base64の本領は、文字列ではない一般のデータを文字列で表現できることにあります。
なので、サンプルとして画像データをBase64エンコードするコードも紹介しておきます。
といっても、やることは単純で、画像をバイナリとして読み込んで、文字列のときと同じメソッドに渡すだけです。

結果はものすごく長いデータになるので出力しませんが、以下のようなコードで、img_base64変数に、
sample.jpg ファイルをBase64した結果が格納されます。


with open("./sample.jpg", "rb") as f:
    img = f.read()

img_base64 = base64.b64encode(img)

元々がbyte型なので文字列の時より単純ですね。
逆変換も文字列の時と同様にbase64.b64decodeでできます。
逆変換した結果を別のファイル名で保存して、元の画像と同じものであることを確認しておきましょう。(結果省略)


with open("./sample2.jpg", "wb") as f:
    f.write(base64.b64decode(img_base64))

EC2(Amazon Linux 2)に日本語フォント(IPAフォント)をインストールする

以前、MacにインストールしたやつのEC2版です。
参考: MacにIPAフォントをインストールする

Macではフォントファイルをダウンロードしてきて自分で配置する必要がありましたが、
EC2では、yumでインストールできます。

yumでインストール可能なものの一覧の中に、IPAフォントが含まれていることを見ておきましょう。


$ yum list | grep ipa- | grep fonts
ipa-gothic-fonts.noarch                003.03-5.amzn2                amzn2-core
ipa-mincho-fonts.noarch                003.03-5.amzn2                amzn2-core
ipa-pgothic-fonts.noarch               003.03-5.amzn2                amzn2-core
ipa-pmincho-fonts.noarch               003.03-5.amzn2                amzn2-core

一つ入れれば十分なのですが、せっかくなので4つともインストールしておきましょう。


sudo yum install ipa-gothic-fonts ipa-mincho-fonts ipa-pgothic-fonts ipa-pmincho-fonts

インストールしたフォントは、 /usr/share/fonts という場所に置かれるようです。


$ cd /usr/share/fonts
$ ls
dejavu  ipa-gothic  ipa-mincho  ipa-pgothic  ipa-pmincho

Linux では fc-list というコマンドで利用可能なフォントが一覧取得できるのですが、その中にも登場します。


$ fc-list | grep ipa
/usr/share/fonts/ipa-gothic/ipag.ttf: IPAGothic:style=Regular
/usr/share/fonts/ipa-mincho/ipam.ttf: IPAMincho:style=Regular
/usr/share/fonts/ipa-pmincho/ipamp.ttf: IPAPMincho:style=Regular
/usr/share/fonts/ipa-pgothic/ipagp.ttf: IPAPGothic:style=Regular

あとはこれを jupyter & matplotlib で使うにはMacの場合と同じように設定すればOKです。

参考: matplotlibのデフォルトのフォントを変更する

font.family :
に指定する文字列は、IPAGothic/IPAPGothic/IPAMincho/IPAPMincho
のいずれかです。

boto3でS3のバケットの操作

前回の記事でboto3を使ったファイル操作について紹介したので、ついでにバケットの操作についても紹介しておこうという記事です。
前回同様、公式ドキュメントはこちらです。
S3 — Boto3 Docs 1.17.84 documentation

バケット名の一覧取得

存在するバケットの一覧は以下のコードで取得できます。


import boto3


s3 = boto3.resource("s3")
for bucket in s3.buckets.all():
    print(bucket.name)

"""
blog-work-sample1
blog-work-sample2
  ~ 以下略 ~
"""

バケットの作成

新規バケットの作成は以下のコードで実行できます。
引数は必ず名前付き引数で渡す必要があります。また、CreateBucketConfigurationは省略できないようです。


s3.create_bucket(
    Bucket="blog-work-sample3",
    CreateBucketConfiguration={
        'LocationConstraint': 'ap-northeast-1'
    }
)
"""
s3.Bucket(name='blog-work-sample3')
"""

バケット名の名前空間は全てのユーザーで共有されており、しかも全てのリージョンでも共有されています。
そのため、誰かが既に作っているバケット名を指定しているとエラーになるので注意が必要です。
以下のようなエラーが出ます。


try:
    s3.create_bucket(
        Bucket="sample",
        CreateBucketConfiguration={
            'LocationConstraint': 'ap-northeast-1'
        }
    )
except Exception as e:
    print(e)
"""
An error occurred (BucketAlreadyExists) when calling the CreateBucket operation:
The requested bucket name is not available. The bucket namespace is shared by all users of the system.
Please select a different name and try again.
"""

バケットの削除

中身が空のバケットであれば、単純にdelete()メソッドを呼び出すだけで消えます。
ただし、中にオブジェクトがある場合はエラーになるので、先にそれらを消しておく必要があります。


bucket = s3.Bucket("blog-work-sample3")
bucket.delete()

boto3でS3のファイル操作

boto3を使ってAWS S3のファイルを操作する方法をあれこれまとめておきます。
公式ドキュメントはこちらです。
S3 — Boto3 Docs 1.17.84 documentation
この記事ではリソースAPIの方を使います。

準備

とりあえず、この記事のために次の名前でS3にバケットを作っておきました。
– blog-work-sample1
– blog-work-sample2

さらに、 blog-work-sample1 の方には、
– sample-folder1
というフォルダーを掘っておきます。

また、サンプルとしてアップロードするファイルが必要なのでローカルに作っておきます。


!echo Hello S3! > samplefile.txt

ファイルのアップロード

まずはファイルのアップロードです。
アップロードしたいバケットを、 s3.Bucket(“バケット名”) で取得し、
upload_file(“アップロードしたいファイルのパス”, “アップロード先のファイルのパス”)でアップロードできます。
次のサンプルコードではアップロード時にファイル名を変更していいますがもちろんローカルのファイルと同じままのファイル名でも大丈夫です。


import boto3


s3 = boto3.resource("s3")
bucket = s3.Bucket("blog-work-sample1")
# バケットの直下に samplefile1.txt という名前でアップロードする場合
bucket.upload_file("samplefile.txt", "samplefile1.txt")
# フォルダ配下にアップロードする場合
bucket.upload_file("samplefile.txt", "sample-folder1/samplefile2.txt")

S3内でのファイルのコピー

ファイルのコピーには、 copy()というメソッドを使うのですが少しクセのある使い方をします。
どういうことかというと
{コピー先のバケットオブジェクト}.copy({コピー元の情報を辞書型で指定}, “コピー先のパス”)
という使い方をするのです。

まず、同じバケット内でコピーしてみます。


bucket = s3.Bucket("blog-work-sample1")  # コピー先のバケット

# 元のファイルを辞書型で指定
copy_source = {
    'Bucket': 'blog-work-sample1',
    'Key': 'samplefile1.txt'
}
bucket.copy(copy_source, "samplefile3.txt")

そして、別のバケットにコピーする時はこうです。copyメソッドの引数ではなく、元のバケットオブジェクトの取得が変わっているのがポイントです。


bucket = s3.Bucket("blog-work-sample2")  # コピー先のバケット

# 元のファイルを辞書型で指定
copy_source = {
    'Bucket': 'blog-work-sample1',
    'Key': 'samplefile1.txt'
}
bucket.copy(copy_source, "samplefile4.txt")

バケット内のオブジェクトの一覧を取得

バケット内のオブジェクトの一覧を取得するには、
bucketが持っている、objectsというプロパティの、all()メソッドでイテレーターとして取得します。
取得したオブジェクトのkey(S3において、ファイルパスやフォルダパスに相当する概念)を表示するコードが次です。


bucket = s3.Bucket("blog-work-sample1")
for obj in bucket.objects.all():
    print(obj.key)

"""
sample-folder1/
sample-folder1/samplefile2.txt
samplefile1.txt
samplefile3.txt
"""

フォルダとファイルを分けて表示したい場合は、Keyの末尾が/で終わっているかどうかで区別するしかないようです。
そもそもS3においてはフォルダという概念が存在せず、全てKey(パスのような概念)と値(ファイルの中身に相当する概念)で管理されているためこうなっているようです。
ただし、AWSの管理コンソールでは気を利かせてくれて、フォルダっぽく表示してくれています。

ファイルのダウンロード

ファイルのダウンロードには、 download_file(“ダウンロードしたいファイルのキー”, “ローカルに保存するパス”)
というメソッドを使います。

したのコードで、 blog-work-sample1/samplefile3.txt がダウンロードされ、
ローカルに、samplefile5.txt という名前で保存されます。


bucket = s3.Bucket("blog-work-sample1")
bucket.download_file("samplefile3.txt", "samplefile5.txt")

ファイルの削除

最後にファイルの削除です。
これには、delete_objectsというメソッドを使います。
一見、{バケットオブジェクト}.delete_objects{“消したいファイルのキー”} で消せそうな気がしますが、なぜかこれはかなり特殊な形の引数で渡す必要があります。

blog-work-sample1/samplefile1.txt
を消したい場合の使い方は以下の通りです。
必ず名前付き引数(Delete=)で、サンプルのような辞書を渡す必要があります。


bucket = s3.Bucket("blog-work-sample1")
bucket.delete_objects(Delete={"Objects": [{"Key": "samplefile1.txt"}]})

以上で、S3へのファイルのアップロード、コピー、リストアップ、ダウンロード、削除ができるようになりました。

複数の確率変数の最大値が従う分布について

確率密度関数が$f(x)$の同一の確率分布に従う$n$個の確率変数$X_1, \dots, X_n$について、これらの最大値が従う分布を考える機会がありました。
初めは少々苦戦したのですが、綺麗に定式化できたので記録として残しておこうと思います。
元々は最大値が従う確率密度関数を直接求めようとしてちまちまと場合分けなど考えていたのですが、
確率密度関数ではなく、累積分布関数を先に求めて、それを微分して確率密度関数を得るようにするとスムーズに算出できました。

最初に記号を導入しておきます。
まず、$X_i$たちが従う確率分布の分布関数を$F(x)$とします。
そして、$Y=\max(X_1, \dots, X_n)$が従う確率分布の確率密度関数を$g(y)$,累積分布関数を$G(y)$とします。

最終的に知りたいのは$g(y)$なのですが、まず$G(y)$の方を算出していきます。
$$
\begin{align}
G(y) &= \text{Yがy以下になる確率}\\
&= X_1, \cdots, X_n \text{が全てy以下になる確率}\\
&= (X_1\text{がy以下になる確率}) \times \cdots \times (X_n\text{がy以下になる確率})\\
&= F(y)^n
\end{align}
$$

こうして、最大値$Y$の累積分布関数が$F(y)^n$であることがわかりました。
確率密度関数は累積分布関数を1回微分することで得られるので次のようになります。
$$
\begin{align}
g(y) &= \frac{d}{dy}G(y)\\
&= \frac{d}{dy}F(y)^n\\
\therefore g(y) &= nF(y)^{n-1}f(y)
\end{align}
$$

ついでに、最小値$Z=\min(X_1, \dots, X_n)$が従う分布の確率密度関数$h(z)$と累積分布関数$H(z)$についても同様に算出できるのでやっておきます。
最大値の場合と同じように$H(z)$の方を求めます。
$$
\begin{align}
H(z) &= \text{Zがz以下になる確率}\\
&= 1-(\text{Zがz以上になる確率})\\
&= 1-(X_1, \cdots, X_n \text{が全てz以上になる確率})\\
&= 1-(X_1\text{がz以上になる確率}) \times \cdots \times (X_n\text{z以上になる確率})\\
&= 1-(1-(X_1\text{がz以下になる確率})) \times \cdots \times (1-(X_n\text{z以下になる確率}))\\
&= 1-(1-F(z))^n
\end{align}
$$
これで、最小値が従う分布の累積分布関数が求まりました。あとはこれを微分して、確率密度関数にします。
$$
\begin{align}
h(z) &= \frac{d}{dz}H(z)\\
&= -n(1-F(z))^{n-1}(-F'(z))\\
\therefore h(z) &= n\{1-F(z)\}^{n-1}f(z)
\end{align}
$$
最大値より若干複雑に見えますが、これで最小値が従う分布も得られました。

boto3のclient API とresource APIについて

boto3を使ったソースコードを読んでいると、
boto3.client(“サービス名”) と使っているものと、boto3.resource(“サービス名”) と使っているものがあり、
自分でも無意識に使い分けていたことに気づいたのでこれらの違いについて調べてみました。

ドキュメントを探すと、次のページに書いてありました。
参照: AWS SDK for Python | AWS

Boto3 には、2 つの異なるレベルの API があります。クライアント(「低レベル」)API では、下層の HTTP API 操作との 1 対 1 のマッピングが提供されます。 リソース API では、明示的なネットワーク呼び出しが表示されず、属性にアクセスしアクションを実行するためのリソースオブジェクトとコレクションが提供されます。

ちょっとわかりにくいですね。実際に動かしてみた違いから考えると、
クライアント API(boto3.clientの方)は、AWSの各リソースが持っているREST APIと1対1に対応した単純なPythonラッパーのようです。
それに対して、リソースAPI(boto3.resourceの方)はAWSのリソースをオブジェクト指向のプログラムで操作できるようにしたもののようです。

実際に動かしてみましょう。
EC2のインスタンスのインスタンスIDとインスタンスタイプ、現在の動作状況を一覧取得するプログラムをそれぞれ書いてみます。

まず、クライアントAPIの方です。
インスタンスの一覧は、describe_instances()で取得できます。
まず、単純に結果を表示すると、結果が辞書型で得られていることが確認できます。 (すごく長いので省略しています)


import boto3


ec2_client = boto3.client("ec2")
result = ec2_client.describe_instances()
print(result)

# 以下出力
{
    'Reservations': [
        {
            'Groups': [],
            'Instances': [
                {
                    'AmiLaunchIndex': 0,
                    'ImageId': 'ami-da9e2cbc',
                    'InstanceId': '{1つ目のインスタンスID}',
                    'InstanceType': 't1.micro',
                    'KeyName': '{キーファイルの名前}',
                    'LaunchTime': datetime.datetime(2021, 3, 20, 5, 35, 47, tzinfo=tzutc()),
                    'Monitoring': {'State': 'enabled'},
                    'Placement': {'AvailabilityZone': 'ap-northeast-1a',
                    'GroupName': '',
                    'Tenancy': 'default'
                },
    # 中略
    'ResponseMetadata': {
        'RequestId': '1b6c171d-d199-46c0-b2a6-7037fcfda28b',
        'HTTPStatusCode': 200,
        'HTTPHeaders': {
            'x-amzn-requestid': '1b6c171d-d199-46c0-b2a6-7037fcfda28b',
            'cache-control': 'no-cache, no-store',
            'strict-transport-security': 'max-age=31536000; includeSubDomains',
            'content-type': 'text/xml;charset=UTF-8',
            'transfer-encoding': 'chunked',
            'vary': 'accept-encoding',
            'date': 'Sun, 23 May 2021 08:21:51 GMT',
            'server': 'AmazonEC2'
        },
        'RetryAttempts': 0
    }
}

この巨大な辞書ファイルの中から必要な情報を取り出します。


for r in result["Reservations"]:
    print(r["Instances"][0]["InstanceId"])
    print(r["Instances"][0]["InstanceType"])
    print(r["Instances"][0]["State"])
    print(" - ・"*10)
"""
{1つ目のインスタンスID}
t1.micro
{'Code': 80, 'Name': 'stopped'}
 - ・ - ・ - ・ - ・ - ・ - ・ - ・ - ・ - ・ - ・
{2つ目のインスタンスID}
t2.nano
{'Code': 16, 'Name': 'running'}
 - ・ - ・ - ・ - ・ - ・ - ・ - ・ - ・ - ・ - ・
"""

上のコードを見ていただければわかる通り、単純な辞書の巨大な塊なのでちょっと情報が取り出しにくいのがわかると思います。

一方で、リソースAPIではどうでしょうか。
こちらはインスタンスの一覧は、ec2_resource.instances.all()でそれらのイテレーターが取得できます。
そして、インスタンスIDやインスタンスタイプ、状態などは属性として取得できます。
実際に先程のクライアントAPIと同様の情報を取得してみましょう。


ec2_resource = boto3.resource("ec2")
for instance in ec2_resource.instances.all():
    print(instance.instance_id)
    print(instance.instance_type)
    print(instance.public_ip_address)
    print(instance.state)
    print(" - ・"*10)
"""
{1つ目のインスタンスID}
t1.micro
None
{'Code': 80, 'Name': 'stopped'}
 - ・ - ・ - ・ - ・ - ・ - ・ - ・ - ・ - ・ - ・
{2つ目のインスタンスID}
t2.nano
54.199.43.209
{'Code': 16, 'Name': 'running'}
 - ・ - ・ - ・ - ・ - ・ - ・ - ・ - ・ - ・ - ・
"""

得られる結果は同じですが、コードがずいぶんわかりやすいのが実感していただけると思います。

さて、結論としてどっちを使えばいいのか、という話ですが、
実現したい操作がリソースAPIでできるのであればリソースAPIを使えばいいのではないかなと思いました。

ただ、提供されているREST APIと1対1に対応しているクライアントAPIと違って、
リソースAPIは全ての操作が実現できると保証されているものではありません。

たとえば、翻訳サービスである、Translateなどは、クライアントAPIしか存在せず、リソースAPIで使おうとするとエラーになります。


try:
    boto3.resource("translate")
except Exception as e:
    print(e)
"""
The 'translate' resource does not exist.
The available resources are:
   - cloudformation
   - cloudwatch
   - dynamodb
   - ec2
   - glacier
   - iam
   - opsworks
   - s3
   - sns
   - sqs

Consider using a boto3.client('translate') instead of a resource for 'translate'
"""

boto3.client(‘translate’) 使えって言われてますね。
このように、リソースAPIが未対応の時は、諦めてクライアントAPIを使いましょう。

LightsailのWordPressにads.txtを設置する

Googleアドセンスの管理画面に入ると、

要注意 – 収益に重大な影響が出ないよう、ads.txt ファイルの問題を修正してください。

という警告が出続けているので、対応することにしました。

ads.txtについての説明は以下のページなどをご参照ください。
広告枠の管理 ads.txt に関するガイド
Ads.Txt – Authorized Digital Sellers

さて、早速作業していきます。
まず、配置するads.txtファイルを入手します。

これは、Googleアドセンスの警告の右側に表示されている「今すぐ修正」をクリックすると、
「ダウンロード」できるようになります。

ダウンロードしたファイルをサイトのルートディレクトリ(トップレベル ドメイン直下のディレクトリ)に配置します。

LightsailのWordpressの場合、
/home/bitnami/apps/wordpress/htdocs/ads.txt
に配置すればOKです。
scpか何かでアップロードしても良いでしょうし、たった1行なので内容をコピーして貼り付けても良いでしょう。

htts://{サイトのドメイン}/ads.txt
にアクセスして、ads.txtファイルの内容が表示されたら成功です。

クローラーが検知してくれるのを気長に待ちましょう。
クローリングしてくれたらGoogleアドセンス管理画面の警告も消えるはずです。

サービスとして動かしているjupyter notebookに環境変数を設定する

jupyterをサービスとして動かしていると、 .bash_profile で設定した環境変数を読み込んでくれなかったので、その対応のメモです。

ちなみに、今回設定したい環境変数はAWSのデフォルトリージョンで、値としては、
AWS_DEFAULT_REGION=ap-northeast-1
です。
方法としては、Unit ファイルに直接書き込んで設定する方法と、
環境変数をまとめた設定ファイルを作成し、そのファイルパスをUnitファイルに指定する方法があります。

直接書き込む場合は、 Unitファイルの [Service] のセクションに、
Environment=”環境変数名=値”
で指定します。
複数指定したい場合は、
Environment=”環境変数名1=値1″
Environment=”環境変数名2=値2″
と2行に分けて書くか、
Environment=”環境変数名1=値1″ “環境変数名2=値2”
のように空白で区切って指定すれば良いようです (値にスペースがない場合はダブルクオーテーションは省略可能)
ドキュメントはここ。
systemd.exec Environment=

ちょっと試してみましょう。


sudo vim /etc/systemd/system/jupyter.service
ファイル中にEnvironment= の2行を追加

[Unit]
Description=Jupyter Notebook

[Service]
ExecStart=/home/ec2-user/.pyenv/shims/jupyter notebook
Restart=always
User=ec2-user
Group=ec2-user
Environment="VAR1=word1 word2"
Environment=VAR2=word3 "VAR3=$word 5 6"

[Install]
WantedBy=multi-user.target

# 上記ファイルを保存
# サービスを再起動
sudo systemctl daemon-reload
sudo systemctl restart jupyter

jupyter notebookで認識できているか確認します。
!をつけるとOSコマンドが実行できるのでそれを使います。


!set | grep VAR
# 以下出力
BASH_EXECUTION_STRING='set | grep VAR'
VAR1='word1 word2'
VAR2=word3
VAR3='$word 5 6'

設定されていますね。

ということで、今回の要件だけ考えれば
Environment=AWS_DEFAULT_REGION=ap-northeast-1
と直書きしてしまって良さそうです。

ただ、将来的に設定したい環境変数が増えていくことも考えられますので、その時Unitファイルが煩雑にならないように、
今の段階でもう一個の方法の、環境変数の設定ファイルを作る方法を使うことにしました。

ドキュメントはこちらです。
systemd.exec EnvironmentFile=

環境変数が入力されたファイル自体は、
/etc/sysconfig/サービス名
に作成するのがお作法らしいです。
そして、作成したファイルのパスを EnvironmentFile= に指定します。


$ sudo vim /etc/sysconfig/jupyter

# 以下の内容を記入して保存
AWS_DEFAULT_REGION=ap-northeast-1

$ sudo vim /etc/systemd/system/jupyter.service
# EnvironmentFile= の行を追加

[Unit]
Description=Jupyter Notebook

[Service]
EnvironmentFile=/etc/sysconfig/jupyter
ExecStart=/home/ec2-user/.pyenv/shims/jupyter notebook
Restart=always
User=ec2-user
Group=ec2-user

[Install]
WantedBy=multi-user.target

# 上記ファイルを保存
# サービスを再起動
sudo systemctl daemon-reload
sudo systemctl restart jupyter

設定されたことを確認してみましょう。


!set | grep VAR
# 以下出力
AWS_DEFAULT_REGION=ap-northeast-1
BASH_EXECUTION_STRING='set | grep AWS'

ちゃんと設定されましたね。
これで boto3 を使うときに、
region_name=”ap-northeast-1
をいちいち指定しなくて良くなりました。

DBのエンドポイントや接続情報など環境変数に入れておきたい内容はこの調子で、
/etc/sysconfig/jupyter
に突っ込んでいきましょう。

失敗しやすい処理にリトライをスクラッチで実装する

とあるSDKを使って実装している処理で、利用しているAPIのエラーが頻発するようになり、
エラーが起きたらリトライする処理をスクラッチで作る必要があったのでその時の実装をメモしておきます。

今回は利用しているSDKのメソッドにリトライ処理が内包されておらず、
気軽にライブラリを追加できる環境でもなかったのでスクラッチで実装しましたが、
通常は他の手段がないか探すことをお勧めします。

例えば、requestsのようなライブラリは引数でリトライ回数を指定できますし、
世の中にはリトライを行う専用のライブラリなども転がっています。

この記事のコードはそれらが使えなかった場合の最後の手段です。

実際の処理はお見せできないので、サンプルとして一定確率(80%)エラーになる関数を作っておきます。


import numpy as np


def main_function():
    num = int(np.random.choice([0, 1], p=[0.8, 0.2]))
    return 1/num

さて、これを成功するまでリトライする関数を作ります。
実装するにあたって定めた要件は以下の通りです。

– 規定回数リトライする。(今回は5回と定めました。)
– 初回の実行と合わせて、実際に実行を試みるのは、{1+リトライ回数}回。
– 発生したエラーは都度表示する。
– 規定回数全てエラーになったら、最後に発生したエラーを呼び出し元に返す。
– エラーから次のリトライまでは、1秒,2秒,4秒,8秒,…と間隔をあける。

書いてみたのが次のコードです。


from time import sleep


max_retry_count = 5  # リトライ回数

for retry_count in range(max_retry_count+1):
    try:
        print(main_function())  # 実行したい処理
        break  # 成功したらループを抜ける
    except Exception as e:
        print(e)
        if retry_count == max_retry_count:
            print(f"規定回数({max_retry_count}回)のリトライに失敗しました。")
            raise
        # {2^リトライ回数}秒待ちを入れる
        dely = 2**retry_count
        print(f"{dely}秒後にリトライします。{retry_count+1}/{max_retry_count}回目。")
        sleep(dely)

2回のリトライ(3回目の試行)で成功すると出力は以下のようになります。


division by zero
1秒後にリトライします。1/5回目。
division by zero
2秒後にリトライします。2/5回目。
1.0

最後まで失敗すると以下のようになります。


division by zero
1秒後にリトライします。1/5回目。
division by zero
2秒後にリトライします。2/5回目。
division by zero
4秒後にリトライします。3/5回目。
division by zero
8秒後にリトライします。4/5回目。
division by zero
16秒後にリトライします。5/5回目。
division by zero
規定回数(5回)のリトライに失敗しました。
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
 in 
      7 for retry_count in range(max_retry_count+1):
      8     try:
----> 9         print(main_function())  # 実行したい処理
     10         break  # 成功したらループを抜ける
     11     except Exception as e:

 in main_function()
      3 def main_function():
      4     num = int(np.random.choice([0, 1], p=[0.8, 0.2]))
----> 5     return 1/num

ZeroDivisionError: division by zero

ちゃんとエラーになりましたね。

EC2にIAMロールを設定する

EC2に構築したJupyterサーバーでboto3を使うとき、ローカルのMacbookと同じようにIAMユーザーのアクセスキーを、
環境変数 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY に設定して使うものだと思っていたのですが、
実はEC2にメタデータとしてIAMロールを設定できることを知ったのでそのメモです。

boto3のドキュメントを見ると、認証情報として以下の8種類が使える(上のものほど優先される)ことがわかります。
参考: Configuring credentials

1. boto.client()メソッドにパラメーターとして渡された認証情報
2. セッションオブジェクトを作成するときににパラメータとして渡された認証情報
3. 環境変数
4. 共有認証情報ファイル(~/.aws/credentials)
5. AWS設定ファイル(~/.aws /config)
6. ロールの引き受けの提供
7. Boto2設定ファイル(/etc/boto.cfg と 〜/.boto)
8. IAMロールが設定されているAmazonEC2インスタンスでは、インスタンスメタデータサービス

(6. とかよく意味がわからない。。。)

この中の、8. のものを使ってみようという話です。

設定したら設定できたことを確認したいので、とりあえず以下の記事で紹介した翻訳サービスでも使ってみましょう。
参考: Amazon Translate を試してみた

まず、何も設定していないと、認証情報がないって趣旨のエラーが出ることを確認しておきます。


import boto3

text = """
メロスは激怒した。
必ず、かの邪智暴虐の王を除かなければならぬと決意した。
"""

client = boto3.client("translate", region_name="ap-northeast-1")

result = client.translate_text(
    Text=text,
    SourceLanguageCode="ja",
    TargetLanguageCode="en",
)

# NoCredentialsError: Unable to locate credentials

ちなみに、 region_name=”ap-northeast-1″ を指定しないと、
NoRegionError: You must specify a region.
が出ます。

さて、本題の IAMの設定に移ります。
まずはEC2に付与するIAMロールを作成します。IAMユーザーではないので注意が必要です。
この辺、正確に理解できてるわけではないのですが、人に権限を付与するのがIAMユーザーで、AWSのリソースに権限を付与するのがIAMロールのようです。(超雑な説明)

1. AWSの管理コンソールのIAMのページの、左ペインでロールを選択します。
https://console.aws.amazon.com/iam/home#/roles
2. ロールの作成、をクリックします。
3. 信頼されたエンティティの種類を選択で、AWSサービスを選択します。 (他にも選択肢があるってことは、AWSサービス以外にもロールを付与できるはずなのですが使ったことがありません。)
4. ユースケースの選択から EC2 を選択します。
5. 次のステップ:アクセス権限 ボタンをクリックします。
6. Attach アクセス権限ポリシー で必要な権限を選択します。 (今回の例では、 TranslateFullAccess を選びます。)
7. 次のステップ:タグ をクリック。
8. 次のステップ:確認 をクリック。
9. ロール名と説明を入力 (例: ec2-jupyter)。
10. ロールの作成 をクリック。

これでロールができるので、 EC2に付与します。
1. EC2の管理画面に移動し、付与したいインスタンスを選択。
2. アクションのセキュリティにある、IAMロールを変更を選択。
3. IAM ロール に先ほど作ったIAMロールを選択。
4. 保存をクリック。

これで、 EC2にIAMロールが付与され、boto3が動くようになりました。
試します。


import boto3

text = """
メロスは激怒した。
必ず、かの邪智暴虐の王を除かなければならぬと決意した。
"""

client = boto3.client("translate", region_name="ap-northeast-1")

result = client.translate_text(
    Text=text,
    SourceLanguageCode="ja",
    TargetLanguageCode="en",
)

print(result["TranslatedText"])
"""
Melos got furious.
He determined that he must exclude the king of wicked violence.
"""

将来的に、 Translate 以外のサービスも使いたくなったら、
ロールの管理画面から今回作ったIAMロールにポリシーを追加でアタッチしていけば使うことができます。