Apple-Silicon(M1チップ)のMBPにk8s環境を作る方法

こちらは個人ブログ記事の転載です。

kotamat.com

Intel チップのときは Docker for Mac で Enable Kubernetes を on にするだけで k8s 環境が手に入っていました。 ただ、M1 チップ用の環境である Docker Desktop for Apple M1 で環境を構築すると執筆時点の段階では k8s をそのままでインストールすることができません。

https://docs.docker.com/docker-for-mac/apple-m1/

Kubernetes does not initialize because of a missing DNS name.

設定画面を見るとチェックボックスは押せるみたいだけど、 Kubernetes is starting... のままスタートしない…

f:id:kotamat:20210210070557p:plain

対応方法

kind

kindは Docker コンテナのノードを利用してローカルに k8s 環境を作るツールです。 Docker Desktop for Apple M1 を入れて Docker 環境を作っておけば、Docker コンテナ自体は立ち上がるようになっているので、kind を使うのが一番簡単かと思います。

GO111MODULE="on" go get sigs.k8s.io/kind@v0.10.0
kind create cluster --image rossgeorgiev/kind-node-arm64:v1.20.0

kind create cluster をする際に、arm64 の image を指定しないとエラーになる点だけ注意します。

Minikube

Minikubeも同様にローカルに k8s 環境を作るツールです。

すでに darwin-arm64 用の minikube がリリースされているので、そちらを使います。

curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-darwin-arm64
sudo install minikube-darwin-arm64 /usr/local/bin/minikube

brew でのインストールもできるようですが、こちらは未確認です。

まとめ

意外とあっさり k8s の環境を構築できました。 今後開発していく中で環境依存の不具合に遭遇するかもしれないですが、その際はまた何か記事にしようかなと思います。

デイリースクラムで一日の計画を立てよう

私が所属しているROXXのagenbank事業部の開発チームでは、スクラムを取り入れています。 前回、スクラムのふりかえりについての記事をかきました 今回は開発チームで行っているデイリースクラムについて書いていきます。

今やってること

今、私達は朝昼夕と1日に3回、決まった時間にデイリースクラムを行っています。 一見、「そんなにやって意味あるの?」「時間の無駄じゃない?」などの声があがりそうですが 私は、ちゃんと目的意識を持って取り組めばとても意味があり効果的なものだと考えています。 ただし、スクラムメンバーがデイリースクラムを行う目的、価値を理解して行わないと効果は発揮しません。

朝会

一日のゴールを達成できるよう、効率よく作業を進められるようその日一日の計画を立てる場です。 前日からの状況、次のタスクの状況、さらには人員リソースの状況を鑑みて、効率の良い進め方を模索します。 また、不確実性が高いタスクをウォッチしてできるだけ計画に影響をきたさないようにします。

やってること

  1. 追加タスクの確認
  2. 一日の進め方を決める
  3. ウォッチする不確実タスクの確認

1. 追加タスクの確認

プランニングでは計画していなかった割り込みタスクや、スプリントのゴールに達する上で足りていなかった追加タスクを確認します。 このようなタスクはプランニングの時点では発生していなく、メンバー全員できちんと話せていない可能性が高いのでここで確認します。

2. 一日の進め方を決める

予めプランニングで一日の進め方は決めていますが、日々状況は変わります。 現在の状況を踏まえて、その時点での最適な一日の進め方を決定します。

3. ウォッチする不確実タスクの確認

ここでいう不確実性が高いとは - 開発チーム外の人が絡んでいる - 調査してからでないとタスクが進められない - 仕様がきちんと定まってない

などのことです。

不確実性が高いタスクが存在していると、進捗が遅れる可能性も高いです。 不確実なタスクをウォッチし、一日の進め方に対してどれくらいの影響をきたしているかを見極め、アクションを取ります。

昼会

午前中のタスクの進捗状況を全員で確認し、朝会で確認した一日の計画を達成するために午後の作業の進め方を調整する場です。 タスクの進捗状況によっては、タスクのアサインの変更やヘルプ依頼などあらゆる手段を用いて一日の計画を達成できるよう模索します。

当初、昼会は行っておらず昼の時点で進捗確認はしていませんでした。 「チームでプランニングしたインクリメントをスプリント内で作りきれない」というProblemが発生し、その改善策として行うことになりました。 1つのタスクに取り掛かるとそれを終わらせることに集中してしまい、全体の進捗がどうなっているかまで意識が回らないです。 全員で進捗を確認し、その進捗に対してアクションを考える場をつくることでタスクの進捗、午後の動きについてチーム全員で共通認識を得ることができます。

やってること

  1. 遅延タスクについての遅延要素の確認
  2. 午後の進め方を決める

1. 遅延タスクについての遅延要素の確認

現在各タスクがどのような状況なのかを、全員で確認し、進捗状況の認識を合わせます。 一日のゴール(一日に完了すべきタスク郡)に対して未完了のタスクや先取りしているタスクを確認し、現在の進捗を客観的に判別します。

ここで一番重要なのは、遅延しているタスクについてです。 遅延しているタスクは何らかの遅延要素が発生しているはずです。一日のゴール設定にも影響が出るため、この時点でしっかり検知をし、一日の予定に影響が出ないよう対策を立てます。

「現在自分が着手しているタスクで想定外に時間がかかっている箇所はないか?」 「仕様が不透明なまま進めてしまっている箇所はないか?」 などを考えこの場で共有する必要があります。

遅延理由を言いやすい雰囲気をつくる

弊社の開発チームではそんな人はいませんが、遅延していること自体を責める発言は良くないです。 重要なのは遅延要素を特定し、それに対してアクションを取ることです。 しかし、自ら遅延している理由をスラスラ話すことができないメンバーもいると思います(僕自身、どっちかというとそういうタイプです) そういう場合は、「不安なことはありませんか?」「困っていることはありませんか?」などの問いかけで出てくることもあります。

自分ごととして考える

タスクの遅延要素は自分で気がつけないこともあります。 他の人がやっているタスクでも「何故遅延しているのか?」、「こうした方がいいんじゃないか?」などを考えることで 遅延要素に気がつく為の目が増え、一日のゴールに向かいやすくなります。

2. 午後の進め方を決める

遅延タスクが発生している場合は、そのタスクの対応を踏まえた計画を作り、午後の進め方の共通認識を得ます。 遅延タスクの対応というのは例えば、 - 仕様が不透明なものがあったら、全員で話す場を設定する - 実装上詰まっているところがあれば、ヘルプ依頼をする

などです。

共通認識を得て、全員が同じ方向を向いていることが重要です。 共通認識を得ていないと、後でまた進め方の話になり昼会の時間が無駄になったり、効率的に進められず一日のゴールに到達しない可能性が高くなるからです。

夕会

書こうと思いましたが時間切れです。 弊社の開発メンバーのniisanが書いた記事を読みましょう。 一日のふりかえりにYWTやってみた

まとめ

一日に3回もデイリースクラムをしているスクラムチームは少ないのではないでしょうか。 重要なのは回数ではなく、スプリントのゴールを達成するために計画を立てることなので、チームの状況に合わせて行えば良いと思います。

おまけ

ROXXでは開発チームのメンバー(エンジニア/デザイナー)を募集しています。 少しでも興味がある方は、私のtwitterにご連絡お願いします。

twitter:@r_sato1201

MetabaseAPIでGoogle認証をするまで

この記事は個人ブログ記事の転載です。

Metabaseに登録されているネイティブクエリを全件取得したかった事案がありました。 MetabaseAPIなるものがあるらしいので、それを使ってみました。
APIを使用するためにはログインが必要です。ログインのAPIは以下のようになっているのですが・・・

POST /api/session/google_auth

Login with Google Auth.

PARAMS:
  • token value must be a non-blank string.
  • request

が、この token が一体何のトークンなのかさっぱりわかりません。( 調べた結果、JWTのid-tokenでした。)
ググってもあまり情報が出て来なかったので、手順をまとめてみます。

GoogleアカウントのOAuthキーを作成する

GoogleAPIsから、OAuthのクライアントIDを作成します。

設定としては、以下のとおり。

また、OAuthのクライアントIDを作成する場合は事前にOAuthの同意画面を作成しておく必要があります。
テスト用に使うので、てきとーに設定しておきます。

作成するとクライアントIDとクライアントシークレットが作成されるので、メモっておきます。

認可画面に飛ばす

MetabaseがOAuthのトークンを必要とするということは、OAuthのフローで認可画面を踏まないといけないということです。
今回はleague/oauth2-googleを使用してみました。

<?php
// index.php

require_once __DIR__ . '/vendor/autoload.php';

$provider = new \League\OAuth2\Client\Provider\Google([
    'clientId' => 'YourClientID',
    'clientSecret' => 'YourClientSecret',
    'redirectUri' => 'http://localhost/callback.php',
]);

// 認可用URIへリダイレクト
header("Location: {$provider->getAuthorizationUrl()}");

sudo php -S localhost で簡易サーバを立ち上げアクセスすると、Googleログインの画面が表示されるはずです。

トークンの取得

Googleでのログインが終わると、登録したコールバックURIに遷移します。(ここでは、 http://localhost/callback.php )
callback後に、JWTのid-tokenを取得します。

<?php
// callback.php

require_once __DIR__ . '/vendor/autoload.php';

$provider = new \League\OAuth2\Client\Provider\Google([
    'clientId' => 'YourClientID',
    'clientSecret' => 'YourClientSecret',
    'redirectUri' => 'http://localhost/callback.php',
]);

// 正常系の場合はcodeというクエリパラメータが付いてくる
assert(isset($_GET['code']));

$token = $provider->getAccessToken('authorization_code', ['code' => $_GET['code']]);
// getValue()でJWT形式の各値を取得できる
$idToken = $token->getValues()['id_token'];

認証とMetabaseAPIの利用

id-tokenを取得できたら、それを利用してMetabaseAPIを使うことができます。
/api/session/google_authのレスポンスにidというキーがあるので、これをヘッダーのX-Metabase-Sessionにセットすることで、各種APIにアクセスできます。

<?php
$guzzle = new \GuzzleHttp\Client();

// metabaseでの認証認可
$response = $guzzle->request('POST', 'https://your.metabase.url/api/session/google_auth', [
    'json' => [
        'token' => $idToken,
    ],
]);
// JSON形式でレスポンスが来るのでデコードする
$sessionData = json_decode($response->getBody()->getContents());
// 成功の場合は必ずidがある
assert(isset($sessionData->id))

// X-Metabase-Sessionに取得したIDを設定することで、各APIを使用できる
$response = $guzzle->request('GET', 'https://your.metabase.url/api/card', ['headers' => [
    'Content-Type' => '‘application/json',
    'X-Metabase-Session' => $sessionData->id,
]]);
$cards = json_decode($response->getBody()->getContents());

Pythonでスクレイピングしてアニメの放送日時を取得してJsonにする

はじめに

昨年末に実家が空き巣に入られたり、年始早々身体に異変があってメンタル的に疲れているところに自分史上間違えなく上位に入る節々の痛さと突発的高熱、その翌週には、首を盛大に寝違えてキョンシーみたいな姿勢での生活を余儀なくされました。どうもagent bank 開発のhironekoです。

ちなみに巷で流行っているコロナではなく、ただの風邪で2日半くらいで回復して味覚等々を失うことなく今は、元気に業務と麻雀に励んでいます。

前回のブログ

  • GitHub Actionsのcronがあるらしいってことでそれを使ってアニメの放送日にSlackへ通知しようぜ!ってのをやりました。

あれから一ヶ月

  • おかげさまでアニメを見逃す日がなくなりました!(素晴らしい)

改善点と改善案

GitHub Actionsのcronラグあり過ぎじゃね?問題

  • こちらは、無料枠で無料でGiHub様の善意の元リソースを使わせていただいているのでちょっと文句言いたいけどスルーすることにしました
    • ただ何かしらのプロダクトには、活用場面なさそうって思いました。

Jsonデータで管理するのだるい問題

  • 前回の目的は、見たいアニメの放送を見逃さない。っていう点にしか焦点は当たっておらずデータの収集や管理に関しては、全くの無関心でした
  • なので改善点としては、以下になると思います。
    • JsonデータのGitHub以外での管理
    • Jsonデータを自動で作成する

改善点挙げておいてなんですが何かと初物や久々(何かを)に触るのがいいと思っていますので、遠い昔に触ったFirebaseでJsonを管理しようかなと考えたんですがクレデンシャルをどう管理すんねんS3か?って考えたんですがこのプロジェクトは、すべてを無料で!を心がけているので却下です。(微々たる金額で済むのも想定できるんですがね)

今回の実装

スクレイピングJsonデータ作るぞの巻きに決定しました。

やらないこと

前提

import requests
from bs4 import BeautifulSoup
from os import getenv
import json
import datetime as dt
import calendar

today = dt.date.today()
dayName = calendar.day_name[today.weekday()]

if today.month < 10:
    month = f'0{today.month}'
else:
    month = today.month

weekList = {
    '月': 'Monday',
    '火': 'Tuesday',
    '水': 'Wednesday',
    '木': 'Thursday',
    '金': 'Friday',
    '土': 'Saturday',
    '日': 'Sunday'
}

load_url = getenv('SCRAPING_TARGET_URL')
html = requests.get(load_url)
soup = BeautifulSoup(html.content, "html.parser")

animeList = soup.find(class_='week_area').find_all('li')

jsonList = []
for e in soup.find(class_='week_area').find_all('li'):
    week = weekList[e.find(class_="oatime").find(class_='youbi').text]
    e.find(class_="oatime").find(class_='youbi').decompose()
    e.find('h4').find('strong').decompose()

    item = {
        'title': e.find('h4').text,
        'publish_at': e.find(class_="oatime").text,
        'channel': 'TOKYO MX',
        'day_of_week': week
    }
    jsonList.append(item)


with open(f'./data/anime/{today.year}{month}.json', 'w') as f:
    json.dump(jsonList, f, ensure_ascii=False, indent=4)

前回同様解説していきます

コードを読めばわかるって内容なので解説すべきことはないんですけどね!   スクレイピングしたあとのDOM操作に関しては、ほんと愚直な方法しか思いつかなかったので誰かいい方法があったら教えて欲しいです

  - あーどうしよっておもったDOM操作が下記のようなHTMLでした

<div class="oatime"><p class="youbi"></p>25:05~ </div>
  • こんな形のHTMLだとclass="oatime"直下のテキストを抜くと25:05〜が改行付きで取得できてしまう。なので先に曜日だけ取得して曜日のクラスのDOMを削除して時刻の取得を行うってことをしました。
    • 下記のような処理
week = weekList[e.find(class_="oatime").find(class_='youbi').text]
e.find(class_="oatime").find(class_='youbi').decompose()

さきに最終的なJsonファイルをどうぞ

[
    {
        "title": "\n7SEEDS\n",
        "publish_at": "22:30~ ",
        "channel": "TOKYO MX",
        "day_of_week": "Monday"
    },
    {
        "title": "\nたとえばラストダンジョン前の村の少年が序盤の街で暮らすような物語\n",
        "publish_at": "23:00~ ",
        "channel": "TOKYO MX",
        "day_of_week": "Monday"
    },
   ......
]
  • 最終的に前回手動で作成したJsonファイルと同じような形式をとることにしました。(処理をわざわざ変えるのもなーっておもったので)
    • ファイル名に関しては、前回ファイル名を取得する際の方法で生成したyyyymmの形式にしました。

新たな問題点

  • TOKYO MXしか見ねー的なノリで始めたんですが、推しキャラの二乃がいる五等分の花嫁約束のネバーランドがT◯Sやフ◯TVじゃないですか。
  • これに関しては、それ以外の取得に関して考えて、結果マージしたデータでSlackに飛ばすようにするんだろうなってうっすら考えてます。まぁどちらもアマプラでいつでも見れるんですけどね!

  • いつcronで動かすよ問題!

    • 1クール単位がいいんですがー言うてそんな設定できるんか?基準日を考慮できないと思うのでn日毎には実行難しそう。
    • ってことでこちらは、月次で実行させておそらくPython側で判定する形にするでしょう!

反省

Pythonのお作法わからずだぁーって書いてしまっているので、リファクタリングとmodule化等を行っていきたいと思います。年末年始、特に年末が何してたかわからんくらいな状態だったので何もできなかったのでこれから平日含め学習をする癖がつくことただただ願います。

最後に独り言

アニメの通知ってamebaTVでできません?やアルっていう漫画サービスで発売日通知できますよって会社の同僚に言われました。「そそそそそうだったの」って内心思ったのは、内緒です。

AWS CloudWatch Logs のログデータを S3 に配信する(Kinesis Data Firehose)

この記事は個人ブログと同じ内容です

www.ritolab.com


CloudWatch Logs では、AWS のコンソールからログデータを S3 にエクスポートできます。

f:id:ro9rito:20210118223828p:plain

全ログデータを CloudWatch 側で保持して、ログデータを抽出する必要があった場合は都度 S3 にエクスポートして使うっていう運用ならこれで問題ないと思います。

別のケースとして、以下のような場合は、CloudWatch Logs から S3 に定期的にログを自動で配信する仕組みが欲しくなります。

  • CloudWatch Logs のログデータを活用するために全てのデータを S3 にも溜めたい(データレイク的なあれ)
  • CloudWatch Logs 側のログデータ保持は限定的(数カ月間だけとか)にするけど、過去全てのログデータは S3 に保管しておきたい(Glacier で氷漬けにする的なあれ)

今回は、CloudWatch Logs のログデータを自動で S3 に配信してみます。

Kinesis Data Firehose

CloudWatch Logs のログデータを S3 に配信するためには、Kinesis Data Firehose を使うとサクッといけます。

Amazon Kinesis Data Firehose

https://aws.amazon.com/jp/kinesis/data-firehose/

Amazon Kinesis Data Firehose は、ストリーミングデータをデータレイクやデータストア、分析サービスに確実にロードする最も簡単な方法を提供するサービスです。
ストリーミングデータを取り込んで変換し、Amazon S3Amazon Redshift、Amazon Elasticsearch Service、汎用 HTTP エンドポイント、さらに Datadog、New Relic、MongoDB、Splunk のようなサービスプロバイダーに配信できます。

流れとしては以下になります。

  1. CloudWatch Logs から Kinesis Data Firehose にログデータを送信
  2. Kinesis Data Firehose から S3 にログデータを配信

サブスクリプションフィルタ

CloudWatch Logs にはサブスクリプションフィルタという機能があり、これを設定する事で Kinesis Data Firehose にログデータを送信できるようになります。

f:id:ro9rito:20210118224036p:plain

しかしながらキャプチャを見ての通り、AWS コンソールからは Elasticsearch もしくは Lambda のサブスクリプションフィルタしか作成できません。(2021年1月16日現在)

なので、Kinesis へのサブスクリプションフィルタを作成するためには AWS CLI などを使用する必要があります。

そういう理由もあり今回は AWS コンソール画面ではなく Terraform(AWS に対応したインフラオーケストレーションツール)を使って一連の仕組みを構築していこうと思います。

Terraform

https://www.terraform.io/

S3 サブスクリプションフィルタ以外は AWS コンソール画面からも作成できるので、この記事の前半はすっ飛ばせます。

ロググループと S3 バケットの作成

main.yml

# CloudWatch Logs - LogGroup
resource "aws_cloudwatch_log_group" "my_test" {
  name = "my-test"
}

# S3 - Bucket
resource "aws_s3_bucket" "cloud_watch_logs" {
  bucket = "my-test-logs"
  acl    = "private"
}

Kinesis Data Firehose 配信ストリームの作成

まずは S3 への配信が行えるように Kinesis 用の IAM Role を作成します。

main.yml

# CloudWatch Logs - LogGroup
# IAM Role - for Kinesis
resource "aws_iam_role" "kinesis_data_firehose_send_log_to_s3" {
  name               = "tf-KinesisFirehoseServiceRole-my-test"
  assume_role_policy = file("aws_iam_role_kinesis_data_firehose.json")
}

# IAM Policy - for Kinesis
resource "aws_iam_policy" "kinesis_data_firehose_send_log_to_s3" {
  name        = "tf-KinesisFirehoseServicePolicy-my-test"
  policy      = templatefile("aws_iam_policy_kinesis_data_firehose", {
    aws_id              = var.aws_id
    region              = var.region
    bucket_arn          = aws_s3_bucket.my_test.arn
    cloudwatch_logs_arn = aws_cloudwatch_log_group.my_test.arn
  })
}

# Attach Policy to Role
resource "aws_iam_role_policy_attachment" "kinesis_data_firehose_send_log_to_s3" {
  role       = aws_iam_role.kinesis_data_firehose_send_log_to_s3.name
  policy_arn = aws_iam_policy.kinesis_data_firehose_send_log_to_s3.arn
}

読み込んでいるポリシーのファイルはそれぞれ以下

aws_iam_role_kinesis_data_firehose.json

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "firehose.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "s3:AbortMultipartUpload",
                "s3:GetBucketLocation",
                "s3:GetObject",
                "s3:ListBucket",
                "s3:ListBucketMultipartUploads",
                "s3:PutObject"
            ],
            "Resource": [
                "${bucket_arn}",
                "${bucket_arn}/*"
            ]
        },
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "logs:PutLogEvents"
            ],
            "Resource": [
                "${cloudwatch_logs_arn}"
            ]
        },
        {
            "Sid": "",
            "Effect": "Allow",
            "Action": [
                "kinesis:DescribeStream",
                "kinesis:GetShardIterator",
                "kinesis:GetRecords",
                "kinesis:ListShards"
            ],
            "Resource": "arn:aws:kinesis:${region}:${aws_id}:stream/%FIREHOSE_POLICY_TEMPLATE_PLACEHOLDER%"
        },
        {
            "Effect": "Allow",
            "Action": [
                "kms:Decrypt"
            ],
            "Resource": [
                "arn:aws:kms:${region}:${aws_id}:key/%FIREHOSE_POLICY_TEMPLATE_PLACEHOLDER%"
            ],
            "Condition": {
                "StringEquals": {
                    "kms:ViaService": "kinesis.${region}.amazonaws.com"
                },
                "StringLike": {
                    "kms:EncryptionContext:aws:kinesis:arn": "arn:aws:kinesis:${region}:${aws_id}:stream/%FIREHOSE_POLICY_TEMPLATE_PLACEHOLDER%"
                }
            }
        }
    ]
}

