mmts1007’s diary

プログラミング関連の技術系ブログです。

Docker で Rails の開発環境を構築する

Docker で Rails の開発環境を配れたら、楽できるのでは?思い、サンプルを作ったのでまとめる。

サンプルコード

github.com

Dockerfile と docker-compose.yml は以下の通り

Dockerfile.development

FROM ruby:2.4.1
MAINTAINER mmts1007

ENV APP_ROOT=/usr/src/app

WORKDIR $APP_ROOT
EXPOSE 3000

RUN apt-get update && apt-get install -y \
      nodejs \
      mysql-client

COPY . $APP_ROOT
RUN bundle install

CMD ["bundle", "exec", "rails", "s", "-b", "0.0.0.0"]

docker-compose.yml

version: '3'
services:
  db:
    image: mysql
    environment:
      MYSQL_ROOT_PASSWORD: root
    volumes:
      - mysql-data:/var/lib/mysql
  web:
    build:
      context: .
      dockerfile: Dockerfile.development
    volumes:
      - .:/usr/src/app
    ports:
      - "3000:3000"
    depends_on:
      - db
volumes:
  mysql-data:

工夫した点

  • 開発時は DB も必要になるため、 docker-compose を利用して 1コマンドで開発に必要がものが揃うようにした
  • 修正したソースコードをすぐに反映したかったため、ソースコードは Docker コンテナにマウント
  • DB に書き込んだデータは保持したかったので、 volumes を使って永続化

使い方

docker-compose build

$ pwd
/path/to/docker-rails/simple_note
$ docker-compose build
...
Successfully tagged simplenote_web:latest

docker-compose up

$ docker-compose up
Starting simplenote_db_1 ... 
Starting simplenote_db_1 ... done
Recreating simplenote_web_1 ... 
Recreating simplenote_web_1 ... done
Attaching to simplenote_db_1, simplenote_web_1
...
web_1  | Use Ctrl-C to stop

ブラウザから http://localhost:3000 にアクセス

その他

初回起動時は DB、テーブルが存在しないため作成する

$ docker-compose run web bundle exec rake db:create
$ docker-compose run web bundle exec rake db:migrate

追記

Gem ファイルの永続化

記事を公開したところ、先輩に「Gemfile 更新時に、全 Gem インストールし直しになっちゃうから Gem ファイルのインストール先も Volume にするのが良いよ」とコメントを頂きました。(ありがとうございます!)

確かに Gemfile 更新した時に毎回 build して、イメージに含めてあげないと動かない状態でした・・・

Gemfile に Kaminari を追加

gem 'kaminari'

bundle install

$ docker-compose run web bundle install
...
Fetching kaminari-core 1.0.1
Installing kaminari-core 1.0.1
...
Fetching kaminari-activerecord 1.0.1
Installing kaminari-activerecord 1.0.1
Fetching kaminari-actionview 1.0.1
Installing kaminari-actionview 1.0.1
...
Fetching kaminari 1.0.1
Installing kaminari 1.0.1
...
Bundle complete! 18 Gemfile dependencies, 76 gems now installed.
Bundled gems are installed into /usr/local/bundle.

もう一度 bundle install を実行すると キャッシュされて Gem は何もインストールされないことを期待していましたが・・・

$ docker-compose run web bundle install
...
Fetching kaminari-core 1.0.1
Installing kaminari-core 1.0.1
...
Fetching kaminari-activerecord 1.0.1
Installing kaminari-activerecord 1.0.1
Fetching kaminari-actionview 1.0.1
Installing kaminari-actionview 1.0.1
...
Fetching kaminari 1.0.1
Installing kaminari 1.0.1
...
Bundle complete! 18 Gemfile dependencies, 76 gems now installed.
Bundled gems are installed into /usr/local/bundle.

全く同じログが出ました・・・

新たに作成したコンテナに Gem をインストールし、そのコンテナは破棄されてしまうため、このような現象が起きてしまいました。

