読者です 読者をやめる 読者になる 読者になる

あすたぴのブログ

astap(あすたぴ)のブログ

RackとWardenについて

Rackについてはこちらをみた。

じぶんの解釈

Rackとはwebサーバーを作る際のお作法(インターフェース)。

インターフェースに従ってWebサーバーを作成することで、サーバーを交換可能。 ミドルウェアを共通化できる。 という感じだろうか

Warden

General Rack Authentication Framework

Rackの上で構築される認証のフレームワーク。 有名なdeviseはwardenのラッパーになる。

Rackミドルウェアとして作られている。 session情報は env['rack.session'] Wardenは env[warden] にオブジェクトを入れる。 このオブジェクトを用いて、認証を行うことができる。

env['warden'].authenticated?               # Ask the question if a request has been previously authenticated
env['warden'].authenticated?(:foo)         # Ask the question if a request is authenticated for the :foo scope
env['warden'].authenticate(:password)      # Try to authenticate via the :password strategy.  If it fails proceed anyway.
env['warden'].authenticate!(:password)     # Ensure authentication via the password strategy. If it fails, bail.

認証が成功されると、 user オブジェクトへアクセスが出来る。 これは nil 以外なら何でもよい。

env['warden'].authenticate(:password) この記述ではパスワードで認証を行っている。 この、どのように認証を行うか。を strategy と呼んでいて、これを拡張して独自で認証機構を実装することができる。

strategy

Rackアプリからwadenを利用していく

ためしにやっていく

Rackアプリ

config.ru というファイル名で以下を作成

require 'rack'


app = Proc.new do |env|
    ['200', {'Content-Type' => 'text/html'}, ['A barebones rack app.']]
end

run app

rackup コマンドで config.ru を読み込んでサーバーを起動する。

vagrant-ubuntu-trusty-64% rackup
Puma starting in single mode...
* Version 3.8.2 (ruby 2.3.0-p0), codename: Sassy Salamander
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://localhost:9292
Use Ctrl-C to stop
vagrant-ubuntu-trusty-64% curl http://localhost:9292/
A barebones rack app.%

curlでリクエストを投げると、レスポンスが確認できる。

wardenの追加

require 'rack'
require 'warden'

app = Proc.new do |env|
    ['200', {'Content-Type' => 'text/html'}, ['A barebones rack app.']]
end

failure_app = Proc.new do |env|
    ['401', {'Content-Type' => 'text/html'}, ['fail.']]
end

use Rack::Session::Cookie, :secret => "replace this with some secret key"
use Warden::Manager do |manager|
  manager.default_strategies :password, :basic
  manager.failure_app = failure_app
end

run app

wardenを使う際には、デフォルトのstrategyと認証が失敗した場合に呼ばれる、rack endpoint を指定する。

sessionシリアライズロジックと認証するユーザー

config.ru の run.app の前あたりに以下を入れる

Warden::Manager.serialize_into_session do |user|
  user.id
end

Warden::Manager.serialize_from_session do |id|
  User.get(id)
end

Userクラスの定義もその下らへんに書く。

class User
  attr_accessor :id, :password

  USER_MAPPING = {
    a: 'hogehoge',
    b: 'mogemoge',
  }

  def initialize(id, pass)
    self.id = id
    self.passworkd = pass
  end

  def self.get(id)
    new(id, USER_MAPPING[id.to_sym])
  end
end

これで認証のもとなるUserの準備ができたので、認証ロジックを作りたいとおもう。

認証をしてみる

strategyは複数設定が可能。 その中の1つでも成功するか、すべての戦略を通るか、戦略がfailするまで呼ばれる。

strategyWarden::Strategies::Base を継承して作られる。 実装が必要なのは、 valid?authenticate! の2つ。

valid?

valid? メソッドは strategy の実行条件(ガード)。 宣言シない場合、strategy は常に実行される。

上記の例では、 username, passwordのどちらか1つでもあれば戦略を実行します。

authenticate!

