🧃

バックエンドも AWS も分からず Chalice を触った体験記【Python/React】

2024/05/17に公開

はじめに

これまでフロントエンドばかりやってきたので、バックエンドのことは全然知りません。REST API…?MVC…?なんとなく聞いたことはあるけど…そんなレベルです。
今回 AWS Chalice と React を用いて簡単な Todo-app の開発を行ったので、体験記として残します。

技術スタック

フロントエンド

  • React + Vite

バックエンド

  • Python
  • AWS Chalice
  • PostgreSQL

リポジトリ

https://github.com/poetrainy/todoapp-python

本記事では一部コードを例示する箇所がありますが、全てこのリポジトリから抜粋したものです。全コードをご覧になりたい方は上記を参照ください。

やったこと

  1. PostgreSQL で DB を作成する
  2. Cognito でユーザーを管理する
  3. SES でメールを送信する
  4. S3 に画像をアップロード+ Cognito のユーザー情報を変更+不要な画像は適宜削除する

順を追って説明します。

1. PostgreSQL で DB を作成する

PostgreSQL で todo DB を作成し、ライブラリを用いて Chalice 側から操作できるようにしました。

結局のところこれが一番難しかった気がします。バックエンド側の README にも書きましたが、インストールしたライブラリの半分くらいは DB のためでした。それほどやることが多く、難解でした…。
また、以下 3 つのデスクトップアプリをインストールしました。社用 PC には入れていたものもある(使っていたとは言っていない)のですが、私用 PC で使うのは初めてでした 👼 ちゃんと一通り触りました

  • PostgreSQL
    UI 上でテーブルを選択するだけでターミナルで SQL を起動できる
  • TablePlus
    DB の中身を確認する
  • Postman
    ローカルサーバーに向かって各種リクエストを送り、動作を確認する

2. Cognito でユーザーを管理する

Cognito に関しては長くなってしまったので別記事にまとめました。
サインイン、サインアップ、サインアウト、ユーザー削除及びユーザー属性の取得と更新について記述しています。

https://zenn.dev/poetrainy/articles/d5029c52d4aa47

また、 todo DB に user_id カラムを追加して、Cognito のユーザー ID を入れるようにしました。
フロント側では、 Todo リストを取得する API を叩く関数の引数に Cognito から取得したユーザー ID を入れることで、 todo DB から必要なデータのみを抽出して返すようにしました。

3. SES でメールを送信する

タスクが完了されると、フロントから送った user_id をもとに Cognito でユーザー検索をして email を取得し、タスクが完了されたことを知らせるメールが送信されるようにしました。

  1. todo_service.update_todo:タスクを更新する
  2. user_service.get_user_attribute:ユーザーを検索して email を取得する("cognito-idp", admin_get_user)
  3. email_send_service.send_email:ユーザーにメールを送信する("ses", send_email)

Cognito でユーザー検索が一番しんどかったです。こちらのコードで落ち着きましたが、かなり遠回りをしました。

4. S3 に画像をアップロード+ Cognito のユーザー情報を変更+不要な画像は適宜削除する

プロフィールアイコンを S3 にアップロードし、登録し、必要に応じて S3 のデータを削除するようにしました。

  1. フロントエンドで選択した画像を file オブジェクトとしてバックエンドに送信
  2. file_service.upload_s3_file_image:S3 に保存("s3", upload_fileobj)
  3. user_service.get_user_attribute:picture 属性を呼び出し、中身があれば(一度でもプロフィールアイコンアイコンを登録していれば)その値を保管("cognito-idp", get_user_attributes)
  4. user_service.update_user_attribute:picture 属性を更新("cognito-idp", admin_update_user_attributes)
  5. file_service.delete_s3_file:3 に値があれば、該当するファイルを S3 から削除("s3", delete_object)
  6. 完了したとし、フロントエンドに 200 を返す
  7. フロントエンドで revalidate を行い、描画を更新

知ったこと

~/.aws/credentials の居場所や中身の役割

$ aws configure

以上を用いて key, region, output を設定したはいいものの、ファイルがどこに設置されたのか見当たらず。先頭の ~ を見落としていてずっとプロジェクトのディレクトリ内で探していました…。 ~ だいじ。(これなんと呼ぶんだろう)
ここで設定した二種類の key で IAM ユーザーにアクセスし、 boto3 で用いるみたいです。

