🚀

GitHub ActionsでMariaDBを使う方法【改】

2021/04/10に公開

概要

モチベーション

  • サーバーサイドは Node.js を用いてサービス作りたいよ!
  • テストをきちんと書いていきたいよ!
  • DB はとりあえず無料で使える MariaDB でいきたいよ!
    • しかもデータベースのテーブルの初期化を起動と同時にしたいよ!!
  • 無料枠もある流行りの GitHub Actions の波に乗りたいよ!

という感じのモチベーションで開発を始めたのですが、基礎をつくるだけでもかなり苦労したので備忘録です。

技術的なポイント

  • GitHub Actions を用いて品質を継続的に担保しながら開発
  • Docker を用いてローカルでも CI 上でも MariaDB を立てて使用
  • Jest を用いて JavaScript コードをテスト

なお、Node.js を用いてサーバーを立てる/API を作るところはこの記事の範囲外とします。

記事執筆時点での環境

ツール バージョン
nodenv 1.4.0
Node v14.16.0
npm 7.6.3
MariaDB 10.5.9
Compose 3

ファイル構成

─┬─ .gitignore
 ├─ .github/workflows/ci.yml
 ├─ package.json
 ├─ node_modules/
 ├─ docker
 |   ├─ docker-compose.yml
 |   └─ .env
 ├─ db-utils.js                 DBの基盤JS
 ├─ __tests__/mariadb.js        ↑の動作確認用のテスト
 └─ db.sql                      DB初期設定のSQLファイル

下準備

  • Node.js および npm をインストールしておく
  • Docker および Docker Compose をインストールしておく
  • GitHub のリポジトリ -> Settings -> Secrets で MariaDB 用の環境変数を設定しておく
  • ローカルでの動作確認のためにルートディレクトリでnpm installしておく
Name Value
MYSQL_ROOT_PASSWORD QWERTY1234
MYSQL_USER developer
MYSQL_PASSWORD ASDFGH5678
docker/.env
MYSQL_ROOT_PASSWORD=QWERTY1234
MYSQL_DATABASE=testdb
MYSQL_USER=developer
MYSQL_PASSWORD=ASDFGH5678

コード

GitHub に上がっています
https://github.com/Kyome22/mariadb-test

.gitignore

.DS_Store
node_modules/
.env

.envには本来公にしたくない環境変数が入る想定のため除外します。

Node の設定

package.json
{
  "name": "mariadb-test",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "test": "jest",
    "dcps": "docker-compose -f docker/docker-compose.yml ps",
    "dcup": "docker-compose -f docker/docker-compose.yml up -d",
    "dcdown": "docker-compose -f docker/docker-compose.yml down"
  },
  "dependencies": {
    "dotenv": "^8.2.0",
    "mariadb": "^2.5.3"
  },
  "devDependencies": {
    "jest": "^26.6.3"
  }
}

Docker コンテナの起動や停止をするための便利コマンドを指定してあります。

Docker の初期化ファイル

docker/docker-compose.yml
version: "3"
services:
  db:
    image: mariadb:10.5.9
    container_name: mariadb_test
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    volumes:
      - ../db.sql:/docker-entrypoint-initdb.d/db.sql
    ports:
      - 3306:3306

もしも MariaDB のバージョンも指定したい場合は、そこを変更したり環境変数で差し込むといいと思います。

GitHub Actions の YAML

.github/workflows/ci.yml
name: CI

on:
  push:
    branches:
      - develop

jobs:
  mariadb_test:
    runs-on: ubuntu-20.04
    timeout-minutes: 5
    env:
      MYSQL_ROOT_PASSWORD: ${{ secrets.MYSQL_ROOT_PASSWORD }}
      MYSQL_USER: ${{ secrets.MYSQL_USER }}
      MYSQL_PASSWORD: ${{ secrets.MYSQL_PASSWORD }}

    steps:
      - uses: actions/checkout@v2

      - name: Shutdown Ubuntu MySQL (SUDO)
        run: sudo service mysql stop

      - name: Setup Node
        uses: actions/setup-node@v2
        with:
          node-version: "14.16.0"

      - name: Check Node & npm Version
        run: |
          node --version
          npm --version

      - name: Setup MariaDB
        shell: bash
        env:
          MYSQL_DATABASE: testdb
        run: |
          docker-compose -f ${{ github.workspace }}/docker/docker-compose.yml up -d

      - name: Run Test
        run: |
          npm ci
          npm run test

Action では、Node のセットアップと MariaDB の起動、Jest での JS 動作テストを行っています。

