Laravel11のコンテナ作成からFargateでサービス開始まで

表題に関していろいろなサイトを参考にしつつなんとかFargateサービス開始まで漕ぎつけましたが結構苦労したので一通りの手順をまとめておきます。

主に以下のサイトを参考にさせてもらい、この記事の内容もかなり参考サイトに近い内容なっていますが、そのままだと動作しなかったり、サービス開始についてまでは記載されていなかったのでそれらを補完した内容になります。

参考サイト:
https://it.kensan.net/laravel11-ecs-fagate-deploy.html

# 前提と構成
前提：
- DBはAWSのRDS(MySQL)をプライベートサブネットに設置済み
- 開発環境に（私の場合はWSL上）にMySQLをインストール済み
- VPC、AZ等作成済み
- AWS CLIインストール済み

構成としてはざっくり以下の図のようになります。

環境変数を設定する.envファイルは、.env.production、.env.localなど開発環境用と本番用に分けて保存しておき、Githubには上げずにビルド時に入れ替える方法もありますが、これだと手間がかかる上に間違いも起きそうなので、開発環境では環境ごとの情報や秘匿したい情報などは.env.laravel.localファイルに保存し、ECS上ではAWS Systems ManagerのParameter storeに保存することにしました。
つまり、.envはGithubに上げ、.env.laravel.localは.gitignoreに入れてローカルで保存するようにします。

![無題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/930440/2133a878-857d-cbc2-be07-85b152c0c9ce.png)

# ディレクトリとファイル作成
ディレクトリ構成は以下のようになります。
```
├── docker
│   ├── nginx
│   │   ├── Dockerfile
│   │   └── default.conf.template
│   └── php
│       └── Dockerfile
├── laravel-project
│       └── .env.laravel.local (Laravelインストール後に作成)
└── docker-compose.yml
```

上記のディレクトリ構成の通りlaravel-project以外のディレクトリを作成し、各ファイルを作成していきます。

```. nginx/Dockerfile
FROM nginx:latest
COPY docker/nginx/default.conf.template /etc/nginx/templates/
```

```nginx/default.conf.template
server {
  listen 80;
  root /var/www/laravel-project/public;
  index index.php;
  location / {
    root /var/www/laravel-project/public;
    index index.php;
    try_files $uri $uri/ /index.php?$query_string;
  }
  location ~ \.php$ {
    fastcgi_pass ${FASTCGI_PASS};
    fastcgi_index index.php;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
  }
}
```
laravel-projectの部分は適当なプロジェクト名に置き換えてください。
${FASTCGI_PASS}は変数にしておくことで開発環境とFargate上とで自動的に適切な値に置き換えます。

```. php/Dockerfile
FROM php:8.3-fpm AS base
EXPOSE 5173

RUN apt-get update && apt-get upgrade -y \
  && apt-get install -y zlib1g-dev vim libzip-dev \
  && docker-php-ext-install zip pdo_mysql

COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

ENV COMPOSER_ALLOW_SUPERUSER 1
ENV COMPOSER_HOME /composer
ENV PATH $PATH:/composer/vendor/bin

# アプリケーションファイルをコピー
COPY . /var/www/

WORKDIR /var/www

# Node.jsのインストール
RUN curl -sL https://deb.nodesource.com/setup_20.x | bash - \
    && apt-get install -y nodejs

### 開発環境 ###
FROM base AS development

# 開発用ツールのインストール
RUN composer global require "laravel/installer"


### 本番環境 ###
FROM base AS production

# Composerの依存関係をインストール（開発用パッケージは除外）
RUN composer install --no-dev --optimize-autoloader

# npmパッケージのインストールとビルド
WORKDIR /var/www/laravel_project
RUN npm install
RUN npm run build

# 権限の設定
RUN chown -R www-data:www-data /var/www \
    && chmod -R 755 /var/www/laravel_project/storage
```

RUN npm run buildをした時点でプロジェクト内のpublic内にbuildフォルダが作成されるはずなのですが、なぜかできないため本番ビルド時は手動でnpm run buildします(原因が分かる方、是非教えてください！)

```docker-compose.yml
services:
  app:
    build:
      context: .
      dockerfile: ./docker/php/Dockerfile
      target: ${DOCKER_TARGET:-development}  # 環境変数で開発/本番を切り替え
    container_name: app_laravel
    volumes:
      - .:/var/www
    ports: ['9001:9000', '5173:5173']  # 5173ポートは開発サーバーで使用される
    env_file:
      - ./laravel-project/.env.laravel.local
  nginx:
    build:
      context: .
      dockerfile: ./docker/nginx/Dockerfile
    container_name: nginx_laravel
    ports:
      - 8000:80
    working_dir: /var/www
    depends_on:
      - app
    environment:
      FASTCGI_PASS: app:9000 # ここで開発環境のDockerで使用されるdefault.conf.template内のfastcgi_passを設定
```
environment: FASTCGI_PASSの値は開発環境でのみ使用されます。ECS上で使用される値は後程Parameter storeで設定します。
ポート番号も5173は開発環境でしか使わないのでここも変数化したほうが良いかも。

# コンテナ起動

docker-compose.ymlファイルがあるディレクトリに移動し、以下のコマンドでコンテナを起動します。
```
docker compose up -d
```

# Laravel 11をインストール

コンテナに入りLaravel11をインストールします。
```
docker compose exec app bash
composer create-project --prefer-dist laravel/laravel laravel-project "11.*"
```

Laravelのインストールが完了したら.envファイルと同じ階層に.env.laravel.localファイルを作成します。
開発環境で使用する環境変数と秘匿情報をこのファイル内に記述します。AWSについてはS3などをアプリ内で使用する場合はここで設定します。使用しない場合は必要ありません。

```laravel-project/.env.laravel.local
APP_ENV=local
APP_KEY=xxxx
APP_DEBUG=true
APP_URL=http://localhost

LOG_CHANNEL=single

DB_HOST=xxx.xxx.xxx.xxx
DB_PORT=3306
DB_DATABASE=dbname
DB_USERNAME=username
DB_PASSWORD=password

AWS_ACCESS_KEY_ID=xxx
AWS_SECRET_ACCESS_KEY=xxx
AWS_DEFAULT_REGION=ap-northeast-1
```
この.env.laravel.localの内容は開発環境のものとは言え、できればセキュリティ的にGithubなどにはアップしたくないので、.gitignoreに追加しておいてください。

次に.envファイルを編集します。.env.laravel.localで設定している変数部分を空にしておきます。
```laravel-project/.env
APP_ENV=
APP_KEY=
APP_DEBUG=
APP_URL=

LOG_CHANNEL=

DB_HOST=
DB_PORT=
DB_DATABASE=
DB_USERNAME=
DB_PASSWORD=

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=
```

コンテナ内でプロジェクトフォルダに移動して開発サーバーを起動します。
```
cd laravel-project
npm run dev
```
以下のように表示されたらブラウザからhttp://localhost:8000 を開くとLaravelのWelcomeページが開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/930440/6b3c1b44-ba4f-cc8a-f513-7681ecf5c80e.png)

# ALBを作成
~~プライベートサブネットに設置するALBを作成します。項目はVPCやセキュリティグループなど各自適切に設定してください。
注意点としてはターゲットグループは空で作成することです。ターゲットグループは後程ECSのサービスを作成する時に設定します。~~
空のターゲットグループのALBを作成することができなくなりました。代わりにこの後のサービス作成時にALBを作成できるようになっているため、この時点ではALBを作成する必要はありません。

# ECR向けにビルド
いよいよECSで動作させるステップに進んでいきます。
まずはイメージをリポジトリ(ECR)にプッシュするところからです。

**まず、ビルドする前にnpm run devで開始した開発サーバーをCtl+cで落とします。**
npm run devをするとプロジェクトフォルダ内に"hot"というファイルが作成されますが、このファイルが残っている状態でこの後のビルドをしてしまうとECS上でも開発サーバー(http://localhost:5173) を読みに行く状態になってしまうので落としておくのを忘れないようにしてください（この状態でECSで起動したアプリをブラウザで開くと画面が崩れているのですぐに分かります）
もし開発サーバーが起動した状態で意図せずPCが落ちた場合などはhotファイルが残ったままになっている場合がありますので、ビルド前にhotファイルが残っていないかもチェックしておいたほうが無難です。
私はこのhotファイル関連で画面崩れの原因が分からずかなり時間をロスしたので注意してください。

以下のコマンドでECS用にビルドします。
```
DOCKER_TARGET=production docker compose build
```

# ECRのリポジトリを作成
ECRを作成する前にログインしておく必要があります。
以下のコマンドでECRにログインします。{AWSアカウントID}の部分を自分のIDに変更してください。
(ログイン状態は数時間は保持されるので連続して操作が必要な場合も毎回ログインする必要はありません)

```
aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin {AWSアカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com
```

以下のコマンドでECRのリポジトリを作成します。
1番目のコマンドでapp、2番目でnginxのリポジトリを作成しています。
```
aws ecr create-repository \
        --repository-name app \
        --image-scanning-configuration scanOnPush=true \
        --region ap-northeast-1 

aws ecr create-repository \
        --repository-name nginx \
        --image-scanning-configuration scanOnPush=true \
        --region ap-northeast-1
```

以下のコマンドでappとnginxイメージにタグを付けてからECRにプッシュします。
今後、コンテナの内容を変更してプッシュする度にタグを付けてプッシュしていき、ECR上に貯まっていくことになるのでタグは適当に分かりやすいものを付けてください。
```
docker tag fargate_laravel-app:latest {AWSアカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com/app:v1
docker tag nginx:latest {AWSアカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com/nginx:v1

docker push {AWSアカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com/app:v1
docker push {AWSアカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com/nginx:v1
```

プッシュが完了するとAWSコンソール > ECR > プライベートリポジトリ で作成したリポジトリが見られます。また、それぞれのリポジトリを選択するとプッシュしたイメージも確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/930440/6b7e24da-17ec-cfcf-2e60-43a2cc61840b.png)

