AWS Cloudfront をL7スイッチで使うならTerraform0.12 Dynamicが便利

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

kotamat.com

こんにちは、 kotamat です。

Terraform 0.12がでてしばらく立ちますが、先日構築したCloudfrontの環境において、Terraform0.12のdynamicを使わないと実現できない事案が発生したため、この際にと思い、一気に0.12にバージョンアップしました。

マイグレーション方法とかは、terraform側が提供しているものを使えば80%くらいはやってくれるので(一部手動で修正が必要ですが、WARNING出してくれるのでポチポチやっていくだけです。)今回は主題のdynamicについて紹介します。

Dynamicとは

terraform 0.12で追加されたシンタックスのうちの一つです。 通常、トップレベルのリソースに関しては、一つ一つのリソースに名前をつけて記述したり、 count とかを使って繰り返しを記述していきますが、security groupcloudfront のようなネストされたリソースを記述する際には一つ一つ重複して書く必要がありました。

特にcloudfrontは機能が多く、befaviorに各パスごとのoriginを書くことによってL7スイッチになることができるため、設定が繰り返し記述になりがちであり、かつネストが多い非常に複雑な構造になっているため、dynamicの恩恵を受けることができます。

記述方法

今回は

slides.com

で紹介した形を例に考えてみます。

各種スイッチの向き先の対照表は下記になっています。

