はじめに
この記事の続きです。
前回の記事で、Serverless+GraphQL+NestJS+TypeORM+RDSという構成でAWSにデプロイすることが出来ました。
今回はローカルでの開発環境を整えます。ローカルでの通常開発に加えて、Dockerでも開発できるようにしていきます。あわせて、データベース接続周りなどの細かい修正とリファクタリングも行います。
今回の成果物はこちらにあります。
データベース接続エラーへの対応
前回作ったマイグレーション周りの実装ですが、ざっと作ったので以前の記事で発生していたデータベース接続エラーが再発していました。
解決するようにプログラムを修正します。
はじめに 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;
}
}
// ...省略
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
も修正します。
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;
}
}
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;
}
}
細かいですが、autoLoadEntities
をfalse
に修正しています。また接続の名前をdefault
とベタ書きしていたところを設定オブジェクトから取得するようにリファクタリングしました。
次に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;
}
}
}
// ...以下省略
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');
}
// ...以下省略
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
で分けることで実現しています。
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での実行時に読み込む設定ファイルです。
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データベースを作っています。
こちらの記事を参考にしました。
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の設定を記述しています
# 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
は次のようになっています。
これは、こちらを参考にしました。
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のオフライン環境を実行しています。
#!/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へのデプロイ
デプロイ時には stage
にdev
を指定してください
$ yarn build
$ sls deploy --stage dev
まとめ
Docker実行を含めてローカルでの開発環境を整えられました。次はAWSにECR経由でデプロイできるようにしようと思っています。
なお、タイトルのインフレが止まらないのが悩みの種です。