LoginSignup
3

More than 1 year has passed since last update.

NestJSのDockerコンテナをCloud Run + Cloud SQLへデプロイする方法

Last updated at Posted at 2022-01-21

はじめに

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を有効にする」ボタンをクリックします。
インスタンスの情報を入力して、インスタンスを作成します。

Untitled.png

左のメニューからユーザとデータベースも作成しておきます。
ユーザはこちらから
Untitled_1.png

データベースはこちらから
Untitled_2.png

 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」を選択して、「アプリケーションデータ」を選択し、
「いいえ、使用していません」を選択します。
Untitled_3.png

シークレットを環境変数として公開するには—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形式で記述する

参考文献

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
3