OpenrestyでS3の静的コンテンツをプロキシするときにハマった話

この記事は、[DENSO Advent Calender 2020](https://qiita.com/advent-calendar/2020/denso "DENSO Advent Calender 2020")、9日目の記事です。
何気にQiita初投稿です。お手柔らかに。

# はじめに
お仕事で、VPN接続した環境を経由してプライベートなS3バケット上の静的コンテンツにアクセスしたいというリクエストがありました。
事例を調べると、クラメソさんでも事例が紹介されてて、簡単に実現できそう！と思ってました。

[nginxとS3を使って静的コンテンツを利用者を限定した形で利用できるようにする](https://dev.classmethod.jp/articles/private-content-hosting-with-nginx/ "nginxとS3を使って静的コンテンツを利用者を限定した形で利用できるようにする")

…なんですが、あれこれやってると、沼にはまってしまったのでその話をご紹介です。

# 構成
動作確認で作った構成は以下です。何も凝ったことはしてません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/930724/607addd0-4fe0-5e0b-311a-9db50866c9ca.png)

* 動作確認用にEC2インスタンスを立てる
* nginxはECSサービス(Fargate)
* S3バケットにはindex.htmlをアップロード

VPCエンドポイントやS3のバケットポリシーの設定については、クラメソさんの記事通りに作りました。
詳しくは前述の記事を参照されてください。

# はまった内容について（本題）
先に、はまった内容についてざっくりと。
起こった事象としては「nginx.conf正しいはずなのに、S3上のファイルが見えない」です。
以下のパターンで試して、結果４つ目のパターンで成功しました。

1. プロキシ先のURLを直打ち
1. コンテナの環境変数を使ってURLを生成
1. 変数を`{}`でくくってURLを生成
1. 末尾の`/`を取ってみる

この先、それぞれやったことと結果を紹介します。

## …と、その前に
先に、今回のDockerfileはこちら。
コンテナから環境変数を取得したいので、luaを使いたかった。
なので、nginx+luaを楽に実現できるopenrestyを採用しました。
alpineでも良かったんですが、何かとコマンドが使えず不便なのでcentosにしてます。

```
FROM openresty/openresty:centos
ADD ./nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
CMD ["/usr/local/openresty/bin/openresty", "-g", "daemon off;"]
```

そして、index.htmlには

```
Hello, World
```

とだけ書いてます。世界よ、こんにちは。

それでは、次こそ本題です。

## プロキシ先のURLを直打ち
まずは、動作確認の定番、「とりあえず直打ち」です。
nginx.confはこんな感じ。

```
worker_processes  1;
env PROXY_BUCKET;
events {
    worker_connections  1024;
}
http {
    client_max_body_size 0;    
    include       mime.types;
    default_type  application/octet-stream;
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    access_log  /dev/stdout main;
    error_log /dev/stdout;
    sendfile        on;
    keepalive_timeout  65;
    server {
        listen       80;
        server_name  localhost;
        location / {
            proxy_pass https://<bucket名>.s3.amazonaws.com/;
            proxy_intercept_errors on;
            resolver 169.254.169.253;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}
```

クラメソさんの記事通りのやり方です。
動作確認用EC2から、curlコマンドを叩くと、当然のことながら、うまく動作します。

```
[ec2-user@ip-10-0-2-100 ~]$ curl 10.0.2.10/index.html
Hello, World!
```

まあ、そりゃ当然。じゃあ、次は環境変数使ってみよう。

## コンテナの環境変数を使ってURLを生成

環境変数をluaの変数に使う際には`nginx.conf`の最上位で変数宣言しておかなければいけません。
あとは、`location`内で`os.getenv`を使って環境変数を変数に格納します。
変数は`set_by_lua`で設定できます。

```
worker_processes  1;
env PROXY_BUCKET;
events {
    worker_connections  1024;
}
http {
    client_max_body_size 0;    
    include       mime.types;
    default_type  application/octet-stream;
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    access_log  /dev/stdout main;
    error_log /dev/stdout;
    sendfile        on;
    keepalive_timeout  65;
    server {
        listen       80;
        server_name  localhost;
        location / {
            set_by_lua $proxy_bucket 'return os.getenv("PROXY_BUCKET")';
            proxy_pass https://$proxy_bucket.s3.amazonaws.com/;
            proxy_intercept_errors on;
            resolver 169.254.169.253;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}
```

これで動作確認用EC2から、curlコマンドを叩くと…

```
[ec2-user@ip-10-0-2-100 ~]$ curl 10.0.2.10/index.html
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Access Denied</Message><RequestId>3B30BCA84B285F50</RequestId><HostId>A9Y2Ld3/OH+r0OYFFB0ANo+7Mri9z7NicC5S5jw6VF/6zdiamxGnuJYOtzVm8CmM/fMCuHk6Nas=</HostId></Error>
```

あれ？Access Deniedになるな。
例えば、存在しないオブジェクトを以下のようにS3のURLで直接呼んだときにも同じ結果です。

```
[ec2-user@ip-10-0-2-100 ~]$ curl https://<バケット名>.s3.amazonaws.com/hogehoge.html
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Access Denied</Message><RequestId>3B30BCA84B285F50</RequestId><HostId>A9Y2Ld3/OH+r0OYFFB0ANo+7Mri9z7NicC5S5jw6VF/6zdiamxGnuJYOtzVm8CmM/fMCuHk6Nas=</HostId></Error>
```

ということは、これだと正しくパスが書かれていないことになるわけですよ。
つまり、変数が読めて無いか、何か変な文字が混ざってしまってるとか…？

あ、もしかして`https://`の`/`と、`.com/`の`/`で囲まれてるから、`$proxy_bucket`が正規表現みたいに捉えられてるのか？
ということで、次のパターンです。

## 変数を`{}`でくくってURLを生成
もし、正規表現でとらえられてるのであれば、以下でいけるはず。

```
worker_processes  1;
env PROXY_BUCKET;
events {
    worker_connections  1024;
}
http {
    client_max_body_size 0;    
    include       mime.types;
    default_type  application/octet-stream;
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    access_log  /dev/stdout main;
    error_log /dev/stdout;
    sendfile        on;
    keepalive_timeout  65;
    server {
        listen       80;
        server_name  localhost;
        location / {
            set_by_lua $proxy_bucket 'return os.getenv("PROXY_BUCKET")';
            proxy_pass https://${proxy_bucket}.s3.amazonaws.com/;
            proxy_intercept_errors on;
            resolver 169.254.169.253;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}
```

結果は…？

```
[ec2-user@ip-10-0-2-100 ~]$ curl 10.0.2.10/index.html
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Access Denied</Message><RequestId>3B30BCA84B285F50</RequestId><HostId>A9Y2Ld3/OH+r0OYFFB0ANo+7Mri9z7NicC5S5jw6VF/6zdiamxGnuJYOtzVm8CmM/fMCuHk6Nas=</HostId></Error>
```

相変わらず、Access Denied
結果変わらずでした。うーん、とりあえず思いつくことなんでもやるか、ということで末尾の`/`を取ってみることに。

## 末尾の`/`を取ってみる

変数名の`{}`は外し、最後の`/`だけを取ってみました。

```
worker_processes  1;
env PROXY_BUCKET;
events {
    worker_connections  1024;
}
http {
    client_max_body_size 0;    
    include       mime.types;
    default_type  application/octet-stream;
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    access_log  /dev/stdout main;
    error_log /dev/stdout;
    sendfile        on;
    keepalive_timeout  65;
    server {
        listen       80;
        server_name  localhost;
        location / {
            set_by_lua $proxy_bucket 'return os.getenv("PROXY_BUCKET")';
            proxy_pass https://$proxy_bucket.s3.amazonaws.com;
            proxy_intercept_errors on;
            resolver 169.254.169.253;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}
```

結果は…

```
[ec2-user@ip-10-0-2-100 ~]$ curl 10.0.2.10/index.html
Hello, World!
```

なんと！！世界にご挨拶できてる！！

## できたんだけども…
バケット名を直書きして出来たものが、変数使うと出来なくなるのってなんで？？
末尾の`/`に関しては、以下のような記事がありましたが、これをみても今の現象は全然納得できない。

[Nginxのlocationとproxy_passの末尾スラッシュによる挙動の違いを理解する](https://kuroeveryday.blogspot.com/2019/03/effect-of-trailing-slash-on-nginx-reverse-proxy.html "Nginxのlocationとproxy_passの末尾スラッシュによる挙動の違いを理解する")

# まとめ
環境変数使って、`proxy_pass`設定するときには、URLの末尾には`/`を入れるな！
だけど、それがなんでそうなるのかはわからない！！
…ので、教えてえらいひと。

と、いうわけでQiita初投稿の結論は「できたんだけど、わからないから教えて」という質問記事になってしまいました。
とりあえず動かしたい誰かの参考になれば嬉しいです。