やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

【レポート】第13回清流elixir勉強会を開催しました【Elixirで作成されたOSSのコードを読んでみる会】

トピック

elixir-sr.connpass.com

第13回目の清流elixirの勉強会を開催しました
今回はOSSのコードを読んでElixir力をあげるという目的かつ、弊コミュニティ初の試みとしてリモート(Zoom)での参加にも対応しました
これで全国からのジョインが可能に!! 勉強会の当日は東海と九州が繋がり、事実上の中部から西日本を制覇したことになった。すごい

初のリモート開催のためZoomに手馴れておらず、バタついてしまいました。申し訳ありません。次はもっとスマートに出来るかと思いますので多めに見て頂きたいですm( )m

清流elixir-infomation
開催場所: 丸の内(愛知)
参加人数: 5 -> 8 update!! コミュニティ参加人数 : 16 -> 21 update!
20191026現在

第13回の勉強会の内容について

コードを早く、美しく形に出来るようにするためには、コードを書く経験を積むことも重要だが、達人のコードを読むという経験も非常に重要だと考えている。Elixirの基礎的な文法に関してはある程度、習得が完了したはずなので、Elixirを使って書かれたOSSのコードで実際にどのような記述が現れてくるのかを、1人だと辛いので、集まって読んでみるかという流れに

時間の都合もあるので、コード量が多すぎず、コード量が少なすぎず、かつOSSの内容が比較的分かりやすいものを用いたいということで、私の独断と偏見でTrotというphoenixとは異なるmicro web frameworkのOSSを選択しました

github.com

まず先に得られた結果・知見から

...

俺の知っているElixirと違う...

なんじゃこれ。知らないことばっかりなんですが、それは..
syntaxに関しては概ね把握しているつもりであったし、Enumでパイプ使って脳汁ブシャーッってのも気持ちよく出来るレベルなんで、まぁ読めるやろと思ってたけど、自分のレベルを思い知らされた

特に苦労したのがmacroに関しての理解。存在は知っているし、プログラミングElixirでも扱われているトピックだったので、うっすら頭に知識があるはずだが実際のところ黒魔術的要素が強いのでユースケースまでが想定できていなかった

Trotの内部の至る所にmacroがふんだんかつ、おしゃれに使われており、頭が????となった
最初に遭遇した頭を悩ませた問題のコードは以下になる

./trot/lib/trot/trot.exより抜粋

# module変数 -> 許可するmethod
  @http_methods [:get, :post, :put, :patch, :delete, :options]

  @doc """
  Returns a boolean indicating whether the passed in atom is a valid HTTP method.
  """
  defmacro is_http_method(thing) do
    quote do
      # unquote(module.func) |> is_atom()
      # get, post, patch, delete
      # unquote -> thing -> 実体化(http_methodsのどれかであれば)
      # return -> boolean
      # Q: macroの使うタイミングむずすぎ問題 -> if(macro!!), for(macro!!), def(macro), defmacro(macro) ?????
      is_atom(unquote(thing)) and unquote(thing) in unquote(@http_methods)
    end
  end

このis_http_method(thing)というものがboolの値を返していることは何となく分かるが、なぜここでmacroなのかという疑問は残る
普通にis_atom()の判定と@http_methodsに引数の値が含まれているかどうかが分かれば良いだけではないのか?
なので以下のように書き直しても問題ないはずだが、macroが実行時評価になるという話が関係するのだろうか、いずれにしろ現段階では弱すぎて理解が出来ていないのでレベル上げをしないといけない

defmodule Sample do
  @http_methods [:get, :post, :put, :patch, :delete, :options]
  def is_http_method(thing) do
    is_atom(thing) and thing in @http_methods
  end
end

Sample.is_http_method(:get) |> IO.puts()
# true

このSampleを書いて、TrotのREAD.MEにあるエンドポイントのサンプルを見てみて1つ思ったことは、このget "/hello", do: "hello"というものがmacroでsyntaxを定義しており、この記述が解釈されてAST(構文抽象木)に変換(elixirの場合は3つの値をもつタプルだった気がする)されるみたいな話しが絡んでくるんだろうなぁと。勉強しよう

ちなみに先ほどのdefmacroはこんな感じで呼び出せた