ディレクト オリジンの向き先
/api/* API
/nova/* nova
/nova-assets/* nova
/nova-api/* nova
/vendor/nova/* nova
default S3(Nuxt.js)

今までであれば、/apiのブロックを一つ、novaのブロックを4つ、deafultのブロックを一つ書く必要がありましたが、dynamicを用いることによって、defaultとordered一つのブロックですべての表現をすることができます。

resource "aws_cloudfront_distribution" "main" {
// api
  ordered_cache_behavior {
    allowed_methods = [
      "GET",
      "HEAD",
      "OPTIONS",
      "POST",
      "PUT",
      "PATCH",
      "DELETE",
    ]

    cached_methods = [
      "GET",
      "HEAD",
    ]

    default_ttl = 0

    forwarded_values {
      cookies {
        forward = "all"
      }

      headers = ["*"]

      query_string = true
    }

    max_ttl                = 0
    min_ttl                = 0
    target_origin_id       = local.api_origin_id
    viewer_protocol_policy = "redirect-to-https"
    path_pattern           = "/api/*"
  }
// nova
  ordered_cache_behavior {
    allowed_methods = [
      "GET",
      "HEAD",
    ]

    cached_methods = [
      "GET",
      "HEAD",
    ]

    default_ttl = 0

    forwarded_values {
      cookies {
        forward = "all"
      }

      headers = ["*"]

      query_string = true
    }

    max_ttl                = 0
    min_ttl                = 0
    target_origin_id       = local.nova_origin_id
    viewer_protocol_policy = "redirect-to-https"
    path_pattern           = "/nova/*"
  }
// 以下 nova-assets/*とかも記述していく

locals {
  path_patterns = [
    "/api/*",
    "/nova/*",
    "/nova-assets/*",
  ]
}
resource "aws_cloudfront_distribution" "main" {
  dynamic "ordered_cache_behavior" {
    // 何を繰り返すのかをfor_eachで指定する
    for_each = locals.path_patterns
    content {
      allowed_methods = [
        "GET",
        "HEAD",
        "OPTIONS",
        "POST",
        "PUT",
        "PATCH",
        "DELETE",
      ]

      cached_methods = [
        "GET",
        "HEAD",
      ]

      default_ttl = 0

      forwarded_values {
        cookies {
          forward = "all"
        }

        headers = ["*"]

        query_string = true
      }

      max_ttl                = 0
      min_ttl                = 0
      target_origin_id       = local.api_origin_id
      viewer_protocol_policy = "redirect-to-https"
      path_pattern           = ordered_cache_behavior.value // dynamicで指定したリソース名 + .valueでpathの中身を取得
    }
  }
}

他の使いみち

上記はbehaviorの設定をまとめるために使ってみましたが、例えばステージング環境は特定のパスだけBasic認証いれたいという要件が来たときには、今までだとlambda_function_associationだけ違う別リソースを記述する必要がありましたが、mapと組み合わせる事によって、下記のように同一ソースで記述することができます。

locals {
  lambda_associations = {
    "stg": {
      // stg特有のもの(Basic認証とか)
      {
        event_type = "viewer-request"
        lambda_arn = module.viewer_request.qualified_arn
      },
      // prodと共通のもの
      {
        event_type = "origin-request"
        lambda_arn = module.origin_request.qualified_arn
      },
    },
    "prod": {
      {
        event_type = "origin-request"
        lambda_arn = module.origin_request.qualified_arn
      }
    }
  }
}

resource "aws_cloudfront_distribution" "main" {
  default_cache_behavior {
    dynamic "lambda_function_association" {
      for_each = local.lambda_associations[terraform.workspace] // workspaceの値を取得し、動的にassociationを切り替える
      content {
        event_type = lambda_function_association.value.event_type
        lambda_arn = lambda_function_association.value.lambda_arn
      }
    }
  }
}

まとめ

terraform 0.12 dyanmicの使い方を、Cluodfrontを用いて紹介させてもらいました。 特に後者の、lambda edgeとの組み合わせは非常に使い勝手がいいので、よかったら試してもらえればと思います。

macOS Catalinaでもphpenv使いたかったのでdefault_configure_optionsいじったりPHP_BUILD_CONFIGURE_OPTS指定したりした

techblog.roxx.co.jp の続きです。

前回はphpenv, php-buildを諦めてしまいましたがいろいろ設定を調査して再挑戦です。

github.com

default_configure_options を調整する

php-buildには default_configure_options というファイルがあり、こちらに configure で利用するオプションを記載しています。 $PHPENV_ROOT/plugins/php-build/share/php-build/default_configure_options をお好みのエディターで編集し、前回エラーが出ていたzlib, bzip2, iconv のオプションを調整します。

default_configure_optionsはこのようになりました。 gist.github.com

それぞれビルド用にHomebrewでインストールしておきましょう。

brew install bzip2 libiconv zlib

これで再度php7.2.22のインストールに挑戦してみましたが libedit を再インストールしろというエラーが起きてしまいました。くやしい。

$ phpenv install 7.2.22
[Info]: Loaded extension plugin
[Info]: Loaded apc Plugin.
[Info]: Loaded composer Plugin.
[Info]: Loaded github Plugin.
[Info]: Loaded uprofiler Plugin.
[Info]: Loaded xdebug Plugin.
[Info]: Loaded xhprof Plugin.
[Info]: Loaded zendopcache Plugin.
[Info]: php.ini-production gets used as php.ini
[Info]: Building 7.2.22 into /Users/jiska/.anyenv/envs/phpenv/versions/7.2.22
[Downloading]: https://secure.php.net/distributions/php-7.2.22.tar.bz2
(中略)

-----------------
|  BUILD ERROR  |
-----------------

Here are the last 10 lines from the log:

-----------------------------------------
configure: error: Please reinstall libedit - I cannot find readline.h
-----------------------------------------

とりあえずlibeditをインストールしてdefault_configure_options に --libedit を指定して再実行しましたが状況が変わりませんでした。

brew install libedit
$ phpenv install 7.2.22
(中略)

-----------------
|  BUILD ERROR  |
-----------------

Here are the last 10 lines from the log:

-----------------------------------------
configure: error: Please reinstall libedit - I cannot find readline.h
-----------------------------------------

くやしい。もうちょっと原因調べないとダメです。

PHP_BUILD_CONFIGURE_OPTS を指定する

php-build のコードを読んでいて気づいたのですが、macOSの場合は --libedit を上書きしてしまっているようです。

https://github.com/php-build/php-build/blob/a73138438907cc2cfefc8279102ccba234c85369/bin/php-build#L621-L627:該当箇所へのリンク

    if is_osx; then
        configure_option -D "--with-gettext"
        configure_option -D "--with-readline"
        configure_option "--with-libedit"

        configure_option -R "--with-png-dir" "/usr/X11"
    fi

そして、envに PHP_BUILD_CONFIGURE_OPTS を指定すればconfigureにオプションとして反映してくれるようです。

https://github.com/php-build/php-build/blob/a73138438907cc2cfefc8279102ccba234c85369/bin/php-build#L135:PHP_BUILD_CONFIGURE_OPTSが存在しなければ空文字で初期化している

[ -z "$PHP_BUILD_CONFIGURE_OPTS" ] && PHP_BUILD_CONFIGURE_OPTS=""

https://github.com/php-build/php-build/blob/a73138438907cc2cfefc8279102ccba234c85369/bin/php-build#L652:指定したoptionsは最終的にマージされる

    CONFIGURE_OPTIONS="$CONFIGURE_OPTIONS $PHP_BUILD_CONFIGURE_OPTS $CONFIGURE_OPTS"

envにlibeditの設定を指定して再度実行してみたところ、ようやくbuildすることができました!ビルドを試したのは7.2.22, 7.2.23, 7.3.12 です。

# 7.2.22
$ PHP_BUILD_CONFIGURE_OPTS="--with-libedit=/usr/local/opt/libedit" phpenv install 7.2.22
(中略)
Generating phar.php
Generating phar.phar
PEAR package PHP_Archive not installed: generated phar will require PHP's phar extension be enabled.
directorytreeiterator.inc
clicommand.inc
directorygraphiterator.inc
invertedregexiterator.inc
pharcommand.inc
phar.inc

Build complete.
Don't forget to run 'make test'.
(中略)

[xdebug]: Installing xdebug configuration in /Users/jiska/.anyenv/envs/phpenv/versions/7.2.22/etc/conf.d/xdebug.ini
[xdebug]: Cleaning up.
[Info]: Enabling Opcache...
[Info]: Done
[Info]: The Log File is not empty, but the Build did not fail. Maybe just warnings got logged. You can review the log in /tmp/php-build.7.2.22.20191127024529.log or rebuild with '--verbose' option
[Success]: Built 7.2.22 successfully.

# 7.3.12 も大丈夫だった
PHP_BUILD_CONFIGURE_OPTS="--with-libedit=/usr/local/opt/libedit" phpenv install 7.3.12

まとめ

  • default_configure_optionsを調整しよう
  • ビルドに必要なものをインストールしておこう
    • brew install bzip2 libedit libiconv zlib
  • PHP_BUILD_CONFIGURE_OPTSに --with-libedit を追加してから phpenv install しよう
    • PHP_BUILD_CONFIGURE_OPTS="--with-libedit=/usr/local/opt/libedit" phpenv install 7.3.12

最後に

株式会社ROXXでは一緒に agent bankback check を作っていくメンバーを随時募集しています。 この記事を読んでROXXに興味を持ってくれた方はぜひご応募ください。   www.wantedly.com

MySQLのDockerイメージをイチから作成する

backcheck事業部の前田です。
この記事は個人ブログと同じ内容になります。

toyo.hatenablog.jp

わけあって、公式のものとは別にMySQLのDockerイメージを作成しました。
この記事はその備忘録です。

公式のイメージはデータ保存の部分がVOLUMEマウントされているので、イメージの中にデータを含められないからです。

公式イメージではデータを保存できない

Docker環境でMySQLを利用したい場合はMySQLの公式イメージを使用するのが一番ラクで便利です。
ですが、公式イメージではデータを保存しておくことができません。 MySQLではデータを /var/lib/mysql というディレクトリ内に保存するのですが、このディレクトリがVOLUMEマウントの対象になっているためです。

「データをdockerイメージに含めるのはおかしい」という話はもちろんあるのですが、それでも含めたいときがあるじゃないですか。。。というお話です。

試行錯誤してみた

自分で色々とやってみて、たどり着いたのが以下でした。

Dockerfile

FROM ubuntu:18.04

ENV TZ Asia/Tokyo
ENV DEBIAN_FRONTEND noninteractive

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        mysql-server \
        tzdata \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# timezone
WORKDIR /opt
RUN mysql_tzinfo_to_sql /usr/share/zoneinfo > timezone.sql

# mysql
RUN mkdir -p /var/run/mysqld \
    && chown mysql:mysql /var/run/mysqld \
    && usermod -d /var/lib/mysql mysql \
    && mv /etc/mysql/conf.d/mysql.cnf /etc/mysql/conf.d/00_mysql.cnf \
    && mv /etc/mysql/conf.d/mysqldump.cnf /etc/mysql/conf.d/01_mysqldump.cnf \
    && mv /etc/mysql/mysql.conf.d/mysqld.cnf /etc/mysql/mysql.conf.d/00_mysqld.cnf \
    && mv /etc/mysql/mysql.conf.d/mysqld_safe_syslog.cnf /etc/mysql/mysql.conf.d/01_mysqld_safe_syslog.cnf \
    && sed -i -e "s~log_error~# log_error~g" /etc/mysql/mysql.conf.d/00_mysqld.cnf \
    && sed -i -e "s~bind_address~# bind_address~g" /etc/mysql/mysql.conf.d/00_mysqld.cnf

COPY original_mysql.cnf /etc/mysql/conf.d/02_original_mysql.cnf
COPY original_client.cnf /etc/mysql/conf.d/03_original_client.cnf
COPY original_mysqld.cnf /etc/mysql/mysql.conf.d/02_original_mysqld.cnf

COPY entrypoint.sh entrypoint.sh
RUN chmod +x entrypoint.sh

EXPOSE 3306

ENTRYPOINT ["./entrypoint.sh"]

entrypoint.sh

#!/bin/sh

set -e

find /var/lib/mysql -type f -exec touch {} \;

if [ ! -f timezone_applied ];then
  mysqld &
  sleep 3
  mysql -uroot -D mysql < timezone.sql
  mysqladmin shutdown -uroot
  touch timezone_applied
fi

mysqld

ひとつずつ解説していきます。

Dockerfile

FROM ubuntu:18.04

コンテナと言えばAlpine Linuxですが、Alpineのパッケージ管理ツール(apk)では、デフォルトではMySQLをインストールできません。
MySQL互換のMariaDBならインストールすることができますが、MariaDBは認証まわりがMySQLとだいぶ違うため、今回は見送りました。
Alpine以外なら何でも大丈夫だと思います。Ubuntuは私の趣味です。

ENV TZ Asia/Tokyo

TZ というタイムゾーンを指定することで、MySQLタイムゾーンの指定をすることができます。1

ENV DEBIAN_FRONTEND noninteractive

RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        mysql-server \
        tzdata \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

MySQLとtzdataをインストールしています。
tzdataはタイムゾーンの情報が入ったパッケージです。
MySQLではデフォルトではタイムゾーンの情報が入っていないため、このtzdataをベースにタイゾーン情報を入れていきます。

また、tzdataをインストールする際に対話シェルが開始されます。
Dockerイメージをビルドする際に対話シェルが開始されると処理が止まってしまうので、 DEBIAN_FRONTEND を指定して対話シェルが開始されないようにしています。

apt使用後はaptに関するデータは不要になるので、最後に使用されていないパッケージや更新されたリポジトリリストを削除しています。

WORKDIR /opt

ここからはファイルを生成したりしていくので、ルートディレクトリから移動をしています。
どこでもいいので、とりあえず /opt に設定しています。

RUN mysql_tzinfo_to_sql /usr/share/zoneinfo > timezone.sql

先ほどインストールしたtzdataの情報をSQLに変換しています。
このSQLはentrypointで使用します。

RUN mkdir -p /var/run/mysqld \
    && chown mysql:mysql /var/run/mysqld \

MySQLのデーモンがpidファイルを作成するディレクトリを作成しています。
また、Dockerイメージをビルドする際は基本的にrootユーザなので、ディレクトリのオーナーもmysqlに変更しています。

    && usermod -d /var/lib/mysql mysql \

mysqlユーザのホームディレクトリが設定されていない場合、 No directory, logging in with HOME=/ というエラーが発生するので、ここでmysqlユーザのホームディレクトリを設定しています。

    && mv /etc/mysql/conf.d/mysql.cnf /etc/mysql/conf.d/00_mysql.cnf \
    && mv /etc/mysql/conf.d/mysqldump.cnf /etc/mysql/conf.d/01_mysqldump.cnf \
    && mv /etc/mysql/mysql.conf.d/mysqld.cnf /etc/mysql/mysql.conf.d/00_mysqld.cnf \
    && mv /etc/mysql/mysql.conf.d/mysqld_safe_syslog.cnf /etc/mysql/mysql.conf.d/01_mysqld_safe_syslog.cnf \

Debian系のLinuxでは、設定をひとつのmy.cnfに書かずに、それぞれの設定をファイルに分割して書きます。 その際にファイル名の順番で読み込まれていきます。
読み込まれる設定の順番を管理するために、デフォルトの設定ファイルに連番のプレフィックスを割り振っています。

ちなみに、 /etc/mysql/mysql.conf.d/ がデーモン寄りの設定が入っており、 /etc/mysql/conf.d/ にクライアント寄りの設定が入っているようです。

    && sed -i -e "s~log_error~# log_error~g" /etc/mysql/mysql.conf.d/00_mysqld.cnf \
    && sed -i -e "s~bind_address~# bind_address~g" /etc/mysql/mysql.conf.d/00_mysqld.cnf

デフォルトの設定のいくつかを無効化(コメントアウト)しています。
Dockerではログファイルにログを吐かれても仕方がないので、 log_error を無効にしています。
また、Dockerコンテナを使う以上、外部ネットワークからの接続を期待するので、 bind_address を無効にしています。

COPY original_mysql.cnf /etc/mysql/conf.d/02_original_mysql.cnf
COPY original_client.cnf /etc/mysql/conf.d/03_original_client.cnf
COPY original_mysqld.cnf /etc/mysql/mysql.conf.d/02_original_mysqld.cnf

自分の作成した設定ファイルをコピーしています。

COPY entrypoint.sh entrypoint.sh
RUN chmod +x entrypoint.sh

自分で作成したエントリーポイントを使用したいため、コピーしてきています。
また、実行可能にするために実行権限を付与しています。

EXPOSE 3306
ENTRYPOINT ["./entrypoint.sh"]

MySQLなので3306番ポートを開き、エントリーポイントを設定しています。

entrypoint.sh

#!/bin/sh
set -e

set -e を指定すると、エラー発生時にスクリプトを止めてくれます。

find /var/lib/mysql -type f -exec touch {} \;

Dockerのストレージエンジン(overlay2)とMySQLの相性が悪いらしく、そのままではCan’t open and lock privilege tables: Table storage engine for ‘user’ doesn’t have this optionというエラーが発生します。
このコメントを参考にさせていただきました。

if [ ! -f timezone_applied ];then

このあとの処理で、タイムゾーンの設定が完了した際に timezone_applied というファイルを作成しています。
つまり、タイムゾーン設定が完了していない場合にタイムゾーンの設定を行っています。

  mysqld &
  sleep 3

mysqldを起動し、起動が完了するまで待機しています。
3秒なのは手抜きです。本当は起動完了しているかどうかを何かしらの方法でチェックする必要があります。

  mysql -uroot -D mysql < timezone.sql

MySQLタイムゾーンのデータをインポートしています。
timezone.sql はDockerfile内で作成しました。

  mysqladmin shutdown -uroot
  touch timezone_applied
fi

mysqldを終了し、 timezone_applied ファイルを作成しています。
これで2回目以降はタイムゾーンの設定がスキップされます。

mysqld

MySQLを実行します。実質のentrypointです。

まとめ

自分でMySQLのイメージを作成してみました。
公式のMySQLのDockerfileがめちゃくちゃ勉強になります。
最初は全く読めないので、こちらの記事がとても参考になりました。

...が意外に便利だった件

はじめまして。
今月からRoxxな人になった前田といいます。 よろしくおねがいします。

PHPでは...というキーワードがあります。

可変長引数と引数のアンパックです。

以下、例です。

<?php
// 可変長引数
function printArgs(...$inputs)
{
    echo $inputs[0]; // apple
    echo $inputs[1]; // banana
    echo $inputs[2]; // chocolate
}

printArgs('apple', 'banana', 'chocolate');
<?php
// 引数のアンパック
function printArgs($a, $b, $c)
{
// 略
}

$args = ['aaple', 'banana', 'chocolate'];
printArgs(...$args);

前から存在は知っていたのですが、わりと自分の中ではただのトリビアになっていて、あまり使用してきませんでした。
というより使用する場面をいまいち把握できていませんでした。

最近、個人で開発しているときに「あれ、意外と...は便利なのでは!?」となったので紹介します。

事例

以下の仕様のクラスを考えます。

  • ユーザ定義クラスStatusがある
  • Statusを複数まとめて扱うクラスがほしい
    • つまり、配列をラップしたクラス
  • 以下の要件を備える

...をつかわない場合

以下のようになると思います。

<?php
class StatusCollection
{
    /** @var Status[] */
    private $items;
    
    public function __construct(array $items)
    {
        foreach ($items as $item) {
            if (!$item instanceof Status) {
                throw new InvalidArgumentException();
            }
        }
        
        $this->items = array_values($items);
    }
}