docker-compose.yml を以下のように変更し、インストールした Gem を永続化するようにしました。

iff --git a/simple_note/docker-compose.yml b/simple_note/docker-compose.yml
index 18e6132..c824f93 100644
--- a/simple_note/docker-compose.yml
+++ b/simple_note/docker-compose.yml
@@ -12,9 +12,11 @@ services:
       dockerfile: Dockerfile.development
     volumes:
       - .:/usr/src/app
+      - gem-store:/usr/local/bundle
     ports:
       - "3000:3000"
     depends_on:
       - db
 volumes:
   mysql-data:
+  gem-store:

この状態であればコンテナが破棄されたとしても Gem 自体は残るので、先ほどの問題は発生しなくなりました!

参考

docs.docker.com

qiita.com

SAML 認証を Ruby on Rails で試してみた

SAML 認証に触れる機会があったので、 Ruby on RailsSAML 認証するサンプルアプリを作ってみた
折角なので、SAML とは何なのか?あたりからまとめてみようと思います。

SAML とは

まずは、SAML とは何なのか

  • 読み方は サムル
  • Security Assertion Markup Language の略
  • 異なるサービス、アプリケーション間で認証情報を交換するための仕組み
    • これによりシングルサインオン(1度のログインで、複数のアプリが利用可能になること) が実現できる

登場人物

Identity Provider

  • 略称は IdP
  • 実際の認証処理を行う人。認証したユーザの情報(ID、 メールアドレス等)を提供してくれる

Service Provider

  • 略称は SP
  • 認証情報を利用する人

今回は SP のサンプルを作成しました。

認証フロー

SAML の認証フローのイメージです。 f:id:mmts1007:20170121235429p:plain

  1. ユーザは SP に対してログインを試みる
  2. SP は Idp に対して認証リクエストを送信する
  3. IdP はユーザにログイン画面を表示する
  4. ユーザはログイン情報を入力する(ex. ID / Password)
  5. IdP は入力情報を検証し、正しければ SP に認証結果を送信
  6. SP は認証結果を検証し、正しければユーザをログインさせる


SAML の説明はこのあたりにして、サンプルアプリについて説明します

構成

IdP

サンプルアプリを動かすためには IdP が必要なので、今回は IdP に Azure Active Directory を利用しました。

azure.microsoft.com

SP

SP は以下の技術要素で作成

また、SAML のライブラリとして ruby-saml を使用しました。

github.com

サンプルアプリ

サンプルアプリはメモ管理アプリとなっています。
Azure AD で認証したユーザのメモ一覧・登録・更新・削除ができる簡単なものです。

SAML 連携部分の実装は https://github.com/onelogin/ruby-saml のサンプル https://github.com/onelogin/ruby-saml-example をベースに
少し修正を入れたものになっています。
(SAML 部分の実装はほぼサンプルのままです)

github.com

動作確認

http://localhost:3000 にアクセス
「Azure AD で Login」をクリック f:id:mmts1007:20170122020523p:plain

Microsoft アカウント(IdP)のログインページが表示される
ID / Password を入力 f:id:mmts1007:20170122020841p:plain

メモアプリ(SP)にログインする f:id:mmts1007:20170122020539p:plain

まとめ

SP を作るためにはもっとゴリゴリ書く必要があるかと思っていたのですが、
https://github.com/onelogin/ruby-saml がとても使いやすく、IdP の設定情報を記述するだけで動かすことができました。サンプルもあるのも良かったです。

Grafana を使ってサーバメモリ使用率、CPU使用率を可視化する

top コマンドや free コマンドなどで確認していたサーバ負荷状況をグラフィカルに見やすできないのかと思い調べたところ、Grafana という可視化ツールがあったので試してみました。

Grafana とは

grafana.org

  • インフラや、アプリケーションの分析データなど時系列データを可視化するためのダッシュボードツール
  • デフォルトで複数のデータストアをサポートしている
    • Graphite
    • Elasticsearch
    • Cloudwatch
    • Prometheus
    • InfluxDB

