【AWS】S3からダウンロードしたファイルのContent-Typeが変なんです
現象
S3からダウンロードしたファイルのContent-Typeがbinary/octet-streamになってしまう。
原因
S3へのアップロード時にContent-Typeを指定していない。
APIでのアップロードなどで忘れがち。
対策
Amplifyの場合
// ファイルアップロード
Storage.put(key, fileObject, {
level: fileLevel,
contentType: ${contentType},
progressCallback(progress) {},
});
【AWS】Amplify使ってAPI Gatewayにリクエスト送信したい
-
-
Angular CLIの環境構築は実施済とする
<参考>
Amplify Docs
https://docs.amplify.aws/start/getting-started/installation/q/integration/angular
環境
NO | 項目 | バージョン |
---|---|---|
1 | Angular | 10.0.5 |
2 | Amplify | 3.3.4 |
3 | Typescript | 3.9.5 |
実装
Angularプロジェクトを作成
ng new ${プロジェクト名}
Amplifyライブラリを追加
npm install --save aws-amplify @aws-amplify/ui-angular
AWSの設定ファイルを作成
フォーマットはAmplifyコマンドで生成されてるものに合わせました。
aws-export.jsを作成
${プロジェクトルート}/src/aws-exports.js
import Amplify from 'aws-amplify';
// cognitoやS3に接続する設定を追記する箇所
const awsmobile = {
};
export default awsmobile;
// API Gatewayの設定
Amplify.configure({
API: {
endpoints: [
{
name: '${API名}',
endpoint: '${エンドポイント}'
}
]
}
});
API名は↓を設定。
エンドポイントは↓を設定。
設定ファイル読み込み処理を実装
app.module.tsに実装しました。
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
// Amplifyと設定ファイルをインポート
import Amplify from 'aws-amplify';
import config from '../aws-exports';
// 設定ファイルを読み込ませる
Amplify.configure(config)
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
AppRoutingModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Shimの追加
polyfills.tsにglobalとprocessのShimを追記する。
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/guide/browser-support
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/**
* Web Animations `@angular/platform-browser/animations`
* Only required if AnimationBuilder is used within the application and using IE/Edge or Safari.
* Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0).
*/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/**
* By default, zone.js will patch all possible macroTask and DomEvents
* user can disable parts of macroTask/DomEvents patch by setting following flags
* because those flags need to be set before `zone.js` being loaded, and webpack
* will put import in the top of bundle, so user need to create a separate file
* in this directory (for example: zone-flags.ts), and put the following flags
* into that file, and then add the following code before importing zone.js.
* import './zone-flags';
*
* The flags allowed in zone-flags.ts are listed here.
*
* The following flags will work for all browsers.
*
* (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
* (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
* (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
*
* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
* with the following flag, it will bypass `zone.js` patch for IE/Edge
*
* (window as any).__Zone_enable_cross_context_check = true;
*
*/
/***************************************************************************************************
* Zone JS is required by default for Angular itself.
*/
import 'zone.js/dist/zone'; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/
// globalとprocessのShimを追記
(window as any).global = window;
(window as any).process = {
env: { DEBUG: undefined },
};
リクエスト送信処理の実装
GET通信の場合
import { Component } from '@angular/core';
import { from } from 'rxjs';
// amplifyをインポート
import { API } from 'aws-amplify';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
title = 'AmplifyTest';
/**
* API Gatewayに通信する
*/
sendRequest() {
const apiName = '${API名}';
const path = '${APIパス}';
const init = {
// クエリパラメータを設定
queryStringParameters: {
orderId: '1234',
},
// ヘッダーを設定
headers: {
'Content-Type': 'application/json',
},
};
// API実行
// RXJSで扱いたいためPromiseをfromで変換
from(API.get(apiName, path, init)).subscribe(
(result) => {
console.log(result);
},
(error) => {
console.log(error);
}
);
}
}
※CORSでエラーが出る場合
POST通信の場合
import { Component } from '@angular/core';
import { from } from 'rxjs';
// amplifyをインポート
import { API } from 'aws-amplify';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent {
title = 'AmplifyTest';
/**
* API Gatewayに通信する
*/
sendPOSTRequest() {
const apiName = 'TestLambda-API';
const path = '/history';
const init = {
// ボディを追加
body: {
name: 'taro',
order: '醤油マシマシ',
date: '2020-10-11',
},
// ヘッダーを追加
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
};
// API実行
// RXJSで扱いたいためPromiseをfromで変換
from(API.post(apiName, path, init)).subscribe(
(result) => {
console.log(result);
},
(error) => {
console.log(error);
}
);
}
}
※CORSでエラーが出る場合
その他
(1)CORSでエラーになる
①CORSを適用したいAPIに以下のパスを追加する。
No | 項目 | 値 |
---|---|---|
1 | パス | {proxy+} |
2 | メソッド | OPTIONS |
②追加したパスに適当なLambdaを統合する。
※ドキュメントにCORSのレスポンスが記載されているがAPI Gateway V2の場合無視されるので、
左メニュー「開発」→「CORS」で設定する。
Lambda
※初期状態でもOK
import json
def lambda_handler(event, context):
# TODO implement
return {
'statusCode': 200,
'body': json.dumps('Hello from Lambda!')
}
CORS
<参考>
REST API リソースの CORS を有効にする
https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/how-to-cors.html
Angular HttpInterceptorを使いたい
AmplifyはHTTPクライアントにAxiosを使用しているためAngularのInterceptorが使用できない
【AWS】DynamoDBを理解する【クエリ編】
<参考>
DynamoDB boto3 API
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html#client
読み込み
プライマリキーでデータを取得
import json
import boto3
from boto3.dynamodb.conditions import Key
import logging
# ロガーの設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
# リクエストから検索条件を取得
orderId = event['queryStringParameters']['orderId']
# DynamoDBを取得
dynamoDB = boto3.resource('dynamodb')
# DynamoDBのテーブル名
table = dynamoDB.Table('History')
try:
# DynamoDBへクエリ実行
queryData = table.get_item(Key={'orderId' : orderId})
except Exception as e:
# エラーをCloudWatchに出力
logger.error(e)
return {
'statusCode': 500,
'body': json.dumps("Internal Server Error.")
}
# 検索結果を取得
orderInfo = queryData['Item']
if orderInfo is None:
return {
'statusCode': 404,
'body': json.dumps('Order Not Found.')
}
# 正常終了
return {
'statusCode': 200,
'headers': { "Content-Type": "application/json; charset=UTF-8"},
'body': json.dumps(orderInfo)
}
強力な整合性のある読み込みをする場合は「ConsistentRead=true」を設定
queryData = table.get_item(
Key={'Id' : userId},
ConsistentRead=True
)
プライマリキーとソートキーでデータを取得
import json
import boto3
from boto3.dynamodb.conditions import Key
import logging
# ロガーの設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
# リクエストから検索条件を取得
name = event['queryStringParameters']['name']
# 属性「order」がソートキーの場合
order = event['queryStringParameters']['order']
# DynamoDBを取得
dynamoDB = boto3.resource('dynamodb')
# DynamoDBのテーブル名
table = dynamoDB.Table('History')
try:
# DynamoDBへクエリ実行
# プライマリキー、ソートキーを指定する
queryData = table.get_item(Key={'name' : name, 'order': order})
except Exception as e:
# エラーをCloudWatchに出力
logger.error(e)
return {
'statusCode': 500,
'body': json.dumps("Internal Server Error.")
}
# 検索結果を取得
orderInfo = queryData['Item']
if orderInfo is None:
return {
'statusCode': 404,
'body': json.dumps('Order Not Found.')
}
# 正常終了
return {
'statusCode': 200,
'headers': { "Content-Type": "application/json; charset=UTF-8"},
'body': json.dumps(orderInfo)
}
セカンダリインデックスでデータを取得
import json
import boto3
from boto3.dynamodb.conditions import Key
import logging
# ロガーの設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
# リクエストから検索条件を取得
order = event['queryStringParameters']['order']
# DynamoDBを取得
dynamoDB = boto3.resource('dynamodb')
# DynamoDBのテーブル名
table = dynamoDB.Table('History')
try:
# DynamoDBへクエリ実行
queryData = table.query(
IndexName='order-index',
KeyConditionExpression=Key('order').eq(order)
)
except Exception as e:
return {
# エラーをCloudWatchに出力
logger.error(e)
'statusCode': 500,
'body': json.dumps("Internal Server Error.")
}
# 検索結果を取得
orderInfoList = queryData['Items']
if not orderInfoList:
return {
'statusCode': 404,
'body': json.dumps('Users Not Found.')
}
# 正常終了
return {
'statusCode': 200,
'headers': { "Content-Type": "application/json; charset=UTF-8"},
'body': json.dumps(orderInfoList)
}
全件取得
最大1MBのデータを取得可能。
それ以上は再度取得処理を実行する。
import json
import boto3
from boto3.dynamodb.conditions import Key
import logging
# ロガーの設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
# DynamoDBを取得
dynamoDB = boto3.resource('dynamodb')
# DynamoDBのテーブル名
table = dynamoDB.Table('History')
try:
# DynamoDBへクエリ実行
queryData = table.scan()
except Exception as e:
# エラーをCloudWatchに出力
logger.error(e)
return {
'statusCode': 500,
'body': json.dumps("Internal Server Error.")
}
# 検索結果を取得
orderInfo = queryData['Items']
logger.info(orderInfo)
# 正常終了
return {
'statusCode': 200,
'headers': { "Content-Type": "application/json; charset=UTF-8"},
'body': json.dumps(orderInfo)
}
データが上限を超える場合「LastEvaluatedKey」を使って再度取得する。
import json
import boto3
from boto3.dynamodb.conditions import Key
import logging
# ロガーの設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
# DynamoDBを取得
dynamoDB = boto3.resource('dynamodb')
# DynamoDBのテーブル名
table = dynamoDB.Table('History')
try:
# DynamoDBへクエリ実行
queryData = table.scan()
# 検索結果を取得
orderInfo = queryData['Items']
# 取得上限を超えた場合、再度クエリを実行
while 'LastEvaluatedKey' in queryData:
queryData = table.scan(
ExclusiveStartKey = queryData['LastEvaluatedKey']
)
# 検索結果をマージ
orderInfo += queryData['Items']
except Exception as e:
# エラーをCloudWatchに出力
logger.error(e)
return {
'statusCode': 500,
'body': json.dumps("Internal Server Error.")
}
# 正常終了
return {
'statusCode': 200,
'headers': { "Content-Type": "application/json; charset=UTF-8"},
'body': json.dumps(orderInfo)
}
取得したデータを絞り込む
指定したキーでデータを取得した後に絞り込むため、
DynamoDBからの転送量やキャパシティユニットの使用量は減らない。
scanを使用した場合もscan結果に対してFilter処理を実行するため、最大1MBの制限は変わらない。
import json
import boto3
from boto3.dynamodb.conditions import Key, Attr
import logging
# ロガーの設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
# リクエストから検索条件を取得
order = event['queryStringParameters']['order']
date = event['queryStringParameters']['date']
# DynamoDBを取得
dynamoDB = boto3.resource('dynamodb')
# DynamoDBのテーブル名
table = dynamoDB.Table('History')
try:
# DynamoDBへクエリ実行
queryData = table.query(
IndexName='order-index',
KeyConditionExpression=Key('order').eq(order),
FilterExpression=Attr('date').eq(date)
)
except Exception as e:
# エラーをCloudWatchに出力
logger.error(e)
return {
'statusCode': 500,
'body': json.dumps("Internal Server Error.")
}
# 検索結果を取得
orderInfoList = queryData['Items']
if not orderInfoList:
return {
'statusCode': 404,
'body': json.dumps('Order Not Found.')
}
# 正常終了
return {
'statusCode': 200,
'headers': { "Content-Type": "application/json; charset=UTF-8"},
'body': json.dumps(orderInfoList)
}
書き込み
プライマリキーのデータが存在しなかったら追加、すでにある場合は更新。
import json
import boto3
from boto3.dynamodb.conditions import Key
import logging
# ロガーの設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
# リクエストボディから登録データを取得
body = json.loads(event['body'])
orderId = body['orderId']
name = body['name']
order = body['order']
date = body['date']
# DynamoDBを取得
dynamoDB = boto3.resource('dynamodb')
# DynamoDBのテーブル名
table = dynamoDB.Table('History')
try:
# DynamoDBへクエリ実行
table.put_item(
Item={
'orderId': orderId,
'name': name,
'order': order,
'date': date
}
)
except Exception as e:
# エラーをCloudWatchに出力
logger.error(e)
return {
'statusCode': 500,
'body': json.dumps('Internal Server Error.')
}
# 正常終了
return {
'statusCode': 200,
'headers': { "Content-Type": "application/json; charset=UTF-8"},
'body': json.dumps('OK.')
}
データの登録イメージ
複数の書き込み
import json
import boto3
from boto3.dynamodb.conditions import Key
import logging
import random
# ロガーの設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
# リクエストボディから登録データを取得
body = json.loads(event['body'])
name = body['name']
order = body['order']
date = body['date']
# DynamoDBを取得
dynamoDB = boto3.resource('dynamodb')
# DynamoDBのテーブル名
table = dynamoDB.Table('History')
try:
# DynamoDBへクエリ実行
with table.batch_writer() as batch:
for i in range(10):
batch.put_item(
Item={
# とりあえずIDは乱数で生成して0埋め
'orderId': str(random.randint(1, 9999)).zfill(4),
'name': name,
'order': order,
'date': date
}
)
except Exception as e:
# エラーをCloudWatchに出力
logger.error(e)
return {
'statusCode': 500,
'body': json.dumps('Internal Server Error.')
}
# 正常終了
return {
'statusCode': 200,
'headers': {"Content-Type": "application/json; charset=UTF-8"},
'body': json.dumps('OK.')
}
【AWS】DynamoDBを理解する【概要編】
-
NoSQLデータベース
-
結果整合性のある読み込み
書き込みの保障はあるが、読み込みの保障はない。
(書き込まれたデータが確実に取得できるとは限らない)
-
RDSとかと違って検索機能が弱い
-
容量無制限
-
高い可用性と耐久性
AWSドキュメントに以下の記載あり。
DynamoDB は十分な数のサーバー間でデータとトラフィックを自動的に分散し、一貫した高速なパフォーマンスを維持したまま、スループットとストレージの要件に対応します。すべてのデータは SSD (Solid State Disk) に保存され、1 つの AWS リージョン内の複数のアベイラビリティーゾーン間で自動的にレプリケートすることによって、高い可用性とデータ堅牢性を実現します。
<参考>
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Introduction.html
料金
オンデマンドモードとプロビジョンドモードによって違いあり。
プロビジョンドモード
課金対象一覧
<参考>
●プロビジョニングされたキャパシティーの料金
https://aws.amazon.com/jp/dynamodb/pricing/provisioned/
機能 | 内容 | 請求単位 | |
---|---|---|---|
主要機能 | |||
プロビジョニングされた書き込みキャパシティー | テーブルにデータを書き込む | WCU | |
プロビジョニングされた読み込みキャパシティー | テーブルからデータを読み込む | RCU | |
データストレージ | データを格納する (インデックス値を含む) | GB 月単位 | |
オプション機能 | |||
継続的バックアップ | 過去 35 日間のバックアップを継続的に取得する | GB 月単位 | |
オンデマンドバックアップ | 過去のある時点におけるスナップショットバックアップを取得する | GB 月単位 | |
バックアップからの復元 | 特定のスナップショットまたは時間のテーブルを復元する | GB 単位 | |
グローバルテーブル | データをレプリケートしてマルチリージョンかつマルチマスターなテーブルを作成する | rWCU | |
DynamoDB Accelerator (DAX) | インメモリキャッシュのレイテンシーを低減する | ノード時間単位 | |
DynamoDB ストリーム | テーブルの項目レベルの変更に関する時系列シーケンスを提供する | ストリーム読み込みリクエスト単位 | |
データ転送 (アウト) | 他の AWS リージョンにデータを転送する 同一リージョン内なら無料 | GB 単位 |
課金対象詳細
※2020/10時点の東京リージョンで計算
プロビジョニングされた書き込みキャパシティー
-
WCUの割り当て数によって課金
-
1WCU/1時間あたり 0.000742USD≒0.078円
-
無料利用枠で月あたり25WCUまで無料
例)テーブルにプロビジョニングされたWCUの合計が30WCUの場合
-
1カ月当たりのWCU単価
1WCU/月=0.53424USD(24時間×30日で計算)
-
1カ月当たりのWCU割り当て数 - 無料利用枠
30WCU/月 - 25WCU/月 = 5WCU
-
日本円に変換
5WCU/月=2.6712USD≒278.48 円
プロビジョニングされた読み込みキャパシティー
-
RCUの割り当て数によって課金
-
1RCU/1時間あたり 0.0001484USD≒0.016円
-
無料利用枠で月あたり25RCUまで無料
例)テーブルにプロビジョニングされたRCUの合計が30RCUの場合
-
1カ月当たりのRCU単価
1RCU/月=0.106848USD(24時間×30日で計算)
-
1カ月当たりのWCU割り当て数 - 無料利用枠
30WCU/月 - 25WCU/月 = 5WCU
-
日本円に変換
5WCU/月=0.53424USD≒55.71 円
データストレージ
-
下記の合計が課金対象
-
アップロードするデータの raw バイトサイズ
-
各項目のストレージオーバーヘッドとしてインデックス作成のための 100 バイト
-
-
月/GBあたり0.285USD≒29.71 円
-
無料利用枠で25 GBまで無料
オプション
あとで書く
オンデマンド
あとで書く
無料利用枠
キャパシティモードによって違いあり。
オンデマンド
-
25 GB のデータストレージ
-
DynamoDB ストリームからのストリーム読み込みリクエスト 250 万回
-
AWS のサービス全体での合計データ転送 (アウト) 1 GB
-
読み込み、書き込みリクエストは無料利用枠の対象外
プロビジョンド
-
WCU 25 個と RCU 25 個のプロビジョニングされたキャパシティー
-
25 GB のデータストレージ
-
2 つの AWS リージョンにデプロイされたグローバルテーブルに対して rWCU 25 個
-
DynamoDB ストリームからの 250 万回のストリーム読み込み要求
-
AWS のすべてのサービスで合計して 1 GB までのデータ転送 (アウト) (最初の 12 か月で 15 GB)
言語集
項目
SQLのレコードと一緒
属性
SQLのカラムとか列とかと一緒
キャパシティモード
-
スループットの定義
-
キャパシティモードの変更は24時間で1回
以下の2種類あり。
オンデマンド
プロビジョニング
-
事前にキャパシティを割り当て(WCU/RCU)
-
割り当てたキャパシティ数によって課金が発生
-
-
スループットは割り当てたキャパシティによって変化
-
オプションのAuto Scalingを設定することでトラフィックに応じて自動でキャパシティ数を増加
プライマリキー
-
テーブルの各項目を一意に識別するもの
-
プライマリキー属性に設定可能なデータ型はString、Number、Binary
以下の2種類あり。
パーティションキー
-
1つの属性で構成されるシンプルなプライマリキー
パーティションキーとソートキー
セカンダリインデックス
-
プライマリキーとは別に検索等に使用可能な代替キー
-
セカンダリインデックスを使用して取得するデータは事前に射影した属性を取得する
以下の2種類あり。
グローバルセカンダリインデックス(GSI)
-
テーブル上のものとは異なるパーティションキーとソートキーを持つインデックス
-
インデックスにはプライマリキーは必須で、ソートキーは必須ではない
-
テーブルにプロビジョニングされたキャパシティとは別にキャパシティを使用する
ローカルセカンダリインデックス(LSI)
-
テーブルと同じパーティションキーを持つが、異なるソートキーを持つインデックス
-
テーブル作成時にしか追加できない
-
インデックスにはパーティションキーとソートキーが必須
-
テーブルにプロビジョニングされたユニットを使用する
射影
読み込み方式
データ読み込み方式は下記の2種類あり。
結果整合性のある読み込み
DynamoDB テーブルからの読み込みオペレーションの応答には、最近の書き込みオペレーションの結果が反映されていないことがあります。応答には古いデータが含まれる場合があります。少し時間がたってから読み込みリクエストを繰り返すと、応答で最新のデータが返されます。
強力な整合性のある読み込み
強力な整合性のある読み込みをリクエストすると、DynamoDB は成功した以前のすべての書き込みオペレーションからの更新が反映された最新データの応答を返します。ただし、この整合性には以下のような欠点があります。
-
ネットワーク遅延や障害が発生した場合、協力な整合性のある読み込みができない時がある
この場合、DynamoDBはサーバーエラー(HTTP 500)を返却する
-
結果整合性のある読み込みよりも待ち時間が長くなる可能性がある
-
グローバルセカンダリインデックスではサポートされていない(2020/10時点)
-
結果整合性のある読み込みよりも2倍キャパシティユニットを使用する
<参考>
Read Consistency
Read/Write Capacity Mode
リクエストユニット
データの読み込み、書き込みの単位。
読込ユニット(RCU)
-
1RCUは1秒間に1回の読み込みを行う
-
1RCUのサイズは4KB
-
強く一貫性のある読み取りの場合は1RCUを使用
-
結果一貫性のある読み取りの場合は0.5RCUを使用
書き込みユニット(WCU)
-
1WCUは1秒間に1回の書き込みを行う
-
1WCUのサイズは1KB
-
通常の書き込みの場合は1WCUを使用
-
トランザクション書き込みの場合は2WCUを使用
Auto Scaling
<参考>
DynamoDB Auto Scaling によるスループットキャパシティーの自動管理
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/AutoScaling.html
制限事項
<参考>
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Limits.html