$items = [Status::on(), Statuss::off(), Status::unknown()];
$statuses = new StatusCollection($items);

ポイントとしては、Status以外の要素がある場合は例外を投げていること、 array_values でキーを振り直しているところです。

こういったバリデーションに近い部分にコードを多く使うと読みづらいですね。

こういった読みづらさを...を使うと解消できます。

array_valuesを取り除く

まず array_values を消していきます。

<?php
class StatusCollection
{
    /** @var Status[] */
    private $items;
    
    public function __construct(...$items)
    {
        foreach ($items as $item) {
            if (!$item instanceof Status) {
                throw new InvalidArgumentException();
            }
        }
        
        $this->items = $items;
    }
}

$items = [Status::on(), Statuss::off(), Status::unknown()];
$statuses = new StatusCollection(...$items);

コンストラクタの仮引数を ... に変更しました。
これにより、キーの採番はPHPが勝手に行うようになるので、array_valuesが不要になります。
また、仮引数を可変長引数にしたので、実引数も忘れずに...でアンパックします。

型チェックのコードを取り除く

次に一番面積をとっているforeachを消していきます。

<?php
class StatusCollection
{
    /** @var Status[] */
    private $items;
    
    public function __construct(Status ...$items)
    {
        $this->items = $items;
    }
}