Kinesis Data Firehose 側の配信ストリームを作成します。

main.yml

# Kinesis Data Firehose - delivery stream
resource "aws_kinesis_firehose_delivery_stream" "s3_stream" {
  name        = "tf-my-test-send-log-from-cwl-to-s3-stream"
  destination = "extended_s3"

  extended_s3_configuration {
    bucket_arn         = aws_s3_bucket.my_test.arn
    role_arn           = aws_iam_role.kinesis_data_firehose_send_log_to_s3.arn
    buffer_size        = 10  # MB
    buffer_interval    = 300 # second
    compression_format = "UNCOMPRESSED"
    s3_backup_mode     = "Disabled"
  }
}
  • destination には S3 を宛て先とするので extended_s3 を指定します
  • buffer_size は MB 単位です。今回の設定の場合は、受信バッファーが 10MB に到達したら buffer_interval に関係無く S3 に配信します。
  • buffer_interval は 秒単位です。今回の設定の場合は、5 分経過したら(=5分おきに)buffer_size に関係無く S3 に配信します。
  • compression_format は圧縮についての設定ですが、CloudWatch Logs のサブスクリプションフィルタを介して Kinesis に送信されるログは Base64 エンコードされて gzip に圧縮されるので、ここでの圧縮は不要です。

サブスクリプションフィルタのための IAM Role 作成

CloudWatch Logs のサブスクリプションフィルタを作成するにあたり、必要なロールを作成しておきます。

main.yml

# IAM Role - for Cloud Watch Logs
resource "aws_iam_role" "cloud_watch_logs_send_log_to_kinesis" {
  name               = "tf-CloudWatchLogsServiceRole-my-test-send-log-to-kinesis"
  assume_role_policy = templatefile("aws_iam_role_cloud_watch_logs_send_log_to_kinesis.json", {
    region = var.region
  })
}

# IAM Role Policy - Cloud Watch Logs
resource "aws_iam_policy" "cloud_watch_logs_send_log_to_kinesis" {
  name        = "tf-CloudWatchLogsServicePolicy-my-test-send-log-to-kinesis"
  policy      = templatefile("aws_iam_policy_cloudwatch_logs_send_log_to_kinesis.json", {
    kinesis_data_firehouse_arn = aws_kinesis_firehose_delivery_stream.s3_stream.arn
  })
}

