Amazon Rekognitionで顔検出

AWSの機械学習・画像認識サービスであるAmazon Rekognition を試してみたので記録を残しておきます。
今回はまず、画像中の人の顔を検出するタスクをやってみました。
また、例によってPython(boto3)を使っています。

使い方はめちゃくちゃ簡単で、boto3のクライアントAPIから、detect_faces()というメソッドを呼び出すだけでした。
ドキュメントはこちらです。
参照: Rekognition — Boto3 Docs 1.17.93 documentation

対象のデータはローカルのファイルをバイト列のデータとして渡す方法と、S3にアップロードしてそのバケット名とファイル名を渡す方法の2種類があります。
ドキュメントには、
The input image as base64-encoded bytes or an S3 object.
と書いてあるので、Base64エンコードしないといけないのかと思ったのですが、これはどうやらドキュメントの誤りです。
Base64エンコードして渡すと逆にエラーになりますので、バイト列で読み込む時はファイルを読み込んだバイトデータをそのまま渡してください。
(予想ですが、boto3のライブラリが内部処理で Base64エンコードしてくれてると思います。)
せっかくこのブログでもBase64エンコーディングの方法を紹介する記事を書いて準備していたのにいらなかったですね。

引数の渡し方は少し特殊で、名前付き引数に辞書型で渡す必要があります。
ローカルのファイルを使う場合は次のようにします。

ちなみに画像は、AWSのコンソールでサンプルとして表示される家族写真を使います。(ファイル名:family.jpg)


import boto3


# 画像データを読み込む。
with open("./family.jpg", "rb") as f:
    img = f.read()

client = boto3.client('rekognition')
response = client.detect_faces(Image={'Bytes': img})

S3にアップロードしたデータを使う場合は次のようにします。


client = boto3.client('rekognition')
response = client.detect_faces(
    Image={
        'S3Object': {
            'Bucket': '{バケット名}',
            'Name': 'family.jpg',
        }
    },
)

結果は、辞書型で帰ってきます。FaceDetailsというキーの中身が、メインの検出結果で、
ResponseMetadataのほうはRequestのIdや、HTTPレスポンスのステータスコード、処理を実行した時刻などのメタデータが入ってます。


print(response.keys())
# dict_keys(['FaceDetails', 'ResponseMetadata'])

response[“FaceDetails”] の中身は配列で、検出された顔一人分ごとに辞書型で検出された情報が入っています。
今回のサンプルでは3人検出されているのですが、全部表示すると長いので一人分お見せすると次のようになります。
(整形のためにjsonライブラリ使います)


import json
print(json.dumps(response["FaceDetails"][0], indent=4))
"""
{
    "BoundingBox": {
        "Width": 0.1937681883573532,
        "Height": 0.3873019516468048,
        "Left": 0.2916979193687439,
        "Top": 0.13570082187652588
    },
    "Landmarks": [
        {
            "Type": "eyeLeft",
            "X": 0.34084638953208923,
            "Y": 0.2765427529811859
        },
        {
            "Type": "eyeRight",
            "X": 0.4209189713001251,
            "Y": 0.31195494532585144
        },
        {
            "Type": "mouthLeft",
            "X": 0.32429391145706177,
            "Y": 0.40175312757492065
        },
        {
            "Type": "mouthRight",
            "X": 0.39117804169654846,
            "Y": 0.43135175108909607
        },
        {
            "Type": "nose",
            "X": 0.3650367856025696,
            "Y": 0.3684481084346771
        }
    ],
    "Pose": {
        "Roll": 17.15113067626953,
        "Yaw": -3.947751760482788,
        "Pitch": -1.8470479249954224
    },
    "Quality": {
        "Brightness": 62.19182586669922,
        "Sharpness": 78.64350128173828
    },
    "Confidence": 99.99921417236328
}
"""

BoundingBox の中にあるのが、顔が検出された位置です。顔を囲む長方形の情報が含まれています。
Landmarksの下に、目、鼻、口の両端の位置が含まれます。
Poseは顔の向きです。
Qualityは画像の明るさなどの情報で、Confidenceは境界ボックス内に顔が含まれている信頼度になります。
詳しくはこちら
参考: イメージ内の顔を検出する – Amazon Rekognition

顔やそのパーツの位置の座標が0〜1の範囲に収まっていることから分かる通り、これらは
画像の左上を(0%,0%)、右下を(100%, 100%)とした時の相対的な位置を示しています。