$items = [Status::on(), Statuss::off(), Status::unknown()];
$statuses = new StatusCollection(...$items);

単純に型宣言(タイプヒント)を使用しています。
...を使う前は配列を受け取っていたのでarrayで指定していましたが、...を使うことで配列の要素の型を指定できます。

面積をとっていたforeachを削除できたので、とても見通しの良いコードになりました。

まとめ

  • ...を使用することで、配列のキーを気にしなくて済む
  • ...を使用することで、配列の要素の型を指定できる

...は型宣言とセットで使用することでパワーを発揮しそうです。 「連想配列じゃなくてもいいんだけど・・・」という場合も結構あるので、配列のキーを気にしなくて良い場合は積極的に使っていこうと思います。

Laravel/Vue.js勉強会#11を開催しました!

株式会社 ROXX の匠平(@show60)です。

先日、10/28にLaravel/Vue.js 勉強会 (通称: Laravue 勉強会) を開催いたしました。

今回は、先月お引越しをされたFABRIC TOKYO様の新しいオフィスにお邪魔して開催いたしました。落ち着いたJazzが流れるキラキラした会場!

今回は Laravue 勉強会の模様を報告いたします!

f:id:show-hei:20191029203435j:plain

LT 登壇内容

スポンサー枠を含め、 6 名の方に登壇いただきました。

