AWS LambdaとECSでの相対パス処理の違いによる問題解決

## はじめに

AWS ECS/EC2でデプロイして問題ないコードはAWS Lambdaで問題が発生と、その解決方法について共有します。

## プロジェクトの背景

RailsアプリはAWS ECS上で安定して動作していましたが、一部機能をサーバーレス化するためにAWS Lambdaを導入しました。

Lambdaは既存のRailsプロジェクト内に実装し、コードの共有や管理を簡単にしました。

プロジェクト構造

```
rails_project/
├── app/
├── config/
├── db/
└── lambda/
    ├── FunctionA/
        ├── app.rb                 # Lambda入口ファイル
        └── Dockerfile             # Lambdaビルド用Dockerfile    
    ├── FunctionB/
    └── ...
```

## 問題の概要

同じコードベースにもかかわらず、ECSでは正常に動作していた機能が、Lambdaではうまく動かない問題に直面しました。

**エラーメッセージ:**
```
"errorMessage": "The keyfile 'config/gcp/credentials.json' is not a valid file.",
"errorType": "Init<RuntimeError>",
"stackTrace": [
  "/usr/local/bundle/gems/googleauth-1.13.1/lib/googleauth/credentials.rb:559:in `verify_keyfile_exists!'",
  "/usr/local/bundle/gems/googleauth-1.13.1/lib/googleauth/credentials.rb:614:in `update_from_filepath'",
  "/usr/local/bundle/gems/googleauth-1.13.1/lib/googleauth/credentials.rb:402:in `initialize'",
  "/usr/local/bundle/gems/google-cloud-tasks-v2-1.2.0/lib/google/cloud/tasks/v2/cloud_tasks/client.rb:205:in `new'",
  "/usr/local/bundle/gems/google-cloud-tasks-v2-1.2.0/lib/google/cloud/tasks/v2/cloud_tasks/client.rb:205:in `initialize'",
  "/usr/local/bundle/gems/google-cloud-tasks-3.0.0/lib/google/cloud/tasks.rb:85:in `new'",
  "/usr/local/bundle/gems/google-cloud-tasks-3.0.0/lib/google/cloud/tasks.rb:85:in `cloud_tasks'",
  "/app/config/initializers/google_cloud_tasks.rb:8:in `<top (required)>'",
]
```

## 原因

この問題の根本的な原因は、**ECS環境とLambda環境でのワーキングディレクトリの違い**にあります。

### 設定ファイルの状態

設定ファイル (`config/settings/staging.yml`) では、Google Cloud の認証ファイルのパスを相対パスで指定していました：

```yaml
gcp:
  workload_identity:
    credentials: 'config/gcp/credentials.json'
```

### ECSでは問題ない理由

ECS環境ではアプリケーションのルートが /app にあり、Rails起動時に RAILS_ROOT も /app に設定されるため、config/gcp/credentials.json のような相対パスも /app/config/gcp/credentials.json として正しく解決されます。

### Lambdaで問題が発生する詳細な仕組み

Lambda環境では、Dockerfileで以下のように設定していました：

```dockerfile
# /app/lambda/RailsRunnerFunction/Dockerfile.lambda
FROM amazonlinux:2

...

# アプリケーションファイルをコンテナにコピー
COPY --chown=user ../../ /app

# 作業ディレクトリをLambdaハンドラーのディレクトリに設定
WORKDIR /app/lambda/MyFunction

ENTRYPOINT ["aws_lambda_ric"]
CMD ["app.LambdaFunction::Handler.process"]
```

問題の詳細な流れは以下の通りです：

#### 1. Lambda関数の起動プロセス

まず、Lambdaが起動すると指定されたハンドラーが実行されます：

```
CMD ["app.LambdaFunction::Handler.process"]
```

#### 2. app.rbでのRails環境の読み込み

Lambdaハンドラーのあるファイル (`app.rb`) では、以下のようにRails環境を読み込んでいます：

```ruby
# /app/lambda/MyFunction/app.rb
# Railsの環境を利用するため
require_relative '../../config/environment'

module LambdaFunction
  class Handler
    def self.process(event:, context:)
      # Lambda処理
    end
  end
end
```

注目すべきは `require_relative '../../config/environment'` という部分です。これは相対パスで、`/app/config/environment.rb` を読み込むことになります。

#### 3. Rails初期化時の問題

そして、Rails環境が読み込まれると、`config/initializers` ディレクトリの全ファイルが実行されます：

```ruby
# /app/config/environment.rb
Rails.application.initialize!  # ここで全初期化処理が走る
```

しかし、**重要なポイントとして**、この時点でもカレントディレクトリは
`/app/lambda/MyFunction` のままなのです。

#### 4. 初期化コードでの認証ファイル読み込み

Google Cloud Tasks の初期化コードでは：

```ruby
# /app/config/initializers/google_cloud_tasks.rb
credentials_path = Settings[:gcp][:workload_identity][:credentials]
```

この `credentials_path` は相対パスなので、カレントディレクトリ (`/app/lambda/MyFunction`) から解決されてしまいます：

```
/app/lambda/MyFunction/config/gcp/credentials.json
```

しかし、実際のファイルは異なる場所にあります：

```
/app/config/gcp/credentials.json
```

#### 5. エラーの発生

`googleauth` ライブラリ内部で、ファイルの存在確認が行われ、見つからないとエラーが発生します：

```ruby:/usr/local/bundle/gems/googleauth-1.13.1/lib/googleauth/credentials.rb
# googleauth gem内部コード
def verify_keyfile_exists! keyfile
  exists = ::File.file? keyfile
  raise "The keyfile '#{keyfile}' is not a valid file." unless exists
end
```

このため、「The keyfile 'config/gcp/credentials.json' is not a valid file.」というエラーが発生するのです。

このエラーメッセージから「ファイルが無効」と思いがちですが、実際には**ファイルが見つからない**ことを意味しています。

## 解決策

この問題には主に2つの解決策があります：

### 解決策1: 絶対パスを使用する

設定ファイルで絶対パスを使用することで、どの環境でも一貫した動作を保証できます：

```yaml
gcp:
  workload_identity:
    credentials: '/app/config/gcp/credentials.json'
```

### 解決策2: パスを動的に解決する

Rails環境内でパスを適切に解決するコードを追加します：

```ruby
# config/initializers/google_cloud_api.rb
if Rails.env.staging? || Rails.env.production?
  begin
    # 相対パスを絶対パスに変換
    credentials_path = Settings[:gcp][:workload_identity][:credentials]
    credentials_path = File.join(Rails.root, credentials_path) if credentials_path && !credentials_path.start_with?('/')

    ...
  rescue => e
    Rails.logger.error "Failed to initialize API client: #{e.message}"
    API_CLIENT = nil
  end
end
```

## 学び
1. **設定ファイルでは絶対パスを優先する**：特に複数の環境で実行される可能性があるアプリケーションでは、絶対パスを使用することでパス解決の問題を避けられます。

2. **Railsアプリケーションを Lambda で実行する際の考慮点**：Lambda環境はウェブサーバーとは異なる動作をするため、パス解決、初期化方法、環境変数の取り扱いなどに注意が必要です。