Cloud Runのジョブ機能を使わずに通常サービスでジョブっぽく動かす

# はじめに
定期的にジョブを実行したい、となった際に、
色々考えた結果、Cloud RunをCloud Schedulerで定期実行して、
CloudSQLの情報をエクスポート処理することになった。その対応にあたってやったことを記載していく。
Cloud RunのジョブがGAされる前の記事となります、、
※CloudSQLのエクスポート後の内容は記載していません

# 概要
最終的なコンポーネントの関連する図として以下となった。
CloudFunctions→BigQueryは記事の範囲外です
![スクリーンショット 2023-02-03 0.31.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1720034/50f2ecfd-8d40-a8c7-7b99-8511eb9ce2c9.png)


# 前提
## 条件
### 実行頻度
- デイリーくらいで実行する
### 実行内容
- CloudSQL内のデータをエクスポートする
- カラム情報を含めてエクスポートする
    - 出力したいテーブルが複数(数百)ありBigQuery側へ手動でのスキーマ定義できないため
- テーブル情報は一括で複数テーブル出力する
### 実行形式
- Cloud Storageをマウントしたほうが出力しやすそうなのと、シェルの方が書きやすそうだった

# 検討事項
軽く経緯説明
## 想定と異なった点
CloudSQLからサーバーレスエクスポートしたところ、以下想定と異なる状態だった
- カラム情報が出力されなかった
- 複数の出力処理を同時に走らせることができなかった
- 複数のテーブル出力ができなかった
## 対応案
以下2点を加味しシェルの方が書き慣れていることもあり、シェルが使えるCloud Runにすることにした。
- サーバーレスエクスポートはまず諦める
- Cloud Functionsなどで処理を書きテーブルをエクスポートする