defmodule Trot do
  @http_methods [:get, :post, :put, :patch, :delete, :options]

  @doc """
  Returns a boolean indicating whether the passed in atom is a valid HTTP method.
  """
  defmacro is_http_method(thing) do
    quote do
      # unquote(module.func) |> is_atom()
      # get, post, patch, delete...
      # unquote -> thing -> 実体化(http_methodsのどれかであれば)
      # return -> boolean
      # Q: macroの使うタイミングむずすぎ問題 -> if(macro!!), for(macro!!), def(macro), defmacro(macro) ?????
      # IO.puts(thing)
      IO.puts(unquote(thing))
      is_atom(unquote(thing)) and unquote(thing) in unquote(@http_methods)
    end
  end
end

defmodule Call do
  import Trot
  def exec(), do: is_http_method(:get)
end

Call.exec() |> IO.puts()
# true

知らない機能がたくさんある

Elixirってこんな風になってんの、そんなことができるのって機能が多すぎて驚いた
iexを立ち上げた状態でh Enum(hのあとはモジュール名とかsyntaxとか)としてやるとEnumに関するhelpを確認することが出来る

                                      Enum                                      

Provides a set of algorithms to work with enumerables.

In Elixir, an enumerable is any data type that implements the Enumerable
protocol. Lists ([1, 2, 3]), Maps (%{foo: 1, bar: 2}) and Ranges (1..3) are
common data types used as enumerables:

    iex> Enum.map([1, 2, 3], fn x -> x * 2 end)
    [2, 4, 6]
    
    iex> Enum.sum([1, 2, 3])
    6
    
    iex> Enum.map(1..3, fn x -> x * 2 end)
    [2, 4, 6]
    
    iex> Enum.sum(1..3)
    6
    
    iex> map = %{"a" => 1, "b" => 2}
    iex> Enum.map(map, fn {k, v} -> {k, v * 2} end)
    [{"a", 2}, {"b", 4}]

同じようにh defってやってみるとdefってのがmacroで作られていることが分かる

 defmacro def(call, expr \\ nil)                         

Defines a function with the given name and body.

さらに驚いたのがh defmacroと記述した時。defmacroがdefmacroによって作られている。なんだこれは笑

defmacro defmacro(call, expr \\ nil)  

macroでめちゃ盛り上がる様子

つまりは、Elixirのコードのどこかに本物のdefmacroがいるということになり、今日一上がった。defmacrodefmacroを作っている。うーん。厨二病、素晴らしい...!!

Elixirのsyntaxは多くがmacroで記述されており、ElixirのKernel.exというファイルを見れば確認することが可能だ

github.com

IExとEEx

今まで知ることも触れることもなかったものだ
IExはElixirのiexに関する関数が扱えるようで、IEx.started?とするとiexが立ち上がっているのかどうかがbool値で判定することが出来る
特に感動したのがEExの方で、このEExを使えば色々と面白いことが出来そうだ

EEx.eval_file(ex_file_path)とすると.exに記述されているコードを文字列として取得することが出来る

iex> EEx.eval_file("mix.exs")
"defmodule Trot.Mixfile do\n 
:
:
\"https://github.com/hexedpackets/trot\"},\n     files: ~w(mix.exs README.md LICENSE lib VERSION)]\n  end\nend\n"

当然、文字列なので|>を使ってStringモジュールの関数を使用することが出来る

iex> EEx.eval_file("mix.exs") |> String.at(3)
"m"

あと1つ文字列からコードを実行する関数があったが、何ていう名前だったのかど忘れしてしまった...

福岡のエンジニア強すぎるって

fukuoka.exの発足人のpiacereさんがElixirに関して知らないことがなくて、もう笑うしかない
twitter.com

東海(広すぎるので本当は岐阜と愛知)にElixirの知識をリモートで届けてくださり感謝しかありません
また今回リモートで参加してくださった方は皆さん、福岡の方(間違いありましたら申し訳ありません)で、いかに福岡でElixirに対して注目が集まってるのかが分かるし、議論している話のレベルも高くて驚くばかりだ...

良い感じで悔しい気持ちになれたのでホットなまま、維持していきたい。やる気がめっちゃ出た

総評

コードを一緒に読むのいいですね。自分の弱さ、何が分かっていないかが分かったので良しとする。まだまだ知らないことがあるんやなと高ぶった
こういう思いが出来るのも清流elixirを開催しある程度、継続してきたからだと身にしみる...

なんと10/30(水)にfukuoka.exさんが開催される秋のLT大会にリモートLTという形で登壇させて頂くことになりました!!
リモートLTって凄いな。初めて聞きました笑
半ば強引に枠に入れて頂けたようで恐縮です、ありがとうございます

fukuokaex.connpass.com

