Published on

aws覚書

Authors

amplify から始めた aws のいろいろ

仕事でもちゃんと使ってみようと思い、構成を amplify + Next.js にしてみました。
作るものはチャットアプリです。

躓いたところ

amplify init コマンドからセッティングをしていったんですが、体系的に解説しているものが見つからなくてこんなところで手間取ってしまいました。
基本的にはせっかく便利な作りになっているので、細かい設定をしていかずに amplify コマンドの対話と aws-amplify パッケージでの実装にしたかったです。

  • appsync の認証設定
  • cognito でサインアップ時に自動的にグループ追加
  • s3 のファイル操作

appsync の認証設定

appsync で GQL を使いたくて、amplify add apiをしました。
コマンドを入れると下記のように対話式で設定が進みます。

? Select from one of the below mentioned services: GraphQL
 //ここはGraphQLを選択

? Here is the GraphQL API that we will create. Select a setting to edit or continue
  Name: myapp
❯ Authorization modes: API key (default, expiration time: 7 days from now)
  Conflict detection (required for DataStore): Disabled
  Continue
  //ここでAuthorization modesにカーソルを合わせて選択肢を表示させる

? Here is the GraphQL API that we will create. Select a setting to edit or continue Authorization modes: API key (default, expiration time: 7 days from now)
? Choose the default authorization type for the API (Use arrow keys)
  API key
❯ Amazon Cognito User Pool
  IAM
  OpenID Connect
  Lambda
  //Cognitoでサインインしたユーザーにだけ使わせたいのでAmazon Cognito User Poolを選択

  ? Choose the default authorization type for the API Amazon Cognito User Pool Using service: Cognito, provided by: awscloudformation

  The current configured provider is Amazon Cognito.

  //ここからCognitoの設定をするための対話に切り替わる

 Do you want to use the default authentication and security configuration? (Use arrow keys)
❯ Default configuration
  Default configuration with Social Provider (Federation)
  Manual configuration
  I want to learn more.

 Do you want to use the default authentication and security configuration? Default configuration
 Warning: you will not be able to edit these selections.
 How do you want users to be able to sign in?
❯ Username
  Email
  Phone Number
  Email or Phone Number
  I want to learn more.
  //Usernameで

  Do you want to configure advanced settings? (Use arrow keys)
  No, I am done.
❯ Yes, I want to make some additional changes.

  //コピーし忘れちゃったけどこの次でカスタム属性とか追加できそうだった。。特に変更せず。
  //enter押すと次の質問へ

 Do you want to enable any of the following capabilities? (Press <space> to select, <a> to toggle all, <i> to invert selection)
  ◯ Add Google reCaptcha Challenge
  ◯ Email Verification Link with Redirect
❯ ◯ Add User to Group
  ◯ Email Domain Filtering (denylist)
  ◯ Email Domain Filtering (allowlist)
  ◯ Custom Auth Challenge Flow (basic scaffolding - not for production)
  ◯ Override ID Token Claims

  //ここでAdd User to Groupを選択。最初はこの選択をしなかったのでユーザーのグループ追加で詰まっていました
  //他のオプションについても調べて勉強しなきゃなぁ

 ? Enter the name of the group to which users will be added. ›
  //任意のグループ名を入力してenter
  //ここまで入力すると設定ファイルが作られる

Next steps:
Check out sample function code generated in <project-dir>/amplify/backend/function/myapp/src
"amplify function build" builds all of your functions currently in the project
"amplify mock function <functionName>" runs your function locally
To access AWS resources outside of this Amplify app, edit the /Users/noine/Desktop/test/my-app/amplify/backend/function/myapp/custom-policies.json
"amplify push" builds all of your local backend resources and provisions them in the cloud
"amplify publish" builds all of your local backend and front-end resources (if you added hosting category) and provisions them in the cloud

