LaravelのECS運用のベストプラクティスを考えてみた
kotamat
誰
- kotamat
- ROXX CTO
- サーバー、インフラ寄り
- 🚴♂️
だけkotamats
最近はあんまり主催できてない…
手伝ってくれる人いたらぜひ!
転職にまつわるサービスを展開
エージェント
転職者
求人企業
話す内容
- ベースとなるコンテナの使い分け
- ロギング
- ECSならではのところ
- GitHub ActionsでCDしてみる
ベースとなるコンテナの使い分け
Laravelの主たる機能
- WebAPI
- キューワーカー
- スケジュールジョブ
- コンソール(artisanコマンド)
WebAPI
いわゆる通常の構成
- Nginxがリバースプロキシして
- PHP-FPMを見に行く
ここでコンテナ1プロセスポリシー
- 基本的に一つのコンテナには一つのプロセスだけを動かす
- そうしないと、自前でプロセス管理をしなければならない。
Nginx,PHP-FPMそれぞれのコンテナを生成する
- Laravelは/publicディレクトリに外だしするファイルを置いているため
- Nginx側では/publicディレクトリのみをCOPY
- FPM側にはルートディレクトリそのままを突っ込む
sockでつなぐかポートでつなぐか
- PHP-FPMとNginxはphpfpm.sockで繋ぐ方法とポートで繋ぐ方法がある。
- ECSなどはボリュームマウントがあるので、sockでつなげなくもないが、コンテナの死活を考えるとポートのほうが安全
キューワーカー
Supervisorで動かすのもいいが…
- コンテナ1プロセス管理の原則からは外れる
- プロセスが死んだらちゃんとコンテナとして死んだ扱いをしたほうが、他の監視ツールとの相性もいい
LaravelのベースとなるDockerfileのentrypointを柔軟にする
- CMDの指定がない場合→PHP-FPMを起動
- CMDにartisanがある場合→artisanを起動
- そうすることで、一つのイメージでどちらの起動方法も使える万能イメージが作れる
entrypoint.sh
#!/bin/bash
if [[ "$1" == *artisan* ]]; then
set -- php "$@"
else
set -- php-fpm "$@"
fi
exec "$@"
Dockerfile
# composer installとか
# apt-getとか
RUN chmod +x /app/docker/php-fpm/entrypoint.sh
ENTRYPOINT ["/app/docker/php-fpm/entrypoint.sh"]
docker-compose.ymlでは
version: '3.1'
services:
php-fpm:
build:
context: .
dockerfile: docker/php-fpm/Dockerfile
env_file:
- .env
worker:
build:
context: .
dockerfile: docker/php-fpm/Dockerfile
env_file:
- .env
command:
- "artisan"
- "queue:work"
- "sqs"
- "--sleep=3"
- "--tries=3"
タスク定義(ECS)では
resource "aws_ecs_task_definition" "main" {
container_definitions = jsonencode(
[
// worker
{
image = "baseimage:latest"
essential = false
name = "laravel-worker"
command = [
"artisan",
"queue:work",
"sqs",
"--sleep=3",
"--tries=3"
],
},
// api
{
image = "baseimage:latest"
essential = true
name = "laravel-api"
}
]
)
}
こんな感じで
- サイドカーチックにworkerを動かしてみてもいいし
- 別Service化して別途起動するコンテナ数を制御してもいい
スケジュールジョブ
ECSを使っているなら標準のタスクスケジューリングが良い
- CloudWatchによる定期実行
- 設定はcron形式なので、Laravel公式の通りに設定できる
- 前に紹介した万能Dockerfileを使えばimage使いまわしでタスク定義を作れる
多重実行防止のために
- この方法だと前のタスクが終了しているかどうかに関わらず次のタスクが実行されてしまう。
- 多重実行を防止したいのであれば、Laravelのキャッシュをfile以外の中央集権ストレージに移した上でwithoutOverlappingを設定する
コンソール(artisan)
コンテナ内部でどうやって実行するのか
- イメージの作り方は前述の通り
- とはいえコンテナに入れなければタスクは実行できない
- 実行したいartisanコマンドごとにタスク定義作るのも気が引ける
システムマネージャーのハイブリッドアクティベーションを使う
- 従来はオンプレサーバーにもSSMできるようにする機能
- SSM: sshポートが空いてなくてもsshっぽいことができる。AWSのリソース権限でのハンドリングができるので権限管理がらく
- Fargateなどは通常のSSMが使えないので、こちらで代替する
1. アクティベーション生成ロールを作成
resource "aws_iam_role" "activation_create" {
name = "${var.service_name}-for-create-activation"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Service = "ssm.amazonaws.com"
}
Action = "sts:AssumeRole"
}
]
})
}
resource "aws_iam_role_policy_attachment" "activation_create" {
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
role = aws_iam_role.activation_create.name
}
2. 実行ロールに1のロールへのパスをつける
resource "aws_iam_policy" "activate_code" {
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"iam:PassRole",
"ssm:CreateActivation"
],
Resource = [
aws_iam_role.activation_create.arn
]
}
]
})
}
resource "aws_iam_role_policy_attachment" "use_activation" {
policy_arn = aws_iam_policy.activate_code.arn
role = aws_iam_role.task_role_with_ssm.name
}
3. Laravelコンテナのentrypointで登録
if [ "$SSM_ACTIVATE" = "true" ]; then
# task実行の場合なので、厳密にエラーを補足する
set -e
ACTIVATE_PARAMETERS=$(aws ssm create-activation \
--default-instance-name "$APP_NAME-ssm" \
--description "$APP_NAME-ssm" \
--iam-role "$APP_NAME-for-create-activation" \
--registration-limit 5)
export ACTIVATE_CODE=$(echo $ACTIVATE_PARAMETERS | jq -r .ActivationCode)
export ACTIVATE_ID=$(echo $ACTIVATE_PARAMETERS | jq -r .ActivationId)
amazon-ssm-agent -register -code "${ACTIVATE_CODE}" \
-id "${ACTIVATE_ID}" -region "${AWS_DEFAULT_REGION}" -y
amazon-ssm-agent
fi
注意点
- 当然、アプリケーションの環境変数が全て見えてしまうので、アクセスできる人は最小限にする必要がある。
- 基本定期的に発生するものであればタスク定義化しておいて、中には入れるようにしないほうが良さそう。
ロギング
laravel.logは使わない
- コンテナに状態をもたせてもあまりうれしくない
- laravel.logをfluentdとかで外出ししてるならまだしも、コンテナ内部に入って確認するようなフローは前述の通りあまり望ましくない。
- ファイル容量オーバーのようなことは避けたい。
Docker log = 標準出力
- 標準では標準エラー、標準出力を外部のログシステムに転送する機能が備わっている
- ECSではCloudWatch Logsに転送してくれる
logging.php
<?php
return [
'channels' => [
'ecs' => [
'driver' => 'stack',
'channels' => ['stderr', 'stdout'],
],
'stdout' => [
'driver' => 'monolog',
'level' => 'info',
'handler' => StreamHandler::class,
'formatter' => env('LOG_STDOUT_FORMATTER'),
'formatter_with' => [
'dateFormat' => '%Y-%m-%d %H:%M:%S'
],
'with' => [
'stream' => 'php://stdout',
],
],
'stderr' => [
'driver' => 'monolog',
'level' => 'critical',
'handler' => StreamHandler::class,
'formatter' => env('LOG_STDERR_FORMATTER'),
'formatter_with' => [
'dateFormat' => '%Y-%m-%d %H:%M:%S'
],
'with' => [
'stream' => 'php://stderr',
],
],
ECS側で複数行に渡る一つのログを一つにまとめる
[
{
// ... 諸々のコンテナの設定
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "${awslog_group}",
"awslogs-region": "${region}",
"awslogs-datetime-format": "%Y-%m-%d %H:%M:%S",
"awslogs-stream-prefix": "ecs"
}
}
},
]
注意点
- PHP-FPMから直接標準出力にストリームすると、PHP-FPMの設定によっては出力内容にプロセスの表示も出てしまう。
- PHP7.3以降であれば `decorate_workers_output` をnoにしてあげれば通常のlaravel.logのような出力にしてくれる
デプロイ(GitHub Actions)
ベースのworkflow.yml
jobs:
job:
runs-on: ubuntu-latest
steps:
# ...
- name: Change Task Definition
id: render-td
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: ${{ steps.fetch-td.outputs.task-definition }}
container-name: my-container-name
image: ${{ steps.built-image.outputs.image }}
- name: Deploy to Amazon ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.render-td.outputs.task-definition }}
service: ecs-service
cluster: ecs-cluster
worker入れたい場合はチェインさせる
jobs:
job:
runs-on: ubuntu-latest
steps:
# ...
- name: Change Task Definition for api
id: render-td-api
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: ${{ steps.fetch-td.outputs.task-definition }}
container-name: my-container-name
image: ${{ steps.built-image.outputs.image }}
- name: Change Task Definition for worker
id: render-td-worker
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: ${{ steps.render-td-api.outputs.task-definition }}
container-name: my-container-name
image: ${{ steps.built-image.outputs.image }}
- name: Deploy to Amazon ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.render-td-worker.outputs.task-definition }}
service: ecs-service
cluster: ecs-cluster
worker入れたい場合はチェインさせる
jobs:
job:
runs-on: ubuntu-latest
steps:
# ...
- name: Change Task Definition for api
id: render-td-api
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: ${{ steps.fetch-td.outputs.task-definition }}
container-name: my-container-name
image: ${{ steps.built-image.outputs.image }}
- name: Change Task Definition for worker
id: render-td-worker
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: ${{ steps.render-td-api.outputs.task-definition }}
container-name: my-container-name
image: ${{ steps.built-image.outputs.image }}
- name: Deploy to Amazon ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.render-td-worker.outputs.task-definition }}
service: ecs-service
cluster: ecs-cluster
GA公式でも下記でデプロイステータスは見れるが…
jobs:
job:
runs-on: ubuntu-latest
steps:
# ...
- name: Deploy to Amazon ECS service
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.render-td-worker.outputs.task-definition }}
service: ecs-service
cluster: ecs-cluster
wait-for-service-stability: true #←これ!
GAは従量課金なのでデプロイに時間かかると辛い
// 内部実装
const waitForService = core.getInput('wait-for-service-stability', { required: false });
// Wait for service stability
if (waitForService && waitForService.toLowerCase() === 'true') {
core.debug(`Waiting for the service to become stable.
Will wait for ${waitForMinutes} minutes`);
const maxAttempts = (waitForMinutes * 60) /
WAIT_DEFAULT_DELAY_SEC;
await ecs.waitFor('servicesStable', {
services: [service],
cluster: clusterName,
$waiter: {
delay: WAIT_DEFAULT_DELAY_SEC,
maxAttempts: maxAttempts
}
}).promise();
ECSのデプロイはCloudWatchEventで補足できるのでSNSに流して通知可能
resource "aws_cloudwatch_event_rule" "main" {
name = local.base_name
event_pattern = jsonencode({
"source" : [
"aws.ecs"
],
"detail-type" : [
"ECS Task State Change"
],
"detail" : {
"clusterArn" : [
var.cluster_arn
]
}
})
}
まとめ
- 本番運用におけるプラクティスを紹介
- 表面的な紹介が多いと思うので、わからないところは聞いてもらえると
- 自動でのマイグレーション周りは課題なので、詳しい方教えて下さい。
文章でみたい方はこちら
https://kotamat.com/post/laravel-on-ecs/
環境変数の管理、マイグレーションとかも言及してます
LaravelのECS運用のベストプラクティスを考えてみた
By Kota Matsumoto
LaravelのECS運用のベストプラクティスを考えてみた
- 5,519