ECS/FargateでDockerfileのchownが効かない|非rootコンテナがPermission deniedになる理由

トラブル

はじめに

ECS/Fargateでコンテナを非rootユーザーとして実行したところ、ログ出力先のディレクトリでPermission deniedが発生したことがありました。

Dockerfileでは、ログ出力先のディレクトリを作成し、アプリケーションを実行するユーザーへchownしていました。Dockerイメージだけを見ると、書き込みできるように設定されています。

問題が起きたのは、アプリケーションコンテナとFluent Bitコンテナでログ用のVolumeを共有する構成にしたときでした。

原因を調べていくと、Dockerfileの設定が常に無効になるという単純な話ではありませんでした。Dockerfile、ECSタスク定義、Volumeを個別に確認するだけではなく、それらを組み合わせた実行時の最終状態まで設計対象にできていたかという問題に行き着きました。

この記事では、実際に発生した事象とAWS公式ドキュメントで確認した仕様を整理しながら、なぜ設計段階でこの問題に気づけなかったのか、事前のPoCで何を確認しておけばよかったのかまで振り返ります。

本記事は実際に発生した事象と、その際に採用して解消した対策をもとに整理しています。あわせて、AWS公式ドキュメントで確認できる仕様と、今回の環境では採用していない対策候補を区別して記載します。

ECS/FargateでPermission deniedが発生した

今回の構成を単純化すると、アプリケーションがファイルへログを書き込み、同じVolumeをマウントしたFluent Bitがそのログを読み取る構成でした。

Application Container
非rootユーザー
        │
        │ ログ書き込み
        ↓
    Shared Volume
        │
        │ ログ読み込み
        ↓
Fluent Bit Container

ECSのタスク定義では、Volumeを定義し、アプリケーションコンテナとFluent Bitコンテナの双方へマウントしていました。

本記事では、タスク定義でボリューム名を定義した空のデータVolumeを、AWS公式ドキュメントに合わせてbind mountと呼びます。AWS公式でも、bind mountの用途として、1つ以上のコンテナにマウントする空のデータVolumeが示されています。Fargateでは、bind mountにタスクへ割り当てられたエフェメラルストレージが利用されます。

詳細は、AWS公式「Amazon ECSでのバインドマウントの使用」とAWS公式「Amazon ECS向けのFargateタスクエフェメラルストレージ」で確認できます。

アプリケーションコンテナは非rootユーザーで動作させていました。Dockerfileでは、概念的には次のようにログ出力先を作成し、所有者を変更しています。

RUN mkdir -p /app/logs
RUN chown appuser:appgroup /app/logs

USER appuser

タスク定義側のuserによる上書きはなく、実行ユーザーはDockerfileのUSERで指定した非rootユーザーでした。

このDockerイメージだけを見ると、appuserが/app/logsへ書き込めるように見えます。

ところがECS/Fargate上でタスクを起動すると、アプリケーションがログを書き込む際にPermission deniedが発生しました。

Dockerfileでchownしたのに、なぜ書き込めなかったのか

問題のポイントは、Dockerfileでchownしたディレクトリと、ECS/Fargate上でアプリケーションが実際に書き込むディレクトリが、Volumeマウント後は同じものではなかったことです。

Dockerイメージ内では、/app/logsの所有者をappuserへ変更していました。

Docker Image
    │
    └─ /app/logs
         owner = appuser

この状態だけを見れば、appuserは/app/logsへ書き込めるように見えます。

ECSで同じ/app/logsへVolumeをマウントすると、イメージ内のディレクトリはマウントによって覆い隠されます。実行中のコンテナから見えるのは、Dockerfileでchownしたイメージ側の/app/logsではなく、Volume側の/app/logsです。

Docker Image
/app/logs
owner = appuser
        │
        │ ECS Task起動
        ↓
Volumeを /app/logs へmount
        │
        ↓
イメージ側の /app/logs は隠蔽
        │
        ↓
コンテナから見える /app/logs
= Volume側

Dockerfileで設定したディレクトリが削除されたわけではありません。同じパスへVolumeをマウントしたことで、イメージ側のディレクトリが実行中のコンテナから見えなくなっています。

Dockerfileでchownしていても、Volumeマウント後の/app/logsが同じ所有者・権限になるとは限りません。

今回見落としていたのは、この「イメージ作成時の権限」と「Volumeマウント後の実効権限」の違いでした。

DockerfileのVOLUMEとcontainerPathが一致する場合