Successfully added the Lambda function locally
? Do you want to edit your add-to-group function now? (Y/n)
  //ここでAdd User to Groupを選択したときのLambda関数が作られるので編集したければY
  //見てもまだうっすら何となくしか分からない。envはLambdaのコンソールから確認できる

Some next steps:
"amplify push" will build all your local backend resources and provision it in the cloud
"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud

? Configure additional auth types? (y/N)
  //他にもAPIキー認証とか追加したければY。今回はN

  //ここでGraphQL APIの設定に戻ってくる
? Here is the GraphQL API that we will create. Select a setting to edit or continue (Use arrow keys)
  Name: myapp
  Authorization modes: Amazon Cognito User Pool (default)
  Conflict detection (required for DataStore): Disabled
❯ Continue
  //Conflict detectionの設定はしなかったのでContinue。ここも何がどうなるか調べたほうが良さそう

? Choose a schema template: (Use arrow keys)
❯ Single object with fields (e.g., “Todo” with ID, name, description)
  One-to-many relationship (e.g., “Blogs” with “Posts” and “Comments”)
  Objects with fine-grained access control (e.g., a project management app with owner-based authorization)
  Blank Schema
  //schemanのテンプレートを作ってくれる。後々作っていくので気にしなくていいかもだけど、テンプレートは良い参考になります。
  //今回はSingleで作成

⚠️  WARNING: Some types do not have authorization rules configured. That means all create, read, update, and delete operations are denied on these types:
         - Todo
Learn more about "@auth" authorization rules here: https://docs.amplify.aws/cli/graphql/authorization-rules
✅ GraphQL schema compiled successfully.

  Edit your schema at /my-app/amplify/backend/api/myapp/schema.graphql or place .graphql files in a directory at /my-app/amplify/backend/api/myapp/schema
? Do you want to edit the schema now? (Y/n)  //こんな感じでschemaファイルが作られる。編集したければY

Some next steps:
"amplify push" will build all your local backend resources and provision it in the cloud
"amplify publish" will build all your local backend and frontend resources (if you have hosting category added) and provision it in the cloud
  //amplify add apiの対話終了

前半のChoose the default authorization type for the API (Use arrow keys)で Cognito User Pool 認証を選択すると Cognito の設定が始まります。
最初は何も分からず色んなサイトを参考に API キーを設定しちゃってました。
そこから Cognito 認証への変更をしたかったのですが、appsync コンソール画面の設定からではうまくいかなかったです。
恐らくプロジェクトディレクトリの amplify ディレクトリ内にある設定が優先されていたのかと思います。

amplify add apiamplify api updateコマンドを使ってChoose the default authorization type for the API (Use arrow keys)の項目で Cognito User Pool 認証を選択するのがシンプルにうまくいきました。

また、スキーマの認証設定で全体公開(allow:public)にすると API キーでの認証が必要になってしまうのが落とし穴でした。
どこかドキュメントに書いてあるのかな。。
API キーには有効期限があって更新が面倒なので使いたくない!

なので、スキーマの設定はこんな感じにしました。

type Message
  @model
  @auth(rules: [{ allow: owner }, { allow: groups, groups: ["groupName"], operations: [read] }]) {
  id: ID!
  uuid: String!
  text: String!
}

扱うデータによって operations は変えていくかなと。

cognito でサインアップ時に自動的にグループ追加

四苦八苦したあとにこの記事を書いているので、前述のamplify add apiで cognito 認証も設定しちゃいましたが、最初は先にamplify add authで Cognito 認証を作ってから add api しました。
なので自動的にグループに追加される方法もわからず、自分で Lambda 関数作ってみたりしたのですがうまくいきませんでした。。
エラーなどから Lambda 関数自体の権限の問題ぽかったんですが解決できなかったので、amplify auth updateコマンドから自動的にグループ追加されるように設定しました。

対話でどんなことを聞かれて、どう答えるとどうなるかまとまっていないのかー!?
まとまってたら教えて下さい

amplify auth update

Using service: Cognito, provided by: awscloudformation
 What do you want to do?
  Apply default configuration with Social Provider (Federation)