# Attach Policy to Role
resource "aws_iam_role_policy_attachment" "cloud_watch_logs_send_log_to_kinesis" {
  role       = aws_iam_role.cloud_watch_logs_send_log_to_kinesis.name
  policy_arn = aws_iam_policy.cloud_watch_logs_send_log_to_kinesis.arn
}

読み込んでいるポリシーのファイルはそれぞれ以下

aws_iam_role_cloud_watch_logs_send_log_to_kinesis.json

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "logs.${region}.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

aws_iam_policy_cloudwatch_logs_send_log_to_kinesis.json

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "firehose:PutRecord",
                "firehose:PutRecordBatch"
            ],
            "Resource": [
                "${kinesis_data_firehouse_arn}"
            ]
        }
    ]
}

ここまでサブスクリプションフィルタ設定の前の準備として Terraform での設定を書きましたが、これらは AWS コンソール画面からでも作成できます。

次の S3 サブスクリプションフィルタ作成が、前述の通り AWS CLI かその他 Terraform 等のツールで行う必要があります。

CloudWatch Logs S3 サブスクリプションフィルタの作成

S3 サブスクリプションフィルタを作成します。

main.yml

## Subscription Filter
resource "aws_cloudwatch_log_subscription_filter" "s3" {
  name            = "tf-my-test-s3-subscription-filter"
  role_arn        = aws_iam_role.cloud_watch_logs_send_log_to_kinesis.arn
  log_group_name  = aws_cloudwatch_log_group.my_test.name
  destination_arn = aws_kinesis_firehose_delivery_stream.s3_stream.arn
  filter_pattern  = ""
}

filter_pattern(フィルターパターン)について、ここには何も記述していないですが、この場合は全てのログイベントに一致(=全てのログが対象)します。

これを例えば、「ELB のヘルスチェックは除外したい」の場合は以下のように設定する事で kinesis への送信を行わないようにできます。

filter_pattern  = "-\"ELB-HealthChecker/2.0\""

フィルターとパターンの構文

docs.aws.amazon.com

動作確認

S3 サブスクリプションフィルタを CloudWatch Logs に設定できたので、S3 を確認してみます。

f:id:ro9rito:20210118224616p:plain

S3 にログが配信された事が確認できました。

ちなみにこれを S3 Select で見てみると

{
    "messageType": "DATA_MESSAGE",
    "owner": "xxxxxxxx",
    "logGroup": "/my-test",
    "logStream": "php/sample/4478f375fl19400daf513cb5b0cd77a9",
    "subscriptionFilters": [
        "tf-my-test-s3-subscription-filter"
    ],
    "logEvents": [
        {
            "id": "123456789...",
            "timestamp": 1610777406264,
            "message": "[2021-01-16 15:10:06] local.INFO: message test. {\"type\":\"info\"} \n"
        },
        {
            "id": "123456789...",
            "timestamp": 1610777406264,
            "message": "[2021-01-16 15:10:06] local.WARNING: message test. {\"type\":\"warning\"} \n"
        }
    ]
},
{
    "messageType": "DATA_MESSAGE",
    "owner": "xxxxxxxx",
    "logGroup": "/my-test",
    "logStream": "nginx/sample/44k86375f03b410daf513p2xb0cd77a9",
    "subscriptionFilters": [
        "tf-my-test-s3-subscription-filter"
    ],
    "logEvents": [
        {
            "id": "123456789...",
            "timestamp": 1610777410150,
            "message": "12.3.45.57 - - [16/Jan/2021:15:10:10 +0900] \"GET /sample/1 HTTP/1.1\" 200 17486 \"-\" \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36\" \"123.45.67.89\""
        }
    ]
}

問題なく配信されてきているようですね。

コストどんな感じだろう

とはいえ、kinesis もお高いんでしょう?という気持ちになったので、CloudWatch ログデータ保存と S3 のデータ保存、そして Kinesis Data Firehose の取り込みデータ料金を見てみました。(2021/01/16時点・東京リージョン)

  • CloudWatch Logs(ログデータ保存)
    • 0.033 USD/GB
  • S3 標準(ストレージ 〜 50TB)
    • 0.025 USD/GB
  • S3 Glacier(〜 50TB)
    • 0.005 USD/GB
  • Kinesis Data Firehose(取込データ量 最初の 500 TB/月)
    • 0.036USD/GB

aws.amazon.com

aws.amazon.com

aws.amazon.com

「ログデータはサービスが終了するまで永久に持ち続ける」とした場合に、まあふつうに考えたら Kinesis 稼働分のコストもあるので CloudWatch Logs のみを使い続けた方がコストは安いようにも思えるけど、例えば、以下のような使い方をした場合

  • CloudWatch Logs
    • 3 ヶ月間だけログデータを保持
  • Kinesis Data Firehose
    • 常時 S3 へログデータを配信
  • S3
    • kinesis から配信されたログデータは 標準 に格納後、3 ヶ月後に Glacier へ移動

この構成だと、7 ヶ月後にはコストがトントンになって、8 ヶ月後には CloudWatch Logs のみで運用するよりも月額のコストが安くなった。(参考程度なのできちんとした試算は各自でお願いします)

f:id:ro9rito:20210118224837p:plain

永久に持ち続けないとしても年単位では持つと思うので、そう考えてもお得かな。ログデータはサービスが動いている以上永延に溜まるし、Glacier 様々という結論に至りました。(あんまり本筋とは関係ない結論)

まとめ

AWS にログデータを溜める時には Glacier を上手く活用しましょう。

ともあれ CloudWatch Logs から S3 へログデータをそっと配信できたのでこれにて終了です。

Twilioを使ってLaravelのSMS通知をする

※ 本記事はZennに投稿したものを転載したものになります。よかったらZennの方の評価もよろしくおねがいします。

zenn.dev

こんにちはみなさん

うん、まあ、ちょっとしたアクシデントがありまして、Laravelが公式提供しているVonage Communication Apiを使ったSMS通知が使えなくなってしまったのですよね。 というわけで、別のSMSサービスとしてTwilioを使うようにします。

ではやっていきましょう。