# 実現方法
## 検討内容
Cloud RunでやるとなるとHTTPリクエストを受けてシェルを実行する必要があり、
調べてみると[OpenRestyのngx_http_lua_module](https://github.com/openresty/lua-nginx-module)が使えそうということがわかった。

### そもそもOpenRestyとは
[Wiki](https://en.wikipedia.org/wiki/OpenResty)だと、Nginxの改良版で、
上記したLuaモジュールを使ううえでは、ただのNginxを使うよりも、
このOpenrestyを利用し、Luaモジュールを使う方が都合がいいみたいです。
NginxだとLua実行環境もインストールするなど、色々あるみたいです。

# 構築していく
## 手順
今回はCloud Runなのでコンテナ化していく。
また、今回はOpenRestyの他に必要なミドル(MySQL Client、gcsfuse)もインストールする。

### ファイルの配置
```
.
├── docker-compose.yml
├── exportcsv.sh
├── mysql.conf
├── nginx
│   ├── nginx.conf
│   └── osexecute.conf
├── openrestydockerfile
└── tablelist.txt
```

### dockerfile
対象部分だけピックアップする。
※記載していない内容
　1.必要なファイルをローカルからイメージ内にコピーする(nginxのconfigやshellとか)
　2.コピー後のシェルの権限設定
```
# 利用するイメージ
FROM openresty/openresty:1.21.4.1-0-bullseye-fat

# ミドルインストール
RUN apt-get update && apt-get install -y default-mysql-client
RUN set -e; \
    apt-get update -y && apt-get install -y \
    tini \
    gnupg2 \
    lsb-release; \
    gcsFuseRepo=gcsfuse-`lsb_release -c -s`; \
    echo "deb http://packages.cloud.google.com/apt $gcsFuseRepo main" | \
    tee /etc/apt/sources.list.d/gcsfuse.list; \
    curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | \
    apt-key add -; \
    apt-get update; \

# OpenResty実行
CMD ["openresty", "-g", "daemon off;"]
```

### nginxのconfig
必要な部分を変更する
```
#user  nobody;
### 変更しないと任意のコマンド実行時に権限の問題で失敗する<>はいらない ###
user  <ユーザ名>;
```
読み込むconfigの場所を追記
```
http {
    include       mime.types;
    default_type  application/octet-stream;
    client_body_temp_path /var/run/openresty/nginx-client-body;
    proxy_temp_path       /var/run/openresty/nginx-proxy;
    fastcgi_temp_path     /var/run/openresty/nginx-fastcgi;
    uwsgi_temp_path       /var/run/openresty/nginx-uwsgi;
    scgi_temp_path        /var/run/openresty/nginx-scgi;
    sendfile        on;
    keepalive_timeout  65;
###### 以下追記場所 ######
    include /usr/local/openresty/nginx/conf/conf.d/*.conf;
}
```

### conf.dに保存するxxxx.conf
luaに関連する部分なのでサンプルとして記載する

location /xxxx にコマンドを実行したい際の、HTTPリクエストを送るパスを記載する。
os.executeの中には実行したいコマンドやシェルのパスを記載する。
今回はシェル化し、その中でgcsfuseのマウントや、mysqlからのレコードの出力コマンドを実行している。

※注意事項
xxxxx_by_lua_block (Lua) 内でのコメントは "#" ではなく "--" を使うことに注意。
以下ではxxxx箇所はcontentとしている

```
server {
    listen 80;
    server_name localhost;

    location / {
        root    /usr/local/openresty/nginx/html;
        index  index.html index.htm;
    }

### location箇所はHTTPリクエストを受けたいパスを記載する
    location /xxxxx {
### content_by_lua_blockは必須
        content_by_lua_block {
-- コメントは "#" ではなく "--" を使うことに注意。
-- 以下は例ですがシェルの内容を記載する
            os.execute('/root/shell/shell_org.sh')
-- 以下は個別にログに出力する内容を定義できるので、実行有無がわかりたいときに便利でした
            ngx.log(ngx.DEBUG, "check")
        }
    }
}
```

### シェルの内容
シェルの中でやっていること
・GCSのマウント
・tablelistからmysqlのテーブル名を読み込んで対象テーブルにselect文を実行し、CSVを出力
・以下のmysql.confは、Cloud Runでsecretとしてマウントしています
※記載例
```
#!/bin/bash
echo "[INFO] 処理開始"
MYSQL_SCHEMA="DB名"
PWD="/mnt/gcs"
CONF="/config/mysql.conf"
LIST="/work/tablelist.txt"

# Storage mount
gcsfuse --debug_gcs <Cloud Storage名> ${PWD}

# ファイルからテーブル取得
cat ${LIST} | while IFS= read -r TABLE || [[ -n "${TABLE}" ]]
do
# シェルを実行、実行ログを受け取る
# 処理の終了コードを取得
# テーブル情報一時出力
mysql --defaults-extra-file="${CONF}" ${MYSQL_SCHEMA} -e \
"select * from ${TABLE};" | sed -e "必要な置換処理" \
> ${PWD}/${TABLE}-output-`date +\%Y\%m\%d`.csv

```

### コンテナのBuild
そのままdockercomposeでbuildする
```
version: '3.9'

services:
  openresty:
    build:
      context: .
      dockerfile: openrestydockerfile
```

### 作成したイメージをGC環境へ
tagづけとpushする
※事前にArtifactRegistryを作成しておく
```
docker tag buildしたイメージ名 asia-northeast1-docker.pkg.dev/pj名/artifactregistry名/コンテナイメージ名
docker push asia-northeast1-docker.pkg.dev/pj名/artifactregistry名/コンテナイメージ名
```

### Cloud Run設定
Cloud Run サービスを作成し、secretをマウントする(特記事項だけ記載)
・secretを作成し、/config/mysql.confへのマウント設定を行う
・Cloud Runのサービスアカウントはカスタムし必要な権限のみ付与した
・セキュリティの認証は「認証が必要」とする
補足：CloudSQLへの通信が必要なので、環境に応じて、内部IPへの通信はServerless VPC Access Connectorを経由するように設定する

### Cloud Scheduler設定
[こちら](https://cloud.google.com/scheduler/docs/http-target-auth?hl=ja)を参考に設定した。(特記事項だ抜粋する)
・Cloud invoke権限を付与したサービスアカウントを作成する
・以下に従い、所定のURLへHTTPアクセス設定、ヘッダー設定、対象URL設定を行う
・実行時間はcron設定するが、その他はデフォルトに従う
これらの設定を行うことで対象サービスアカウントにて認証を行い、対象URLを叩くことができる

### 結果
シェル内でログ出力するようにしていた場合、Cloud Logging内でログが出力され、
今回はCloud Storageにファイル出力しているので、ファイルの出力も確認できた。
また、当該記事外だが、Cloud Storageのファイル出力を機にBigQueryへのインポートも動作していた。


### 全体でハマったこと
1.Luaモジュール
Luaモジュールが初めてだったので、書き方にかなりハマった。
以下のようなものを挟むことでアクセスできているか切り分けした
```
        access_by_lua_block {
            ngx.say( "アクセスできているよ");
        }
```
2.OpenRestyのmysqlでのエンコード
mysqlのレコード出力において、clientのエンコードをutf-8に固定する必要があった。
utf-8でないと空ファイルになるのと、Cloud Storageへの出力ができなかった。