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 を併せて役割を分ければ開発環境の構築も可能です。

TypeScriptで深いJSON構造から要素を取り出すときに型をちゃんと取るTIPS

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

kotamat.com

下記のような多段のマスターデータが存在しているときに、ちゃんと型安全に値を取り出したいとなったときの型定義を考える

large.json

{
  "1": "foo",
  "2": "baz"
}

detail.json

{
  "1": {
    "1001": "foo1",
    "1002": "foo2"
  },
  "2": {
    "2001": "baz1",
    "2002": "baz2"
  }
}

これに対して、下記のような参照をしたら、ちゃんと値を取りたい。

import large from "~/large.json";
import detail from "~/detail.json";

// anyもちゃんとした型に変更する
function getValue(largeKey: any, detailKey: any) {
  // hogeを型がちゃんとついてる状態で取得する
  const hoge = detail[largeKey][detailKey];
}

※ちなみに上記で import している large, detail は as const したような厳密な型定義になっていることを前提とする。

declare module "~/detail.json" {
  type Detail = {
    "1": {
      "1001": "foo1";
      "1002": "foo2";
    };
    "2": {
      "2001": "baz1";
      "2002": "baz2";
    };
  };
  const data: Detail;
  export default data;
}

Step1: 深さ指定して key の型を取れるようにする

https://qiita.com/KuwaK/items/587205867d333b705a41 を参考にさせていただき、下記のような型を作成する(Property2 を NthDepthProperty に変えているが、必要であればもとに戻します)

type NumMap = {
  3: 2;
  2: 1;
  1: 1;
};

type ValueOf<T> = T[keyof T];

export type NthDepthProperty<T, P extends keyof NumMap> = P extends 1
  ? keyof T
  : ValueOf<
      {
        [K in keyof T]: T[K] extends object
          ? NthDepthProperty<T[K], NumMap[P]>
          : never;
      }
    >;

簡単に説明すると、最大 3 段ネストするオブジェクトに対して、型引数 2 つ目に指定した階層に存在する key を Union Type にして返却するというもの。NumMap を増やせば当然いくらでもネストは可能だが、今回は 3 段のままで大丈夫なのでそのままにする

その上で元々のファイルを下記のように変えると、引数として望まれる型が適切に指定できる

import large from "~/large.json";
import detail from "~/detail.json";
import { NthDepthProperty } from "./types";

function getValue(
  largeKey: NthDepthProperty<typeof large, 1>, // largeKey: "1" | "2"
  detailKey: NthDepthProperty<typeof detail, 2> // detailKey: ValueOf<{"1": "1001" | "1002"; "2": "2001" | "2002"}> = "1001" | "1002" | "2001" | "2002"
) {
  const hoge = detail[largeKey][detailKey];
}

largeKey は keyof でもいいんだけど、見た目的に揃えた

Step2: Union Type を Intersection に変える型を用意する

上記のままでも行けそうな感じはするが、下記のようなエラーが出てしまう

(parameter) detailKey: ValueOf<{
    1: "1001" | "1002";
    2: "2001" | "2002";
}>
型 'ValueOf<{ 1: "1001" | "1002"; 2: "2001" | "2002"; }>' の式を使用して型 '{ "1001": "foo1"; "1002": "foo2"; } | { "2001": "baz1"; "2002": "baz2"; }' にインデックスを付けることはできないため、要素は暗黙的に 'any' 型になります。
  プロパティ '1001' は型 '{ "1001": "foo1"; "1002": "foo2"; } | { "2001": "baz1"; "2002": "baz2"; }' に存在しません。

これはdetail[largeKey] で取得した値の型が

{
    "1001": "foo1";
    "1002": "foo2";
} | {
    "2001": "baz1";
    "2002": "baz2";
}

となっており、例えばこのオブジェクトに対して"1001"という key を指定した場合、largeKey=2 で参照すると出てくる後方の型にマッチしない可能性があるためエラーになるかもしれないから。

回避策としては、下記のように丁寧に TypeGuard をして行くことも考えられるが、key が増えるたびに処理が増えてしまい、なんのための型定義をしているのかよくわからなくなってしまう。

if (largeKey === "1" && detailKey === "1001") {
  const l = detail[largeKey];
  const hoge = l[detailKey];
} else if (largeKey === "2" && detailKey === "2001") {
  const l = detail[largeKey];
  const hoge = l[detailKey];
}