データベース初期化用の SQL ファイル

db.sql
USE testdb;

CREATE TABLE table1
(
  user_id   VARCHAR(8)  UNIQUE NOT NULL,
  user_name VARCHAR(20) UNIQUE NOT NULL,
);

INSERT INTO table1
  (user_id, user_name)
values("A0001", "Mike");
INSERT INTO table1
  (user_id, user_name)
values("A0002", "Jhon");
INSERT INTO table1
  (user_id, user_name)
values("A0003", "Caitlyn");

適当なテーブルを作って初期データを挿入してみています。ファイルの権限の問題で、データベースの初期化がうまくいかないことがあるので、$ chmod +x db.sqlなどで実行権限を付与しておくといいです。

js

MariaDB を Node.js で扱うための便利クラス

db-utils.js
require("dotenv").config({ path: "docker/.env" });
const mariadb = require("mariadb");

class DBUtils {
  constructor() {
    this.pool = mariadb.createPool({
      host: "localhost",
      user: process.env.MYSQL_USER,
      password: process.env.MYSQL_PASSWORD,
    });
  }

  poolEnd() {
    this.pool.end();
  }

  async postQuery(queryStr) {
    let conn;
    try {
      conn = await this.pool.getConnection();
      return await conn.query(queryStr).then((array) => {
        delete array.meta;
        return array;
      });
    } catch (error) {
      throw error;
    } finally {
      conn.end();
    }
  }
}

module.exports = {
  DBUtils: DBUtils,
};

poolEnd()を忘れると、テストが永遠に終わらなくなり、GitHub Actions を 6 時間消費することになるかもしれません。

上の便利クラスの動作確認をしつつ、MariaDB に初期データが読み込まれているか確認するためのテスト(Jest を使用します)

__tests__/mariadb.js
const { DBUtils } = require("../db-utils");

// console.error() の mock
jest.spyOn(console, "error").mockImplementation((...args) => {
  console.log(args.join(", "));
});

const sut = new DBUtils();

afterAll(() => {
  sut.poolEnd();
});

describe("DBの初期化の確認", () => {
  it("Database: testdb が存在する", (done) => {
    sut
      .postQuery("SHOW databases")
      .then((actual) => {
        expect(actual).toEqual(
          expect.arrayContaining([
            {
              Database: "testdb",
            },
          ])
        );
        done();
      })
      .catch((error) => {
        done(error);
      });
  });

  it("Table: table1 が存在する", (done) => {
    sut
      .postQuery("SHOW tables FROM testdb")
      .then((actual) => {
        expect(actual).toEqual(
          expect.arrayContaining([
            {
              Tables_in_testdb: "table1",
            },
          ])
        );
        done();
      })
      .catch((error) => {
        done(error);
      });
  });

  it("table1 の中のデータが格納されている", (done) => {
    sut
      .postQuery("SELECT * FROM testdb.table1")
      .then((actual) => {
        expect(actual).toEqual(
          expect.arrayContaining([
            {
              user_id: "A0001",
              user_name: "Mike",
            },
            {
              user_id: "A0002",
              user_name: "Jhon",
            },
            {
              user_id: "A0003",
              user_name: "Caitlyn",
            },
          ])
        );
        done();
      })
      .catch((error) => {
        done(error);
      });
  });
});

ローカル環境での動作確認

Docker ですでにコンテナーが起動していないか確認
$ npm run dcps

コンテナーを起動
$ npm run dcup

テストを実行(うまく起動していれば jest のテストが実行されるはず)
$ npm run test

コンテナーを停止
$ npm run dcdown

GitHub Actions 上での動作確認

今回の例では、developブランチへの push で Action が動くようにしてあります。

ハマりどころ

  • 現状の GitHub Actions はservices.volumesの機能が未完成なため、Docker Composeを用いるのが吉。データベース初期化のための SQL を読み込ませる方法を模索するのにかなり時間を消費しました。getong/mariadb-actionの Action を使う手もありますが、ソースをよくみると、Docker コンテナ上で Docker コンテナを起動していて無駄です。
  • Docker 経由するため、ポート関連のデバッグが大変でした。
  • npm runを使用しているため、データベースを使用するユーザーがmariadb-test@0.0.1のようにPackage.jsonの設定に従うようになるみたいですが、それに気付くのに時間を要しました。
  • シンプルにnode_modulesmariadbの扱い方の前例記事が役に立たず、試行錯誤しながら実装するのが大変でした。

Discussion