それぞれの登壇内容をさっくりとお伝えいたします。

FABRIC TOKYO での Laravel 開発アーキテクチャ

スポンサー枠はFABRIC TOKYOの高橋 (@ssth_jp) さんに登壇いただきました。

f:id:show-hei:20191029203359j:plain

よくある悩みを、チームにおけるクリーンアーキテクチャの解釈を行い解決するというお話をしていただきました。

Laravelのドキュメント通りではなく、DDD、クリーンアーキテクチャを取り入れることで未来の自分が助けられる。

全部を解釈するのは難しいけど、部分的に取り入れることでも恩恵が受けられるよというお話でした。

speakerdeck.com

Laravel で ParaTest を導入する

発表者は弊社のエンジニアのniisan-tokyo(@niisantokyo)です。

f:id:show-hei:20191029203452j:plain

弊社のagent bankというサービスをモノレポ化した際、テストが遅くなってしまったため、解決するためにParatestを導入したところ、Laravel 6.0.2 固有のバグを発見し、解決したという大変貴重なお話です

qiita.com

初めてのプロジェクト参加とDIのお話

同じく FABRIC TOKYO より真嶋 (@shio0594) さんの登壇です。

f:id:show-hei:20191029203520j:plain

初めてプロジェクトに参画された際に、DI(依存性注入)を分かりやすく噛み砕いて理解されたというお話です。

speakerdeck.com

異業種から転職され、サーバーサイドエンジニアとして FABRIC TOKYO で初めてのプロジェクトに入られたそうで、とても分かりやすいスライドと軽快なお話で会場を楽しませていただきました。

PhpStormからLaradock上のPHPUnitを動かしたら開発が捗った話

ザッキー(@ytzk_)さんの登壇です。

f:id:show-hei:20191029203542j:plain

PHPUnitテストが完了するまで時間がかかる、完了するまでエラー箇所が見れないのが辛いという課題に会場のみなさんも頷く、とても共感度の高いテーマでお話いただきました。

speakerdeck.com

詳しい設定方法はこちらの Qiita の記事にまとめていらっしゃいますのでこちらもぜひご覧ください!

qiita.com

Laravel 5.5から 6.4 にアップデートしたときに必要だった7つのこと

青ごへいもち(@blue_goheimochi)さんの登壇です。

f:id:show-hei:20191029203644j:plain

タイトルの通り、アップデートをされた際にどういうエラーが発生し、どうやって乗り越えたかというテーマでお話いただきました。

www.slideshare.net

アップデートでみんなハマりそうな箇所を一つずつ順を追って説明いただきました。分かりやすくまとめていただいているので、これからアップデートされる方には大変ためになる内容です。

過剰なほどに型安全なAxiosラッパー aspida

最後に m-mitsuhide(@m_mitsuhide)さんの登壇です。

f:id:show-hei:20191029203702j:plain

今回は6つの登壇枠の中、唯一のVue.jsのお話となりました。

JavaScriptの世界に存在するカーストのお話で会場を大いに沸かせ、ご自身が開発された aspida というモジュールの発表をしていただきました。

t.co

HTTP リクエストを型安全にし、フロントエンドの開発支援を行うツールです。

実際にライブコーディングでデモンストレーションをしていただき、来場者の方もとても興味深く見ていらっしゃいました。

懇親会

最後はスポンサーのFABRIC TOKYOさんにご用意いただき、参加者同士の懇親会を行いました。

f:id:show-hei:20191029203720j:plain

今後も Laravue 勉強会を行ってまいりますので、最新イベント情報がほしいという方はぜひ connpass のグループへご参加ください!

LaraVue勉強会 - connpass

さいごに

株式会社ROXXでは一緒にサービスを伸ばしていく仲間を募集をしております。

興味のある方は、是非下記からご応募お願い致します!

www.wantedly.com

www.wantedly.com

macOS Catalinaにアップデートしたらphpenv installでビルドエラーしたのでhomebrew phpつかうようにした

f:id:jiska_roxx:20191031010953j:plain:w200

こんにちは。開発部の森です。