今回は InfluxDB を利用しました。

構成

VagrantVM を 2個立ち上げて作成しました。

  • 負荷状況を送信するサーバ(CentOS 6.7)
  • Grafana, InfluxDB サーバ(CentOS 6.7)
    • 本来は Grafana と InfluxDB は別サーバにすべきだと思いますが、お試しのため同居させています。

f:id:mmts1007:20170110182941p:plain

  1. 負荷情報を確認したいサーバの CPU, メモリ情報を InfluxDB に送信
  2. InfluxDB は CPU, メモリ情報を DB に格納
  3. ブラウザから Grafana ダッシュボードにアクセス
  4. Grafana はダッシュボードを表示するために必要な情報を InfluxDB から取得し、ダッシュボードを表示する

インストール手順

Grafana のインストール

http://docs.grafana.org/installation/rpm/ を参考に Grafana をインストール

# Grafana のインストール
$ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-4.0.2-1481203731.x86_64.rpm

# Grafana を起動
$ sudo service grafana-server start

http://192.168.33.10:3000/login にブラウザからアクセスし、Grafana のログイン画面が開くことを確認 f:id:mmts1007:20170110190024p:plain

InfluxDB のインストール

https://www.influxdata.com/downloads/ に記載されている「RedHat & CentOS」のとおり実施

f:id:mmts1007:20170110191059p:plain

# influxDB をダウンロード
$ wget https://dl.influxdata.com/influxdb/releases/influxdb-1.1.1.x86_64.rpm

# インストール
$ sudo yum localinstall -y influxdb-1.1.1.x86_64.rpm

# InfluxDB 起動
$ sudo service influxdb start

InfluxDB データベースの作成

https://docs.influxdata.com/influxdb/v1.1/introduction/getting_started/ を参考にデータベースを作成します。

$ influx
Visit https://enterprise.influxdata.com to register for updates, InfluxDB server management, and monitoring.
Connected to http://localhost:8086 version 1.1.1
InfluxDB shell version: 1.1.1
> CREATE DATABASE grafana

# DB が作成されているか確認
> SHOW DATABASES
name: databases
name
----
_internal
grafana

Grafana のセットアップ

Grafana の管理画面から利用するデータストアや、ダッシュボードの設定します。

  1. http://192.168.33.10:3000/login にブラウザからアクセス(admin/admin でログイン)
  2. 左上 Grafana アイコンクリック → 「Data Sources」 をクリック f:id:mmts1007:20170110202657p:plain

  3. 「Add Data Source」をクリック f:id:mmts1007:20170110202952p:plain

  4. InfluxDB の情報を入力、「Add」をクリック f:id:mmts1007:20170110203120p:plain

負荷情報の取得方法

InfluxDB へのデータアクセス方法は、複数サポートしています。
今回は負荷情報を確認したいサーバから InfluxDB の HTTP API を利用して負荷情報を送信します。

CLI

https://docs.influxdata.com/influxdb/v1.1/tools/shell/

$ influx -execute 'SELECT * FROM "h2o_feet" LIMIT 3' -database="NOAA_water_database" -precision=rfc3339
name: h2o_feet
--------------
time                           level description        location         water_level
2015-08-18T00:00:00Z     below 3 feet               santa_monica     2.064
2015-08-18T00:00:00Z     between 6 and 9 feet  coyote_creek  8.12
2015-08-18T00:06:00Z     between 6 and 9 feet  coyote_creek  8.005

HTTP API

https://docs.influxdata.com/influxdb/v1.1/tools/api/

$ curl -i -XPOST "http://localhost:8086/write?db=mydb&precision=s" --data-binary 'mymeas,mytag=1 myfield=90 1463683075'

クライアントライブラリ

https://docs.influxdata.com/influxdb/v1.1/tools/api_client_libraries/

実際に利用したコマンドは以下のとおり