ここまでだけを見ると、「VolumeをマウントするとDockerfile側の権限設定は使われない」と考えたくなります。

Amazon ECSには、もう一つ重要な仕様があります。

DockerfileのVOLUMEで指定した絶対パスと、タスク定義のcontainerPathが一致する場合、ECSはそのパス内のデータをデータVolumeへコピーします。

AWS公式ドキュメントには、非rootユーザー向けに対象ディレクトリをchownし、USERを指定した後にVOLUMEを宣言する例も掲載されています。この説明の中では、既定のVolume権限を0755、所有者をrootとし、Dockerfileでカスタマイズできることも示されています。

詳細は、AWS公式「Amazon ECSでのバインドマウントの使用」を参照してください。

概念的には次のような記述です。

RUN mkdir -p /app/logs
RUN chown appuser:appgroup /app/logs

USER appuser
VOLUME ["/app/logs"]

今回使用していたDockerfileには、ログ出力先をVOLUMEとして宣言する記述はありませんでした。

Dockerfile
/app/logs を appuser に chown
        │
        ↓
ECSで同じパスへVolumeをmount
        │
        ↓
イメージ側の /app/logs は隠蔽
        │
        ↓
VOLUME宣言なし
        │
        ↓
Volumeマウント後の権限で
書き込み可否が決まる

今回の構成では、Dockerfileでchownした/app/logsへVolumeをマウントしたことで、イメージ側のディレクトリは隠蔽されました。また、Dockerfileに対象パスのVOLUME宣言がなく、VOLUMEとcontainerPathを一致させてイメージ側の状態をVolumeへ引き継ぐ仕組みも利用していませんでした。

当時は、このDockerfileとECS Volumeを組み合わせた後に、実行時にどのような状態になるのかを設計観点として持っていませんでした。

VOLUME宣言がなかったことだけで、Permission deniedの原因をすべて説明できるわけではありません。実際の書き込み可否は、Volumeマウント後のowner、group、modeと、アプリケーションの実効UID/GID、書き込み対象パスの組み合わせで決まります。

今回の問題で重要だったのは、Dockerfileでchownしたという設定だけを見て書き込み可能と判断し、Volumeマウント後にどの所有者・権限になるのかを確認していなかったことです。

今回の案件ではVOLUMEを利用する方法は採用していないため、ここではAWS公式仕様から確認できる対策候補の一つとして扱います。

実効ユーザーもDockerfileだけで確認しない

ファイルやディレクトリの権限とあわせて確認する必要があるのが、コンテナを実際にどのユーザーで実行しているかです。

ECSでは、DockerfileのUSERとは別に、タスク定義のuserでも実行ユーザーを指定できます。このパラメータはDockerのUserや--userに対応します。

今回のタスク定義ではuserを指定しておらず、DockerfileのUSERを上書きする設定はありませんでした。

設計レビューで確認すべきなのは、「DockerfileにUSER appuserと書いてある」ことではありません。

ECS上で最終的にどのUID/GIDでプロセスが動き、そのユーザーがVolumeマウント後のパスへ必要な操作をできるかまで確認する必要があります。

Volumeの権限と同じく、Dockerfileやタスク定義を個別に見るだけでは不十分です。設計対象にすべきなのは、それらを組み合わせた後の実行時の最終状態です。

userパラメータの仕様は、AWS公式「FargateでのAmazon ECSタスク定義パラメータ」で確認できます。

対策はchownの方法だけで考えない

Permission deniedが発生すると、chmodやchownの方法から考えたくなります。

今回の構成を設計の観点から見直すと、その前に確認すべきことがあります。

アプリケーションコンテナとFluent Bitの間に、そもそも共有Volumeが必要なのか。

権限をどう合わせるかだけではなく、権限調整が必要になる境界そのものをなくせないかを最初に検討します。

共有Volume自体をなくせないか

今回の構成は次のようになっていました。

Application
    ↓ file
Shared Volume
    ↓ file
Fluent Bit

ログ出力要件が許せば、アプリケーションのログをファイルへ出力せず、awsfirelensログドライバ経由でFluent Bitへ渡す構成も考えられます。

FireLensでは、アプリケーションコンテナにawsfirelensログドライバを設定し、Fluent BitまたはFluentdをログルーターとして利用できます。

詳細は、AWS公式「Amazon ECSタスク定義の例:FireLensにログをルーティングする」を参照してください。

Application
    ↓ awsfirelens
Fluent Bit