上のJSON型データを見てもどのくらい正確に検出できているかわからないと思うので可視化してみましょう。
使い慣れているので、Matplotlibでやってみました。
別途、skimageで画像をNumpy配列として読み取り、画像の幅と高さを取得しています。
そして、それをRekognitionで取得した相対的な位置と掛け合わせることで、絶対値での座標に変換しています。


import matplotlib.pyplot as plt
from matplotlib import patches
from skimage import io


# matplotlib表示用に画像を配列で読み込み
img_array = io.imread("./family.jpg")
# 画像の高さと横幅を取得
image_h, image_w, _ = img_array.shape

fig = plt.figure(facecolor="w", figsize=(12, 12))
ax = fig.add_subplot(111)
# 元の画像を表示する
ax.imshow(img_array)

for face_detail in response["FaceDetails"]:
    # 検出された顔の位置を取得し、座標に変換する
    left = face_detail["BoundingBox"]["Left"] * image_w
    top = face_detail["BoundingBox"]["Top"] * image_h
    width = face_detail["BoundingBox"]["Width"] * image_w
    height = face_detail["BoundingBox"]["Height"] * image_h
    # 取得した座標の位置に長方形を描写する
    patch = patches.Rectangle(
        xy=(left, top),
        width=width,
        height=height,
        fill=False,
        ec="w",
        linewidth=2,
    )
    ax.add_patch(patch)

    # 目、鼻、口の両端に点をプロット
    ax.scatter(
        [landmark["X"] * image_w for landmark in face_detail["Landmarks"]],
        [landmark["Y"] * image_h for landmark in face_detail["Landmarks"]],
        c="w",
        s=3
    )

このコードで出力されるのが次の画像です。

3人分の顔が精度良く検出できていることがわかりますね。

さて、detect_faces()ですが、実はもう一つ引数を持っています。
それが、Attributes です。
Attributes=[“DEFAULT”] (こちらがデフォルト)
または、
Attributes=[“ALL”]
と指定します。 []も必須です。


response = client.detect_faces(
    Image={'Bytes': img},
    Attributes=["ALL"]
)

のように、 [“ALL”]を指定すると、取得できる情報が一気に増えます。
表情(笑顔かどうか)や、メガネやサングラスの有無、髭の有無や口が開いているかどうか、
大まかな年齢の推定なども行ってくれます。
また、目や口の位置情報はより詳細になり、輪郭や眉毛などに関する位置も取得されます。

結果がものすごく大きくなるのでこの記事には載せませんが、ぜひ一度試してみてください。

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へのファイルのアップロード、コピー、リストアップ、ダウンロード、削除ができるようになりました。

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を使いましょう。

サービスとして動かしている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
に突っ込んでいきましょう。

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ロールにポリシーを追加でアタッチしていけば使うことができます。

Amazon RDS(Aurora)で日本語文字列や絵文字を使えるようにする

RDSのデフォルトの設定のクラスタで、データベースとテーブルを特に文字コードを設定せずに作成した場合、
VARCHAR(文字列)型の列を作ってもそこに日本語などのマルチバイト文字を格納することはできません。

カラムか、テーブルか、データベース単位で設定してもいいのですが、パラメーターグループを使って設定する方法を紹介します。
Aurora(正確にはそれが互換性を持っているMySQL)では、文字コード(Character Set)と照合順序(Collation)をそれぞれ独立した設定として持っています。
照合順序というのはソート順やアルファベットの大文字小文字を同一視するかどうかといったルールのセットです。

文字コードは日本語を使いたいのであればUTF8を使えるように設定する必要がありますが、
歴史的な経緯により、MySQLのUTF8には、utf8とutf8mb4の2種類があります。
utf8には3バイトまでの文字しか含まれておらず、絵文字等の4バイト文字も使えるようにするためにはutf8mb4を設定する必要があります。

参考: 10.1.10.7 utf8mb4 文字セット (4 バイトの UTF-8 Unicode エンコーディング)

さて、とりあえず何も設定しなかった場合の設定を見ておきましょう。


MySQL [(none)]> SHOW VARIABLES LIKE 'char%';
+--------------------------+-------------------------------------------------------------------------+
| Variable_name            | Value                                                                   |
+--------------------------+-------------------------------------------------------------------------+
| character_set_client     | utf8                                                                    |
| character_set_connection | utf8                                                                    |
| character_set_database   | latin1                                                                  |
| character_set_filesystem | binary                                                                  |
| character_set_results    | utf8                                                                    |
| character_set_server     | latin1                                                                  |
| character_set_system     | utf8                                                                    |
| character_sets_dir       | /rdsdbbin/oscar-5.7.serverless_mysql_aurora.2.08.3.38.0/share/charsets/ |
+--------------------------+-------------------------------------------------------------------------+
8 rows in set (0.01 sec)

