hosting データベース ロリポップ ムームードメイン ヘテムル CI ISR

DBのリストアテストを全自動化した話

hosting データベース ロリポップ ムームードメイン ヘテムル CI ISR

ホスティング事業部の業務信頼性向上チームでエンジニアをしているはらちゃんです。 先日STREET FIGHTER 6のオープンベータに参加し、友人にボコボコに負けました。 製品版買っていい勝負ができるように特訓を重ねたいと思います。

今回、ホスティング事業部のサービスであるロリポップ、ムームードメイン、ヘテムル、おさいぽのDBリストアテストを自動化したので紹介します。

  1. まず業務信頼性向上チームとは?
  2. リストアテストを継続的にやっている理由
  3. なぜ自動化したのか
  4. 全体像
  5. 具体的な実装
  6. 実装時に困ったこと
    1. dumpのサイズが大きすぎて通常のrunnerではリストアテストができない場合
    2. scpをするアカウントにdumpファイルを操作する権限がない場合
    3. dumpファイルのファイル名が微妙に違ってうまく指定できない場合
  7. 終わりに

まず業務信頼性向上チームとは?

最初に、自分の所属している業務信頼性向上チームの紹介です。 ホスティング事業部内では、Internal System Reliability(業務信頼性向上)の頭文字をとってISRチームと呼ばれています(以下ISRチーム)。 ISRチームは社内で使用する顧客管理システムや業務ツールなどの信頼性を高めるチームです。 加えて、内部統制を含む業務プロセス改善を行うことやDX推進を支援することで、事業部全体の生産性向上をミッションとしています。 ざっくり言うと、一緒に働く仲間が働きやすい環境を整えるチームです。 現在2名(2023/06/01時点)のメンバーで、さまざまな業務改善やホスティング事業部で使用している社内システムの運用を行っています。
売上集計システムのマイグレーションを行った際の記事も投稿しているのでぜひ見てみてください!

リストアテストを継続的にやっている理由

データベースのリストアテストを継続的にやっているのは、有事の際にしっかりとデータベースを復旧するためです。 リストアテストを行うことで、リストアに使用したバックアップファイルの完全性をある程度担保できます。 これで、何かあった際に「このバックアップファイル、リストアできない!」ということを未然に防ぐことができます。

なぜ自動化したのか

今回DBのリストアテストを自動化したのには下記の理由があります。

  • リストアテスト実施の責務がインフラエンジニアからアプリケーションエンジニア側に変わった
  • 月初作業として手動でシェルスクリプトを使ったリストアテストを実施していた
    • 検証のたびVMを立てて手動でシェルスクリプトを実行したくなかった

このように、テスト実施者が変わったこのタイミングでもっと楽に運用できるように自動化を行いました。

全体像

全体の構成は下記のようになっています。

構成図

DBからdumpをしたファイル自体は、各DBサーバーで定期実行され所定のディレクトリに保管されています。
今回の自動化では下記一連の流れを自動化しました。

  1. 各DBサーバーよりdumpファイルの取得
  2. 一時的なDBインスタンスへリストア
  3. リストアしたDBへ対しSQLクエリを発行し動作のテスト
  4. サマリーの出力

今回の自動化では、この1~3の処理をtakutakahashi/database-restore-actionに任せています。
このアクションは、設定ファイルを記載しておくだけで、scpを用いてDBサーバーからdumpファイルを取得して指定DBにリストア、クエリのテストまで行ってくれる便利Actionです。 また、scpだけではなくAmazon S3からファイルを取得してきたり、ローカルのファイルを直接指定したりもできます。とても便利〜

具体的な実装

今回はscpを用いた場合で自動化したので、その方法で実装を紹介します。 まず、GitHub Actionsのworkflowの内容です。
今回は、さっと動かしたいのでトリガーをworkflow_dispatchとしています。

name: restore_test
on:
  workflow_dispatch:
jobs:
  service1: # service1のリストアテストを行うjob
    runs-on: hoge
    services:
      db: # リストア先のDBを設定
        image: mysql
        ports:
          - 3307:3306
        env:
          MYSQL_ROOT_PASSWORD: hogefuga
        options: --health-cmd "mysqladmin ping -h localhost" --health-interval 20s --health-timeout 10s --health-retries 10
    steps:
      - uses: actions/checkout@v3
      - name: setup mysql-cli # リストアに必要なmysql-clientをインストール
        run: |
          apt update && apt install -y mysql-client 
      - name: set credentials # scpの際に必要となる秘密鍵をrunnerにセット
        run: |
          mkdir -p /root/.ssh/
          echo "${{secrets.[sshに使用する秘密鍵]}}" > /root/.ssh/id_rsa
          chmod 600 /root/.ssh/id_rsa
      - name: backup test # リストアテスト実施
        uses: takutakahashi/database-restore-action@main
        env:
          SSH_KEY: "/root/.ssh/id_rsa"
        with:
          config-path: .database/service1.yaml
  service2: # service2のリストアテストを行うjob
    runs-on: hoge
    services:
      db: # リストア先のDBを設定
        image: mysql
        ports:
          - 3307:3306
        env:
          MYSQL_ROOT_PASSWORD: hogefuga
        options: --health-cmd "mysqladmin ping -h localhost" --health-interval 20s --health-timeout 10s --health-retries 10
    steps:
      - uses: actions/checkout@v3
      - name: setup mysql-cli # リストアに必要なmysql-clientをインストール
        run: |
          apt update && apt install -y mysql-client 
      - name: set credentials # scpの際に必要となる秘密鍵をrunnerにセット
        run: |
          mkdir -p /root/.ssh/
          echo "${{secrets.[sshに使用する秘密鍵]}}" > /root/.ssh/id_rsa
          chmod 600 /root/.ssh/id_rsa
      - name: backup test # リストアテスト実施
        uses: takutakahashi/database-restore-action@main
        env:
          SSH_KEY: "/root/.ssh/id_rsa"
        with:
          config-path: .database/service2.yaml
  summary: # リストア結果のサマリーをissueに作成するjob
    runs-on: hoge
    needs: [service1, service2]
    if: ${{ always() }}
    steps:
      - name: Create Title # issueのタイトルを変数にセット
        id: create-title
        run: echo "title=$(date '+%Y' --date '1 month ago')年$(date '+%m' --date '1 month ago')月レポート" >> "$GITHUB_OUTPUT"
      - name: Create report # issueを作成
        uses: imjohnbo/issue-bot@v3
        with:
          labels: "report"
          title: ${{ steps.create-title.outputs.title }}
          body: |
            # report

            | service | host  | result |
            | --- | --- | --- | 
            | service1 | [service1のhost]  | ${{ needs.service1.result }} |
            | service2 | [service2のhost]  | ${{ needs.service2.result }} |

