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

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

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

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

TomoyaIwata

July 21, 2022
Tweet

More Decks by TomoyaIwata

Other Decks in Technology

Transcript

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  5. API仕様書の肥⼤化

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  8. 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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  11. 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

    View full-size slide

  12. デプロイ速度の問題

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  21. 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,

    View full-size slide

  22. RDS Proxyのピン留め問題

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  28. X-Rayの導⼊

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  33. キャプチャ処理は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.

    View full-size slide

  34. ラッパークラスにキャプチャ処理を実装
    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
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  38. ここでもX-Rayが活躍

    View full-size slide

  39. 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,})
    )}

    View full-size slide

  40. シークレットをキャッシュして対応
    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

    View full-size slide

  41. ログ出⼒の改善

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide