LoginSignup
25
26

More than 1 year has passed since last update.

Laravel 9 を ECS on Fargateで運用にするにあたり、Tipsまとめ

Last updated at Posted at 2022-04-03

はじめに

下記の続きになります。タイトル通り、Fargateを運用にするにあたり、Tipsまとめます。

事前構築

下記の記事で、fargateを構築済み

ソースに関して、mainブランチは、この記事の対応前のソースになります。
この記事の対応後のソースは、feature/tipsブランチにあります。

この記事のtips一覧

  • envファイルをs3に保管
  • Basic認証とヘルスチェックを通す
  • Laravel.logをCloudWatchLogsに出力
  • EFSを利用
  • ElastiCache Redis Cluster を利用
  • AWS Graviton2を利用し、コスト削減
  • npm run prodコマンドでコンパイルして圧縮処理
  • 読み取り専用コンテナ化

envファイルをs3に保管

envファイルは、API_KEYなどがあるため、GitHubに載せたくないです。
envファイルをS3に保管し、Fargateをデプロイのタイミングで、S3からenvファイルを取得する方法を説明します。

S3バケットを作成

下記の設定で、S3バケットを作成します。
バケット名は、laravel-environment-variableにしました。
スクリーンショット 2022-04-03 11.04.41.png

作成後、envファイルをアップロードします。
注意点として、ファイルの拡張子は「.env」にする必要があります。
envファイルの名前は、laravel.envという名前にします。

ファイルの内容について、# で始まる行はコメントとして扱われ、無視されます。

laravel.env
# ここの文言は無視されます
ENV=production
API_KEY=xxxxxxxxxx
# ここの文言は無視されます

laravel.envファイルのAmazon リソースネーム (ARN)をコピーしておきます。

スクリーンショット 2022-04-03 11.09.23.png

IAMポリシーを作成

