【Jest, enzyme】 useSelectorやuseDispatchを使ったFunctionComponentをうまくテストする
ReactのテストでJestやenzymeを使うことは多いと思うのですが、React v16.8以降でFunctionComponentでの書き方を使う機会が増えたにもかかわらず、useSelectorやuseDispatchあたりをclassでなくFunctionComponentで使ったテストケースがあまり見つからず困ったのでサンプルをいくつか書いてみます。
typescriptで書いてますが、使わない場合はよしなに読み替えていただければと思います。
The product code
かなり単純化したものになりますが、下記のようなコードに対してテストを書きたい場合です。
import React from 'react' import { useDispatch, useSelector } from 'react-redux' import * as actions from './actions' const UserList: React.FC = () => { const dispatch = useDispatch() const users = useSelector(state => state.reducer.users) const companies = useSelector(state => state.reducer.companies) const submit = (userId: number) => { dispatch(actions.postUser(userId)) } return ( <div> {users.map(user => <div> <p>{user.name}</p> <button onClick={() => submit(user.id)}>submit</button> </div> )} </div> ) }
Writing Test code with mocking
単純なのですが、いろいろハマるところがあったのでひとつずつ解いていきます。
※ やり方はここで書く以外にも何通りかあります
# UserList.spec.tsx import React from 'react' import { useSelector, useDispatch } from 'react-redux' import { shallow } from 'enzyme' import * as actions from './actions' jest.mock('react-redux') // -> ① jest.mock('./actions') // -> ②
まず、① react-redux
をimportして jest.mock
しておくのが最初のポイントです。② actionsもここでmockしておくと良いです。
const useSelector = useSelector as jest.mock // -> ③
useSelectorはここでmockしましょう。いよいよ具体的にテストを書いていきます。
describe('UserList', () => { afterEach(() => { jest.resetAllMocks() // -> ④ }) let wrapper beforeEach(() => { const users = [{ id: 1, name: 'userName' }] as User[] const companies = [{ id: 1, divisionName: 'R&D' }] useSelectorMock.mockReturnValueOnce(users) // -> ⑤ useSelectorMock.mockReturnValueOnce(companies) // -> ⑥ useDispatch.mockReturnValue(jest.fn()) // -> ⑦ wrapper = shallow(<UserList />) }) it('renders view', () => { expect(wrapper.text()).toEqual( expect.stringContaining('userName') ) }) })
これでテストには通ると思います。④でafterEachとしてmock化の解除を行います。beforeEachの中が需要で、⑤⑥でReturnValueOnceを設定することで、それぞれuseSelectorの返すべき値をusersとcompaniesに振り分けています。simulateなどで再レンダリングさせる際は、このペアをもう一度事前に登録しておくことを忘れないようにしてください。⑦はuseDispatchのmockです。
Additionally
ついでに、actionのテストも簡単に見てみましょう。
describe('click the button', () => { let spy beforeEach(() => { // -> actionsは最初にmock化してあります spy = jest.spyOn(actions, 'postUser') // -> 先程のbeforeEachが効いているscope内として、 // あと2つ再レンダリング用に書きます useSelectorMock.mockReturnValueOnce(users) useSelectorMock.mockReturnValueOnce(companies) }) it('calls api', () => { wrapper.find('button').simulate('click') expect(spy).toHaveBeenCalled() }) })
こんな感じです。 import * as actions
のmock化とかも地味に躓きましたが、上記のような感じで上手く行けると思います。他にも act
周りとかで細かいtipsはあるのですが、それはまたの機会に。enzymeのshallow, mountになれるのもjestテストを快適に書くポイントですよね。
StorybookをReact, Typescriptな環境に導入する
StorybookをReact, Typescript, Atomic Designな環境に導入する手順を紹介します。
Why Storybook?
Frontendの開発を行っていると、再利用性を高めたいという意識が湧いてくると思うのですが、それをチーム間で共有したり、デザイナーさんと認識を合わせるのが容易でなかったりします。また、開発者も「このコンポーネントどこでどう使ってたっけ?」と言うように、視認性を持ちながら検索性も高くするというのがなかなか難しかったりします。
そこで、Storybook ( https://storybook.js.org ) の出番です。
How to install Storybook in my environment?
まず、先にも記述しましたが、今回は、下記の要件のアプリへの導入を紹介します。
- React.js
- yarn
- npmな方は適宜読み替えてください
- Typescript
- 特に使ってなければ問題ないのですが、Typescript特有のハマリポイントの紹介もしています
- SCSS (CSS)
- SCSSを利用しているコンポーネントの使用にもいくつかハマリポイントがあったのでそこも紹介しています。
- Atomic Design
- こちらもこれで運用していなくても全く問題なく読めますが、例として考えやすいのでこちらをベースにしています。
公式ドキュメントはこちらです。
https://storybook.js.org/docs/guides/guide-react/
また、Storybookには数多くのaddonがあり、それらを利用することでより便利にすることが可能です。この記事の中でもある程度メジャーそうなaddonをインストールしながら進めていきます。
まずは下記のコードをFrontend root(package.jsonなどを置いている場所)な場所で実行しましょう。
$ npx -p @storybook/cli sb init
これでかなりの部分をいい感じに自動で用意してくれます。下記のコマンドを打てばひとまずstorybookを確認することができます。(デフォルトで、Welcome.jsとButton.jsが用意されているはず)
$ yarn storybook
いい感じですね。
さて、ここからが本題です。サンプルコードは .js
で書かれていますが、 .ts
あるいは .tsx
で作りたいですよね。そうするといくつか追加で設定が必要になってきます。
その前に、軽くstorybookのディレクトリ構造などに触れておきたいのですが、一番はじめのコマンドで作られたのを見るとわかるように、storybookは下記の2つのディレクトリをベースに作っていきます。
stories ├── atoms/ └── molecules/ .storybook ├── config.js ├── main.js ├── manager.js └── preset.js
.storybook
(各ファイルはあとから出てくるので今わからなくても問題ないです)
- config.js
- 全体に反映させたい設定などを行います
- main.js
- webpack.config.jsのstorybook用という感じのファイルです。loaderの設定などを行います
- manager.js
- addonの初期設定を行います
- preset.js
- こちらはtypescript用に一行設定が必要なファイルです
stories
storybookでは、ひとつひとつのファイルをstoryと呼び、ファイル名.stories.拡張子
という形で保存すると、storybookに反映されます。このディレクトリ構造は一旦atomic designに沿って、atoms, moleculesというような形にしてあります。
次に進む前に、前述したaddonをいくつか入れておきましょう。
$ yarn add -D @storybook/addon-knobs @storybook/addon-viewport @storybook/addon-storysource react-docgen-typescript-loader @storybook/addon-info @storybook/addon-console
typescript用のライブラリも入れておきます。
$ yarn add -D @types/storybook__react @types/storybook__addon-info @types/storybook__addon-actions @types/storybook__addon-knobs
ちなみに、どのライブラリだったか忘れたのですが、2020/02/05現在でcore-jsのv2に依存しているのがありまして(stableで最新がv3なのでイケてないのですが)、core-js v2を入れなければなりません。実コードでv3使ってる方は、addonを外すようにしましょう。core-js v2の入れ方は下記のとおりです。
$ yarn add -D core-js@2
さて、各ライブラリが何をやっているのかは別途検索していただければと思いますが、簡単に紹介しておくと、
- addon-info
- componentが受け取るpropsの情報や、componentの使い方をviewに表示してくれる
- addon-knobs
- propsの内容をviewから変更してデザインの変化などを確認できる
このあたりは便利です。
では、早速typescriptに合わせて設定を行っていきましょう。まずは、簡単なところから。 .storybook/preset.js
に下記のように書いて保存しましょう。
module.exports = ['@storybook/preset-typescript']
次に、.storybook/main.js
を、下記のように書いて保存します。addonの設定なども既に記述しています。
const path = require('path') module.exports = { stories: ['../stories/**/*.stories.tsx'], addons: [ '@storybook/addon-links', '@storybook/addon-knobs/register', '@storybook/addon-actions/register', '@storybook/addon-viewport/register', { name: '@storybook/preset-typescript', options: { tsLoaderOptions: { configFile: path.resolve(__dirname, './tsconfig.json') } } } ], webpackFinal: async config => { config.module.rules.push({ test: /\.(ts|tsx)$/, use: [ { loader: require.resolve('babel-loader') }, // Optional { loader: require.resolve('react-docgen-typescript-loader') } ] }) config.module.rules.push({ test: /\.scss$/, use: ['style-loader', 'css-loader?modules', 'sass-loader'], include: path.resolve(__dirname, '../') }) config.resolve.extensions.push('.ts', '.tsx') return config } }
webpack.config.jsと同じように、typescriptとscssのloaderの設定をここで行います。addonの登録もここで行います。
これで一通りの設定はおしまいです。例として、サンプルのWelcome.jsとButton.jsを Welcome.tsx
と Button.tsx
に変更して、 yarn storybook
でstorybookを立ち上げてみましょう。うまくいきましたか?
Advanced (さらに便利に)
グローバルなaddonの初期設定
Storybookのパネルが下に表示されると思うのですが、邪魔なので右側にしたい。といった場合に、 .storybook/manager.js
にその設定を書いておけば、常に右側になります。
import { addons } from '@storybook/addons' addons.setConfig({ panelPosition: 'right' })
すべてのstoriesにaddonを適用
addon-info
や addon-knobs
は便利なので、すべてのstoriesに適用させておきたくなります。 .storybook/config.js
に下記のように書いて保存しましょう。
import { addDecorator } from '@storybook/react' import { withInfo } from '@storybook/addon-info' import { withKnobs } from '@storybook/addon-knobs' import '../css/app.css' addDecorator(withInfo({ inline: true })) addDecorator(withKnobs)
ついでに、実コードのcssをインポートする方法も載せておきました。こうしたstoriesへのグローバルな設定はここで行なえます。
静的ファイルがstorybookでうまく読み込めない
storybookの立ち上げコマンドを工夫すればできるようになります。package.json
にかかれている実行コマンドを下記のように修正しましょう。
"storybook": "start-storybook -p 6006 -s ./staticファイルのディレクトリ名",
これでかなり便利にStorybookを使えるようになったと思います。最後にサンプルとして上記の設定を行った上でのコードを載せておきます。
import React from 'react' import { action } from '@storybook/addon-actions' import { storiesOf } from '@storybook/react' import { text, boolean } from '@storybook/addon-knobs' import Button from '@/components/atoms/Button' // storyに載せたいコンポーネントをインポート storiesOf('atoms', module).add('Button', () => ( <Button title={text('Title', 'テキスト')} disabled={boolean('Disabled', false)} onClick={action('clicked')} /> ))
以上です。Storybookはatomic designを推進するのにもかなり相性良さそうですね!
【実践: 詳しくわかる】TerraformでCircleCIを通してAWSにECS環境を自動構築する方法
まず最初に今回の記事で扱う内容を書かせていただきます。
1. docker-composeで開発環境を構築し 2. masterにpushした時のみCircleCIでECRにDockerイメージを構築し 3. 同時にCircleCIからterraformで本番環境のインフラをAWSにECSで整え 4. Deployを完了する
ということを実際のコードを交えながら解説していきたいと思います。つまるところmasterにプッシュしたら自動でDockerイメージが作られて自動で環境整えてデプロイされている状態を目指す、という話です。Blue/Greenデプロイと呼ばれるやつの自動化ですね。
技術的には、下記のものを扱いますが、ところどころご自身の技術スタックと違っていても読み替えられるかと思います。
- terraform v0.12.18 (v0.11とv0.12で少し違いがあるので注意)
- Rails (他のWAFでも読み替えられると思います)
- CircleCI ( Continuous Integration and Delivery - CircleCI )
- AWS & ECS (AWSに関してはECS以外にも様々なサービスをterraformを通して利用します)
Terraformの構成としては、環境ごとのVPC内に、2つのavailability zoneを用意し、それぞれの中にpublic, private subnetを1つずつ用意します。ロードバランサがpublicにいて、Clusterはprivateにいるような形です。また、web app環境としてstagingとproductionを用意し、RDS(MySQL)とつなぎます。Route53で設定しているドメインの設定や、taskでmigrationを走らせるところなどもカバーしています。
1. docker-composeで開発環境を構築
まずは手始めに一番簡単なところから行きます。最早あらゆる視点から考えて開発環境はdocker化しておいたほうが楽なので必須ですね。ここは簡単にyamlファイルだけ載せておきます。
ポイントとしては、Dockerfileのdev用を用意しておくことでしょうか。Rails envの設定などをproductionとわけたり、productionではサイズの小さいimageを使いたいなどの要件に対応できるようにしておきます。
2. masterにpushした時のみCircleCIでECRにDockerイメージを構築
次に、CircleCIの設定をしていきましょう。本当はTerraformから書いたほうが順序的には良いと思うのですが、Terraformの方は長くなるので、後にまわします。CircleCIをRailsに組み込むあたりは、それほど難しくないと思うので割愛します。CircleCI公式サイトの手順に沿って行ってください。
早速ですが、先に .circleci/config.yml
の中身を載せます。その後、重要なポイントを解説していきます。
いくつかポイントになりそうな部分を見ていきましょう。
テスト
このファイルでもいくつかポイントが有るのですが、まず、CircleCI上でテストをまわすところです。CIでテストを回す方法は色々ありますが、ここではローカルでテストを回すのと同じように、docker-composeで構築してテストをさせています。jobs -> testの部分になります。deploy前にテストを回すようにしています。
条件によってデプロイする
このファイルでは、masterブランチにマージされたときにstagingにデプロイして、tagが打たれたときにproductionにデプロイするようにしています。この設定は filters
で行えます。
ECRにイメージを作る
まずこの話に入る前に、CircleCIにはorbsという仕組みがあり、それを使えばある程度処理が楽になることを知っておきましょう。これにより、ECRへのpushは非常に簡単になっています。workflowの aws-ecr/build-and-push-image
がその設定になります。この際にいくつかパラメータが必須になっているので、設定しています。ひとつポイントとして、 extra-build-args
というパラメータを設定でき、ここで渡したものは、Rails側でENVとして受け取ることができます。こうすることで環境変数の管理は楽になります。また、 ${CIRCLE_SHA1}
は、CircleCI側で乱数を生成して設定してくれるもので、自分で設定する必要はありません。ちなみに、ENV(環境変数)はそもそもどこに設定するのかという話ですが、CircleCIに環境変数を設定する箇所があります。(もしわかりにくければ画像キャプチャを載せることを検討します)
Terraformでデプロイする
Terraformの中身をまだ見ていないので細かいところはわからないと思うのですが、流れとしては、
- git cloneでterraformのrepositoryを落としてくる(ここではterraform_ecs)
- terraform getでmoduleを使用可能にする
- terraform init
- terraform applyで実際にデプロイする
という感じです。applyの部分にいくつかポイントがあるのですが、まず、 -input=false -auto-approve
これでterraformが対話的に聞いてくるのをdisabledしています。その代わりに必要な変数などを -var
と -var-file
で補っています。-varはgit管理したくない機密情報を、-var-fileは環境ごとに違う値を設定したいだけで特にpublicでも問題ないものをファイルに設定して読み込むようにしています。
3. 4. CircleCIからterraformで本番環境のインフラをAWSにECSで整えデプロイ完了
さて、いよいよ本題です。CircleCIでterraform applyするところは先程書いたので、terraformの中身を見ていきましょう。
※ terraformのインストールや、terraform自体の詳しい使い方はご自身で調べてください
Repositoryはこちら github.com
terraformのディレクトリ構成のベストプラクティスはいくつかあるようですが、今回は下記のように配置しています。modulesで使いまわしできるものを用意し、staging, productionなどのように環境ごとにmainファイルを配置します。
. ├── modules │ ├── ecs │ │ ├── main.tf │ │ ├── outputs.tf │ │ ├── policies │ │ │ ├── ecs-autoscale-role-policy.json │ │ │ ├── ecs-autoscale-role.json │ │ │ ├── ecs-execution-role-policy.json │ │ │ └── ecs-task-execution-role.json │ │ ├── tasks │ │ │ ├── db_create_task_definition.json │ │ │ ├── db_migrate_task_definition.json │ │ │ └── web_task_definition.json │ │ └── variables.tf │ ├── networking │ │ ├── main.tf │ │ ├── output.tf │ │ └── variables.tf │ └── rds │ ├── main.tf │ ├── output.tf │ └── variables.tf ├── production │ └── production.tf └── staging ├── _main.tf ├── route53.tf ├── staging_key ├── staging_key.pub ├── terraform.tfvars └── variables.tf
modules/ecs
まずはmodulesのecsの中から見ていきましょう。こちらになります。
https://github.com/mr-myself/terraform_ecs/blob/master/modules/ecs/main.tf
ECS自体の説明は端折りますが、web appが動くものをserviceとして管理し、migrationなどの単発で良いものはtaskとして定義します。ここでは、まず、Clusterを環境名をprefixに用意し、その中にtaskを用意していきます。細かい説明をするよりはファイルを見ていただいたほうが早いかと思いますが、ロードバランサの設定などやAutoScallingの設定などもここで行っています。
modules/networking
次にnetworkです。vpcやgatewayの設定を行います。最初に書いたpublic, private subnetもここで定義します。
https://github.com/mr-myself/terraform_ecs/blob/master/modules/networking/main.tf
modules/rds
次にRDS周り。subnet, security group, dbのインスタンス定義などを行っています。MySQLの5.7として作っています。
https://github.com/mr-myself/terraform_ecs/blob/master/modules/rds/main.tf
補足として、上記の各所で出てくる、 var.xxx
という記述ですが、名前の通り変数を渡しています。使いたい箇所で var.xxx
のように書き、同じディレクトリ内のvariables.tfに使いたい変数を定義し、 staging/_main.tf
などから値を渡すことができます。
staging/__.tf
最後にstaging環境の方を説明して終わりにします(productionもほぼ同じなので)。まずは、アクセスキーを作る必要があるので、staging_keyとしてkey-genで作っておいてください。今回はsampleなので空のファイルを配置していますが、通常はprivate keyはこんな風にさらさないようにしてください。
デフォルトの値としていくつか用意しておきたいものは、terraform.tfvarsに予め値を記載しています。また、短いのでさらっと書きますが、 route53.tf
でドメインの設定などを行っています。
最後に、 _main.tf
です。まず、terraformはstateをどこかに保持しておく必要があり、保持することができる場所は様々あるのですが、今回はせっかくガッツリAWSを使っているので、S3に置くような設定にしています。
terraform { backend "s3" { encrypt = "true" region = "ap-northeast-1" bucket = "terraform_ecs-tfstates" key = "terraform.tfstate" acl = "bucket-owner-full-control" } }
あとは、先程説明してきたmodulesをstaging環境として使えるように、変数に値を入れていっているだけです。
以上となります。さらっと書いたのですが、構築中はいろいろ模索したので、ハマリポイントを思い出したらまた追記していきたいと思います。わかりにくい点などあればコメントいただければと思います。terraformで構築するのは初めてだったのですが、一旦作ってしまうと楽ですし良いですね。
Reactでドロップダウンメニューを実装する
Reactでドロップダウンメニューを作成する機会があったのですが、いくつかハマりポイントがあったので書いておきます。まずはじめに要件としては下記のことがあげられます。
- ボタンを押すとdropdownメニューが開く
- ドロップダウンメニュー以外の場所を押すとblurを発火しメニューを閉じる
- メニューボタンはトグルになっていて、open or closeできる
- 中のリンクももちろん押せる
なお、Reactはhooksを使用可能なversion以上とします。
実際に書き始めるとなぜこの記事が必要になるか気づかれるかと思うのですが、例えば簡単に下記のように書いてみたとします。
const Menu = () => { const [isOpen, setIsOpen] = useState(false) return ( <div> <img src={Button} onClick={() => setIsOpen(!isOpen)} onBlur={() => setIsOpen(false)} /> {isOpen && <ul> <li>menu1</li> <li>menu2</li> </ul> } </div> ) } export default Menu
一見特に問題なさそうに見えるかもしれませんが、これではonBlurが発火しません。メニューの方にonBlurを当てるのが正しいと思い、下記のように修正してみます。
const Menu = () => { const [isOpen, setIsOpen] = useState(false) return ( <div> <img src={Button} onClick={() => setIsOpen(!isOpen)} /> {isOpen && <ul onBlur={() => setIsOpen(false)} > <li>menu1</li> <li>menu2</li> </ul> } </div> ) } export default Menu
これでもblurの発火がうまくいかないはずです。これらはメニューにフォーカスが当たらないことが原因なので、そこをuseRefなどを使って修正してみます。
const Menu = () => { const [isOpen, setIsOpen] = useState(false) const menuRef: any = useRef() useEffect(() => { isOpen && menuRef.current.focus() }, [isOpen]) return ( <div> <img src={Button} onClick={() => setIsOpen(!isOpen)} /> {isOpen && <ul onBlur={() => setIsOpen(false)} ref={menuRef} tabIndex={1} > <li><a href="/somewhere">menu1</a></li> <li>menu2</li> </ul> } </div> ) } export default Menu
ちょっと動く気配がするようになってきました。ここではrefを使ってfocus対象を決め、isOpenの値が変わったときにfocusを当てるようにしています。忘れがちなポイントとして、tabIndex={1}
としておかないと、divなどの要素にfocusを当てれないので注意です。ただ、これだとまだ完璧にはうまく行きません。メニューの中の要素をクリックしてみると、リンクに飛べないことがわかります。これはReact特有の問題のようですが、子をクリックすると親のblurが先に発火してしまうので、子のイベントを発火できなくなるようです。これはちょっとイマイチな解決方法ですが、timeoutを使って回避しましょう。下記が完成形です。
const Menu = () => { const [isOpen, setIsOpen] = useState(false) const menuRef: any = useRef() useEffect(() => { isOpen && menuRef.current.focus() }, [isOpen]) return ( <div> <img src={Button} onClick={() => setIsOpen(isOpen ? false : true)} /> {isOpen && <ul onBlur={() => setTimeout(() => setIsOpen(false), 100)} ref={menuRef} tabIndex={1} > <li><a href="/somewhere">menu1</a></li> <li>menu2</li> </ul> } </div> ) } export default Menu
これでうまく1~4の要件が満たせました。(しれっと上で変えましたが、imgにセットしたonClickのところもblurで変な挙動をしないためのポイントです。)
Ectoチートシート(データ取得系)
EctoとはORマッパーで、DBとのインターフェースになってくれるものです。RailsでいうActiveRecordです。 Phoenix/ElixirはわかりやすいけどEctoで躓く、という方向けのチートシートになります。Rails使いでPhoenix書いてみてる方が多いと思うので、Rails(ActiveRecord)の文法と比較して載せています。
英語問題ない方はEctoのGetting Startedがめちゃわかりやすいので、そちらを読んでみることをおすすめします。(※下記のサンプルもそちらを抽出したものです)
Getting Started — Ecto v3.1.7
前提として、FriendsというDB内のPeopleというtable(なのでモデル名は単数のPerson)を使った操作です。取得系のみ載せており、create, update, deleteは上記のドキュメントを読んでください。
対象のテーブルの最初の1件を取得
# ActiveRecord Person.first # Ecto Friends.Person |> Ecto.Query.first |> Friends.Repo.one
対象のテーブルの最後の1件を取得
# ActiveRecord Person.last # Ecto Friends.Person |> Ecto.Query.last |> Friends.Repo.one
対象のテーブル全件取得
# ActiveRecord Person.all # Ecto Friends.Person |> Friends.Repo.all
対象のテーブルのid: 1を取得
# ActiveRecord Person.find(1) # Ecto Friends.Person |> Friends.Repo.get(1)
特定カラムの条件で1件取得
# ActiveReocrd Person.find_by(first_name: “Ryan”) # Ecto Friends.Person |> Friends.Repo.get_by(first_name: "Ryan")
特定カラムの条件でフィルタリングし全件取得
# ActiveRecrod Person.where(last_name: “Smith”) # Ecto Friends.Person |> Ecto.Query.where(last_name: "Smith") |> Friends.Repo.all
注意
Ectoで変数名を条件内に使いたい場合は、pin operatorを使う必要があります。
Friends.Person |> Ecto.Query.where(last_name: ^last_name) |> Friends.Repo.all
Elixir on PhoenixでDBConnectionエラーでアプリが落ちる場合の解決策
環境: Elixir 1.9.1, Phoenix 1.4を使用
エラー内容は下記の通り。mixにライブラリを追加した後に起こる。急にDBConnectionと言われてDB周りのバグかと思い調べたがそうではなく、mixの書き方の問題だった。
web_1 | web_1 | Generated your_app app web_1 | ** (Mix) Could not start application your_app: YourApp.Application.start(:normal, []) returned an error: shutdown: failed to start child: YourApp.Repo web_1 | ** (EXIT) shutdown: failed to start child: DBConnection.ConnectionPool web_1 | ** (EXIT) exited in: GenServer.call(DBConnection.Watcher, {:watch, DBConnection.ConnectionPool.Supervisor, {DBConnection.ConnectionPool.Pool, {#PID<0.374.$>, #Reference<0.342264912.689831938.179060>, Postgrex.Protocol, [types: Postgrex.DefaultTypes, repo: YourApp.Repo, telemetry_prefix: [:your_app, :repo], otp_app: :your_app, timeout: 15000, username: "postgres", password: "postgres", database: "your_app_dev", hostname: "db", port: 5432, pool_size: 10, pool: DBConnection.ConnectionPool]}}}, infinity) web_1 | ** (EXIT) no process: the process is not alive or there's no process currently associated with the given name, possibly because its application isn't $ tarted
起こったこととしては下記と同様。 elixirforum.com
解決策としては下記のリンクの通り。
ライブラリを入れる時に mix.exs
の下記パートに applications: [:library_name]
を書くとREADMEにあることが多々あるが、Elixirがよしなにやってくれるので書かなくてよい。
def application do [ mod: {YourApp.Application, []}, extra_applications: [:logger, :runtime_tools] ] end
私が今回入れてこのバグを引き起こしたのは下記のライブラリ。こちらのREADMEを読むと、applicationsパートにライブラリ名を追加しているが、それは不要。 GitHub - fdietz/elixir-feed-parser: Elixir Feed Parser
エラー文言から理解しにくいバグなので、参考になれば。これPhoenixのバージョンいくつ以降は書かないでよくなったみたいな感じなのかも。Elixir, Phoenixは初学者なのでそのあたりの知見がない。
Rails + webpacker on Dockerの環境をdocker-composeで構築する
dockerは知識として入れてはいましたが、実際にdocker上で開発をしたことはなかったので、勉強がてらやってみました。そんなにハマりどころもなく、便利なので今後はこれを基本にしていくと思います。
まず前提として、今回はdocker-composeで開発環境を作るというところまでを想定していて、本番運用はスコープ外とします。細かい手順は覚えていないので、ファイルベースで進めていきます。
# docker-compose.yml version: '2' services: rails: &app_base build: context: . dockerfile: "Dockerfile.dev" command: /bin/sh -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'" env_file: - "./.env.dev" volumes: - ".:/app" volumes_from: - data - bundle ports: - "3000:3000" depends_on: - db tty: true stdin_open: true webpack: <<: *app_base command: bash -c "rm -rf /app/public/packs; /app/bin/webpack-dev-server" ports: - "3035:3035" depends_on: - rails tty: false stdin_open: false bundle: image: app_rails # 開発環境による。前述のrails imageの名前を記載 volumes: - /bundle data: image: "busybox" volumes: - "/local_path:/var/lib/mysql/data" # どこでも良いのでローカル環境にsharedとしてdirectoryを用意する db: image: mysql:5.7 restart: always environment: MYSQL_USER: root MYSQL_ROOT_PASSWORD: password ports: - "3306:3306" volumes_from: - data command: --innodb-use-native-aio=0 --ignore-db-dir=data
だいたい読んで頂けばわかるかと思いますが、rails, webpack, data, dbという4つを用意しています。volumesを".:/app"
とすることで、通常の開発のように手元で好きなエディタで開発できるのが良いです。また、dbもbusyboxでサンドボックス的に気軽に用意できるので、例えば案件によって違うバージョンを使いたい、違うDBを使いたいとかでも問題なく対応できます。
# Dockerfile.dev FROM node:11.12.0 as node FROM ruby:2.6.1 ENV LANG C.UTF-8 ENV BUILD_PACKAGES="ruby-dev bash" \ DEV_PACKAGES="libxml2-dev libxslt-dev tzdata" \ RUBY_PACKAGES="ruby-json nodejs" RUN apt-get update -qq -y && \ apt-get upgrade -y && \ apt-get install -y --no-install-recommends \ build-essential \ libpq-dev \ wget \ ca-certificates \ $BUILD_PACKAGES \ $DEV_PACKAGES \ $RUBY_PACKAGES \ libfontconfig1 && \ rm -rf /var/lib/apt/lists/* ENV ENTRYKIT_VERSION 0.4.0 RUN wget https://github.com/progrium/entrykit/releases/download/v${ENTRYKIT_VERSION}/entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \ && tar -xvzf entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \ && rm entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \ && mv entrykit /bin/entrykit \ && chmod +x /bin/entrykit \ && entrykit --symlink ENV YARN_VERSION 1.15.2 COPY --from=node /opt/yarn-v$YARN_VERSION /opt/yarn COPY --from=node /usr/local/bin/node /usr/local/bin/ RUN ln -fs /opt/yarn/bin/yarn /usr/local/bin/yarn \ && ln -fs /opt/yarn/bin/yarnpkg /usr/local/bin/yarnpkg ENV BUNDLE_JOBS=4 \ APP_DIR=/app/ \ BUNDLE_PATH=/bundle/ RUN mkdir $APP_DIR WORKDIR $APP_DIR COPY ./Gemfile $APP_DIR COPY ./Gemfile.lock $APP_DIR RUN gem install bundler RUN bundle config build.nokogiri --use-system-libraries RUN bundle install --no-deployment --quiet --path /bundle RUN yarn install --check-files COPY ./ $APP_DIR CMD [ \ "prehook", "rm /app/tmp/pids/server.pid" \ "prehook", "ruby -v", "--", \ "prehook", "bundle install -j3 --quiet --path /bundle", "--"]
Dockerfileはこんな感じです。こちらは基本的なインストール周りを行い準備しているだけという感じですね。必要に応じて変更してください。Railsの場合、ポイントとしてconfig/database.yml
をどう書くかというところで迷うかと思うのですが、こちらもdockerの記述をうまく使えばとても簡単です。
# config/database.yml default: &default adapter: mysql2 host: db pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> username: root password: password timeout: 5000 development: <<: *default database: app_development test: <<: *default database: app_test production: <<: *default host: <%= ENV['HOST'] %> database: app_production username: username password: <%= ENV['PASSWORD'] %>
defaultのhostがdb
になっているところに注目してください。これはdocker-compose.ymlで指定したdbを指しています。これだけでうまく連携してくれるので簡単ですよね。
基本的には以上となりあます。あとは適当にdocker-compose, dockerコマンドを組み合わせて開発をすすめましょう。例えば下記のような感じでコマンドを使えます。
$ docker-compose up $ docker-compose run rails rake db:create $ docker-compose run rails bundle exec rails db:migrate $ docker-compose exec rails bundle exec rails c $ docker-compose logs -f # see logs
以上です。