MySQL [(none)]> SHOW VARIABLES LIKE 'coll%';
+----------------------+-------------------+
| Variable_name        | Value             |
+----------------------+-------------------+
| collation_connection | utf8_general_ci   |
| collation_database   | latin1_swedish_ci |
| collation_server     | latin1_swedish_ci |
+----------------------+-------------------+
3 rows in set (0.01 sec)

character_set_client, character_set_connection, character_set_results
はデフォルトでは utf8 になっていますが、 character_set_database と、 character_set_server が、latin1 になっています。
絵文字含む日本語文字を使うためにはこれら5つをutf8mb4に設定する必要があります。

ここからがややこしいことなのですが、RDSのパラメーターグループではこの5つをそれぞれ設定できるようになっているのに、
character_set_client, character_set_connection, character_set_results にはそれが反映されません。
そして、 character_set_database には character_set_server に設定した値が入ります。
反映されない3つは、クライアント側で設定する必要があるようです。

また、collation の方も、 collation_connection には設定した値が反映されず、
collation_server に設定した値が、collation_server と collation_database に反映されます。

これを踏まえて設定していきます。まず、RDSのパラメーターグループには次のふたつを設定します。

character_set_server: utf8mb4
collation_database: utf8mb4_bin

この段階で、設定は次のようになります。


MySQL [(none)]> SHOW VARIABLES LIKE 'char%';
+--------------------------+-------------------------------------------------------------------------+
| Variable_name            | Value                                                                   |
+--------------------------+-------------------------------------------------------------------------+
| character_set_client     | utf8                                                                    |
| character_set_connection | utf8                                                                    |
| character_set_database   | utf8mb4                                                                 |
| character_set_filesystem | binary                                                                  |
| character_set_results    | utf8                                                                    |
| character_set_server     | utf8mb4                                                                 |
| character_set_system     | utf8                                                                    |
| character_sets_dir       | /rdsdbbin/oscar-5.7.serverless_mysql_aurora.2.08.3.38.0/share/charsets/ |
+--------------------------+-------------------------------------------------------------------------+
8 rows in set (0.01 sec)

MySQL [(none)]> SHOW VARIABLES LIKE 'coll%';
+----------------------+-----------------+
| Variable_name        | Value           |
+----------------------+-----------------+
| collation_connection | utf8_general_ci |
| collation_database   | utf8mb4_bin     |
| collation_server     | utf8mb4_bin     |
+----------------------+-----------------+
3 rows in set (0.00 sec)

次にクライアント側の設定です。
僕は、Amazon Linux 2 で標準のRDSになった、MariaDBを利用しています。
設定ファイルは、 /etc/my.cnf なのですが、
その中を見ると、 !includedir /etc/my.cnf.d となっていて、他のファイルを読み込んでいます。

/etc/my.cnf.d/client.cnf と言うファイルの中に、 [client]と言うセクションがあるのでそこに設定します。


$ sudo vim /etc/my.cnf.d/client.cnf

[client]
# 以下の1行を追加
default-character-set = utf8mb4

MySQL [(none)]> SHOW VARIABLES LIKE 'char%';
+--------------------------+-------------------------------------------------------------------------+
| Variable_name            | Value                                                                   |
+--------------------------+-------------------------------------------------------------------------+
| character_set_client     | utf8mb4                                                                 |
| character_set_connection | utf8mb4                                                                 |
| character_set_database   | utf8mb4                                                                 |
| character_set_filesystem | binary                                                                  |
| character_set_results    | utf8mb4                                                                 |
| character_set_server     | utf8mb4                                                                 |
| character_set_system     | utf8                                                                    |
| character_sets_dir       | /rdsdbbin/oscar-5.7.serverless_mysql_aurora.2.08.3.38.0/share/charsets/ |
+--------------------------+-------------------------------------------------------------------------+
8 rows in set (0.00 sec)

MySQL [(none)]> SHOW VARIABLES LIKE 'coll%';
+----------------------+--------------------+
| Variable_name        | Value              |
+----------------------+--------------------+
| collation_connection | utf8mb4_general_ci |
| collation_database   | utf8mb4_bin        |
| collation_server     | utf8mb4_bin        |
+----------------------+--------------------+
3 rows in set (0.01 sec)

これで、ほぼ設定できました。
実用上これで困ることなく使うことができると思います。

後1点, collation_connection の設定が utf8mb4_bin にならずに、 utf8mb4_general_ci になってしまうのですが、
どなたか collation_connection の設定を utf8mb4_bin にする方法をご存知の方がいらしたら教えていただけないでしょうか。

