はじめに
NestJSの開発環境をローカルのDockerコンテナ上に構築し、簡単なAPIの実装を行なってから、本番環境としてCloud Run + Cloud SQLへデプロイする方法を記載します。
DockerコンテナでNestJSを起動する
NestJSアプリとデータベース、両方のコンテナを同時に起動するためにDocker Composeを使いたいので、docker-compose.ymlを作成します。
$ mkdir my_project && cd my_project
$ touch docker-compose.yml
ローカルの開発環境なのでデータベースのパスワードなどはべた書きで構いません。
ここでのポイントはNestJSアプリのルートディレクトリをnest-app
という一つ下の階層に
しておくこことと、node_modules
を匿名ボリュームに設定することです。
そうしないと、バインドマウントされてnode_modules
の中身が消えてしまいます。
version: '3'
services:
db:
image: mysql:8.0.27
volumes:
- ./data/mysql:/var/lib/mysql
- ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
- ./logs:/var/log/mysql
environment:
MYSQL_ROOT_PASSWORD: 'my_database_root_password'
MYSQL_DATABASE: 'my_database_name'
MYSQL_USER: 'my_database_user'
MYSQL_PASSWORD: 'my_database_password'
TZ: 'Asia/Tokyo'
ports:
- "3306:3306"
node:
container_name: nest-app
build:
context: ./nest-app
dockerfile: Dockerfile
volumes:
- ./nest-app:/usr/src/app
- /usr/src/app/node_modules
ports:
- "3000:3000"
stdin_open: true
depends_on:
- db
Dockerfileも作成します。
$ mkdir nest-app && cd nest-app
$ touch Dockerfile
Dockerfileは最小限の構成にします。
FROM node:16-alpine3.14
WORKDIR /usr/src/app
RUN npm install -g @nestjs/cli
Dockerイメージのビルドとコンテナの起動を行います。
$ docker-compose up -d --build
コンテナが起動しているか、確認します。
$ docker compose ps
NAME COMMAND SERVICE STATUS PORTS
my_project-db-1 "docker-entrypoint.s…" db running 0.0.0.0:3306->3306/tcp
nest-app "docker-entrypoint.s…" node running 0.0.0.0:3000->3000/tcp
NestJSのプロジェクトを作成します。
$ docker compose exec node nest new .
どのパッケージマネージャーを使うか訊かれるので、yarnを選択します。
? Which package manager would you ❤️ to use?
npm
❯ yarn
pnpm
プロジェクトの作成が完了したら、さっそくNestJSアプリを起動します。
$ docker compose exec node yarn start:dev
以下にアクセスして、Hello World!
と表示されいれば、成功です!
http://localhost:3000/
## DockerfileにNestJS起動までの処理を追加する
ここまでの手順がDockerイメージがビルドされる時に自動で実行されるようにDokerfileに
処理を追加します。
FROM node:16-alpine3.14
WORKDIR /usr/src/app
RUN npm install -g @nestjs/cli
+ COPY package.json ./
+ COPY yarn.lock ./
+ RUN yarn install
+ COPY . .
+ ENTRYPOINT ["yarn", "start:dev"]
## TypeORMをインストールして、NestJS内でTypeORMを有効化する
今回はNestJSでデータベースを操作するためにTypeORMというO/Rマッパーを使います。
以下のコマンドで、TypeORMをインストールします。
$ docker compose exec node yarn add @nestjs/typeorm typeorm mysql2
次に、app.module.tsに以下を追加します。
import { Module } from '@nestjs/common';
+ import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [
+ TypeOrmModule.forRoot(),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
さらに、データベース接続情報としてルートディレクトにormconfig.jsonファイルを以下の内容で追加します。
“synchronize”: true
を設定しておくと自動的にモデル定義をデータベースに反映してくれます。開発中はtrue
にしておくとめちゃくちゃ便利なのでおすすめです。
{
"type": "mysql",
"host": "db",
"port": 3306,
"username": "my_database_user",
"password": "my_database_password",
"database": "my_database_name",
"entities": ["dist/**/*.entity{.ts,.js}"],
"logging": true,
"synchronize": true
}
## ユーザをデータベースで操作できるように実装する
今回はuserモデルを作成し、データベースで扱えるように実装していきます。
まずはコマンドでCRUDのひな型を作成します。
$ docker compose exec node nest g resource users
APIの方式はRESTを選択します。
? What transport layer do you use?
❯ REST API
GraphQL (code first)
GraphQL (schema first)
Microservice (non-HTTP)
WebSockets
エントリポイントも作成します。
? Would you like to generate CRUD entry points? (Y/n)
ここまでのディレクトリ構造は以下のようになります。(distとtestとspecファイルは除外)
$ tree -I "dist|test|*.spec.ts"
.
├── Dockerfile
├── README.md
├── nest-cli.json
├── node_modules
├── ormconfig.json
├── package-lock.json
├── package.json
├── src
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── app.service.ts
│ ├── main.ts
│ └── users
│ ├── dto
│ │ ├── create-user.dto.ts
│ │ └── update-user.dto.ts
│ ├── entities
│ │ └── user.entity.ts
│ ├── users.controller.ts
│ ├── users.module.ts
│ └── users.service.ts
├── tsconfig.build.json
├── tsconfig.json
└── yarn.lock
user.entity.tsでUserモデルの定義を行います。
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number
@Column({ unique: true })
email: string;
@Column()
name: string;
}
users.service.tsでデータベース操作の処理を実装します。
import { Injectable } from '@nestjs/common';
+ import { InjectRepository } from '@nestjs/typeorm';
+ import { Repository } from 'typeorm';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
+ import { User } from './entities/user.entity';
@Injectable()
export class UsersService {
+ constructor(
+ @InjectRepository(User)
+ private readonly userRepository: Repository<User>,
+ ) {}
+
create(createUserDto: CreateUserDto) {
- return 'This action adds a new user';
+ return this.userRepository.create(createUserDto);
}
findAll() {
- return `This action returns all users`;
+ return this.userRepository.find();
}
findOne(id: number) {
- return `This action returns a #${id} user`;
+ return this.userRepository.findOne(id);
}
update(id: number, updateUserDto: UpdateUserDto) {
- return `This action updates a #${id} user`;
+ return this.userRepository.update(id, updateUserDto);
}
remove(id: number) {
- return `This action removes a #${id} user`;
+ return this.userRepository.delete(id);
}
}
実装できたので、user登録と一覧のAPIを実行して動作確認します。
$ curl -X POST http://localhost:3000/users -d '{"email": "hoge@example.com", "name": "hogehoge"}' -H "Content-Type: application/json"
{"email":"hoge@example.com","name":"hogehoge","id":1}
$ curl http://localhost:3000/users
[{"id":1,"email":"hoge@example.com","name":"hogehoge"}]
これでローカルでの実装は完了したので、いよいよGCPにデプロイしていきます。
## Cloud Runで動作するように設定
NestJSアプリをCloud Runでも動作するように設定します。
main.tsで以下の変更を加えます。
- 環境変数PORTで起動するように修正
- '0.0.0.0' を追加して外部からのアクセスを許可する
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
+ const port = Number(process.env.PORT) || 3000; // 環境変数PORTで起動するように修正
+ await app.listen(port, '0.0.0.0'); // '0.0.0.0' を追加して外部からのアクセスを許可する
}
bootstrap();
ここまででアプリ側の準備はできました。
## GCPのプロジェクトを作成する
次のコマンドでGCPにログインし、プロジェクトを作成します。
[プロジェクトID]、[プロジェクト名]はご自身のものに置き換えてください。
$ gcloud auth login
$ gcloud projects create [プロジェクトID] --name "[プロジェクト名]"
$ gcloud config set project [プロジェクトID]
## Cloud SQLインスタンスを起動する
Cloud SQLコンソールから新しいインスタンスを作成します。
「請求先アカウント設定」のポップアップが表示されたらアカウントを設定しましょう。
「インスタンスを作成」ボタンをクリックして「データベースエンジンの選択」はMySQLを選択します。
APIが有効化されてなければ「APIを有効にする」ボタンをクリックします。
インスタンスの情報を入力して、インスタンスを作成します。
左のメニューからユーザとデータベースも作成しておきます。
ユーザはこちらから
## Secret Managerでシークレットを設定する
Secret Managerを使うと本番環境の環境変数がセキュアに利用できます。
Secret ManagerコンソールでAPIが有効化されてなければ「有効にする」ボタンをクリックします。
APIを有効化したらコマンドでデータベースに接続するための情報をシークレットに設定していきます。まずは、シークレットを作成します。
$ gcloud secrets create typeorm_connection
$ gcloud secrets create typeorm_driver_extra
$ gcloud secrets create typeorm_port
$ gcloud secrets create typeorm_username
$ gcloud secrets create typeorm_password
$ gcloud secrets create typeorm_database
$ gcloud secrets create typeorm_entities
$ gcloud secrets create typeorm_logging
$ gcloud secrets create typeorm_synchronize
シークレットに値を設定します。
ここでの注意点は、socketPath
を指定するときはTYPEORM_DRIVER_EXTRA
環境変数に
json形式で記述しないといけないというところです。
$ printf "mysql" | gcloud secrets versions add typeorm_connection --data-file=-
$ printf '{"socketPath": "/cloudsql/[プロジェクトID]:asia-northeast1:my-db"}' | gcloud secrets versions add typeorm_driver_extra --data-file=-
$ printf "3306" | gcloud secrets versions add typeorm_port --data-file=-
$ printf "my-username" | gcloud secrets versions add typeorm_username --data-file=-
$ printf "my-passowrd" | gcloud secrets versions add typeorm_password --data-file=-
$ printf "my-database" | gcloud secrets versions add typeorm_database --data-file=-
$ printf "dist/**/*.entity.js" | gcloud secrets versions add typeorm_entities --data-file=-
$ printf "true" | gcloud secrets versions add typeorm_logging --data-file=-
$ printf "true" | gcloud secrets versions add typeorm_synchronize --data-file=-
## シークレットにアクセスするために権限を付与する
権限を付与するサービスアカウントを調べるためにコマンドを実行します。
この場合、220306768224-compute@developer.gserviceaccount.com
がサービスアカウントになります。
$ gcloud iam service-accounts list
DISPLAY NAME EMAIL DISABLED
Compute Engine default service account 220306768224-compute@developer.gserviceaccount.com False
サービスアカウントにシークレットにアクセスするための権限を付与します。
$ gcloud projects add-iam-policy-binding [プロジェクトID] \
--member=serviceAccount:220306768224-compute@developer.gserviceaccount.com \
--role=roles/secretmanager.secretAccessor
## Cloud Runにデプロイする
Cloud Run APIコンソールにアクセして「有効にする」をクリックします。
「管理」をクリックして画面右上の「認証情報を作成」をクリックします。
「Cloud Run Admin API」を選択して、「アプリケーションデータ」を選択し、
「いいえ、使用していません」を選択します。
シークレットを環境変数として公開するには—update-secrets
オプションで設定します。
$ gcloud run deploy nestjs-app --region asia-northeast1 --source . --add-cloudsql-instances=[プロジェクトID]:asia-northeast1:my-db \
--update-secrets=TYPEORM_CONNECTION=typeorm_connection:1 \
--update-secrets=TYPEORM_DRIVER_EXTRA=typeorm_driver_extra:1 \
--update-secrets=TYPEORM_PORT=typeorm_port:1 \
--update-secrets=TYPEORM_USERNAME=typeorm_username:1 \
--update-secrets=TYPEORM_PASSWORD=typeorm_password:1 \
--update-secrets=TYPEORM_DATABASE=typeorm_database:1 \
--update-secrets=TYPEORM_ENTITIES=typeorm_entities:1 \
--update-secrets=TYPEORM_LOGGING=typeorm_logging:1 \
--update-secrets=TYPEORM_SYNCHRONIZE=typeorm_synchronize:1
最後に動作確認してみます。
$ curl -X POST https://nestjs-app-tfrtn4c2ca-an.a.run.app/users -d '{"email": "hoge@example.com", "name": "hogehoge"}' -H "Content-Type: application/json"
{"email":"hoge@example.com","name":"hogehoge","id":1}
$ curl https://nestjs-app-tfrtn4c2ca-an.a.run.app/users
[{"id":1,"email":"hoge@example.com","name":"hogehoge"}]
正常に動作することが確認できました
以上で、開発環境の構築から実装、そして本番環境のデプロイまで完了しました!
まとめ
-
node_modules
は匿名ボリュームで - TypeORMでの開発中は
"synchronize": true
が便利 - Cloud Runの本番環境で環境変数を利用したいときはSecret Managerを使う
- socketPathを指定するときはTYPEORM_DRIVER_EXTRA環境変数に
json形式で記述する