認証ロジック。 利用可能なメソッドがいくつかある。

  • request
  • session
  • params
  • env

  • halt!

    • strategyの実行を止める。後続のstrategyは実行されない
  • pass
    • strategyを実行しない
  • success!
    • 認証に成功。userオブジェクトをsessionに格納し、ログインする。halt!する
  • fail!
    • 認証失敗。halt!
  • redirect!
    • 別のURLへリダイレクトする。halt!
  • custom!

    • わからん
  • headers

    • headerを設定する
  • errors
    • errorオブジェクトへのアクセス

では、実際に strategy を書いてみる。 こんな感じのコードをまた run.app の前あたりにいれる。

Warden::Strategies.add(:password) do

  def valid?
    params['id'] || params['password']
  end

  def authenticate!
    if User.authenticate(params['id'], params['password'])
      success! User.get(params['id'])
    else
      fail!('failll')
    end
  end
end

User.authenticateを実装する

  def self.authenticate(id, pass)
    return false unless password = USER_MAPPING[id.to_sym]
    !!(password == pass)
  end

Rack endpointを修正する

app = Proc.new do |env|
  env['warden'].authenticate!
  ['200', {'Content-Type' => 'text/html'}, ['A barebones rack app.']]
end

やってみる

vagrant-ubuntu-trusty-64% curl "http://localhost:9292/?id=a&password=hogehoge"
A barebones rack app.%

vagrant-ubuntu-trusty-64% curl "http://localhost:9292/?id=b&password=hogehoge"
fail.%

id=a, password=hogehogeは認証が成功し、id=b, password=hogehogeはfailしている。

scope

複数のユーザー(タイプ?)をログイン可能にする。

defautユーザーとAdminユーザーで認証ロジックを変えたい場合など、 スコープ(defalt, admin)でそれぞれ定義が可能。

callbacks

  • after_set_user
  • after_authentication
  • after_fetch
  • before_failure
  • after_failed_fetch
  • before_logout
  • on_request

これらのタイミングにcallbackを仕込める。

Warden::Manager.after_set_user do |user, auth, opts|
  unless user.active?
    auth.logout
    throw(:warden, :message => "User not active")
  end
end

set_userは認証に成功し、env[‘warden’].user にユーザーオブジェクトを入れる時のこと。 そのあとに上記のcallbackが呼ばれる。

まとめ

これだけ見ると非常にシンプル。 deviseを理解するために、wardenを見た。 次はdeviseを理解していくぞ。

CircleCi2.0が最高かもしれん

ローカルの環境をdockerで整えたので、次はCI環境を整備する。 dockerで構築したのだから、テストもdockerにしたい。 ちょうどCircleCi2.0がBetaテスト中で、さらにネイティブでdockerサポートが入っている。 これは試すしかないということでやってみた。

CricleCi2.0を使う

プロジェクトのホームディレクトリに .circleci ディレクトリを作成。 config.yml を作成する

config.yml の中身

version: 2
jobs:
  build:
    working_directory: ~/app
    docker:
      - image: hogehoge/mogemoge:latest
    steps:
      - checkout
      - run: echo "hello world"

これでCircleCiを通すだけの準備は完了する。 imageはDockerHubのpublicディレクトリであれば普通に取ってこれる。privateリポは別途設定が必要。 config.yml の設定は、try & error で直して、pushして、CircleCI流して〜ってやるとかなり消耗する。 複雑なテストをやろうとすると死ねる。

CircleCi2.0ではローカルでほぼ同様のことが可能になっている。 (docker executorのみ使える。従来のCircleCiの主流であったmachine executorは使えない)

circleci コマンド

install

vagrant-ubuntu-trusty-64% sudo curl -o /usr/local/bin/circleci https://circle-downloads.s3.amazonaws.com/releases/build_agent_wrapper/circleci && sudo chmod +x /usr/local/bin/circleci

usage はこんな感じ

vagrant-ubuntu-trusty-64% circleci
Receiving latest version of circleci...
The CLI tool to be used in CircleCI.