# Parameter storeにECSタスクの環境変数を設定
AWSコンソール > Systems Manager > パラメータストア > パラメータの作成 を開きます。
ここでタスク上で使用する環境変数を設定します。設定する項目は.env.laravel.localで定義したものです。

以下、設定項目の説明です。説明のない項目はそのままでOKです。

**名前：** 環境変数の名前ではないので注意。/を使って階層で分けて分かりやすく分類しておきます。例えば/stg/db-password/みたいな感じです
**タイプ：** 基本は文字列でOK。パスワードなどの場合は「安全な文字列」を選択します
**値：** 保存する値を入れます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/930440/f4c40878-58f9-5084-95f6-232c21528874.png)
たくさんあると一つ一つ設定するのが大変なので、慣れてきたら以下のようなコマンドでAWS CLIで一気に作成すると簡単です。

```
$ aws ssm put-parameter \
    --name "/test/secret" \
    --description "secret sring" \
    --value "password123" \
    --type SecureString
```

# ECSクラスターを作成
AWSコンソール > Amazon Elastic Container Service > クラスターの作成 を開きます。
**クラスター名：** 適当な名前を付けてください
**インフラストラクチャ：** AWS Fargate (サーバーレス)を選択します。
その他は適当に。

# タスク定義を作成
AWSコンソール > Amazon Elastic Container Service > タスク定義 > 新しいタスク定義の作成 を開きます。
主な設定項目は以下の通りです。

[タスク定義の設定]
**タスク定義ファミリー：** 適当な名前を付けておきます(例：stg)

[インフラストラクチャの要件]
**起動タイプ：** AWS Fargate
**OS、アーキテクチャ、ネットワークモード：** 自分の環境に合わせる（基本はx86_64、M1/M2 MacはARM64を選択）
**CPU：** 適当に（ステージングなら.25 or .5、本番なら1以上とか）
**メモリ：** 1GB（必要に応じて調整してください）
**タスク実行ロール：** ecsTaskExexutionRoleを選択します。ecsTaskExexutionRoleがデフォルトで定義されていたのかちょっと記憶が曖昧なのですが、無い場合は以下を参考に作成してください。なお、既にある場合もParameter storeから値を参照する必要があるため、以下のようなロールを追加しておきます。

この辺りの設定は以下のサイトを参考にしました。
https://dev.classmethod.jp/articles/ecs-secrets/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/930440/d8ea7167-505f-8a16-f023-d551ce7a2d00.png)

```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ssm:GetParameters",
                "kms:Decrypt"
            ],
            "Resource": [
                "arn:aws:ssm:ap-northeast-1:{ID}:parameter/stg/*",
                "arn:aws:kms:ap-northeast-1:{ID}:alias/aws/ssm"
            ]
        }
    ]
}
```


[コンテナ – 1]
**名前：** app
**イメージURI：** {AWSアカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com/app:v1 (先ほどアップしたリポジトリのappイメージのURIを参照)
**コンテナポート：** 9000
**プロトコル：** TCP
**アプリケーションプロトコル：** なし
**環境変数：** Laravelに渡す環境変数を設定します。値は先ほど設定したParameter storeを参照します。キーは実際のLaravelの環境変数名、値のタイプはValueFromを選択し、値はParameter storeの各値のARNを使用します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/930440/dafd30f4-7d26-320e-d560-41fe766b224c.png)

**HealthCheckコマンド：** コンテナのヘルスチェックが行われるように設定しておきます。私はLaravelのアプリでログイン画面を作成したので以下のようなコマンドを設定しておきました。
```
CMD-SHELL,curl -f http://localhost/login || exit 1
```

