シンプルなRails環境を最速プロビジョニング。各種ツールの利用比較 (Chef [Berkshelf], Ansible [Playbook], Docker [Docker Compose], 手動)

プロビジョニングのための構成管理フレームワークには様々なものがあり、例に挙げると以下のようなものがあります。

  • Chef
  • Ansible
  • Puppet
  • SaltStack
  • CFEngine
  • PowerShell DSC
  • Itamae
  • AWS CloudFormation
  • Terraform
  • Mobingi

ItamaeはCookpadの社員の方が開発した、機能がシンプルで学習コストが低く、Chefを簡略化したようなものです。AWS CloudFormationはAWS上のサーバなどの構成をJSONまたはYAMLで記述して管理するものです。TerraformVagrantPackerなどで有名なHashiCorp社により開発されたもので、AWSやGCP、Azureなどの様々なクラウドに対応した管理ツールとなっています。Mobingiは、従来のようなChefやAnsibleは開発者をターゲットとした扱いの難しいものとして、クラウドのデスクトップとしてGUIベースで管理できるというものです。

ここでは、Chef,Ansible,Dockerによる設定例を取り上げます。
ChefはBerkshelf、AnsibleはAnsible Playbook、DockerはDocker Composeを用いました。また、手動による設定のインストールの例も取り上げました。

例ではそれぞれ最後に、ChefはAWS OpsWorks、AnsibleはAmazon EC2、DockerはAmazon ECSを例に行っていますが、他の環境の場合は適宜置き換えて対応してください。

Chef [Berkshelf]

ローカルでテストせずにOpsWorksでデプロイする場合はVagrant周りの設定は不要で、サブタイトルに(※)のマークを入れた

  • Gemfileの作成
  • Berksfileの作成
  • Recipeの作成

の3つをやれば良い。

ディレクトリ構成

.
├── Berksfile
├── Gemfile
├── README.md
├── Vagrantfile
└── site-cookbooks
    ├── nginx
    │   ├── CHANGELOG.md
    │   ├── README.md
    │   ├── attributes
    │   │   └── default.rb
    │   ├── metadata.rb
    │   ├── recipes
    │   │   └── default.rb
    │   └── templates
    │       └── default
    │           └── nginx.conf.erb
    ├── nodejs
    │   ├── CHANGELOG.md
    │   ├── README.md
    │   ├── attributes
    │   │   └── default.rb
    │   ├── metadata.rb
    │   └── recipes
    │       └── default.rb
    └── ruby-env
        ├── CHANGELOG.md
        ├── README.md
        ├── attributes
        │   └── default.rb
        ├── metadata.rb
        ├── recipes
        │   └── default.rb
        └── templates
            └── default

VagrantでCentOS 6.7の環境設定

$ vagrant box add centos6-7 https://github.com/CommanderK5/packer-centos-template/releases/download/0.6.7/vagrant-centos-6.7.box
$ mkdir -p mkdir ./projects/chef_zero_test
$ cd projects/chef_zero_test
$ vagrant init centos6.7

Vagrantの設定ファイルを以下のように編集する。

$ vim Vagrantfile
Vagrant.configure("2") do |config|
  config.vm.box = "centos6.7"
  config.vm.box_url = "https://github.com/CommanderK5/packer-centos-template/releases/download/0.6.7/vagrant-centos-6.7.box"
  config.vm.network "private_network", ip: "192.168.33.10"
  config.vm.provider "virtualbox" do |vb|
    vb.customize ["modifyvm", :id, "--memory", "4096"]
  end
end

このままでVagrantを立ち上げると、SSH接続の設定で

default: Warning: Authentication failure. Retrying...

のようなエラーが繰り返し表示されて権限の問題でできないので、VagrantにSSHで入って

vagrant ssh

ホスト側で以下のコマンドを実行する。

cd /home
chmod 755 -R ./vagrant
exit

http://qiita.com/jshimazu/items/9db49ce64478e82d511e

Vagrantを立ち上げる。

vagrant up

Vagrantfileの設定

Vagrantfileをプロビジョニングの設定を以下のように追加する。

Vagrant.configure("2") do |config|
  config.vm.box = "centos6.7"
  config.vm.box_url = "https://github.com/CommanderK5/packer-centos-template/releases/download/0.6.7/vagrant-centos-6.7.box"
  config.vm.network "private_network", ip: "192.168.33.10"
  config.vm.provider "virtualbox" do |vb|
    vb.customize ["modifyvm", :id, "--memory", "4096"]
  end

  config.vm.provision :chef_solo do |chef|
    chef.cookbooks_path = ["./cookbooks", "./site-cookbooks"]
    chef.json = {
      nginx: {
        env: ["ruby"]
      }
    }
    chef.run_list = %w[
      recipe[yum-epel]
      recipe[nginx]
      recipe[ruby-env]
      recipe[nodejs]
    ]
  end
end

Gemfileの作成(※)

以下のようにGemfileを作成する。

$ vim Gemfile
source 'https://rubygems.org'

gem 'chef'
gem 'knife-solo'
gem 'berkshelf'
$ cd projects/chef_zero_test
$ rbenv exec bundle install

Berksfileの作成(※)

以下のようにBerksfileを作成する。

$ vim Berksfile
source "https://supermarket.chef.io"

cookbook "yum-epel"
cookbook "nginx", path: "./site-cookbooks/nginx"
cookbook "ruby-env", path: "./site-cookbooks/ruby-env"
cookbook "nodejs", path: "./site-cookbooks/nodejs"

※最初、site :opscodeの宣言方法はDeplicated

Recipeの作成(※)

nginxのレシピ

1.Cookbookの作成。

bundle exec knife cookbook create nginx -o ./site-cookbooks

2.Recipeファイルの作成。

vim site-cookbooks/nginx/recipes/default.rb
default.rb
include_recipe "yum-epel"

package "nginx" do
  action :install
end

service "nginx" do
  action [ :enable, :start ]
  supports :status => true, :restart => true, :reload => true
end

template 'nginx.conf' do
  path '/etc/nginx/nginx.conf'
  source "nginx.conf.erb"
  owner 'root'
  group 'root'
  mode '0644'
  notifies :reload, "service[nginx]"
end

3.attributeファイルの作成。

vim site-cookbooks/nginx/attributes/default.rb
default.rb
default['nginx']['env'] = []

4.nginx.confのテンプレートファイルの作成。

sudo cp /usr/local/etc/nginx/
nginx.conf ~/workspace/chef-tests/chef-test/projects/chef_zero_test/site-cookbooks/nginx/templates/default/nginx.conf.erb
vim site-cookbooks/nginx/template/default/nginx.conf.erb
nginx.conf.erb
user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;
    sendfile        on;
    keepalive_timeout  65;

    upstream app_server {
      server unix:/tmp/unicorn.sock;
    }

    server {
        listen       80 default_server;
        server_name  _;

        location / {
            rewrite ^/app_server/(.+) /$1 break;
            proxy_pass http://app_server/$1;
        }
    }
}

Rubyのレシピ

1.Cookbookの作成。

$ bundle exec knife cookbook create ruby-env -o ./site-cookbooks

2.Recipeファイルの作成。

$ vim site-cookbooks/ruby-env/recipes/default.rb
%w{vim git openssl-devel sqlite-devel readline-devel}.each do |pkg|
    package pkg do
        action :install
    end
end

git "/home/#{node['ruby-env']['user']}/.rbenv" do
    repository node["ruby-env"]["rbenv_url"]
    action :sync
    user node['ruby-env']['user']
    group node['ruby-env']['group']
end

template ".bash_profile" do
    source ".bash_profile.erb"
    path "/home/#{node['ruby-env']['user']}/.bash_profile"
    mode 0644
    owner node['ruby-env']['user']
    group node['ruby-env']['group']
    not_if "grep rbenv ~/.bash_profile", :environment => { :'HOME' => "/home/#{node['ruby-env']['user']}"  }
end

directory "/home/#{node['ruby-env']['user']}/.rbenv/plugins" do
    owner node['ruby-env']['user']
    group node['ruby-env']['group']
    mode 0755
    action :create
end

git "/home/#{node['ruby-env']['user']}/.rbenv/plugins/ruby-build" do
    repository node["ruby-env"]["ruby-build_url"]
    action :sync
    user node['ruby-env']['user']
    group node['ruby-env']['group']
end

execute "rbenv install #{node['ruby-env']['version']}" do
    command "/home/#{node['ruby-env']['user']}/.rbenv/bin/rbenv install #{node['ruby-env']['version']}"
    user node['ruby-env']['user']
    group node['ruby-env']['group']
    environment 'HOME' => "/home/#{node['ruby-env']['user']}"
    not_if { File.exists?("/home/#{node['ruby-env']['user']}/.rbenv/versions/#{node['ruby-env']['version']}")}
end


execute "rbenv global #{node['ruby-env']['version']}" do
    command "/home/#{node['ruby-env']['user']}/.rbenv/bin/rbenv global #{node['ruby-env']['version']}"
    user node['ruby-env']['user']
    group node['ruby-env']['group']
    environment 'HOME' => "/home/#{node['ruby-env']['user']}"
    not_if { File.exists?("/home/#{node['ruby-env']['user']}/.rbenv/versions/#{node['ruby-env']['version']}")}
