GCSFuseとnofailマウントオプションの話

# 概要

タイトルの通り、GCSFuseを使ってLinuxにGCSをマウントする話です。
nofailオプションを普通に指定してマウントした場合のエラーと、その解決法についての記事になります。

# 前提

まず前提となる実行環境と、GCSFuseの概要について軽く触れておきます。

## 操作元の環境

手元の環境はM1 Macです。
ところどころterraformやansibleの内容を載せているので、以下は参考までに。

```fish
> terraform --version
Terraform v1.8.1
on darwin_arm64
+ provider registry.terraform.io/hashicorp/google v5.26.0

> ansible --version
ansible [core 2.17.5]
```

## 実行VM環境

実験したのはGCEのdebian-11環境です。

```terraform
resource "google_compute_instance" "main" {
    name = "test-gcsfuse"

# 略

    boot_disk {
        initialize_params {
            image = "debian-cloud/debian-11"
            size = 10
            type = "pd-balanced"
        }
    }

# 略
}
```

## GCSFuseを使ったマウント方法の概要

基本的には[公式ドキュメント](https://cloud.google.com/storage/docs/cloud-storage-fuse/install#ubuntudebian)を参照いただければと思います。
ざっくり抜粋すると

1. GoogleCloudのリポジトリを登録したaptでgcsfuseをインストールする
1. マウントを実行する

という流れになります。

マウントの実行にはいくつか方法があります。

1. 一時的なマウント
    1. gcsfuseコマンド
    1. mountコマンド
1. 持続的なマウント
    1. /etc/fstabに記述してmountまたはreboot

今回は持続的なマウントに焦点を当てています。
nofailオプションは/etc/fstabに記述する際のオプションです。

# エラーについて

本題となる実際に遭遇したエラーについて触れていきます。

## エラー内容

GCSのマウントをfstabに記述する際には、公式ドキュメントにも言及がある通りオプションに `_netdev` や `implicit_dirs` を指定するなど、一工夫必要なケースが多いと思います。

以下は `/etc/fstab` の記述内容の一例になります。

```
bucket-name /path/to/mount/point gcsfuse rw,_netdev,implicit_dirs,uid=0,gid=0,allow_other,nofail 0 2
```

これを記述した状態でインスタンスごと `sudo reboot` を実行すれば、正常にマウント成功します。
しかし `sudo mount /path/to/mount/point` を実行するとエラーになってしまいます。

同じ記述内容であれば普通はどちらも成功するはずでは、と思っていました。ここで原因がわからず結構詰まった。

また、類似のパターンとしてAnsibleから以下のようにして普通にマウントしようとするとエラーになります。

```xml
- name: Mount GCS
  ansible.posix.mount:
    path: /path/to/mount/point
    src: "bucket-name"
    fstype: gcsfuse
    opts: rw,_netdev,implicit_dirs,uid=0,gid=0,allow_other,nofail
    passno: 2
    state: mounted
```

エラーメッセージ全体をコピペ掲載することは避けますが、エラーは以下のような内容です。

```
Error: daemonize.Run: readFromProcess: sub-process: Error while mounting gcsfuse: mountWithArgs: mountWithStorageHandle: mount: mount: invalid argument
...
running gcsfuse: exit status 1
```

mountに不正な引数を指定したことによるエラーとなっています。
この不正な引数というのが `nofail` になります。試しに `nofail` オプションを外して実行すると成功します。

ならオプションを外せば良いかというと、そうもいきません。
GCSのマウントはネットワークの状況などにも左右されやすく、失敗するリスクを考慮しておきたいため `nofail` オプションはなるべく指定しておきたいところです。

## エラー原因

私も詳細に語れるほどの知識はないのですが、同じ設定で違う結果が得られることから、少なくとも実行内容が異なることはお分かりいただけると思います。
reboot時はsystemdがmountを管理しているのに対して、手動で実行する場合は直接mountを実行しています。この違いによって今回のようにreboot時にはエラーが発生せず、手動mount時にはエラーが発生する、といったことが起きます。

GCSFuseは名前の通りFuseをベースとしています。
Fuseのオプションにはnofailが存在しないため、直接mountを実行した場合はnofailオプションがそのままGCSFuseに渡されて `invalid argument` エラーを出すことになります。
一方で、systemdが管理してmountを実行した場合はsystemd側でnofailオプションを除外した状態でmountを実行しているものと思われます。

## エラー回避方法

つまり、回避するにはmountを直接は実行せずに、systemdに管理させればOKです。

```bash
sudo systemctl start path-to-mount-point.mount
```

### Ansible記述例

参考までに、本エラーを回避してGCSをマウントするAnsible記述例です。

```yml
- name: Create fstab
  ansible.posix.mount:
    path: /path/to/mount/point
    src: "bucket-name"
    fstype: gcsfuse
    opts: rw,_netdev,implicit_dirs,uid=0,gid=0,allow_other,nofail
    passno: 2
    state: present
  register: fstab_result
- name: Reload systemd
  ansible.builtin.systemd_service:
    daemon_reload: true
  when: fstab_result.changed
- name: Mount with systemctl using fstab
  service:
    name: path-to-mount-point.mount
    state: started
  when: not ansible_check_mode
```

# おわりに

意外と検索してもこのエラーについての記事が見当たらなかったので、備忘も兼ねて久しぶりに記事を書きました。
なかなかエラー発生原因が掴めず苦労したので、どなたかの助けになれば幸いです。