fargateがs3のenvファイルを取得できるように権限を付与します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::laravel-environment-variable/*"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": "s3:GetBucketLocation",
            "Resource": "arn:aws:s3:::laravel-environment-variable"
        }
    ]
}

このポリシーをタスクロールではなくタスク実行ロールにアタッチします。
タスク定義作成時にデフォルトで、 ecsTaskExecutionRoleが作成されていると思います。
スクリーンショット 2022-04-03 11.22.40.png

ecsTaskExecutionRoleget-S3-envポリシーをアタッチしましょう。
スクリーンショット 2022-04-03 11.20.43.png

タスク定義の設定

あとは、タスク定義のappコンテナ内の環境ファイルに先程コピーしたS3のARNを貼り付けると完了です。
環境変数内に記載する必要はなくなりました。
スクリーンショット 2022-04-03 11.23.21.png

タスク定義更新後、サービス内で、新しいタスク定義を選択し、強制デプロイするだけです。

エラー[ResourceInitializationError]

envファイルのダウンロードに失敗したエラーが出た場合、セキュリティーグループを確認してください。

  • elbのsgは、httpで全開放(0.0.0.0/0)
  • fargateのsgは、elbからのhttpで許可
  • ACLとセキュリティグループが、サブネットからのポート 443 へのアウトバウンドアクセスを許可
ResourceInitializationError: failed to download env files: file download command: non empty error stream: service call has been retried 5 time(s): RequestCanceled: request context canceled caused by: context deadline exceeded
ResourceInitializationError: unable to pull secrets or registry auth: execution resource retrieval failed: unable to retrieve ecr registry auth: service call has been retried 3 time(s): RequestError: send request failed caused by: Post https://api.ecr....

Basic認証とヘルスチェックを通す

開発中や管理画面は、Basic認証を設定することがあると思います。
Basic認証の設定と、ELBからのヘルスチェックを200で返す方法について説明します。

Basic認証の設定

/docker/nginx/default.conf内を修正します。

/docker/nginx/default.conf
server {
  listen 80;
  root /var/www/html/public;
  error_log /var/log/nginx/error.log;
  index index.php;

+  if ($http_user_agent = "ELB-HealthChecker/*") {
+    set $auth off;
+  }
+  location /test/ {
+    try_files $uri $uri/ /index.php?$query_string;
+    satisfy any;
+    allow   all;
+  }

  location = /favicon.ico { access_log off; log_not_found off; }
  location = /robots.txt  { access_log off; log_not_found off; }

  location / {
    try_files $uri $uri/ /index.php?$query_string;
+    auth_basic $auth;
+    auth_basic_user_file /etc/nginx/.htpasswd;
  }
  error_page 404 /index.php;

  location ~ \.php$ {
    fastcgi_pass 127.0.0.1:9000;
    fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
    include fastcgi_params;
  }

  location ~ /\.(?!well-known).* {
    deny all;
  }
}

ヘルスチェックは、Basic認証をパスします。

/docker/nginx/default.conf
+  if ($http_user_agent = "ELB-HealthChecker/*") {
+    set $auth off;
+  }

パス/test/の場合、Basic認証をパスします。

/docker/nginx/default.conf
+  location /test/ {
+    try_files $uri $uri/ /index.php?$query_string;
+    satisfy any;
+    allow   all;
+  }

上記以外の場合、Basic認証をありにします。
/etc/nginx/.htpasswdファイルは、Basic認証で必要なユーザー名とパスワードを記載されたものになります。後ほど説明します。

/docker/nginx/default.conf
  location / {
    try_files $uri $uri/ /index.php?$query_string;
+    auth_basic $auth;
+    auth_basic_user_file /etc/nginx/.htpasswd;
  }

htpasswdの設定

Basic認証で必要なユーザーとパスワードを作成していきます。
./docker/nginx/配下にhtpasswdディレクトリを作成し、.htpasswdを加えます。

.
├── docker
│   ├── nginx
│   │   ├── Dockerfile
│   │   ├── default.conf
│   │   ├── nginx.conf
│   │   └── htpasswd
│   │        └── .htpasswd
│   └── php
│       ├── Dockerfile
│       └── php.ini
└── src
./docker/nginx/Dockerfile
FROM nginx:1.20-alpine
COPY ./docker/nginx/default.conf /etc/nginx/conf.d/default.conf
COPY ./docker/nginx/nginx.conf /etc/nginx/nginx.conf
+ COPY ./docker/nginx/htpasswd /etc/nginx
COPY ./src /var/www/html
ENV TZ Asia/Tokyo
EXPOSE 80

.htpasswdには、Basic認証時に必要なユーザー名とパスワードを下記のように記載します。

.htpasswd
ユーザー名:ハッシュ化されたパスワード

こちらのサイトでは、Basic認証用の.htpasswdファイルを自動生成できます

スクリーンショット 2022-04-03 12.20.06.png
ユーザー名をtest、パスワードをpassとしました。

./docker/nginx/htpasswd/.htpasswd
test:$apr1$YEgvP5H/$YSKmv5c5Xp6mSCgQGPfoN.

あとは、ビルドし、ecrにpushし、タスクを強制デプロイします。詳細は、前回の記事を参考にしてください

laravel-ecs $ docker build -t nginx:prod -f ./docker/nginx/Dockerfile .

$ docker tag nginx:prod [アカウントエイリアス12桁].dkr.ecr.ap-northeast-1.amazonaws.com/nginx:prod

$ docker push [アカウントエイリアス12桁].dkr.ecr.ap-northeast-1.amazonaws.com/nginx:prod

http://xxxx.comにアクセスすると、Basic認証が表示されました。ここに、testとpassを入力すると、laravelのページが表示されます。

また、ヘルスチェックも成功しているようですね。
スクリーンショット 2022-04-03 12.30.47.png

スクリーンショット 2022-04-03 12.31.18.png

.gitignore追加

htpasswdディレクトリ内に.gitignoreを追加し、.htpasswdがgithubに上がらないようにしましょう。

.docker/nginx/htpasswd/.gitignore
.htpasswd

Login機能がある場合、ELBのヘルスチェックパスとrouteの修正

ELBのヘルスチェックパスは、/index.phpとなっております。
login機能がある場合、/index.phpにヘルスチェックをしても強制的に/loginにリダイレクトされてしまい、ヘルスチェックが失敗します。

その場合、src/route/web.phpファイルを修正し、/healthcheckにアクセスしたら'health ok'を返す(表示する)設定をすることで、ヘルスを成功させることができます。

.src/route/web.php
<?php

use Illuminate\Support\Facades\Route;

+ Route::get('/healthcheck', function()
+ {
+     return 'health ok';
+ });

また、ロードバランサーのfargateのターゲットグループのヘルスチェックパスを/index.phpから/healthcheck修正します。

再度、ビルドし、再デプロイしましょう。
http://xxxx.com/healthcheckにアクセスするとhealth okと返ってきます。
スクリーンショット 2022-04-03 13.15.45.png

Laravel.logをCloudWatchLogsに出力

fargateのログは、CloudwatchLogsに出力する設定をタスク定義でできますが、Laravelの場合、laravel.logはデフォルトでは、CloudwatchLogs出力しないため、設定する必要があります。

src/config/logging.phpファイルを一度確認しましょう。

src/config/logging.php
<?php 
一部省略

'channels' => [
  'stack' => [
    'driver' => 'stack',
    'channels' => ['single'],
    'ignore_exceptions' => false,
  ],

  'single' => [
    'driver' => 'single',
    'path' => storage_path('logs/laravel.log'),
    'level' => env('LOG_LEVEL', 'debug'),
  ],

  'daily' => [
    'driver' => 'daily',
    'path' => storage_path('logs/laravel.log'),
    'level' => env('LOG_LEVEL', 'debug'),
    'days' => 14,
  ],

  'slack' => [
    'driver' => 'slack',
    'url' => env('LOG_SLACK_WEBHOOK_URL'),
    'username' => 'Laravel Log',
    'emoji' => ':boom:',
    'level' => env('LOG_LEVEL', 'critical'),
  ],

  'papertrail' => [
    'driver' => 'monolog',
    'level' => env('LOG_LEVEL', 'debug'),
    'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
    'handler_with' => [
      'host' => env('PAPERTRAIL_URL'),
      'port' => env('PAPERTRAIL_PORT'),
      'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
    ],
  ],

  'stderr' => [
    'driver' => 'monolog',
    'level' => env('LOG_LEVEL', 'debug'),
    'handler' => StreamHandler::class,
    'formatter' => env('LOG_STDERR_FORMATTER'),
    'with' => [
      'stream' => 'php://stderr',
    ],
  ],

  'syslog' => [
    'driver' => 'syslog',
    'level' => env('LOG_LEVEL', 'debug'),
  ],

  'errorlog' => [
    'driver' => 'errorlog',
    'level' => env('LOG_LEVEL', 'debug'),
  ],

  'null' => [
    'driver' => 'monolog',
    'handler' => NullHandler::class,
  ],

  'emergency' => [
    'path' => storage_path('logs/laravel.log'),
  ],
],

'default' => env('LOG_CHANNEL', 'stack'),とあるように、デフォルトでは、LOG_CHANNELが設定されていない場合、stackとなります。
stacksingleチャンネルを使用されるようですね。
下記を見ると分かりますが、出力ログ先は、logs/laravel.logのようです。

  'single' => [
    'driver' => 'single',
    'path' => storage_path('logs/laravel.log'),
    'level' => env('LOG_LEVEL', 'debug'),
  ],

Laravelがエラーログを CloudWatchLogs 出力する設定は、stderrになります。
そのため、envファイルにLOG_CHANNEL=stderrの一行を加えるだけで、LaravelのログがCloudWatchLogsに出力できます。

laravel.env
+ LOG_CHANNEL=stderr

envファイルは、s3にあるため、envファイル修正しアップロードしましょう。

awslogs-datetime-formatオプション追加

また、タスク定義のappコンテナのログ設定で以下の一行を加えます。

awslogs-datetime-format:%Y-%m-%d %H:%M:%S"

このオプションは、指定した形式の日時がログのある行に含まれていれば,続く行も同じ日時とみなされます。
この一行を加えない場合、Stacktrace のようにログレコードが複数行にわたる際、ログレコードが細分化されてしまい、可読性が下がります。
スクリーンショット 2022-04-03 14.06.58.png

これで、デプロイすると、laravelのログがCloudwatchlogsに出力されるようになりました。

ちなみに、laravelにエラーログが出力された場合、slackに通知させる方法はこちらになります。

EFSを利用

Fargateでは、EFSをマウントすることができます。

以下の記事がわかりやすかったです。

ポイントは、EFSのマウントターゲットに設定するセキュリティグループは、fargateのsgに対して、port2049を許可する点くらいです。
fargate側のsgは、特に変更することはないですね。

ちなみに、マウントポイントは、fargate内のディレクトリに存在しない場合、作成してくれます。
例えば、マウントポイントが/var/www/html/efsの場合、efsディレクトリがないのであれば、作成してくれます。

ちなみに、以下の記事で、EFSに直接ファイルが転送できます。

タスクが起動できない場合

デプロイ後、以下のエラーログを出た場合、セキュリティーグループが適正でない可能性があります。
先程も説明しましたが、EFSのマウントターゲットに設定するセキュリティグループは、fargateのsgに対して、port2049を許可が必要です。

ResourceInitializationError:
failed to invoke EFS utils commands to set up EFS volumes: stderr: 
Failed to resolve "fs-0eaebf306ce71301a.efs.ap-northeast-1.amazonaws.com" 
- check that your file system ID is correct. See https://docs.aws.amazon.com/conso...

ElastiCache Redis クラスター使用

タスクが複数になった場合、sessionデータをRedisに保存しないと、ログイン機能などが使用できません。

そのため、クラスターモード有効のElastiCache Redis を作成し、cacheとsessionをRedisに保存するようにします。

作成方法は、省略します。
スクリーンショット 2022-10-21 18.16.37.png

スクリーンショット 2022-10-21 18.13.59.png

.envファイル修正

以下の項目を修正しました。

.env
CACHE_DRIVER=redis
SESSION_DRIVER=redis
REDIS_HOST=test.wwww.clustercfg.apne1.cache.amazonaws.com
REDIS_PASSWORD=null
REDIS_PORT=6379

REDIS_HOSTは、クラスター詳細の設定エンドポイントになります。末尾のポートは不要です。

php.iniファイル修正

以下の項目を修正しました。

php.ini
[redis]
extension = redis.so
session.save_handler = rediscluster
session.save_path = "seed[]=".${REDIS_HOST}.":6379&failover=distribute&persistent=1"

${REDIS_HOST}は、envの環境変数を参照しています。

下記を参考にしました。

config/database.phpファイルを修正

以下の項目を修正しました。

src/config/database.php
    'redis' => [
        'client' => env('REDIS_CLIENT', 'phpredis'),

        'options' => [
            'cluster' => env('REDIS_CLUSTER', 'redis'),
        ],

        'clusters' => [

            'default' => [
                [
                  'host' => env('REDIS_HOST', 'localhost'),
                  'password' => env('REDIS_PASSWORD', null),
                  'port' => env('REDIS_PORT', 6379),
                  'database' => 0,
                ],
            ],

            'cache' => [
                [
                  'host' => env('REDIS_HOST', '127.0.0.1'),
                  'password' => env('REDIS_PASSWORD', null),
                  'port' => env('REDIS_PORT', '6379'),
                  'database' => env('REDIS_CACHE_DB', '1'),
                ]
            ],
        ],
    ],

これでcacheとsessionをRedisに保存できます。
下記を参考にしました。

Dockerfileを修正

以下の項目を修正しました。

docker/php/Dockerfile
RUN pecl install redis \
         docker-php-ext-enable redis

これでビルドすると、cacheとsessionをredisに保存することができます。

AWS Graviton2を利用し、コスト削減

AWS Fargate の Graviton2 が使用できます。
従来のよりも 20% 使用料が安く、最大 40% 高性能なリソースを利用できるようです。

Docker ImageでARM64を探し、Dockerfileに記載し、ビルドします。
タスク定義でARM64を設定し、デプロイする、といった流れになります。

Docker ImageでARM64を探す

php:8.1-fpm-bullseyeで検索し、linux/arm64を探します。
linux/arm64/v8があったため、選択しsha256:1a.......をコピーし、Dockerfileにペーストします。
スクリーンショット 2022-04-03 16.58.24.png

./docker/php/Dockerfile
+ FROM php:8.1-fpm-bullseye@sha256:804ea577022c459dbbc69ac42e0adf405a24c12d032a045cb558fe98ccea03de
- FROM php:8.1-fpm-bullseye

nginxも同様に、nginx:1.20-alpineで検索し、linux/arm64/v8があったため、sha256:88.......をコピーし、Dockerfileにペーストします。
スクリーンショット 2022-04-03 17.01.32.png

./docker/nginx/Dockerfile
+ FROM nginx:1.20-alpine@sha256:88d982bc06d4c192f6eb3a96f78d551b79a18840910907323fd719237a21b15b
- FROM nginx:1.20-alpine

タスク定義で設定

タスク定義のボリュームのJSONによる設定で、以下を加えることで、ARM64を指定したデプロイができます。

json設定
    "runtimePlatform": {
-        "cpuArchitecture": null,
+        "cpuArchitecture": "ARM64",
        "operatingSystemFamily": "LINUX"
    },

スクリーンショット 2022-04-03 17.06.42.png

デプロイすると、Graviton2に対応した構築になり、コストが20%削減されます。

ARM64は、ビルド時間が長い場合があります。

私の場合、phpのビルド時間が長かったです。
特に、phpのDockerfile内のRUN docker-php-ext-install intl pdo_mysql bcmath zipが長かったです。

イメージ ベースのイメージ ビルド時間
php:8.1-fpm-bullseye x86/amd64 2分
php:8.1-fpm-bullseye arm64 20分
nginx:1.20-alpine x86/amd64 1.5秒
nginx:1.20-alpine arm64 4.0秒

下記の記事でも同様に、ビルド時間が長いことが確認されており、結果としてx86/amd64を利用することにしました。

npm run prodコマンドでコンパイルして圧縮処理

srcディレクトリ配下にpackage.json,package-lock.json,webpack.mix.js等を設置します。
Dockerfileにnpmをインストールし、npm installnpm run prodを追加すると、CSSファイルとJavaScriptファイルを簡単にコンパイルして圧縮できます。

docker/php/Dockerfile
FROM php:8.1-fpm-bullseye

# 省略

RUN apt-get update \
  && apt-get -y install --no-install-recommends \
    locales \
+    npm \
    git \
# 省略

RUN composer install -q -n --no-ansi --no-dev --no-scripts --no-progress --prefer-dist \
  && chmod -R 777 storage bootstrap/cache \
  && php artisan optimize:clear \
  && php artisan optimize \
  && php artisan cache:clear \
  && php artisan config:clear \
  && php artisan route:clear \
  && php artisan view:clear \
+  && npm install \
+  && npm run prod

コンテナを使うにあたり便利ツール

Laravelに限らず、コンテナを使うにあたり便利ツールを紹介します。

hadolintを利用してベストプラクティスに基づいたDockerfileを作成する

以下の記事を参考に、hadolintを terminal もしくは VSCode にインストールしましょう。

Dockleを利用してコンテナのセキュリティ診断する

Dockleは、コンテナイメージから危険な項目をチェックするとともに、ベストプラクティスに沿ったコンテナイメージが作れているか確認できます。
下記の記事を参考にインストールしましょう。

ECRの脆弱性スキャン機能を利用してコンテナのセキュリティ診断する

ECRにpushすると、自動で脆弱性スキャンをする機能がECRにあります。
下記通りに設定するとよいです。

読み取り専用コンテナ化

記事を作成しましたので、こちらを参考にしてください。

参考

25
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
25
26