ここで開発者は事前に largeKey と detailKey の組み合わせがほぼ確実に正しい形で渡って生きていることを知っている(そうでない場合は一旦想定しないで OK)とした場合、Union 型を Intersection 型に変える事によって解決する。 つまり

{
    "1001": "foo1";
    "1002": "foo2";
} | {
    "2001": "baz1";
    "2002": "baz2";
}

{
    "1001": "foo1";
    "1002": "foo2";
} & {
    "2001": "baz1";
    "2002": "baz2";
}

に変えてしまえば、"1001"でアクセスしたとしても確実に値を得られるということになる。

ここで、Intersection に変換する方法はこちらを参考に下記のような型で解決できる

export type UnionToIntersection<U> = (
  U extends any ? (k: U) => void : never
) extends (k: infer I) => void
  ? I
  : never;

あとはこんな感じでコードを変更すれば hoge がいい感じの型で取得できるようになる

import large from "~/large.json";
import detail from "~/detail.json";
import { UnionToIntersection, NthDepthProperty } from "./types";

function getValue(
  largeKey: NthDepthProperty<typeof large, 1>,
  detailKey: NthDepthProperty<typeof detail, 2>
) {
  const l = detail[largeKey];
  const hoge = (l as UnionToIntersection<typeof l>)[detailKey]; // hoge: "foo1" | "foo2" | "baz1" | "baz2"
}

おしまい

「プロジェクトマネージャーいらなくね?」と思って辞めた話

back check 開発チームの秋葉です

概要

※ PjM: プロジェクトマネージャー

三行でまとめると

  • PjMの役割を開発チームへと移譲していった
  • チームとしてPjMいならいと思い辞めてみた
  • 辞めた結果支障なかった

PjMの仕事

自分は2020年の4月頃から、開発チームのPjMというポジションにつきました。
PjMをやっていましたが、1メンバーとして普通に開発もしていました。(開発をするうえで、負荷は減らしてもらっていました)

PjMというと、会社やチームによってやることが千差万別だとおもいますが、自分は以下のようなことをやってました。

  • スケジュールの管理(短期)
  • 差し込みタスクのハンドリング
  • スプリントプランニング
  • POと開発チームの橋渡し的な役割
  • POがいないときの開発の意思決定
  • etc....

色々書きましたが個人的には、やる人が決まっていな雑多なこと を やっていました。

PjM辞めたわけ

back check 開発チーム は アジャイル開発の手法として、スクラム開発 を導入しています。
チームとしてはより良いアジャイル開発、スクラム開発を目指しています。

ここで、アジャイル開発名著のである アジャイルサムライ から言葉をいくつか引用させていただきます。

自己組織化

最良のアーキテクチャ・要求・設計は、
自己組織的なチームから生み出されます。

成果責任と権限委譲

意欲に満ちた人々を集めてプロジェクトを構成します。
環境と支援を与え仕事が無事終わるまで彼らを信頼します。

といことで、開発チームを自己組織化していくために、
スプリント内での開発の成果責任 と PjMの権限 を開発チーム移譲してみました、
徐々にやっていくなかで、「あれ、もうプロジェクトマネージャーいらなくね?」となり、 自分がこの役割を辞めることを決めました。
(開発チームの内訳には自分も含まれています)

実際には以下のようなことをやった。

  • スプリントの成果を個人ではなくチームの成果であることを意識した

    • デイリーで進捗を共有し相互にフォローをしあう
    • 達成した成果も達成できなかった成果も個人ではなくチームのもの
  • 開発への依頼は、個人へ宛ではなく開発チーム全体への依頼としてもらう

    • Slack で開発チーム全体にメンションしてもらうようにした
    • 差し込みのタスクもチーム全体のスケジュール影響することとして認識する
  • スプリントプランニングをチーム全員で考えるようにした

    • 実施するタスクを、指名性ではなく挙手制にし、やりたいことを優先する
  • 開発の進捗は全員で管理し、プロダクトオーナを含めた全員が見えるようにした

上記については自分が提案した内容もありますが、基本的にはスクラム開発のチームビルディングの一貫とて行ったことが多いです。
スクラムマスターとしてチームビルディングを実施してくれた、同僚に感謝です。