Usage:
  circleci [flags]
  circleci [command]

Available Commands:
  build       run a full build locally
  config      validate and update configuration files
  tests       collect and split files with tests
  version     output version info

Flags:
  -c, --config string   config file (default is .circleci/config.yml)
      --taskId string   TaskID
      --verbose         emit verbose logging output

config fileのvalidate

vagrant-ubuntu-trusty-64% circleci config validate

  config file is valid

buildのhelp

vagrant-ubuntu-trusty-64% circleci build --help

run a full build locally

Usage:
  circleci build [flags]

Flags:
      --branch string         Git branch
      --checkout-key string   Git Checkout key (default "~/.ssh/id_rsa")
      --config string         config file (default ".circleci/config.yml")
      --index int             node index of parallelism
      --job string            job to be executed (default "build")
      --parallelism int       parallelism level (default 1)
      --repo-url string       Git Url
      --revision string       Git Revision
      --skip-checkout         use local path as-is (default true)
  -v, --volume value          Volume bind-mounting (default [])

Global Flags:
      --taskId string   TaskID
      --verbose         emit verbose logging output

circleci build をふつうに叩いたらふつうに落ちた。 どうやら working_directroyが相対パスだとダメらしい。最初のymlで相対パスで設定させといてそれ?w

working_directory: /app に変更して通った。

最終的なかたち

version: 2
jobs:
  build:
    working_directory: /app
    docker:
      - image: docker image name
        environment:
          RACK_ENV: 'test'
      - image: mysql:5.7.17
        environment:
          MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
          MYSQL_ROOT_HOST: '%'
    steps:
      - checkout
      - run: bundle install
      - run: bundle exec rails db:create
      - run: bundle exec rails db:migrate
      - run: bundle exec rspec
      - deploy:
          name: run deploy
          command: |
            if [ "${CIRCLE_BRANCH}" == "master" ]; then
              apk add curl
              curl --user ${CIRCLE_API_TOKEN}: \
                --data build_parameters[CIRCLE_JOB]=deploy \
                --data revision=$CIRCLE_SHA1 \
                https://circleci.com/api/v1.1/project/github/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/tree/$CIRCLE_BRANCH \
                >> /dev/null
            fi
  deploy:
    working_directory: /app
    docker:
      - image: docker image name
        environment:
          RACK_ENV: 'production'
    steps:
      - checkout
      - run: bundle install
      - setup_remote_docker
      - deploy:
          name: deploy
          command: echo 'hello'

docker-composeのように、imageがリンクされるわけではないので 127.0.0.01 でアクセスが出来るようにする必要があった。MYSQL_ROOT_HOST: '%'

config/database.yml にはこんな感じにかく host: <%= ENV['DB_HOST'] || ENV['MYSQL_ROOT_HOST'] || '127.0.0.1' %>

以下のサポートを参照した。 https://discuss.circleci.com/t/rails-mysql-container-connection/10604

deploy

こちらを参考にさせていただきました。 http://h3poteto.hatenablog.com/entry/2017/03/31/231410

別ジョブで実行するという方式。 残念ながら別ジョブはCLIコマンドではテストが出来ないので、 pushして try & error になってしまった。 ハックだし、いずれより良い形で公式にサポートがされるだろう。 現時点では docker executorでdeployするimageをbuildするいい方法がないかもしれない。

deployタスクは1.0のときのように、デフォルトで branchで別けれるようにしてほしかった。

【追記】 公式ドキュメントに増えた?昨日はなかった気がするのにw

circleci.com

This approach is a temporary workaround for the current features available during Beta. Soon we’ll be adding a much more elegant way to manage multiple jobs.

より良い方法をそのうち公開するっていってるのでよかったね

まとめ

deploy抜きで考えるならば、テスト、構築ともに速くなって非常にいいと思った。 そもそもここでdeploy用のimageをbuildするという方式も違うのかもしれない。 testが通ったwebhookで別のところでやってもいいかもしれない。 それも考えてみよう。

