goofysを使ってElastic BeanstalkにS3をマウントするために死闘を繰り広げました。

お疲れ様です。福里(♂)です。

今回は表題の通り、僕の死闘を書き記しておこうと思います。実はちゃんとした技術記事を書くのは初めてなので、ドキドキしますね。これが恋か。

やりたかったこと


Elastic Beanstalkにデプロイしているコンテナ内の一部のファイルを、デプロイを伴わずに更新できるようにする。

試したこと


一部のファイルをS3に切り出して、goofysを使ってS3をマウントすればできるじゃん!採用!

結論(できたのかできなかったのか)


結論からいうと、無事できました。

そのため、どうやって実現したのか、どこでつまづいたのかを書いていきます。

事前準備


  1. マウントしたいS3バケットを作成
  2. BeanstalkのEC2インスタンスに設定しているIAMロールにAmazonS3FullAccessポリシーをアタッチする。(バケットを絞りたい場合は、カスタムポリシーを作成してアタッチしてもOKです。)

goofysを使うための.configファイルを.ebextension/以下に配置する


配置したファイル(失敗例)

今回、goofysを使うためにBeanstalkにデプロイするソースの.ebextensios/に以下のファイルを追加しました。

※失敗例のファイルなので、コピペはまだ我慢してください。

packages:
  yum:
    golang: []
    fuse: []

commands:
  01_mount:
    command: "/tmp/01_unmount.sh"
  02_mount:
    command: "/tmp/02_goofys.sh"

files:
  "/tmp/01_unmount.sh":
    mode: "000755"
    owner: root
    group: root
    content: |
      #!/usr/bin/env bash
      if mountpoint -q [マウントポイント] ; then
        umount [マウントポイント]
      fi

  "/tmp/02_goofys.sh":
    mode: "000755"
    owner: root
    group: root
    content: |
      #!/usr/bin/env bash
      export GOPATH=$HOME/go
      go get -d github.com/kahing/goofys@latest
      go install github.com/kahing/goofys@latest
      mkdir -p [マウントポイント]
      $GOPATH/bin/goofys --uid `id -u root` -o allow_other [マウントしたいS3バケット名] [マウントポイント]

ちなみに[マウントポイント]は、インスタンスのファイルシステムのどこにマウントしたいかの指定です。/mnt/hogeとかにしとくとわかりやすいね。

これを含めていざデプロイしましたが、先ほど書いた通りこちらは失敗しました。

エラー1: No space left on device


Beanstalkのcfn-init-cmd.logを見てみると、先ほどのconfigファイルの02_goofys.shを実行している段階で以下のエラーが出ていました。

[INFO] 	# cd .; git clone -- https://github.com/grpc/grpc-go /root/go/src/google.golang.org/grpc
[INFO] 	Cloning into '/root/go/src/google.golang.org/grpc'...
[INFO] 	/root/go/src/google.golang.org/grpc/.git: No space left on device

容量が足りないとのことで、EC2インスタンスにeb sshで入りdf -hを叩いてみると、

$ df -h
Filesystem      Size  Used Avail Use% Mounted on
tmpfs           996M     0  996M   0% /dev/shm
tmpfs           996M   17M  979M   2% /run
tmpfs           996M     0  996M   0% /sys/fs/cgroup
/dev/xvda1      8.0G  8.0G   20K 100% /
tmpfs           200M     0  200M   0% /run/user/1000

EBSがパンパンのパンになっていました。ごめんね気づいてあげられなくて。。。。。

解決策: 容量の拡張


ということで、拡張しました。大まかな手順としては、

  1. EBSのボリュームサイズを変更する。
  2. growpartでパーティションをボリュームサイズに合わせて拡張する。
  3. xfs_growfsでファイルシステムを上限まで展開する。

となっています。

1. EBSのボリュームサイズを変更する。

AWSコンソールから、EC2インスタンスにマウントしているボリュームのサイズを変更します。

図1: ボリュームの変更

画像の通り、現状は8GBになっていたため、8 -> 16GBに変更しました。

2. growpartでパーティションをボリュームサイズに合わせて拡張する。

EC2インスタンスに入り、lsblkを叩きます。

$ lsblk
NAME    MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
xvda    202:0    0  16G  0 disk
└─xvda1 202:1    0   8G  0 part /

ボリュームサイズは引き上げられていますが、まだパーティションが8GBのままなので拡張してあげます。

$ sudo mount -o size=10M,rw,nodev,nosuid -t tmpfs tmpfs /tmp <- growpartができないくらい容量がパンパンだったので、一時フォルダ用のマウントを追加
$ sudo growpart /dev/xvda 1
$ sudo umount /tmp

