前回は生成AIを使ってカードを作成するところまで完成しました。最終回となる今回はDiscordへの画像の送信と、S3への画像アップロードを実装し、アプリを完成させます。
画像アップロード先のS3の準備
はじめに、開発環境
S3を使うことで、大量のデータを保存、取得できるようになります。詳しい情報については、Amazon S3のサイトを参照してください。
AWS CLIを使えるようにする
S3を利用するには、バケットというものを作成する必要があります。バケットはブラウザからでも作成できますが、今回はAWS CLIを使って作成します。
AWS CLIはターミナル上からコマンドを使ってさまざまなAWSサービスを作成、操作できるツールです。AWS CLIはインストールして使う必要がありますが、本番環境構築用のDockerである
今回はこの本番環境構築用のDockerを利用してS3にバケットを作成します。作業ディレクトリは一旦
以下のコマンドで、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

S3のバケットを作成する
次にmb
なお、S3のバケット名は全AWS内で一意である必要があるため、今回はtradingcard-upload-image-
の後にランダムな文字列を付け加えて重複がないようにします。
以下のコマンドを入力します。
# aws s3 mb s3://任意のs3バケット名
aws s3 mb s3://tradingcard-upload-image-$(openssl rand -hex 4)
以下の図のようにバケットが作成された成功です。バケット名をメモしておきましょう。

S3のバケットの削除方法を確認する
なお、S3のバケットは手動で作成したので、削除時も手動で行います。また--force
オプションを使うことで、中身にデータがあったとしてもバケットごと削除が可能です。S3の削除は不要になったタイミングで実施することになります。
# 対象のs3バケット名を一覧から確認 aws s3 ls # 対象のs3バケットを中身を含めて削除 aws s3 rb s3://バケット名 --force
AWS Secrets Managerを使った環境変数の管理
S3を作ったので、環境変数にこのS3のバケット名を登録します。
Discordボットトークンをターミナル上から環境変数に設定したときはexport DISCORD_
を入力したように、同じ方法でS3バケットの環境変数の設定も可能です。しかし、設定回数が増えてくると面倒になってきます。そこで今回はAWSのSecrets Managerを利用して、環境変数を設定します。
Secrets Managerを使うことで、設定した環境変数をローカル環境でも本番環境でも利用でき、かつ安全に管理できるようになります。
この設定作業は、cdk-gihyo-torecaのDocker内で進めます。具体的には、次のコマンドでMY_
というAWS上の環境変数を用意し、S3のバケット名を登録します。このバケット名に、先ほど作成したtradingcard-upload-image-xxxxxxxx
を指定します。
# --name は環境変数名 --secret-string は登録したい値 aws secretsmanager create-secret --name MY_S3_BUCKET_NAME --secret-string "あなたのS3バケット名"
以下の図のように結果が表示されれば成功です。

ブラウザからSecrets Managerを確認する
ブラウザ上から登録内容を確認してみましょう。AWS Consoleより、

リージョンが

その後

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

Discordのボットトークンも登録する
ここでDiscordのボットトークンも登録しておきましょう。
これでローカル環境でのDiscordのボットトークンを環境変数に設定するためのコマンドexport DISCORD_
や、本番起動時のDockerコマンド内にあった-e DISCORD_
の指定が不要になります。
登録方法はS3のバケット名を設定した時と同じで、DISCORD_
というAWS上の環境変数を用意して、ボットトークンを入力して登録します。
# --name は環境変数名 --secret-string は登録したい値 aws secretsmanager create-secret --name DISCORD_BOT_TOKEN --secret-string "あなたのDiscordBotのトークン"
以下の図のように結果が表示されればDiscordのボットトークンも登録成功です。