このworkflowは、service1service2のリストアテストを並列に行い、両方の完了を待ってからリストア結果のサマリーをissueに作成するjobであるsummaryが動きissueを作成します。 続いて、database-restore-actionの設定ファイルです。

database:               # リストア先のDB情報(今回はActionで立てているDBの情報)
  type: mysql           # sqlドライバー
  name: service1        # DB名
  user: root            # DBユーザー
  password: hogefuga    # DBパスワード
  host: 127.0.0.1       # DBホスト
  port: 3307            # DBポート
check:
- query: "select * from users" # テストクエリ
  operator: exists             # operator(今回は結果がある場合はテストPASSするように指定)
backup:
  scp:                # dumpファイルがあるサーバーの情報
    host: 127.0.0.1   # ホスト
    port: 22          # scpに使用するポート
    user: user        # scpをするユーザー
    key: "path_to_backup_file/service1_{{ .Yesterday.Format \"20060102\" }}.gz" #dumpファイルがあるパス

設定のkeyにある、{{ .Yesterday.Format \"20060102\" }}は、Action実行時に昨日の日時を20060102のフォーマットで置き換えてくれます。 なので、毎日バックアップして世代管理している場合でもファイル名の指定がしやすくなります。
上記のような設定ファイルをservice2も作成し、Actionsを実行してみると下記のようにservice1service2のリストアテストを並列に実行され両方の完了を待ってからsummaryが動きissueを作成していることがわかります。 Actions Graphの様子 この結果、issueに作成されるサマリーは下記のようになります。 作成されたサマリー

これで、リストアテストの一連の流れが自動化できました。
実際に運用されているコードはもう少しそれぞれの環境に合ったものにカスタマイズされていますが、おおむね上記のようなコードとなっています。

実装時に困ったこと

今回の実装にあたり、困ったことが3点あったのでご紹介します。

dumpのサイズが大きすぎて通常のrunnerではリストアテストができない場合

今回ロリポップのDBのdumpはとても大きく、通常のrunnerだとストレージ容量が足りずリストアテストができませんでした。
セルフホストランナーを使用していたためストレージを多く持つインスタンスを生成し、runnerとして使用することで解決しました。

scpをするアカウントにdumpファイルを操作する権限がない場合

この場合、取れる方法は3つほどあると思います。

  1. 操作権限を持ったアカウントを使う
  2. dumpファイルを該当アカウントでも操作できる権限で生成するように変更する
  3. 権限昇格できるならば、sshで権限昇格しdumpファイルをhomeディレクトリなどにコピーして権限を変更する
    1. appleboy/ssh-actionなどを使うと便利だと思います。

今回の実装時には、実際に権限が足りない場合があり、database-restore-actionを実行する前に3の方法で解決しました。 その時に書いたstepのコードは下記になります。

- name: cp backup file
  uses: pepactions/ssh-action@v0.1.10
  with:
    host: [dumpファイルがあるホスト]
    username: hoge
    key: ${{secrets.[sshに使用する秘密鍵]}}
    port: 22
    envs: FILE
    script: |
      sudo cp path_to_backup_file/[dumpファイル名] /home/hoge/. 
      sudo chmod 755 /home/hoge/[dumpファイル名]

dumpファイルのファイル名が微妙に違ってうまく指定できない場合

dumpのファイル名にダンプ完了時の時刻を使っている場合、微妙に分秒が違う場合があると思います。 その場合、database-restore-actionの設定ファイルでファイル名の指定ができないと思います。 下記のようなコードを使ってファイル名をサーバーから取得してきて設定ファイルにkeyを動的に挿入するstepを入れば解決しそうです。(keyが2つ設定されていると、うまく処理ができないので、あらかじめkeyは設定しないで動的に挿入されるものだけになるようにしてください。)

- name: set-file-name
  id: set-file-name
  run: |
    name=`ssh  hoge@[dumpファイルがあるホスト] "ls path_to_backup_file/" | grep "[dumpファイルの固定されている部分].*.gz"` # ファイル名をサーバーから取得
    echo "file=$name" >> "$GITHUB_OUTPUT" # 後続でも使えるようにoutputしておく
    echo "    key: '/home/hoge/$name'" >> .database/service1.yaml # database-restore-actionのkey設定を追記する

終わりに

今回、リストアテストの自動化を行いました。
DBリストアテストのレポートが1つのリポジトリにまとまっているので内部統制の観点からも便利になったかと思います。 今後、リストアに問題ないということが確認されたファイルをS3にアップロードするなどカスタマイズもできそうです。
自分の所属しているISRチームは、こういった業務改善を技術力をもって行っているチームです。 一緒に働く仲間の環境やリソース状況を改善することに興味がある方は、お気軽にカジュアル面談の申し込みお待ちしています!!