先日MacBook Proが故障してしまったので修理に出したらクリーンインストールされて返却されたので、せっかくだから新しい環境作るかと思いmacOS MojaveからCatalinaにアップデートしたのです。

OSバージョン

10.15ですね。

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15
BuildVersion:   19A602

症状

phpenvphpのビルドが失敗するようになってしまいました。

以下はphp7.3.9をビルドしようと試みたターミナルの出力結果です。

$ phpenv install 7.3.9
[Info]: Loaded extension plugin
[Info]: Loaded apc Plugin.
[Info]: Loaded composer Plugin.
[Info]: Loaded github Plugin.
[Info]: Loaded uprofiler Plugin.
[Info]: Loaded xdebug Plugin.
[Info]: Loaded xhprof Plugin.
[Info]: Loaded zendopcache Plugin.
[Info]: php.ini-production gets used as php.ini
[Info]: Building 7.3.9 into /Users/jiska/.anyenv/envs/phpenv/versions/7.3.9
[Skipping]: Already downloaded and extracted https://secure.php.net/distributions/php-7.3.9.tar.bz2
[Preparing]: /var/tmp/php-build/source/7.3.9

-----------------
|  BUILD ERROR  |
-----------------

Here are the last 10 lines from the log:

-----------------------------------------
configure: WARNING: This bison version is not supported for regeneration of the Zend/PHP parsers (found: 2.3, min: 204, excluded: ).
configure: error: Cannot find zlib
-----------------------------------------

The full Log is available at '/tmp/php-build.7.3.9.20191029095924.log'.
[Warn]: Aborting build.

tmpに出力されたログには Cannot find zlib しか情報がありませんでした。

ログ全文はこちら https://gist.github.com/jiska/2be7ee5a8bf70d3dbf88e537b4537c0b#file-php-build-7-3-5-20191022215209-log

phpenvがダウンロードした /var/tmp/php-build/source/7.3.9 に出力された config.log を確認してみます。

config.log全文はこちら https://gist.github.com/jiska/2be7ee5a8bf70d3dbf88e537b4537c0b#file-config-log

エラーを抜粋すると、hファイルが参照できていないようです。

$ cd /var/tmp/php-build/source/7.3.9
$ grep 'fatal err' config.log
conftest.c:9:10: fatal error: 'ac_nonexistent.h' file not found
conftest.c:9:10: fatal error: 'ac_nonexistent.h' file not found
conftest.c:9:10: fatal error: 'ac_nonexistent.h' file not found
conftest.c:52:10: fatal error: 'minix/config.h' file not found
conftest.c:19:10: fatal error: 'minix/config.h' file not found
conftest.c:60:10: fatal error: 'sys/pstat.h' file not found
conftest.c:27:10: fatal error: 'sys/pstat.h' file not found
conftest.c:28:10: fatal error: 'sys/exec.h' file not found
conftest.c:40:10: fatal error: 'sys/prctl.h' file not found
conftest.c:47:12: fatal error: 'port.h' file not found
conftest.c:48:12: fatal error: 'sys/devpoll.h' file not found
conftest.c:47:12: fatal error: 'sys/epoll.h' file not found
conftest.c:48:10: fatal error: 'sys/apparmor.h' file not found
conftest.c:98:10: fatal error: 'crypt.h' file not found
conftest.c:101:10: fatal error: 'ieeefp.h' file not found
conftest.c:124:10: fatal error: 'sys/statfs.h' file not found
conftest.c:125:10: fatal error: 'sys/vfs.h' file not found
conftest.c:125:10: fatal error: 'sys/sysexits.h' file not found
conftest.c:125:10: fatal error: 'sys/varargs.h' file not found
conftest.c:126:10: fatal error: 'sys/loadavg.h' file not found
conftest.c:128:10: fatal error: 'unix.h' file not found
conftest.c:305:10: fatal error: 'io.h' file not found
conftest.c:272:10: fatal error: 'io.h' file not found

エラーは置いておいて手動でconfigure, makeしてみます。

# iconv指定しないとconfigure正常終了しないのでlibiconvもインストールしておく
$ brew install libiconv
$ ./configure --with-iconv=$(brew --prefix libiconv) 

configureは終了しましたがmakeが途中で失敗してしまいます。

$ make 
 (中略)
Undefined symbols for architecture x86_64:
  "_libiconv", referenced from:
      _php_iconv_string in iconv.o
      __php_iconv_strlen in iconv.o
      _zif_iconv_substr in iconv.o
      __php_iconv_strpos in iconv.o
      _zif_iconv_mime_encode in iconv.o
      __php_iconv_appendl in iconv.o
      _php_iconv_stream_filter_append_bucket in iconv.o
      ...
  "_libiconv_close", referenced from:
      _php_iconv_string in iconv.o
      __php_iconv_strlen in iconv.o
      _zif_iconv_substr in iconv.o
      __php_iconv_strpos in iconv.o
      _zif_iconv_mime_encode in iconv.o
      __php_iconv_mime_decode in iconv.o
      _php_iconv_stream_filter_factory_create in iconv.o
      ...
  "_libiconv_open", referenced from:
      _php_iconv_string in iconv.o
      __php_iconv_strlen in iconv.o
      _zif_iconv_substr in iconv.o
      __php_iconv_strpos in iconv.o
      _zif_iconv_mime_encode in iconv.o
      __php_iconv_mime_decode in iconv.o
      _php_iconv_stream_filter_factory_create in iconv.o
      ...
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [sapi/cli/php] Error 1

make失敗しました。これを修正しても他にもオプションを追加しないといけないので大変です。

手軽に複数バージョンのphpをインストール、ビルドしたかったのにこれだとつらいですね。

暫定対応

homebrewでphpをインストールする

