Cloud Run + PipenvでPATHエラーが出てハマった話

## 概要

Cloud Runを試そうと思い、Pipenvで作ったサンプルアプリのDockerイメージをデプロイしたところ、以下のエラーが出て起動しませんでした。

`Error: the command XXX could not be found within PATH.`
※ XXXは対象のコマンド

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/fe27bd29-e155-2cd9-0288-cfc105e706dd.png)

画像はCloud Loggingのログです。今回は `uvicorn` を使用していました。

このエラーを解消したお話です。

## 要約

Dockerfileに以下を追加して解消しました。
`PIPENV_VENV_IN_PROJECT` は、pipenvの仮想環境をプロジェクトのディレクトリに作成するための環境変数です。

```Dockerfile
ENV PIPENV_VENV_IN_PROJECT=1
```

**ローカルとCloud Runでコンテナの実行ユーザーが異なり、パスが合わなかったことが原因**と考えられます。

## 環境

 - Python 3.10.13
 - pipenv, version 2023.10.24
 - alpine3.19

## 詳細

### エラー原因

Pipenvは、デフォルトだとプロジェクト外の場所に仮想環境を作成します。
ビルドしたときのログをみると、`/root` 配下に環境を作成していることがわかります。

```
✔ Successfully created virtual environment!
Virtualenv location: /root/.local/share/virtualenvs/app-4PlAip0Q
```

元々のDockerfileは以下です。プロジェクトは `/app` 配下です。

```docker
FROM python:3.10.13-alpine3.19

WORKDIR /app

COPY Pipfile Pipfile.lock app/ /app/
RUN pip install pipenv && pipenv sync

EXPOSE 80

ENTRYPOINT ["pipenv", "run", "serve"]
```

このイメージをCloud Runにデプロイすると、先のエラーが出力されます。

ここでCloud Loggingのログを見ると、仮想環境の場所として`/home`配下のディレクトリが参照されていることがわかります。

`Virtualenv location: /home/.local/share/virtualenvs/app-4PlAip0Q`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451175/fe27bd29-e155-2cd9-0288-cfc105e706dd.png)

エラーの直接原因は、**PATHの違いにより作成された仮想環境を参照していないため**と想定されます。

### なぜPATHが違うのか

**Cloud Runでのコンテナ実行ユーザーが非rootユーザーであるため**、と考えられます。

ローカルではあまり意識しないかもしれませんが、コンテナのベストプラクティスとしてrootユーザーでのコンテナ実行は推奨されていません。これはOWASPの Kubernetes Top 10 で確認できます。


> Application processes should not run as root: Running the process inside of a container as the root user is a common misconfiguration in many clusters. While root may be an absolute requirement for some workloads, it should be avoided when possible. If the container were to be compromised, the attacker would have root-level privileges that allow actions such as starting a malicious process that otherwise wouldn’t be permitted with other users on the system.

> ---
> (Google 翻訳) アプリケーションプロセスは root として実行しないでください。コンテナ内でプロセスを root ユーザーとして実行することは、多くのクラスターでよくある構成ミスです。一部のワークロードでは root が絶対必要な場合がありますが、可能な場合は避ける必要があります。コンテナが侵害された場合、攻撃者は root レベルの権限を持ち、システム上の他のユーザーには許可されない悪意のあるプロセスの開始などのアクションを許可することになります。


https://owasp.org/www-project-kubernetes-top-ten/2022/en/src/K01-insecure-workload-configurations

Cloud Run は Google Cloud が管理する Borg (KubernetesのベースになったGoogle社内ツール) 上でコンテナが稼働するサービスです。このプラクティスと同様に、Cloud Runも非rootユーザーで実行されているのだと考えられます。

非rootユーザーでは `/root` 配下に作られた仮想環境は参照できず、PATHのエラーとなります。試しに、Dockerfile に非rootユーザーを追加して`docker build` & `docker run` すると、同じエラーが再現できます。

```diff_docker
FROM python:3.10.13-alpine3.19

WORKDIR /app

COPY Pipfile Pipfile.lock app/ /app/
RUN pip install pipenv && pipenv sync

+ RUN adduser -D appuser -h /home/appuser && \
+     chown -R appuser:appuser /app /home/appuser
+ USER appuser

EXPOSE 80

ENTRYPOINT ["pipenv", "run", "serve"]
```

 - `docker run` の実行結果

```bash
✔ Successfully created virtual environment!
Virtualenv location: /home/appuser/.local/share/virtualenvs/app-4PlAip0Q
Error: the command uvicorn (from serve) could not be found within PATH.
```

### 解決方法

以下の通り、`PIPENV_VENV_IN_PROJECT` の環境変数を設定します。
これにより、プロジェクト配下の `/app/.venv` に仮想環境が作成され、非rootユーザーにもPATHが通ります。

```diff_docker
FROM python:3.10.13-alpine3.19

WORKDIR /app

# pipenvの仮想環境をプロジェクトに作成
+ ENV PIPENV_VENV_IN_PROJECT=1

COPY Pipfile Pipfile.lock app/ /app/
RUN pip install pipenv && pipenv sync

EXPOSE 80

ENTRYPOINT ["pipenv", "run", "serve"]
```

## 参考

 - Github の issue

https://github.com/pypa/pipenv/issues/4174

 - pipenv 公式ドキュメント

https://docs.pipenv.org/advanced/#configuration-with-environment-variables

 - Cloud Run を徹底解説！ - G-gen Tech Blog
 
https://blog.g-gen.co.jp/entry/cloud-run-explained