Rail5.1 rc1+Dockerでいい感じの環境を構築する

目的

いい感じに使える、Dockerの開発環境を構築する。 最終的な目的はProductionでDockerを使うイメージを固めること。

環境

  • Vagrant
  • Docker
    • Docker version 17.03.1-ce, build c6d412e
  • Rails5.1 rc1

docker for mac はいい噂を聞かないのでVagrant上でDockerを使用する。 当然、docker for mac でも動く。

Railsアプリ作成

bundle init
gem 'rails', '~> 5.1.0.rc1'
bundle instlal --path vendor/bundle

sprockets, turbolinksを抜いています。 DBはmysqlを使用

bundle exec rails new --skip-bundle -d mysql --skip-sprockets --skip-turbolinks .

ローカルにbundle設定があるとダメなので消します。

rm -rf vendor/bundle
rm -rf .bundle

Docker install

sudo apt-get update
sudo apt-get install \
    linux-image-extra-$(uname -r) \
    linux-image-extra-virtual

sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    software-properties-common

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"

sudo apt-get install docker-ce

dockerグループが出来ているので、vagrantユーザーをdockerグループに追加する必要があります。 その後、vagrantからlogoutし再度sshで入ります。

sudo gpasswd -a vagrant docker

docker-compose install

sudo curl -L "https://github.com/docker/compose/releases/download/1.11.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

Dockerfile

Dockerfileを作成します。 alpine linuxをベースにRuby2.4.1の入っているイメージを元にします。 必要なパッケージをインストール後、不要なファイルを消したりします。 mysql2 gem をbuildするために必要なmariadb-devを入れるとmariadb本体も付いてきてしまうため、mariadb本体のファイルを削除しています。

cmd, entrypointはrun時に指定するので付けていません。

Dockerfile

FROM ruby:2.4.1-alpine

ENV LANG ja_JP.UTF-8
ENV BUILD_PACKAGES="curl-dev build-base" \
    DEV_PACKAGE="mariadb-libs mariadb-client mariadb-client-libs tzdata"

RUN gem install bundler \
  && apk --update --upgrade add $BUILD_PACKAGES \
  && apk add mariadb-dev tzdata linux-headers postgresql-dev sqlite-dev git nodejs \
  && rm /usr/lib/libmysqld* \
  && echo 'gem: --no-document' > /etc/gemrc

WORKDIR /app
COPY Gemfile Gemfile
COPY Gemfile.lock Gemfile.lock
RUN bundle install

EXPOSE  3000

docker-compose.ymlを作成します。

spring server起動用のコンテナをwebと同一Dockerfileから作っています。 springについては以下を参考にさせていただきました。

参考

https://github.com/jonleighton/spring-docker-example http://tech.degica.com/ja/2016/06/14/dockerized-rails-development/

user指定時に、ホストOSのユーザーIDとグループIDを指定している。 ユーザーを指定しない場合、rootで実行されるため、Railsコマンドで生成したファイルがrootの所有となって、ホストOSのエディタから編集が出来なくなる。

version: '2'
services:
  db:
    image: mysql:5.7.17
    volumes:
      - ./store:/var/lib/mysql
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'

  web:
    build: .
    command: bundle exec rails s
    user: "${uid}:${gid}"
    working_dir: /app
    environment:
      RACK_ENV: 'development'
    volumes:
      - .:/app
    ports:
      - "3000:3000"
    depends_on:
      - db

  spring:
    build: .
    command: docker/run.sh spring
    volumes:
      - .:/app

mysqlの設定

※追記 mysqlコンテナを用意するようにしたのでこちらは不要になりました。

DockerコンテナからホストOSのMySQLに接続するためにホストOS側のMySQLの設定を修正します。

sudo vi /etc/mysql/my.cnf

bindするアドレスに0.0.0.0を追加します。

bind-address    = 127.0.0.1
bind-address    = 0.0.0.0

MySQLを再起動します。

sudo service mysql restart

bindアドレス同様に、 rootアカウントがlocalhostからしかログインできないためユーザーを追加します。

