Lambda Layers を含む AWS CDK ライブラリをパッケージ化する

要約

  • CDK で Lambde + Lambda Layers の Construct ライブラリを作る
  • 別の CDK App から npm install して使う
  • npm の postinstall を活用して Layers のアセットを生成する

CDK の記述言語は Typescript 前提で扱います。 (2020/03 現在 cdk init lib が Typescript しか対応していないため)

背景

CDK + SAM を使った Lambda の開発環境

CDK を使うと簡単に Lambda および周辺環境のデプロイをすることができます。さらに、AWS SAM CLI を使うとローカルで Lambda 環境を立ち上げてテスト実行を行うことができとてもはかどります。 docs.aws.amazon.com

Lambda + Lambda Layers

Lambda で外部パッケージを使う方法の一つとして Layers があります。 AWS CDK を使う場合においても、以下リンクのように Layers のアセットを用意する方法が紹介されています。

dev.classmethod.jp 記事の内容について今回関係するところだけ簡単にまとめると、

  • Layers 以下の npm install を行う function を作成しておく
  • CDK App 実行時に function を呼び出す
  • cdk synthcdk deploy 時に Layers 以下 nodejs/node_modules が生成される

というものです。いくつか課題があることが言及されているものの、多くの場合で有効に機能します。

インフラのライブラリ化

CDK の特徴として、作成した Construct / Stack をライブラリ、パッケージとして扱えることが挙げられます。具体的な言い方をするならば、 CDK で作成した Lambda ないしインフラ一式は、他の CDK App から npm install, import ... して参照、カスタマイズして使い回せるということです。これについても参考になる記事があります。 dev.classmethod.jp

やること詳細

今回は Layers を含む Lambda を作成し、それが持ち運び可能なライブラリとしてうまく機能するかを試してみます。 まとめると実現したいことはこのようになります。

  • CDK で Lambda 単体の開発, テスト環境の作成
  • Layers を含む Lambda として整備
  • 作成した Lambda を別の CDK App からライブラリとして使えるようパッケージとして整備

一つづつ具体的なコードで確認していきます。

CDK で Lambda 単体の開発環境を作成

cdk init lib --language typescript でひな形を生成します。 この時点で CDK ライブラリとしての一式は揃っています。今回はここに、

  • Lambda 関数本体 lambda/index.js を追加
  • Lambda をテスト実行するためのランナーとして bin/app.ts を追加

します。bin/app.tscdk init app で初期化したときに生成されるのと基本的に同じものです。 lib としては不要なのですが、Lambda 関数のテストにはあったほうが便利なので作成しています。ツリーは以下のようになりました。

├── bin
│   └── app.ts
├── jest.config.js
├── lambda
│   └── index.js
├── lib
│   └── index.ts
├── package.json
├── package-lock.json
├── README.md
├── test
└── tsconfig.json

Lambda 本体はこれです。

'use strict';

exports.handler = async function(event) {
  console.log('Hello.');

  return 'Hello !!';
}

app はこんなです。

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';
import { CdkExampleLambdaWithLayer } from '../lib/index';

const app = new cdk.App();
const stack = new cdk.Stack(app, 'app', { stackName: 'lambda-with-layer'} );

new CdkExampleLambdaWithLayer(stack, 'LambdaWithLayer');

前述の SAM CLI を使う方法でローカルテストができることを確認します。

$ cdk synth --app ./bin/app.js --no-staging > template.yaml
$ grep Lambda::Function template.yaml -B1
  LambdaWithLayermyHandlerDA1D7776:
    Type: AWS::Lambda::Function

$ sam local invoke LambdaWithLayermyHandlerDA1D7776
Invoking index.handler (nodejs12.x)
...
START RequestId: 27e66fad-f3be-13c4-246f-85de8eca3863 Version: $LATEST
...
END RequestId: 27e66fad-f3be-13c4-246f-85de8eca3863
REPORT RequestId: 27e66fad-f3be-13c4-246f-85de8eca3863  Init Duration: 293.40 ms        Duration: 7.15 ms       Billed Duration: 100 msMemory Size: 128 MB      Max Memory Used: 38 MB

"Hello !!"

正常に実行できました。

Layers を含む Lambda として整備

外部モジュールを Layers に含め本体 Lambda から読むこむようにしてみます。 Layers の導入は冒頭で貼ったクラスメソッドさんの方法と同じように、プリプロセスを挟む方法をとります。

  • layer/nodejs ディレクトリ以下に、Lambda Layers 用の package*.json を置く
  • Layers 用の npm install を行うスクリプトを追加し、./bin/app.ts から呼ぶ
  • Lambda から require

Layers 用の package.json を準備します。

$ mkdir -p layers/nodejs
$ cd layers/nodejs
$ npm init   # name 等は適当に
$ npm install uuid

npm install 用 function を用意します。 lib/js/setup_layer.js にしました。

const childProcess = require('child_process');
const path = require('path');

module.exports.setupLayer = () => {
  const layerDir = path.join(__dirname, '..', '..', 'layer', 'nodejs');
  childProcess.execSync(`npm install ${layerDir} --prefix ${layerDir} --production`, {
    shell: 'bash'
  });
}

cdk synth などの際に呼びだされるように、./bin/app.ts に書いておきます。

 import { CdkExampleLambdaWithLayer } from '../lib/index';
+// @ts-ignore
+import { setupLayer } from '../lib/js/setup_layer';

+setupLayer();
 const app = new cdk.App();

Construct に Layers を追加します。

diff --git a/lib/index.ts b/lib/index.ts
@@ ...
   constructor(scope: cdk.Construct, id: string, props: CdkExampleLambdaWithLayerProps = {}) {
     super(scope, id);

+    const layer = new lambda.LayerVersion(this, 'layer', {
+      code: lambda.Code.fromAsset(path.join(__dirname, '..', 'layer')),
+      compatibleRuntimes: [lambda.Runtime.NODEJS_12_X],
+    });
     new lambda.Function(this, 'myHandler', {
       runtime: lambda.Runtime.NODEJS_12_X,
       code: lambda.Code.fromAsset(path.join(__dirname, '..', 'lambda')),
       handler: 'index.handler',
       environment: { MY_MESSAGE: props.myMessage || 'Hello' },
+      layers: [layer],
     });
   }