この構成に変更できれば、アプリケーションとFluent Bitの間でログファイルを共有するVolumeをなくせる可能性があります。Volumeに付随するowner、group、mode、UID/GID、マウント後の権限といった設計項目も減らせます。

実案件でも、ログ出力方式そのものを変更して共有Volumeをなくす案を検討しました。既存のログ出力方式を変更するとAP側での改修と再テストが必要になるため、今回は共有Volumeを残したうえで権限設定方法を変更する判断としました。

設計として上位の選択肢を検討しても、変更影響やプロジェクト条件によって採用できないことがあります。

重要なのは、「権限エラーをどう直すか」から始めるのではなく、「その権限設計自体をなくせないか」を一度確認したうえで、案件条件に合った方法を選ぶことです。

VOLUMEディレクティブを利用する方法

共有Volumeを残す場合には、前述したDockerfileのVOLUMEとcontainerPathの仕様を利用する方法も候補になります。

この方法ではDockerfile、すなわちコンテナイメージそのものを変更します。ECS上の課題を解決する目的でVOLUMEを追加しても、同じイメージを別の環境で実行した場合の挙動にも影響します。

今回の環境ではこの方法を追加検証していないため、AWS公式仕様から確認できる対策候補としての紹介に留めます。

補足:configuredAtLaunchはbind mountの設定ではない

対策を調べる過程では、ECSのVolumeに関する情報を複数確認しました。その中で紛らわしかったのがconfiguredAtLaunchです。

名称からはVolumeの初期設定に使えるようにも見えますが、configuredAtLaunch=trueは、ECSのデプロイ時にAmazon EBS Volumeを構成するための設定です。今回利用していたbind mountの権限を変更するためのオプションではありません。

AWS公式「Amazon ECSのデプロイ時にAmazon EBSボリューム設定を指定する」でも、configuredAtLaunchをtrueに設定したタスク定義に対して、デプロイ時にAmazon EBS Volumeを構成する仕組みとして説明されています。

同じ「Volume」という言葉で扱われていても、bind mount、EBS、EFSでは仕様も設定方法も異なります。目にした情報をそのまま設計判断に使うのではなく、自分が利用しているVolume方式を特定したうえで、該当する公式ドキュメントを確認する必要があります。

権限設定用コンテナを先行して実行する

別の方法として、Volumeの所有者や権限を設定するコンテナを先に実行し、正常終了した後にアプリケーションコンテナを起動する構成があります。

ECSではdependsOnでコンテナ間の起動依存関係を設定できます。SUCCESSを指定すると、依存先コンテナが終了コード0で完了するまで後続コンテナを開始しません。SUCCESSとCOMPLETEの依存先にはessential containerを指定できません。

詳細は、AWS公式「FargateでのAmazon ECSタスク定義パラメータ」を参照してください。

Permission Setting Container
        │
        │ Volumeの所有者・権限を設定
        │ exit 0
        ↓
Application Container
        ↓
Fluent Bit Container

権限設定用コンテナには、共有Volumeへ必要な所有者・権限を設定できる権限を持たせます。アプリケーションコンテナ自体は従来どおり非rootユーザーで実行します。

この方法であれば、アプリケーションイメージを変更せず、タスク定義を中心とした基盤側の変更で対応できます。

なお、権限設定用コンテナでVolumeの初期権限を整えても、その後アプリケーションが生成するファイルを別コンテナから読めるとは限りません。TomcatとFluent BitでVolumeを共有した際に実際に発生したPermission deniedと、UID/GID・setgid・umaskを含む権限設計について、続編で整理しました。

Fluent BitがPermission deniedでログを読めない|ECS/Fargate共有VolumeのUID/GID設計
ECS/FargateでTomcatとFluent Bitが共有Volumeを利用する構成でPermission deniedが発生。Fluent BitのUID/GID、setgid、umask、read-onlyマウントなど、Dockerコンテナ間でログを共有する際の権限設計を実例から解説します。

rootユーザーで実行する

アプリケーションコンテナ自体をrootユーザーで実行する方法もあります。

権限問題は回避しやすくなりますが、非root実行を前提としていたセキュリティ設計との整合を確認する必要があります。動作するかどうかだけではなく、なぜ非rootユーザーで実行する設計にしていたのかまで戻って判断する必要があります。

技術的に動く対策と、プロジェクトで採用できる対策は違う

今回検討した方法を、変更対象まで含めて整理すると次のようになります。