問題設定

今回はユーザへのSMS通知を、Twilioを使って実現してみましょう。 方針は以下の通りです。

  • SMS通知チャンネルを作る
  • 通知チャンネルで使用するクライアントをTwilioにする。
  • 実際に送信する

結局どうしたかというと、パッケージを作成して、簡単にtwilioを入れられるようにしたって感じです。

Quick Start

どうやっているのかを知りたい人はほとんどおらんと思いますので、どうやれば動かせるかを先に解説しちゃいます。

パッケージインストール

まず、パッケージとsdkを導入します。

composer require niisan/laravel-sms-notification twilio/sdk

個人的な趣向により、sdkを直接はパッケージに依存させていないのです。

ついで、設定ファイルを作ります。

php artisan vendor:publish --provider "Niisan\Notification\SmsNotificationServiceProvider"

設定ファイルの中身は、基本的には環境変数で外から入れることができます。

SMS_DRIVER=twilio
SMS_DEFAULT_FROM='+1111222233'
TWILIO_SID=***********************
TWILIO_TOKEN=***********************

SID と TOKEN はTwilioに登録したときにもらえます。 また、FROMはTwilioにログインしたときのダッシュボードから、無料の電話番号を発行して、それを使います。

通知の方法

SMS通知をたくさんうつ場合は通知対象のモデルに以下のメソッドをはやしておきます。

<?php
// 省略
class User extends Authenticatable
// 省略
    public function routeNotificationForSms()
    {
        return $this->phone_number;
    }

細かいことですが、Twilioでは国際電話番号の規格を使用していて、+から必要になるようです。(ex. +1122223333 )

次にNotificationを作ります。

<?php

namespace App\Notifications;

use Illuminate\Notifications\Notification;
use Niisan\Notification\SmsMessage;

class SmsNotification extends Notification
{
    /**
     * Get the notification's delivery channels.
     *
     * @param  mixed  $notifiable
     * @return array
     */
    public function via($notifiable)
    {
        return ['sms'];
    }

    public function toSms($notifiable): SmsMessage
    {
        return (new SmsMessage)
                ->unicode()
                ->body('hello world! こんにちは!' . $notifiable->name);
    }
}

viaメソッドでsmsを追加し、toSmsメソッドで、メッセージクラスを返します。 SmsMessageオブジェクトで、メッセージに日本語を含める場合はunicodeメソッドで、マルチバイトをオンにしておきます。

あとは、普通にnotifyするだけです。

$user->notify(new SmsNotification);

おまけ

送信先や送信元をNotificationで変更することもできます。

        return (new SmsMessage)
                ->unicode()
                ->body('hello world! こんにちは!' . $notifiable->name)
        ->to('+1122223333')
        ->from('+2233334444')

こうすると、notifiableやdefault_fromに設定してある電話番号を無視して、ここで設定した電話番号を使うようにします。

SMS通知チャンネルを作る

ここからはパッケージの実装を説明します。多くの人にはいらんですが、興味のある方だけ見て行ってくださいませ。

公式のパッケージではないので、Laravelの通知チャンネルを拡張する必要があるわけですが、公式のマニュアルがちょっと不親切というか、interface用意しておいてほしいのよね。 https://laravel.com/docs/8.x/notifications#custom-channels

チャンネルの拡張

SMSの通知チャンネルをこんな風に作っておきます。

<?php
namespace Niisan\Notification;

use Illuminate\Notifications\Notification;
use Niisan\Notification\Contracts\SmsClient;
use RuntimeException;

class SmsChannel
{

    /** @var SmsClient $client */
    private $client;

    public function __construct(SmsClient $client)
    {
        $this->client = $client;
    }

    public function send($notifiable, Notification $notification)
    {
        $message = $notification->toSms($notifiable);
        if (!$message->to) {
            if (!method_exists($notifiable, 'routeNotificationForSms')) {
                throw new RuntimeException('A notifiable object must have "routeNotificationForSms" method.');
            }
            $message->to($notifiable->routeNotificationForSms());
        }

        $message->from(config('sms-notification.default_from'));
        
        $this->client->send($message);
    }
}

NotificationオブジェとのtoSmsメソッドからメッセージオブジェクトを取得し、必要であればtoやfromを入れて、クライアントに投げる形式になっています。 基本、クライアントはTwilio以外のも考えられると思ったので、他にも入れられるようにしてあります。

SmsClientはinterfaceになっており、public function send(SmsMessage $message): void;の未定義されていますので、これをクライアントごとに実装すればオッケーとなっています。

チャンネルの拡張を適用する

サービスプロバイダーを使って、チャンネルの拡張を適用します。

<?php
namespace Niisan\Notification;

use Illuminate\Notifications\ChannelManager;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\ServiceProvider;
use Niisan\Notification\Clients\Twilio;
use Niisan\Notification\Contracts\SmsClient;

class SmsNotificationServiceProvider extends ServiceProvider
{

    public function boot()
    {
        $this->publishes([
            __DIR__.'/config/sms-notification.php' => config_path('sms-notification.php'),
        ]);
    }

    public function register()
    {
        $this->app->bind(SmsClient::class, function ($app) {
            if (config('sms-notification.driver') === 'twilio') {
                $client = new \Twilio\Rest\Client(
                    config('sms-notification.twilio.sid'),
                    config('sms-notification.twilio.token')
                );
                return new Twilio($client);
            }
        });

        Notification::resolved(function (ChannelManager $service) {
            $service->extend('sms', function ($app) {
                return $app[SmsChannel::class];
            });
        });
    }
}

まあ、Twilioしかないんですがね。 一応、SmsClientについては入れ替えできるようになっているので、別のSMSサービス使いたい場合は新しいクライアントクラスを実装して、サービスプロバイダで入れ替えちゃえばオッケーです。 多分。

クライアントクラスの実装

クライアントクラスはそのまま、Twilioのクライアントをラップする感じです。

<?php
namespace Niisan\Notification\Clients;

use Niisan\Notification\Contracts\SmsClient;
use Niisan\Notification\SmsMessage;
use Twilio\Rest\Client;

class Twilio implements SmsClient
{

    private $client;

    public function __construct(Client $client)
    {
        $this->client = $client;
    }
    
