Db2:リストアを全自動スケジュール実行

# はじめに
本番環境からテスト環境へデータベースを定期的にコピーしたいことはないでしょうか。具体的な理由は以下になります。

- サンプルデータではテストが甘くなるため、本番環境に近いテストデータを作成したい
- 負荷テストをするため、本番環境と同じボリュームがほしい
- データ例外がないかをチェックするため、全件テストをしたい

リストアの手順をスクリプトにして、本番環境の日次バックアップからデータベースを全自動でリストアすることにより、テスト環境へデータベースをコピーします。
※コピーした後の個人情報の秘匿化などはここでは扱いません。

# 環境

- Windows 10
- IBM Db2 V11.5 Windows
- PowerShell 5.1

# どのようにコピーするのか

本番系データベースの日次バックアップをテスト系データベースとしてリストアします。一言でいうとこれだけですが、やらなければならない手順は結構あります。

1. アーカイブログを復元するテンポラリーフォルダーを作成
1. 本番系バックアップファイルを選択
1. 既存のテスト系データベースへの接続を切断
1. 既存のテスト系データベースをドロップ
1. 本番系バックアップファイルをテスト系データベースとしてリストア
1. テスト系データベースをロールフォワード

これらの手順をPowerShellで記述、タスクスケジューラに登録することでリストアを全自動で行います。

# リストア手順

スクリプトの全体は最後のまとめに掲載しています。処理のポイントを説明します。

### 0.変数を定義

例では、本番系データベース"QIITA01"からテスト系データベース"QIITA01T"にコピーします。

```powershell:アーカイブログを復元するパスを作成します
$dbname     = 'QIITA01T'                          # 復元先のデータベース名
$logpath    = 'F:\tmp\ARCHIVE_LOG'                # アーカイブログを復元するパス 
$imagepath  = 'F:\Backup\DB2'                     # バックアップイメージが存在するパス
$prefix     = 'QIITA01'                           # バックアップイメージのプレフィックス
```

### 1.テンポラリーフォルダーを作成

本番環境ではサービスを稼働させたままでデータベースをバックアップしたいため、オンライン・バックアップを取ることが一般的です。オンライン・バックアップを復元するにはリストア実行後、アーカイブログを使ってロールフォワードをする必要があります。アーカイブログを配置するテンポラリーフォルダーを作成します。前回実行したフォルダーが残っている場合は削除します。

```powershell:アーカイブログを復元するパスを作成します
    if (Test-Path $logpath) {
        Remove-Item -path($logpath) -Recurse -Force
    }
    New-Item $logpath -itemType Directory -Force
```

### 2.バックアップファイルの選択

バックアップイメージのファイル名はDb2が命名します。ファイル名のプレフィックスはバックアップしたデータベース名になります。ファイルは歴管理され、複数あるので最新のファイル名を取得します。

```powershell:バックアップイメージを確認します
    $file = $prefix + ".*"
    $image = (Get-ChildItem $file -Path $imagepath | Sort-Object LastWriteTime -Descending)[0].FullName
```

### 3.テスト系データベースへの接続を切断

データベースがクライアントから接続されたままですと、データベースをドロップできないため、接続を切断します。

1. テスト系データベース（QIITA01T）に接続しているリストを$linesに格納
1. \$linesを行単位に$lineに格納、foreachで処理を繰り返し
1. \$lineから3列目にあるアプリケーションハンドルを$applhandleに格納
1. メッセージを出力
1. アプリケーションハンドル単位で接続を切断

