Serverlessなフレームワークを持つAWSラムダへのNESTJS APIの配備


あなたはどのように展開し、APIをホストすることができますどのように簡単に疑問がありますか?スケーラブル、安定、ケーキの一部を展開し、ほとんど何もコスト.この記事の目的は、それを証明することです.私たちは、APIゲートウェイの背後にある単一のラムダ関数としてAWSクラウドに配備される単純なAPIを開発します.ラムダがそのように使用されるべきかどうかは、私が喜んでビールを議論する異なった話題です.🙂🍺

この記事から何を期待するのですか。


我々は、ちょうどNESTJSフレームワークとそのきちんとした開発経験の表面をひっかいます.一旦Serverlessなフレームワークでそれを配線するならば、我々のAPIがどれくらい速く日の光を見ることができるかについて学びます.これを実証するために、私たちは歌のデータベースを管理するAPI -歌APIを作成します、そして、我々はそれが役に立たないと偽ります.

要件


曲のAPIは、データベース内のすべての曲のリストには、単一の曲の詳細を取得し、曲を削除するエンドポイントを公開します.要件を与えられて、歌モデルは特性を持ちますid , name , artist , length 秒単位でgenre and album . APIエンドポイントは次のようになります.
  • GET songs
  • GET songs/:id
  • POST songs
  • DELETE songs/:id
  • テックスタック


  • NestJS - 静的サイドアプリケーションを作成するための強力なフレームワーク

  • TypeORM - TypeScriptのためのORMライブラリは、データベースアクセスのために巣とうまく統合されます

  • Serverless Framework - 簡単に使用してフレームワークを開発し、サーバーレスアプリケーションを展開

  • Serverless Jetpack - 我々のコードをAWSラムダに展開するパッケージを設定する低configプラグイン

  • Serverless Express - 我々の「プレーンな」NESTJS APIを作る図書館は、Serverlessでうまく遊びます
  • 管理サービスLambda , API Gateway and RDS
  • 私はそれが楽しいと簡単に十分なので、掘りましょうね.

    巣CLIをインストールし、新しいプロジェクトとモジュールを作る


    npm i -g @nestjs/cli
    nest new songs-api
    
    この時点でAPIは既に設定されていますnpm run start オープンlocalhost:3000 Hello World Responseを見るにはこれは可能ですmain.ts プロジェクトルートで生成されるファイル:
    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
      await app.listen(3000);
    }
    bootstrap();
    
    今、私たちは創造するつもりですsong モジュールで、コントローラ、サービス、エンティティ定義を含みます.これらのそれぞれを個別に作成することができますが、ネストCLIは、モジュールを作成するための有用なコマンドを提供し、すべての必要なファイルを1つの移動します.残りのAPIを作成するときに便利です.
    nest generate resource song
    
    の骨格song モジュールが生成されます.次に、データベースにアクセスするための依存関係をインストールする必要があります.APIはMySQLデータベースの上で実行されるので、次のライブラリをプロジェクトに追加する必要があります.
    npm install --save @nestjs/typeorm typeorm mysql2
    

    実装


    モジュールのスケルトンを生成する便利だったが、もちろん我々のビジネスロジックを記述する必要があります.おそらく、我々はすべての生成されたDTsを必要としないでしょう、我々はコントローラに若干の経路を変えるか、加えるかもしれません、そして、もちろん、我々の実体を実装する必要があります.
    typeRM依存関係をインストールしたので、上記の仕様に従って、曲エンティティのオブジェクトリレーショナルマッピングを構成するために使用しましょう.
    import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
    
    @Entity()
    export class Song {
      @PrimaryGeneratedColumn()
      id: number;
    
      @Column()
      name: string;
    
      @Column()
      artist: string;
    
      @Column()
      duration: number;
    
      @Column()
      genre: string;
    
      @Column()
      album: string;
    }
    
    今ではモジュール定義にimportを追加する必要があります.
    import { Module } from '@nestjs/common';
    import { SongService } from './song.service';
    import { SongController } from './song.controller';
    import { TypeOrmModule } from '@nestjs/typeorm';
    import { Song } from './entities/song.entity';
    
    @Module({
      imports: [TypeOrmModule.forFeature([Song])],
      controllers: [SongController],
      providers: [SongService],
    })
    export class SongModule {
    }
    
    さて、サービス層を実装しましょう.SongService 用途Repository typeRMがデータベースにアクセスするために提供されます.
    import { Injectable } from '@nestjs/common';
    import { InjectRepository } from '@nestjs/typeorm';
    import { Repository } from 'typeorm';
    import { Song } from './entities/song.entity';
    
    @Injectable()
    export class SongService {
      constructor(
        @InjectRepository(Song) private songRepository: Repository<Song>,
      ) {
      }
    
      async create(song: Song): Promise<Song> {
        return await this.songRepository.save(song);
      }
    
      async findAll(): Promise<Song[]> {
        return await this.songRepository.find();
      }
    
      async findOne(id: number): Promise<Song> {
        return await this.songRepository.findOne({ id });
      }
    
      async remove(id: number): Promise<void> {
        await this.songRepository.delete(id);
      }
    }
    
    簡単にするために、私はDTOとして実体を再利用するでしょうdto 生成されたフォルダ.それから、コントローラとサービスは次のように書き換えられます.
    import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post } from '@nestjs/common';
    import { SongService } from './song.service';
    import { Song } from './entities/song.entity';
    
    @Controller('songs')
    export class SongController {
      constructor(private readonly songService: SongService) {
      }
    
      @Post()
      async create(@Body() song: Song): Promise<Song> {
        return await this.songService.create(song);
      }
    
      @Get()
      async findAll(): Promise<Song[]> {
        return await this.songService.findAll();
      }
    
      @Get(':id')
      async findOne(@Param('id', ParseIntPipe) id: number): Promise<Song> {
        return await this.songService.findOne(id);
      }
    
      @Delete(':id')
      async remove(@Param('id', ParseIntPipe) id: number): Promise<void> {
        await this.songService.remove(id);
      }
    }
    
    一般的なルールとしては、DTDとエンティティクラスの両方に対して常に良いです.

    データベース


    データベースはかなり数回言及されていますが、どこですか?🤔
    まず、ローカルのMySQLデータベースに対してコードをテストしましょう.ローカルサーバーに接続したら、次のinitスクリプトを実行します.
    CREATE DATABASE `songsapi`;
    
    USE `songsapi`;
    
    CREATE TABLE `song`
    (
        `id`       int(11)      NOT NULL AUTO_INCREMENT,
        `name`     varchar(200) NOT NULL,
        `artist`   varchar(200) NOT NULL,
        `duration` int(11)      DEFAULT NULL,
        `genre`    varchar(45)  DEFAULT NULL,
        `album`    varchar(200) DEFAULT NULL,
        PRIMARY KEY (`id`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8;
    
    その後、APIは、次の設定を追加することによって、それに接続できることを確認しますapp.module.ts :
    import { Module } from '@nestjs/common';
    import { AppController } from './app.controller';
    import { AppService } from './app.service';
    import { TypeOrmModule } from '@nestjs/typeorm';
    import { SongModule } from './song/song.module';
    
    @Module({
      imports: [
        TypeOrmModule.forRoot({
          type: 'mysql',
          host: 'localhost',
          port: 3306,
          username: 'root',
          password: 'xxx',
          database: 'songsapi',
          autoLoadEntities: true,
        }),
        SongModule,
      ],
      controllers: [AppController],
      providers: [AppService],
    })
    export class AppModule {
    }
    
    上記の値は、ローカルデータベースの設定に対応するハードコードを自由に感じてください.

    実行する🚀


    種類npm run start ターミナルで、そして、数秒で、それは起きていて、走らなければなりません.リクエストを送信してテストします.
    curl -X POST 'localhost:3000/songs' \
    --header 'Content-Type: application/json' \
    --data-raw '{
        "name": "In corpore sano",
        "artist": "Konstrakta",
        "duration": 182,
        "album": "In corpore sano",
        "genre": "pop"
    }'
    # Response:
    # {"name":"In corpore sano","artist":"Konstrakta","duration":182,"album":"In corpore sano","genre":"pop","id":1}
    
    # Get a single song by id
    curl 'localhost:3000/songs/1'
    # Response:
    # {"name":"In corpore sano","artist":"Konstrakta","duration":182,"album":"In corpore sano","genre":"pop","id":1}
    
    動く!我々はローカルAPIをテストした今、それは雲に展開し、それが世界に利用できるようにする時だ!

    クラウドへの移行🌥


    注意1 :既にAWSアカウントを持っていると仮定します.
    注2 :手順に従う十分な権限があることを確認してください.IAMユーザの場合、ショートカットはarn:aws:iam::aws:policy/AdministratorAccess 管理ポリシーを添付.

    AWSアカウント資格情報の設定


    AWS資格情報ファイルにプロファイルを追加します.
    ...
    [profile-name]
    region=your_region
    aws_access_key_id=xxx
    aws_secret_access_key=yyy
    aws_session_token=... (if applicable)
    ...
    
    その後、環境変数を設定してプロファイルをアクティブにします.
    export AWS_PROFILE=profile-name
    
    あなたはAWSクラウドと対話する準備ができているはずです.aws s3 ls

    フリーティアRDSデータベースのスピンアップ


    これまでのところ、我々は正常にローカルMySQLデータベースを使用してAPIをテストしてきましたが、今私たちはAWS上で1つが必要です.AWSコンソールで手動で行うことができます.
    警告:あなたのアカウントの価格設定と自由層適格性についてお知らせください.すべての新しいAWSの顧客は、特定のサービスのための無料ティアの1年間を取得する必要があります.さもなければ、あなたは公式AWS RDS価格設定ガイドで説明されるように若干のコストを被るかもしれませんhttps://aws.amazon.com/rds/mysql/pricing
    AWSTemplateFormatVersion: '2010-09-09'
    
    Resources:
      SongsDatabase:
        Type: AWS::RDS::DBInstance
        Properties:
          AllocatedStorage: 20
          DBInstanceClass: db.t3.micro
          DBInstanceIdentifier: songs-database
          PubliclyAccessible: true
          StorageType: gp2
          MasterUsername: xxx # change
          MasterUserPassword: yyy # change
          Engine: mysql
          EngineVersion: 8.0.28
    
    上記のファイルをRDSとして保存します.例えばYAMLで、AWS CLIを使います.
    aws cloudformation deploy --stack-name songs-api-db --template-file rds.yaml
    
    数分でデータベースが準備されます.
    データベースURLをRWSに移動したり、次のコマンドを使用してCloudFormationのエクスポートを一覧表示することでAWSコンソールを取得します.aws cloudformation list-exports . ローカルのインスタンスで行われたデータベースのinitスクリプトを実行します.
    今私たちのデータベースは、クラウドで実行されている、それはローカルの1つの代わりにRDSのデータベースで動作するように我々のアプリを再構成する時間です-ので、URL、パスワード、残りのような関連する詳細を更新することを忘れないでくださいapp.module.ts ファイル.その後は、次のSTEでカバーされる展開する準備が整いました.

    Serverlessフレームワークのインストールと設定


    ServerlessフレームワークCLIをインストールします
    npm install -g serverless
    
    プロジェクトの根源に、我々はserverless.yaml 配備を記述するファイル:
    service: songs-api
    
    frameworkVersion: '3'
    
    plugins:
      - serverless-jetpack
    
    provider:
      name: aws
      runtime: nodejs14.x
      region: eu-central-1 # or whatever your region is
    
    functions:
      api:
        handler: dist/lambda.handler
        events:
          - http:
              method: any
              path: /{proxy+}
    
    この設定では、APIゲートウェイは、ラムダ関数と我々のNESTJSアプリは、それを処理するプロキシごとに要求されます.The handler 値は、我々のアプリのエントリポイントを含むファイルであり、分で説明されます.
    注意serverless-jetpack プラグイン-それはServerlessのために非常に効率的に我々のアプリを包装の世話をする.これには他のプラグインがありますが、最近これを発見しました.これまで使ってきた人よりもずっと速いです.続きを読むits official github page .
    NPMを使用してdev依存関係としてインストールします.
    npm i -D serverless-jetpack
    
    ここでは、ラムダ環境で動作するようにAPI - Serverless Expressライブラリを展開する前にもう一つの手順があります.

    サーバーレスエクスプレス


    インストールserverless-express Bootstrapsを使用するライブラリは、ラムダで動作するようにアプリケーションを表現します
    npm i @vendia/serverless-express
    
    次に、ソースフォルダでlambda.ts 上記で参照したように、エントリポイントであるラムダハンドラ関数を含むファイルserverless.yaml .
    import { configure as serverlessExpress } from '@vendia/serverless-express';
    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    
    let cachedServer;
    
    export const handler = async (event, context) => {
      if (!cachedServer) {
        const nestApp = await NestFactory.create(AppModule);
        await nestApp.init();
        cachedServer = serverlessExpress({ app: nestApp.getHttpAdapter().getInstance() });
      }
    
      return cachedServer(event, context);
    }
    

    ビルド、配備、テスト🚀


    最後に、APIをクラウドに配備します.これはかなり簡単です.最初にビルドする必要があります.
    npm run build
    
    ... それから、
    serverless deploy
    
    すぐに、あなたはそれを追加し、リストを削除し、曲を削除することによってそれをテストするために自由に感じ、APIをヒットするために使用できる自動生成されたURLを取得します.あなたはログを見ることができますし、アプリを実行する方法を監視ラムダとCloudWatch AWS管理コンソールのサービス

    クリーンアップ


    あなたのAPIを少し再生した後、それはあなたのAWSクラウドで作成されたすべてのリソースをクリーンアップするための時間です.この手順を厳密に実行すると、2つのCloudFormationスタックが配備されます.データベースには1つ、Serverless展開に対しては次のようになります.コンソールを介して手動で削除するか、次のCLIコマンドを実行します.
    serverless remove
    aws cloudformation delete-stack --stack-name songs-api-db
    

    結論と最終思考


    私は、あなたがこれまでにそれを作ったことを望みます、そして、私はあなたにあまり退屈しませんでした.主な焦点はAWSラムダでのServerlessな展開についてであったが、この記事ではTorrmを使った簡単なNSTEJSプロジェクトを設定し、CloudFormationを通してAWS上のRDS MySQLデータベースインスタンスを作成するような方法をいくつか取り上げた.
    この種のAPIがより良いスケールになるのは素晴らしいことですRDS Proxy データベースの先頭にあります.また、AWS認知を使用することによってユーザー認証を加えることは、このセットアップにうまく合う何かです.非常に最近Lambda function URL 機能は、APIゲートウェイの必要性を排除するが、他のトレードオフを持っています.
    確かにいくつかのセキュリティ面でこれを議論する価値があるこれは生産準備になるが、それはこの記事の範囲を超えています.
    読んでいただきありがとうございます、ご質問やご提案があればコメントを自由に感じる!