    /**
     * Send message through Twilio.
     *
     * @param SmsMessage $message
     *
     * @return void
     */
    public function send(SmsMessage $message): void
    {
        $this->client->messages->create(
            $message->to,
            [
                'from' => $message->from,
                'body' => $message->body,
                'smartEncoded' => $message->unicode
            ]
        );
    }
}

これはもう、単純にTwilio SDKのクライアントをラップしているだけです。 別のサービスのクライアントを実装する場合は、Niisan\Notification\Contracts\SmsClientを実装しておいて、サービスプロバイダで入れ替えればオッケーです。

SmsMessageクラスは対して言及するところもないので、割愛します。

まとめ

ということで、Laravel公式のSMS通知が使えなくなってしまったので、自作したという話でした。 いや、Laravelはパッケージ自作して突っ込むの簡単ななんですが、微妙にinterfaceがないとか、足りんところもあるなぁといった感想。 まあ、パッケージづくりは楽しいというか、プロダクト開発のようにカネに直結しない分、気楽にできますね。 いや、カネ儲けも好きなんですがね。

今回はこんなところです。

おまけ

弊社ROXXでは、エンジニアさんをとてもとても採用しております。 興味のある方は私のtwitterにDMするかリプライつけてくれると、反応すると思います。 よろしくどうぞ!

twitter.com

docker で nginx & php-fpm の PHP 実行環境を構築する(TCP/UNIX domain socket)

この記事は個人ブログと同じ内容です

www.ritolab.com


nginx と php-fpm の構成で docker コンテナの PHP アプリケーション実行環境を構築してみます。

(開発環境構築の話ではないので docker-compose は使いません)

nginx と php-fpm の通信(How)が今回の話のメインです。

ディレクトリ構成など

docker イメージとコンテナの作成に入る前に、今回は php-fpm と nginx のコンテナを作成するため、それぞれ php-fpm 用に php というディレクトリを作成し、nginx 用に web というディレクトリを切って進めます。

そして、アプリケーション(の代わり)として index.phpphp-fpm 側に配置しておきます。

docker/
├─ php/
│   └ src/
│       └─ index.php
└─ web/

なお、index.php は phpinfo を表示させるだけの簡単なものになっています。

php/src/index.php

<?php phpinfo(); ?>

php-fpm の Dockerfile を作成

Dockerfile を作成して、まずは最もベースとなる部分の定義を行います。

php/ 配下に Dockerfile を作成し以下を記述します。

php/Dockerfile

FROM php:8.0-fpm-alpine

# install configure file
RUN cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini

# settings
COPY settings/php.ini /usr/local/etc/php/conf.d/php.ini

# app sources
COPY src /usr/share/nginx/html
  1. ベースイメージには PHP の公式イメージを指定しています。
  2. PHP の設定ファイルである php.ini を作成します。
  3. 追加で設定するための PHP 設定ファイルを設置しています。
  4. アプリケーションをドキュメントルートにコピーしています。

PHP の設定ファイル php.ini について

工程 2 では、PHP に予め用意されているテンプレートからそのまま php.ini を作成し設置しています。

設定値をデフォルトから変更したいものについては、予め用意した php.ini にその設定を記述し、工程 3 で conf.d/ 配下に設置する事で設定値を上書きしています。

php/settings/php.ini

[PHP]
date.timezone = "Asia/Tokyo"

なので、変更が不要なら工程 3 の記述は不要です。

nginx の Dockerfile を作成

次に、nginx 側の Dockerfile を作成します。

web/ 配下に Dockerfile を作成し以下を記述します。

web/Dockerfile

FROM nginx:1.19-alpine

# settings
COPY settings/default.conf /etc/nginx/conf.d/default.conf
  1. ベースイメージには nginx の公式イメージを指定しています。
  2. nginx の設定ファイルを設置します。

nginx - Docker Official Images

hub.docker.com

nginx と php-fpm の通信を定義する

nginx と php-fpm の通信方式は 2 通りあって、それぞれで設定ファイルや Dockerfile への記述が異なります。

どちらを採用すべきか?については検証と結論に至れていないので、今回は両方で構築してみます。

TCP で通信を行う

TCP で通信する場合は、nginx の設定ファイルを以下のように定義します。

web/settings/default.conf

server {
    listen 80;
    root   /usr/share/nginx/html;

    location / {
        index          index.php index.html index.htm;
        fastcgi_pass   php-sample:9000;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }
}

設定のもろもろは条件によって書き方が異なるためここの記述自体はかなり簡略化してますが、ポイントは fastcgi_pass の値です。

fastcgi_pass php-sample:9000;

  • php-sample というのは、今回作成する php-fpm のコンテナ名です。この後、php-fpm のコンテナを立ち上げる際にこの名前で起動させるので、それをここに指定します。

  • :9000 はポート番号です。php-fpm のデフォルトの待受ポートは 9000 なのでこれを指定しています。

前項で php-fpm と nginx の、ベースの定義を行いましたが、TCP で通信する場合は nginx の設定ファイルの定義のみで実現できます。

動作確認

ではイメージの作成とコンテナの起動を行いアプリケーションを動作させてみます。

以下のコマンドを実行して環境を構築します。

# 1. PHP のイメージを作成
docker build --no-cache -t php/sample:20210109 .

# 2. nginx のイメージを作成
docker build --no-cache -t nginx/sample:20210109 .

# 3. ネットワークを作成
docker network create --driver bridge sample_nw

# 4. PHP のコンテナを起動
docker run --net=sample_nw --name php-sample php/sample:20210109

# 5. nginx のコンテナを起動
docker run -p 80:80 --net=sample_nw --name nginx-sample nginx/sample:20210109

ブラウザからアクセスします。

f:id:ro9rito:20210112202912p:plain

nginx と php-fpm を TCP で通信させてアプリケーションが実行できました。

最終的なファイル構成

docker/
├── php/
│   ├ Dockerfile
│   ├ settings/
│   │   └─ php.ini
│   └ src/
│       └─ index.php
└── web/
    ├ Dockerfile
    └ settings/
        └─ default.conf

Docker コンテナ・ネットワークについて

先程の環境立ち上げの際に、イメージとコンテナ以外に「ネットワーク」を作成しました。

1つのネットワークを作成しそのネットワークに各コンテナを接続する事で、コンテナ間が互いを識別できるようにしています。

