はじめに
下記の続きになります。タイトル通り、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
にしました。
作成後、envファイルをアップロードします。
注意点として、ファイルの拡張子は「.env」にする必要があります。
envファイルの名前は、laravel.env
という名前にします。
ファイルの内容について、# で始まる行はコメントとして扱われ、無視されます。
# ここの文言は無視されます
ENV=production
API_KEY=xxxxxxxxxx
# ここの文言は無視されます
laravel.env
ファイルのAmazon リソースネーム (ARN)
をコピーしておきます。
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
が作成されていると思います。
ecsTaskExecutionRole
にget-S3-env
ポリシーをアタッチしましょう。
タスク定義の設定
あとは、タスク定義のappコンテナ内の環境ファイル
に先程コピーしたS3のARNを貼り付けると完了です。
環境変数内に記載する必要はなくなりました。
タスク定義更新後、サービス内で、新しいタスク定義を選択し、強制デプロイするだけです。
エラー[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
内を修正します。
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認証をパスします。
+ if ($http_user_agent = "ELB-HealthChecker/*") {
+ set $auth off;
+ }
パス/test/
の場合、Basic認証をパスします。
+ location /test/ {
+ try_files $uri $uri/ /index.php?$query_string;
+ satisfy any;
+ allow all;
+ }
上記以外の場合、Basic認証をありにします。
/etc/nginx/.htpasswd
ファイルは、Basic認証で必要なユーザー名とパスワードを記載されたものになります。後ほど説明します。
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
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認証時に必要なユーザー名とパスワードを下記のように記載します。
ユーザー名:ハッシュ化されたパスワード
こちらのサイトでは、Basic認証用の.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のページが表示されます。
.gitignore追加
htpasswdディレクトリ内に.gitignoreを追加し、.htpasswdがgithubに上がらないようにしましょう。
.htpasswd
Login機能がある場合、ELBのヘルスチェックパスとrouteの修正
ELBのヘルスチェックパスは、/index.php
となっております。
login機能がある場合、/index.php
にヘルスチェックをしても強制的に/login
にリダイレクトされてしまい、ヘルスチェックが失敗します。
その場合、src/route/web.php
ファイルを修正し、/healthcheck
にアクセスしたら'health ok'を返す(表示する)設定をすることで、ヘルスを成功させることができます。
<?php
use Illuminate\Support\Facades\Route;
+ Route::get('/healthcheck', function()
+ {
+ return 'health ok';
+ });
また、ロードバランサーのfargateのターゲットグループのヘルスチェックパスを/index.php
から/healthcheck
修正します。
再度、ビルドし、再デプロイしましょう。
http://xxxx.com/healthcheck
にアクセスするとhealth ok
と返ってきます。
Laravel.logをCloudWatchLogsに出力
fargateのログは、CloudwatchLogsに出力する設定をタスク定義でできますが、Laravelの場合、laravel.logはデフォルトでは、CloudwatchLogs出力しないため、設定する必要があります。
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
となります。
stack
はsingle
チャンネルを使用されるようですね。
下記を見ると分かりますが、出力ログ先は、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に出力できます。
+ LOG_CHANNEL=stderr
envファイルは、s3にあるため、envファイル修正しアップロードしましょう。
awslogs-datetime-formatオプション追加
また、タスク定義のappコンテナのログ設定で以下の一行を加えます。
awslogs-datetime-format:%Y-%m-%d %H:%M:%S"
このオプションは、指定した形式の日時がログのある行に含まれていれば,続く行も同じ日時とみなされます。
この一行を加えない場合、Stacktrace のようにログレコードが複数行にわたる際、ログレコードが細分化されてしまい、可読性が下がります。
これで、デプロイすると、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に保存するようにします。
.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ファイル修正
以下の項目を修正しました。
[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ファイルを修正
以下の項目を修正しました。
'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を修正
以下の項目を修正しました。
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にペーストします。
+ 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にペーストします。
+ FROM nginx:1.20-alpine@sha256:88d982bc06d4c192f6eb3a96f78d551b79a18840910907323fd719237a21b15b
- FROM nginx:1.20-alpine
タスク定義で設定
タスク定義のボリュームのJSONによる設定
で、以下を加えることで、ARM64を指定したデプロイができます。
"runtimePlatform": {
- "cpuArchitecture": null,
+ "cpuArchitecture": "ARM64",
"operatingSystemFamily": "LINUX"
},
デプロイすると、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 install
とnpm run prod
を追加すると、CSSファイルとJavaScriptファイルを簡単にコンパイルして圧縮できます。
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にあります。
下記通りに設定するとよいです。
読み取り専用コンテナ化
記事を作成しましたので、こちらを参考にしてください。
参考