end


execute "rbenv shell #{node['ruby-env']['version']}" do
    command "/home/#{node['ruby-env']['user']}/.rbenv/bin/rbenv shell #{node['ruby-env']['version']}"
    user node['ruby-env']['user']
    group node['ruby-env']['group']
    environment 'HOME' => "/home/#{node['ruby-env']['user']}"
    not_if { File.exists?("/home/#{node['ruby-env']['user']}/.rbenv/versions/#{node['ruby-env']['version']}")}
end

3.attributeファイルの作成。

$ vim site-cookbooks/ruby-env/attributes/default.rb
default['ruby-env']['user'] = "vagrant"
default['ruby-env']['group'] = "vagrant"
default['ruby-env']['version'] = "2.3.1"
default['ruby-env']['rbenv_url'] = "https://github.com/sstephenson/rbenv"
default['ruby-env']['ruby-build_url'] = "https://github.com/sstephenson/ruby-build"

※EC2にデプロイする場合は以下のようにuserとgroupの内容をec2-userにする。

default['ruby-env']['user'] = "ec2-user"
default['ruby-env']['group'] = "ec2-user"
default['ruby-env']['version'] = "2.3.1"
default['ruby-env']['rbenv_url'] = "https://github.com/sstephenson/rbenv"
default['ruby-env']['ruby-build_url'] = "https://github.com/sstephenson/ruby-build"

4..bash_profileのテンプレートファイルの作成

$ vim site-cookbooks/ruby-env/template/default/.bash_profile.erb
bash_profile.erb
# .bash_profile

if [ -f ~/.bashrc] ; then
    . ~/.bashrc
fi

PATH=$PATH:$HOME/bin
export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init -)"

Node.jsのレシピ

1.Cookbookの作成。

$ bundle exec knife cookbook create nodejs -o ./site-cookbooks

2.Recipeファイルの作成。

$ vim site-cookbooks/nodejs/recipes/default.rb
bash "install nodejs" do
    user "root"
    cwd "/tmp"
    code <<-EOC
        curl --silent --location https://rpm.nodesource.com/setup_6.x | bash -
        yum -y install gcc-c++ make nodejs
     EOC
end

Berkshelfの実行 + Vagrantのプロビジョニング

Berkshelfを実行して、Vagrantでプロビジョニングを実行する。

$ bundle exec berks vendor ./cookbooks
$ vagrant reload
$ vagrant provision

bundle exec berks install --path ./cookbooksはdeprecated

Railsの動作確認

ホスト側でRailsをインストールする。

$ vagrant ssh
# rbenv shell 2.3.1
$ gem install rails -V
$ rails new sample --skip-bundle
$ cd sample/
$ mkdir -p shared/{pids,log}

Gemfileを開いてgem 'unicorn'の一行を追加する。

vim Gemfile
source 'https://rubygems.org'


gem 'rails', '~> 5.0.0', '>= 5.0.0.1'
gem 'sqlite3'
gem 'puma', '~> 3.0'
gem 'sass-rails', '~> 5.0'
gem 'uglifier', '>= 1.3.0'
gem 'coffee-rails', '~> 4.2'
gem 'jquery-rails'
gem 'turbolinks', '~> 5'
gem 'jbuilder', '~> 2.5'

gem 'unicorn'

group :development, :test do
  gem 'byebug', platform: :mri
end

group :development do
  gem 'web-console'
  gem 'listen', '~> 3.0.5'
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
end

gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

以下のコマンドで上記の内容をインストール。

bundle install

unicornの設定ファイルを以下のように編集する。

vim config/unicorn.rb
listen "/tmp/unicorn.sock"
worker_processes 2

pid "/home/vagrant/sample/shared/pids/unicorn.pid"
stderr_path "/home/vagrant/sample/shared/log/unicorn.log"
stdout_path "/home/vagrant/sample/shared/log/unicorn.log"
cd sample/ 
bundle exec unicorn -c config/unicorn.rb [-D]

UnicornではなくPumaで動かす場合は以下のコマンド

bundle exec rails s (-e production) -b '0.0.0.0'

 http://192.168.33.10:3000 にアクセスすると以下のような画面が現れる。

スクリーンショット 2016-11-17 0.59.18.png

Unicornのプロセスの終了(-Dでデーモンで立ち上げた場合)

kill -QUIT `cat /home/vagrant/test_unicorn/shared/pids/unicorn.pid`

OpsWorksでデプロイ

Berkshelfによるパッケージ化

以下のコマンドを実行して、S3にアップロードする。

$ bundle exec berks package cookbooks.tar.gz
$ aws s3 cp cookbooks.tar.gz s3://sample-bucket/

OpsWorks(OpsWorks Stacks)の操作

  1. マネジメントコンソールからOpsWorksを選択
  2. [Go to OpsWorks Stacks]を選択。

Stackの設定

  1. [Stacks]から[Add stack]を選択。
  2. [Chef 12 stack]タブを選択し、以下のように入力する。

Stack name: sample-stack
Region: Asia Pacific (Tokyo)
VPC: vpc-xxxxxxxx (default)
Default subnet: xxx.xxx.xxx.xxx/xx – ap-northeast-1a
Default operating system: Linux, Amazon Linux 2016.09
Default SSH key: Do not use a default SSH key
Chef version: 12
Use custom Chef cookbooks: Yes
Repository type: S3 Archive
Repository URL: http://.s3-website-ap-northeast-1.amazonaws.com/cookbooks.tar.gz
Access key ID: AKIAXXXXXXXXXXXXXXXXXXXX
Secret access key: xxxxxxxxxxxxxxxxxxxxxxxxx
Stack color: blue (default)

Advanced optionsは以下のような項目がある。
Default root device type: EBS backed
IAM role:
Default IAM instance profile:
API endpoint region: ap-northeast-1a (REGIONAL)
Hostname theme: Layer Dependent
OpsWorks Agent version: 4021(Dec 16th 2016)
Custom JSON: (空欄)
Use OpsWorks security groups: Yes

Layerの設定

  1. [Layers]の設定
  2. [Add layer]を選択し、以下のように入力する。
    Name: sample-layer
    Short name: sample

[Add layer]を選択。

  1. 作成したLayerを選択し、[Recipes]を選択。
  2. Setupに以下の5つを追加する。
  • nginx::default
  • nodejs::default
  • ruby-env::default
  • yum::default
  • yum-epel::default

[Save]を選択。

※これを忘れた場合、Chefによるプロビジョニングが行われずに、後述のインスタンスが起動してしまう。

Instanceの作成

1.[Instances]を選択。
2.[+ Instance]を選択し、以下のように入力する。

Hostname: sample
Size: t2.micro
Subnet: XXX.XXX.XXX.XXX/XX – ap-northeast-1a

[Add Instance]を選択。

3.作成したインスタンスのActionsから[start]を選択。以降プロビジョニングが始まる。もし、起動に失敗した場合は、Hostnameのホスト名を選択した時に遷移するページの最下部にあるLogsから確認出来るLogを確認する。

補足

Berkshelfの実行

bundle exec berks install --path ./cookbooks

これはdeprecatedなので以下を実行

bundle exec berks vendor --path ./cookbooks

注意点

ホスト側でgemコマンドが見つからない

rbenv shell 2.3.1

を実行することでgemを認識するようになる。
http://qiita.com/kasumani/items/042bf799d6794bd2e4f2

Ansible

導入

インストール

brew install ansible

試しにホストに疎通確認。

[sample]
<IP address> ansible_user=ec2-user ansible_ssh_private_key_file=~/.ssh/id_rsa
$ ansible -i hosts all -m ping
<IP address> | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}

Ansible Playbookによるプロビジョニング

ディレクトリ構成

.
├── ansible.cfg
├── group_vars
│   └── sample
├── inventory
│   └── hosts
├── sample.yml
└── roles
    ├── nginx
    │   ├── defaults
    │   │   └── main.yml
    │   ├── handlers
    │   │   └── main.yml
    │   ├── tasks
    │   │   └── main.yml
    │   └── templates
    │       ├── 404.html.j2
    │       ├── 50x.html.j2
    │       ├── index.html.j2
    │       └── nginx.conf.j2
    ├── rails
    │   ├── defaults
    │   │   └── main.yml
    │   └── tasks
    │       └── main.yml
    ├── rbenv
    │   └── tasks
    │       └── main.yml
    └── yum
        └── tasks
            └── main.yml

ファイル内容

sample.yml
- hosts: sample
  become: yes 
  roles:
    - yum 
    - rbenv
    - rails
    - nginx
ansible.cfg
[defaults]
remote_user=ec2-user
private_key_file=~/.ssh/id_rsa
inventory=./inventory/hosts
executable = /bin/bash -l
  • inventory
[defualts]
<IP address>
  • group_vars
rbenv_user: ec2-user
rbenv_ruby_version: 2.3.1
  • roles

    • yum
yum
└── tasks
    └── main.yml