これで、ボリュームサイズに合わせてパーティションが拡張されました。

$ lsblk
NAME    MAJ:MIN RM SIZE RO TYPE MOUNTPOINT
xvda    202:0    0  16G  0 disk
└─xvda1 202:1    0  16G  0 part /

3. xfs_growfsでファイルシステムを上限まで展開する。

ここで、一度df -hを叩いてみると。。。

$ df -h
Filesystem      Size  Used Avail Use% Mounted on
tmpfs           996M     0  996M   0% /dev/shm
tmpfs           996M   17M  979M   2% /run
tmpfs           996M     0  996M   0% /sys/fs/cgroup
/dev/xvda1      8.0G  8.0G   20K 100% /
tmpfs           200M     0  200M   0% /run/user/1000

まだ上限が8GBのままになっています。これはファイルシステムが展開できていないことが原因なので、展開してあげました。

$ sudo xfs_growfs /dev/xvda1
$ df -h
Filesystem      Size  Used Avail Use% Mounted on
tmpfs           996M     0  996M   0% /dev/shm
tmpfs           996M   17M  979M   2% /run
tmpfs           996M     0  996M   0% /sys/fs/cgroup
/dev/xvda1     16.0G  8.0G  8.0G  51% /
tmpfs           200M     0  200M   0% /run/user/1000

これで今度こそ容量問題が解決したはずなので、再度デプロイ!!!!!

エラー2: undefined io.ReadAll


容量問題は解決したものの、別のエラーが出てしまいました。。。

Beanstalkのcfn-init-cmd.logを見てみると、また先ほどのconfigファイルの02_goofys.shを実行している段階で以下のエラーが表示されていました。

[INFO] Command 02_mount
[INFO] -----------------------Command Output-----------------------
[INFO] 	# golang.org/x/oauth2/google/internal/externalaccount
[INFO] 	root/go/src/golang.org/x/oauth2/google/internal/externalaccount/executablecredsource.go:256:15: undefined: io.ReadAll
[INFO] 	/tmp/02_goofys.sh: line 6: /root/go/bin/goofys: No such file or directory
[INFO] ------------------------------------------------------------

なんらかの原因でgoofysのインストールに失敗し、goofysコマンドが通ってないみたいです。

そこでio.ReadAllのエラーに関して調べてみたところ、どうやらgolangのバージョンが1.16系以降じゃないとio.ReadAllが使えないとのこと。

ですがconfigファイルのpackagesでは特にgolangのバージョンを指定していないはずなので、EC2インスタンスに入りgo versionを叩いてみたところ。。。。

$ go version
go version go1.15.14 linux/amd64

めちゃくちゃ1.15でした。なんで????????????

特にバージョン指定をしていないはずなので、AWSのドキュメントにパッケージのバージョンに関して記載がないか調べてみました。

Amazon Linux 2022 には、Amazon Linux 2 に存在していた同じパッケージの多くが含まれています。これらのパッケージバージョンのいくつかは Amazon Linux 2022 用に更新されました。

参考: Amazon Linux 2022 リリースノート

図2: Amazon Linux 2とAmazon Linux 2022でのGolangバージョン

お前お前お前お前お前ぇぇぇ!!!

つまり、Amazon Linux 2 インスタンスのリポジトリだと、1.15.14が最新バージョンとなっているらしいです。

そのため、yumでgolangをインストールした場合、サードパーティ製のものをgo installしようとするとバージョンの関係により失敗することがあるみたい。

解決策: Golangパッケージのv1.16.13を直インストール


yumでgolangをインストールするとあかんということで、EC2インスタンスの中に入り直接golangパッケージをインストールしてあげることにしました。 インストールコマンドをconfigファイルにぶち込みました。詳しくは追記項目をご参照ください。最初からこうすればよかったじゃん。。。

$ sudo yum remove golang -y <- すでにyumでgolangが入ってる場合
$ wget https://go.dev/dl/go1.16.13.linux-amd64.tar.gz
$ sudo tar -C /root -xzf ./go1.16.13.linux-amd64.tar.gz

1.16.13にしたのは、一応Amazon Linux 2022インスタンスのバージョンに合わせたかったからです。

また、今回直接golangをインストールしているので、先ほどのconfigファイルも少し修正しました。

配置したファイル(失敗例その2)

packages:
  yum:
    fuse: []

commands:
  01_mount:
    command: "/tmp/01_unmount.sh"
  02_mount:
    command: "/tmp/02_goofys.sh"