❯ Walkthrough all the auth configurations
  Create or update Cognito user pool groups
  Create or update Admin queries API

  //ここで改めてCognitoの設定をしなおしていきました。

  Do you want to enable any of the following capabilities? (Press <space> to select, <a> to toggle all, <i> to i
nvert selection)
 ◯ Add Google reCaptcha Challenge
 ◯ Email Verification Link with Redirect
❯◉ Add User to Group
 ◯ Email Domain Filtering (denylist)
 ◯ Email Domain Filtering (allowlist)
 ◯ Custom Auth Challenge Flow (basic scaffolding - not for production)
 ◯ Override ID Token Claims

  //前述のapi作成時のように進めていくと上記の質問がでるので、Add User to Groupを選択して進めていくとLambda関数が作られて自動的にグループに追加されるようになります

s3 のファイル操作

これも色々調べたのですが、色んな書き方があってどうすればいいのか分かりにくかったです。
やりたいことは、

  • Cognito 認証ユーザーのみアクセスできる
  • ファイルのアップロード
  • ファイルの表示
  • ファイルの削除

といった基本的なこと。

Cognito 認証ユーザーのみアクセス

こちらはamplify add storageで s3 を追加するときの対話で設定できました。

amplify add storage

? Select from one of the below mentioned services:
❯ Content (Images, audio, video, etc.)
  NoSQL Database

? Provide a friendly name for your resource that will be used to label this category in the project: ›
 //わかりやすい名前つけておくと後で便利

? Provide bucket name: ›
  //これもわかりやすい名前つけておくと後で便利

? Restrict access by? …  (Use arrow keys or type to filter)
  Auth/Guest Users
❯ Individual Groups
  Both
  Learn more
  //ここでIndividual Groupsを選択!!
  //Cognito認証はAuthだと思っていてはまってしまいました

? Select groups: …  (Use arrow keys or type to filter)
❯● group
(Use <space> to select, <ctrl + a> to toggle all)
  //アクセス可能なグループを設定

? What kind of access do you want for group users? …  (Use arrow keys or type to filter)
❯● create/update
read
 ● delete
(Use <space> to select, <ctrl + a> to toggle all)
  //そのグループがどんな操作ができるか設定する
  //今回は全部できるようにした

? Do you want to add a Lambda Trigger for your S3 Bucket? (y/N)  //Lambdaトリガーを追加できるっぽいけど分からないのでN

⚠️ Current auth configuration is: userPoolOnly, but identityPoolAndUserPool was required.
✅ Successfully updated auth resource locally.
✅ Successfully added resource locally

⚠️ If a user is part of a user pool group, run "amplify update storage" to enable IAM group policies for CRUD operations
✅ Some next steps:
"amplify push" builds all of your local backend resources and provisions them in the cloud
"amplify publish" builds all of your local backend and front-end resources (if you added hosting category) and provisions them in the cloud

  //これでamplify pushするとs3が作られる

ファイルのアップロード、表示、削除

実際の実装は条件がいくつかあって長くなってしまったので抜粋です。