メモリの使用率を取得するスクリプト

while true; do
  curl -i -s -XPOST 'http://192.168.33.10:8086/write?db=grafana' --data-binary "memory,host=serverA,region=jp_east value=`free -t | grep Total | sed 's/[\t ]\+/\t/g' | cut -f3`"
  sleep 5
done

CPU の使用率を取得するスクリプト

while true; do
  idle=`mpstat | tail -n 1 | sed 's/[\t ]\+/\t/g' | cut -f11`
  rate_of_cpu=`echo "100-${idle}" | bc`
  curl -i -s -XPOST 'http://192.168.33.10:8086/write?db=grafana' --data-binary "cup,host=serverA,region=jp_east value=${rate_of_cpu}"
  sleep 5
done

まとめ

ダッシュボードを作成し、以下のような値が取得できるようになりました。 f:id:mmts1007:20170111000020p:plain

過去のデータも確認することができますし、毎回サーバにログインする手間もなくなるので楽になりそうですね。

本番で運用するためには Grafana の admin ユーザのパスワード変更や、InfluxDB の パスワード設定などセキュリティの設定をする必要があります。(今回は Vagrant 上にお試しで作ったのでセキュリティ部分に関しては設定していません)

傘が必要か通知する bot を作った

注意:今回は趣味全開の内容です。

ことりちゃんが毎朝 Slack に今日傘が必要か通知する bot を作りました。

f:id:mmts1007:20160320181655p:plain

その日の降水確率が 50% を超えている時は、傘を忘れないよう Mention 付きでメッセージを投稿します。

f:id:mmts1007:20160323001324p:plain

これで、傘を忘れてびしょ濡れになることは少なくなるはず。

経緯

  • 3月初旬に傘を持って行かずびしょ濡れになり、後悔した
  • 毎朝傘を持っていくべきか調べて出勤するのは面倒臭い
  • 自分で作りたかった

という理由から、毎朝 Slack に傘が必要か通知する bot を作りました。

ソースコード

github.com

使用技術

  • Heroku
  • Ruby
  • Rake
  • Slack(Incoming WebHooks)

Heroku Scheduler を利用し、指定の時間に傘が必要か Slack に通知する Rake タスクを実行しています。
Slack への通知は Incoming WebHooks を利用しています。

降水確率の取得

傘の必要性はその日の降水確率から判断しています。 天気予報の Web API は種類があったのですが、降水確率を取得できる Web API はあまり見つけられず。
今回は 気象庁の天気予報情報を XML で配信 - drk7jp を利用しました。

指定の都道府県・地域の 6時間ごと(00-06, 06-12, 12-18, 18-24)の降水確率が取得できるため、1日の最高降水確率によってメッセージを分ける実装にしました。

text = case today.probabilities_of_rain.max
         when 50..100 # %
           DANGER_MESSAGE
         when 40..50 # %
           CAUTION_MESSAGE
         else
           NOTICE_MESSAGE
         end

https://github.com/mmts1007/rake_lake/blob/6ce6421ae942dc50f163d9a2cfeee7baa5cb9101/lib/tasks/slack/notify/probability_of_rain.rb#L24-L31

その他

heroku は Web 上から簡単に環境変数をセットできるため、 都道府県・地域・IncomingWebHook URL・メッセージは環境変数を利用して切り替えられるようにしました。

まとめ

趣味全開ですが、heroku を試すこともでき、実用性もあるので良しとしますw

技術情報の集め方

私の技術ネタ、情報の集め方を紹介
通勤中はこれらのページをざっくり見て、情報を集めています。
気になる記事をお気に入りして後でじっくり読んだり、帰宅して触ってみたりしています。

はてなブックマーク

はてなブックマーク - テクノロジー

はてなブックマークのテクノロジーカテゴリ
スマホアプリで閲覧しています。

Qaleidospace

Qaleidospace

Qiita のランキングサイトです。
期間を指定できるのも良いところです。 どんな技術が流行っているのかをチェックするために使っています。