files:
  "/tmp/01_unmount.sh":
    mode: "000755"
    owner: root
    group: root
    content: |
      #!/usr/bin/env bash
      if mountpoint -q [マウントポイント] ; then
        umount [マウントポイント]
      fi

  "/tmp/02_goofys.sh":
    mode: "000755"
    owner: root
    group: root
    content: |
      #!/usr/bin/env bash
      export GOROOT=$HOME/go
      export PATH=$PATH:$GOROOT/bin
      export GOPATH=$HOME/go
      go get -d github.com/kahing/goofys@latest
      go install github.com/kahing/goofys@latest
      mkdir -p [マウントポイント]
      $GOPATH/bin/goofys --uid `id -u root` -o allow_other [マウントしたいS3バケット名] [マウントポイント]

golangの記述を削除したのと、02_goofys.shの中にgoのパスを通す記述を追加しました。

これで再度デプロイしたところ、undefined: io.ReadAllのエラーは出なくなりましたが、次のエラーが待っていました。

エラー3: accountsRes.Value undefined & not enough arguments in call to client.ListKeys


次に出たエラーは以下です。

root/go/pkg/mod/github.com/kahing/goofys@v0.24.0/api/common/conf_azure.go:272:34: accountsRes.Value undefined (type storage.AccountListResultPage has no field or method Value)
[INFO] 	root/go/pkg/mod/github.com/kahing/goofys@v0.24.0/api/common/conf_azure.go:373:35: not enough arguments in call to client.ListKeys
[INFO] 		have (context.Context, string, string)
[INFO] 		want (context.Context, string, string, storage.ListKeyExpand)
[INFO] 	/tmp/02_goofys.sh: line 8: /root/go/bin/goofys: No such file or directory

これに関してはほんとにさっぱりで、調べども調べども失敗した原因がわからず、ごはんの味も感じられないほど頭を悩ませました。嘘です、ごはんはいつでも美味しかったです。

解決策: インストールするgoofysのバージョンをv0.20.0に修正


ダメ元でgoofysのリポジトリのissueに上がってないかな〜とリポジトリ内検索をかけたところ、とあるissueに解決策が書いてありました。なんでいままで調べても出てこなかったんだよ

参考: https://github.com/kahing/goofys/issues/664#issuecomment-944895024

これによると、最新バージョンv0.24.0(2022/06/29当時)の場合はエラーがでるらしく、恒久対応はまだされていないため急ぎで使いたいならv0.20.0にするといいよとのこと。

なので、そのコメントの言う通りconfigファイルを以下のように修正しました。

配置したファイル(成功例)

packages:
  yum:
    fuse: []

commands:
  01_mount:
    command: "/tmp/01_unmount.sh"
  02_mount:
    command: "/tmp/02_goofys.sh"

files:
  "/tmp/01_unmount.sh":
    mode: "000755"
    owner: root
    group: root
    content: |
      #!/usr/bin/env bash
      if mountpoint -q [マウントポイント] ; then
        umount [マウントポイント]
      fi

  "/tmp/02_goofys.sh":
    mode: "000755"
    owner: root
    group: root
    content: |
      #!/usr/bin/env bash
      export GOROOT=$HOME/go
      export PATH=$PATH:$GOROOT/bin
      export GOPATH=$HOME/go
      go get -d github.com/kahing/goofys@v0.20.0
      go install github.com/kahing/goofys@v0.20.0
      mkdir -p [マウントポイント]
      $GOPATH/bin/goofys --uid `id -u root` -o allow_other [マウントしたいS3バケット名] [マウントポイント]

goofysの後ろにつけていた@latest@v0.20.0に修正しました。

先ほどのコメントを信じてデプロイしてみると、なんとデプロイが成功しました!!

念の為EC2インスタンスに入りdf -hを叩いてみると、指定したマウントポイントが作成されS3のマウントにも成功していました!!!!

めでたしめでたし。

となるところですが、もう一つだけ忘れていることがありました。Dockerrun.aws.jsonへの追記です。

今の状態だとインスタンスにはマウントできていますがコンテナにはマウントできていない状態なので、以下のように追記しました。

{
  "AWSEBDockerrunVersion": "1",
  "Image": {
    "Name": "ECRのイメージ名"
  },
  "Ports": [
    {
      "ContainerPort": "8080"
    }
  ],
  "Logging": "/var/log/httpd",
  "Volumes": [
    {
      "HostDirectory": "[マウントポイント]",
      "ContainerDirectory": "[マウントポイント]"
    }
  ]
}

これで完璧にBeanstalkとS3のマウントができました。ここまでお疲れ様でした。

結局どうやって更新するの?


さて、これで無事にマウントは完了したわけですが、僕の最終目標はそこではありません。その後、ほんとにデプロイせずにファイルを更新してBeanstalkに反映できるのかという部分が大事です。

