LoginSignup
0
1

More than 1 year has passed since last update.

続・Serverless+GraphQL+NestJS+TypeORM+RDSをAWSにデプロイする&ローカルDocker開発環境整える

Last updated at Posted at 2022-05-17

はじめに

この記事の続きです。

前回の記事で、Serverless+GraphQL+NestJS+TypeORM+RDSという構成でAWSにデプロイすることが出来ました。

今回はローカルでの開発環境を整えます。ローカルでの通常開発に加えて、Dockerでも開発できるようにしていきます。あわせて、データベース接続周りなどの細かい修正とリファクタリングも行います。

今回の成果物はこちらにあります。

データベース接続エラーへの対応

前回作ったマイグレーション周りの実装ですが、ざっと作ったので以前の記事で発生していたデータベース接続エラーが再発していました。

解決するようにプログラムを修正します。

はじめに src/lib/migration.tsを修正します。

src/lib/migration.ts 変更前
import { createConnection, Connection, ConnectionOptions } from 'typeorm';

interface MigrationIndexSignature {
  [key: string]: any;
}

export default class Migration implements MigrationIndexSignature {
  private config: ConnectionOptions;
  private connection: Connection | null;
  [handlerName: string]: any;

  constructor(config: ConnectionOptions) {
    this.config = config;
    this.connection = null;
  }

  private async init() {
    try {
      this.connection = await createConnection(this.config);
    } catch (error) {
      throw error;
    }
  }
// ...省略
src/lib/migration.ts 変更後
import {
  Connection,
  ConnectionOptions,
  ConnectionManager,
  getConnectionManager,
} from 'typeorm';
import ormconfig from 'src/config/ormconfig';

interface MigrationIndexSignature {
  [key: string]: any;
}

export default class Migration implements MigrationIndexSignature {
  private config: ConnectionOptions;
  private connection: Connection | null;
  [handlerName: string]: any;

  constructor(config: ConnectionOptions) {
    this.config = config;
    this.connection = null;
  }