接続したい AWS のサービス毎に IAM ユーザーを作成+ポリシーをアタッチし、権限を付与する

セキュリティの観点から、サービス毎にアクセスキーを発行するユーザーを分散させることが推奨されていますね。今回はデモ開発だったのでここまでしなくともよかったかもしれませんが、学習のために倣いました。

  1. IAM ユーザーを作成する
  2. 作成した IAM ユーザーのアクセスキーおよびシークレットアクセスキーを発行する
  3. ポリシーを作成し、サービス毎に権限を与える(コード挿入する)
    ここで権限を与えたものに限り、 chalice 上で呼び出すことができるようです(後述1)。いったんすべて * でやってしまいました。
  4. ポリシーを IAM ユーザーにアタッチする
    ポリシーがアタッチされた IAM ユーザーに限り、2 で作成したキーを Boto3 (後述2)で設定することで、 AWS のサービスにアクセス可能になります。

サービス毎に違いはあるのでしょうが、だいたいはこんな流れだと理解しました。

Chalice から AWS に接続するには、 boto3 ライブラリを用いる

以下のように用いるサービス毎に key とともに定義しておき、呼び出しやすくしました。 .aws/credential に保管するより楽だと気づくのにかなり時間がかかってしまいました。

Boto3 を用いた AWS のサービスに接続する関数

環境変数として保管してあるのは、 IAM で発行した アクセスキーとシークレットアクセスキーです。前述の通り、サービス毎にユーザーを作成したので、全て値が異なります。

chalicelib/routers/service_register.py
import boto3

s3 = boto3.client(
  "s3",
  aws_access_key_id=os.environ["AWS_S3_ACCESS_KEY"],
  aws_secret_access_key=os.environ["AWS_S3_SECRET_ACCESS_KEY"],
  region_name=os.environ["AWS_PROJECT_REGION"]
)

ses = boto3.client(
  "ses",
  aws_access_key_id=os.environ["AWS_SES_ACCESS_KEY"],
  aws_secret_access_key=os.environ["AWS_SES_SECRET_ACCESS_KEY"],
  region_name=os.environ["AWS_PROJECT_REGION"]
)

cognito = boto3.client(
  "cognito-idp",
  aws_access_key_id=os.environ["AWS_COGNITO_ACCESS_KEY"],
  aws_secret_access_key=os.environ["AWS_COGNITO_SECRET_ACCESS_KEY"],
  region_name=os.environ["AWS_PROJECT_REGION"]
)

# ...

作っておいた関数を使って実際に操作を行う

AWS 上で IAM ユーザーにポリシーがアタッチされていると、chalice 上で以下のように関数を呼び出せます。先ほどは * で権限をまとめて与えてしまいましたが、 upload_fileobjsend_email 等、個別で与えることもできるようです。より厳重に管理できますね。

chalicelib/routers/service_register.py
s3.upload_fileobj(
  ...
)

ses.send_email(
  ...
)

cognito.admin_get_user(
  ...
)

# ...

こけたこと

ポリシーの ActionResource は配列で指定する

文字列だった ActionResource を配列に修正すると動いたことが何度かあったので、そういうものなんだろうなとふんわり理解しました。

{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Action": 
          [
              "*"
          ],
        "Resource": 
          [
              "*"
          ]
      }
    ]
}

S3 で画像ファイルをアップロードする時は file ではなく fileobj を用いる

これも詳しい理由は調べていないのですが、こっちだと動きました。

# エラーが出てしまう
s3.upload_file(
  ...
)

# こっちだとうごく
s3.upload_fileobj(
  ...
)

S3 に置いた画像 URL は開くと勝手にダウンロードされてしまうので、アップロード時に ExtraArgs を指定する

Boto3 からアクセスする際に ExtraArgs を指定することで、初めて URL から Web 上で画像を読み込めるようです。そっちがデフォルトではないんだ…

s3.upload_fileobj(
  ...,
  ExtraArgs={"ContentType": "image/jpeg"}
)

やり残したこと

デプロイすること

試行錯誤しましたがもう諦めかけています。インフラってなんでこんなにややこしいんだ

https://zenn.dev/poetrainy/scraps/cc2bcd6d21d6b4

感想

「バックエンド も DB も AWS もガチでマジでなんも分からん」から、「2%くらいは理解した」まで進化しました。ゼロを脱するのは大変ですが、何事もやってみるものですね。
たぶんこれからもフロントで食っていきます。

Discussion