といってもここまできたらあとはもう簡単で、S3に置いているファイルを更新した後、Beanstalkの画面でアプリサーバーの再起動をすると更新が反映されています。天才ですね。

例えば、今回の僕の例で言うとBeanstalkにはWebサイトをデプロイしていて、検索機能に使用するマスタデータを更新していました。実際の画面はお見せできませんが、S3内のマスタデータファイルを更新し、アプリサーバーの再起動後、無事更新が反映されていました🥳

これにてほんとうにめでたしめでたしです。

最後に


死闘といいつつ、こうやって書いてみると意外とあっけなかったかもしれないですね。ただ結論に2、3日ほどかかったので、実際はほんとに大変でした。。。

無事マウントすることができましたが、もしもっと賢いやり方がある場合は知りたいですね。(特に、初回だけとはいえgolangパッケージを直接インストールしてあげないといけないのはめんどくさいので、なんとかならないかなと思ってます。)

この記事が、少しでもお役に立てたなら幸いです。

【2022/07/01 追記】: Golangインストールコマンドもconfigに記述


Golangパッケージを直インストールしていましたが、そもそもインストールコマンド全部configにぶちこめばいいじゃんって今更気づきました。ウケる(?)

packages:
  yum:
    fuse: []

commands:
  01_mount:
    command: "/tmp/01_unmount.sh"
  02_mount:
    command: "/tmp/02_goofys.sh"

files:
  "/tmp/01_unmount.sh":
    mode: "000755"
    owner: root
    group: root
    content: |
      #!/usr/bin/env bash
      if mountpoint -q [マウントポイント] ; then
        umount [マウントポイント]
      fi

  "/tmp/02_goofys.sh":
    mode: "000755"
    owner: root
    group: root
    content: |
      #!/usr/bin/env bash
      if [ ! -e go1.16.13.linux-amd64.tar.gz ]; then
        wget https://go.dev/dl/go1.16.13.linux-amd64.tar.gz
      fi
      if [ ! -e /root/go ]; then
        tar -C /root -xzf ./go1.16.13.linux-amd64.tar.gz
      fi
      export GOROOT=$HOME/go
      export PATH=$PATH:$GOROOT/bin
      export GOPATH=$HOME/go
      go get -d github.com/kahing/goofys@v0.20.0
      go install github.com/kahing/goofys@v0.20.0
      mkdir -p [マウントポイント]
      $GOPATH/bin/goofys --uid `id -u root` -o allow_other [マウントしたいS3バケット名] [マウントポイント]

一刻も早くマウントしないといけない人向け


  1. マウントしたいS3バケットを作成
  2. BeanstalkのEC2インスタンスに設定しているIAMロールにAmazonS3FullAccessポリシーをアタッチする。(バケットを絞りたい場合は、カスタムポリシーを作成してアタッチしてもOKです。)
  3. .ebextensions/に以下のファイルを追加する。
packages:
  yum:
    fuse: []

commands:
  01_mount:
    command: "/tmp/01_unmount.sh"
  02_mount:
    command: "/tmp/02_goofys.sh"

files:
  "/tmp/01_unmount.sh":
    mode: "000755"
    owner: root
    group: root
    content: |
      #!/usr/bin/env bash
      if mountpoint -q [マウントポイント] ; then
        umount [マウントポイント]
      fi

  "/tmp/02_goofys.sh":
    mode: "000755"
    owner: root
    group: root
    content: |
      #!/usr/bin/env bash
      export GOROOT=$HOME/go
      export PATH=$PATH:$GOROOT/bin
      export GOPATH=$HOME/go
      go get -d github.com/kahing/goofys@v0.20.0
      go install github.com/kahing/goofys@v0.20.0
      mkdir -p [マウントポイント]
      $GOPATH/bin/goofys --uid `id -u root` -o allow_other [マウントしたいS3バケット名] [マウントポイント]
  1. Dockerrun.aws.jsonにマウントに関する記述を追記する。
{
  "AWSEBDockerrunVersion": "1",
  "Image": {
    "Name": "ECRのイメージ名"
  },
  "Ports": [
    {
      "ContainerPort": "8080"
    }
  ],
  "Logging": "/var/log/httpd",
  "Volumes": [
    {
      "HostDirectory": "[マウントポイント]",
      "ContainerDirectory": "[マウントポイント]"
    }
  ]
}
  1. BeanstalkのEC2インスタンスに入り、以下のコマンドを叩く
$ sudo yum remove golang -y <- すでにyumでgolangが入ってる場合
$ wget https://go.dev/dl/go1.16.13.linux-amd64.tar.gz
$ sudo tar -C /root ./go1.16.13.linux-amd64.tar.gz
  1. デプロイを実行する。