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
と呼んでいて、これを拡張して独自で認証機構を実装することができる。
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するまで呼ばれる。
strategy
は Warden::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
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を使うイメージを固めること。
環境
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
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が必要・・・。 うーん。