そのセッションに限った設定であれば、
SET collation_connection = utf8mb4_bin;
で設定できるのですが、接続を切ると元に戻ってしまいます。

また、
SET GLOBAL collation_connection = utf8mb4_bin;
は RDS では実行できないようです。

先述の通り、パラメーターグループで設定しても反映されません。
あまり困ることもないのですが、ここだけ設定がズレていて気持ち悪いので可能であればutf8mb4_binに揃えたいです。

RDSのタイムゾーンを日本時間(Asia/Tokyo)にする

EC2の時刻設定の話を書いたついでに、RDSの時刻設定を変える方法も紹介しておきます。
(ちなみに僕が使ってるRDSは Amazon Aurora Serverless です。)

結論から言うと、RDSにはパラメーターグループと呼ばれる設定値のコンテナのようなものがあり、
それを使ってシステム環境変数を設定します。

ドキュメントはここかな。(これを読むより実際に動かした方がわかりやすい)
参考: DB パラメータグループを使用する

まず、デフォルトの設定を確認しておきましょう。
何も設定せずに、RDSのクラスタを立ち上げ、現在時刻を表示してみます。

これ実行したのは 2021-03-26 23:48:09 です。


MySQL [(none)]> SELECT NOW();
+---------------------+
| NOW()               |
+---------------------+
| 2021-03-26 14:48:09 |
+---------------------+
1 row in set (0.01 sec)

MySQL [(none)]> SHOW VARIABLES LIKE 'time_%';
+---------------+-------------------+
| Variable_name | Value             |
+---------------+-------------------+
| time_format   | %H:%i:%s          |
| time_zone     | SYSTEM            |
| timestamp     | 1616770134.005551 |
+---------------+-------------------+
3 rows in set (0.01 sec)

time_zone は SYSTEM となっていますが9時間ずれていてUTCだとわかりますね。
これを修正していきます。

– RDSの管理画面の左ペインから、パラメーターグループを選択する
– パラメータグループの作成 を押下
– 以下のように項目選択
パラメータグループファミリー aurora-mysql5.7
タイプ DB Cluster Parameter Group
グループ名 任意
説明 任意
– 作成 を押下
– 作成したパラメーターグループを選択し、パラメーターグループアクションから編集を選択
– time_zone を探して、 Asia/Tokyo を選択
– 変更を保存
– データベースの一覧に戻り、設定を変更したいデータベースを選択
– 変更を押下
– DB クラスターのパラメータグループ に先ほど作ったパラメーターグループを選択し、続行を押下
– 変更を適用するタイミング は すぐに適用 を選択
– クラスターの変更 を押下

この後、セッションを切ってしばらく待った後繋ぎ直すと反映されています。


MySQL [(none)]> SELECT NOW();
+---------------------+
| NOW()               |
+---------------------+
| 2021-03-26 23:59:23 |
+---------------------+
1 row in set (0.01 sec)

MySQL [(none)]> SHOW VARIABLES LIKE 'time_%';
+---------------+-------------------+
| Variable_name | Value             |
+---------------+-------------------+
| time_format   | %H:%i:%s          |
| time_zone     | Asia/Tokyo        |
| timestamp     | 1616770768.009869 |
+---------------+-------------------+
3 rows in set (0.00 sec)

Aurora Serverless と言うより RDS では、他のステム変数も同じようにパラメーターグループを使って編集できます。

EC2(Amazon Linux 2)の時刻設定を日本時間にする

初期設定ではUTCになっている、EC2の時刻設定を東京時間にします。
昔は設定ファイルを書き換えたりリンクを貼ったりと若干手間だったような覚えがあるのですが、
Amazon Linux 2 では、timedatectl という便利なコマンドがあり、これを使って設定ができるようです。

まず、
timedatectl list-timezones
で設定可能なタイムゾーンの一覧を取得できます。
東京時間があることを確認しておきましょう。


$ timedatectl list-timezones | grep Tokyo
Asia/Tokyo

次に、TimeZoneを設定し、確認します。


$ timedatectl set-timezone Asia/Tokyo
$ timedatectl status
      Local time: 木 2021-03-18 22:58:37 JST
  Universal time: 木 2021-03-18 13:58:37 UTC
        RTC time: 木 2021-03-18 13:58:37
       Time zone: Asia/Tokyo (JST, +0900)
     NTP enabled: yes
NTP synchronized: yes
 RTC in local TZ: no
      DST active: n/a

これで設定できました。

あとは念の為、サーバーを再起動しておきましょう。
$ sudo reboot