対策主な変更対象実務上の論点
共有Volumeをなくすアプリケーション/ログ出力設定/タスク定義既存のログ出力方式を変更できるか
VOLUMEを利用Dockerfile/コンテナイメージAP側成果物の変更と再テストが必要
権限設定用コンテナタスク定義基盤側で完結しやすいが構成要素が増える
rootで実行実行ユーザー/セキュリティ設計非root要件との整合が必要

最終的には、権限設定用コンテナを先行して実行する構成を採用しました。

アプリケーション側を修正したりログ出力仕様を変更したりする場合、AP側での改修とテストが必要になります。権限設定用コンテナの追加であれば、タスク定義を管理する基盤側で変更を完結でき、既存のコンテナイメージにも影響を与えません。

変更範囲を基盤側に限定できることから、この方式を採用しました。 この構成への変更後は、非rootユーザーで実行するアプリケーションからログを正常に書き込めるようになり、Permission deniedは解消しました。

技術的に成立する対策と、プロジェクトとして採用しやすい対策は同じとは限りません。変更主体、責任分界、変更影響、追加試験の範囲まで含めて対策を決める必要があります。

なぜ設計段階で気づけなかったのか

今回の問題で重要なのは、ECSのVolume仕様を知っていたかどうかだけではありません。

結果として、Dockerfileで設定した権限と、ECSがVolumeをマウントした後の実効権限との接続部分を、設計時に十分確認できていませんでした。

Dockerfileだけを見れば、確認対象は次の範囲です。

Dockerfile
 ├ mkdir
 ├ chown
 └ USER

実際には、その後ろまで設計する必要があります。

Docker Image
      ↓
ECS Task Definition
      ↓
Volume生成
      ↓
containerPathへmount
      ↓
実効UID/GID
      ↓
Applicationからwrite
      ↓
Fluent Bitからread

「Dockerfileで対象ディレクトリをchownしたこと」と、「Volumeをマウントした実行環境でアプリケーションユーザーが書き込めること」は同じではありません。

前者は設定内容です。設計で最終的に保証したいのは後者です。

「担当者が知らなかった」で終わらせない

今回のECS Volumeの仕様を知っている担当者であれば、設計段階で問題に気づけた可能性があります。

原因を「担当者が仕様を知らなかった」で終わらせても、次の案件で同じ問題を防ぐ仕組みにはなりません。

クラウドでは、自分たちがDockerfileや設定ファイルへ直接記述するものだけでなく、クラウドサービスが実行時に生成、マウント、変更するものがあります。必要なのは、それらを含めた実行時の最終状態を設計する観点です。

今回であれば、設計レビューでは次のような項目を確認すべきでした。

設計項目確認する内容
Volume方式bind mount、EBS、EFSなど、どの方式を利用するか
Volume生成誰が、いつ生成するか
containerPathどのパスへマウントするか
Dockerfile VOLUME定義の有無とcontainerPathとの関係
実効UID/GIDDockerfileのUSER、タスク定義のuserを踏まえて誰が実行するか
Application側権限誰が、どのパスへwriteするか
Fluent Bit側権限誰が、どのパスをreadするか
owner/group/modeVolumeマウント後にどうなるべきか
起動順権限設定が必要な場合、いつ実施するか
Volumeライフサイクルタスク停止・再作成時にデータや設定がどうなるか
変更主体Dockerfile、タスク定義、ログ設定の誰が変更するか

Fargateで利用するbind mountはタスクのエフェメラルストレージ上にあり、bind mountを使用するすべてのコンテナが停止するとデータは削除されます。今回のようなFargateタスクでは、タスク停止時のライフサイクルまで含めて設計する必要があります。

詳細は、AWS公式「Amazon ECSでのバインドマウントの使用」を参照してください。

設計書に「Dockerfileでchownする」と書くだけでは足りません。Volumeをマウントした後に、どのUID/GIDが、どのパスへ、どの権限でアクセスできるのかを定義する必要があります。

ここまで書いて、初めて実行時の権限設計になります。

この考え方は、ECSだけに限りません。基本設計で決めた方式を、構築・テストできる状態までどう落とすかについては、次の記事で詳しく整理しています。

事前PoCでは、ここまで確認すべきだった

今回の問題は、設計段階のPoCで発見できた可能性があります。

ECSでコンテナが起動することだけを確認するPoCでは足りません。

今回のPermission deniedを検出するだけなら、大掛かりな試験は必要ありませんでした。本番と同じ非rootユーザーとVolume構成でタスクを起動し、次の範囲を確認すれば、書き込み権限の問題を設計段階で検出できた可能性があります。