phpenvをいったん諦め、 homebrewで提供されているphp を使うことにします。

パッチバージョンは指定できないですが(hash指定すればできるけど手軽でないので割愛)マイナーバージョンは指定できるのでなんとかなります。

# 2019-10-29現在 7.3.11 がインストールされる
$ brew install php
(後略)

$ /usr/local/opt/php/bin/php -v
PHP 7.3.11 (cli) (built: Oct 24 2019 11:29:00) ( NTS )
(後略)

# php@7.2 だと 7.2.24 がインストールされる
$ /usr/local/opt/php@7.2/bin/php -v
PHP 7.2.24 (cli) (built: Oct 25 2019 11:13:56) ( NTS )
(後略)

direnvでPATHを変更する

phpenvを使っているとディレクトリに .php-version ファイルを配置することで使用するphpを指定できますね。それに近い設定をするべく direnv でPATHを変更します。

direnvをインストール、.profileなどに設定を追加後にphpのバージョンを変更したいディレクトリに以下のような .envrc を配置し direnv allow . します。

$ cd path/to/my-project

# この例ではphp 7.2を指定している
$ cat .envrc
export PATH="/usr/local/opt/php@7.2/bin:$PATH"

$ direnv allow .

direnv: loading .envrc
direnv: export ~PATH

これでphpのバージョンが変更されます。

$ cd path/to/my-project

direnv: loading .envrc
direnv: export ~PATH

# 7.2.24 になった
$ which php
/usr/local/opt/php@7.2/bin/php

$ php -v
PHP 7.2.24 (cli) (built: Oct 25 2019 11:13:56) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.2.24, Copyright (c) 1999-2018, by Zend Technologies

# 他のディレクトリに移動するとphpのバージョンが標準のものにもどる
$ cd

direnv: unloading

$ which php
/usr/local/bin/php

$ php -v
PHP 7.3.11 (cli) (built: Oct 24 2019 11:29:00) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.11, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.3.11, Copyright (c) 1999-2018, by Zend Technologies

Xdebugをインストールする

開発には Xdebug が欠かせないので pecl でインストールします。

# peclのPATHも変更されている
$ which pecl
/usr/local/opt/php@7.2/bin/pecl

$ pecl install xdebug
(後略)

# Xdebugがインストールされた
$ php -v
PHP 7.2.24 (cli) (built: Oct 25 2019 11:13:56) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
    with Xdebug v2.7.2, Copyright (c) 2002-2019, by Derick Rethans
    with Zend OPcache v7.2.24, Copyright (c) 1999-2018, by Zend Technologies

これでphpの準備ができ、なんとか窮地を脱しました。

まとめ

  • phpをhomebrewでインストールした
  • direnvでPATH変更した
  • xdebugを追加した

Catalinaにバージョンアップした際には皆様もお気をつけください。

最後に

株式会社ROXXでは一緒に agent bankback check を作っていくメンバーを随時募集しています。 この記事を読んでROXXに興味を持ってくれた方はぜひご応募ください。   www.wantedly.com

Inversifyを使った、型堅牢なDIコンテナの構築

Inversifyを使った、型堅牢なDIコンテナの構築

こんにちは、 kotamatです。 新マイクロサービスのアーキテクチャーにNode.jsとTypeScriptを導入したのですが、そちらの基盤技術としてInversifyを導入したらめちゃくちゃ良かったので、使い方含めて紹介します。

DIコンテナって何?

DI(Dependency Injection)を達成するためのコンテナのことです。

DIは色んな所で紹介されているので、割愛しますが、簡単に言うと

class Hoge {
    get() {
        const fuga = new Fuga()
        ...
    }
}

とあったときに、Hoge::get()はFugaがnewされないと処理を実行できないため、例えばこの関数をユニットテストしたいときに、Fugaのコンストラクタの実装に依存してしまいます。 例えばコンストラクタ上でデータストアに接続するような処理がある場合、テスト環境にもデータストアの環境を整備しなければならず、本来やりたかったこと以上のことをする必要があり、面倒でありメンテコストも上がってしまいます。

これを

class Hoge {
    get(fuga: Fuga) {
        ...
    }
}

って外部から注入してあげれば、例えばfugaをモックしたオブジェクトを外から注入してあげるだけで上記の問題が解決します。 これがいわゆるDIと呼ばれるものです。

当然呼び出し元でnewすれば注入できるわけですが、そうすると呼び出し元がFugaに依存するようになってしまい、処理がより複雑になってしまいます。 CleanArchitectureなどに代表される、多層化されたアーキテクチャーを採用する場合、この問題はより顕著になり、下記のようになってしまいます。

/// A -> B -> Cという依存

class A {
    constructor() {
        new c = C()
        new b = B(c)
    }
}

class B {
    constructor(c: C) {
    ...
    }
}

class C {
}

また、どのクラスをnewすればいいかという観点において、基本的にはアプリケーションごとに唯一のクラスが指定できればいい(通常はモックなしのもの、テスト時はモックありのもの)はずなので、どこかで定義できてしまえば解決します。

これを解決するために、newする処理を一つのコンテナ(オブジェクト、グローバル変数のようなもの)に集約することによって、依存性解決をわかりやすくしようというのが、DIコンテナの役割となります。

Inversifyを使った方法

DIコンテナは各言語、フレームワークごとに独自で実装されていたりするのですが、TypeScriptではInversifyというライブラリがあります。 Inversify これは、TypeScriptのアノテーションを使うことによって、DIコンテナを実現してくれます。 TypeScriptベースで作られているため、型安全に実装できるのがポイント。 JavaScriptでも使用できるため、TSユーザじゃなくても使うことができるのが魅力的です。

