Google Cloud Batchで手軽にPDFの圧縮をバッチ処理する

## バッチについて
Google Cloud Batchについて前回の記事の続きになります。基本的な事項はこちらから。

https://qiita.com/kuromiya123/items/b3843d17688594427322

## つくったもの
[mp4をエンコーディングする公式チュートリアル](https://github.com/GoogleCloudPlatform/batch-samples/tree/main/transcoding)に手を加えて、より手軽にバッチ処理を開始するスクリプトを作成しました。コードは以下のレポジトリにあります。

https://github.com/kr-my/batch-samples/tree/compressing

基本的な仕組みとしては、`starter.sh`を実行すると、`job.json.template`、`task.sh.template`から必要なファイルが生成され、バッチにjobが登録されます。

ディレクトリ構成は以下の通り。

```
compressing/
├── starter.sh
├── job.json.template
├── task.sh.template
└── input/
    ├── photobook-a.pdf
    ├── photobook-b.pdf
    └── photobook-c.pdf
```

`input`以下にphotobookのPDFが格納されており、これをバッチ処理してファイルサイズの小さなPDFを生成します。実際には、バケット内に配置されたPDFのファイル名とファイルの数に規則性はなくバラバラの状況を想定しています。

## 使用方法

まずはバッチのAPIを有効にします。

```
gcloud services enable batch.googleapis.com
```

Cloud Storageにバケットを作成して`input`ディレクトリをコピーします。

```
gsutil mb -p [PROJECT_ID] -b on -l US gs://[BUCKET_NAME]
gsutil cp -R input gs://[BUCKET_NAME]
```

starter.shを編集して、`[BUCKET_NAME]`を作成したバケット名に変更します。
バッチによる処理の対象は、拡張子.pdfのファイル全てとしています。
```sh:starter.sh
BUCKET_NAME="[BUCKET_NAME]"
gcsFilePaths=$(gsutil ls gs://$BUCKET_NAME/input | grep .pdf) #All pdf files in bucket will be proccessed.
```

準備ができたら`starter.sh`を実行します。

Note: テンプレートからファイルを生成するため`envsubst`コマンドを使用しています。インストールされているかは`which envsubst`で確認。コマンドのパスが返ってこない場合、`gettext`パッケージを先に入れてください。

```
bash starter.sh
```

テンプレートから必要なファイルが生成され、バッチにjobが登録されます。少しすると、PDFの圧縮が始まります。

クラウドコンソールを開くと、以下のように進行中のjobが確認できます。

![スクリーンショット 2022-08-21 21.16.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2661777/dae9ef8e-c670-5caa-b0f0-68af72389887.png)

# starter.shがやっていること
* バケット内のPDFファイルのファイル名を取得する
* 各PDFファイルを圧縮する`task.sh`を生成する
* ファイルの総数＝タスク数として`job.json`を生成する
* 新規のバッチjobを作成する

### task.shの生成

バッチが実行するタスクは、`job.json`に記述します。
```json:job.json
"script": {
  "text": "bash /mnt/share/input/task${BATCH_TASK_INDEX}.sh"
}
```
```json:job.json
"taskCount": 3,
```

タスクが実行されるたびに`${BATCH_TASK_INDEX}`がカウントアップしていきます。例えば、上記の例であれば、task0.sh task1.sh task2.shと`taskCount`の数だけshファイルが実行されます。

さて`starter.sh`はというと、バケット内の各PDFファイルに対応する`task.sh`を生成します。

```sh:starter.sh
### Create task.sh files
echo "Creating each task.sh files"
TASK_COUNT=0
for gcsFilePath in $gcsFilePaths
do
  FILE_NAME=${gcsFilePath##*/};
  export FILE_NAME
  cat task.sh.template | envsubst '$FILE_NAME' > $temp_dir/task$TASK_COUNT.sh

  TASK_COUNT=`expr $TASK_COUNT + 1`
done

gsutil -m cp $temp_dir/task*.sh gs://$BUCKET_NAME/input/
```

`starter.sh`実行後の`gs://[BUCKET_NAME]/input`は以下のようになります。

```:gs://[BUCKET_NAME]/input
input
├── photobook-a.pdf
├── task0.sh
├── photobook-b.pdf
├── task1.sh
......
```
生成された`task0.sh`の中身を見てみましょう。

```
gsutil cat gs://[BUCKET_NAME]/input/task0.sh
```

```sh:task0.sh
#!/bin/bash

sudo apt-get -y update
sudo apt-get -y install ghostscript

filename=photobook-a.pdf # ここに圧縮したいPDFのファイル名が入る

dir=/mnt/share
infile=$dir/input/$filename
outfile=$dir/output/${filename/.pdf/_compressed.pdf}
vopts="-sDEVICE=pdfwrite -dCompatibilityLevel=1.4 -dPDFSETTINGS=/ebook -dNOPAUSE -dBATCH -dQUIET"

mkdir -p $dir/output
gs $vopts -sOutputFile=$outfile $infile
```
ポイントとしては、`starter.sh`がバケットから取得したPDFファイル名`$FILE_NAME`を、`task.sh.template`に渡しているので、生成された`task0.sh`自体はPDFのファイル名がハードコーディングされたスクリプトになっています。バッチが`task${BATCH_TASK_INDEX}.sh`を順々に実行していくことで、それぞれのPDFファイルの圧縮処理が行われる、という訳です。

タスクの内容を変更したい場合は、`task.sh.template`を変更してください。

### job.jsonの生成

あとは、`task.sh`の数だけバッチにタスクを実行して欲しいので、`TASK_COUNT`の値を`job.json.template`に渡して、`job.json`を生成します。

```sh:starter.sh
### Create job.json
echo "Creating job.json"
export BUCKET_NAME
export TASK_COUNT
cat job.json.template | envsubst '$BUCKET_NAME $TASK_COUNT'> $temp_dir/job.json
```
並列するタスク数や、マシーンタイプといったパラメーターは、`job.json.template`を変更してください。

### 新規jobの作成
最後に、生成した`job.json`を使用して、バッチのjobを登録します。

```sh:starter.sh
### Submit the job
echo "Submiting the job"
date=$(date +"%Y%m%d%H%M")
gcloud beta batch jobs submit pdf-compression_$date --location=us-central1 --config=$temp_dir/job.json

```
Job Nameには重複した値をつけることができないので`date`を付加しています。

以上で、ファイル名の取得から、必要なファイルの生成、バッチへのjobの登録まで完了です。