Lambda 本体から外部モジュールを参照します。

diff --git a/lambda/index.js b/lambda/index.js
@@ ...
 'use strict';
+const uuid = require('uuid').v4;

 exports.handler = async function(event) {
-   console.log('Hello.');
+   console.log(`Hello. ${uuid()}`);

-  return 'Hello !!';
+  return `Hello, ${uuid()}`;
 }

動作確認します。

$ cdk synth --app ./bin/app.js --no-staging > template.yaml
$ sam local invoke LambdaWithLayermyHandlerDA1D7776
...
"Hello, 4c5c267e-7eb9-409e-9d3e-4385e2d708f3"

ここまではよさそうです。

最後に、他から install されたときに Layers がちゃんと生成されるよう、package.jsonpostinstall を追記しておきます。実行するのは、App から呼び出しているものと同じ、npm install 用 function です。

diff --git a/bin/postinst.js b/bin/postinst.js
new file mode 100755
@@ ...
+#!/usr/bin/env node
+const setupLayer = require('../lib/js/setup_layer').setupLayer;
+
+setupLayer();

diff --git a/package.json b/package.json
@@ ...
   "scripts": {
     "build": "tsc",
     "watch": "tsc -w",
-    "test": "jest"
+    "test": "jest",
+    "postinstall": "bin/postinst.js"
   },

npm パッケージとして公開しておきます。こちらにもあるように、基本的には npm publish すれば OK です。もしくは、npm は git レポジトリでもパッケージとして扱えるので、github から直接参照することも可能1ではあります。

別の CDK App から使う

別の CDK App を用意し、先程作成したパッケージを npm install します。

$ npm install @<myusername>/cdk-example-lambda-with-layer

node_modules 以下の自作パッケージディレクトリに、 postinstall によって Layers の外部モジュールが配置されていることが確認できます。

$ ls node_modules/@<myusername>/cdk-example-lambda-with-layer/layer/nodejs/node_modules/
uuid/

スタックからは以下のように利用できます。

diff --git a/lib/cdk-example-import-lambda-stack.ts b/lib/cdk-example-import-lambda-stack.ts
@@ ...
 import * as cdk from '@aws-cdk/core';
+import * as myfunc from '@<myusername>/cdk-example-lambda-with-layer';

 export class CdkExampleImportLambdaStack extends cdk.Stack {
   constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
     super(scope, id, props);

     // The code that defines your stack goes here
+    new myfunc.CdkExampleLambdaWithLayer(this, 'myFunc', { myMessage: 'Imported!'});
   }
 }

$ cdk deploy でデプロイし、Lambda Layer確認します。

f:id:Zarari:20200428003902p:plain
Lambda 実行確認

無事に Layers で含めた外部モジュールを呼び出せていることを確認できました。

おわりに

CDK で Layers を含む Lambda を作成できること、および作成したものを他のアプリケーションから使えることを確認しました。パッケージ管理ができるようになるとインフラのモジュール化が実用的になってくるので、IaC の世代が一歩進む感覚があります。

不便に感じた部分としては、ライブラリ側と呼び出し側で CDK バージョンを揃える必要がある点です。CDK 自体のバージョンアップは非常に頻繁に行われているので、ライブラリとして作るなら常に最新版に対応して公開していく仕組みを自動化するなどが必要かと思いました。引き続き CDK は手探りを続けていきたいと思います。



  1. cdk init 生成したときの .gitignore, .npmignore のままでは git レポを install しようとしてもパッケージにならないので調整が要ります

AWS CDK さわってみた、たのしかったのだけど

AWS CDK Workshop をやってみたので、 普段使っている従来の CloudFormation や、ちょっと触っていた SAM と比べてどうか、という視点で感想を書きます。

AWS CDK とは

プログラミング言語を使って抽象化された CloudFormation を書けるフレームワークです。生の CloudFormation に比べて抽象化されており、ループや条件分岐などのプログラミング言語のテクニックを使えるため冗長な記述を減らすことができたり、まとまった単位でモジュールとして切り離して再利用できるなどのメリットがあります。

2020/01 現在使える言語は

があります。

専用の CLI を使い、 cdk synth による 生 CloudFormation テンプレート書き出しや、cdk deploy による一発デプロイなどができます。CDK 独自の概念というかリソース体系があるので、以下の記事の前半を読んでおくとわかりやすいかと思います。 https://dev.classmethod.jp/cloud/aws/aws-cdk-construct-explanation/

CDK で扱うリソースの単位は、Construct と呼ばれています。このリソースを new したりパラメータ渡して呼び出したりして AWS を宣言的に操作します。 https://docs.aws.amazon.com/cdk/api/latest/docs/aws-construct-library.html

ワークショップ所感

TypeScript でやってみました。 やっていて楽しかったので基本的にはポジティブな印象です。とはいっても万能では無いのでつらつらっと思ったこと書きます。

まぁまぁの数のファイルがある

CDK ワークショップをやった時点では次のようなディレクトリ構成になります。 git 管理されているものだけ載せています。

├── bin
│   └── cdk-workshop.ts
├── cdk.context.json
├── cdk.json
├── jest.config.js
├── lambda
│   ├── hello.js
│   └── hitcounter.js
├── lib
│   ├── hitcounter.ts
│   └── cdk-workshop-stack.ts
├── package.json
├── package-lock.json
├── README.md
├── test
│   └── cdk-workshop.test.ts
└── tsconfig.json

フレームワーク、といわれるものとしては普通というか少ないほうですが、当然生 CloudFormation や SAM と比べると多いです。小さい環境なら awscli と YAML ファイルのみで CloudFormation を管理する場合もあるかもしれませんし、こういう構成を複雑と捉える人もいるかもしれません。 ただし、この中でワークショップ中で実際に直接コード編集を行ったのは、 lambda 以下と lib 以下で、そして npm を介しての package*.json 変更があったくらいでした。 書く量は非常に少ないです。

言語固有の問題は起きる

