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 synth
やcdk 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.ts
は cdk 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.json
に postinstall
を追記しておきます。実行するのは、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確認します。
無事に Layers で含めた外部モジュールを呼び出せていることを確認できました。
おわりに
CDK で Layers を含む Lambda を作成できること、および作成したものを他のアプリケーションから使えることを確認しました。パッケージ管理ができるようになるとインフラのモジュール化が実用的になってくるので、IaC の世代が一歩進む感覚があります。
不便に感じた部分としては、ライブラリ側と呼び出し側で CDK バージョンを揃える必要がある点です。CDK 自体のバージョンアップは非常に頻繁に行われているので、ライブラリとして作るなら常に最新版に対応して公開していく仕組みを自動化するなどが必要かと思いました。引き続き CDK は手探りを続けていきたいと思います。
-
cdk init
生成したときの .gitignore, .npmignore のままでは git レポを install しようとしてもパッケージにならないので調整が要ります↩
AWS CDK さわってみた、たのしかったのだけど
AWS CDK Workshop をやってみたので、 普段使っている従来の CloudFormation や、ちょっと触っていた SAM と比べてどうか、という視点で感想を書きます。
AWS CDK とは
プログラミング言語を使って抽象化された CloudFormation を書けるフレームワークです。生の CloudFormation に比べて抽象化されており、ループや条件分岐などのプログラミング言語のテクニックを使えるため冗長な記述を減らすことができたり、まとまった単位でモジュールとして切り離して再利用できるなどのメリットがあります。
2020/01 現在使える言語は
- Javascript/TypeScript
- Python
- .NET
- Java
があります。
専用の 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 はビルドの必要がありました。他にも、例えば CURL は docker-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/console
で CLI が使えるようになっています。他のフレームワークでも Rails の bin/
のように 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 アプリを立ちあげられるようにはならなそうなので、それはそれ、これはこれという感じで。
やりたいこと
普段の仕事では 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 + zsh が起動するようになると思います。
HOME 環境変数を設定する
シェル内や後述の Emacs の動作を Unix 系に近づけるため、環境変数 HOME
を設定します。
C:\Users\username
あたりが HOME
になってほしいので、%USERPROFILE%
をそのまま設定で ok です。
ちなみに、PowerShell で env
のように環境変数を一覧したい場合は 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/config
も eval `ssh-agent`
も問題ありません。
Git, Ruby, Node
Git, Ruby, Node は chocolatey で入れます。
PC > choco install git ruby nodejs
Ruby は C:\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/NTEmacs64 の IME パッチ版を使用します。これはパッケージ管理から漏れますが諦めました…。
runemacs ショートカットに[作業フォルダー]を指定することで、起動直後の場所を自由に設定できます。
その他
- 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.html
と example.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.html の 2.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 や雰囲気を見ながらですと superagent や axios もよさそうですが、複雑なことはしないので単純でよいというのもあり、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