mysql -u root
CREATE USER 'root'@'172.17.0.2';
GRANT ALL PRIVILEGES ON *.* TO 'root'@'172.17.0.2';
FLUSH PRIVILEGES;

開発

docker-compose系のコマンドを叩くのが面倒な為、scriptを用意しています。

  • 引数なしでヘルプ表示
  • 第一引数がupなら、docker-compose up
  • 第一引数がrspecならRACK_ENV=testでrspec起動
  • 第一引数がsならフォアグラウンドでRails起動 port指定
  • 第一引数がspringならspringサーバーをバックグラウンドで起動
  • 引数ありの場合は引数をそのまま渡す。

docker/bundle.sh

#!/bin/bash
ENV=$RACK_ENV
export uid=$UID
export gid=$GID

if [ "$ENV" = 'production' ]; then
  docker-compose run --entrypoint=$entrypoint -e RACK_ENV=$ENV web s
  exit 0
fi

entrypoint=docker/run.sh
if [ "$1" = 'up' ]; then
  docker-compose up
elif [ "$1" = 'rspec' ]; then
  docker-compose run --entrypoint=$entrypoint -e RACK_ENV=test web $@
elif [ "$1" = 'spring' ]; then
  docker-compose run -u "$UID:$GID" --entrypoint=$entrypoint -d spring spring
elif [ "$1" = 's' ]; then
  docker-compose run -p 3000:3000 -u "" --entrypoint=$entrypoint -e RACK_ENV=development web s
else
  docker-compose run -u "$UID:$GID" --entrypoint=$entrypoint -e RACK_ENV=development web $@
fi

docker/run.sh

alpine linuxにはbash等が入っていなく、わざわざ入れる必要もないコードなのでデフォルトのシェルであるashで書いています。

#!/bin/ash