以上でaws cliによる操作は完了しました。次のコマンドでcdk-gihyo-torecaのDockerを終了させましょう。
# Dockerの終了 exit
Secrets Managerの環境変数を削除方法を確認する
バケット名とボットトークンは手動で設定したため、アプリが不要になったタイミングで手動で削除する必要があります。最後に行いますが、ここであわせて説明しておきます。
MY_
の環境変数を削除するときのコマンドと、DISCORD_
の環境変数を削除するコマンドは次のとおりです。
# 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.
Secrets Managerに登録した環境変数を読み込む
app.
Secrets Managerとの連携にはboto3が必要です。get_
関数を使うことで、Secrets Managerから任意の環境変数を取得できます。
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.
は不要となり、あらたにget_
で環境変数をSecrets Managerから読み込むようになりました。
Discordへ画像を送信する
Discordへの画像の送信方法は、@bot.
内で、一旦環境内に保存した画像をimage_
の値からファイルとして呼び出します。そしてdiscord.
オブジェクトを利用してDiscordに送信します。
# 生成された画像をDiscordに送信
await ctx.send(file=discord.File(image_path, filename=os.path.basename(image_path)))
os.
はファイルパスからファイル名のみを抽出しています。
S3への画像をアップロードする
S3アップロードにはboto3で用意されているupload_
関数を使います。ファイル名はDiscordへの送信時と同じく、os.
でファイル名のみを抽出しています。
# 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上でインタラクティブな選択肢を提供し、ユーザーが

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_
関数を呼び出して、メッセージとともにS3へ画像を保存します。その後、不要になった実行環境内の画像ファイルを削除して終了します。
「いいえ」
この機能は、discord.View
クラスを継承し、カスタマイズしたConfirmView
クラスを用意することで実現しています。View
クラスはDiscord UIの部分を提供しており、@discord.
の箇所でユーザーがクリック可能なボタンを設定できます。
この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_
の環境変数の設定は不要になります。
本番環境へのデプロイ
本番環境構築用のDockerがある
# macでm1およびm2をお使いの方用 cd mac-m1-m2 # 上記以外の方用 cd cdk
今回はAmazon S3と、Secrets ManagerをEC2から呼び出しているため、EC2のロールにポリシーを追加する必要があります。そのためapp.
# 第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 宝石が埋め込まれたカエルのモンスター
をリクエストしてみます。すると次のように完成画像が表示されるはずです。

また、
いくつか、モンスターを生成を繰り返してみましょう。今のプロンプトではどうやら名前生成が安定しないため、名前は指定せずに作ってみました。
!make 凶暴化してしまった金魚のモンスター、属性は水 !make 天使の剣を持った精霊のモンスター、強さ6が、属性は光 !make 山にひっそり住む石の巨人のモンスター !make 小さな鳥のモンスター、属性は風、強さが1 !make 骸骨の口から魂が出ているモンスター、強さが4 !make 3匹が一体化した鬼のモンスター、属性は火、特殊能力は強力な爪を使ったもの !make 太陽の形をした翼が生えているモンスター、特殊能力は太陽風を使ったもの !make 砂漠で穴を掘って住む大きなミミズのモンスター !make きのこの胞子を撒き散らすモンスター、強さが2、属性は土 !make 立髪が炎で燃えている馬のモンスター、強さが10、属性は火 !make 伝説のケルベロスのモンスター、強さが10、属性は光 !make 神のペットだった子猫のモンスター、強さが1、属性は光
ブラウザからS3上に画像が保存されたことも確認してみましょう。AWS Consoleを開いて

検索欄から

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

S3内の画像データを一括ダウンロードする
S3上に保存した画像を一括でダウンロードしたい場合は、aws cliコマンドを使って行いましょう。
ターミナルをもう一つ立ち上げ、
# 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

公開停止・削除
公開停止および削除の流れは基本的に、これまでと同じです。
# EC2とローカルとの接続解除 exit # AWS本番環境の削除 cdk destroy # ローカル環境のDockerの終了 exit
ただし、手動作成S3のバケット、Secrets Managerの環境変数はcdk destroy
で削除されません。よって、不要になったタイミングで記事内の削除コマンドでそれぞれを削除しておく必要があります。
S3のバケットの削除は
連載のおわりに
この連載ではAWSの様々なサービスを組み合わせることで、個人でも効率的に大量のコンテンツを作成する環境を構築できること、AIを活用することでさらにその可能性を広げられること、そしてWeb技術がブラウザだけでなく様々な用途に応用できることを紹介しました。

これからの開発やコンテンツ作成において、本連載で取り上げたことを活かしていただければ幸いです。
最後までお読みいただき、ありがとうございました!