インストール

npm install inversify reflect-metadata --save

refrect-metadataというものも必要なので、注意。 refrect-metadataはグローバルのシングルトンなので、一度だけimportするようにしましょう

tsconfig.jsonには下記のように入れていきましょう。

{
    "compilerOptions": {
        "target": "es5",
        "lib": ["es6", "dom"],
        "types": ["reflect-metadata"],
        "module": "commonjs",
        "moduleResolution": "node",
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true
    }
}

使用方法

まず先に実装クラスと、対応するinterfaceを実装します。 今回はDB接続を想定した設計でやってみます。

interface DatabaseInterface {
    connect(option: Option): void;
    find<T>(query: Query): T;
}

interface UserRepositoryInterface {
    find(id: string): User;
}

次にDIで用いる識別子を登録していきます。 Symbolで実装することを推奨しているようです。

let TYPES = {
    DB: Symbol("DB"),
    UserRepo: Symbol("UserRepo"),
    UserCntl: Symbol("UserCntl")
}
export default TYPES;

最後に実装していきます。DIされるクラスには@injectableデコレータを、DIするインスタンス@injectデコレータをつけていきます。

import { injectable, inject } from 'inversify';
import 'reflect-metadata';
import TYPES from './types';

// DB
@injectable()
class Mongo implements DatabaseInterface {
    public connect(option): void {
        // ...
    }
    
    public find<T>(query: Query): T {
        // ... 
        const res: T= {} as T;
        return res
    }
}

// Repo
@injectable()
class UserRepository implements UserRepositoryInterface {

    private _db: DatabaseInterface;

    public constructor(
        @inject(TYPES.DB) db: DatabaseInterface // constructorに注入
    ) {
        this._db = db
    }

    public find(id: string): User {
        const query: Query = {};
        return this._db.find<User>(query);
    }
}

// Controller

class UserController {
    @inject(TYPES.UserRepo) private _repo: UserRepositoryInterface; // property に直接注入
    
    public get(): User {
        const userId: string = 'hoge';
        return this._repo.find(userId);
    }
}

こちらを見てもらえれば分かる通り、UserControllerはどのDBの実装クラスが使われているかも、どのUserRepositroyのインスタンスを使うかも気にすることなく、直下のUserRepositoryInterfaceを継承する何かを使うという処理を書くだけで終了します。

実際の依存関係は inversify.config.ts に記載します。

import { Container } from 'inversify';
import TYPES from './types';

const container = new Container();
container.bind<DatabaseInterface>(TYPES.DB).toConstantValue(new Mongo()); // シングルトンで登録
container.bind<UserRepositoryInterface>(TYPES.UserRepo).to(UserRepository);
container.bind<UserController>(TYPES.UserCntl).to(UserController);

最後にコンポジションルート(最上位のmain処理)に依存性解決の処理を記載します。

import container from './inversify.config';
import { UserController, DatabaseInterface } from './main';
import TYPES from './types';

function main() {
    // Controllerでゴニョゴニョする
    const controller = container.get<UserController>(TYPES.UserCntl);

    const user = controller.get()
    console.log(user) // なにかする
}

main();

何がいいの?

開発環境と本番環境でDB接続が違う場合

例えばAWSにはDocumentDBというMongoDB互換のDBがあります。 こちらのDBはほとんどMongoDBと同等なのですが、TLS接続が標準であり安全性のために必須にしているかと思います。 ただ、ローカルではTLS接続はしたくないといったパターンの場合、上記でいうconnect()の関数だけ変更したくなります。

そういった場合は、下記のように専用のクラスを作り、inversify.config.tsを変更するだけです。

@injectable()
class DocDB extends Mongo {
  protected ca: Buffer[];

  public constructor() {
    super();
    this.ca = [fs.readFileSync("/path/to/pem/rds-combined-ca-bundle.pem")]; // caファイルの読み込み
  }
  public connect(): void { // connectのオーバーライド
    ...
    this.client = new MongoClient(url, {
      auth: { user, password },
      useNewUrlParser: true,
      useUnifiedTopology: true,
      sslValidate: true,
      sslCA: this.ca
    });
  }
}

inversify.config.ts

container.bind<DatabaseInterface>(TYPES.DB).toConstantValue(
    process.env.NODE_ENV === 'production' ? new DocDB() : new Mongo()
);

たったこれだけで本番環境と開発環境でデータストアを切り替えることができます。

テストで使いたい

テスト環境で使いたい場合も非常にシンプルです。 UserRepositoryをテストしたい場合は、 下記のように注入したいモックオブジェクトを作成し、それを注入するだけです。

import * as TypeMoq from "typemoq";

describe("test", (): void => {
  test("repo", async () => {
    const user = {};
    const mock: TypeMoq.IMock<DatabaseInterface> = TypeMoq.Mock.ofType<
      DatabaseInterface
    >();
    mock
      .setup(m => m.find<User>("id"))
      .returns(() => user);
    const repo = new UserRepository(mock.object);

    const result = repo.find("id");
    expect(result).toBe(user);
  });
});

また、この他containerにはrebindという関数もあるため、テスト全体で用いるinversify.config.tsを予め用意しておき、テスト専用のモックをrebindで新たにbindした上でテストを行うことによって、多層的な依存関係のクラスのテストも容易になります。

まとめ

今回はTypeScriptでinversifyを使ったDIコンテナの紹介をさせていただきました。 軽量なアプリケーションではこのようなアーキテクチャは不要かもしれませんが、変化が激しい、レイヤーが複数あるアプリケーションや、外部との接続が多いBFFのようなものを実装する際は、処理の実行確認が非常に難しくなるため、使える技術かなと思っています。