これは単に自分が抜けているという例なのですが…

  • パラメータの型が合わない、と怒られて npm 側の aws-cdk-* バージョンを見るとずれていた
  • 別マシンで作業を引き継いで、 git 上コード差分が無いのに cdk diff が出てしまった、よく見たら自動ビルドが走っておらず .ts のビルドがされてなかった

など、プログラミング言語のプラクティスが使えるということはプログラミング言語のよくある問題も起き得る、ということを当たり前ながら実感しました。

SAM を完全に内包するものではない

CDK は AWS リソース全般をカバーするようなフレームワークなので、たとえば sam local ~ 便利コマンドや sam log のような機能は CDK CLI にはありません。 lambda の動作テストや log tail のようなことは CDK の外側になるので、この辺もうまくカバーされるか他のツールと連携しやすいといいなと思いました。

結局 CloudFormation デプロイ

CloudFormation からデプロイするので、反映までの速度はそれなりです。 lambda のコードちょっと変えるだけでも ts ビルド、CloudFormation changeset 作成して apply なのでサクサクと反映できるわけではありません。そもそも直接実行環境にデプロイして力技でデバッグしていくのではなく、ちゃんとロジック部分を分離しテスタブルに作っとけよと暗に言われた気がしました。

全体的に

楽しいフレームワークです。ただし既存の CloudFormation で作成されたサービス中の環境を置き換えるには難易度が高そうです。これから作成するものには使って損はなさそうです。ワークショップの内容以外にも、現在実用している CloudFormation テンプレートと同じことをさせてみてどうなるかをもう少し検証してみます。

エンジニアをしながら育児をすること

専門職を続けること

技術は日進月歩です。どんな業界にせよ、専門職でごはんを食べていくためにはその道における現在を把握し、自分に何ができるかを示し続けなければいけません。とりわけ、ソフトウェア業界はそのスピードが速いと言われていますので、技術、知識のアップデートが日常的に必要とされます。

これまでやってこれたわけ

独身時や夫婦二人だけの時期は、業務時間以外でも、やる気が余っているときに適当に Web アプリや bot を書いたりして、趣味の一部として仕事につながる技術をさわって過ごすことができていました。また、業務タスクを多めに積んでしまったとしても、ゆるゆると会社に残って作業したり、土日に調べ物をしたりと、バッファの調整は容易でした。

今思えば、贅沢なほどに時間があったのです。 足りないものがあれば、とにかく時間をかけてそれを補うことができる環境だったという、そのことによって自分の仕事のレールはどうやら支えられていました。

子供の時間と自分の時間

2016 年に娘が、2018年に息子が産まれました。今現在は妻は仕事復帰して子供は保育園に通っており、夫婦の就業時間の都合で送りを私が、迎えを妻が行っています。 朝早く出勤して作業することはできなくなりました。また、夜の乳幼児ワンオペは消耗が大きいものですので、むやみに会社に残って作業することもできなくなりました。 休日にいたってはずっと子供がいるので、平日よりむしろ消耗します。

自分の仕事のレールを支えていた時間の一部が消滅しました。このままではできることがなくなる、仕事を続けていけないという未来を容易に想像できるようになりました。

"なんとなく勉強" からの脱却

生活を変える必要がありました。趣味の一部で、つまり空き時間にやる気ドリブンで技術を追いかけることは無理でした。精神力を育児で使い切るのでやる気は残りませんし、そもそも業務時間以外に時間がありません。別の効率的な手段を探しました。

人に伝えてみる

人に教えることは自分の理解を助けるとよくいわれます。勉強はインプットとアウトプットのバランスを取ることで効率が上がるので、主に通勤時間をインプットに、業務時間中に積極的に人に伝えることをアウトプットとすることで短い時間で効率よく理解が深まることを実感できました。具体的には、これまでは「こんなこと他の人も普通に知ってるか調べればすぐわかるだろ…」と思っているようなことでも、ためらわずに外に出すようにしました。また、勉強会を主催したりもしました。

人から教わってみる

これも本質的には前項と変わりませんが、たとえなんとなくわかっていたとしても、自分の言葉を使って人に質問してみるようにしました。もちろん人の時間で迷惑にならないような範囲ですが… これも結局はアウトプットです。自分の理解を明確にするのに役立ちました。

試験を受けてみる

これはいわゆる締め切りドリブンです。時間がないという問題を本質的に解決していないのですが、うっかり重要度も優先度も低い作業に手を出してしまうことを避けるという意味で役に立ちます。試験自体は英語や技術資格のような、基礎的なものにフォーカスしました。

レールの修復

時間がないことをリアルに突きつけられたことで、30半ばを過ぎてやっと、やっと、本当に今更ながら、続けるべき習慣、避けるべき習慣を考えることになりました。アウトプットの大切さを知りました。思考の整理と定着の関係がわかってきました。仕事のレールを維持する方法がすこしずつ見えてきました。残念ながら削った趣味もありますが、少なくとも育児が落ち着くまでの何年かは仕方ないと割り切っています。

その先

2019 年は時間のない生活の実態を理解し、いろいろな方法を試す中で少しの活路を見出すこともできました。この1,2年で理解した、と感じたことのほとんどは、すでに知っていたはずのことです。アウトプットで学ぶことも、続けるべき習慣とは何なのかも、20代のころに聞いていたことばかりです。知り、理解し、実践するというステップがあることさえ、育児がなければ思い出せなかったかもしれません。 2019 年中には実践まで日常のリズムに乗せることはできていませんが、2020 年はこのリズムを掴む方法を模索していきたいと思います。

docker-compose で apache2+PHP 開発環境 (silex, CLI, mysql, redis)

docker-compose でなるべく楽をして PHP 開発環境を作成する流れについて、一応使えそうになったのでまとめます。

ローカルに PHP を入れたりせず、Docker さえあればアプリケーションの CLI や composer を使って開発ができることを目的にします。

想定する PHP アプリ

  • Silex
  • MySQL、Redis
  • apache2 + PHP7.1

作業環境

Windows, Mac どちらでも Docker が動けば問題ないはずです。いちおう下記で基本動作を確認。

開発環境

ディレクトリ構成