  private async init() {
    try {
      const connectionManager: ConnectionManager = getConnectionManager();

      try {
        if (connectionManager.has(ormconfig.name)) {
          this.connection = connectionManager.get(ormconfig.name);
        }
      } catch (err) {}
      if (!this.connection) {
        this.connection = connectionManager.create(this.config);
      }
      if (!this.connection.isInitialized) {
        await this.connection.initialize();
      }
    } catch (error) {
      throw error;
    }
  }
// ...省略

ConnectionManager経由で接続を行うことで、他の部分と接続周りを一本化しました。

もう一つ、src/config/database.tsも修正します。

src/config/database.ts 変更前
import { Injectable } from '@nestjs/common';
import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm';
import { ConnectionManager, getConnectionManager } from 'typeorm';
import ormconfig from './ormconfig';

@Injectable()
export class TypeOrmConfigService implements TypeOrmOptionsFactory {
  async createTypeOrmOptions(): Promise<TypeOrmModuleOptions> {
    const ormOptions: TypeOrmModuleOptions = {
      ...ormconfig,
      keepConnectionAlive: true,
      autoLoadEntities: true,
    };

    const connectionManager: ConnectionManager = getConnectionManager();
    let options: any;

    if (connectionManager.has('default')) {
      options = connectionManager.get('default').options;
    } else {
      options = ormOptions as TypeOrmModuleOptions;
    }
    return options;
  }
}

src/config/database.ts 変更後
import { Injectable } from '@nestjs/common';
import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm';
import { ConnectionManager, getConnectionManager } from 'typeorm';
import ormconfig from './ormconfig';

@Injectable()
export class TypeOrmConfigService implements TypeOrmOptionsFactory {
  async createTypeOrmOptions(): Promise<TypeOrmModuleOptions> {
    const ormOptions: TypeOrmModuleOptions = {
      ...ormconfig,
      keepConnectionAlive: true,
      autoLoadEntities: false, //ここをfalseに修正
    };

    const connectionManager: ConnectionManager = getConnectionManager();
    let options: any;

    const connectionName = ormconfig.name;
    if (connectionManager.has(connectionName)) {
      options = connectionManager.get(connectionName).options;
    } else {
      options = ormOptions as TypeOrmModuleOptions;
    }
    return options;
  }
}

細かいですが、autoLoadEntitiesfalseに修正しています。また接続の名前をdefaultとベタ書きしていたところを設定オブジェクトから取得するようにリファクタリングしました。

次にsrc/tasks/tasks.service.tsを修正します。

src/tasks/tasks.service.ts 変更前
import { Injectable } from '@nestjs/common';
import {
  ConnectionManager,
  getConnectionManager,
  getRepository,
  Repository,
} from 'typeorm';
import { CreateTaskInput } from './dto/create-task.input';
import { UpdateTaskInput } from './dto/update-task.input';
import { Task } from './entities/task.entity';

@Injectable()
export class TasksService {
  get taskRepostiory(): Repository<Task> {
    const connectionManager: ConnectionManager = getConnectionManager();
    if (connectionManager.has('default')) {
      const entityMetadata = connectionManager
        .get('default')
        .entityMetadatas.find((metadata) => {
          return metadata.tableName === 'task';
        });
      if (entityMetadata) {
        return getRepository(entityMetadata.name);
      } else {
        return null;
      }
    }
  }
// ...以下省略
src/tasks/tasks.service.ts 変更後
import { Injectable } from '@nestjs/common';
import { Repository } from 'typeorm';
import { CreateTaskInput } from './dto/create-task.input';
import { UpdateTaskInput } from './dto/update-task.input';
import { Task } from './entities/task.entity';
import { getEntityRepository } from 'src/config/ormUtils';

@Injectable()
export class TasksService {
  get taskRepostiory(): Repository<Task> {
    return getEntityRepository('task');
  }
// ...以下省略
src/config/ormUtils.ts
import {
  ConnectionManager,
  getConnectionManager,
  getRepository,
  Repository,
} from 'typeorm';
import ormconfig from './ormconfig';

export const getEntityRepository = <T>(name: string) => {
  const connectionManager: ConnectionManager = getConnectionManager();
  if (connectionManager.has(ormconfig.name)) {
    const entityMetadata = connectionManager
      .get(ormconfig.name)
      .entityMetadatas.find((metadata) => {
        return metadata.tableName === name;
      });
    if (entityMetadata) {
      return getRepository(entityMetadata.name) as Repository<T>;
    } else {
      return null;
    }
  }
};

このファイルはエンティティのサービスについて記述したものですが、今後作るエンティティでも同じ記述を行う部分がありますので、再利用可能部分を別ファイルに切り出すリファクタリングを行いました。

ローカル開発環境の整備

ローカル開発環境として、通常にsls offlineで開発する場合とDocker上でsls offlineを実行して開発する場合に対応します。

関連するファイル
Dockerfile
Dockerfile.local
docker-compose.yml
docker.env.default
docker/mysql/my.cnf
serverless.yml
serverless/environment.dev.yml
serverless/environment.local-docker.yml
serverless/environment.local.yml
serverless/startup.sh

上記は関連するファイルの一覧です。以下にざっくりと説明します。

serverless.yml

serverless.ymlに対しては環境ごとの設定を外部に切り出しました。ローカルでの実行やDockerでの実行をstageで分けることで実現しています。

serverless.yml
org: shinobushiva
app: myapp
service: nestjs-typeorm-ts-example
frameworkVersion: '3'

custom:
  environment:
    local: ./serverless/environment.local.yml
    local-docker: ./serverless/environment.local-docker.yml
    dev: ./serverless/environment.dev.yml

provider:
  name: aws
  runtime: nodejs14.x
  region: ap-northeast-1
  stage: ${opt:stage, 'local'} #デフォルトではlocalを使う
  iamRoleStatements:
    - Effect: Allow
      Action:
        - rds-data:*
        - ec2:CreateNetworkInterface
        - ec2:DescribeNetworkInterfaces
        - ec2:DeleteNetworkInterface
      Resource: "*"
  vpc:
    securityGroupIds:
      - Ref: LambdaSecurityGroup
    subnetIds:
      - Ref: PrivateSubnetB
      - Ref: PrivateSubnetC
  # stageごとに異なる環境変数を読み込む
  environment: ${file(${self:custom.environment.${self:provider.stage}})}
# ...以下省略

以下はDockerでの実行時に読み込む設定ファイルです。

serverless/environment.local-docker.yml
DB_HOST: db
DB_PORT: ${env:DB_PORT}
DB_USERNAME: ${env:DB_USERNAME}
DB_PASSWORD: ${env:DB_PASSWORD}
DB_DATABASE: ${env:DB_DATABASE}

Docker

docker-compose.ymlではserverlessの実行環境とMySQLデータベースを作っています。

こちらの記事を参考にしました。

docker-compose.yml
version: '3'
services:
  serverless:
    build:
      context: ./
      dockerfile: Dockerfile.local
      args:
        - AWS_ACCESS_KEY_ID=$AWS_ACCESS_KEY_ID
        - AWS_SECRET_ACCESS_KEY=$AWS_SECRET_ACCESS_KEY
    volumes:
      - ./:/app
    working_dir: /app
    ports:
      - 3000:3000
  db:
    platform: linux/x86_64 # for M1 Mac
    image: mysql:8.0
    volumes:
      - db-store:/var/lib/mysql
      - ./logs:/var/log/mysql
      - ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
    environment:
      - MYSQL_DATABASE=${DB_DATABASE}
      - MYSQL_ROOT_PASSWORD=${DB_PASSWORD}
      - TZ='Asia/Tokyo'
    ports:
      - 3306:3306
volumes:
  db-store:

docker/mysql/my.cnfにはMySQLの設定を記述しています

docker/mysql/my.cnf
# MySQLサーバーへの設定
[mysqld]
# 文字コード/照合順序の設定
character-set-server = utf8mb4
collation-server = utf8mb4_bin

# タイムゾーンの設定
default-time-zone = SYSTEM
log_timestamps = SYSTEM

# デフォルト認証プラグインの設定
default-authentication-plugin = mysql_native_password

# エラーログの設定
log-error = /var/log/mysql/mysql-error.log

# スロークエリログの設定
slow_query_log = 1
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 5.0
log_queries_not_using_indexes = 0

# 実行ログの設定
general_log = 1
general_log_file = /var/log/mysql/mysql-query.log

# mysqlオプションの設定
[mysql]
# 文字コードの設定
default-character-set = utf8mb4

# mysqlクライアントツールの設定
[client]
# 文字コードの設定
default-character-set = utf8mb4

Dockerfile.localは次のようになっています。

これは、こちらを参考にしました。

Dockerfile.local
FROM node:latest
 
ARG AWS_ACCESS_KEY_ID
ARG AWS_SECRET_ACCESS_KEY
 
RUN apt-get update
RUN apt-get install -y \
    python3-pip \
    jq
 
RUN pip3 install awscli --upgrade --user
RUN pip3 install yq
 
RUN apt-get install -y awscli
 
RUN npm install -g serverless serverless-offline
 
RUN sls config credentials --provider aws --key $AWS_ACCESS_KEY_ID --secret $AWS_SECRET_ACCESS_KEY
EXPOSE 3000

CMD ["serverless/startup.sh"]

serverless/startup.shでserverlessのオフライン環境を実行しています。

serverless/startup.sh
#!/bin/bash

cd /app
export $(cat .env | grep -v ^\# | xargs)
sls offline --host 0.0.0.0 --stage local-docker

諸々の実行方法

ローカルでの実行

MySQLデータベースはローカルに起動しておいてください。

$ yarn build
$ sls offline

Dockerでの実行

$ yarn build
$ docker-compose build
$ docker-compose up

AWSへのデプロイ

デプロイ時には stagedevを指定してください

$ yarn build
$ sls deploy --stage dev

まとめ

Docker実行を含めてローカルでの開発環境を整えられました。次はAWSにECR経由でデプロイできるようにしようと思っています。

なお、タイトルのインフレが止まらないのが悩みの種です。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1