if [ "$RACK_ENV" = 'development' -o "$RACK_ENV" = 'test' -o "$RACK_ENV" = '' ]; then
  if [ $# -eq 0 ]; then
    bundle exec rails -h
  elif [ $1 = 'rspec' ]; then
    bundle exec $@
  elif [ $1 = 'spring' ]; then
    bundle exec spring binstub --all
    #bundle exec spring binstub --remove --all
    bin/spring server
  else
    time bundle exec rails $@
  fi
  exit 0
fi

bundle exec unicorn -c config/unicorn.rb -E $RACK_ENV

deploy

deploy時に使用するDockerfileを作る

FROM ruby:2.4.1-alpine

ENV LANG ja_JP.UTF-8
ENV BUILD_PACKAGES="curl-dev build-base" \
    DEV_PACKAGE="mariadb-libs mariadb-client mariadb-client-libs tzdata"

RUN gem install bundler \
  && apk --update --upgrade add $BUILD_PACKAGES \
  && apk add mariadb-dev tzdata linux-headers postgresql-dev sqlite-dev git nodejs \
  && rm /usr/lib/libmysqld* \
  && echo 'gem: --no-document' > /etc/gemrc

WORKDIR /app
COPY Gemfile Gemfile
COPY Gemfile.lock Gemfile.lock
RUN bundle install

ENV APP_HOME /app
RUN mkdir -p $APP_HOME
WORKDIR $APP_HOME
COPY . $APP_HOME

EXPOSE  3000

circle_ciでbuildして、testを実行。 testが通ったら、imageをECRなり、DockerHubなり、GCRへpushする。

まとめ

当初、DockerfileでアプリのソースをCOPYしてイメージを作っていたが、 ソースを含めてイメージを作るのはdeploy時だけでいいのでやめた。 軽く開発してみたが上手く進められている。 deploy周り(CricleCI含め)を妄想しかしていないので、いざやってみたらまた変わるかもしれない。

問題点が1つあって、user指定で実行するとbundler関係のディレクトリがroot所有なのでwarningが出る。 解決策が思いつかない。どなたかいい解決策があれば教えてください。

`/` is not writable.
Bundler will use `/tmp/bundler/home/unknown' as your home directory temporarily.

今回のソースは以下にあります。 https://github.com/astapi/development_rails_with_docker

terraform で aws のいい感じの構成を作る(基盤編1)

対象読者

terraformが何かを知っていて、 terraformを使おうと考えている人。

terraform version 0.9.1

初期設定

適当にディレクトリを作成します。

mkdir terraform_test
cd terraform_test

0.9.1からstate environmentsを設定できるようになったので、 dev, stg, prod みたいに同一ソースで分岐ができるようになる。

vagrant-ubuntu-trusty-64% terraform env new dev
Created and switched to environment "dev"!

You're now on a new, empty environment. Environments isolate their state,
so if you run "terraform plan" Terraform will not see any existing state
for this configuration.

env new をすると、作成したenvに移る。 gitのbranchみたいだな。

vagrant-ubuntu-trusty-64% terraform env list
  default
* dev

AWS 設定

provider "aws" {
  region = "ap-northeast-1"
}
variable "aws_account_id" {
  default = "1111111111"
}

IAMアカウントの作成 AWSのIAMベストプラクティスをちゃんとやる(見る) 上記エントリに沿って、初期設定を行う。

resource "aws_iam_user" "astapi" {
  name = "astapi"
  path = "/"
}

resource "aws_iam_access_key" "astapi" {
  user = "${aws_iam_user.astapi.name}"
}

plan

IAMのベストプラクティスでは、ルートアカウントのアクセスキーとか消さないといけないのだけど、 terraformでIAMユーザーから作りたいので、最初だけルートアカウントのキーを使う。

export AWS_ACCESS_KEY_ID="anaccesskey"
export AWS_SECRET_ACCESS_KEY="asecretkey"
export AWS_DEFAULT_REGION="ap-northeast-1"

planコマンドで作成予定のリソースを確認する。

terraform plan
中略

+ aws_iam_access_key.astapi
    encrypted_secret:  "<computed>"
    key_fingerprint:   "<computed>"
    secret:            "<computed>"
    ses_smtp_password: "<computed>"
    status:            "<computed>"
    user:              "astapi"

+ aws_iam_user.astapi
    arn:           "<computed>"
    force_destroy: "false"
    name:          "astapi"
    path:          "/"
    unique_id:     "<computed>"

apply

terraform apply

f:id:astap:20170326234644p:plain

IAMユーザーができました。

terraform show

このコマンドで作成したアクセスキーの情報が閲覧できます。

vagrant-ubuntu-trusty-64% terraform show
aws_iam_access_key.astapi:
  id = AKJLAKJDA
  secret = aslkfjaslfkjasljfas;ljfalskjdfalsjkfak
  ses_smtp_password = asdklfjasldkfjasdkfjasldjkfas;l
  status = Active
  user = astapi
aws_iam_user.astapi:
  id = astapi
  arn = arn:aws:iam::1111111111:user/astapi
  force_destroy = false
  name = astapi
  path = /
  unique_id = AAKLJALKFJAKDJALKJD

ポリシーのアタッチ

ルートアカウントを使わなくてもterraformで全ての権限が必要なので、AdministratorAccessが必要。

resource "aws_iam_policy_attachment" "admin" {
  name = "admin-policy"
  users = ["${aws_iam_user.astapi.name}"]
  policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
}

今後は作成したIAMユーザーのACCESS_KEYを使って実行していきます。 ルートアカウントのACCESS_KEYを削除しましょう。

まとめ

terraform でIAMユーザーのアクセスキーまで作成したが、terraform showでキーが参照できるため、 複数人開発ではこの方式だとNGになりますね。 また、Backend(terraformで作成したAWSの状態を保存しておくところ)がlocalのため、 そもそも自分のローカルでしか実行が出来ない。 backendとしては色々用意されているが、S3 backendでは、terraform envに対応していなかった為、 counsul等、自前サーバーを用意したりする必要がありそう。 そのconsulサーバーを用意するterraformが必要・・・。 うーん。