基本的には Silex Skeleton に従います。

├── bin
│   └── console
├── composer.json
├── composer.lock
├── config
├── docker
│   └── app
│        ├── app.conf
│        ├── Dockerfile
│        ├── mpm_prefork.conf
│        └── php.ini
├── docker-compose.yaml
├── etc
│   ├── mysql.php
│   └── redis.php
├── README.md
├── src
├── templates
├── tests
├── var
└── web

アプリのルートに docker-compose.yaml を、docker/app 以下にアプリケーションコンテナ関連のファイルを置いています。

composer install

composer install します。composer 本体はDocker公式レポジトリのイメージ をバイナリのように使えます。

アプリのルートで以下のコマンドを実行します。

$ docker run --rm --interactive --tty --volume ${PWD}:/app composer install

無事 vendor ディレクトリが生成されるはずです。

msys2 など cygwin 互換環境の場合は --volume `cygpath $PWD -w`:/app とするとパスが解決できるかと思います。(以下 volume オプション使用時同様)

主題とは関係ないですが、日本で使う場合には composer.json にミラーの設定をしておくと速度が上がって多少苦しみが緩和されると思います。

...
"repositories": {
    "packagist": {
         "type": "composer",
         "url": "https://packagist.jp"
    }
},
...

アプリケーションコンテナ

apache2 + PHP が動くコンテナをビルドします。 手軽さを優先し、Docker公式レポジトリのPHPイメージ を使います。

docker/app/Dockerfile を書きます。今回は MySQL, Redis を使いたいので extension を追加します。

docker/app/Dockerfile

FROM php:7.1-apache

RUN docker-php-ext-install -j$(nproc) mbstring opcache pdo pdo_mysql

RUN apt-get update && apt-get install -y zlib1g-dev \
 && pecl install redis-3.1.3 \
 && docker-php-ext-enable redis \
 && apt-get autoremove --purge -qq \
 && apt-get clean \
 && rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/*

COPY php.ini /usr/local/etc/php/
COPY app.conf /etc/apache2/sites-available/app.conf
RUN a2dissite '*default*' \
 && a2ensite app \
 && service apache2 restart

公式イメージのページにも十分な説明があるとは言えないのですが、docker-php-ext-install だけですんなり入るものとそうでないものがあります。phpredis はビルドの必要がありました。他にも、例えば CURLdocker-php-ext-install の候補に含まれているものの、事前に libcurl3-dev が必要だったりするので、目的の物によっては GitHub issues を探す必要はあるかもしれません。

ここでは、MySQL、Redis 関連の extension 追加の他に、php.ini と apache の設定をイメージに追加しています。

apache2 には後述の docker-compose で調整するマウントポイントが DocumentRoot になるように設定しておきます。この辺はお好みです。

docker/app/app.conf

<VirtualHost *:80>
    DocumentRoot /app/web

    <Directory /app/web>
        Options all
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>

php.ini には、開発用に

docker/app/php.ini

log_errors = On
error_log = /dev/stderr

を記述しておくことで、PHP ログを docker 標準のログとしてとり扱うことができます。

MySQL、Redis コンテナ

今回は公式のイメージそのままで使います。docker-compose.yaml の記述のみですむので専用の Dockerfile は用意しません。

docker-compose で環境立ち上げ

docker-compose でアプリケーション、MySQL、Redis を立ち上げます。

docker-compose.yaml

version: '3'
services:
  app:
    build:
      context: ./docker/app
    volumes:
      - .:/app
    working_dir: /app
    ports:
      - "3000:80"
    environment:
      - SYMFONY_ENV=dev
      - DB_MASTER_HOST=mysql
      - DB_MASTER_USER=app
      - DB_MASTER_PASSWORD=app
      - DB_REPLICA_HOST=mysql
      - DB_REPLICA_USER=app
      - DB_REPLICA_PASSWORD=app
    depends_on:
      - mysql
      - redis

  mysql:
    image: mysql:5.7
    command: "mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci"
    volumes:
      - "mysql-myapp-data:/var/lib/mysql"
    environment:
      - "MYSQL_DATABASE=myapp_dev"
      - "MYSQL_ROOT_PASSWORD=root"
      - "MYSQL_USER=app"
      - "MYSQL_PASSWORD=app"
    ports:
      - "3306:3306"

  redis:
    image: redis

volumes:
  mysql-myapp-data:

app はアプリケーションを動かす apache2+PHP コンテナです。docker/app 以下をビルドし、カレント(アプリのルート)ディレクトリを /app にマウントしています。また、後々コマンドラインが使いやすいように working_dir を設定しています。環境変数では主に MySQL 関連の情報を渡しています。

以前の compose file v1 の頃からの差分になりますが、v3 では link を書かなくてもデフォルトで他のコンテナを名前解決できるようになっています。depends_on は起動順番を指定するものですが、MySQL コンテナは起動後準備できるまでにラグがあり、これを書いたからといって app 起動前に他の環境が全て ready というわけでもなくちょっと微妙です。

mysql では永続化ボリュームを設定しています。

ここまでアプリの動作環境がそろいました。

$ docker-compose up

で初回のアプリケーションコンテナのビルド、MySQL、Redis コンテナの Pull が行われるはずです。

立ち上がったら http://localhost:3000 でアプリケーションにアクセスできます。

CLI を使う

Silex Skeleton では bin/consoleCLI が使えるようになっています。他のフレームワークでも Railsbin/ のように CLI ツールを置くことがあるでしょう。

今回のようにローカルに実行言語を入れない場合でも、app コンテナを使って CLI を叩くことができます。

$ docker-compose exec app bin/console -h
Usage:
  list [options] [--] [<namespace>]

Arguments:
  namespace            The namespace name

Options:
...

今回はお試し CLI として、doctrine/migrations で DB のマイグレーションをやってみます。doctorin 自体の使い方はこちらの Silexだってdoctorin/migrations使う を参考にさせていただきました。

├── bin
│   └── console
├── composer.json
├── composer.lock
├── config
├── db
│  ├── migrations
│  ├── migrations.yml
│  └── migrations-db.php
...
├── src
│  ├── console.php

db/ 以下に関連ファイルを置きました。

db/migrations.yml

name: Doctrine Migrations
migrations_namespace: MyApp\Db\Migrations
table_name: doctrine_migration_versions
migrations_directory: ./db/migrations

db/migrations-db.php

<?php

return [
    'driver'    => 'pdo_mysql',
    'host'      => getenv('DB_MASTER_HOST'),
    'dbname'    => 'myapp_' . getenv('SYMFONY_ENV'),
    'user'      => getenv('DB_MASTER_USER'),
    'password'  => getenv('DB_MASTER_PASSWORD'),
    'charset'   => 'utf8mb4',
];

参考記事のように、console.php に migration コマンドを登録します。

use Doctrine\DBAL\Migrations\Tools\Console\Command\DiffCommand;
use Doctrine\DBAL\Migrations\Tools\Console\Command\ExecuteCommand;
use Doctrine\DBAL\Migrations\Tools\Console\Command\GenerateCommand;
use Doctrine\DBAL\Migrations\Tools\Console\Command\LatestCommand;
use Doctrine\DBAL\Migrations\Tools\Console\Command\MigrateCommand;
use Doctrine\DBAL\Migrations\Tools\Console\Command\StatusCommand;
use Doctrine\DBAL\Migrations\Tools\Console\Command\VersionCommand;

$console = new Application('My Silex Application', 'n/a');
...

/** Doctrine\DBAL\Migrations */
$console->add(new DiffCommand());
$console->add(new ExecuteCommand());
$console->add(new GenerateCommand());
$console->add(new LatestCommand());
$console->add(new MigrateCommand());
$console->add(new StatusCommand());
$console->add(new VersionCommand());

