Upgrade to Pro — share decks privately, control downloads, hide ads and more …

実例から学ぶ! AWSを活用したシステム開発の勘所

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

実例から学ぶ! AWSを活用したシステム開発の勘所

「Developers.IO 2022 〜技術で心を揺さぶる3日間〜」 の発表で利用した資料です

Avatar for TomoyaIwata

TomoyaIwata

July 21, 2022
Tweet

More Decks by TomoyaIwata

Other Decks in Technology

Transcript

  1. アジェンダ • 案件概要 • API仕様書の肥⼤化 • デプロイ速度の問題 • RDS Proxyのピン留め問題

    • X-Rayの導⼊ • ENIのDNSスロットリング問題 • ログ出⼒の改善 • まとめ
  2. 単⼀ファイルによる管理の限界 • 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
  3. 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
  4. ディレクトリ構成の⼀例 ├ api.yaml…⼦ファイルを読み込むメインのファイル ├ paths …パスごとにAPIをグルーピングして定義 │ ├ pets.yaml │

    └ users.yaml └ components ├ schemas.yaml…共通のスキーマを定義 └ responses.yaml…共通のレスポンスを定義
  5. 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
  6. Lambda Layers導⼊前後の⽐較 導入前 導入後 全Lambda Functionのビルド時間 100秒超 約40秒 Assetサイズ (圧縮前のファイルサイズ合計)

    約1.5G 約60M Assetサイズ (ZIPファイル1つあたりの平均) 約4.2M 約100K ビルド対象のLambda Function数:110で⽐較
  7. 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 (…略
  8. 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",
  9. [ '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や型定義ファイルを削除
  10. 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,
  11. キャプチャ処理は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.
  12. ラッパークラスにキャプチャ処理を実装 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 }
  13. テスト中に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/
  14. 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,}) )}
  15. シークレットをキャッシュして対応 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
  16. ログのフォーマットを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" }
  17. 構造化ログの出⼒処理 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 }) }