ファイル操作するコンポーネント例
function S3Image({ user }: WithAuthenticatorProps) {
  // Cognitoユーザーのuuidを取得
  const uuid = user?.attributes?.sub

  const [file, setFile] = useState<File>();
  const [filename, setFilename] = useState<string>("");
  const [key, setKey] = useState("");
  const [imagePath, setImagePath] = useState("");

  //<input type="file">でファイルを選択したときの挙動
  useEffect(() => {
    const handleFileSelect: any = (event: ChangeEvent<HTMLInputElement>) => {
      const selectedFile = event.target as HTMLInputElement
      const selectedFiles = (selectedFile.files as FileList)[0]

      // 一回選択したあとにキャンセルするとnullになってしまうのでこの書き方
      if (selectedFiles && selectedFiles.length !== 0) {
        // ファイル自体をセット
        setFile(selectedFiles)

        // ファイル名をセット
        setFilename(selectedFiles.name)

        // keyをセット
        // s3のアップロード先URLになるのでCognitoユーザーのuuidごとに分けた
        // このキーをappsyncで保存しておくと表示したり操作するときに便利
        setKey(`images/${uuid}/${selectedFiles.name}`)
      } else {
        // 選択をキャンセルしたらリセット
        setFilename('')
      }
    }

    // useEffectの中からfunctionにアクセスするため
    // input要素の変更を監視
    const inputElement = document.getElementById('fileInput') as HTMLInputElement
    inputElement.addEventListener('change', handleFileSelect)

    return () => {
      // input要素の変更を監視するのをやめる
      inputElement.removeEventListener('change', handleFileSelect)
    }
  }, [uuid, key, file])

  //ファイルをアップロードする関数
  async function handleImageUpload() {
    // ここでappsyncへ登録するクエリを作って、キーを登録しておくと便利

    try {
    // s3にファイルのアップロード
    const { type: mimeType } = file

    await Storage.put(key, file, {
      contentType: mimeType,
    })
    } catch (err) {
      console.log('error: ', err)
    }
  }

  //ファイル(今回は画像)を表示する
  async function imageDisplay(){
    ログインした自分の画像を表示したかったので
    const variables: ListUserImagesQueryVariables = {
      filter: {
        uuid: {
          contains: uuid,
        },
      },
    };
    const image: any = await API.graphql(graphqlOperation(listUserImages, variables));

    if ("data" in image) {
      const imageList = image.data as ListUserImagesQuery;
      const imageKey = imageList.listUserImages?.items[0]?.image;
      // ここにキーを入れているという仮定
      try {
          if (imageKey) {
            // キーを入れてURLを取得
            // このURLは閲覧権限つきの長いURLになっている
            const imageData = await Storage.get(imageKey);
            setImagePath(imageData);
          }
      } catch (err) {
        console.log("error: ", err);
      }
    }
  }

  useEffect(() => {
    // なにか処理書いたり、アップロードしたものsubscriptionでリアルタイム更新で表示するならここに書いたり

    imageDisplay()
  })

  async function deleteFile(){
    //Storage.list()で引数に入れたキーに該当するファイルのリストが取得できる

    // uuidで検索してs3内の画像を取得
    // 下記はディレクトリ内を空にしたかったのでこうした
    // 一つだけ消すなら個別にkeyを入れて消す
    Storage.list(`images/${uuid}/`)
      .then(({ results }) => {
        results.forEach((e) => {
          if (e.key) {
            // 画像の削除
            Storage.remove(e.key);
          }
        });
      })
      .catch((err) => console.log(err));
  }

  return (
    <div>
      <input type="file" id="fileInput" accept="image/*" />
        <button onClick={handleImageUpload}>Upload</button>
        {file && <p>Selected file: {filename}</p>}
        <div>
          <h2>画像ファイルの表示</h2>
          <img src={imagePath} />
        </div>
      <button onClick={deleteFile}>Delete</button>
    </div>
  );

}

export default withAuthenticator(S3Image)

こんな感じです。
実際のコードから抜粋なので、このままコピペしてもエラー起こします。
import 入れていないですし。。
アップロードしたりするときの

await Storage.put(key, file, {
  contentType: mimeType,
})

この key をどうやって作ればいいか試行錯誤してました。
だってどこにも書いてなかったんだもん。
見落としてるだけかもですが。

ブラウザで s3 のコンソールに入って、バケット → 対象の「オブジェクトの概要」を見ると、キーという項目があります。
それを見つけて、public 以下の URL を入れればいいのねってなりました。
なのでどんなディレクトリ構造でファイルを扱うかで書き方が変わってきます。
改めて実装したコードを見直すともっときれいに書けそうです。

長くなってしまいましたが、自分の覚書としてざっくり書きました。
困ってる人のヒントになればありがたいです。
それでは〜