準備できたら、migration 定義を生成、

$ docker-compose exec app bin/console migrations:generate --configuration db/migrations.yml --db-configuration db/migrations-db.php

db/migrations 以下に生成されたファイルを編集し、migration 実行します。

$ docker-compose exec app bin/console migrations:migrate --configuration db/migrations.yml --db-configuration db/migrations-db.php

array(5) {
  ["host"]=>
  string(5) "mysql"
  ["dbname"]=>
  string(9) "myapp_dev"
  ["user"]=>
  string(3) "app"
  ["password"]=>
  string(3) "app"
  ["charset"]=>
  string(7) "utf8mb4"
}
Loading configuration from command option: db/migrations.yml

                    Doctrine Migrations


WARNING! You are about to execute a database migration that could result in schema changes and data lost. Are you sure you wish to continue? (y/n)y
Migrating up to 2017xxxxxxxxxx from 0

  ++ migrating 2017xxxxxxxxxx

     -> CREATE TABLE example (col1 LONGTEXT NOT NULL) DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB

  ++ migrated (0.33s)

  ------------------------

  ++ finished in 0.33s
  ++ 1 migrations executed
  ++ 1 sql queries

(migration の中身は適当です) 無事コマンド実行できました。

まとめ

docker-compose でローカル環境の影響を最小限に開発環境が整いました。

引き続き、lint ツールの統合やデプロイについてもまとめる予定です。

特に参考にさせていただいた記事

Windows で zsh と開発環境を整備

最近 Surface Pro 4 を購入した勢いで Windows のシェル環境整備し一新したまとめ。Surface Pro 4 よいです。

この記事は 2016/05現在プレビュー版である bash on Ubuntu on Windowns とは無関係です。そちらは正式版が出たら試したいと思います。

bash on Ubuntu on Windows が来るならそれでシェル環境間に合うかなとも思ったんですが、仕組み上 Linux サブシステム側から Windwos アプリを立ちあげられるようにはならなそうなので、それはそれ、これはこれという感じで。

やりたいこと

  • アプリのパッケージ管理したい
  • zsh 使いたい
  • putty ではなく普通の ssh がほしい
  • git
  • Ruby 少々
  • node 少々
  • NTEmacs 使いたい

普段の仕事では OSX / Linux 中心なので、なるべくどこでも同じような使用感が得られる環境を目指します。

Ruby/node 少々というのは、ガチ開発になるとは辛いけどサンプル動かすくらいはできる、程度の意味です。

検討結果

結論としては、以下のツール郡でなかなかよい感じになりました。

  • パッケージ管理 : Chocolatey + MSYS2内 pacman の2段構成
    • なるべく Chocolatey で導入する
    • MSYS2 シェル内でしか使わないものは pacman で入れる
  • シェル : MSYS2 の zsh
  • 端末エミュレータ : ConEmu
  • OpenSSH : MSYS2 pacman パッケージ
  • Ruby : Chocolatey パッケージ
  • node : Chocolatey パッケージ
  • Emacs : https://github.com/chuntaro/NTEmacs64 のビルド(手動インストール)

それぞれ詳細

パッケージ管理

まずはパッケージマネージャについてです。

Windows が諸 Linux に最も差をつけられているのがここだと思っていましたが、Windows10 では MS 製の PowerShell モジュール PackageManager (旧OneGetと同じ?) が使えるようになりましたし、サードパーティ製ではあるものの、apt 的な使い勝手をもつ Chocolatey も成熟してきているようで、2016 年現在ほとんど不便は解消していそうです。

パッケージマネージャは環境構築の基礎になるところなので、できれば長いサポートが見込める公式のものを使いたいところです。 なので PackageManager を…と思いましたが、これがまだイマイチこなれてないような…のと、新しいせいで情報が少なめです。よく見るとこの MS PackageManager は複数リポジトリプロバイダを統括する層のツールで、サポート対象には Chocolatey のリポジトリも含まれていました。これなら Chocolatey の今後もしばらく安心できそうですし、何より既にシェアが大きいようで情報も多いので Chocolatey をメインで使うようにしました。

Chocolatey のインストールは、公式トップにある手順で問題なくできました。 https://chocolatey.org/

シェル環境、端末エミュレータ

MSYS2 はパッケージマネージャがまともそうで、生まれが比較的新しいことに期待して選びました。MSYS2 installer にしたがってインストールします。