GitHub(Trend)

github.com

GitHub ではトレンドランキングが表示できます。
言語で絞ることもできるので、私は Ruby, Java, JavaScript のトレンドを見ています。
世界的にどのような言語・ライブラリ・FWが人気なのか、動きが活発なのかをチェックしています。

こんな感じで毎日情報を集めています。
是非自分にあった情報の集め方の参考になればと思います。

構成管理ツールを作ってみた

siman(シーマン) という構成管理ツールを作りました。
(SImple configuration MANagement tool の略です。seaman ではありません。)

github.com

Vagrant仮想マシンを作るたびに環境構築するのが面倒だったので、楽にするためのツールを作成しました。 Chef よりもさらにシンプルに、Ruby を使わずにできたらと思いこの形になりました。 Chef の様にリモートサーバの構成管理は行えません。

"No." "実行内容" "レシピ(シェルスクリプト)の URL"

の形式でメニューを作成し、siman を実行すると
レシピを DL し、順次実行します。

冪等性を保つために、メニューの何番目まで実行したか記憶しています。 既に実行済みのメニューは再度実行されません。

レシピは URL 形式で指定するため

github.com

な感じでリポジトリに置いたシェルスクリプトの URL を指定したり、 Gist の URL を張るなりして使用します。

例えば、私は Rails の環境をよく作るので
rbenv を install -> ruby 2.2.3 を install -> rails を install といったメニューを作成しました。
(URL は git.io で短絡しています。)

001  install_rbenv     https://git.io/vzohD
002  install_ruby_223  https://git.io/vzohy
003  install_rails     https://git.io/vzohH

インストール方法等は siman/README.md at master · mmts1007/siman · GitHub を参考にしていただければと思います。

Ruby Enumerable#group_by を使ってみた

DB から取得したデータを Ruby 上でグルーピングしたくてドキュメントを漁っていたら見つけたので紹介。

経緯

DB に入っている

id task_type_id task_id
1 1 1
2 1 2
3 1 3
4 2 4
5 2 5
6 2 6

こんな感じでデータを

[
  {
    task_type_id: 1,
    task_id: [1, 2, 3]
  },
  {
    task_type_id: 2,
    task_id: [4, 5, 6]
  }
]

こんな感じにタスクの種別に紐付いているタスクの一覧 JSONRuby で作りたかった。 task_type_id でグルーピングされた値が取れれば と思い、繰り返し関係を提供している Enumerable モジュールのドキュメントを漁った。

結果

案の定欲しいメソッドはあった。メソッド名見た瞬間、「これ!」ってなった。

instance method Enumerable#group_by (Ruby 2.2.0)

使い方

# DB から取得したデータ
records = [
  { id: 1, task_type_id: 1, task_id: 1 },
  { id: 2, task_type_id: 1, task_id: 2 },
  { id: 3, task_type_id: 1, task_id: 3 },
  { id: 4, task_type_id: 2, task_id: 4 },
  { id: 5, task_type_id: 2, task_id: 5 },
  { id: 6, task_type_id: 2, task_id: 6 }
]

records.group_by { |record| record[:task_type_id] }
# => {1=>[{:id=>1, :task_type_id=>1, :task_id=>1}, {:id=>2, :task_type_id=>1, :task_id=>2}, {:id=>3, :task_type_id=>1, :task_id=>3}], 2=>[{:id=>4, :task_type_id=>2, :task_id=>4}, {:id=>5, :task_type_id=>2, :task_id=>5}, {:id=>6, :task_type_id=>2, :task_id=>6}]}

ということで、task_type_id でグルーピングされた値を取得することができた。
後はデータの形式を求めている形に変換すれば完了

records.group_by { |record| record[:task_type_id] }
  .map { |k, v| { task_type_id: k, task_id: v.map { |e| e[:id] } } }

(group_by の後の map が分かりづらい気がする…。また考えよう。)