AWSを使って生成AIを組み込んだカードゲームを開発する!

第5回Discordへの画像の送信とS3への画像アップロードを実装してアプリを完成させよう!

前回は生成AIを使ってカードを作成するところまで完成しました。最終回となる今回はDiscordへの画像の送信と、S3への画像アップロードを実装し、アプリを完成させます。

画像アップロード先のS3の準備

はじめに、開発環境(ローカル環境)と本番環境(EC2)から共通で使える、AWSのストレージサービスS3へ画像をアップロードするための設定を行います。

S3を使うことで、大量のデータを保存、取得できるようになります。詳しい情報については、Amazon S3のサイトを参照してください。

AWS CLIを使えるようにする

S3を利用するには、バケットというものを作成する必要があります。バケットはブラウザからでも作成できますが、今回はAWS CLIを使って作成します。

AWS CLIはターミナル上からコマンドを使ってさまざまなAWSサービスを作成、操作できるツールです。AWS CLIはインストールして使う必要がありますが、本番環境構築用のDockerである「cdk-gihyo-toreca」にはすでに用意してあります。

今回はこの本番環境構築用のDockerを利用してS3にバケットを作成します。作業ディレクトリは一旦「cdk-gihyo-toreca」のほうで作業を行いますので、そちらに移動してください(⁠⁠gihyo-toreca」と間違えないように注意してください⁠⁠。

以下のコマンドで、Dockerコンテナを起動します。そしてaws-cliのバージョンを確認しておきましょう。

# cdk-gihyo-toreca内のディレクトリにいることを確認
# macでm1およびm2をお使いの方   
cd mac-m1-m2

# 上記以外の方
cd cdk

# コンテナを起動させます
docker-compose run --rm cdk

# コンテナを起動させたらaws cliがDocker内に入っているか確認してみましょう。
aws --version
図1

S3のバケットを作成する

次にmb(make bucket)コマンドを使って、S3のバケットを作成します。

なお、S3のバケット名は全AWS内で一意である必要があるため、今回はtradingcard-upload-image-の後にランダムな文字列を付け加えて重複がないようにします。

以下のコマンドを入力します。

# aws s3 mb s3://任意のs3バケット名
aws s3 mb s3://tradingcard-upload-image-$(openssl rand -hex 4)

以下の図のようにバケットが作成された成功です。バケット名をメモしておきましょう。

図2

S3のバケットの削除方法を確認する

なお、S3のバケットは手動で作成したので、削除時も手動で行います。また--forceオプションを使うことで、中身にデータがあったとしてもバケットごと削除が可能です。S3の削除は不要になったタイミングで実施することになります。

# 対象のs3バケット名を一覧から確認
aws s3 ls

# 対象のs3バケットを中身を含めて削除
aws s3 rb s3://バケット名 --force

AWS Secrets Managerを使った環境変数の管理

S3を作ったので、環境変数にこのS3のバケット名を登録します。

Discordボットトークンをターミナル上から環境変数に設定したときはexport DISCORD_BOT_TOKEN='ボット作成時にコピーしたトークン'を入力したように、同じ方法でS3バケットの環境変数の設定も可能です。しかし、設定回数が増えてくると面倒になってきます。そこで今回はAWSのSecrets Managerを利用して、環境変数を設定します。

Secrets Managerを使うことで、設定した環境変数をローカル環境でも本番環境でも利用でき、かつ安全に管理できるようになります。

この設定作業は、cdk-gihyo-torecaのDocker内で進めます。具体的には、次のコマンドでMY_S3_BUCKET_NAMEというAWS上の環境変数を用意し、S3のバケット名を登録します。このバケット名に、先ほど作成したtradingcard-upload-image-xxxxxxxxを指定します。

# --name は環境変数名 --secret-string は登録したい値
aws secretsmanager create-secret --name MY_S3_BUCKET_NAME --secret-string "あなたのS3バケット名"

以下の図のように結果が表示されれば成功です。

図3

ブラウザからSecrets Managerを確認する

ブラウザ上から登録内容を確認してみましょう。AWS Consoleより、⁠Secrets Manager」を検索してください。

図4

リージョンが「東京」になっていることを確認して、⁠シークレットの名前」を選択します。

図5

その後「シークレットの値を取得する」を選択します。

図6

これにより、登録されている値を確認できます。

図7

Discordのボットトークンも登録する

ここでDiscordのボットトークンも登録しておきましょう。

これでローカル環境でのDiscordのボットトークンを環境変数に設定するためのコマンドexport DISCORD_BOT_TOKEN='ボット作成時にコピーしたトークン' や、本番起動時のDockerコマンド内にあった-e DISCORD_BOT_TOKEN='あなたのボットのトークン'の指定が不要になります。

登録方法はS3のバケット名を設定した時と同じで、DISCORD_BOT_TOKENというAWS上の環境変数を用意して、ボットトークンを入力して登録します。

# --name は環境変数名 --secret-string は登録したい値
aws secretsmanager create-secret --name DISCORD_BOT_TOKEN --secret-string "あなたのDiscordBotのトークン"

以下の図のように結果が表示されればDiscordのボットトークンも登録成功です。

図8

以上でaws cliによる操作は完了しました。次のコマンドでcdk-gihyo-torecaのDockerを終了させましょう。

# Dockerの終了
exit

Secrets Managerの環境変数を削除方法を確認する

バケット名とボットトークンは手動で設定したため、アプリが不要になったタイミングで手動で削除する必要があります。最後に行いますが、ここであわせて説明しておきます。

MY_S3_BUCKET_NAMEの環境変数を削除するときのコマンドと、DISCORD_BOT_TOKENの環境変数を削除するコマンドは次のとおりです。

# MY_S3_BUCKET_NAMEを削除する
aws secretsmanager delete-secret --secret-id MY_S3_BUCKET_NAME --force-delete-without-recovery

# DISCORD_BOT_TOKENを削除する
aws secretsmanager delete-secret --secret-id DISCORD_BOT_TOKEN --force-delete-without-recovery

app.pyの変更点

app.pyの変更点をみていきます。作業ディレクトリを「gihyo-toreca」「chapter-5」に移動しましょう。

Secrets Managerに登録した環境変数を読み込む

app.pyにSecrets Managerから環境変数を読み込むためのコードを追加しておく必要があります。

Secrets Managerとの連携にはboto3が必要です。get_secret関数を使うことで、Secrets Managerから任意の環境変数を取得できます。

app.py
import boto3 # AWSサービス連携用

# AWS Secrets Managerと連携
secrets_manager_client = boto3.client('secretsmanager')

# AWS Secrets Managerからシークレット情報を取得する関数
def get_secret(secret_name):
    try:
        # AWS Secrets Managerから指定したシークレットの値を取得します。
        get_secret_value_response = secrets_manager_client.get_secret_value(SecretId=secret_name)
        return get_secret_value_response['SecretString']
    except Exception as e:
        logging.error(f"シークレットの取得中にエラーが発生しました: {e}")
        return None

# シークレット名を指定しS3のBUCKET名を取得
BUCKET_NAME = get_secret('MY_S3_BUCKET_NAME')

# シークレット名を指定してDiscord BotのTOKENを取得
# TOKEN = os.getenv('DISCORD_BOT_TOKEN') こちらを廃止して下記に置き換え
TOKEN = get_secret('DISCORD_BOT_TOKEN') 

これによって、今まで実行環境から環境変数を読み込んでいたos.getenv('DISCORD_BOT_TOKEN')は不要となり、あらたにget_secret('DISCORD_BOT_TOKEN') で環境変数をSecrets Managerから読み込むようになりました。

Discordへ画像を送信する

Discordへの画像の送信方法は、@bot.command()内で、一旦環境内に保存した画像をimage_pathの値からファイルとして呼び出します。そしてdiscord.Fileオブジェクトを利用してDiscordに送信します。

# 生成された画像をDiscordに送信
await ctx.send(file=discord.File(image_path, filename=os.path.basename(image_path)))

os.path.basename(image_path)はファイルパスからファイル名のみを抽出しています。

S3への画像をアップロードする

S3アップロードにはboto3で用意されているupload_file関数を使います。ファイル名はDiscordへの送信時と同じく、os.path.basename(image_path)でファイル名のみを抽出しています。

# AWS S3と連携
s3_client = boto3.client('s3')

# S3にアップロードする関数
def upload_to_s3(file_path):
    max_retries = 3
    for attempt in range(max_retries):
        try:
            # upload_file(ファイルの場所, バケットの名前, ファイル名)
            s3_client.upload_file(file_path, BUCKET_NAME, os.path.basename(file_path))
            logging.info(f'{file_path}{BUCKET_NAME} にアップロードしました。')
            return
        except Exception as e:
            logging.error(f"S3へのアップロード中にエラーが発生しました: {e}")
            if attempt < max_retries - 1:
                logging.info(f"リトライ {attempt + 1}/{max_retries}")
            else:
                logging.error(f"S3へのアップロードが失敗しました。リトライ上限に達しました: {file_path}")

なお、ここではアップロードに失敗した場合は最大3回まで再試行するようにしています。

Discordに選択ボタンを用意し⁠ボタンによって処理を変える

さらに以下の図のようにDiscord上でインタラクティブな選択肢を提供し、ユーザーが「はい」または「いいえ」を選んで、画像をS3に保存するかどうかを選択できる機能を実装しました。

図9

Discordの選択肢ボタンを機能させるコードは以下のとおりです。

# Discordの選択肢ボタンの機能
class ConfirmView(View):
    def __init__(self, file_path):
        # ボタンの押せる有効時間を1分間に設定
        super().__init__(timeout=60)
        # 画像パスをインスタンス変数に入れる
        self.file_path = file_path

    # 『はい』ボタンの設定 ボタンの色は緑
    @discord.ui.button(label="はい", style=discord.ButtonStyle.green)
    async def confirm(self, interaction: discord.Interaction, button: Button):
        # S3にアップロードの処理を呼び出し
        upload_to_s3(self.file_path)

        # ローカルファイルの削除
        os.remove(self.file_path)
        #「画像を保存しました!」というメッセージを送信
        await interaction.response.send_message("画像を保存しました!")
        # ボタン機能を終了
        self.stop()

    # 『いいえ』ボタンの設定 ボタンの色は灰
    @discord.ui.button(label="いいえ", style=discord.ButtonStyle.grey)
    async def cancel(self, interaction: discord.Interaction, button: Button):
        # ローカルファイルの削除
        os.remove(self.file_path)
        #「画像を保存しませんでした。」というメッセージを送信
        await interaction.response.send_message("画像を保存しませんでした。")
        #ボタン機能を終了
        self.stop()

「はい」を選択するとupload_to_s3関数を呼び出して、メッセージとともにS3へ画像を保存します。その後、不要になった実行環境内の画像ファイルを削除して終了します。

「いいえ」を選択すると「保存をしませんでした」というメッセージの後、実行環境内の画像ファイルを削除して終了します。

この機能は、discord.pyライブラリが提供するViewクラスを継承し、カスタマイズしたConfirmViewクラスを用意することで実現しています。ViewクラスはDiscord UIの部分を提供しており、@discord.ui.buttonの箇所でユーザーがクリック可能なボタンを設定できます。

このConfirmViewクラスをインスタンス化して、プログラムのmake関数内で以下のように利用しています。

async def make(ctx, *, text: str):
    try:
        logging.info(f'受信したメッセージ: {text}')

        await ctx.send("ただいま作成中...")

        monster_info = generate_monster_bedrock(text)
        image_path = generate_card(monster_info)

        # 生成された画像をDiscordに送信
        await ctx.send(file=discord.File(image_path, filename=os.path.basename(image_path)))

        # ConfirmViewを使ってユーザーに保存するか尋ねる
        view = ConfirmView(image_path)
        await ctx.send("S3に保存しますか?", view=view)

        # Viewが停止するのを待ちます(ユーザーがボタンを押すか、タイムアウトするまで)
        await view.wait()

    except Exception as e:
        logging.error(f"コマンドの実行中にエラーが発生しました: {e}")
        await ctx.send("エラーが発生しました。しばらく待ってから再度お試しください。")

ここでは、ユーザーがボタン選択もしくはタイムアウトするまでBotは待機させています。また、try-exceptを使って例外処理を行っています。

これでアプリが完成しました!

今回は開発環境での動作確認については特に説明はしませんが、任意でターミナルからdocker-comose upで起動して実施してみてください。その際、前述したとおりSecrets Managerを設定しているため、export DISCORD_BOT_TOKEN='ボット作成時にコピーしたトークン'の環境変数の設定は不要になります。

本番環境へのデプロイ

本番環境構築用のDockerがある「cdk-gihyo-toreca」ディレクトリに移動し、本番環境へデプロイしましょう。パソコンの環境によって以下のどちらかのディレクトリに移動してください。

# macでm1およびm2をお使いの方用
cd mac-m1-m2
# 上記以外の方用
cd cdk

今回はAmazon S3と、Secrets ManagerをEC2から呼び出しているため、EC2のロールにポリシーを追加する必要があります。そのためapp.pyで以下の設定を行っています。

# 第5回目に必要 S3のインラインポリシーを作成しロールにアタッチ
ec2_role.add_to_policy(iam.PolicyStatement(
    actions=["s3:*"],
    resources=["arn:aws:s3:::*"],
    effect=iam.Effect.ALLOW,
))

# 第5回目に必要 Secrets Manager アクセス権限のインラインポリシーを作成しロールにアタッチ
ec2_role.add_to_policy(iam.PolicyStatement(
    actions=[
        "secretsmanager:GetSecretValue",
        "secretsmanager:DescribeSecret",
    ],
    resources=["*"],
    effect=iam.Effect.ALLOW,
))

それでは以下の一連の流れで本番公開をしましょう。

# Dockerを起動して、コンテナ内に入ります
docker-compose run --rm cdk

# AWSへデプロイします 前回cdk destroyでAWS環境を削除していない方は、すでにAWS上に環境が構築されているので、すぐに処理が終わります。削除されている方は5分ほどで作成されます。
cdk deploy

# AWS環境が作成後、EC2に接続します。 
aws ssm start-session --target 作成したEC2のインスタンスID
sudo su - ec2-user

# GitHubよりアプリをクローンする 
git clone https://github.com/あなたのgithubアカウント名/gihyo-toreca.git

# chapter-5に移動
cd gihyo-toreca/chapter-5

# DockerfileからDockerイメージを作成
docker build --no-cache -t gihyo-toreca .

# Dockerの起動 第五回ではSecret Managerを設定したので、環境変数のオプションは不要になります。
docker run --rm -it --name app-container -v "$(pwd)":/app gihyo-toreca

アプリの動作確認

アプリが起動できたところで、Discord上から!make 宝石が埋め込まれたカエルのモンスターをリクエストしてみます。すると次のように完成画像が表示されるはずです。

5-10

また、⁠S3に保存しますか?」と尋ねられる聞かれるので「はい」を押すと「画像を保存しました!」というメッセージとともに、S3へ画像がアップロードされます。

いくつか、モンスターを生成を繰り返してみましょう。今のプロンプトではどうやら名前生成が安定しないため、名前は指定せずに作ってみました。

!make 凶暴化してしまった金魚のモンスター、属性は水
!make 天使の剣を持った精霊のモンスター、強さ6が、属性は光
!make 山にひっそり住む石の巨人のモンスター
!make 小さな鳥のモンスター、属性は風、強さが1
!make 骸骨の口から魂が出ているモンスター、強さが4
!make 3匹が一体化した鬼のモンスター、属性は火、特殊能力は強力な爪を使ったもの
!make 太陽の形をした翼が生えているモンスター、特殊能力は太陽風を使ったもの
!make 砂漠で穴を掘って住む大きなミミズのモンスター
!make きのこの胞子を撒き散らすモンスター、強さが2、属性は土
!make 立髪が炎で燃えている馬のモンスター、強さが10、属性は火
!make 伝説のケルベロスのモンスター、強さが10、属性は光
!make 神のペットだった子猫のモンスター、強さが1、属性は光

ブラウザからS3上に画像が保存されたことも確認してみましょう。AWS Consoleを開いて「S3」を検索してください。

図11

検索欄から「trading」と入力して、対象のバケットを選択します。

図12

バケット内に入ったらファイルをチェックボックスから選択し、ダウンロードを押してダウンロードします。

図13

S3内の画像データを一括ダウンロードする

S3上に保存した画像を一括でダウンロードしたい場合は、aws cliコマンドを使って行いましょう。

ターミナルをもう一つ立ち上げ、⁠cdk-gihyo-toreca」ディレクトリからもう一つDockerを起動させます。

# cdk-gihyo-torecaディレクトリにて
# macでm1およびm2をお使いの方用
cd mac-m1-m2
# 上記以外の方用
cd cdk

# Dockerを起動して、コンテナ内に入ります
docker-compose run --rm cdk

このDocker内でaws cliツールが使えますので、次のコマンドからS3に保存されている画像を一括でダウンロードします。ダウンロード先には、現在のフォルダ内のs3-imagesという場所に設定しました。

aws s3 sync s3://あなたのバケット名 ./s3-images
図14

公開停止⁠⁠削除

公開停止および削除の流れは基本的に、これまでと同じです。

# EC2とローカルとの接続解除
exit

# AWS本番環境の削除
cdk destroy

# ローカル環境のDockerの終了
exit

ただし、手動作成S3のバケット、Secrets Managerの環境変数はcdk destroyで削除されません。よって、不要になったタイミングで記事内の削除コマンドでそれぞれを削除しておく必要があります。

S3のバケットの削除は「 S3のバケットの削除方法を確認する」の項の記述のとおり、削除してください。また、Secrets Managerの環境変数は「Secrets Managerの環境変数を削除方法を確認する」の項の記述のとおり、削除してください。

連載のおわりに

この連載ではAWSの様々なサービスを組み合わせることで、個人でも効率的に大量のコンテンツを作成する環境を構築できること、AIを活用することでさらにその可能性を広げられること、そしてWeb技術がブラウザだけでなく様々な用途に応用できることを紹介しました。

図15

これからの開発やコンテンツ作成において、本連載で取り上げたことを活かしていただければ幸いです。

最後までお読みいただき、ありがとうございました!

おすすめ記事

記事・ニュース一覧