PjM辞めた結果

何も変わらなかった

辞めて1ヶ月ほど現状としては、自分がPjM辞めた前と後では特に変化はなく、チームも問題なく回っている状況です。
新しい問題が発生しても、基本的にはチームとして対処していく形です。

無くして良かった というよりは、 無くしても問題なかった という結果でした。

まとめ

PjMは必要ない という話ではなく、今の開発チームはPjM必要ない という状態にチームがなったというお話です。

状況やチームの編成が変わればまた必要になるかもしれないし、新しい役割が必要になるかもしれません。
特定の役割がなくなっても、自然とチームが回るのはチームとして成長し、より良チームに近づけたのかなと思います。

PHP 8.0 の設定ファイル php.ini-development と php.ini-production の違い

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

www.ritolab.com


PHP には php.ini という設定ファイルがありますが、これを作成する元となる設定ファイルには本番用と開発用の2つが存在しています。

今回はこの2つの php.ini の違いを見ていきたいと思います。

環境

PHP 8.0 の環境で見ていきます。

$ php  -v
PHP 8.0.0 (cli) (built: Dec 17 2020 08:54:56) ( NTS )
Copyright (c) The PHP Group
Zend Engine v4.0.0-dev, Copyright (c) Zend Technologies

php.ini

php.ini は PHP の設定ファイルの事で、PHP の動作に関する様々な設定をここに記述することができます。

設定ファイル
https://www.php.net/manual/ja/configuration.file.php

php.ini がどこに設置されているのかは以下のコマンドを叩くと確認する事ができます。(環境によって設置箇所は異なります)

# php.ini の場所を確認する
php --ini

# 実行結果
$ php --ini
Configuration File (php.ini) Path: /usr/local/etc/php
Loaded Configuration File:         /usr/local/etc/php/php.ini
Scan for additional .ini files in: /usr/local/etc/php/conf.d

本番用と開発用の php.ini

php.ini が設置されているディレクトリに php.ini-development と php.ini-production が存在しています。

/usr/local/etc/php
├── conf.d
├── php.ini
├── php.ini-development
└── php.ini-production

この2つのファイルを基に php.ini を作成するわけですが、これらのファイルについて、具体的にどこの設定項目がどう違うのでしょうか。

その違いを見ていきます。

余談ですが php.ini が無い場合はその項目ごとにデフォルト値が決まっていて、デフォルト値が適用されます

php.ini-development と php.ini-production の違い

2つのファイルの差分を取り、設定の違いを確認していきます。

f:id:ro9rito:20201228080006p:plain

zend.exception_ignore_args

例外用に生成されたスタックトレースに引数を含めるかどうかの設定。

  • 本番環境ではこの設定をオンにしてスタックトレースで機密情報を出力しないようにすることが推奨されている。

  • デフォルト:Off
  • 開発用:Off
  • 本番用:On

-zend.exception_ignore_args = Off
+zend.exception_ignore_args = On

開発用では引数を出力するようになっているが、本播用では出力しない設定になっている

zend.exception_ignore_args
https://www.php.net/manual/ja/ini.core.php#ini.zend.exception-ignore-args

zend.exception_string_param_max_len

文字列化されたスタックトレースの引数の最大文字列長を 0 から 1000000 までの範囲で値を設定できる。

  • zend.exception_ignore_args が有効になっている場合、ここは無視される。
  • 本番環境ではこれを 0 に設定してスタックトレースでの機密情報の出力を減らすことが推奨されている。

  • デフォルト:15
  • 開発用:15
  • 本番用:0

-zend.exception_string_param_max_len = 15
+zend.exception_string_param_max_len = 0

開発用では最大文字列長が 15 文字になっていて、本番用では 0 になっている。 (本番用は zend.exception_ignore_args = On なのでそもそも使われない)