「清流elixirのこれまで。なぜ東海でelixirをやるのか」みたいなタイトルでLTします。資料はどこかにアップしようと思うので、ごひいきに〜

Elixirでプロセスをspawnで生成してErlangの:random.uniformを呼び出すと全て同じ値になる

何が起こったのか

まず、プロセスを複数立ち上げて並行で処理をしようと考えた。その処理の過程の中でErlang:random.uniform()(ElixirからErlangのモジュール関数を呼び出す際には:をつける)を呼び出して実行してみると、何と全て同じ値になっているではないか!!

ランダムに生成したidをPOST経由で送信した際にuserが1人しか生成されていないので不思議に思って、デバッグをしている際に気づいた
以下は、問題を再現するために記述したコードになる

Enum.map(1..10, fn num ->
  spawn(fn ->
    num = :random.uniform()
    IO.puts(num)
  end)
end)

このコードをiex起動して実行してみると、全て同じ値が出力されることが確認できる

[#PID<0.722.0>, #PID<0.723.0>, #PID<0.724.0>,
 #PID<0.725.0>, #PID<0.726.0>, #PID<0.727.0>,
 #PID<0.728.0>, #PID<0.729.0>, #PID<0.730.0>,
 #PID<0.731.0>]
0.4435846174457203
0.4435846174457203
:
:
0.4435846174457203
0.4435846174457203

なぜ〜

Erlangの公式ドキュメントをふと読んでみることに

答えは全てそこにあった。公式 is GOD
以下は引用になります

Data Types
ran() = {integer(), integer(), integer()}
The state.

uniform() -> float()
Returns a random float uniformly distributed between 0.0 and 1.0, updating the state in the process dictionary.

ここから分かることは、ランダム値の生成にprocess dictionaryに保存されているData Typesの値を利用しているということだ
つまり、このstateの値(seed値)が全てのプロセスで重複してしまっているために、同じ値が生成されているのではないかと考えられる

さらにページ下部から決定的な記述を発見したので引用

Some of the functions use the process dictionary variable random_seed to remember the current seed.
If a process calls uniform/0 or uniform/1 without setting a seed first, seed/0 is called automatically.

ふむふむ、要は:random.uniform()を呼び出した際に、最初にseed値を設定するために、seed/0を内部的に呼び出しているようで、その際にseed値をセットしているもしくは、process dictionaryからseed値を読みこんでいると考えることが出来る

そうであれば、同じ値が生成されるという現象を説明することが出来る
Erlangのコードは読み慣れていないが、該当する部分のコードを発見できたので確認しておこう

-spec uniform() -> float().

uniform() ->
    {A1, A2, A3} = case get(random_seed) of
               undefined -> seed0();  %% ここでseed0()を呼び出している!! 
               Tuple -> Tuple
           end,
    B1 = (A1*171) rem ?PRIME1,
    B2 = (A2*172) rem ?PRIME2,
    B3 = (A3*170) rem ?PRIME3,
    put(random_seed, {B1,B2,B3}),
    R = B1/?PRIME1 + B2/?PRIME2 + B3/?PRIME3,
    R - trunc(R).

なるほど、やはりそういうことだね
ということは別プロセスを立ち上げた際にこのseed値を変えてあげれば良さそう

清流elixir常連参加のりきくんにも助けて貰いました。圧倒的感謝ッ!!


問題を解決したコード

ということで以下のコードに変更したところ、無事にそれぞれのプロセスで異なる値が生成されるのを確認できた seed値の設定値については気にしてはいない。まずは結果が変化することを確認することが優先だ

base_num = :random.uniform(1000)
Enum.map(1..10, fn num ->
  spawn(fn ->
    :random.seed(num+base_num, num+base_num, num+base_num)
    num = :random.uniform()
    IO.puts(num)
  end)
end)
[#PID<0.766.0>, #PID<0.767.0>, #PID<0.768.0>,
 #PID<0.769.0>, #PID<0.770.0>, #PID<0.771.0>,
 #PID<0.772.0>, #PID<0.773.0>, #PID<0.774.0>,
 #PID<0.775.0>]
0.7457427656808528
0.7626736718805098
:
:
0.8811900152781076
0.8981209214777643

ヨシ!

おまけ。Erlangの関数を使用せずにランダム値を生成する

面倒なのでEnum.random()を使って乱数を擬似的に作成してあげれば良い
基本形は以下の通り。rangeもenumerableなので記述するのが楽だ

iex> Enum.random(1..100)
47

少数にしたい場合は任意の10n(希望の桁数)で割ってやれば良い感じになる

iex> Enum.random(1000..1000000) / 1000000
0.534401

Elixirで定数を定義する

Elixirには標準にconstがない

※書き方が早く知りたい方はこの章は読み飛ばして頂いて構いません

Elixirはいわゆる関数型言語だが変数の再代入、正しくはパターンマッチを何度も行うことが出来る。またgo-langのような厳密な型の指定も必要ない

user_input = "hello world!"
IO.puts(user_input)

user_input = "good bye hello!"
IO.puts(user_input)

# 実行結果
# hello world!
# good bye hello! <- 変数の値が変化している

pythonrubyのように気軽に変数への代入を行うことが出来る一方でjavascriptで言う所のconstたる定数の宣言を標準の組み込み構文では行うことが出来ない

ピン演算子を使用することで値の束縛、すなわち再代入不可の状態を作り出すことは可能だが、どうせなら、何かしらのファイルにまとめて定数を宣言しておきたいので、うーん..となる

そんな中、以前twitterにてyamazakiさんに定数の作り方を教えて頂いたことを思い出し、参照を頼りにElixirで定数を実装してみた

Elixirでの定数の置き方

大きくやり方は3種類ある
それぞれにメリットがあると思われるので、読者さんの気に入ったものを使って頂ければと思う

基本的な設計方針は外部ファイルに定数用のモジュールもしくはマクロを定義しており、それを本モジュールから呼び出して使用するという形になる。言葉で説明したところで良く分からないので、さっそくコードを見てみてほしい

ベーシックな書き方

定数の値を返す、関数をそれぞれ実装するスタイル

defmodule Const do
  def fruits(), do: ["apple", "banana", "orange"]
  def price("apple"), do: 120
  def price("banana"), do: 90
  def price("orange"), do: 80
end

使い方

defmodule Main do
  def main() do
    Const.fruits()
    |> Enum.map(fn val -> Const.price(val) end)
    |> Enum.sum()
  end
end

Main.main() |> IO.puts()
# 290

簡単に使えて良い。関数の戻り値として成立すればどんな形であっても問題ない

モジュール内変数を渡す書き方

先ほどとやっていることはあまり変わらない
モジュール内に変数が定義できるsyntaxを利用して、その値を関数を利用して返すというスタイルになる
この方法の利点としてはベースの値をモジュール内変数として用意して、別途、用途に応じて変化させた値を返すことが出来るという点にある

defmodule Const do
  @fruits ["apple", "banana", "orange"]
  @min_price 60
  def fruits(), do: @fruits
  def winter_fruits(), do: @fruits ++ ["grape", "strawberry"]
  def summer_fruits(), do: @fruits ++ ["Watermelon", "pineapple"]
  def choice_one(), do: Enum.random(@fruits)
  def price("apple"), do: 120
  def price("banana"), do: 90
  def price("orange"), do: 80
  def price(_another), do: @min_price + :random.uniform(300)
end

黒魔術(ブラックマジック)

Elixirのマクロを使ってconst構文を用意するやり方。こちらもyamazakiさんに教えて頂いた参照を頼りに動作を確認した

const 関数名 値とすることで指定した値を返す関数を作成することが可能になる

defmodule Const do
  defmacro const(name, value) do
    quote do
      def unquote(name), do: unquote(value)
    end
  end
end

で、こいつを外部のモジュールから呼び出す

defmodule Sample do
  import Const
  const const_value, 1
  def foo(num), do: const_value() == num
end

Sample.foo(1) |> IO.puts()
Sample.foo(100) |> IO.puts()

# true
# false

すごい。個人的にマクロに関してはあまり理解していない & メタプログラミングよく分かってないので、別の機会に改めて学習しようと思う

おまけ(定数を使ってガード節でパターンマッチできないのつらい)

Constにリスト定義して、代入された値がリストに存在していなければ、パターンマッチで同名の別関数を実行させるという処理を行いたいが、ガード節ではConst.lists()のような値を呼び出すことが出来ない

defmodule Const do
  def fruits(), do: ["apple", "banana", "orange"]
end

defmodule Main do
  def main(val) when val in Const.fruits() do
    IO.puts("nice fruits")
  end
  def main(_val), do: IO.puts("normal fruits")
end


Main.main("apple")

こんな感じでerrorが出てしまう

** (ArgumentError) invalid args for operator "in", 
it expects a compile-time proper list or compile-time range on the right side when used in guard expressions, 
got: Const.fruits()

多少不満ではあるが、helper関数を用意して以下のように実装すればこの問題をクリアすることが出来た。再帰関数を実装する時にこの記述を良く使う

defmodule Main do
  def main(val) do
    is_exist? = val in Const.fruits()
    _main(is_exist?, val)
  end
  defp _main(true, val), do: IO.puts("nice fruits: #{val}")
  defp _main(false, _val), do: IO.puts("normal fruits")
end


Main.main("apple")
# nice fruits: apple

ガード節で使用することが可能な関数は決まっており、この部分の仕様は分かれば別の解が思いつくかもしれない
ということでElixirの公式Documentsのガード節に関数部分を流し読みしてみた

まずガード節に対する公式の見解は以下(翻訳はノリでやってます)

  • 使える条件式は意図的に制限している。なぜなら、ガードの中で意図せぬことが起こらぬように
  • 引数だけでは表現不可な複雑なパターンマッチを行うためにガード節を用意している
  • 時にはコードを最適化するに十分に機能する

ふむふむ、どうやらcustomのガード節を作成することが可能なようだが、ガード節に元々使用できない条件は利用不可なので、やりたいことは出来ないだろう

defmodule MyInteger do
  defguard is_even(value) when rem(value,2) == 0 and is_integer(value)
end


defmodule Main do
  import MyInteger, only: [is_even: 1]
  def my_function(number) when is_even(number) do
    # do stuff
    IO.puts("nice")
  end
end

Main.my_function(4)

やはり、ガード節で定数に対して条件を当てることは難しそうなので、先ほどの記述が良いんじゃないかな〜

【頭おかしい(褒めてる】ゼロからトースターを作ってみた結果を読了

なぜ読んだのか

下記ブログにて紹介されていたところを発見し、ぶっとんだタイトルとおそらく著者が作成したであろうトースター?のサムネイルが強烈すぎて、これは読まずにはいられない!!とAmazonにて即購入。中古品でだいたい600円ぐらい。新品だと税別で750円の模様
著者が当時は大学院だったというバックグラウンドも面白いと思った
※ブログ運営費のためリンクを貼らせて頂きます

いわゆる文庫本のサイズでページ数は約200ページ程。元々がブログであったためにシンプルかつユーモアな記述でスラスラと読むことができ、2時間ほどで読了

概要

お察しの通りだが、タイトルのままである
著者トーマス・トウェイツは突如「トースター」を作ることを決意する。トースターとは何か。日本人ならば誰しもが知っているパンを焼くアレだ
そんなトースターをゼロから、つまりは実際に鉄鉱石を採取して精錬して型を取るようなレベルから作成が始まる
30ページぐらいにはキャリーバック1つでツルハシも持たず鉱山に鉄鉱石を堀りに行き出して最高にぶっ飛んでいる

その中で様々な問題にぶち当たり、バカなのか天才なのか(褒めてる)と思わせるアイディアで問題を乗り越えていく

本書で扱うメインのトピックは個人視点で3つ

  • 金属について
  • プラスティックについて
  • これまでの製作を経て、実際にトースターを焼くまで

それぞれのトピックに問題とアイディアがあり、経験を経て著者が何かを感じていく..
いかに自分の力が世界のスケールでは小さいもので、身の回りには長い歴史を得て形作られた様々な技術が溢れていることに気づいていく

著者のTwitterが無いものかと調べたところTEDでの公演を発見したので少しでも興味があれば視聴してみてはいかがでしょうか

www.ted.com

プロダクト視点で面白いと思ったポイント

個人的にいいねと思ったのは2点ある

専門家に相談するということ

1つは無謀とも思えるトースターをゼロから作り上げるためにどのような計画を立てれば良いかという部分で非常に感心させられた
著者は当時デザインを学ぶ大学院生であり、電気機器や電気回路の知識が必ずしもある状態ではない。その状態でスタートして、最終的にトースターを作れたのは何故なのか?(パンは焼けなかったけど..)

著者が最初に行なったことは「トースターの分解」であり、すなわちリバースエンジニアリングとよばれる作業だ
市販のトースターを一度、細部まで分解してみることで

  • どんな材料が必要なのか
  • どんな作業が必要なのか
  • どんな道具が必要なのか
  • どんな技術を知る必要があるのか
  • どんな知識を知る必要があるのか

という点をリスト出しすることで何をすればいいのかをハッキリさせている
その上で、同大学の鉱石の分野で活躍している教授、鉱山職員や、企業、多くの専門家の意見やアイディアを取り入れていく
分からないことは専門家に聞くというフットワークの軽さに驚いた

私が推しているElxiirの前衛となったErlangというプログラミング言語の作者の1人であるJoe Armstrongも以下の記事でこう答えている

問題を解決する際には、まず最も難しい問題から解決せよ
最も難しい問題が何であるかは専門家に聞け

postd.cc

素人には何が最も難しい問題なのかを判断することがそもそも出来ない。何が最も難しい問題なのかをハッキリさせるためにも専門家の協力を仰ぐということは非常に重要なことだということをJoeは語っている

日本では他社に協力を求めるという行為はあまり推奨されない。それ故に著者の当たり前のようなこの行動に思わず「いいね」と思った

身近にあるものは多くのモノの集合であること

トースターを家電屋で買おうと思えば1000円もあれば現代では十分だろう
しかしながら、トースターがトースターというものを型取るためには多くの材料と技術が使用されている

電気回路を作成するための基盤や、パンをトーストするための発熱体、電源コード、カバーに至るまで多種多様だ
上げればキリが無いが、どれか1つ欠けてもトースターはトースターになることはない

どれだけ鉄の精錬を熟知していてもそれはトースターを作る過程ではある1つの知識にしかなり得ない
しかしながら、鉄の精錬を熟知しているものがいなければトースターはいつまで経っても完成しないだろう

現代の物の多くが相互、影響を与え合って作られている。そのことに気づき、我々に伝えてくれる著者に「いいね」と思った

ここに書いていないこと

なぜ倫理をもって技術を考えるべきなのかという著者の考えがまとまっており、この部分はぜひ自身で読んでみてほしい(要約: 買ってね)
人によっては考え方が分かれそうなので、あえて記述しません

docker-composeを使ってReact+phoenix+postgresの環境を構築する

概要

ほとんど個人的メモです
自身でよく構築することの多い以下の環境を毎度、調べながら作るのが面倒なのでまとめたって感じです

  • React(front end)
  • phoenix(backend, elixir web framework)
  • postgres(open source database)

毎度悩むのが、dockerのimageをpullしてきたけど、どの場所、どのタイミングでcreate-react-appなりmix phx.new ...を実行するかなといったところ
個人的なベストプラクティスとしてはlocal環境で作成して依存環境ガァ^〜で何度も怒られてつらい思い出があるので
作成したcontainerの内部で上記のコマンドを実行してプロジェクトを作成し
生成されたプロジェクトをcontainerからlocal環境にコピーしてくるという方針を採用してみた

プロジェクトの生成環境が実行環境と同じになるというメリットがあるが、準備に多少の手間が発生する
しかしながら、やることは毎度同じなので大した苦労ではないなーと感じている(まだ2回しか作ってないけど)

やってることは色んな記事からアイディアをパクってまとめただけです
記事の一番最後にreferenceをまとめているのでご参照下さい

共通作業

pathはどこでも構わないので適当にdocker-composeをwrapするディレクトリを生成しておきます

$ mkdir react-phoenix-postgres
$ cd react-phoenix-postgres

以下の作業は全て、このディレクトリ内で行います
ちなみにDockerとdocker-composeについてはinstall済みという前提で進めていますのでお願いします

React環境の構築とcreate-react-appの実行

まずはReactのための環境から。node imageをbaseにした環境を用意して、そこでReactのプロジェクトを作成します
さっそくReactのためのディレクトリとDockerfileを生成

$ mkdir frontend
$ cd frontend
$ touch Dockerfile

続いて最低限の設定をDockerfileに記述

FROM node:12.2.0-alpine

RUN mkdir /app
WORKDIR /app

# nodeとnpmがinstallされているか確認用。省略しても問題なし
RUN node -v && npm --version

このDockerfileを使ってcontainerを作成します
-t というオプションの後に記述されているのは割り当てるタグと呼ばれるものです
後にタグを使ってコマンドを実行するので視認性のあるものにしておくと良いかと思います

$ docker build -t react_test:dev .

このコマンドを実行することで手元のDockerfileを元にcontainerが作成されます
メッセージログが流れてしまったので恐縮ですが

RUN node -v && npm --version
12.2.0
6.xx

Successfully built **containerID**

のようなメッセージが出ていればnode環境の作成は完了です
次にcreate-react-appを実行するための用意をします

create-react-appのコマンドは一度しか使用しないので、dockerのcontainerにaccessして、直接installします まずはcontainerにaccessするためにdockerのcontainerを立ち上げます
以下オプションの詳細ですが、無視してコマンドを打って頂いて構いません。一応書いておきました

  • d -> backgroundでdocker daemonを実行
  • t -> tty true -> docker containerを立ち上げたままにしておく
  • p -> spec port -> 立ち上げるportとexposeするportの指定
  • name -> containerに名前を割り当てる(タグとは別で実行中のみ有効と判断しています)
  • react_test:dev 先ほど付与したタグ名

cotainerを立ち上げます

$ docker run -dt --name react_train -p 3000:3000 react_test:dev
# cotainerIDが表示され、terminalが入力可能状態になれば成功

よくあるミスとしては以下が候補に上がります

  • すでにportが使用されている(別のportにするか重複元の環境を停止させる)
  • 同盟の--nameが割り当てられたcontainerがすでにUPされている(docker stop containerIDで停止させる)

問題なく進められていれば先ほど立ち上げたcontainerにashを使ってアクセスします
なんでash?ってなりますけど、alpineではshellにashが使われているそうで、bashやらにすると怒られます

$ docker exec -it react_train ash

無事にアクセスができるとroot#containerIDのようなプロンプトが立ち上がります
まずは適当にnode.jsとnpmのversionでも確認しておきます

# dockerのcontainer内部
node -v && npm --version
12.2.0
6.x.x

無事にinstallされているようなので、次にcreate-react-appコマンドが使いたいのでnpm経由でinstallします
ここは-gをつけてglobal installにしておいてください。package.jsonファイルが現状のディレクトリにないので-gをつけないと怒られます
ついでにcreate-react-appも実行します

$ npm i -g create-react-app && create-react-app app-name

Happy Hacking!と準備できたからserver立てれるよって文言が表示されていればOKです
これでプロジェクトの生成は完了です。あとはこの作成したプロジェクトをlocal環境にcopyしてあげるのみ

exitと入力し現在、アクセス中のcontainerから離脱します
続いて、以下コマンドを使ってlocal環境にプロジェクトをcopyします

# 現在のカレントディレクトリにプロジェクトをcopy
$ sudo docker cp react_train:/app/app-name .

これでlocal環境に生成したプロジェクトがcopyされた
しかしながらcopyされたプロジェクトの権限をlコマンドで確認してみるとオーナーがrootになっていることがある
そのためchownを使ってオーナーを変更しておきます(後にハマってだるいことになったので

sudo chown -R login-user-name app-name

再びlコマンドで確認してオーナーが自身のユーザーネームになっていればOKです
これでReactのプロジェクトの生成は完了です

phoenix(Elixir)環境の構築とmix phx.newでのプロジェクト作成

やることはReact環境を用意した時と全く同じです。まずはディレクトリとDockerfileを用意します

$ mkdir backend
$ cd backend
$ touch Dockerfile

やはり最低限の設定をDockerfileに記述

FROM elixir:alpine

WORKDIR /app

RUN yes | mix local.hex
RUN yes | mix archive.install https://github.com/phoenixframework/archives/raw/master/phx_new.ez
RUN mix local.rebar --force

# elixirがinstallされているかの確認用(省略可)
RUN elixir -v

先ほど同様に作業を進めていきます

$ docker build -t elixir_test:div .
$ docker run -dt --name elixir_train -p 8080:8080 elixir_test:dev
$ docker exec -it elixir_train ash

これでphoenixのプロジェクトの作成準備は整いましたので早速...

# dockerのcontainer内部(phoenixの生成に関するオプションは適宜、自身で付与してください(no-webpackを打ち忘れたorz))  
mix phx.new backend

これでphoenixのプロジェクトが生成されたので同様にlocal環境にcopyします

$ sudo docker cp elixir_train:/app/backend .

phoenixのプロジェクトの生成は以上ですが、database設定を行う必要があるためdocker-composeの設定と共に行なっていきます

docker-compose.ymlの作成とdatabaseの設定

これまで行なってきたdocker runをdocker-composeを使ってまとめてできるようにしてあげます
記述量は若干ありますが、どれも1つずつ見るとやっていることは大したことではないのでご心配なく
レゴを積み上げるようなノリで記述出来ます

version: "3"

services:

  # reactへのsetting
  frontend:
    build:
      context: test # buildしたいDockerfile(node)が格納されているディレクトリ
      dockerfile: Dockerfile
    container_name: react_frontend # cotainerに命名
    volumes:
      - "./test:/app/test" # 先ほど作成したreactプロジェクトのディレクトリ名
    ports:
      - "3001:3000" # 内部ポートと外部ポートをexpose
    environment: 
      - NODE_ENV=development
    tty: true # 起動を継続するように指定
    working_dir: "/app/test" # マウントしたプロジェクトのディレクトリを指定(package.jsonがあるため)
    command: "npm run start" # 起動時に実行させるcommand

  # phoenixへのsetting(ほとんどReactと同じ)
  backend:
    build:
      context: elixir
      dockerfile: Dockerfile
    volumes:
      - "./elixir/backend:/app/backend"
    container_name: elixir_backend
    ports:
      - "8080:4000"
    tty: true
    working_dir: "/app/backend"
    command: ash -c "mix deps.get && mix phx.server"
    depends_on: # databaseを使いたいので下記記述のdbに連携しておく
      - db

  # postgresへのsetting
  db:
    image: postgres:12.0 # ymlの中でimageを指定
    container_name: postgres_for_phoenix
    tty: true
    volumes:
      - "./postgres/lib:/var/lib/postgres"
    # postgres用の環境変数をset
    environment:
      - MYSQL_ROOT_PASSWORD=rootpassword
      - POSTGRES_USER=root
      - POSTGRES_PASSWORD=root_password
      - POSTGRES_INITDB_ARGS="--encoding=UTF-8"
      - POSTGRES_HOST=db
    ports:
      - "5432:5432"

やっていることはかなり共通事項が多いので特に難しくないです
インフラを構築する才能がないねーと言われた僕ですら書けるのがdocker-composeです。最高か

postgresのマウント用に指定したディレクトリが現在ない状態ですので、作っておきます

$ mkdir -p mysql/lib

phoenixのdatabase設定

configディレクトリ内部のdev.exsの最下部を編集します
先ほどdocker-compose.ymlに環境変数として用意したものに値を変更します

./project-name/config/dev.exs

config :backend, Backend.Repo,
  adapter: Ecto.Adapters.Postgres,
  username: "root", # POSTGRES_USER
  password: "root_password", # POSTGRES_PASSWORD
  database: "backend_dev", # ecto.createコマンドを使わないので変更しても良いし、しなくても良い
  hostname: "db", # service名
  pool_size: 10

準備も完了したのでcontainer達を立ち上げてみます

$ docker-compose up --build

frontendとdatabaseは特に問題なく立ち上がるかと思いますが、backendのcontainerはdatabase is not exist的なmessageを出し続けているかと思います
なので接続するためのdatabaseを作ってあげましょう

先ほどdocker-compose up --buildでcontainerを立ち上げているので、postgresのimageをbuildして生成されたcontainerが立ち上がっているはずです
docker psで確認します

$ docker ps
9f676931727a        postgres:12.0          "docker-entrypoint.s…"   2 days ago          Up 49 seconds       0.0.0.0:5432->5432/tcp   postgres_for_phoenix

立ち上がっているのが確認できたら、このcontainerにアクセスして、内部にdatabaseを作成します
postgresのプロンプトを立ち上げる

$ docker exec -it container-id /bin/bash
psql
root=#

先ほど./project-name/config/dev.exsに指定した名前のdatabaseを作成します

root=# create database backend_dev;
CREATE DATABASE

lで作成したdatabaseが存在していることを確認できたらdatabaseの作成は完了です
これで全作業が終了

Reactとphoenixのそれぞれのプロジェクトに対してdocker-compsoe.ymlで設定したportにアクセスしてみて下さい

でそれぞれ、welcomeページが表示されていれば成功です(phx->no-webpackを指定し忘れてcssが読み込まれていない)
f:id:takamizawa46:20191007222214p:plain
f:id:takamizawa46:20191007222301p:plain

docker psで確認すると3つのcontainerが立ち上がっているはずです

$ docker ps
CONTAINER ID        IMAGE                  COMMAND                  CREATED             STATUS              PORTS                    NAMES
a9457b933808        react_train_frontend   "npm run start"          52 seconds ago      Up 49 seconds       0.0.0.0:3001->3000/tcp   react_frontend
a80a6a939350        react_train_backend    "ash -c 'mix deps.ge…"   2 days ago          Up 47 seconds       0.0.0.0:8080->4000/tcp   elixir_backend
9f676931727a        postgres:12.0          "docker-entrypoint.s…"   2 days ago          Up 49 seconds       0.0.0.0:5432->5432/tcp   postgres_for_phoenix

containerを終了したい時はdocker-compose downとすればOKです

参考文献