AWS SDKでよく使う記述についてまとめてみた
はじめに
使用例ではNext.js、TypeScriptの場合によく使用する記述をまとめています。
S3とDynamoDBについて記述していますが、今後他のサービスについても追記する予定です。
AWSの認証情報を安全に管理する
AWSの認証情報、特にアクセスキーの安全を確保するためには、SDKの使用は必ずサーバーサイドで行ってください。
もしクライアントサイドでSDKを利用したい場合は、Next.jsのAPIルートを通じて操作を行うようにしてください。クライアントサイドで直接SDKを使用してしまうと、ブラウザのネットワークタブから認証情報が確認されてしまい、情報が漏洩するリスクがあります。
特にRequest Headers > AuthorizationヘッダーにはAWSアクセスキーが含まれています。(黒塗りの部分)
Next.jsのAPIルートを利用する際、アクセスキーが漏洩しない理由は、APIルートがサーバーサイドで実行されるためです。この処理はクライアント(ユーザーのブラウザ)とは独立してサーバー上で完了し、ユーザーのブラウザには処理結果のみが送信されます。
このようにすることで、機密情報の安全を確保しつつ、クライアントからのリクエストに対して必要なデータだけを安全に提供することができます。
SDKとは
SDK(Software Development Kit)は、AWSのサービスをプログラムから利用するためのツールのことで、プログラミング言語で記述されます。
AWS SDKのインストール
複数のAWSサービスを使用する場合
aws-sdk
はAWSの全サービスにアクセスするための包括的なライブラリです。複数のAWSサービスを使用し、プロジェクトがAWSの幅広い機能にアクセスする必要がある場合は、aws-sdk
を使用した方が良いと思います。
npm install aws-sdk
サービスに特化したライブラリ
必要なサービスに特化したライブラリを使用することで、不要な機能を含まずにライブラリを軽量化することができます。
npm install @aws-sdk/client-<サービス名>
↓DynamoDBのライブラリ
DynamoDBとS3の場合
aws-sdk
をインストールしている場合は気にしなくて良いです。
DynamoDBとS3の操作に特化したライブラリとして、@aws-sdk/lib-<サービス名>
という形式のパッケージが提供されています。
これらのライブラリを使用すると、@aws-sdk/client-<サービス名>
よりもシンプルに記述することができます。
AWS認証設定
一度しか記述しない場合はファイルを分けなくても良いのですが、それ以外の場合は、AWS認証設定ファイルを作成しておくと便利です。環境変数は.env
に設定して読み込んでいます。
import AWS from 'aws-sdk';
AWS.config.update({
region: process.env.REGION || '', // DynamoDB のリージョンを指定
accessKeyId: process.env.ACCESS_KEY_ID || '', // IAM ユーザーアクセスキー
secretAccessKey: process.env.SECRET_ACCESS_KEY || '', // IAM ユーザーシークレットアクセスキー
});
export default AWS;
インポート文を記述して使用してください。
import AWS from '../../awsCredential';
リクエスト
GETリクエスト
try {
const response = await axios.get('/api/aws');
console.log('response', response);
console.log('response.data', response.data);
} catch (error) {
console.error('データの取得に失敗しました。:', error);
}
POSTリクエスト
const response = await axios.post('/api/aws', {
artist: artist, // プロパティと変数名
songTitle: songTitle,
});
console.log('response', response);
console.log('response.data', response.data);
} catch (error) {
console.error('ネットワークエラー:', error);
}
};
プロパティと変数の名前が同じなので、オブジェクトリテラルの省略記法を使用すると下記のように記述できます。
const response = await axios.post('/api/aws', {
artist,
songTitle,
});
console.log('response', response);
console.log('response.data', response.data);
} catch (error) {
console.error('ネットワークエラー:', error);
}
};
APIルートの設定
下記は各サービスでのAPIルートの設定です。
S3
取得
getObject
export async function GET() {
// S3クライアントの作成
const s3 = new AWS.S3(); // AWS S3クライアントをインスタンス化
const params = {
Bucket: process.env.S3_BUCKET_NAME || '', // バケット名を環境変数から取得
Key: process.env.FILE_PASS || '', // 取得したいオブジェクトのキー(ファイルパス)を指定
};
try {
const data = await s3.getObject(params).promise();
return NextResponse.json(data);
} catch (error) {
throw error;
}
}
画像や動画を取得する
responseType
をblob
(Binary Large OBjectの略、バイナリデータを表すオブジェクト)オプションを指定してリクエストを送信することで、レスポンスデータをバイナリ形式(Blobオブジェクト)で受け取ることができます。
URL.createObjectURL
メソッドを使用して、受け取ったBlobデータから一時的なURLを生成します。
このURLは、画像や動画などのバイナリデータをブラウザで表示するために使用できます。
ブラウザを閉じるまで 生成されたURL有効でメモリに常駐するため、URL.revokeObjectURL
を使用して削除する必要があります。
const [url, setUrl] = useState('');
try {
const response = await axios.get('/api/aws', {
responseType: 'blob',
});
console.log('response', response);
console.log('response.data', response.data);
const url = URL.createObjectURL(response.data);
setUrl(url);
} catch (error) {
setError('データの取得に失敗しました。');
console.error('Error fetching data:', error);
}
return (
{/* 画像 */}
{url && (
<img
src={url}
alt='S3 Image'
style={{ width: '300px', height: 'auto' }}
/>
)})
{/* 動画 */}
{url && (
<video
src={videourl}
controls
style={{ width: '300px', height: 'auto' }}
/>
)}
data.Body
をバイナリデータを扱うための型であるUint8Array
型として扱うように記述しています。
Content-Type
にバイナリデータのMIMEタイプを設定してください。
export async function GET() {
// S3クライアントの作成
const s3 = new AWS.S3(); // AWS S3クライアントをインスタンス化
const params = {
Bucket: process.env.S3_BUCKET_NAME || '', // バケット名を環境変数から取得
Key: process.env.FILE_PASS || '', // 取得したいオブジェクトのキー(ファイルパス)を指定
};
try {
const data = await s3.getObject(params).promise();
if (data.Body) {
return new NextResponse(data.Body as Uint8Array, {
headers: {
'Content-Type': 'application/octet-stream',
},
});
} else {
return new NextResponse('アイテムが見つかりませんでした', {
status: 404,
});
}
} catch (error) {
console.error('S3からファイルを取得する際にエラーが発生しました:', error);
return new NextResponse(
'S3からファイルを取得する際にエラーが発生しました',
{
status: 500,
}
);
}
}export async function GET() {
// S3クライアントの作成
const s3 = new AWS.S3(); // AWS S3クライアントをインスタンス化
const params = {
Bucket: process.env.S3_BUCKET_NAME || '', // バケット名を環境変数から取得
Key: process.env.FILE_PASS || '', // 取得したいオブジェクトのキー(ファイルパス)を指定
};
try {
const data = await s3.getObject(params).promise();
if (data.Body) {
return new NextResponse(data.Body as Uint8Array, {
headers: {
'Content-Type': data.ContentType || 'application/octet-stream',
},
});
} else {
return new NextResponse('アイテムが見つかりませんでした', {
status: 404,
});
}
} catch (error) {
throw error;
}
}
画像、動画のアップロード
署名付きURLを使用してアップロードする
const [file, setFile] = useState<File | null>(null);
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
let file = null;
if (event.target.files) {
file = event.target.files[0];
}
setFile(file);
};
const uploadFile = async () => {
if (!file) return; // ファイルが選択されていなければ何もしない
try {
const response = await axios.post('/api/aws', {
file,
fileName: file.name,
fileType: file.type,
});
console.log('アップロードが成功しました: ', response);
} catch (error) {
console.error('レスポンスが取得できませんでした', error);
}
};
return (
<div>
<input type='file' onChange={handleFileChange} />
<button onClick={uploadFile}>S3にアップロードする</button>
</div>
);
}
const [file, setFile] = useState<File | null>(null);
のようにnullを使用しない場合は、Fileオブジェクトのプロパティを空にしたものを設定してください。
const [file, setFile] = useState<File>({
name: '',
size: 0,
type: '',
lastModified: Date.now(),
});
サーバーサイドの記述
export async function POST(req: NextRequest) {
// リクエストボディからデータを抽出
const { file, fileName, fileType } = await req.json();
// S3クライアントの初期化
const s3 = new AWS.S3();
const params = {
Bucket: process.env.S3_BUCKET_NAME || '',
Key: fileName,
Expires: 60, // URLの有効期限(秒)
ContentType: fileType,
};
try {
// アップロード用の署名付きURLを生成
const signedUrl = await s3.getSignedUrlPromise('putObject', params);
console.log('生成された署名付きURL:', signedUrl);
await fetch(signedUrl, {
method: 'PUT',
headers: {
'Content-Type': fileType,
},
body: file,
});
return NextResponse.json({
success: true,
message: 'アップロードが成功しました',
file: fileName,
});
} catch (error) {
throw error;
}
}
FormDataを使用する記述
await req.json()
を使用すると、ファイルのバイナリデータではなく、そのメタデータのみが取得できます。
ファイルのバイナリデータを扱うためには、File
オブジェクトをFormData
として送信し、サーバー側で適切に処理する必要があります。
axios.post
を使用する場合、リクエストボディとしてFormData
を送信するためには、axios.post
の第二引数にformData
を直接渡し、ヘッダーの設定を行う必要があります。method
プロパティは不要です。
const [file, setFile] = useState<File | null>(null);
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
let file = null;
if (event.target.files) {
file = event.target.files[0];
}
setFile(file);
};
const uploadFile = async () => {
if (!file) return;
const formData = new FormData();
formData.append('file', file);
formData.append('fileName', file.name);
formData.append('fileType', file.type);
formData.forEach((value, key) => {
console.log('key&value', key, value);
});
try {
const response = await axios.post('/api/aws', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
console.log('アップロードが成功しました: ', response);
} catch (error) {
console.error('レスポンスが取得できませんでした', error);
}
};
return (
<div>
<input type='file' onChange={handleFileChange} />
<button onClick={uploadFile}>S3にアップロードする</button>
</div>
);
}
サーバーサイドの記述
export async function POST(req: NextRequest) {
// formData
const formData = await req.formData();
const file = formData.get('file');
const fileName = formData.get('fileName');
const fileType = formData.get('fileType');
if (!file || !fileName || !fileType || !(file instanceof File)) {
return NextResponse.json(
{ success: false, message: 'ファイルデータが不足しています' },
{ status: 400 }
);
}
// S3クライアントの初期化
const s3 = new AWS.S3();
const params = {
Bucket: process.env.S3_BUCKET_NAME || '',
Key: fileName,
Expires: 60, // URLの有効期限(秒)
ContentType: fileType,
};
try {
// アップロード用の署名付きURLを生成
const signedUrl = await s3.getSignedUrlPromise('putObject', params);
console.log('生成された署名付きURL:', signedUrl);
// fetch
await fetch(signedUrl, {
method: 'PUT',
headers: {
'Content-Type': fileType.toString(),
},
body: file,
});
// axiosを使用する場合
// ファイルデータをArrayBufferとして読み取る
// fetchを使用する方が低レベルの制御が可能になり、バイナリデータを簡単に送信できる
const arrayBuffer = await file.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
// 署名付きURLを使ってS3にファイルをアップロード
await axios.put(signedUrl, buffer, {
headers: {
'Content-Type': file.type,
'Content-Length': buffer.length,
},
});
return NextResponse.json({
success: true,
message: 'アップロードが成功しました',
file: fileName.toString(),
});
} catch (error) {
throw error;
}
}
File() コンストラクター
署名付きURL
有効期限が設定されており、特定のリソースへの一時的なアクセスを提供するために使用されるURLです。
-
期限設定: URLには有効期限が設定されており、その期限が過ぎるとURLは無効になります。期限は生成時に設定され、数秒から数日の範囲で設定可能です。
-
セキュリティ: URLは特定のリソースへのアクセスを許可するために、AWSのアクセスキーIDとシークレットアクセスキーを用いて署名されます。この署名により、URLが改竄された場合に無効となります。
-
直接アクセス: 生成されたURLを通じて、ユーザーまたはシステムは追加の認証なしでリソースにアクセスできます。したがってURLを知っている人は誰でも、指定されたリソースに対してアクセス(読み取りまたは書き込み)が可能になります。
署名付きURLを生成する
getSignedUrlPromiseメソッド
AWS SDK for JavaScript(v2)を使用して、AWS S3の特定のオブジェクトに対して署名付きURLを生成するためのメソッドです。https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#getSignedUrlPromise-property
getSignedUrlメソッド
AWS SDK for JavaScript (v3) では、getSignedUrl
メソッドを使用して署名付きURLを生成します。v3では、@aws-sdk/s3-request-presigner
パッケージを使用して、署名付きURLを生成します。
DynamoDB
下記のデータを使用します。
DynamoDBクライアントの作成
下記を記述してDynamoDBクライアントを作成してください。
const dynamoDB = new AWS.DynamoDB.DocumentClient();
取得
データを取得するために3つの主要な操作が提供されています。
主キーは、プレースホルダーを使用しませんが、主キー以外の属性は属性名を記述する際にプレースホルダーを使用する必要があります。
get
指定された主キーに基づいて、1つのアイテムを取得します。
主に、一意の識別子を持つアイテムを取得するために使用されます。特定のユーザーの情報を取得する場合などです。
GETリクエストの場合
export async function GET() {
// DynamoDBクライアントの作成
const dynamoDB = new AWS.DynamoDB.DocumentClient();
const key = {
artist: 'No One You Know', // パーティションキー
songTitle: 'My Dog Spot', // 設定している場合はソートキー
};
const params = {
TableName: process.env.TABLE_NAME || '',
Key: key,
};
try {
const data = await dynamoDB.query(params).promise();
return NextResponse.json(data);
} catch (error) {
throw error;
}
}
POSTリクエストの場合
Next.jsのサーバーサイドのリクエストとレスポンスを扱うためのモジュールもインポートしてください。
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
// リクエストボディからデータを抽出
const { artist, songTitle } = await req.json();
// DynamoDBクライアントの作成
const dynamoDB = new AWS.DynamoDB.DocumentClient();
const key = { artist, songTitle };
const params = {
TableName: process.env.TABLE_NAME || '',
Key: key,
};
try {
const data = await dynamoDB.get(params).promise();
return NextResponse.json(data);
} catch (error) {
throw error;
}
}
query
指定された主キーに基づいて、複数のアイテムを取得することができます。
export async function GET() {
// DynamoDBクライアントの作成
const dynamoDB = new AWS.DynamoDB.DocumentClient();
const expressionAttributeValues = {
':artistName': 'The Acme Band', // 検索したいアーティスト名
};
const params = {
TableName: process.env.TABLE_NAME || '',
KeyConditionExpression: 'artist = :artistName',
ExpressionAttributeValues: expressionAttributeValues,
};
try {
const data = await dynamoDB.query(params).promise();
return NextResponse.json(data);
} catch (error) {
throw error;
}
}
scan
テーブル内のすべての項目をスキャンし、すべてのアイテムを取得するために使用されます。
scan
でもフィルタリングは可能ですが、テーブル内のすべての項目を読み取り、その後にフィルタリングを適用して条件に一致する項目のみを返します。そのため、フィルタリングする場合はquery
を使用するようにしてください。
export async function GET() {
// DynamoDBクライアントの作成
const dynamoDB = new AWS.DynamoDB.DocumentClient();
const params = {
TableName: process.env.TABLE_NAME || '',
KeyConditionExpression: 'artist = :artistName',
};
try {
const data = await dynamoDB.scan(params).promise();
return NextResponse.json(data);
} catch (error) {
throw error;
}
}
更新
export async function GET() {
// DynamoDBクライアントの作成
const dynamoDB = new AWS.DynamoDB.DocumentClient();
const key = {
artist: 'No One You Know', // パーティションキー
songTitle: 'My Dog Spot', // 設定している場合はソートキー
};
// 更新する属性と値
const Expression = 'set #attribute = :newCategory';
const expressionAttributeValues = {
':newCategory': 'JPOP', // 新しいカテゴリー
};
const expressionAttributeNames = {
'#attribute': 'category', // プレースホルダーに対応する実際の属性名
};
const ConditionExpression =
'attribute_exists(artist) AND attribute_exists(songTitle)';
// クエリの設定
const params = {
TableName: process.env.TABLE_NAME || '',
Key: key,
UpdateExpression: Expression,
ExpressionAttributeValues: expressionAttributeValues,
ExpressionAttributeNames: expressionAttributeNames,
ConditionExpression: ConditionExpression,
ReturnValues: 'ALL_NEW', // オプション 更新した内容をdataとして受け取る際に必要
};
try {
const data = await dynamoDB.update(params).promise();
return NextResponse.json(data);
} catch (error) {
throw error;
}
}
Condition Expressions
条件式を使用して、特定の条件を満たしている場合にのみ実行されるようにすることができます。
今回の場合だと、artist
とsongTitle
が存在する場合にのみ更新が実行され、存在しない場合はエラーが返されます。
ConditionExpressionがない場合には、DynamoDBは指定されたキーが存在するかどうかを確認せずに更新操作を行うので、キーが存在しない場合には新しいアイテムが作成されてしまいます。
トランザクション
transactWriteメソッド
更新をtransactWrite
メソッドを使用してトランザクション管理を行ってみます。
params
とtry
ブロック内をtransactWrite
メソッドで使用できる記述に変更してください。
transactWrite
メソッドでは、ReturnValues
パラメータを使用することはできないようなので、更新後のデータを取得してレスポンスしています。
const params = {
TransactItems: [
{
Update: {
TableName: process.env.TABLE_NAME || '',
Key: key,
UpdateExpression: Expression,
ExpressionAttributeValues: expressionAttributeValues,
ExpressionAttributeNames: expressionAttributeNames,
},
},
// 意図的にエラーを発生させるために存在しないテーブルを使用
{
Update: {
TableName: 'NonExistentTable',
Key: {
Key: 'value',
},
UpdateExpression: 'SET #attribute = :newCategory',
ExpressionAttributeNames: {
'#attribute': 'category',
},
ExpressionAttributeValues: {
':newCategory': 'KPOP',
},
},
},
],
};
try {
// トランザクション開始
const data = await dynamoDB.transactWrite(params).promise();
// 更新後のデータを取得
const getParams = {
TableName: process.env.TABLE_NAME || '',
Key: key,
};
// transactWrite
const result = await dynamoDB.get(getParams).promise();
const updatedItem = result.Item;
return NextResponse.json(updatedItem);
}
トランザクションのライフサイクル
下記の操作はAWSによって内部的に管理されているので、明示的にトランザクションのコミットや終了を行う必要はありません。
開始 (Begin)
transactWrite
の呼び出し時に自動的に行われます。
コミット (Commit)
すべての操作が成功した場合に自動的に行われます。
ロールバック (Rollback)
いずれかの操作が失敗した場合に自動的に行われます。
DynamoDB:主キー(パーティションキーとソートキーの組み合わせ)の値を更新する場合
実際には新しいアイテムを挿入(put)して古いアイテムを削除(delete)する方法が一般的です。
更新しようとするとアイテムを一意に識別するための要素なので更新することができず、下記が表示されます。
ValidationException: One or more parameter values were invalid: Cannot update attribute <パーティションキーまたはソートキー>. This attribute is part of the key
handleSubmit関数
react-hook-form
ライブラリの一部で、フォーム送信時のカスタム処理を実装するために使用されます。
event.preventDefault()
を使用してデフォルトのフォーム送信動作を防ぎます。フォームが送信されてもページがリロードされないようにしています。
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
}
getSignedUrlPromise
署名付きURL
署名付きURLを使う目的は、特定の操作(この場合はファイルアップロード)を AWS SDK の認証情報をクライアントに公開することなく行うためです。AWS S3 の署名付きURLを使用する場合、通常はクライアント(ブラウザなど)が直接署名付きURLを介して S3 にファイルをアップロードします。
終わりに
何かありましたらお気軽にコメント等いただけると助かります。
ここまでお読みいただきありがとうございます🎉
Discussion