ちなみにこれは PHP 8.0 で新たに追加されたディレクティブです。(RFC

error_reporting

エラー出力レベルを設定できる。

-error_reporting = E_ALL
+error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT

開発用では全てのエラーと警告を出力するようにしているが、本番用では E_DEPRECATED と E_STRICT 以外のエラーや警告が出力されるように設定されている。

エラー出力レベルについては以下を参照ください

www.ritolab.com

error_reporting

https://www.php.net/manual/ja/errorfunc.configuration.php#ini.error-reporting

display_errors

PHP がエラーや通知、警告を表示するかどうかを設定する。

  • エラー表示は開発時には役に立つが、本番環境では機密情報が漏れるかもしれないので非表示にする事が推奨されている。
  • 本番環境では、エラーは STDOUT に送信するのではなく、ログに記録することが推奨されている。

  • デフォルト:On
  • 開発用:On
  • 本番用:Off

-display_errors = On
+display_errors = Off

開発用ではエラーを表示するが、本番用では表示しない設定になっている。

display_errors

https://www.php.net/manual/ja/errorfunc.configuration.php#ini.display-errors

display_startup_errors

PHPの起動シーケンス中に発生するエラーの表示は display_errors とは別に処理されるため、こちらで表示有無を設定する。

  • 構成の詳細が漏洩しないように本番環境ではオフにする事が推奨されている。

  • デフォルト:On
  • 開発用:On
  • 本番用:Off

-display_startup_errors = On
+display_startup_errors = Off

開発用ではエラーの表示が行われるのに対し、本番用では表示されない設定になっている。

display_startup_errors

https://www.php.net/manual/ja/errorfunc.configuration.php#ini.display-startup-errors

mysqlnd.collect_memory_statistics

mysqlnd によるメモリ使用統計の収集を行うかの設定。(MySQLのチューニングやモニタリングに使える)

-mysqlnd.collect_memory_statistics = On
+mysqlnd.collect_memory_statistics = Off

開発用では有効になっているのに対し、本番用では無効になっている。

mysqlnd.collect_memory_statistics

https://www.php.net/manual/ja/mysqlnd.config.php#ini.mysqlnd.collect-memory-statistics

zend.assertions

PHP が実行コードを生成する際にアサーションを含めるかの設定

設定値

  • -1:含めない
  • 0:含めるが実行時はアサーションをスキップする
  • 1:含める

  • 負の値から、または負の値への変更は、php.iniでのみ可能。
  • zend.assertions = 1 にした場合、実行時のアサーションのオン・オフの切り替えは assert.active で行える。

  • デフォルト値:1
  • 開発用:1
  • 本番用:-1

-zend.assertions = 1
+zend.assertions = -1

開発用はアサーションを含めるのに対して、本番用では含めない設定になっている。

zend.assertions

https://www.php.net/manual/ja/ini.core.php#ini.zend.assertions

assert.active

https://www.php.net/manual/ja/info.configuration.php#ini.assert.active

opcache.huge_code_pages

PHPコードを Huge Page にコピーするかどうかの設定。

  • Huge Page は Linux の機能で、メモリページングの際のチャンクサイズを増やしてパフォーマンスを向上させる機能。
  • Linux では PHP 7.0.0 以降、FreeBSD では PHP 7.4.0 以降が必要。
-;opcache.huge_code_pages=0
+;opcache.huge_code_pages=1

開発用では無効になっていて、本番用では有効になっている。

しかしどちらの環境でもコメントアウトされているので、opcache を有効化してここをチューニングする場合はコメントアウトを外してあげる必要がある。

opcache.huge_code_pages

https://www.php.net/manual/ja/opcache.configuration.php#ini.opcache.huge_code_pages

まとめ

開発時にはデバッグがしやすいように、そして本番では不要な情報の露出や無駄なリソースの消費がないように、それぞれの ini ファイルで設定されていましたね。

あなたが作っているサービスの本番環境の php.ini は、本当に本番用を元に作成されていますか?

見直してみても良いかもしれません。

ちなみに、php.ini を php.ini-development もしくは php.ini-production のどちらかから作成した場合は About this file セクションを見ると以下のように表記されているので、それをみればどちらから生成したのかがわかると思います。

# php.ini-development から作成
; This is the php.ini-development INI file.

# php.ini-production から作成
; This is the php.ini-production INI file.

GitHub ActionsにCronがあると聞いたんだ

自己紹介

agent bank開発チームのhironekoです。 個人ブログは、こちらです。

腹筋ローラーを初めて以降、筋肉痛なのか、姿勢の悪さからのコリなのかわからない苦しみが起きています。

あとウォーキングデットやゴールデンカムイを見ていついかなる状況になっても生存しなくてはいけないと感じてキャンプに興味を持ちました。

さて今回の目的

GitHub ActionsのCronで何すっかって話なのですが

最近に限ったことではなくて、僕は、数字の4桁以上を覚えるのがなかなか苦手なレベルの脳内メモリ所有者なのですが

来期のアニメの見なくてはいけないタイトルが10タイトルくらいあり、放送日時を把握し切れる訳がないじゃないか!って危機的状況に陥っている訳です。

仮にリアタイで見れなくてもアマプラさんで解決する部分もあったり絶命するわけではないのですが、個人的にHPが2割以下くらいになって日々のモチベーションがダダ下がりするのだろうと想定されるのです。

なのでよしGitHub Actionsの学習がてら毎朝か業後くらいの時間に通知行くようにしようぜ!

なんならPythonでやってみようぜ!

って昨日の夜中に思い立ったのでやってみたいとおもいます。

補足

我が家は、録画機能を有した何かしらの文明機器を持ち合わせていません。

前提

ゴール

最終的に以下のようにslackへ放送日に通知が行くようにします。 f:id:hironekosun:20201223154138p:plain

実装:Python

コードを先に晒します。

import json
import slackweb
from os import getenv
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

data = json.load(open(f'./data/anime/{today.year}{month}.json', 'r'))

slack = slackweb.Slack(url=getenv('SLACK_WEBHOOK_URL'))

for v in data:
    if v['day_of_week'] == dayName:
        attachments = [
           {
               "fallback": 'アニメの放送時間のご案内',
               "pretext": '本日放送のアニメ',
               "fields": [
                   {
                       "title": 'タイトル',
                       "value": v['title'],
                   },
                   {
                       "title": '放送時間',
                       "value": v['publish_at'],
                       "short": "true"
                   },
                   {
                       "title": 'チャンネル',
                       "value": v['channel'],
                       "short": "true"
                   }
               ]
           } 
        ]
        slack.notify(attachments=attachments)

解説

  • 特別語ることは少ないです。

  • slackweb

こちらのライブラリが一番手っ取り早く簡単そうだったので使いました。

所感としては、ほんと単純なコードで通知することが可能なので誰でも迷わず実装が行えると思います。

  • getenv

getenvを使用して環境変数を取得してslackのweb hook urlをコードにハードコードしないようにします。

  • 全体

読めばわかるとは思うのですが、予めJSONのfileにデータを入れておきそのデータを元に今日放送をするアニメかどうかを判定して通知を行っています。

.
├── attachments.json
├── data
│   └── anime
│      ├── 202001.json
│      └── 202012.json
├── read.py
└── requirements.txt
  • JSON 下記の形式で通知対象にしたい情報を入れておきます(ここはスクレイピングでどうにかしたい)

file名は、必ずyyyymm.jsonとします。

[
    {
        "title": "転生したらスライムだった件",
        "publish_at": "23:00",
        "channel": "TOKYO MX",
        "day_of_week": "Tuesday"
    },
    ....
]

実装:GitHub Actions

  • こちらも先にコードを晒します。
on: 
  schedule:
    - cron: '55 0 * * *'

jobs:
  slack:
    name: Run slack
    runs-on: ubuntu-latest
    env:
      SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Set up Python 3.8
        uses: actions/setup-python@v1
        with:
          python-version: 3.8
      - name: pip install
        run: pip install -r requirements.txt
      - name: post
        run: python read.py
  • cron

注意点として5分以下は動かないっぽいのとdefault branch以外は、cronの対象にならないとのこと

またこのcronが結構いい加減なので5分くらいのズレは、デフォです。

なので今回実行時間を10時に実行されるといいなーって気持ちでUTC 00:55 = 日本時刻 09:55 に設定しました。

GitHub Actionsよろしく!

  • secrets

今回slackのweb hook urlを使いますのでコードにハードコードしたくなく、外に晒すのもなーっていう気持ちなのでGitHubの機能を使用しました

Settings > Secretsで設定ページへ遷移します。New repository secretボタンを押下し追加します。

使い方は${{ secrets.KEY_NAME }}となります。

今後

漫画やアニメが好きなんですが既存の世に出ているサービスでは、僕のやってほしいことを満たすサービスが1mmもないのでこれを気に作ってやるんだから!って気持ちがふつふつと湧いてきました。

年末年始、時間があるようでないようなものですが積読している書籍読みつつ、開発していきたいなって気持ちです。

最後に独り言

グーカレでよくね?という意見は、受け付けますん。すみませんブログネタ考えたあとに振り返ったらグーカレの定期でよくねって考えに至りました。

"OK Google" + Nature Remo で部屋の照明をコントロールする

ROXX エンジニアの匠平 (@show60)です

新型コロナの感染防止対策のため、弊社エンジニアメンバーも多分に漏れず自宅作業がメインとなっています。

せっかく長く過ごす自宅環境なのでもっと楽しくしていこうと思い、Nature Remo の API で遊んでみました。

経緯

もう2年前になりますが、ROXX 社内の LT 大会で1位をいただき、賞品にスマートリモコン Nature Remo をいただきました。

家電をアプリでコントロールできるという代物なんですが、当時持っていた家電が古すぎてほとんど使えていませんでした。家時間が増えたことも相まって、今年になってエアコンと照明を新調したため対応家電が増えて遊べる環境が整ってまいりました。

今回はスマホで「OK Google, 休憩入ります」と話すと部屋の照明を消してくれる遊びをやってみます。

環境

  • Android スマートフォン (Pixel3)
  • Nature Remo (第二世代)
  • 照明
  • IFTTT
  • AWS API Gateway

Nature Remo の設定、照明との連携は先に済ませておきます。

想定する動作

Google Assistant での音声リクエストから消灯までの流れは以下のようになります。

  1. Google Assistant で Webhook を叩く。
  2. Webhook が AWS API Gateway にリクエスト。
  3. AWS API Gateway が Nature Remo の Web API を叩く。
  4. Nature Remo が作動して照明を操作する。

準備

Nature Remo

Nature Remo の利用を開始する際にアカウントを作成します。以下の URL から同じアカウントでログインし、アクセストークンを発行します。

アクセストークンは再表示できないので忘れずにメモっておきます。

https://home.nature.global/

Nature Remo の OpenAPI 情報はこちらにまとまっています。

https://developer.nature.global/

家電の ID を取得してみます。

エンドポイントは https://api.nature.global/1/appliances で、先ほどのアクセストークンを付けてリクエストします。

f:id:show-hei:20201223030429p:plain

登録されている家電の情報を取得してきました。アイリスオーヤマの照明であることが分かりますね。

ここで表示される家電の ID をメモっておきます。また、家電の操作の情報も記述されているので確認しておきます。

[
    {
        "id": "{家電 ID}",
        "device": {},
        "model": {},
        "light": { // ① 照明として登録されている場合、このキー名になっている
            "buttons": [
                {
                    "name": "on", // ②
                    "image": "ico_on",
                    "label": "Light_on"
                },
                {
                    "name": "off", // ②
                    "image": "ico_off",
                    "label": "Light_off"
                },
                {
                    "name": "on-100", // ②
                    "image": "ico_light_all",
                    "label": "Light_all"
                },,,,
            ]
        }
    }
]

照明を操作するエンドポイント URL は https://api.nature.global/1/appliances/{家電 ID}/①?button=② のような構成になります。

今回の場合だと https://api.nature.global/1/appliances/{家電 ID}/light?button=off となります。

接続している家電の情報を取得するだけでなく、それらの家電を操作する API も存在しており、今回は照明を消灯する操作の API を使用することにします。

API Gateway

AWS API Gateway から [API を作成] > REST API [構築] を選択します。任意の API 名を入力し作成します。

※ 今回の API は IFTTT で使うのみで他者には共有しないため、プライベート (VPC からのみアクセス可) にはしていません。

照明の操作は POST リクエストのため、[リソース] > [アクション] から POST メソッドを選択します。

メソッドの設定は以下のように行います。

  • 統合タイプ: HTTP
  • HTTP メソッド: POST
  • エンドポイント URL: https://api.nature.global/1/appliances/{家電 ID}/light?button=off
  • HTTP ヘッダー: Authorization : Bearer {アクセストークン}

設定したら再度 [リソース] > [アクション] を開き、[API のデプロイ] を選択します。デプロイするステージなど適宜入力すると API の完成です。

IFTTT

API Gateway で作った URL を IFTTT の Webhook に叩かせましょう。

IFTTT の create から新規の Applet を作っていきます。

ifttt.com

以下が Applet の作成手順になりますが、シンプルな UI であまり迷わずに直感的に作ることができると思います。

Applet 作成: "If This" 項目

  1. If ThisGoogle Assistant を選択。
  2. trigger として Say a simple phrase を選択。
  3. trigger 設定: What do you want to say?休憩入ります と入力。(句読点は入れないよう注意)
  4. trigger 設定: What do you want the Assistant to say in response?ゆっくり休んでね と入力 (ここは空欄でも可)
  5. trigger 設定: Language で Japanese を選択し、Create trigger をクリック。

Applet 作成: "Then That" 項目

  1. Then ThatWebhooks を選択。
  2. action として Make a web request を選択。
  3. web request 設定: URL に API Gateway で作成した API を入力。
  4. web request 設定: Method -> POST, Content Type -> application/json を選択し、Create action をクリック。

完成

以上の工程で完成となります。

手持ちのスマホや Google home などで「OK Google, 休憩入ります」と話せば「ゆっくり休んでね」と応えてくれて消灯までしてくれるようになりました。

さいごに

Nature Remo を声で動かすだけであればシンプルに IFTTT だけで実装できますが、そこから伝搬して他の処理につなげていけるのは自分で実装する楽しさでもあります。

ここから拡張して休憩時間の終わりになると、アラーム代わりに照明を点けてくれて、ついでに Spotify API 経由で音楽を流してくれる、みたいにすると午後も楽しくお仕事できそうです。

Laravelの環境構築が一瞬で終わった

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

Laravelの環境構築をしようとしたら、一瞬で終わって感動したという感想メモです。
公式サイトに書いてあること以上のことは出ません。

環境構築手順

今までは、Laravelの環境構築のために、

  1. PHPを入れる
  2. Laravelコマンドを入れる
  3. Laravelをインストールする
  4. データベースとかも準備する
  5. 必要であればnodeやらyarnやらも準備する
  6. ローカルが汚れないように仮想環境を準備する

みたいな面倒なやつが必要でした。
そして、ちょいわけあって新規の環境を用意する必要があり、「面倒だなー」と思いながら環境構築をしたのですが、一瞬で終わってびっくりしました。

今やLaravelの環境構築は3ステップで終わります。

  1. Docker for Mac を入れる
  2. curl -s https://laravel.build/example-app | bash
  3. ./vendor/bin/sail up

.env.example のコピーや artisan key:generate は必要ありません。
composer install も不要です。
PHPを入れる必要すらありません。

この sail がLaravel8から入った新機能のようでした。

できる環境

Laravelの開発環境を作るライブラリのようです。

デフォルトでは

  • PHP8.0
  • MySQL8.0
  • Redis
  • MailHog
  • node ( npm )

の環境が出来上がります。
デフォルトではコメントアウトされていますが、Laravel Dusk のために selenium のコンテナも建てられます。
ちなみにPHPバージョンは7.4でも作れるようです。

dockerがわかる人であれば、 docker-compose.yml を読めば「ああなるほどね」となると思います。

操作体系

docker環境となると、「dockerわからん人どうするんじゃい!」という話が上がりそうですが、そこらへんもわかりやすくまとまってました。
./vendor/bin/sail がエントリポイントとなり、色々とできるようです。

やりたいこと 今までのコマンド sailでのコマンド
環境立ち上げ docker-compose up / php artisan serve sail up
環境破棄 docker-compose down / ( serveを終了する ) sail down
PHPコマンド php info.php sail php info.php
Composer操作 composer update sail composer update
npm操作 npm i sail npm i
Artisanコマンド php artisan make:model User sail artisan make:model User
Laravel Dusk php artisan dusk sail dusk
Laravel Tinker php artisan tinker sail tinker
Shellコマンド docker-compose exec sh sail shell

だいたいのことはできそうです。 ./vendor/bin/sail が起点になるので、エイリアスシンボリックリンクを用意すると良いと思います。

まとめ

個人開発でサクッと環境構築するにはとても便利そうです。
プロダクションで使用する場合は、もうちょっとコンテナの中を覗いたりする必要がありそうです。