Share
Embed
実例から学ぶ! AWSを活用したシステム開発の勘所
「Developers.IO 2022 〜技術で心を揺さぶる3日間〜」 の発表で利用した資料です
TomoyaIwata
July 21, 2022
Transcript
49 slides
-
アジェンダ • 案件概要 • API仕様書の肥⼤化 • デプロイ速度の問題 • RDS Proxyのピン留め問題
• X-Rayの導⼊ • ENIのDNSスロットリング問題 • ログ出⼒の改善 • まとめ -
単⼀ファイルによる管理の限界 • YAMLファイルが1万⾏超 • メンテナンスの負荷が⾼い openapi: "3.0.0" info: version: 1.0.0
title: Swagger Petstore license: name: MIT servers: - url: http://petstore.swagger.io/v1 paths: /pets: get: summary: List all pets operationId: listPets tags: - pets parameters: - name: limit in: query description: How many items to return at one time (max 100) required: false schema: type: integer format: int32 responses: '200': description: A paged array of pets headers: x-next: description: A link to the next page of responses schema: type: string content: application/json: schema: $ref: "#/components/schemas/Pets" default: description: unexpected error -
openapi: "3.0.0" info: version: 1.0.0 title: Swagger Petstore paths: /pets:
$ref: ./paths/pets.yaml#/paths/~1pets /pets/{pet_id}: $ref: ./paths/pets.yaml#/paths/~1pets~1{petId} ファイル分割のアプローチ $refで参照 openapi: "3.0.0" paths: /pets: get: summary: List all pets operationId: listPets tags: - pets parameters: - name: limit -
ディレクトリ構成の⼀例 ├ api.yaml…⼦ファイルを読み込むメインのファイル ├ paths …パスごとにAPIをグルーピングして定義 │ ├ pets.yaml │
└ users.yaml └ components ├ schemas.yaml…共通のスキーマを定義 └ responses.yaml…共通のレスポンスを定義 -
Swagger UI等の環境を構築する場合 • swagger-mergerで定義ファイルのマージが可能 • ただしv1.5.4 時点では{}を含む参照がマージできない ☓ swagger-mergerでマージ不可 ◦
swagger-mergerでマージ可 paths: /pets/{petId}: paths: /pets-petId: /pets/{pet_id}: $ref: ./paths/pets.yaml#/paths/~1pets~1{petId} /pets/{pet_id}: $ref: ./paths/pets.yaml#/paths/~1pets-petId -
(after) サイズの⼤きなassetはLayer⽤の1つだけ Lambda Layers (node_modules) 関数コード Lambda Layers (node_modules) 関数コード
Lambda Layers (node_modules) -
Lambda Layers導⼊前後の⽐較 導入前 導入後 全Lambda Functionのビルド時間 100秒超 約40秒 Assetサイズ (圧縮前のファイルサイズ合計)
約1.5G 約60M Assetサイズ (ZIPファイル1つあたりの平均) 約4.2M 約100K ビルド対象のLambda Function数:110で⽐較 -
Lambda Layers導⼊のデメリット Layerを使わない⽅がバンドルファイルのサイズを最適化できる import { S3Client } from '@aws-sdk/client-s3’ export
const handler = async (…略 import { SQSClient } from '@aws-sdk/client-sqs’ export const handler = async (…略 -
Lambda Layers導⼊のデメリット Layerを使うと未使⽤のライブラリまでデプロイされる Lambda 1でのみ利⽤する 全Lambdaで利⽤ Lambda 2と3でのみ利⽤ コールドスタートに悪影響 "dependencies":
{ “@aws-sdk/client-s3”: “^3.53.1”, "@aws-sdk/client-secrets-manager": "^3.53.0", "@aws-sdk/client-sqs": "^3.53.0", "@aws-sdk/s3-request-presigner": "^3.52.0", -
[ 'mkdir -p /asset-output/nodejs/’, 'npm install -g [email protected] [email protected]’, …略
'npm ci –prefix=/asset-output/nodejs/’, 'cd /asset-output/nodejs/’, 'find node_modules -type l | xargs rm -f’, 'find node_modules -type f -name *.d.ts | xargs rm -f’, 'modclean’, 'node-prune', ].join(' && '), node_modulesのサイズを最適化 READMEや型定義ファイルを削除 -
CDKでLambda Layersを利⽤する際のポイント package-lock.jsonやyarn.lockに変更が無い限り ビルド&デプロイしない const lockFileBuffer = readFileSync(path.join(pjRootDir, 'yarn.lock')) const
lockFileHash = createHash('sha256') lockFileHash.update(lockFileBuffer) const lockFileDigest = lockFileHash.digest('hex’) const nodeModuleLayer = new lambda.LayerVersion(this, `NodeModuleLayer${suffix}`, { code: lambda.Code.fromAsset(pjRootDir, { assetHash: lockFileDigest, assetHashType: AssetHashType.CUSTOM, -
ピン留めが発⽣する条件 • SET⽂の使⽤ • tmpテーブルの作成 • Prepared statementの利⽤ • …etc
何も意識せずORMを使うとほぼ確実にピン留めが発⽣ -
対応⽅針 原則Prepared Statementの利⽤を回避 const pet = connection.getRepository(Pet).findOne(PetId) const pet =
connection.getRepository(Pet).findOne({ where: `pet_id = ‘${PetId}’` }) -
キャプチャ処理はhandler内で実⾏する import {capturePromise} from 'aws-xray-sdk-core' capturePromise() export const handler =
async ( event: APIGatewayProxyEventBase<APIGatewayEventDefaultAuthorizerContext> ): Promise<APIGatewayProxyResult> => { // ...略 } Error: Missing AWS Lambda trace data for X-Ray. Ensure Active Tracing is enabled and no subsegments are created outside the function handler. -
ラッパークラスにキャプチャ処理を実装 export class ExternalApiClient implements I ExternalApiClient { #captured =
false async requestWith<Type>(options: RequestWithOptions): Promise<Type> { … if (!this.#captured) { captureHTTPsGlobal(http, true) captureHTTPsGlobal(https, true) capturePromise() this.#captured = true } -
ユニットテスト実⾏時はsetContextMissingStrategyを describe('some tests', () => { beforeEach(()=>{ setContextMissingStrategy('IGNORE_ERROR') }) })
Error: Failed to get the current sub/segment from the context. のエラーを抑⽌ -
テスト中にLambdaのエラーが 2022-02-18T01:27:18.747Z b9dfaf70-f7d8-465c-8a26-e2a736b4c97a ERROR Error: getaddrinfo EMFILE secretsmanager.ap-northeast-1.amazonaws.com at GetAddrInfoReqWrap.onlookup
[as oncomplete] (dns.js:71:26) { errno: -24, code: 'EMFILE', syscall: 'getaddrinfo', hostname: 'secretsmanager.ap- northeast-1.amazonaws.com', '$metadata': { attempts: 1, totalRetryDelay: 0 } } ENIあたり1024パケット/sの上限に抵触 https://aws.amazon.com/jp/premiumsupport/knowledge-center/vpc-find-cause-of-failed-dns-queries/ -
SecretsManagerへの過剰なアクセスが原因 外部API呼び出しの都度SecretsManagerにアクセスしていた const output = await axios .request<Type>({ headers: {‘X-Hoge-Token’:
await this.#driver.fetchToken()}…略 async fetchToken(): Promise<string> { return (await this.#fetchSecret(process.env.HOGE_TOKEN!)) .SecretString! } async #fetchSecret(secretId: string): Promise<GetSecretValueCommandOutput> { return this.#secretsManagerClient.send( new GetSecretValueCommand({SecretId: secretId,}) )} -
シークレットをキャッシュして対応 static変数を使い取得済みのシークレットをキャッシュ async #fetchSecret(secretId: string): Promise<GetSecretValueCommandOutput> { if (secretId in
SecretsStorageDriver.#cachedSecrets) { return SecretsStorageDriver.#cachedSecrets[secretId] } const secret = await this.#secretsManagerClient.send( new GetSecretValueCommand({ SecretId: secretId, }) ) SecretsStorageDriver.#cachedSecrets[secretId] = secret return secret -
ログのフォーマットをJSONに CloudWatch Logs Insights等のツールで容易に検索可能 { "petId": 1, "userId": 1, "message":
"ペットの登録が完了しました", "level": "info", "timestamp": "2022-06-09T12:41:14.236+09:00", "xRayTraceId": "1-62b3cc5b-5b3366656062344734c1c0e3", "requestId": "51f3d508-2067-4417-9b34-25f40e77c186" } -
構造化ログの出⼒処理 const logging = (loggingFn: (...data: any[]) => void, options:
LoggingOptions) => { const logObj = { xRayTraceId: process.env._X_AMZN_TRACE_ID, lambdaFunction: { name: process.env.AWS_LAMBDA_FUNCTION_NAME, …略 }, ...options, } loggingFn(JSON.stringify(logObj)) } export const debug = (options: LogOptions) => { logging(console.debug, { level: 'debug', ...options }) } -
ロギング⽤オプションの型定義 • システム全体を通して重要な意味を持つ項⽬を最上位のキーに • 外部APIのキー項⽬、DBキー項⽬、APIのパスパラメータ等 • ⼀部の処理においてのみ重要な情報はcontext配下に • 型定義が簡潔に type
LogOptions = { message: string petId?: Pet[‘petId'] userId?: User[‘userId’] context?: Object }