MSYS2 はデフォルト端末として mintty が起動するようになっています。mintty は見た目も悪くないのですが、node や irb など対話型の扱いに問題がありましたので、ConEmu から MSYS2 のシェルを起動する形をとります。

まずは付属の MSYS2.shell を起動し zsh をインストールします。

$ pacman -S zsh

次に ConEmu をインストールします。今度は Chocolatey で入れます。PowerShell を管理者権限で起動し、以下の様にインストールします。

PS :> choco install ConEmu

インストールできたら、ConEmu で MSYS2 zsh を起動する設定をします。[Settings] の [Startup] > [Tasks] に zsh 起動コマンドを追加します。

  • set CHERE_INVOKING=1 & %ConEmuDrive%\msys64\usr\bin\zsh.exe --login -i -new_console:C:"%ConEmuDrive%\msys64\msys2.ico"
  • "Default Shell" にチェック

conemu screenshot

これで ConEmu + zsh が起動するようになると思います。

f:id:Zarari:20160504113020p:plain

HOME 環境変数を設定する

シェル内や後述の Emacs の動作を Unix 系に近づけるため、環境変数 HOME を設定します。

C:\Users\username あたりが HOME になってほしいので、%USERPROFILE% をそのまま設定で ok です。

ちなみに、PowerShellenv のように環境変数を一覧したい場合は PS > Get-ChildItem env: で参照できます。

OpenSSH

Windows 版 OpenSSH もあるのですが、自分が試した限りでは ssh-agent が MSYS2 環境で動かせなかったので MSYS2 の OpenSSH を使用します。OpenSSH はどうやら残念ながら $HOME/.ssh/ を見てくれないようなので「msys2での$HOMEとOpenSSHでのホームディレクトリの違い」を参考に、$HOME/home/username にマウントする方法をとります。

/etc/fstab に追加

# User home
C:/Users/username /home/username

.ssh/configeval `ssh-agent` も問題ありません。

Git, Ruby, Node

Git, Ruby, Node は chocolatey で入れます。

PC > choco install git ruby nodejs

RubyC:\tools\ruby22 にインストールされるようなので、MSYS2 側で /c/tools/ruby22/bin にパスを通します。

$ git --version
git version 2.8.1.windows.1
$ ruby -v                                                                                              
ruby 2.2.4p230 (2015-12-16 revision 53155) [x64-mingw32]
$ node -v                                                                                              
v5.10.1

また、Windows 版 git は改行コードを勝手に変換してしまうので、自動変換を無効化します。

$ git config --global core.autoCRLF false

Emacs

GUI 版の Emacs については flycheck の都合で libxml2 同梱のものが欲しかったため、https://github.com/chuntaro/NTEmacs64IME パッチ版を使用します。これはパッケージ管理から漏れますが諦めました…。

runemacs ショートカットに[作業フォルダー]を指定することで、起動直後の場所を自由に設定できます。

f:id:Zarari:20160504164737p:plain

その他

  • Ctrl と Caps Lock の入れ替えには MS 製ユーティリティの Ctrl2Cap を使っています。
  • 全体的なキー割当カスタマイズには Keyhac が使い易かったです。
  • 管理者権限でシェルを起動してくれる Sudo が地味に便利です。

以上です。簡単な Web 系開発であれば快適に作業できるようになりました!

React + Redux に取り組んだ際のハマりポイント振り返り

javascript 初学者が React + Redux に取り組んだ際のハマりポイント、時間がかかったポイントを時系列で書いていきます。

振り返りのまとめ

  • React も Redux もドキュメント充実してるのでちゃんと読むべき
  • フロントエンド開発環境は空気読むのが難しい

動機

  • javascript ぜんぜん書いてないので、よさそうな ES2015 とやらが出たこの機会に触っておきたかった
  • Single Page Application を作る機会があった
    • 一人でさっと作る類のものなので、せっかくなので近年話題のものを調べてみたい

振り返り

※ 最終的にできあがったサンプルはこちらに置いています

ハマり Points

React について

そもそも正確には何をするライブラリで、どの範囲を指している言葉なのか? これはハマりというか事前の理解が違っていました。ぼんやりと複雑なものを想像していましたが、概念的にはむしろシンプルで小さいというのが今の理解です。

https://facebook.github.io/react/docs/getting-started.html

公式のドキュメントがしっかりしています。StarterKit によると

  • Just the UI
  • Virtual DOM
  • Data flow

の UI ライブラリだそうです。この時点ではちょっとピンと来ていなかったですが、フレームワークの類ではなく本当に View でしかないものです。

もう少し React

雑に言えば、Component というのをたくさん作ってそれを組み合わせ仮想 DOM を構築し、それをよい具合にリアル DOM に変換してくれるライブラリ…でよい?

Component

Component とはパラメータを渡せる UI 部品で

  • JSX という HTML 風シンタックスを使って書ける
  • ライフサイクルイベントを持っている。
  • 最小限にすべきだが、Component ごとの state を持つこともできる

というものです。

Component の組み合わせ方としては、通常の HTML 要素を組むように Component のツリー構造を作り、最上位の ReactDOM.render() に渡せばよいようです。公式チュートリアル が良かったです。

このチュートリアルは cdn からライブラリを取得し index.htmlexample.js にモリモリ書いていくだけなのでハマりどころが無くわかりやすかったです。一点 <script type="text/babel"> ってなんだ…?と思いつつも、簡単に動くものが作れます。

フロントエンドの開発環境

チュートリアルまで終えたところで、じゃあ npm 使ってパッケージ管理して書いていこうと思ったわけですが、この辺で javascript というかフロントエンド開発環境の複雑さに気づきました…。

まだ過渡期なのかもしれませんがちょっとバラバラしすぎている印象が…。調べながら現代に追いつくのが困難でした。ググッて 2014 くらいだと既に変わっていることが多いので、2015後半〜 くらいの期間指定検索で追いかけていました。最近は「一新した」系記事が多いところを見るとそろそろ落ち着いたのかな…?

babel

動機の一つであった ES2015 はブラウザ処理系ではほぼ使えないので、トランスパイラをはさむのが普通らしく、これは babel というのを使っておけばよさそうです。しかし、どうビルドしていくかについては、facebook の StarterKit の段階では grunt なるものを使うガイドになっていましたが、glup がいいよとか npm の script で十分行けるとか、webpack がよいとか調べるほど混乱が増えてさっそく一度折れかけました。