作成したネットワークの詳細を確認すると、コンテナが2つ接続されている事がわかります。

# ネットワークを作成
docker network create --driver bridge sample_nw

# ネットワーク一覧
% docker network ls
NETWORK ID     NAME             DRIVER    SCOPE
9326dc6f3546   sample_nw        bridge    local

# 作成したネットワークを確認
% docker network inspect sample_nw
[
  {
      "Name": "sample_nw",
      "Id": "9326dc6f35463041ec9511fba0426bd81311a704f757da755a81a42fd5da51ef",
      "Created": "2021-01-09T04:31:03.73817688Z",
      "Scope": "local",
      "Driver": "bridge",
      "EnableIPv6": false,
      "IPAM": {
          "Driver": "default",
          "Options": {},
          "Config": [
              {
                  "Subnet": "172.18.0.0/16",
                  "Gateway": "172.18.0.1"
              }
          ]
      },
      "Internal": false,
      "Attachable": false,
      "Ingress": false,
      "ConfigFrom": {
          "Network": ""
      },
      "ConfigOnly": false,
      "Containers": {
          "61d2d36499511f76ea2e64cd3124da3fc0fc05aa5b8af115bd70a3b937258e31": {
              "Name": "nginx-sample",
              "EndpointID": "27a848cb6d4e39121a2a8fcd0ec69c0929411e6bf353b868c7dc70354a351ef9",
              "MacAddress": "08:19:ac:14:00:06",
              "IPv4Address": "172.18.0.3/16",
              "IPv6Address": ""
          },
          "acbc9a2bf88caeb1fad3a8d0d0e5a409642ea2651487abede28157ad883b027d": {
              "Name": "php-sample",
              "EndpointID": "b1fe75cad1c2e6d9541cfca82577a1e02e93107b6e026ecb476e9309456ae1ce",
              "MacAddress": "08:19:ac:14:00:05",
              "IPv4Address": "172.18.0.2/16",
              "IPv6Address": ""
          }
      },
      "Options": {},
      "Labels": {}
  }
]

Docker コンテナ・ネットワークの理解

docs.docker.jp

ちなみに、ネットワークを作成せずに、 nginx 側のコンテナ起動時に --link オプションを使って php-fpm 側のコンテナを識別させる方法もありますが、現在では非推奨となっています。

コンテナ・リンク機能(古い機能)

docs.docker.jp

UNIX ドメインソケット で通信を行う

UNIX ドメインソケットで通信する場合に TCP の時と違うのは、通信の為にソケットを指定する事と、nginx 側からソケットを参照できるようにする事です。

php-fpm

php/Dockerfile

FROM php:8.0-fpm-alpine

# add user
RUN addgroup -S nginx && adduser -S nginx -G nginx

# install setting
RUN cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini

# settings
COPY settings/zzz-docker.conf /usr/local/etc/php-fpm.d/zzz-docker.conf
COPY settings/php.ini /usr/local/etc/php/conf.d/app-php.ini

# unix socket
RUN mkdir /var/run/php-fpm
VOLUME ["/var/run/php-fpm"]

# app sources
COPY src /usr/share/nginx/html
  1. ベースイメージには PHP の公式イメージを指定しています。
  2. nginx ユーザを作成します。
  3. PHP の設定ファイルである php.ini を作成します。
  4. php-fpm の設定ファイル(更新分)を設置します。
  5. PHP の設定ファイル(更新分)を設置します。
  6. ソケットを設置するディレクトリを作成し、ボリュームマウントします。
  7. アプリケーションをドキュメントルートにコピーします。

php-fpm の設定ファイルはデフォルトで設置されていますが、UNIX ドメインソケットを利用するために値を変更したい(工程 5 のところ)ので、設定値を上書きするための設定ファイルを作成します。

php/settings/zzz-docker.conf

[www]
listen = /var/run/php-fpm/php-fpm.sock
listen.owner = nginx
listen.group = nginx
listen.mode = 0660

unix ドメインソケットを使うように指定したのと、ソケットを使用するユーザ(パーミッション)を設定しています。

owner/group は、nginx 側からソケットを参照するユーザを指定しています。

nginx

web/Dockerfile

FROM nginx:1.19-alpine

# settings
COPY settings/default.conf /etc/nginx/conf.d/default.conf
  1. ベースイメージには nginx の公式イメージを指定しています。
  2. nginx の設定ファイルを設置します。

Dockerfile は TCP の時と変わりません。

nginx の設定ファイルを以下に定義します。

web/settings/default.conf

server {
    listen       80;
    server_name  localhost;
    root         /usr/share/nginx/html;

    location / {
        index          index.php index.html index.htm;
        fastcgi_pass   unix:/var/run/php-fpm/php-fpm.sock;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }
}

TCP の時との違いは、fastcgi_pass に unix ドメインソケットを指定している点です。

動作確認

ではイメージの作成とコンテナの起動を行いアプリケーションを動作させてみます。

以下のコマンドを実行して環境を構築します。

# 1. PHP のイメージを作成
docker build --no-cache -t php/sample:20210109 .

# 2. nginx のイメージを作成
docker build --no-cache -t nginx/sample:20210109 .

# 3. PHP のコンテナを起動
docker run --name php-sample php/sample:20210109

# 4. nginx のコンテナを起動
docker run -p 80:80 --volumes-from php-sample --name nginx-sample nginx/sample:20210109

ブラウザからアクセスします。

f:id:ro9rito:20210112203405p:plain

nginx と php-fpm を UNIX ドメインソケット で通信させてアプリケーションが実行できました。

最終的なファイル構成

docker/
├ php/
│   ├ Dockerfile
│   ├ settings/
│   │   ├─ php.ini
│   │   └─ zzz-docker.conf
│   └ src/
│       └─ index.php
└ web/
    ├ Dockerfile
    └ settings/
        └── default.conf

まとめ

Laravel や CakePHP 等の PHP フレームワークを動かす際も基本的には同じ要領で、nginx の設定ファイル側を調整したり、必要なパッケージを PHP 側に入れたりなどすれば動作させる事ができます。

実行環境なのでアプリケーションごとイメージに固めていますが、これらの dockerfile と docker-compose を併せて役割を分ければ開発環境の構築も可能です。