```powershell:接続しているアプリケーションを切断します
    db2 list applications for db $dbname | where-object { $_.trim() -ne "" } | select-object -skip 3 | Set-Variable lines
    foreach ($line in $lines){
        $applhandle  = (-split $line)[2]
        "アプリケーションを強制終了します " + $applhandle | %{(Get-Date).ToString() + " $_"} | Tee-Object $outfile -Append
        db2 force application `($applhandle`) | where-object { $_.trim() -ne "" } | %{(Get-Date).ToString() + " $_"} | Tee-Object $outfile -Append
    }
```

### 4.既存のテスト系データベースをドロップ

データベースの切断処理は非同期に行われるので、少し待ってからドロップします。

1. 処理をスリーブ
1. データベースをドロップ

```powershell:データベースをドロップします
    sleep 5
    db2 drop db $dbname | where-object { $_.trim() -ne "" } | %{(Get-Date).ToString() + " $_"} | Tee-Object $outfile -Append
```

### 5.テスト系データベースをリストア

バックアップファイルが複数ある場合、Db2のリストアは該当するタイムスタンプを指定する必要があります。

1. 処理をスリーブ
1. バックアップファイル名の第5区分にあるタイムスタンプを$stampに格納
1. データベースをQIITA01Tとしてリストア、アーカイブログはテンポラリーフォルダーに出力

```powershell:データベースをリストアします
    sleep 5
    $stamp = ($image -split "." , 0 , "simplematch")[4]
    db2 restore db $prefix from $imagepath taken at $stamp on E: dbpath on E: into $dbname logtarget $logpath | where-object { $_.trim() -ne "" } | %{(Get-Date).ToString() + " $_"} | Tee-Object $outfile -Append
```

### 6.テスト系データベースをロールフォワード

オンラインバックアップのリストアを完了させるにはロールフォワードが必須です。アーカイブログを出力したテンポラリーフォルダーを指定してロールフォワードします。

1. 処理をスリープ
1. データベースをロールフォワード

```powershell:データベースをロールフォワードします
    sleep 5
    db2 rollforward db $dbname to end of logs and stop OVERFLOW LOG PATH `($logpath`) | where-object { $_.trim() -ne "" } | %{(Get-Date).ToString() + " $_"} | Tee-Object $outfile -Append
```

# まとめ

スクリプト全体を掲載します。エラー処理を行うため、try~catchで処理を括っています。タスクスケジューラに登録すれば、データベース全自動リストアができあがりです。

<details><summary>スクリプト（左の三角をクリックして展開）</summary><div>


```powershell:db2_restore_QIITA01T
$dbname     = 'QIITA01T'                          # 復元先のデータベース名
$logpath    = 'F:\tmp\ARCHIVE_LOG'                # アーカイブログを復元するパス 
$imagepath  = 'F:\Backup\DB2'                     # バックアップイメージが存在するパス
$prefix     = 'QIITA01'                           # バックアップイメージのプレフィックス

set-item -path env:DB2CLP -value "**$$**" # Db2コマンドラインを使用可能にする
$image   = $null
$lines   = $null
$stamp   = $null
$basename = (Split-Path $MyInvocation.MyCommand.Path) + "\" + [io.path]::GetFileNameWithoutExtension($MyInvocation.MyCommand.Name)
$outfile  = $basename + ".log"

"処理開始 " + $MyInvocation.MyCommand.Name | %{(Get-Date).ToString() + " $_"} | Tee-Object $outfile
Write-EventLog -LogName Application -Source Apius -EventID 100 -EntryType Success -Message ($dbname + " リストア処理を開始しました")

try {
    "アーカイブログを復元するパスを作成します... " + $logpath | %{(Get-Date).ToString() + " $_"} | Tee-Object $outfile -Append
    if (Test-Path $logpath) {
        Remove-Item -path($logpath) -Recurse -Force
    }
    New-Item $logpath -itemType Directory -Force

    "バックアップイメージを確認します... " | %{(Get-Date).ToString() + " $_"} | Tee-Object $outfile -Append
    $file = $prefix + ".*"
    $image = (Get-ChildItem $file -Path $imagepath | Sort-Object LastWriteTime -Descending)[0].FullName
    $image | %{(Get-Date).ToString() + " $_"} | Tee-Object $outfile -Append

    "接続しているアプリケーションを切断します... " + $dbname | %{(Get-Date).ToString() + " $_"} | Tee-Object $outfile -Append
    db2 list applications for db $dbname | where-object { $_.trim() -ne "" } | select-object -skip 3 | Set-Variable lines
    foreach ($line in $lines){
        $applhandle  = (-split $line)[2]
        "アプリケーションを強制終了します " + $applhandle | %{(Get-Date).ToString() + " $_"} | Tee-Object $outfile -Append
        db2 force application `($applhandle`) | where-object { $_.trim() -ne "" } | %{(Get-Date).ToString() + " $_"} | Tee-Object $outfile -Append
    }

    "接続しているアプリケーションを確認します... " + $dbname | %{(Get-Date).ToString() + " $_"} | Tee-Object $outfile -Append
    db2 list applications for db $dbname | where-object { $_.trim() -ne "" } | %{(Get-Date).ToString() + " $_"} | Tee-Object $outfile -Append

    "データベースをドロップします... "  + $dbname | %{(Get-Date).ToString() + " $_"} | Tee-Object $outfile -Append
    sleep 5
    db2 drop db $dbname | where-object { $_.trim() -ne "" } | %{(Get-Date).ToString() + " $_"} | Tee-Object $outfile -Append

    "データベースをリストアします... "  + $dbname | %{(Get-Date).ToString() + " $_"} | Tee-Object $outfile -Append
    sleep 5
    $stamp = ($image -split "." , 0 , "simplematch")[4]
    db2 restore db $prefix from $imagepath taken at $stamp on E: dbpath on E: into $dbname logtarget $logpath | where-object { $_.trim() -ne "" } | %{(Get-Date).ToString() + " $_"} | Tee-Object $outfile -Append

    "データベースをロールフォワードします... "  + $dbname | %{(Get-Date).ToString() + " $_"} | Tee-Object $outfile -Append
    sleep 5
    db2 rollforward db $dbname to end of logs and stop OVERFLOW LOG PATH `($logpath`) | where-object { $_.trim() -ne "" } | %{(Get-Date).ToString() + " $_"} | Tee-Object $outfile -Append

    "処理終了 " + $MyInvocation.MyCommand.Name | %{(Get-Date).ToString() + " $_"} | Tee-Object $outfile -Append
    Write-EventLog -LogName Application -Source Apius -EventID 100 -EntryType Success -Message ($dbname + " リストア処理が終了しました")

} catch {
    $Error[0] | Out-String | %{(Get-Date).ToString() + " $_"} | Tee-Object $outfile -Append
    "異常終了 処理を途中で中止します。" | %{(Get-Date).ToString() + " $_"} | Tee-Object $outfile -Append
    Write-EventLog -LogName Application -Source Apius -EventID 100 -EntryType Error   -Message ($dbname + " リストア処理が異常終了しました")

} finally {
    "" | Tee-Object $outfile -Append
}
```

</div></details>

スクリプトを実行してリストアが成功すると以下のような出力になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/698263/8f173b3a-a5a5-4575-85ce-4f504d0f4e00.png)
最後までお読みいただき誠にありがとうございました。