ECS Task起動
      ↓
実効UID/GIDを確認
      ↓
Volume mount後のowner/group/modeを確認
      ↓
Applicationからファイル作成
      ↓
Applicationからディレクトリ作成

これが、今回の問題を発見するために必要だった最低限のPoCです。

ログ機構全体の成立性まで確認するのであれば、その先にFluent Bitからの読み取り、ログ転送、Task再作成後の動作まで確認します。

Applicationからwrite
      ↓
Fluent Bitからread
      ↓
ログ転送
      ↓
Task再作成後も成立するか

設計レビュー表とPoCで似た項目を確認するのには理由があります。

設計では、実行時に成立すべき状態を定義する。PoCでは、その状態が実際の環境で成立することを確認する。

今回なら、設計では「非rootのアプリケーションユーザーがVolumeマウント後のログ出力先へwriteできる」と定義します。PoCでは、本番相当のユーザーとVolume構成で実際にファイルを作成して確認します。

PoCは「AWSのサービスが使えるか」を試すだけの工程ではありません。

本番と同じ境界を通したときに、設計したシステムが成立するかを確認する工程です。

クラウドリフトでも、AWSサービス単体の機能確認だけを行い、オンプレミスからクラウドへ移した後の実際の処理や運用まで通さなければ、問題が後工程で表面化します。今回のVolume権限も規模は小さいものの、構造は同じでした。

クラウドリフトでPoCに何を含めるべきかは、次の記事で詳しく整理しています。

今回の事象から得た3つの学び

一つ目は、Dockerfileは設定であって、実行時の最終状態ではないことです。

DockerfileでUSERやchownを指定しても、それだけでECS上の実効ユーザーやVolumeマウント後の権限は確定しません。設計で確認するべきなのは、各設定をECS上で組み合わせた後の最終状態です。

二つ目は、設計で抜けやすいのはコンポーネント単体ではなく、その接続部分であることです。

Dockerfileの設定もECSのVolume定義も、それぞれ単体で存在しています。今回確認できていなかったのは、DockerイメージとECS Volumeを接続した後に、どのユーザーがどのファイルシステムへアクセスするのかという実行時の状態でした。

三つ目は、PoCでは本番と同じ境界を通す必要があることです。

コンテナが起動したことだけでは、システムとして成立したことにはなりません。本番と同じUID/GID、Volume、マウントポイント、サイドカーを含めて実際の処理を通し、設計した状態が成立することを確認する必要があります。

今回の事象を振り返ると、

「Dockerfileでchownした」ではなく、「Volumeをマウントした後に、必要なユーザーが必要な操作をできる」まで確認して初めて権限設計は完了する

ということになります。

クラウドでは、自分たちが直接構築・設定したものだけを見るのでは足りません。クラウドサービスが実行時に生成、変更、接続するものまで含めて最終状態を設計し、不確実性が残る境界は事前PoCで実際に通す。

今回のPermission deniedは小さな権限エラーでしたが、クラウド設計とPoCで何を確認するべきかを改めて考えるきっかけになりました。

参考にしたAWS公式ドキュメント


関連記事

ECS/Fargateの権限問題を、設計レビューで何を確認するかという観点を、運用設計や要件定義の工程まで広げて確認したい方は、以下の記事も参考にしてください。

  • 運用設計とは?運用設計書の目次サンプルと項目一覧|基盤SE視点
    ログ、監視、ジョブ、バックアップ、障害対応など、本番運用を成立させるために設計しておくべき項目を整理しています。
運用設計とは?運用設計書の目次サンプルと項目一覧|システム基盤エンジニア視点
運用設計とは何かをシステム基盤エンジニアの実務目線で解説します。運用設計書の目次サンプルと検討項目一覧を示し、監視・ログ・バックアップ・ジョブ管理、誰がやるのか、要件定義・基本設計との関係、処理方式設計との双方向連携を整理します。
  • 要件定義とは何をする工程か|RFP・見積前提から認識差を潰す実務
    RFPや提案内容を前提に、スコープや責任分界を確定し、設計・構築・テストできる水準まで具体化する実務を解説しています。
要件定義とは何をする工程か|RFP・見積前提から認識差を潰す実務
要件定義とは何をする工程なのか。RFP、質問回答、提案書、見積前提から認識差を潰し、スコープや責任分界を確定する実務を、大規模SIの具体的な失敗事例とともに解説します。

コメント

タイトルとURLをコピーしました