Slide 1

Slide 1 text

実例から学ぶ︕ AWSを活⽤したシステム開発の勘所 2022/7/19 CX事業本部 Delivery部 MADグループ 岩⽥ 智哉

Slide 2

Slide 2 text

アジェンダ • 案件概要 • API仕様書の肥⼤化 • デプロイ速度の問題 • RDS Proxyのピン留め問題 • X-Rayの導⼊ • ENIのDNSスロットリング問題 • ログ出⼒の改善 • まとめ

Slide 3

Slide 3 text

概要 外部API Aurora PostgreSQL互換 RDS/外部APIの認証情報 REST API VPC Lambda ID基盤はAuth0

Slide 4

Slide 4 text

概要 l⾔語…TypeScript lORM…TypeORM lIaC、デプロイ… CDK

Slide 5

Slide 5 text

API仕様書の肥⼤化

Slide 6

Slide 6 text

API仕様の管理⽅法 • APIの仕様はOASでドキュメント化 • API GWのデプロイには利⽤せず • CDKのapigateway.SpecRestApiは未使⽤ • 参照するのは開発メンバーのみ • 単⼀のYAMLファイルで管理

Slide 7

Slide 7 text

単⼀ファイルによる管理の限界 • 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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

ファイル分割のアプローチ 参照される⼦ファイルもOAS形式にすることで ⼦ファイル単体のプレビュー表⽰が可能

Slide 10

Slide 10 text

ディレクトリ構成の⼀例 ├ api.yaml…⼦ファイルを読み込むメインのファイル ├ paths …パスごとにAPIをグルーピングして定義 │ ├ pets.yaml │ └ users.yaml └ components ├ schemas.yaml…共通のスキーマを定義 └ responses.yaml…共通のレスポンスを定義

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

デプロイ速度の問題

Slide 13

Slide 13 text

デプロイのざっくりした流れ • 各Lambda Functionをビルド • TS→JSのトランスパイル • バンドル • S3にAssetをアップロード • デプロイ

Slide 14

Slide 14 text

node_modulesをLambda Layersに引っ越し Lambda Layers (node_modules) 関数コード 関数コード + node_modules

Slide 15

Slide 15 text

(before)巨⼤なassetを⼤量にデプロイ 関数コード + node_modules 関数コード + node_modules

Slide 16

Slide 16 text

(after) サイズの⼤きなassetはLayer⽤の1つだけ Lambda Layers (node_modules) 関数コード Lambda Layers (node_modules) 関数コード Lambda Layers (node_modules)

Slide 17

Slide 17 text

Lambda Layers導⼊前後の⽐較 導入前 導入後 全Lambda Functionのビルド時間 100秒超 約40秒 Assetサイズ (圧縮前のファイルサイズ合計) 約1.5G 約60M Assetサイズ (ZIPファイル1つあたりの平均) 約4.2M 約100K ビルド対象のLambda Function数:110で⽐較

Slide 18