結局、今の時点でツールを増やしたくないので npm script でやります。http://mae.chab.in/archives/2765 を参考にし、watchify + babelify で ruby の Guard っぽいことができるということでこれを使ってみます。

npm install --save-dev を知りました。

$ npm install --save-dev babel-plugin-transform-object-rest-spread
$ npm install --save-dev babel-preset-es2015 babel-preset-react browserify watchify babelify

.babelrc

{
  "presets": ["react", "es2015"],
  "plugins": ["transform-object-rest-spread"]
}

package.json

  "scripts": {
    "watch": "watchify -v -t babelify -o public/bundle.js src/index.jsx"
  }

監視開始

$ npm run watch

src/index.jsx を編集するそばから即反映されるようになりました。欲しかった import も、 () => {} も、 const/let も、{...obj} も使えます。よさそうです。

ファイル拡張子については、JSX が含まれるものは .jsx, そうでないものは .js としてみます。(全部 .js のパターンのほうが多そうで迷ってます。どうなんでしょう)

スタイルガイド

書き始めると、自分では良いスタイルが全然わからないので、Rubocop のようなものがほしいところです。

普段は Emacs で書きながら FlyCheck でチェックさせているので、近いものをさがしていたらまさにFlyCheck で ESLint するという記事を書かれている方がいましたので、参考にさせていただきました。

$ npm install -g eslint

.eslintrc

{
  "parser": "babel-eslint",
  "env": {
    "es6": true
  },
  "ecmaFeatures": {
    "jsx": true
  }
}

本来は eslint も package.json 管理下とするのがよさそうですが、現在の自分のエディタ設定の都合でグローバルに入れちゃいます。そのうちなんとかしたい。

JSX のシンタックスって?

単一の要素しか返せないのか? パラメータをまとめて渡すにはどうすれば?

などで無駄につまづいてしまいました。これは最初に https://facebook.github.io/react/docs/jsx-in-depth.html を読むべきでした。React.createElement() の別の記法だと理解していれば非常にシンプルです。

var Nav, Profile;
// Input (JSX):
var app = <Nav color="blue"><Profile>click</Profile></Nav>;
// Output (JS):
var app = React.createElement(
  Nav,
  {color:"blue"},
  React.createElement(Profile, null, "click")
);

パラメータについても、1つずつ <Comp param={value} /> でも良いしまとめて <Comp {...props} /> でも渡せます。

Debug どうすれば

トランスパイラをかませるとデバッガ上の表示が元ソースと別になる問題でしたが、source map という仕組みがありました。近年のブラウザは問題なく対応してるようで割と何年も前からあるみたいです。常識でしたか…

watchify に -d オプションと exorcist 挟むのを追加。

$ npm install --save-dev exorcist

package.json

  "scripts": {
    "watch": "watchify -v -t babelify -o \"exorcist ./public/bundle.js.map > public/bundle.js\" src/index.jsx -d",
    "build": "watchify -v -t babelify -o public/bundle.js src/index.jsx"
  }

エラー行の対応付け、ブレークポイントの設定ができるようになりました。しかし生成されるファイルは非常に巨大になるので dev 専用です。build と分けました。

Redux って?

Reducer, actionCreator, connect, mapXXXToProps, ... 見慣れない言葉が多くて全体が見えず、二度目の折れかけポイントでしたが、資料は少なくないので路頭に迷わずにはすみました。

最初に http://redux.js.org/index.html2.Basics まで目を通します。その後、こちらのビデオ をざーっと見て概要を理解できた気になれました。

概要はわかったものの、動く状態が無いとよくわからないので無理やり動くものを用意します。ファイル分割は一旦おいといて、index.jsx にまとめて書いてみました。

$ npm install --save react react-dom redux react-redux

index.jsx

import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import { connect, Provider } from 'react-redux'

/// actionCreators >>>
function action1(value) {
  return {
    type: 'ACTION1',
    value
  }
}
/// <<<

/// reducers >>>
function appReducer(state, action) {
  switch (action.type) {
    case 'ACTION1':
      return Object.assign({}, state, { v1: action.value });
    default:
      return state
  }
}
/// <<<

/// components >>>
const Comp1 = (props) => (
  <div>{console.log(props)}
    <div>This is Comp1. value is '{props.value}'.</div>
    <button onClick={() => props.action('VALUE')} >doAction1</button>
  </div>
);
/// <<<

class RootApp extends Component {
  render() {
    return (
      <div>
        Hello, this is root component.
        <Comp1
            value={this.props.value1}
            action={this.props.action1}
        />
      </div>
    )
  }
}

const App = connect(
  (state) => { return { value1: state.v1} }, // mapStateToProps
  (dispatch) => { return { action1: (v1) => dispatch(action1(v1)) } }// mapDispatchToProps
)(RootApp);

const rootElement = document.getElementById('content');
const initialValues = { v1: 'initialvalue' };
const store = createStore(appReducer, initialValues);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  rootElement
);

これで最低限は動くはずです。意図的に変な名前の部分と便利なヘルパーを使ってない部分がありますが、最終的には整理しています。

確認を兼ねてちょっと説明

先ほどの例で上のほうにあるのが Redux 世界のもので、

  • actionCreator は action を返す関数。
  • action というのはただのオブジェクト。type が必須。
  • reducer は、dispatch(action) された時に呼ばれる関数。全体の状態ツリーである state と action が渡ってくるので、action に応じて新しい state オブジェクトを生成して返す。
  • createStore() で全体の状態を保持する store を作成する。これには reducer と、必要なら初期値を与える。    - reducer は、state, action を受け取って新しい state を返す関数。

ここまでが Redux の枠組みかと思います。UI に直接は関係しないので、この時点で store.dispatch(action) のようにテストできます。(とビデオでもやってました)