main.yml
---
- name: Install package for bundle install
  yum: name={{ item }} state=latest
  with_items:
    - sqlite-devel
    - gcc-c++
- name: Update all packages
  yum: name=* state=latest

 gcc-c++はtherubyracerのインストールに必要

  • rbenv
rbenv
└── tasks
    └── main.yml
---
- name: Install dependencies for rbenv
  yum: name={{ item }} state=latest
  with_items:
    - git

- name: Install rbenv
  become: yes
  become_user: "{{ rbenv_user }}"
  git: repo=https://github.com/sstephenson/rbenv.git dest=~/.rbenv

- name: Add ~.rbenv/bin to PATH
  become: yes
  become_user: "{{ rbenv_user }}"
  lineinfile: >
    dest="~/.bash_profile"
    line="export PATH=$HOME/.rbenv/bin:$PATH"
- name: Eval rbenv init in ~/.bash_profile
  become: yes
  become_user: "{{ rbenv_user }}"
  lineinfile: >
    dest="~/.bash_profile"
    line='eval "$(rbenv init -)"'

- name: Install dependencies for ruby-build (see. https://github.com/sstephenson/ruby-build/wiki)
  yum: name={{ item }} state=latest
  with_items:
    - gcc
    - openssl-devel
    - libyaml-devel
    - libffi-devel
    - readline-devel
    - zlib-devel
    - gdbm-devel
    - ncurses-devel

- name: Install ruby-build as rbenv plugin
  become: yes
  become_user: "{{ rbenv_user }}"
  git: repo=https://github.com/sstephenson/ruby-build.git dest=~/.rbenv/plugins/ruby-build

- name: Check if version is installed ruby
  become_method: yes
  become_user: "{{ rbenv_user }}"
  shell: "rbenv versions | grep {{ rbenv_ruby_version }}"
  register: rbenv_check_install
  changed_when: False
  ignore_errors: yes

- name: Install ruby
  become_method: yes
  become_user: "{{ rbenv_user }}"
  shell: "rbenv install {{ rbenv_ruby_version }}"
  when: rbenv_check_install|failed

- name: Check if version is the default ruby version
  become_method: yes
  become_user: "{{ rbenv_user }}"
  shell: "rbenv version | grep {{ rbenv_ruby_version }}"
  register: rbenv_check_default
  changed_when: False
  ignore_errors: yes

- name: Set default ruby version
  become_method: yes
  become_user: "{{ rbenv_user }}"
  shell: "rbenv global {{ rbenv_ruby_version }}"
  when: rbenv_check_default|failed

※注意点で、rbenvのコマンドのパスを通すところで、.bash_profileに設定を追記しているが、sourceコマンドでは反映されない。なので、パスを適用させるために、シェルはログインシェルとして実行することで解決できる。具体的には、ansible.cfg中でexecutable = /bin/bash -lを宣言すると良い。

  • rails
rails
├── defaults
│   └── main.yml
└── tasks
    └── main.yml
  • defaults
main.yml
---
railsenv_deploy_dir: "/var/www/sample"
railsenv_deploy_user: ec2-user
railsenv_deploy_group: ec2-user
  • tasks
main.yml
---
- name: Install mysql-devel
  yum: name={{ item }} state=latest
  with_items:
    - mysql-devel
    - gcc-c++

- name: Install dependencies for nokogiri
  yum: name={{ item }} state=latest
  with_items:
    - patch

- name: Install bundler
  become_user: "{{ rbenv_user }}"
  gem: name={{ item }} user_install=False
  with_items:
    - bundler

- name: Create deploy directory
  file: path={{ railsenv_deploy_dir }} state=directory owner={{ railsenv_deploy_user }} group={{ railsenv_deploy_group }} mode=0755
  • nginx
nginx
├── defaults
│   └── main.yml
├── handlers
│   └── main.yml
├── tasks
│   └── main.yml
└── templates
    ├── index.html.j2
    └── nginx.conf.j2
  • tasks
main.yml
---
- name: Install nginx
  yum: name=nginx state=latest

- name: Set nginx service to start on boot
  service: name=nginx enabled=true

- name: Put nginx.conf
  template: src=nginx.conf.j2 dest=/etc/nginx/nginx.conf backup=true mode=0644
  notify: restart nginx

- name: Put share index.html
  template: src=index.html.j2 dest=/usr/share/nginx/html/index.html mode=644
  • handlers
main.yml
---
- name: restart nginx
  service: name=nginx state=restarted
  • defualts
main.yml
---
nginx_woker_processes: "auto"
nginx_server_tokens: "off"
nginx_includes: []
  • templates
index.html.j2
<html>
    <body>
        <h1>Hello, world!</h1>
    </body>
</html>
nginx.conf.j2
user  nginx;
worker_processes  {{ nginx_woker_processes }};

error_log  /var/log/nginx/error.log;

pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    error_log  /var/www/sample/log/nginx.error.log; 
    access_log /var/www/sample/log/nginx.access.log; 

    server_tokens {{ nginx_server_tokens }};

    sendfile        on;

    keepalive_timeout  65;

    include /etc/nginx/conf.d/*.conf;
{% for include in nginx_includes %}
    include {{ include }};
{% endfor %}

    index   index.html index.htm;

    client_max_body_size 2G;
    upstream app_server {
        server unix:/var/www/sample/tmp/sockets/.unicorn.sock fail_timeout=0;
    }

   server {
        listen 80;
        server_name <IP address>;

        keepalive_timeout 5;
        root /var/www/sample/public;
        try_files $uri/index.html $uri.html $uri @app;
        location @app {
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header Host $http_host;
            proxy_redirect off;
            proxy_pass http://app_server;
        }
    }
}

実行

ansible-playbook sample.yml

ansible.cfg中でremote_user, private_key_file, inventoryの設定を追加しない場合は以下のようにコマンドを打つ必要がある。

ansible-playbook -i "<IP address>," --user=ec2-user --private-key=~/.ssh/id_rsa sample.yml

注意点

  • rbenvの設定ファイルを.bash_profileの内容がsourceでは反映されないのでshellモジュールを使う時は必ずログインシェルとして実行する
[defaults]
executable = /bin/bash -l
- name: Sample shell execution
  become_method: yes
  become_user: ec2-user
  shell: "~~~"

http://www.bunkei-programmer.net/entry/2015/05/16/162020

  • therubyracerの依存パッケージでgcc-c++をyumで入れる。

Docker [Docker Compose]

FROM ruby:2.3.1

ENV APP_ROOT /app

RUN apt-get update -qq && 
    apt-get install -y build-essential 
                       libmysqld-dev 
                       libmysqlclient-dev 
                       mysql-client 
                       --no-install-recommends && 
    rm -Rf /var/lib/opt/lists/*

RUN mkdir $APP_ROOT
WORKDIR $APP_ROOT

ADD . /app
RUN bundle install
docker-compose.yml
version: '2' 
services:
  app:
    build: .
    command: bundle exec rails s -b '0.0.0.0'
    volumes:
      - .:/app
    ports:
      - "3000:3000"
    links:
      - db
  db: 
    image: mysql:5.6.30
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: root

ただし、Gemfile中でtherubyracerを使用しない場合は、Dockerfile中でapt-get installnodejsもインストールする。

ビルド & 実行

以下のコマンドはターミナルのウィンドウを新たに開くたびに実行する。

docker-machine start default
eval $(docker-machine env defualt)

ビルド

docker-compose build

実行

docker-compose up

IPアドレスを以下のコマンドで調べる。

docker-machine ip

192.168.99.100:3000のようにブラウザにアクセスする。

ECSでデプロイ

ECSでデプロイする場合は別投稿の下記のURLを参考にして下さい。
http://qiita.com/hayashier/items/b34f82c42053f85e5b09

マニュアルで環境構築

Railsの環境準備

sudo yum install -y git make gcc-c++ patch openssl-devel libyaml-devel libffi-devel libicu-devel libxml2 libxslt libxml2-devel libxslt-devel zlib-devel readline-devel mysql mysql-server mysql-devel ImageMagick ImageMagick-devel epel-release
sudo yum install -y nodejs npm --enablerepo=epel
git clone https://github.com/sstephenson/rbenv.git ~/.rbenv 
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile 
echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
source .bash_profile
git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
rbenv install 2.3.1
rbenv global 2.3.1
ruby -v

Webサーバのインストール

sudo yum install gem
gem install bundle
bundle -v
rake secret
sudo yum install nginx
sudo service nginx start

以下、Capistrano等のデプロイツールを用いてデプロイする場合は必ずしも必要ではありません。

gitの準備

vim ~/.netrc
cd ~/.ssh
ssh-keygen -t rsa -C "<メールアドレス>"
ssh-add ~/.ssh/id_rsa
cat id_rsa_admin.pub 
ssh -T git@github.com

GitHubに公開鍵の登録をする。

  • .netrc
machine github.com
login <GitHubユーザ名>
password xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Webサーバの環境・設定

cd /var/www/
sudo git clone https://github.com/<GitHubユーザ名>/<レポジトリ名>.git
sudo chown -R ec2-user:ec2-user <レポジトリ名>/
cd <レポジトリ名>/
bundle install --path ./vendor/bundle/ 
sudo vim /etc/nginx/conf.d/admin.conf 
sudo service nginx restart
bundle exec rake secret
cat config/secrets.yml 
vim .env
source .env
echo $SECRET_KEY_BASE
sudo service nginx restart
rails s -e production
  • .env
export DATABASE_PASSWORD_PRODUCT=xxxxxxxxxxxxxxx

config/database.yml中のデータベースへのパスワードを以下のように環境変数として定義しておき、.env中でインポートする。

<%= ENV[‘DATABASE_PASSWORD_PRODUCT’] %>

  • sample.conf
error_log  /var/www/sample/log/nginx.error.log; 
access_log /var/www/sample/log/nginx.access.log;

client_max_body_size 2G;
upstream app_server {
  server unix:/var/www/sample/tmp/sockets/.unicorn.sock fail_timeout=0; 
}
server {
  listen 80;
  server_name <IPアドレス>;
  # nginx so increasing this is generally safe...
  keepalive_timeout 5;
  # path for static files
  root /var/www/sample/public; 
  # page cache loading
  try_files $uri/index.html $uri.html $uri @app;
  location @app {
    # HTTP headers
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://app_server;
  }     
  # Rails error pages
  error_page 500 502 503 504 /500.html;
  location = /500.html {
    root /var/www/sample/public; 
  }
}

参考

Chef

Ansible

Docker

https://docs.docker.com/compose/rails/

続きを読む

AWS EC2にMongoDBインストールとレプリケーション設定

MongoDBインストールとレプリケーション設定について、簡易的な抜粋メモとして書いています。
詳細に関しては、記事の最後の参考サイトを確認するようにしてください。

◆ 今日やること

  • MongoDBをEC2にインストール
  • レプリケーションの設定と確認
    • 今回せっていするレプリケーションの形式は、以下の図の通り、「Primary with Secondary Members」です。

mongorepl.png
引用元:Replication — MongoDB Manual 3.4

◆ バージョン

  • MongoDB 3.2
  • Linux 4.4.30-32.54.amzn1.x86_64 #1 SMP Thu Nov 10 15:52:05 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux

◆ 実装編

> EC2へのインストール

sudo yum update -y
sudo vim /etc/yum.repos.d/mongodb-org-3.2.repo

[mongodb-org-3.2]
name=MongoDB Repository
baseurl=https://repo.mongodb.org/yum/amazon/2013.03/mongodb-org/3.2/x86_64/
gpgcheck=1
enabled=1
gpgkey=https://www.mongodb.org/static/pgp/server-3.2.asc
sudo yum install -y mongodb-org
sudo service mongod start

sudo chkconfig mongod on

> 設定周り

  • WoredTigerストレージエンジンの主な設定

    • cacheSizeGB: システムメモリの6、7割
    • blockCompressor:デフォルトはsnappy(中間です)
/etc/mongod.conf
# Where and how to store data.
storage:
  dbPath: /data
  journal:
    enabled: true
  wiredTiger:
    engineConfig:
       cacheSizeGB: 1
       journalCompressor: snappy
    collectionConfig:
       blockCompressor: snappy

> EBSボリュームをAttach

  • EBSボリュームにmongoのデータを溜めるようにする。

Amazon EBS ボリュームを使用できるようにする – Amazon Elastic Compute Cloudに従って、EBSボリュームをアタッチする

  • パーミッションを変更するのを忘れないように。
sudo chown -R mongod:mongod /data
  • /etc/mongod.conf
/etc/mongod.conf
# Where and how to store data.
storage:
  dbPath: /data

> レプリケーションの設定

MongoDBではマスターのことをプライマリ,スレーブのことをセカンダリと呼びます。
MongoDBのレプリケーションの最小構成は,3つのノードが必要です。

  • ネットワークインターフェイスの設定で、レプリケーションを組むサーバのIPを記述しておくこと

レプリケーション設定前には、お互いに通信できることを確認しないといけません
Troubleshoot Replica Sets — MongoDB Manual 3.4

mongodb が listen するIPアドレスはデフォルトでは 127.0.0.1 に設定されており、ローカルからのみアクセスを許可するようになっています
mongod.conf の bind_ip に設定されたIPアドレスで listen するのでこれを変更することで外部からの接続を許可します
ここに 0.0.0.0 を設定すると全てのIPアドレスからの接続を許可します

/etc/mongod.conf
# network interfaces
net:
  port: 27017
  bindIp: [127.0.0.1,10.1.52.111,10.1.51.227,10.1.51.68]


  • レプリケーション名を決める
  • Oplogのサイジングと設定サイズを決める
/etc/mongod.conf
replication:
   oplogSizeMB: 500
   replSetName: testRepl

第4回 MongoDBのレプリケーションを構築してみよう:MongoDBでゆるふわDB体験|gihyo.jp … 技術評論社

OplogはCapped Collectionなので,作成時以外にサイズ変更することができません。 デフォルトのOplogのサイズは「1GBまたは,ディスクの空き領域の5%」
Staleを避けるために,Oplogのサイジングは非常に重要となります。
Oplogのサイズは,mongodの初回起動時にoplogSizeオプションで変更可能です。
Oplogの適切なサイズを見積もる方法のひとつに,本番を想定した書き込みテストを実施し,作成されたOplogのサイズを取得する方法があります。
1時間程度,本番想定と同程度の書き込みテストを行った後,以下のコマンドでレプリケーションの最新情報を取得します。

> db.getReplicationInfo()

1時間で作成されたOplogのサイズがわかれば,Oplogのサイジングの目安となります。
少なくとも8時間のセカンダリのダウンタイムに耐えられるようにしておくことが推奨されています。

  • レプリケーションの設定

どれかのサーバに入って、以下のコマンドを実行

config = {
  _id : "testRepl",
  members : [
    { _id : 0, host : "10.1.51.227:27017" },
    { _id : 1, host : "10.1.51.68:27017" },
    { _id : 2, host : "10.1.52.111:27017"} ] }

rs.initiate(config)
  • スレーブの読み取り専用動作設定

そのままだと、スレーブが読み取り専用としてアクセスできないので以下の設定を行っておく。

スレーブノードで、以下のコマンドを実行

 db.getMongo().setSlaveOk()
  • レプリケーションステータスの確認
testRepl:PRIMARY> rs.status();
{
    "set" : "connobaRepl",
    "date" : ISODate("2017-01-12T07:03:05.556Z"),
    "myState" : 1,
    "term" : NumberLong(6),
    "heartbeatIntervalMillis" : NumberLong(2000),
    "members" : [
        {
            "_id" : 0,
            "name" : "10.1.51.227:27017",
            "health" : 1,
            "state" : 1,
            "stateStr" : "PRIMARY",
            "uptime" : 100310,
            "optime" : {
                "ts" : Timestamp(1484182286, 1),
                "t" : NumberLong(6)
            },
            "optimeDate" : ISODate("2017-01-12T00:51:26Z"),
            "electionTime" : Timestamp(1484104344, 1),
            "electionDate" : ISODate("2017-01-11T03:12:24Z"),
            "configVersion" : 3,
            "self" : true
        },
        {
            "_id" : 1,
            "name" : "10.1.51.68:27017",
            "health" : 1,
            "state" : 2,
            "stateStr" : "SECONDARY",
            "uptime" : 100245,
            "optime" : {
                "ts" : Timestamp(1484182286, 1),
                "t" : NumberLong(6)
            },
            "optimeDate" : ISODate("2017-01-12T00:51:26Z"),
            "lastHeartbeat" : ISODate("2017-01-12T07:03:04.017Z"),
            "lastHeartbeatRecv" : ISODate("2017-01-12T07:03:04.020Z"),
            "pingMs" : NumberLong(0),
            "syncingTo" : "10.1.51.227:27017",
            "configVersion" : 3
        },
        {
            "_id" : 2,
            "name" : "10.1.52.47:27017",
            "health" : 1,
            "state" : 2,
            "stateStr" : "SECONDARY",
            "uptime" : 99751,
            "optime" : {
                "ts" : Timestamp(1484182286, 1),
                "t" : NumberLong(6)
            },
            "optimeDate" : ISODate("2017-01-12T00:51:26Z"),
            "lastHeartbeat" : ISODate("2017-01-12T07:03:05.025Z"),
            "lastHeartbeatRecv" : ISODate("2017-01-12T07:03:05.022Z"),
            "pingMs" : NumberLong(2),
            "syncingTo" : "10.1.51.227:27017",
            "configVersion" : 3
        }
    ],
    "ok" : 1
}

> mongoログインの時の警告メッセージを消す方法

[ec2-user@ip-10-1-52-47 ~]$ mongo
MongoDB shell version: 3.2.11
connecting to: test
Server has startup warnings:
2017-01-11T03:10:18.308+0000 I CONTROL  [initandlisten]
2017-01-11T03:10:18.308+0000 I CONTROL  [initandlisten] ** WARNING: /sys/kernel/mm/transparent_hugepage/defrag is 'always'.
2017-01-11T03:10:18.308+0000 I CONTROL  [initandlisten] **        We suggest setting it to 'never'
2017-01-11T03:10:18.308+0000 I CONTROL  [initandlisten]
testRepl:SECONDARY>

◆ 参考サイト

続きを読む

ELBのPre-WarmingのついでにEBSのPre-Warmingを調べた

はじめに

最近下記の記事を書いたのですが、

ELB暖気運転(Pre-Warming)申請についていくつかサポートに問い合わせた

直後にEBSにもPre-Warmingという概念があるのを思い出したので、
勢いそのままについでに書くことにします。

WS000001.JPG

http://www.slideshare.net/AmazonWebServicesJapan/aws-black-belt-online-seminar-amazon-elastic-block-store-ebs/45

EBSのPre-Warmingとは

ざっくりいうと下記のとおりです。

  • スナップショットからリストアした直後のEBSボリュームはIOがめっちゃ遅い
  • どのくらい遅いかというと5%~50%分遅い
  • 事前にコマンドによって読み込み処理を行えば上記を回避できる
  • その処理をPre-Warmingという

付け加えると下記のとおり。

  • 以前はスナップショットからのリストアだけでなく、新規ボリューム作成時にも遅かった
  • 今は遅くないのでPre-Warmingはむしろやらないほうがいい
  • Pre-Warmingというのは昔の呼称で、今は初期化(initialization)という

きちんとドキュメントで読みたい

下記をご参照ください。
http://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/ebs-initialize.html

新しい EBS ボリュームは、利用可能になるとすぐに最大のパフォーマンスを発揮し、初期化 (以前は事前ウォーミングと呼ばれました) を必要としません。ただし、スナップショットから復元されたボリュームのストレージブロックは、アクセスするためには事前に初期化する必要があります (Amazon S3 からプルダウンしてボリュームに書き込みます)。この準備処理には時間がかかるため、初めて各ブロックにアクセスした場合に、I/O 操作のレイテンシーの著しい増加が発生する可能性があります。ほとんどのアプリケーションにとって、ボリュームの存続期間全体でこのコストを割り当てることは、許容範囲内です。一度データにアクセスされると、パフォーマンスは元に戻ります。

ボリュームを使用する前に、そのボリュームのすべてのブロックを対象に、読み取りを実行することで、本番環境におけるパフォーマンスの低下を回避することができます。このプロセスを「初期化」と呼びます。スナップショットから作成された新しいボリュームの場合は、ボリュームを使用する前に、データのあるすべてのブロックを読み取る必要があります。

Pre-Warmingに使うコマンド

Linuxの場合

dd

$ sudo dd if=/dev/xvdf of=/dev/null bs=1M

fio

$ sudo fio --filename=/dev/xvdf --rw=randread --bs=128k --iodepth=32 --ioengine=libaio --direct=1 --name=volume-initialize

Windowsの場合

dd

C:>dd if=\.PHYSICALDRIVEn of=/dev/null bs=1M --progress --size
http://docs.aws.amazon.com/ja_jp/AWSEC2/latest/WindowsGuide/ebs-initialize.html

あと少し気になった箇所

スナップショットから復元された io1 ボリュームを初期化している間は、ボリュームのパフォーマンスが想定レベルの 50% を下回る場合があります。このため、ボリュームの [I/O Performance] ステータスチェックでは warning 状態が表示されます。これは想定の動作です。初期化中の io1 ボリュームの warning 状態は無視してかまいません。

io1とはつまり「プロビジョンド IOPS SSD」のことですが、
なぜio1だけ言及されているのか、他のボリュームタイプでは無視してはいけないのか?
というのが気になります。(詳細未確認)

おわりに

スナップショットとしてS3に保管されているデータ。
それらはリストア後のEBSボリュームの、
各ストレージブロックに初回アクセスが合った際に「プルダウン」されてくる。

「プルダウン」には少し時間がかかるので、
ボリューム全体まとめて一気に「プルダウン」しておきましょう、
というのがPre-Warming(初期化)のイメージでしょうか。

「初期化」と聞くと「え、全部データ消えちゃうの」と一瞬思いましたが、
そんなことはありませんでした。

dd ユーティリティまたは fio ユーティリティを使用します。ボリュームのすべてのデータが保持されます。

あと、あまり積極的にはやらなくてもいいよ、
というニュアンスを感じました。

実運用時は事前ウォーミングが不可能なケースもあるため、運用要件から判断して実行可能であれば取り込む程度でOK

とはいえ本番環境に適用する前には、
こういった注意事項があると頭の片隅に置いておいたほうが良さそうですね。

以上です。

続きを読む

CLIでEC2をローンチする際にエフェメラルディスクをつけない方法

Webコンソール以外でEC2をローンチする際にエフェメラルディスク(インスタンスストア)が付いてしまう仕様があるようです。
少なくともCLIとTerraformではこの現象が起きます。

この現象の解消方法は現在のところBlockDeviceMappingのエフェメラルディスクに片っ端からNoDeviceを指定する他ありません。

以下にCLIとTerraformでの例を記載します。

CLI

BlockDeviceMapping用のjsonファイルを以下のように記述します。

block_device_mappings.json
[
    {
        "DeviceName": "/dev/sda1",
        "Ebs": {
            "VolumeSize": 50,
            "DeleteOnTermination": true,
            "VolumeType": "gp2"
        }
    },
    {"DeviceName": "xvdca", "NoDevice": ""},
    {"DeviceName": "xvdcb", "NoDevice": ""},
    {"DeviceName": "xvdcc", "NoDevice": ""},
    {"DeviceName": "xvdcd", "NoDevice": ""},
    {"DeviceName": "xvdce", "NoDevice": ""},
    {"DeviceName": "xvdcf", "NoDevice": ""},
    {"DeviceName": "xvdcg", "NoDevice": ""},
    {"DeviceName": "xvdch", "NoDevice": ""},
    {"DeviceName": "xvdci", "NoDevice": ""},
    {"DeviceName": "xvdcj", "NoDevice": ""},
    {"DeviceName": "xvdck", "NoDevice": ""},
    {"DeviceName": "xvdcl", "NoDevice": ""},
    {"DeviceName": "xvdcm", "NoDevice": ""},
    {"DeviceName": "xvdcn", "NoDevice": ""},
    {"DeviceName": "xvdco", "NoDevice": ""},
    {"DeviceName": "xvdcp", "NoDevice": ""},
    {"DeviceName": "xvdcq", "NoDevice": ""},
    {"DeviceName": "xvdcr", "NoDevice": ""},
    {"DeviceName": "xvdcs", "NoDevice": ""},
    {"DeviceName": "xvdct", "NoDevice": ""},
    {"DeviceName": "xvdcu", "NoDevice": ""},
    {"DeviceName": "xvdcv", "NoDevice": ""},
    {"DeviceName": "xvdcw", "NoDevice": ""},
    {"DeviceName": "xvdcx", "NoDevice": ""},
    {"DeviceName": "xvdcy", "NoDevice": ""},
    {"DeviceName": "xvdcz", "NoDevice": ""}
]

そして、以下のようにCLIを実行します。

ec2_run-instances.sh
aws ec2 run-instances 
    --image-id ami-12345678 
    --key-name example 
    --instance-type c3.large 
    --subnet-id subnet-12345678 
    --iam-instance-profile Arn=arn:aws:iam::123456789123:role/example-role 
    --security-group-ids sg-12345678 
    --block-device-mappings file://block_device_mappings.json 
    --desable-api-termination

Terraform

TerraformもVer0.8からNoDeviceをサポートしています。
tfファイルには以下のように記述します。

aws_instance.tf
resource "aws_instance" "example-instance" {
    ami = "ami-12345678"
    instance_type = "c3.large"
    key_name = "example"
    subnet_id = "${aws_subnet.example.id}"
    security_groups = ["${aws_security_group.example.id}"]
    disable_api_termination = true
    tags {
        Name = "example-instance"
    }
    iam_instance_profile = "example-role"
    root_block_device {
        volume_type = "gp2"
        volume_size = 50
    }
    ephemeral_block_device { device_name = "xvdca" no_device = "true" }
    ephemeral_block_device { device_name = "xvdcb" no_device = "true" }
    ephemeral_block_device { device_name = "xvdcc" no_device = "true" }
    ephemeral_block_device { device_name = "xvdcd" no_device = "true" }
    ephemeral_block_device { device_name = "xvdce" no_device = "true" }
    ephemeral_block_device { device_name = "xvdcf" no_device = "true" }
    ephemeral_block_device { device_name = "xvdcg" no_device = "true" }
    ephemeral_block_device { device_name = "xvdch" no_device = "true" }
    ephemeral_block_device { device_name = "xvdci" no_device = "true" }
    ephemeral_block_device { device_name = "xvdcj" no_device = "true" }
    ephemeral_block_device { device_name = "xvdck" no_device = "true" }
    ephemeral_block_device { device_name = "xvdcl" no_device = "true" }
    ephemeral_block_device { device_name = "xvdcm" no_device = "true" }
    ephemeral_block_device { device_name = "xvdcn" no_device = "true" }
    ephemeral_block_device { device_name = "xvdco" no_device = "true" }
    ephemeral_block_device { device_name = "xvdcp" no_device = "true" }
    ephemeral_block_device { device_name = "xvdcq" no_device = "true" }
    ephemeral_block_device { device_name = "xvdcr" no_device = "true" }
    ephemeral_block_device { device_name = "xvdcs" no_device = "true" }
    ephemeral_block_device { device_name = "xvdct" no_device = "true" }
    ephemeral_block_device { device_name = "xvdcu" no_device = "true" }
    ephemeral_block_device { device_name = "xvdcv" no_device = "true" }
    ephemeral_block_device { device_name = "xvdcw" no_device = "true" }
    ephemeral_block_device { device_name = "xvdcx" no_device = "true" }
    ephemeral_block_device { device_name = "xvdcy" no_device = "true" }
    ephemeral_block_device { device_name = "xvdcz" no_device = "true" }
}

まとめ

書式としてはあまりスマートとは言えませんが、現状はこうするしかないようです。
SDKでは検証していませんが、おそらく同じかと思います。
もっとよい書き方があったら教えてください。

続きを読む

Vagrant による EC2 のプロビジョニング + Jupyter(IPython Notebook)on Docker

概要

(前半)Vagrantを利用してのEC2のプロビジョニング
(後半)Dockerを利用してのJupyter(IPython Notebook)の起動

初めてQiitaに書かせていただきます。
正月休みにゼロから学ぶディープラーニングと、あわよくばTensorFlowを勉強しよう、せっかくなら環境構築も勉強しよう……と試してみたので、備忘録兼ねての紹介です。
今回前後半は独立しているので片方のみの参照も可能かと思います。

:)

  • EC2なのでスペックの上げ下げが簡単
  • VagrantでEC2のStart/Stopが簡単に -> 節約に
    作業後の落とし忘れミスを最小限にしてくれます。
  • Dockerで可搬性向上
    ここは勉強兼ね。データ分析環境をあちこちに立てるのに便利、という印象。

:(

  • EC2にお金がかかる
  • Dockerを噛ませる意味合いが薄い
    書籍の勉強のように最初から環境決め打ちなら、Ansibleなどでシンプルかつ全体を通しで、環境構築できる気がします。

構築手順

前提として

  • Vagrantのインストール
  • EC2の起動権を持つIAMユーザーのクレデンシャルの設定
  • EC2を起動するVPCやサブネットの設定

が必要です。

こちらが参考になるかと思います。

(前半)Vagrantを利用してのEC2のプロビジョニング

HashiCorpのGitHubページ: Vagrant AWS Provider を参考に設定を行います。

まずAWSを利用するためのプラグインをインストールします。

$ vagrant plugin install vagrant-aws

マシンイメージに相当するBOXですが、EC2用にダミーボックスが公開されているのでこちらを利用します。

(公式だとbox名がdummyですが、少し曖昧なのでec2と変えています。)

$ vagrant box add ec2 https://github.com/mitchellh/vagrant-aws/raw/master/dummy.box

次に作業用のディレクトリへ移動し、Vagrantfileの作成を行います。

$ mkdir jupyter_ec2
$ cd jupyter_ec2
$ vim Vagrantfile

Vagrantfile を編集し、各種パラメータを設定します。今回はあらかじめ作成したVPCでの起動を指定しています。

他にもかなりの数のパラメータがあるようで、指定したい場合は適宜READMEや、AWS APIドキュメントを参照するのが良いかと思います。

Vagrantfile
Vagrant.configure("2") do |config|
  config.vm.box = "ec2" # dummy box name

  config.vm.provider :aws do |aws, override|
    # AWSクレデンシャル
    aws.aws_dir = ENV['HOME'] + "/.aws/"
    aws.aws_profile = "YOUR_PROFILE"
    aws.keypair_name = "YOUR_AWS_ACCESS_KEY"

    # SSHクレデンシャル
    override.ssh.username = "ec2-user"
    override.ssh.private_key_path = "SSH_KEY_PATH"

    # VPC設定
    aws.region = 'ap-northeast-1'
    aws.availability_zone = 'ap-northeast-1a'
    aws.subnet_id = 'subnet-XXXXXXXX'
    aws.security_groups = 'sg-XXXXXXXX'

    # EC2設定
    aws.ami = "ami-9f0c67f8" # Amazon Linux 2016.09.01
    aws.instance_type = 'm4.large'
    aws.elastic_ip = true
    aws.associate_public_ip = true
    aws.tags = { 'Name' => 'NAME_TAGS_VALUE' }

    # EBS設定
    aws.block_device_mapping = [
      {
        'DeviceName' => "/dev/xvda",
        'Ebs.VolumeSize' => 50,
        'Ebs.DeleteOnTermination' => true,
        'Ebs.VolumeType' => 'standard',
      }
    ]
  end
end

編集終了後、EC2の起動を行います。

$ vagrant up --provider=aws

うまくいかないときですが、

$ export VAGRANT_LOG=DEBUG

としたのちvagrant upを再実行すると詳細なエラーメッセージを表示してくれるようになります。

エラーメッセージを元に戻したいときはexport VAGRANT_LOG=WARN

セキュリティグループのSSHもきちんと開けているか注意しましょう。上限緩和していない場合EIP数上限も引っかかりがちです。

起動し終わったら

$ vagrant ssh

でSSHログインできるか確認します。

$ vagrant halt

でインスタンスstopが行えますので、作業しないときは適宜停止して節約に努めます。

(後半)Dockerを利用してのJupyter(IPython Notebook)の起動

Installing Dockerを参考に、まずEC2にDockerをインストールします。

$ sudo yum update -y
$ sudo yum install -y docker

docker daemonを起動し、インスタンス再起動後も自動で起動するようにしておきます。

$ sudo service docker start
$ sudo chkconfig docker on

sudo 権限なしでもdockerコマンドを追加できるようにec2-userをdockerグループに追加しておきます。

$ sudo usermod -a -G docker ec2-user

一旦exitし、再ログインすることでsudo なしでdockerが起動するようになります。

次にDockerfileの記述を行います。

$ vim Dockerfile

今回は簡単に進めるために、anacondaのイメージをベースにTensorFlowをインストールする形にします。
CMD でdocker run 時にjupyterが起動するように指定をします。

FROM continuumio/anaconda:latest

RUN mkdir /opt/notebooks
RUN pip install tensorflow
# Another installations comes here.

CMD ["/opt/conda/bin/jupyter", "notebook", "--notebook-dir=/opt/notebooks", "--ip='*'", "--port=8888", "--no-browser"]

編集終了後、Dockerfileからコンテナイメージを作成します。

$ docker build -t myjupyter .

最後にコンテナを起動します。notebookの永続化やホスト側からも覗けるようにvolumeを指定しマウントします。また、–restartオプションによりインスタンスstop/start後もdockerデーモンが自動でコンテナを立ち上げるようにします。

$ mkdir notebooks
$ docker run -p 8888:8888 -v /home/ec2-user/notebooks:/opt/notebooks --restart=always myjupyter

セキュリティグループでTCPのポート8888番の通信を許可したのち、ブラウザで[Elastic IP]:8888へアクセスすることで、Jupyterの起動を確認します。


これで勉強するときだけvagrant upしブラウザ上で学習、一旦やめるときはvagrant haltで課金停止、ローカルで試したいときは同じDockerfileからビルド、ipythonnbをVagrant経由でとってくれば簡単移植、な環境になった・・・!はずなので、書籍勉強しつつ適宜修正していこうと思います。

続きを読む

AWS ES、kibana、grafana

可視化ツールを比較してみた。(AWS ES,kibana,grafana)

シチュエーション

DBに入っているデータを可視化しようと思ったとする。ただしBI系のツールは使わない(設定が直感的でなくて、面倒なので)。
選択肢としては、ElasticSearchとkibanaがまず頭に浮かんでくるが、AWSのElasticSearch Serviceを使うのか、EC2にElasticSearchとkibanaを立てるのか、はたまたGrafanaを使うのか悩んだので、ざっくり比較してみました。

各種メリット表

今ぱっと思いついたメリットは以下のとおりです。

  • AWS ES

    • 他のAWSサービスとの連携が容易
    • サーバの運用、管理をしなくて良い
    • バックアップが自動
    • グラフやダッシュボードの設定がシンプルで直感的
    • リソースモニターが付いている
    • IAMによる権限管理が可能
  • EC2 grafana

    • 画像データが取得できるので定期レポーティング実装が容易
    • golangなので、リソースが削減できそう
    • グラフの作り込みができる
    • PrometheusやCloudwatchなどの情報を取り込める
    • 比較的細かな権限管理ができる
    • 特定の時間だけ起動することによって、コスト削減可能
  • EC2 ELK

    • kibanaのバージョンアップがしやすい(v5リリース予定)
    • kibanaUIプラグインが利用可能
    • グラフやダッシュボードの設定がシンプルで直感的
    • 特定の時間だけ起動することによって、コスト削減可能

比較

  • 比較項目

    1. 認証機能がつけられるか
    2. グラフのカスタマイズのがしやすいか
    3. レポーティング機能がついているか
    4. 累積グラフが使えるか
    5. バックアップが容易にとれるか
    6. 構築や設定が容易か
    7. お金が安いか
  • 前提条件

    1. サービスは停止しない
    2. データ容量は10G
    3. メモリはkibanaが2G必要で、Elasticsearchは4G必要とする。
    4. データ転送量は無視
# サービス 認証 グラフ作成 累積グラフ レポーティング バックアップ 構築 コスト
1 AWS ES IAM × × 自動で月に1度 $196.02/月(※1)
2 EC2 ELK SG Plugin × × × $116.4/月(※2)
3 EC2 Grafana × × $116.4/月(※3)
4 ES and EC2 kibana SG Plugin × 自動で月に1度 $112.26/月(※4)

※1 $196.02/月 = ES($0.270/1時間 * 24 * 30) + EBS($0.162/G * 10)
※2 $116.4/月 = EC2($0.16/1時間 * 24 * 30) + EBS($0.12/G * 10)
※3 $116.4/月 = EC2($0.16/1時間 * 24 * 30) + EBS($0.12/G * 10)
※4 $112.26/月 = ES($0.112/1時間 * 24 * 30) + EC2($0.04/1時間 * 24 * 30) + ES EBS($0.162/G * 10) + EC2 EBS($0.12/G * 10)

※参考
ES料金表
EC2料金表
EBS料金表

結論

結局、Elasticsearchを AWS ES でたてて、kibanaを EC2 で立てるのがいいと思った。
早く新バージョンにしたいとか、プラグインを使いたいという思いがなければ、 AWS ES だけで十分なんですが。。

作業ログ

Oracleのセットアップ

DBはEnterpriseっぽく、Oracleであるという想定で行う。

  • Dockerを使ってOracleを起動する
console
$ docker run -d --shm-size=2g -p 1521:1521 -p 8080:8080 alexeiled/docker-oracle-xe-11g

※参考
https://hub.docker.com/r/alexeiled/docker-oracle-xe-11g/

  • tableを作成して適当なデータを入れる。(今回は説明の便宜上ここで、 MATSUNO_TABLE というテーブルを作成したこととする)

logstashのセットアップ

* logstash用のディレクトリ作成&移動

console
$ mkdir logstash && cd logstash
  • Oracleのドライバー(ojdbc6.jar)をダウンロードページからダウンロードしてlogstash用ディレクトリに配置する。

1. Amazon Elasticsearch Service

環境構築

※参考
https://aws.amazon.com/jp/elasticsearch-service/

  • Step 1: Define domain

    • スクリーンショット 2016-08-31 14.22.04.png
  • Step 2: Configure cluster
    • スクリーンショット 2016-08-31 14.30.22.png
  • Step 3: Set up access policy
select_template
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "AWS": "*"
      },
      "Action": "es:*",
      "Condition": {
        "IpAddress": {
          "aws:SourceIp": [
            "YOUR_IP"
          ]
        }
      },
      "Resource": "arn:aws:es:ap-northeast-1:YOUR_AWS_ACCOUNT:domain/matsuno-test/*"
    }
  ]
}

YOUR_IP のところに接続許可したいIPを記入する。

  • Step 4: Review

    • スクリーンショット 2016-08-31 14.35.19.png

起動に10分くらいかかる。

  • 起動確認

    • スクリーンショット 2016-08-31 15.35.12.png

Status: Green となっていればOK

データ投入(DBが入っているサーバにて)

  • logstash.conf作成(logstashのディレクトリにて)
logstash.conf
input {
    jdbc {
        jdbc_connection_string => "jdbc:oracle:thin:@oracle:1521/xe"
        jdbc_user => "system"
        jdbc_password => "oracle"
        jdbc_driver_library =>"/config-dir/ojdbc6.jar"
        jdbc_driver_class => "Java::oracle.jdbc.driver.OracleDriver"
        statement => "
SELECT
    *
FROM
    MATSUNO_TABLE
"
    }
}
output {
    elasticsearch {
        ssl => true
        hosts => ["search-matsuno-test-7brub6nxbn352qchitarrjncdu.ap-northeast-1.es.amazonaws.com:443"]
        index => "matsuno"
    }
}

※参考
https://www.elastic.co/guide/en/logstash/current/plugins-outputs-elasticsearch.html

AWSのESは、https(443)がエンドポイントとなっているので、その点に注意する。

console
$ docker run -it --rm --link 821c58da87eb:oracle -v "$PWD":/config-dir logstash -f /config-dir/logstash.conf

Settings: Default pipeline workers: 2
Pipeline main started
Pipeline main has been shutdown
stopping pipeline {:id=>"main"}
  • データの確認

    • スクリーンショット 2016-08-31 15.39.33.png

データが入っていることが確認できた。

プラグイン

Amazon ES supports the ICU Analysis Plugin and the Kuromoji plugin. You can configure these normally through the Elasticsearch Mapping API. Amazon ES does not currently support commercial plugins like Shield or Marvel. The AWS equivalents for these plugins are AWS Identity and Access Management (IAM) and CloudWatch.

AWSにプラグインは入れられない模様。
その代わり、IAMなどでエンドポイントの制御はできる。

※参考
https://aws.amazon.com/jp/blogs/aws/new-amazon-elasticsearch-service/

2. ElasticsearchとKibanav4 on EC2

環境構築

  • EC2を作成(AmazonLinux)
  • SecurityGroupで9200,5601を許可
  • ElasticSearchインストール
console
$ sudo rpm --import https://packages.elastic.co/GPG-KEY-elasticsearch
/etc/yum.repos.d/elasticsearch.repo
[elasticsearch-2.x]
name=Elasticsearch repository for 2.x packages
baseurl=https://packages.elastic.co/elasticsearch/2.x/centos
gpgcheck=1
gpgkey=https://packages.elastic.co/GPG-KEY-elasticsearch
enabled=1
console
$ sudo yum update
$ sudo yum install elasticsearch

※参考
https://www.elastic.co/guide/en/elasticsearch/reference/current/setup-repositories.html

  • elasticsearchの外部アクセス設定
$/etc/elasticsearch/elasticsearch.yml
# ---------------------------------- Network -----------------------------------
#
# Set the bind address to a specific IP (IPv4 or IPv6):
#
# network.host: 192.168.0.1
 network.host: 0.0.0.0
#
# Set a custom port for HTTP:
#
# http.port: 9200
#
# For more information, see the documentation at:
# <http://www.elastic.co/guide/en/elasticsearch/reference/current/modules-network.html>
#
# --------------------------------- Discovery ----------------------------------
#
  • elasticsearch起動
console
$ sudo chkconfig --add elasticsearch
$ sudo /etc/init.d/elasticsearch start
  • kibanaをダウンロード
console
$ wget https://download.elastic.co/kibana/kibana/kibana-4.5.4-linux-x64.tar.gz
$ tar xfvz kibana-4.5.4-linux-x64.tar.gz
  • kibana設定(コメントアウトを外す)
/home/ec2-user/kibana-4.5.4-linux-x64/config/kibana.yml
# Kibana is served by a back end server. This controls which port to use.
 server.port: 5601

# The host to bind the server to.
 server.host: "0.0.0.0"

# If you are running kibana behind a proxy, and want to mount it at a path,
# specify that path here. The basePath can't end in a slash.
# server.basePath: ""

# The maximum payload size in bytes on incoming server requests.
 server.maxPayloadBytes: 1048576

# The Elasticsearch instance to use for all your queries.
 elasticsearch.url: "http://localhost:9200"

# preserve_elasticsearch_host true will send the hostname specified in `elasticsearch`. If you set it to false,
# then the host you use to connect to *this* Kibana instance will be sent.
# elasticsearch.preserveHost: true

..............
..............
..............


# Time in milliseconds for Elasticsearch to wait for responses from shards.
# Set to 0 to disable.
# elasticsearch.shardTimeout: 0

# Time in milliseconds to wait for Elasticsearch at Kibana startup before retrying
# elasticsearch.startupTimeout: 5000

# Set the path to where you would like the process id file to be created.
 pid.file: /var/run/kibana.pid

# If you would like to send the log output to a file you can set the path below.
# logging.dest: stdout

/etc/init.d/kibana(INSTALLED_DIRを環境に合わせる)
#!/bin/sh
#
#
# chkconfig:   2345 80 20
# description: Starts and stops a single kibana instance on this system

### BEGIN INIT INFO
# Provides:          kibana
# Required-Start:    $local_fs $remote_fs $network
# Should-Start:      $time
# Required-Stop:     $local_fs $remote_fs $network
# Default-Start:     3 5
# Default-Stop:      0 1 2 6
# Short-Description: Kibana 4
# Description:       Service controller for Kibana 4
### END INIT INFO"

INSTALLED_DIR=/home/ec2-user/kibana-4.5.4-linux-x64
EXEC_SCRIPT="$INSTALLED_DIR/bin/kibana"
LOG_DIR=/var/log/kibana
PID_DIR=/var/run
PID_FILE="$PID_DIR"/kibana.pid
LOG_FILE="$LOG_DIR"/kibana.log

test -d $LOG_DIR || mkdir $LOG_DIR

# Source function library.
. /etc/init.d/functions

RETVAL=0

case "$1" in
    start)
        if [ ! -f "$PID_FILE" ]; then
          echo -n "Starting Kibana"
          nohup $EXEC_SCRIPT 0<&- &> $LOG_FILE &
          echo $! > $PID_FILE
          success
        else
          echo -n "Kibana is already running"
          RETVAL=1
          failure
        fi
        echo
        ;;
    stop)
        if [ -f "$PID_FILE" ]; then
          echo -n "Stopping Kibana"
          test -f $PID_FILE && cat $PID_FILE | xargs kill -s SIGKILL && rm -f $PID_FILE
          success
        else
          echo -n "Kibana is not running"
          RETVAL=1
          failure
        fi
        echo
        ;;
    restart)
        $0 stop
        $0 start
        ;;
    reload)
        $0 restart
        ;;
    status)
        status kibana
        RETVAL=$?
        ;;
*)
echo "Usage: $0 {start|stop|status|restart|reload}"
exit 1
;;
esac
exit $RETVAL
  • kibana起動
console
$ chmod +x /etc/init.d/kibana
$ chkconfig --add kibana
$ /etc/init.d/kibana start
  • ブラウザでアクセス

YOUR_INSTANCE_IP:5601

データ投入(DBが入っているサーバにて)

  • logstash.conf作成(logstashのディレクトリにて)
logstash.conf
input {
    jdbc {
        jdbc_connection_string => "jdbc:oracle:thin:@oracle:1521/xe"
        jdbc_user => "system"
        jdbc_password => "oracle"
        jdbc_driver_library =>"/config-dir/ojdbc6.jar"
        jdbc_driver_class => "Java::oracle.jdbc.driver.OracleDriver"
        statement => "
SELECT
    *
FROM
    MATSUNO_TABLE
"
    }
}
output {
    elasticsearch {
        hosts => ["YOUR_ELASTICSEARCH_IP:9200"]
        index => "matsuno"
    }
}
console
$ docker run -it --rm --link 821c58da87eb:oracle -v "$PWD":/config-dir logstash -f /config-dir/logstash.conf

Settings: Default pipeline workers: 2
Pipeline main started
Pipeline main has been shutdown
stopping pipeline {:id=>"main"}

プラグイン

  • git install
console
$ sudo yum install -y git
  • install plugin
console
$ cd /home/ec2-user/kibana-4.5.4-linux-x64/installedPlugins
$ git clone https://github.com/sbeyn/kibana-plugin-line-sg.git
  • 確認
    http://54.199.223.185:5601/status にアクセスしてプラグンが ready になっていること

3. ElasticsearchとGrafana on EC2

環境構築

  • EC2を作成(AmazonLinux)
  • SecurityGroupで9200,3000を許可
  • ElasticSearchインストール

※DBはElasticSearchでなくてもOK。
今回は先ほどのlogstashの設定を使いまわすので、ElasticSearchとする。

console
$ sudo rpm --import https://packages.elastic.co/GPG-KEY-elasticsearch
/etc/yum.repos.d/elasticsearch.repo
[elasticsearch-2.x]
name=Elasticsearch repository for 2.x packages
baseurl=https://packages.elastic.co/elasticsearch/2.x/centos
gpgcheck=1
gpgkey=https://packages.elastic.co/GPG-KEY-elasticsearch
enabled=1
console
$ sudo yum update
$ sudo yum install elasticsearch
  • grafanaインストール
$ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-3.1.1-1470047149.x86_64.rpm

※参考
http://docs.grafana.org/installation/rpm/

  • 起動
$ /etc/init.d/grafana-server start
  • 確認

http://YOUR_IP:3000 admin/admin でアクセス

続きを読む

AWSの失敗談 – しらなくてミスした話

お疲れ様です。
株式会社アカツキでサーバエンジニアとして働いている申です。

はじめに

AWSを使用する上で失敗(ミス)した話と、それの原因及び対処方法などを共有し、みなさんは私と同じ過ちを起こさないようにすることが目的です。

Single-AZに比べてMulti-AZのほうが重い

概要

Single-AZとMulti-AZの両方性能テストする機会があり、Single-AZに比べてMulti-AZの方が性能が悪くていろいろ調べました。(特にコミット遅い)

原因

別AZ間でマスター・スレーブを配置するMulti-AZ構成では、AZ間で通信のレイテンシーが発生するためです。
Multi-AZのRDSにおいて、AZ間のデータレプリケーションは、非同期ではなく、 同期レプリケーション です。
よって、マスターDBに書き込み及びコミットが発生した場合、スレーブDBに書き込むための通信が発生し、それが完了するまで待つことになります。

対処

Single-AZを使用する

高可用性及びフェイルオーバーのため、AWSはMulti-AZをおすすめしていますが、
以下の理由でSingle-AZを選択することもありえるかと思います。

・性能優先
・コスト削減
・しばらく中断されても、すぐ対応できればいいサービス

EBSの最適化を提供する(PIOPS最適化)RDSインスタンスタイプを使用する

一般のEBSネットワークは他のトラフィックも使用する共存ネットワークです。そのため、高性能のIOPSを持っていても、他のネットワークトラフィックによりIOPSが影響され、IOPSが低下する場合があります。
EBSの最適化を提供しないRDSインスタンスタイプを使用する場合、他のトラフィック状況によって(例えば、マスターとスレーブ間で通信で)、IOPSも低下されることがあります。

ebs.png

EBSの最適化を提供するRDSインスタンスタイプで、専用のネットワークが確保ができ、プロビジョンドIOPSを最大まで引き上げることができます。

EBSの最適化を提供するRDSインスタンスタイプは、r3ですと、
r3.xlarge ~ r3.4xlargeまでです。r3.8xlargeは提供しません!
もし、他のネットワークトラフィックに影響されやすい環境だと、場合によってはr3.8xlargeの方がもっとIOPSが低いのでは?という疑問になりますが、これは後日ベンチマークしてみます。

新しくEC2をELBに登録した途端にOutOfServiceになってしまった話

概要

1個のAZで構成されているELBに新しくもう1個のAZを追加しました。
そのあと、新しいAZに1個のEC2を追加しましたが、追加したEC2が「InService」になったとたんに負荷が高まり、すぐ「OutOfService」になってしまったことがあります。

原因

デフォルトのクラシックELBは全てのEC2に均等に分散されるのではなく、 まず AZごとに均等に分散される ためです。

以下の画像をみてください。
elb.png

例えば、 ap-northeast-1aap-northeast-1c に跨っているELBがあるとします。そのELBにそれぞれ、10台、1台のEC2インスタンスが登録されています。
ELBに200のリクエストが来た場合、ELBには ap-northeast-1aap-northeast-1c に100リクエストずつ均等に分散されます。
そうしてしまった場合、 ap-northeast-1a の10台のEC2インスタンスは1台あたり10リクエストを受け付けますが、ap-northeast-1c の1台EC2インスタンスは100リクエストを一人で受け付けてしまいます。

対処

安定的に追加するためには、

・既存AZと同じ数のEC2を新しいAZに追加するか。
・「クロスゾーン負荷分散を有効化」する ※1

※1 「クロスゾーン負荷分散」を有効化することでAZごとではなくELBに登録されている全EC2ごとに分散されるようになります。
しかし、耐障害性を高めるために、できれば各AZに等しいEC2を分散配置するのがベストです。

AWSサービスはできるだけ新しい世代を使用する

新しい世代は以下のメリットがあります。

・コストが安い、性能がいい
・いいオプションがデフォルトで付いている ※1

※1 例えば、いいオプションは、EC2のC3世代にはなくて、C4世代にはデフォルトで付いているEBS最適化オプションなどになるでしょう。

RDSにおいて容量変更やスケールダウンするとたまに変更完了するまで時間がかかる話

概要

普段なら、10分〜20分で終わるRDS変更がたまに3〜4時間かかったりする場合がありました。
メンテ作業を10分〜20分で見積もっていざとなってメンテ入って変更を行うとなかなか終わらなくて汗かいたことがあります。

原因

いまだに原因不明です。
AWS側に、問い合わせしたい。

Redisのバックアップを取得したが、データが入っているはずのバックアップが0MBだった話

概要

Redisのバックアップ完了後、それを元に復元をしましたが、入っているはずのデータが空っぽでした。AWSマネジメントコンソルから、バックアップ容量をみたら、0MBでした。

翌日、再度バックアップされたデータ容量をみたら、1000MBになっていました。

原因

いまだに原因不明です。AWSの不具合?
AWS側に、問い合わせしたい。

教訓

バックアップをとったら、ちゃんと容量を見てから復元作業を行いましょう。

いろいろ、AWSって女心のように予測できないことがおおいため、丁寧に研究しながら、やさしく付き合っていきましょう。

以上、AWS失敗談でした。

続きを読む