Slide 18 text

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 (…略

Slide 19

Slide 19 text

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",

Slide 20

Slide 20 text

[ '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や型定義ファイルを削除

Slide 21

Slide 21 text

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,

Slide 22

Slide 22 text

RDS Proxyのピン留め問題

Slide 23

Slide 23 text

ピン留めについておさらい コネクションプール クライアントとの接続 チェックアウト中 未チェックアウト

Slide 24

Slide 24 text

ピン留めが発⽣する条件 • SET⽂の使⽤ • tmpテーブルの作成 • Prepared statementの利⽤ • …etc 何も意識せずORMを使うとほぼ確実にピン留めが発⽣

Slide 25

Slide 25 text

対応⽅針 原則Prepared Statementの利⽤を回避 const pet = connection.getRepository(Pet).findOne(PetId) const pet = connection.getRepository(Pet).findOne({ where: `pet_id = ‘${PetId}’` })

Slide 26

Slide 26 text

Prepared Statementを利⽤しないことの懸念 • SQLインジェクションのリスク • ⼊⼒値のバリデーションを徹底 • node-pg-formatでエスケープ • ⽣産性の低下

Slide 27

Slide 27 text

割り切りも⼤事 • 更新系はピン留めを許容 • handler内でDBと接続/切断 ※未実施 • ピン留め⾃体が問題なのではなく、ピン 留めによって何が起きるのかを考える

Slide 28

Slide 28 text

X-Rayの導⼊

Slide 29

Slide 29 text

パフォーマンスの問題が • ⼀部APIのレスポンスが遅い • 外部APIのレスポンス遅延が疑わしい

Slide 30

Slide 30 text

ボトルネックが視覚的に分析可能に

Slide 31

Slide 31 text

Lambdaが外部APIを呼び出した際のエラー率を可視化

Slide 32

Slide 32 text

外部APIの応答時間を可視化

Slide 33

Slide 33 text

キャプチャ処理はhandler内で実⾏する import {capturePromise} from 'aws-xray-sdk-core' capturePromise() export const handler = async ( event: APIGatewayProxyEventBase ): Promise => { // ...略 } Error: Missing AWS Lambda trace data for X-Ray. Ensure Active Tracing is enabled and no subsegments are created outside the function handler.

Slide 34

Slide 34 text

ラッパークラスにキャプチャ処理を実装 export class ExternalApiClient implements I ExternalApiClient { #captured = false async requestWith(options: RequestWithOptions): Promise { … if (!this.#captured) { captureHTTPsGlobal(http, true) captureHTTPsGlobal(https, true) capturePromise() this.#captured = true }

Slide 35

Slide 35 text

ユニットテスト実⾏時はsetContextMissingStrategyを describe('some tests', () => { beforeEach(()=>{ setContextMissingStrategy('IGNORE_ERROR') }) }) Error: Failed to get the current sub/segment from the context. のエラーを抑⽌

Slide 36

Slide 36 text

ENIのDNSスロットリング問題

Slide 37

Slide 37 text

テスト中に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/

Slide 38

Slide 38 text

ここでもX-Rayが活躍

Slide 39

Slide 39 text

SecretsManagerへの過剰なアクセスが原因 外部API呼び出しの都度SecretsManagerにアクセスしていた const output = await axios .request({ headers: {‘X-Hoge-Token’: await this.#driver.fetchToken()}…略 async fetchToken(): Promise { return (await this.#fetchSecret(process.env.HOGE_TOKEN!)) .SecretString! } async #fetchSecret(secretId: string): Promise { return this.#secretsManagerClient.send( new GetSecretValueCommand({SecretId: secretId,}) )}

Slide 40

Slide 40 text

シークレットをキャッシュして対応 static変数を使い取得済みのシークレットをキャッシュ async #fetchSecret(secretId: string): Promise { 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

Slide 41

Slide 41 text

ログ出⼒の改善

Slide 42

Slide 42 text

テキスト形式のログが出⼒されていた 2022-06-09T12:41:14.236+09:00 ユーザー1がペットの登録を完了しました テキスト形式のログは検索性が低くCloudWatch Logs Insights等 のツールと相性が悪い

Slide 43

Slide 43 text

ログのフォーマットを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" }

Slide 44

Slide 44 text

構造化ログの出⼒処理 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 }) }

Slide 45

Slide 45 text

ロギング⽤オプションの型定義 • システム全体を通して重要な意味を持つ項⽬を最上位のキーに • 外部APIのキー項⽬、DBキー項⽬、APIのパスパラメータ等 • ⼀部の処理においてのみ重要な情報はcontext配下に • 型定義が簡潔に type LogOptions = { message: string petId?: Pet[‘petId'] userId?: User[‘userId’] context?: Object }

Slide 46

Slide 46 text

GAに期待 https://dev.classmethod.jp/articles/aws-lambda-powertools-typescript/

Slide 47

Slide 47 text

まとめ

Slide 48

Slide 48 text

まとめ • システム開発の過程では様々な技術的問 題が発⽣する • 開発初期段階からこれらの課題が想定で きていれば⽣産性は⼤きく低下しない • 先⼈たちのハマりごとを活かしましょう

Slide 49

Slide 49 text

No content