次に、Redux 世界と React との接続については

  • おまじない的に store を与えた <Provider> をルートに置き、実質のルートになる <App> コンポーネントを入れる。   - <Provider> には1つしかコンポーネントを入れられない。
  • <App> は、connect() されたコンポーネントである必要がある   - この connect() というのは、Redux の store を React の props に接続するもの   - 第一引数が、reducer で返された state を props にマップする関数   - 第二引数が、actionCreators を props にマップする関数

となります。Component に props が渡った先は React の文脈になるので、普通に細かく Component を書いていけば OK です。ひとつ違うのは、Redux では state は1つなので、Component 自体の state を変更はできず、動的な値を扱うには上からパラメータを渡してもらうしかありません。

Redux のデバッグ

redux-logger ミドルウェアを入れると、dispatch の度に state を表示してくれます。

$ npm install --save redux-logger

redux-devtools なる本格的なのもあるようですがこちらはまだ試していません。

Redux で非同期 dispatch

どこで非同期リクエストのコードを書けば良いのかについて、http://redux.js.org/index.html の 3. Advanced 以降に詳しく書いてあります。このあたりredux-thunk ミドルウェア を使って actionCreator で fetch すればよいです。action を細かく分けると混乱せずに済みます。

$ npm install --save redux-thunk

非同期 API

前後しますが、そもそも非同期リクエストって何使えばよいの…って時点でもつまづいていました。

React Tutorial では、わかりやすさを重視したのか jQuery$.ajax を使っていましたが、リクエスト投げたいだけなのに jQuery を読み込むのは間違っている気がしますし、単独の非同期リクエスト用ライブラリで何がデファクト標準なのかを探すのに時間を取られてしまいました。

GitHub star や雰囲気を見ながらですと superagentaxios もよさそうですが、複雑なことはしないので単純でよいというのもあり、redux-async の例で使われていた fetch(isomorphic-fetch) でやっています。

$ npm install --save isomorphic-fetch

Redux で form

React の form の例を探すと、該当 Component に input 値の state を持たせるパターンばかりが見つかります…。

Redux で使えないため、値を伝搬させるのを自力で書こうかと思いましたが明らかに面倒くさいので redux-form ミドルウェア を使います。

$ npm install --save redux-form

非同期の redux-thunk のときもそうでしたが、こういう基本的な機能は本体付属のライブラリ無いのかな?と思いましたが無いようです。何気なくつぶやいたら Twitter で教えて頂けてとても安心しました。雰囲気としてどんどん外部ライブラリ使っていく、というものなんでしょう。

Redux のディレクトリ、ファイル構成

多くの例にあるとおりにしています。

src
├── actions                # redux actionCreators
├── reducers               # redux reducer functions
├── store                  # redux middleware 使えるようにするところ
├── containers             # react-redux 接続部分
├── components             # UI 部品
└── index.jsx              # エントリポイント

特に containers と components について、公式ドキュメントの http://redux.js.org/docs/basics/UsageWithReact.html が重要そうです。シンプルな場合なら、components には Redux 世界のものは全く含まれない状態になると思います。

redux-form の接続部分は containers なのか component なのかまだ理解が浅いです…。

Components の書き方

一つ一つの Component は割とスカスカになりそうに見えます。ライフサイクルイベント(componentDidMount とか) がどうしても必要な場合を除き、関数スタイルで書くのがすっきり書けてよさそうです。

import React, { PropTypes } from 'react'

const MyItem = (props) => {
  return (
    <span>{props.name} : {props.value}</span>
  )
}

MyItem.propTypes = {
  name: PropTypes.string.isRequired,
  value: PropTypes.string.isRequired
}

export default MyItem

最終的に

こうなりました(再掲) https://github.com/komazarari/react-redux-form-example

すっきりしましたが、特に reducer や form にまだ改善の余地がありそうです。

足りないこと

今回は CSS について触れていません。webpack とやらを使えばすっきりするらしいので引き続き試してみたいと思います。

日本人向け Packer スクリプト差分 for Ubuntu Trusty

Packer?

仮想マシン作成をコード化し、同一のソースから様々なフォーマットのVMイメージを作成する Packer というツールがあります。私は主にこれを VagrantBox 作成の自動化のために使ってます。

このための設定ファイルやスクリプトhttps://github.com/opscode/bento や veewee から変換して取得することができます。

最低限のローカライズをする

前述の設定ファイルをそのまま使えばほとんどの場合問題無いものの、自分用に作るならせめてタイムゾーンや近いミラーサーバくらいはデフォルトからいじっておいたほうが使い勝手がよいので、いくつか追記/変更しておく。

ひな形は bento

https://github.com/opscode/bento をちょっといじる

build JSON

packer/ubuntu-14.04-amd64.json について。packer の設定ファイル。

キーボード

最初のこれは正直どっちでもよいのだが、boot_command の時点で日本語キーボードに合わせてしまう

+ ” keyboard-configuration/layout=Japanese<wait>",
+ ” keyboard-configuration/variant=Japanese<wait>",

preseed.cfg

Debian 系のインストール自動化が行える設定ファイル。RH系の Kickstart みたいなもの json に preseed のパスが指定できる。今回はhttp経由で packer/http/ubuntu14.04/preseed.cfg 等が使われる

タイムゾーン

下記を編集

--- a/packer/http/ubuntu-14.04/preseed.cfg
+++ b/packer/http/ubuntu-14.04/preseed.cfg
@@ -27,3 +27,3 @@ d-i pkgsel/update-policy select unattended-upgrades
 d-i pkgsel/upgrade select full-upgrade
-d-i time/zone string UTC
+d-i time/zone string Japan
 d-i user-setup/allow-password-weak boolean true

d-i clock-setup/utc はシステム時刻の設定なので特に変えないほうがよい。

country

apt のミラーがデフォルトで us.archive.ubuntu.com になってしまうので日本のミラーにしておく

+ d-i debian-installer/country string JP
+ d-i mirror/http/mirror select jp.archive.ubuntu.com

を加える

その他のカスタマイズ

あとは細々とやりやすいように変更する

  • chef の最新版を入れておきたい
  • VirtualBox GuestAdditions をビルドできる環境はクリーンアップせずに残しておきたい

結果

こんな感じにしました。 https://github.com/komazarari/packer