前書き
こんにちは、インフラエンジニアの小野島です!
Amplify Gen2 のキャッチアップがてら以下のようなアプリケーションを作ってみます
- Google ログイン機能がある
- 画像をアップロードできる
- ユーザーごとに自分の画像の管理ができる
今回作成するのは基本的な要素だけですが、自作の画像を公開できるようなプラットフォームを意識して作業を進めています
仮のサービス名は mypic としています
この記事は作業ログ形式の文章となっております、一緒に作業している感覚でお読みください
Amplify のセットアップ
基本的に https://docs.amplify.aws/vue/start/quickstart/ の通りに実行
Amplify コンソールから GitHub リポジトリを作成
AWS が公開している to-do リスト作成アプリケーションのサンプルリポジトリをもとにリポジトリが作成されます
検証段階なので Private に設定
リポジトリ検出のために GitHub App と Amplify の連携を許可します
Amplify 向けに構築されたデプロイ対象リポジトリを選択します
他のアプリケーションや Terraform, Ansible と一緒に管理する場合はモノレポを選択することもできるようです
その他設定は今回デフォルトのままとしデプロイ実施
Amplify プロジェクトが作成され初回デプロイが走ります
ドメインが発行されるのでデプロイ途中でアクセスすると、この段階ではまだ簡素なデザインです
6分ほど待ってデプロイ完了
いい感じのデザインになり、to-do リスト作成機能が実装されていました
言うまでもなくインフラ構築はセットアップに埋め込まれており、インフラを意識しないコンセプトを感じます
作成されたリソースを眺めてみる
初回デプロイでは主に以下のリソースが作成されており、これは各サービスのコンソールからも確認できます
- S3 (メタデータ管理用)
- DynamoDB
- Cognito
- Appsync
- Lambda
これらは CloudFormation を使って作成されているので、CloudFormation のコンソールからも確認することができます
(CloudFront 使ってないんだ...... Amplify が内部で CloudFront を使っているようです)
当然ですが DynamoDB の料金体系はオンデマンドになっており、開発から小規模展開までのコスト設計は意識しなくてよさそうだなと感じました
ログイン機能の追加
機能追加の前に、ローカルでの開発には作成したリソースの ID 情報等が必要なので Amplify の出力ファイルをダウンロードして配置します
vim amplify_outputs.json
terraform output
のようなものですね
とりあえずログイン機能がほしいのでドキュメントに沿って以下を実施
npm add @aws-amplify/ui-vue
code -r src/App.vue
src/App.vue
にはドキュメント通り追記
<script>
+ import { Authenticator } from "@aws-amplify/ui-vue";
+ import "@aws-amplify/ui-vue/styles.css";
// ... other imports
</script>
<template>
<main>
+ <authenticator>
+ <template v-slot="{ signOut }">
+ <Todos />
+ <button @click="signOut">Sign Out</button>
+ </template>
+ </authenticator>
</main>
</template>
またドキュメントには書かれていませんでしたが、.vue 拡張子を認識できていなかったので下記の記事を参考に src/shims.d.ts
を追加
動かしてみる
npm run dev
これだけでログイン機能 + ToDo 機能が実装されました
ドキュメントに指示されるがままに push
git commit -am "added authenticator"
git push
すると自動でデプロイが走り、発行済みのドメイン側でも変更が反映されました
CI/CD まで意識せずセットアップされており、コーディングに集中できることが実感できました
sandbox を使ってみる
現状だとローカル操作でもデプロイ済みの DynamoDB にデータが追加されてしまうので、sandbox 機能を使ってみます
sandbox は開発者の個人環境という位置づけで、先ほどデプロイしたリソース一式を複製するイメージです
ただしローカル開発を想定しているようで、結果的にドメインは発行されませんでした
AWS の認証情報が必要になる(エラーが出た)ので追加します
aws configure
早速サンドボックスを起動
npx ampx sandbox
ローカルのコンソールに進捗が表示され、しばらく待つと追加の DynamoDB 等が作成されました
サンドボックスの管理はコンソールから実施できます
ただこれ以上の情報はないので、sandbox 名をローカルで入力しておけると良いなと感じました
(デフォルトでマシン名が入るようだが、わかりづらい)
ドキュメント によると --identifier
で名前を付けられるようなので、運用する際には指定してみようと思います
Google ログインの追加
実用的なアプリケーションを意識すると SNS ログイン機能はほしいところです
以下ドキュメントに沿って Google 認証を実装してみます
※Google の OAuth client ID 取得手順は割愛します
email: true
は無いと後々エラーになるので残しつつ、Google 認証に関する記述を追加
vim amplicy/auth/resource.ts
export const auth = defineAuth({
loginWith: {
email: true,
+ externalProviders: {
+ google: {
+ clientId: secret('GOOGLE_CLIENT_ID'),
+ clientSecret: secret('GOOGLE_CLIENT_SECRET'),
+ scopes: ['email'],
+ attributeMapping: {
+ email: 'email'
+ }
+ },
+ callbackUrls: ['http://localhost:5173/', "https://main.d1q82ko8i5l0ez.amplifyapp.com/"],
+ logoutUrls: ['http://localhost:5173/', "https://main.d1q82ko8i5l0ez.amplifyapp.com/"],
+ }
},
});
なお callbackUrls
と logoutUrls
は sandbox では localhost
だけで十分ですが、push 後に本番ドメインも必要になるので先に書いておきます
sandbox 環境では resource.ts
の変更がリアルタイムで反映されます
助かる反面デプロイに分単位の時間がかかるので、無暗にトリガーしないようにしたほうがよさそうと感じました
またやりたいことに対してはデプロイの時間が長いなとは感じました
(裏の動きまでちゃんと見ていないですが。Terraform でもこのくらいかかるのかな)
amplify_outputs.json
が生成されるのをまって作業再開
secret 関数が取得できるよう変数を登録 (対話式です、read GOOGLE_CLIENT_ID
みたいな感じ)
npx ampx sandbox secret set GOOGLE_CLIENT_ID
npx ampx sandbox secret set GOOGLE_CLIENT_SECRET
もちろんこれも AWS のリソース (SSM Parameter Store) に保存されます
一通り準備できたので起動
npm run dev
難なく Google 認証ボタンが追加されました
ドメインの指定でハマる
Google アカウントでのログインテストをしようとしたところ、"different origin" と怒られてしまいました
これは VSCode の Your service runnning on port **** is available
ポップアップにつられて Open in Browser
ボタンを押すと localhost
ではなく 127.0.0.1
を使って接続するためでした
これだけのことで小一時間悩みました
画像のアップロード機能を追加する
今回は mypic というサービスを想定しているので、まずは画像アップロード機能を追加してみます
まずはストレージを追加します
例によってドキュメントに沿って実施
追加リソースが必要なので storage/resource.ts
に追加します
mkdir amplify/storage
vim amplify/storage/storage.ts
import { defineStorage } from '@aws-amplify/backend';
export const storage = defineStorage({
name: 'amplifyTeamDrive',
access: (allow) => ({
'profile-pictures/{entity_id}/*': [
allow.guest.to(['read']),
allow.entity('identity').to(['read', 'write', 'delete'])
],
'picture-submissions/*': [
allow.authenticated.to(['read','write']),
allow.guest.to(['read', 'write'])
],
})
});
ここはドキュメントをそのままコピペしてしまいます
コードの通り、path に応じて権限が設定されているのがわかります
なお sandbox で実行しているので push はスキップします
当然ですがこの権限は S3 側には設定されていません
ちょっと本筋と逸れますが、S3 のバケットポリシーを見てみましょう
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Principal": {
"AWS": "*"
},
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::amplify-amplifyvuetemplat-amplifyteamdrivebucket28-ABCDefgh1234",
"arn:aws:s3:::amplify-amplifyvuetemplat-amplifyteamdrivebucket28-ABCDefgh1234/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
},
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/amplify-amplifyvuetemplat-CustomS3AutoDeleteObjects-wxyz1234ABCD"
},
"Action": [
"s3:PutBucketPolicy",
"s3:GetBucket*",
"s3:List*",
"s3:DeleteObject*"
],
"Resource": [
"arn:aws:s3:::amplify-amplifyvuetemplat-amplifyteamdrivebucket28-ABCDefgh1234",
"arn:aws:s3:::amplify-amplifyvuetemplat-amplifyteamdrivebucket28-ABCDefgh1234/*"
]
}
]
}
乱数乱文字列は上書きしましたが、デフォルトの Amplify ロールからの操作権限しかないことがわかります
該当する IAM Role のポリシーを見てみましょう
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "s3:GetObject",
"Resource": [
"arn:aws:s3:::amplify-d1q82ko8i5l0ez-ma-amplifyteamdrivebucket28-kuud4recn5ce/profile-pictures/${cognito-identity.amazonaws.com:sub}/*",
"arn:aws:s3:::amplify-d1q82ko8i5l0ez-ma-amplifyteamdrivebucket28-kuud4recn5ce/picture-submissions/*"
],
"Effect": "Allow"
},
{
"Condition": {
"StringLike": {
"s3:prefix": [
"profile-pictures/${cognito-identity.amazonaws.com:sub}/*",
"profile-pictures/${cognito-identity.amazonaws.com:sub}/",
"picture-submissions/*",
"picture-submissions/"
]
}
},
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::amplify-d1q82ko8i5l0ez-ma-amplifyteamdrivebucket28-kuud4recn5ce",
"Effect": "Allow"
},
{
"Action": "s3:PutObject",
"Resource": [
"arn:aws:s3:::amplify-d1q82ko8i5l0ez-ma-amplifyteamdrivebucket28-kuud4recn5ce/profile-pictures/${cognito-identity.amazonaws.com:sub}/*",
"arn:aws:s3:::amplify-d1q82ko8i5l0ez-ma-amplifyteamdrivebucket28-kuud4recn5ce/picture-submissions/*"
],
"Effect": "Allow"
},
{
"Action": "s3:DeleteObject",
"Resource": "arn:aws:s3:::amplify-d1q82ko8i5l0ez-ma-amplifyteamdrivebucket28-kuud4recn5ce/profile-pictures/${cognito-identity.amazonaws.com:sub}/*",
"Effect": "Allow"
}
]
}
権限の制御は IAM Role で実施していることがわかりました
本筋に戻りましょう、backend.ts
で import を追加します
vim amplify/backend.ts
import { data } from './data/resource';
+import { storage } from './storage/resource';
defineBackend({
auth,
data,
+ storage,
});
これで sandbox のデプロイが完了したら、ローカル開発のストレージは作成完了です
実際の機能をドキュメントに沿って実装していきます
vim src/components/ImageUpload.vue
そのままだと動かないので最低限動くようにコードを補完します
amplify/storage/resource.ts
で権限を分けているので、今回はゲストが読み込みのみ可能な path を選択します
<template>
<div class="file-uploader">
<input type="file" id="file" />
<button id="upload">Upload</button>
</div>
</template>
<script>
import { onMounted } from "vue";
import { uploadData } from "aws-amplify/storage";
export default {
name: "FileUpload",
setup() {
onMounted(() => {
const file = document.getElementById("file");
const upload = document.getElementById("upload");
upload.addEventListener("click", () => {
const fileReader = new FileReader();
fileReader.readAsArrayBuffer(file.files[0]);
fileReader.onload = async (event) => {
console.log("Complete File read successfully!", event.target.result);
try {
await uploadData({
data: event.target.result,
path: ({identityId}) => `profile-pictures/${identityId}/${file.name}`,
});
} catch (e) {
console.log("error", e);
}
};
});
});
},
};
</script>
App.vue で取り込みます
vim src/App.vue
<template v-slot="{ signOut }">
<Todos />
+ <ImageUpload />
<button @click="signOut">Sign Out</button>
</template>
スタイルは崩れたままですが機能は実装できていそうです
確認も兼ねて画像表示機能を追加します
自分のアップロードした画像を取得してみる
画像を取得する機能をドキュメントに沿って実装していきます
これも Get Started と違ってコピペでは動かないので頑張って最低限のコードを書きます
権限制御は後回しにして、とりあえず自分のアップロードした画像を表示するように実装します
<template>
<div class="image-gallery">
<h2>Uploaded Images</h2>
<div v-for="(image, index) in images" :key="index">
<img :src="image.url" />
</div>
</div>
</template>
<script>
import { list, downloadData } from "aws-amplify/storage";
export default {
name: "ImageGallery",
data() {
return {
images: [],
};
},
async mounted() {
try {
// 画像リストを取得
const fileListResponse = await list({
path: ({ identityId }) => `profile-pictures/${identityId}/`,
});
// list の戻り値から items を取得
const files = fileListResponse.items || [];
console.log("File list: ", files);
// 各画像の署名付き URL を取得
const imagePromises = files.map(async (file) => {
const downloadResult = await downloadData({
path: file.path,
}).result;
const blob = await downloadResult.body.blob();
const imageUrl = URL.createObjectURL(blob);
return {
url: imageUrl,
};
});
// 非同期処理を全て解決
this.images = await Promise.all(imagePromises);
console.log("Images with signed URLs: ", this.images);
} catch (error) {
console.error("Error fetching images: ", error);
}
},
};
</script>
<style scoped>
.image-gallery {
text-align: center;
margin-top: 20px;
}
.image-gallery img {
max-width: 300px;
height: auto;
border: 1px solid #ccc;
border-radius: 5px;
}
</style>
getUrl
ではなく downloadData
を使うことで amplify がブラウザへキャッシュを指示してくれるようにします
なおドキュメントにも記載のある通り、CDN 側での S3 コンテンツキャッシュは未実装とのことでした
Image compression or CloudFront CDN caching for your S3 buckets is not yet possible.
App.vue で取り込みます
vim src/App.vue
<ImageUpload />
+ <MyPage />
<button @click="signOut">Sign Out</button>
できました
本番へ反映してみる
できたコードを push します
git commit -a
git push
Amplify 本番稼働ブランチでデプロイが進行します
Google 認証キーが無いと怒られてしまいました
本番用の認証情報を追加で作成して、ホスティングのシークレットに登録します
手順はドキュメントの https://docs.amplify.aws/vue/deploy-and-host/fullstack-branching/secrets-and-vars/#set-secrets を参考
再デプロイ
デプロイが完了し、cognito のドメインが発行されているので Google の OAuth クライアントへ登録
本番ドメインでもアップロードと表示ができました!
キャッシュされているか確認してみる
キャッシュはデフォルトで以下のドキュメントの通りになっているそうです
S3 コンテンツは先述の通り CDN キャッシュされませんが、静的アセットは1年とのこと
どのコンテンツがキャッシュされているか確認してみます
ログイン後のページをキャッシュクリアして更新したところ、画像の通りディスクキャッシュがされていませんでした
一方でこのログイン後のページをリロードした際には downloadData
を使っているのでディスクキャッシュを使用してくれるようです
繰り返しになりますが、Amplify Gen2 ではコンテンツの CDN キャッシュを提供していないとのことです
容量の大きいコンテンツを扱う場合は CDN キャッシュだけ別の方法を検討したほうがいいかもしれません
(シンプルに CDN + S3 を作るだけで十分だと思います)
DynamoDB のテーブルを見てみる
DynamoDB の設計は今回触っていない amplify/data/resource.ts
に定義されています
import { type ClientSchema, a, defineData } from "@aws-amplify/backend";
const schema = a.schema({
Todo: a
.model({
content: a.string(),
})
.authorization((allow) => [allow.publicApiKey()]),
});
content 列があるだけのシンプルなテーブルです
なおパーティションキーはデフォルトで id
となっており、identifier()
メソッドで定義できるようです
ただし権限が publicApiKey()
となっています
ドキュメントでも
(only for getting started)
とあるように、適切な権限ではなようです
またこの resource.ts
は AppSync の定義も兼ねているので、DynamoDB と Appsync 双方の認証に関する挙動はドキュメントを読み込んだ方がいいなと感じました
SRE な感想
インフラエンジニアとして SRE に取り組んでいるので、結びとしてその視点からの感想をいくつか
まず小規模なサービスをすぐ展開したい、というニーズには最高だと感じました
一方でアクセスや機能追加が増えてくるとクォータやコスト、負荷性能の問題は避けられないでしょう
ぱっと思いつくだけでも負荷周りでは以下事項を要検討かなと思います
- S3 コンテンツの CDN キャッシュ
- DynamoDB のインデックス設計
- スケーラブルなサービスの暖気 (ちょうどこんなアップデートが)
- DynamoDB をプロビジョニングへ変更
- AppSync をプロビジョニングへ変更
実際にサービスとして運用するなら以下も検討したいところです
- S3 のバージョニング
- DB のリカバリ戦略
- NoSQL から RDBMS への移行
- 各種バックアップ
- DDoS 対策
ご清覧ありがとうございました!
免責事項
-
この記事の内容は個人の見解であり、所属する組織の意見を代表するものではありません。
-
一部のコードの出力に生成 AI を使用しています