[コンテナ – 2]
「コンテナを追加」ボタンを押してnginxコンテナを設定します。
**名前：** nginx
**イメージURI：** {AWSアカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com/nginx:v1 (先ほどアップしたリポジトリのnginxイメージのURIを参照)
**コンテナポート：** 80
**プロトコル：** TCP
**アプリケーションプロトコル：** HTTP
**環境変数：** 必要なのはdefault.conf内で参照されるFASTCGI_PASSです。こちらもParameter storeを参照します(Parameter storeで設定していない場合は設定しておいてください)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/930440/4bcc7ca7-3781-0209-385b-fe40599df38b.png)

**HealthCheckコマンド：** appと同じくコンテナのヘルスチェックが行われるように設定しておきます。
```
CMD-SHELL,curl -f http://localhost/ || exit 1
```

**スタートアップの依存関係の順序：** appコンテナの起動を先にしたいので、コンテナ名"app"、条件"Start"を選択します

あとは必要に応じて設定して作成ボタンを押してください。
すぐに作成されたタスク定義が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/930440/118e7e07-55f3-f6a7-6446-c16fc63ecfd2.png)

今後タスク定義を変更する場合はタスク定義を選択して「新しいリビジョンの作成」ボタンを押して変更したバージョンを新たに作成していく形になります。
私は最初、設定が上書きされるものと思っていたのですが上書きというものはなく、この操作をするたびに新しいリビジョンが作成されていき履歴が残るようになります。こうすることで作成したリビジョンで問題があっても前のリビジョンを選択することですぐに元に戻せるという利点があるのでしょう。

# サービスを作成
Amazon Elastic Container Service > クラスター > 作成したクラスター を開きサービスタブにある「作成」ボタンを押します。

設定項目は以下の通りです。特に記述がないものはデフォルトでOKです。

[デプロイ設定]
**アプリケーションタイプ：** サービス
**ファミリー：** 作成しておいたタスク定義ファミリーを選択
**リビジョン：** 基本は最新のものを選択
**サービス名：** 適当に
**必要なタスク：** 最低限起動しておくタスク数です。とりあえず1でOKです

[ネットワーキング]
VPC、サブネット、セキュリティグループなど適当なものを選択
**パブリックIP：** 今回はFargateをプライベートサブネットに設置してALB経由で接続するのでオフにします

[ロードバランシング]
**ロードバランサー：** ここでロードバランサーを新規作成します
![unnamed.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/930440/431be513-4f7e-9405-c83e-99f02e07898c.png)

**コンテナ：** nginx 80:80を選択
**Application Load Balancer：** 既存のロードバランサーから選択
**リスナー：** 既存のリスナーで80:HTTPを選択
**ターゲットグループ：** 既存のターゲットグループHTTP:80を選択
**ヘルスチェックパス：** 私の場合は/loginを入力しました

以上の設定が終わったら「作成」ボタンを押すとサービスの作成が始まります。完了まで時間がかかるのでしばらく待ち以下のように表示されたらサービスの起動が完了したことになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/930440/8180b386-1b93-c9b2-3380-9365a4257fba.png)

ロードバランサーのDNS名をブラウザで開いて作成したアプリのページが開けば成功です。

# コンテナの更新の手順
ソースを変更したりしてコンテナの内容が変わってFargateのコンテナをアップデートしたい場合の手順を簡単に記しておきます。

１．上記「ECR向けにビルド」を行います
２．「ECRのリポジトリを作成」内のタグ付けとプッシュを行います。Laravelアプリのソースを変更しただけであればappコンテナだけでOKです
３．Amazon ECR > プライベートレジストリ > リポジトリ から更新したいリポジトリを選択して今プッシュしたタグのイメージを選択し、URIをコピーします
４．Amazon Elastic Container Service > タスク定義 からタスク定義を選択し、変更したいリビジョンをクリックし、「新しいリビジョンの作成」ボタンを押します
５．「コンテナ」セクションで更新したいコンテナ(例えばappコンテナならコンテナ-1)セクション内にある「イメージ URI」フィールドに３番でコピーしたURIを張り付けて「作成」ボタンを押します
７．Amazon Elastic Container Service > クラスター からサービスを選択し「サービスを更新」ボタンを押します
８．「デプロイ設定」セクションのリビジョンの中から今作成したリビジョンを選択して「更新」ボタンを押します

全ての操作が完了すると現在のタスクを最新のものに入れ替える処理が始まります。処理中は新旧2つのタスクがコンソール上に表示されますが、暫くすると新しいものに収束します。

# GitHub Actionsから自動デプロイ
上記のように手動で更新するのは数回ならいいですが何回もとなると流石に手間と時間がかかるのでGitHubにプッシュした時点で自動的にFargateへのデプロイまで自動で行われるようにしておくと便利です。
GitHub Actionsを使った方法について以下に説明があるのでこれを参考にしてみてください。結構簡単にできます。
https://dev.classmethod.jp/articles/githubactions-ecs-fargate-cicd-beginner/