private docker registry のはまりどころ(S3連携、SSL)

AWSでprivate docker registryをコンテナで立てた際のハマったポイントをメモ。

ケース

・AWS EC2(Amazon Linux)上にコンテナで実現
・コンテナイメージはdocker registry 2.1
・ボリュームはS3に設定
・SSLはオレオレ証明書で対応
・外部インターネットとの接続は原則ない(クローズド環境)

ハマった点

① docker push/pullで以下のようなエラーとなる

x509: certificate signed by unknown authority

opensslでオレオレ証明書(crtファイル)を作成し、pull/pushするクライアント側に格納するが、/etc/docker/certs.d/配下に格納してしまうと、dockerがOSのroot証明書を参照しなくなり、S3など他のSSL接続ができなくなる。
今回のようにボリュームをS3に指定する場合は特に注意が必要。
crtファイルは[registryのドメイン名].crtとして、/etc/pki/ca-trust/source/anchors配下に格納する。

その後、設定を反映し、dockerを再起動する。

sudo update-ca-trust
sudo service restart docker

これでエラーは解決した。

② docker push/pullでio.timeoutとなる

SSLの設定が解決したところで、クライアントからpush/pullをすると、以下のようなエラーとなる。

tcp XX.XX.XX.XX:443: i/o timeout.

IPアドレスを逆引きしてみると、s3-ap-northeast-1.amazonaws.comと判明。

どうやらクライアントは、push/pushの際、docker registryサーバだけでなく、S3に直接イメージpull/pushのための接続を行っている模様。
つまり、docker registryでボリュームをS3に指定した場合、当然registryを立てたEC2から、ボリュームとして指定したS3バケットへのアクセス権(ポリシーやROLEなど)は必須だが、同時にクライアント側も対象S3バケットへアクセスできる必要がある。
オンプレミスとのハイブリッド環境など、クローズドな環境の場合は特に注意が必要。

スクリーンショット 2017-01-18 20.54.28.png

参考

https://ishiis.net/2017/01/12/docker-distribution-install/

http://dev.classmethod.jp/cloud/docker-registry-recipes/

https://docs.docker.com/registry/deploying/#configure-tls-on-a-registry-server

http://qiita.com/suzukihi724/items/c8135fcfbf74fcbc80d0

続きを読む

シンプルな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/

続きを読む

無料で使えるSSL – 「Let’s Encrypt」をh2oに設定してhttp2.0に対応してみた

2014年11月にElectronic Frontier Foundation、Mozilla、Cisco Systems、ミシガン大学などが共同で立ち上げた「Let’s Encrypt」。最近では、GoogleがSSLを推奨しており、サイトをSSL対応しておかないと検索順位に影響すると言われています。そんな中、Let’s Encryptは無料で使えるということもあり、導入しているサイトも増えてきています。個人でSSLを導入してhttp2.0対応するにはちょうど良いですね。

ここでは、「Let’s Encrypt」をインストール&h2oに設定してhttp2.0に対応させる方法を説明します。

インストールに使用した環境

Amazon Linux AMI release 2016.09
h2o 2.1.0

h2oがインストールされており、80番ポート、443番ポートともに通信可能になっていることが前提となります。ファイアウォールで80、443が遮断されているとインストールできませんのでご注意下さい。

インストール手順

ソースコード取得

Let’s Encryptのソースコードをgithubのリポジトリから取得してきます。
以下の例では、ホームディレクトリにcloneしていますが、場所はどこでもかまいません。

cd ~
git clone https://github.com/letsencrypt/letsencrypt

インストール

letsencrypt-autoというコマンドが用意されているのでこれを使用します。
必要なパッケージ類を一通りインストールしてくれます。
rootユーザーで実行する必要がありますので、sudoをつけましょう。

cd ./letsencrypt
sudo ./letsencrypt-auto --debug --help

実行中に
「Installing Python packages…」でしばらく止まります。少し待ちましょう。。
以下のようなメッセージが表示されたらインストール完了です。

Installation succeeded.

  letsencrypt-auto [SUBCOMMAND] [options] [-d DOMAIN] [-d DOMAIN] ...

Certbot can obtain and install HTTPS/TLS/SSL certificates.  By default,
it will attempt to use a webserver both for obtaining and installing the
cert. The most common SUBCOMMANDS and flags are:

obtain, install, and renew certificates:
    (default) run   Obtain & install a cert in your current webserver
    certonly        Obtain or renew a cert, but do not install it
    renew           Renew all previously obtained certs that are near expiry
   -d DOMAINS       Comma-separated list of domains to obtain a cert for

  --apache          Use the Apache plugin for authentication & installation
  --standalone      Run a standalone webserver for authentication
  --nginx           Use the Nginx plugin for authentication & installation
  --webroot         Place files in a server's webroot folder for authentication
  --manual          Obtain certs interactively, or using shell script hooks

   -n               Run non-interactively
  --test-cert       Obtain a test cert from a staging server
  --dry-run         Test "renew" or "certonly" without saving any certs to disk

manage certificates:
    certificates    Display information about certs you have from Certbot
    revoke          Revoke a certificate (supply --cert-path)
    delete          Delete a certificate

manage your account with Let's Encrypt:
    register        Create a Let's Encrypt ACME account
  --agree-tos       Agree to the ACME server's Subscriber Agreement
   -m EMAIL         Email address for important account notifications

More detailed help:

  -h, --help [TOPIC]    print this message, or detailed help on a topic;
                        the available TOPICS are:

   all, automation, commands, paths, security, testing, or any of the
   subcommands or plugins (certonly, renew, install, register, nginx,
   apache, standalone, webroot, etc.)

証明書発行

次に、letsencrypt-autoコマンドを使って証明書の発行を行います。
パラメータに、h2o.confで設定されているホームディレクトリ、ドメイン名(FQDN)、管理者のメールアドレスを指定します。

sudo ./letsencrypt-auto certonly _
  --webroot -w [h2oホームディレクトリのパス] _
  -d [ドメイン名(FQDN)] _
  -m [管理者のメールアドレス] _
  --agree-tos

コマンドを実行すると、以下のディレクトリに証明書とキーファイルが作成されます。

/etc/letsencrypt/live/[ドメイン名]/fullchain.pem
/etc/letsencrypt/live/[ドメイン名]/privkey.pem

h2o.confの設定

作成された証明書をh2o.confに設定します。
併せてhttpでアクセスがあった場合に自動的にhttpsに301リダイレクトする設定にします。

pid-file: /etc/h2o/pid-file
user: root
access-log: /var/log/h2o/access-log
error-log: /var/log/h2o/error-log
file.index: [ 'index.php', 'index.html' ]

listen: 80
listen:
  port: 443
  ssl:
        # 作成した証明書ファイルを設定
    certificate-file: /etc/letsencrypt/live/[ドメイン名]/fullchain.pem
    key-file:  /etc/letsencrypt/live/[ドメイン名]/privkey.pem

hosts:
    # httpでアクセスがあった場合、httpsに301リダイレクトする
  "[ドメイン名]:80":
    paths:
      "/":
        redirect:
          url: https://[ドメイン名]/
          status: 301

    # httpsでアクセスがあった場合はホームディレクトリへ
  "[ドメイン名]:443":
    paths:
      "/":
        file.dir: [ホームディレクトリ]

h2o.confのテスト

設定ファイルに問題が無いかチェックします。

sudo service h2o configtest

証明書を設定してconfigtestを行うと、証明書ファイルのチェックもしてくれます。
結果の中に以下が表示されていれば、正しく証明書が発行されています。

... 略

/etc/letsencrypt/live/[ドメイン名]/fullchain.pem: good

... 略

/etc/letsencrypt/live/[ドメイン名]/fullchain.pem: good

... 略

configuration OK
                                                           [  OK  ]

h2o再起動

configtestに成功したらh2oを再起動しましょう。

sudo service h2o restart

動作確認

ブラウザからhttpsでアクセスして動作を確認します。
以下のように「保護された通信」と緑色で表示されればOKです。

スクリーンショット 2017-01-15 23.13.59.png

また、httpでアクセスすると、自動的にhttpsにリダイレクトされるかもチェックしておきましょう。

併せてアクセスログを見て、http2.0通信になっているか確認します。

XXX.XXX.XXX.XXX - - [15/Jan/2017:23:19:48 +0900] "GET / HTTP/2" 200 43306 "" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36"

http2.0で通信されていれば、アクセスログに「HTTP/2」と表示されます。

まとめ

Let’s Encryptを使うことで、コストをかけること無くSSL通信に対応することが出来ます。また、h2o等のサーバに設定することによってhttp2.0にも対応することが出来るのでとても便利です。是非一度お試し下さい。

最後に注意点

Let’s Encryptは他の証明書とは異なり、3ヶ月に1回更新が必要となります。
無料で使える代わりに管理者の所在確認をまめに行う必要があると言ったところでしょうか。。

しかし3ヶ月に1回更新する手間を考えても、無料で使えるというのは大きなメリットだと思います。

続きを読む

ローカル環境でAmazon Linuxをテストする

はじめに

可搬性(ポータビリティ)に立ちふさがる密林、Amazon Linux。担当プロジェクトでもご多分に漏れず、Amazon LinuxをOSとしたシステムを組んでいます。
chefを用いて構成管理(商用環境は、OpsWorks)、Test Kitchen + ServerspecでインフラテストしているがOSはCentOS6.7(packerで生成したVagrant box)…OSが違うのに、何が構成管理か、何が可搬性か。

AWS公式のDockerイメージもリリースされたことですし、Dockerを武器にローコストで可搬性を手に入れることが出来ると思い立ったが吉日。
1営業日くらいで可搬性のあるインフラテスト環境を手に入れることが出来ました :dragon_face:

変更のサマリ

OpsWorks用cookbook ローカル環境用cookbook Test KitchenでプロビジョニングするホストのOS Test Kitchenのドライバ 初回のテスト完了までの所要時間
ビフォー example example_local CentOS6.7 Vagrant 20分程度
アフター example Amazon Linux Docker 5分程度

主たる変更は、kitchen-dockerを導入し、Test KitchenのドライバとしてDockerを採用したことです。
変更により、以下のメリットを享受出来るようになりました。

  • cookbookの統一
  • プロビジョニング対象ホストのOSをOpsWorksと合わせることが出来る
  • 初回テスト時の所要時間の短縮

以下で、詳細を記載していきます。

ビフォー

before.png

  1. exampleアプリケーションを稼働させるためのcookbook(example)を開発する

    • OpsWorksを利用しているので、リポジトリ直下にcookbookを配置する必要がある
  2. example cookbookをTest Kitchenで稼働させるために、example_localを開発する
    • example::defaultをincludeし、attributeを上書きするcookbook
  3. Serverspecでテストコードを開発する
    • Test Kitchenにも対応するので、example_localに配置
  4. Test Kitchenでexample cookbookの動作確認とテストを実施
    • プロビジョニング:bundle exec kitchen converge default-example
    • テスト:bundle exec kitchen verify default-example
  5. テストが通ったら、packerに対応させる
    • packer/example.jsonを開発し、cookbookと一緒にAtlasにアップロード
    • AtlasのpackerでVagrant boxを生成する(リモートビルド)
  6. 開発担当者がAtlasからVagrant boxをダウンロードし、exampleアプリケーションを開発

…だるい

設計し、実装を終えた当初は、堅牢で素晴らしいワークフローだと思ったものです。
このケースの課題は

  • cookbook開発のオーバーヘッドが無視できない

    • cookbookの見通しが悪い。_localって何??
    • Atlasは、日本の日勤帯の時間は、ネットワーク帯域を絞っているのでboxのダウンロードが激重
  • attributeでパッケージ名を上書きしている

    • OSが異なることが諸悪の根源
  • 学習コストが結構かかる

    • chefとServerspecは必要経費だとしても、Test Kitchen、Vagrant、Packer、Atlas….

ゴールデンイメージなVagrant boxを中心に添えたワークフローは、堅牢ですがミドルウェアのアップデートのスピードには着いていくことが難しいというのが個人的な印象です。

アフター

after.png

  1. exampleアプリケーションを稼働させるためのcookbook(example)を開発する

    • OpsWorksを利用しているので、リポジトリ直下にcookbookを配置する必要がある
  2. Serverspecでテストコードを開発する
    • Test Kitchenにも対応するので、example_localに配置
  3. Test Kitchenでexample cookbookの動作確認とテストを実施
    • Test Kitchenのドライバとして、Dockerを指定
    • kitchen-dockerでAmazon LinuxのイメージをPULLし、example cookbookをプロビジョニング
    • プロビジョニング:bundle exec kitchen converge default-example
    • テスト:bundle exec kitchen verify default-example
  4. テストが通ったら、リモートリポジトリにPUSH
  5. OpsWorksのスタックをsetupし、AWS上のインフラをプロビジョニング

cookbook開発だけでなく、アプリケーションのローカル開発環境の見直しも併せて行った(Dockerの導入)ので、かなりシンプルはワークフローとなりました。
メリットをまとめると

  • 同一のOSに対してプロビジョニングし、テスト出来る

    • AWS公式Dockerイメージに対してプロビジョニングした結果をテスト出来る
  • コードベースがシンプルになる

    • example_local cookbookを破棄することが出来る
  • cookbook開発に注力出来る

    • 以前に比べて、学習費用対効果が良い
    • 必要経費(chef、Serverspec)のみで開発出来る

Tips

導入に際してのポイントをコードを交えて説明してみます。

kitchen-dockerの導入

gemのインストール

kitchen-dockerというgemが必要になりますので、Gemfileに追記しましょう。
Kitchen-vagrantは不要となりますので、削除しました。

source "https://rubygems.org"

gem "chef"
gem "berkshelf"
gem "test-kitchen"
gem "kitchen-docker"

Gemfileの編集が終わったらbundlerでインストールします。

bundle install --path ./vendor/bundle

.kitchen.ymlの編集

kitchen-dockerのREADMEを参考に、Test Kitchenの設定ファイルを編集していきます。

provisioner:
  name: chef_solo
  environments_path: ../environments
  solo_rb:
    environment: local

Test KitchenのドライバとしてDockerを指定します。sudoするとdockerコマンドへのパスが見つからない場合が多いかと思うので、use_sudo: falseと指定しておきます。
OpsWorks環境とは異なるattributeがあれば、environmentsとしてJSON形式のファイルにまとめておくことをおすすめします。

platforms:
  - name: amazonlinux-201609
    driver_config:
      platform: rhel
      image: amazonlinux:2016.09

name要素でプロビジョニング対象のホストの名前を指定します。
Amazon Linuxは、Test Kitchenの分類ではRHEL系のディストリビューションとなるので、platform: rhelと指定します。
image要素で、Dockerレジストリから取得したいイメージを指定します。OpsWorks環境のOSバージョンと合わせるため、image: amazonlinux:2016.09と指定します。

suites:
  - name: default
    run_list:
      - recipe[example::amazonlinux-local]
      - recipe[example::default]
    attributes:

テストしたいrecipeをリストします。
amazonlinux-localというレシピについては、後述します。

上記をまとめると、以下の様な.Kitchen.ymlとなります。

---
driver:
  name: docker
  use_sudo: false

provisioner:
  name: chef_solo
  environments_path: ../environments
  solo_rb:
    environment: local

platforms:
  - name: amazonlinux-201609
    driver_config:
      platform: rhel
      image: amazonlinux:2016.09

suites:
  - name: default
    run_list:
      - recipe[example::amazonlinux-local]
      - recipe[example::default]
    attributes:

amazonlinuxの導入

amazonlinuxは、AWS公式のDockerイメージですが、AWS環境と全く同一のものではありません。あくまでも、可搬性の観点で現状取りうる手段の中で最もAWS環境で稼働するAmazon Linuxに近い仮想環境です。
そのため、AWS環境との差異(OSの初期設定含む)については、自分たちで対応する必要があります。
私達のチームでは、差異を埋めるrecipeを開発して対応しました。

# sysconfigファイル
default[:sysconfigs] = ['i18n', 'network']

システム設定ファイル(/etc/sysconfig)の差異をattributeにまとめ、templateとして配置しておきます。

$ tree /path/to/example/example/templates/default/
/path/to/example/example/templates/default/
├── i18n.erb
├── config.erb
└── network.erb
# ローカル開発環境(Amazon LinuxのDockerイメージ)でのレシピ開発のための各種ファイルの配置
node[:sysconfigs].each do |sysconfig|
  template "/etc/sysconfig/#{sysconfig}" do
    owner "root"
    group "root"
    mode 0644
    not_if { File.exist?("/etc/sysconfig/#{sysconfig}") }
  end
end

配置したtemplateを上記の様なrecipeでプロビジョニングしていきます。

# SELinuxは明示的にDisable
include_recipe 'selinux'
template "/etc/selinux/config" do
  owner "root"
  group "root"
  mode 0644
  not_if { File.exist?("/etc/selinux/config") }
end

これは、テストのためのプロビジョニングです。
ServerspecでSELinuxの設定をテストしているのですが、/etc/selinux/configファイルが必須の内容となっておりましたので、ローカル環境でのrecipe開発のためだけにプロビジョニングしています。

# peclを利用するのでコンパイラをインストール
package "gcc"

gccがインストールされておりませんので、テスト対象のrecipeをプロビジョニングする前にインストールしておきます。

上記をまとめると、以下の様なrecipe(amazonlinux-local.rb)となります。
このrecipeをテスト対象のrecipe(example::default)の前にプロビジョニングします(前述の.kitchen.yml参照)。

# Cookbook Name:: example
# Recipe:: amazonlinux-local

# ローカル開発環境(Amazon LinuxのDockerイメージ)でのレシピ開発のための各種ファイルの配置
node[:sysconfigs].each do |sysconfig|
  template "/etc/sysconfig/#{sysconfig}" do
    owner "root"
    group "root"
    mode 0644
    not_if { File.exist?("/etc/sysconfig/#{sysconfig}") }
  end
end

# SELinuxは明示的にDisable
include_recipe 'selinux'
template "/etc/selinux/config" do
  owner "root"
  group "root"
  mode 0644
  not_if { File.exist?("/etc/selinux/config") }
end

# peclを利用するのでコンパイラをインストール
package "gcc"

Test Kitchen

kitchen-dockerの導入を紹介してきましたが、Test Kitchenのコマンド集をまとめておきます。

# cookbookのディレクトリに移動
$ cd /path/to/example/example

# インスタンスの確認
$ bundle exec kitchen list
Instance                    Driver  Provisioner  Verifier  Transport  Last Action  Last Error
default-amazonlinux-201609  Docker  ChefSolo     Busser    Ssh        Verified     <None>

# インスタンスを作成
$ bundle exec kitchen create default-amazonlinux-201609

# インスタンスのセットアップ
$ bundle exec kitchen converge default-amazonlinux-201609

# インスタンスへログイン(tty)
$ bundle exec kitchen login default-amazonlinux-201609
Last login: Thu Jan  5 20:23:42 2017 from 172.17.0.1
[kitchen@685fdd7c1349 ~]$
[kitchen@685fdd7c1349 ~]$ pwd
/home/kitchen

# テスト(serverspec)の実行
$ bundle exec kitchen verify default-amazonlinux-201609

# インスタンスの破棄
% bundle exec kitchen destroy default-amazonlinux-201609

おわりに

OpsWorksを導入して以来、Amazon Linuxをどうテストするかという課題を抱えていましたが、AWS公式のDockerイメージが提供されたことにより、ローコストで可搬性のあるテストを実施出来るようになりました。
今後の課題は、プロビジョニングをCIすることを目標に必要なライブラリの導入や開発、変更を加えて行きたいと思います。
増え続けるcookbookや優秀なchefの育成に困っている開発チームの課題解決の一助となれば幸いです。

参考

続きを読む

Amazon EC2 VPCのIPv6対応 インスタンスへのIPv6アドレス付与は m-flagつきRA + stateful DHCPv6

サマリ

Amazon VPC内のEC2インスタンスへのIPv6アドレス付与は RA with m-flag + stateful DHCPv6 で行われている。

ubuntu 16.04 のインスタンスを立てて、DHCPv6の設定をする

Developers.IO: Amazon EC2とVPCがIPv6に対応しました #reinvent
http://dev.classmethod.jp/cloud/aws/vpc-ipv6/

上記の記事の通りに、オハイオリージョンのVPCにIPv6の設定をして、ubuntu 16.04 LTSのEC2インスタンスを作ってみたが初期設定では、eth0にIPv6アドレスがつかない。

eth0 I/Fに prefix information option を含むRAが降ってくるが、m-flagがついるので、stateful DHCPv6クライアントを起動(IA_NAをリクエスト)してIPv6アドレスを取得する。

手動で起動するなら

# sudo dhclient -6 eth0

OS起動時の設定にするなら下記の設定を入れておく

# echo "iface eth0 inet6 dhcp" > /etc/network/interfaces.d/60-default-with-ipv6.cfg

Amazon Linux, Windows インスタンスの初期設定

Amazon提供のドキュメントには下記の記述があり、標準でDHCPv6クライアントを起動する設定が入っている。Windowsについては、m-flagつきRAを受け取った時、stateful DHCPv6クライアントを起動するのはOSデフォルトの挙動である。

Amazon Linux 2016.09.0 以降、または Windows Server 2008 R2 以降のバージョンを使用してインスタンスを起動した場合、インスタンスは IPv6 に設定され、追加の手順は必要ありません。

Amazon提供のドキュメント

日本語版も不自然な翻訳は見当たらず質が高いですが、英語版のほうが改定が進んでいて若干の違いがあります(2017.1.12現在)

[1] 日本語版 IPv6 への移行 http://docs.aws.amazon.com/ja_jp/AmazonVPC/latest/UserGuide/vpc-migrate-ipv6.html
[2] 英語版 Migrating to IPv6 http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/vpc-migrate-ipv6.html

続きを読む

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>

◆ 参考サイト

続きを読む

CentOS-7の再起動時、Amazon SSM エージェントが立ち上がらなかった時の対応

Amazon SSM エージェントがOS再起動時こける

AWSのEC2インスタンスでCentOS7を作成し、Amazon SSMエージェントを使うためインストールしたが、OSを再起動したらAmazon SSMエージェントがこけていたので、その対応した時の備忘録。

今回の環境

  • Amazon EC2 (Instance Type: t2.micro)
  • CentOS-7 (AMI ID ami-eec1c380)

再起動したときのエラー

/var/log/message
Jan  6 19:09:23 hogehoge2 systemd: Started amazon-ssm-agent.
Jan  6 19:09:23 hogehoge2 systemd: Starting amazon-ssm-agent...
Jan  6 19:09:23 hogehoge2 amazon-ssm-agent: 2017/01/06 19:09:23 Failed to load instance info from vault. RegistrationKey does not exist.
Jan  6 19:09:23 hogehoge2 amazon-ssm-agent: Error occured fetching the seelog config file path:  open /etc/amazon/ssm/seelog.xml: no such file or directory
Jan  6 19:09:23 hogehoge2 amazon-ssm-agent: 2017-01-06 19:09:23 INFO Starting Agent: amazon-ssm-agent - v2.0.562.0
Jan  6 19:09:23 hogehoge2 amazon-ssm-agent: 2017-01-06 19:09:23 INFO OS: linux, Arch: amd64
Jan  6 19:09:25 hogehoge2 amazon-ssm-agent: 2017-01-06 19:09:25 ERROR error fetching the region, Failed to fetch region. Data from vault is empty. Get http://169.254.169.254/latest/dynamic/instance-identity/document: dial tcp 169.254.169.254:80: connect: network is unreachable
Jan  6 19:09:25 hogehoge2 amazon-ssm-agent: 2017-01-06 19:09:25 ERROR error occured when starting core manager: Failed to fetch region. Data from vault is empty. Get http://169.254.169.254/latest/dynamic/instance-identity/document: dial tcp 169.254.169.254:80: connect: network is unreachable
Jan  6 19:09:25 hogehoge2 amazon-ssm-agent: 2017-01-06 19:09:25 ERROR error occured when starting amazon-ssm-agent: Failed to fetch region. Data from vault is empty. Get http://169.254.169.254/latest/dynamic/instance-identity/document: dial tcp 169.254.169.254:80: connect: network is unreachable

エラーが表示されているところで Get http://169.254.169.254/latest/dynamic/instance-identity/document: dial tcp 169.254.169.254:80: connect: network is unreachable とあり、ネットワークがまだ立ち上がろうとしている前にAmazon SSM エージェントが立ち上がろうとし、コケているのではと推測しました。

psコマンドで確認するも、Amazon SSM エージェントのプロセスは無かったです。

対応

社内で「systemdの順序を定義してやれば、解消する気がしますね。」という話とともに、Systemd メモ書き – Qiita の記事をもらいましたのでやってみました。

やってみた

/etc/systemd/system/amazon-ssm-agent.service
[Unit]
Description=amazon-ssm-agent
After=network.target ←追記部分
  〜〜snip〜〜

結果、うまく動いた感じっぽ・・・

上記にある /etc/systemd/system/amazon-ssm-agent.service のファイルに、 After=network.target を追記してOSを再起動した。
そのときのログ

/var/log/message
Jan 10 12:38:25 hogehoge2 systemd: Started amazon-ssm-agent.
Jan 10 12:38:25 hogehoge2 systemd: Starting amazon-ssm-agent...
Jan 10 12:38:25 hogehoge2 amazon-ssm-agent: 2017/01/10 12:38:25 Failed to load instance info from vault. RegistrationKey does not exist.
Jan 10 12:38:25 hogehoge2 amazon-ssm-agent: Error occured fetching the seelog config file path:  open /etc/amazon/ssm/seelog.xml: no such file or directory
Jan 10 12:38:25 hogehoge2 amazon-ssm-agent: 2017-01-10 12:38:25 INFO Starting Agent: amazon-ssm-agent - v2.0.562.0
Jan 10 12:38:25 hogehoge2 amazon-ssm-agent: 2017-01-10 12:38:25 INFO OS: linux, Arch: amd64
Jan 10 12:38:25 hogehoge2 amazon-ssm-agent: 2017-01-10 12:38:25 INFO Initializing bookkeeping folders
Jan 10 12:38:25 hogehoge2 amazon-ssm-agent: datastore file /var/lib/amazon/ssm/i-XXXXXXXXXXXXXXXXX/longrunningplugins/datastore/store doesn't exist - no long running plugins to execute
Jan 10 12:38:25 hogehoge2 amazon-ssm-agent: 2017-01-10 12:38:25 INFO Initializing bookkeeping folders for long running plugins
Jan 10 12:38:25 hogehoge2 amazon-ssm-agent: 2017-01-10 12:38:25 INFO Initializing healthcheck folders for long running plugins
Jan 10 12:38:25 hogehoge2 amazon-ssm-agent: 2017-01-10 12:38:25 INFO Initializing locations for inventory plugin
Jan 10 12:38:25 hogehoge2 amazon-ssm-agent: 2017-01-10 12:38:25 INFO Initializing default location for custom inventory
Jan 10 12:38:25 hogehoge2 amazon-ssm-agent: 2017-01-10 12:38:25 INFO [instanceID=i-XXXXXXXXXXXXXXXXX] Create new startup processor

うまく動いたようです〜!
以下の様にプロセスも確認できました。

$ ps -ax | grep ssm
  756 ?        Ssl    0:00 /usr/bin/amazon-ssm-agent

最後に・・・

Amazon Linux (AMI ID ami-9f0c67f8)で同じ事になっているか同じように確認したのですが、こちらは /etc/systemd/system/amazon-ssm-agent.service を設定する事なく、デフォルトのままでOSを再起動をしてもSSMエージェントが立ち上がらないということは無いようでした。

理由はわかりませんが、こんな事もあったと言うことで、備忘録として記録しておきます。

続きを読む

AWS amazon linuxにpostfixadmin3.0を入れる

postfixadminを導入したのでメモ
(postfixadminだけのメモです。メールサーバーの構築はページの下にあるリンクを参考)
メールサーバーの構築のためElastic IPをインスタンスに直接関連つけます。

postfixadminを入手

・postfix3.0のダウンロード
$wget https://sourceforge.net/projects/postfixadmin/files/postfixadmin/postfixadmin-3.0/postfixadmin-3.0.tar.gz

・「postfixadmin-3.0.tar.gz」というファイルがあるのでこれを解凍する。
$tar xfvz postfixadmin-3.0.tar.gz
展開されたのでlsコマンドでpostfixadmin-3.0があるか確認の後圧縮ファイルはいらないので削除する
$rm -f postfixadmin-3.0.tar.gz

webで閲覧できるようにする

必要なものをインストール

root権限で実行する
#yum install -y httpd24 php56 mysql56-server
(他にもPHPのモジュールが必要なのでこれらは後にインストールします。とりあえずこの3つ)

apacheの設定

#cd /etc/httpd/conf.d/
#vi mail.conf

mail.confを編集

mail.conf
ServerName [ドメイン、またはIP]
<VirtualHost *:80>
Servername [ドメイン、またはIP]
DocumentRoot [postfixadmin-3.0があるディレクトリ]

         <Directory "[postfixadmin-3.0があるディレクトリ]">
                Options FollowSymlinks
                AllowOverride All
                Require all granted
         </Directory>

ErrorLog /var/log/httpd/mail-error_log
CustomLog /var/log/httpd/mail-access_log common
</VirtualHost>

postfixadmin-3.0のディレクトリをapacheユーザーがアクセスできるように権限を変更する。
(ここは各々違うので省略。以下の二つはは参考程度に…)
・簡単な方法は[#chgrp -R apache ディレクトリ指定]して[#chmod -R 770 ディレクトリ指定]でだいたい行ける。
または、
・ディレクトリは権限「770」ファイルは「660」に設定する方法は[#find ディレクトリ指定 -type d -exec chmod 0770 {} \;]と[#find ディレクトリ指定 -type f -exec chmod 0660 {} \;]

httpdを起動
/etc/init.d/httpd start

webで確認。apacheが通っていたらwellcomeページ(下の画像)が表示される。(URLにはElastic IPでも可能。ドメインの場合はroute53など設定が必要。)
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
postfixadmin-welcom.PNG

↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑

mysql設定

mysql起動

#/etc/init.d/mysqld start
#mysql
>create database postfixadmin CHARACTER SET utf8;
>GRANT ALL PRIVILEGES ON postfixadmin.* TO postfixadmin@localhost IDENTIFIED BY 'postfixadmin';
>quit

postfixadmin設定

postfixadmin-3.0のディレクトリに移動
オリジナルバックアップをとっておく
cp config.inc.php config.inc.php.bk
編集
vi config.inc.php

変更点

$CONF['configured'] = true;
$CONF['setup_password'] = 'password';
$CONF['default_language'] = 'ja';
$CONF['database_type'] = 'mysqli';
$CONF['database_host'] = 'localhost';
$CONF['database_user'] = 'postfixadmin';
$CONF['database_password'] = 'postfixadmin';
$CONF['database_name'] = 'postfixadmin';
$CONF['footer_text'] = 'Return to ドメイン';
$CONF['footer_link'] = 'http://ドメイン';
$CONF['emailcheck_resolve_domain']='NO';

必要なモジュールインストール
#yum install -y php56-mbstring php56-mysqlnd php56-imap
モジュール読み込み
#/etc/init.d/httpd reload

http://ドメイン/postfixadmin/setup.phpを開く

account.PNG

setup passwordはconfig.inc.phpで設定した$CONF['setup_password'] = 'password';のパスワードを記入する

管理者追加を押したらハッシュ化されたものがでてきますので変更する。
キャプチャ.PNG

$CONF[‘setup_password’] = ‘ハッシュ化されたものをコピーして貼り付け’;

再びsetup.phpにアクセスして管理者を追加する。
セットアップするための必要なパスワードはハッシュ化されるまえのものを入力する。

http://ドメイン/login.phpを開いて先ほど作成した管理者ログインできたら成功。
最後にsetup.phpにアクセスできないように権限を変更しておく。

各サービスの自動起動

#chkconfig httpd on
#chkconfig mysqld on

postfix,dovecot設定(メールサーバー構築)

以下のサイトを参考にしました。
Qiita-EC2にメールサーバを構築(複数ドメイン)

上記のリンクで自分が出会った注意点メモ
注意点

続きを読む

AmimotoにLet’s encryptを簡単インストール

WordPressをEC2で使うにはおなじみの高速実行環境Amimoto

よく使う構成はプロビジョニング時に自動設定できるようになっているので、色々と見てみると便利。
(逆にこれを忘れていると自分で用意した構成を失ってしまったりちゃんと起動しなかったりする可能性も……無いとは言えないかもしれない。多分無い)

そんな構成群については Launch-with-1-Click/lw1-amimoto を参照。

今回はまっさらな WordPress Powered by AMIMOTO (HVM PHP7 Ready) にLet’s encrypt の公式クライアント (certbot) のインストールとNginxのHTTP2有効化を一発でやる。
(といっても設定される証明書は用意されている自己署名証明書なのであしからず)

とりあえずインスタンスを立て、初回プロビジョニングが終わるまで待つ。初回はyum installとかあるので数分かかる模様。パブリックDNSにブラウザからアクセスできるようになったら準備OK。

設定した秘密鍵を使ってSSHでインスタンスに接続し、Amimotoの設定ファイルを上書き。

$ cat /opt/local/amimoto.json|jq '. + {"nginx":{"http2_enable":true},"letsencrypt":{"install":true}}| .run_list |=.+ ["recipe[amimoto::nginx_default]"]' > ~/amimoto.json
$ cp ~/amimoto.json /opt/local/amimoto.json

sudoでプロビジョニングを実行。

$ sudo /opt/local/provision

これでLet’s encryptが使えるようになる……!はずだったんだけど無限にcertbotのビルドに時間がかかった。めっちゃCPUクレジット食われる……

失敗したかと思ってやり直しそうになったものの、気長に待てばなんとか終了。

あ、それとNginxのconfファイルを作成する有効にしたので念のため戻しておく。

$ jq '. .run_list|= .-["recipe[amimoto::nginx_default]"]' /opt/local/amimoto.json > ~/amimoto.json
$ cp ~/amimoto.json /opt/local/amimoto.json

(jqは便利だ……)

試しに https でインスタンスにアクセスできることを確認する。

ところでAmazon Linuxは OpenSSL 1.0.1系のためALPNが使えずNPNのみなので残念ながら Chrome でHTTP/2が使えない。いい加減どうにかしてくれ〜

certbotコマンドへのシンボリックリンクは現状張られていないのでletsencryptコマンドを使う。

独自ドメインを使う場合は事前にDNSをインスタンスに向けておくこと。(というかEC2のパブリックDNSではLet’s encryptは使えなさそう。ビルド待ちの間にしておけると良さそう。)

$ sudo su -
# letsencrypt auth --webroot -d {your-domain} -w /var/www/vhosts/{instance-id}/

初回は登録するメールアドレスとか聞かれる。

Congratulations! のメッセージが表示されていれば成功。

/etc/nginx/conf.d/default-ssl.confssl_certificate,ssl_certificate_keyを発行した証明書のパスに書き換え。

    ...
    ssl_certificate     /etc/letsencrypt/live/{your-domain}/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/{your-domain}/privkey.pem;
    ...

反映させる。

# service nginx reload

設定したドメインでhttpsでアクセスして証明書エラーが出ていないことが確認できれば成功。

以後は通常のAmimoto同様に使えば良い。

スクリーンショット 2017-01-10 5.40.31.png


……CPUクレジット食い過ぎ💢
スクリーンショット 2017-01-10 5.43.33.png

めっちゃ時間かかるしCPU食うし簡単というのは看板に偽りしかなかった気もする

普通にやったほうが絶対はやい……

$ cd /tmp
$ sudo yum install python27-pip python27-virtualenv augeas-libs dialog gcc libffi-devel openssl-devel system-rpm-config
$ sudo mkdir /opt/letsencrypt
$ sudo virtualenv /opt/letsencrypt/
$ sudo /opt/letsencrypt/bin/pip install certbot
$ sudo ln -s /opt/letsencrypt/bin/certbot /usr/local/bin/certbot

続きを読む

Jenkins + GitBook で GitHub のドキュメント(PDF)のパブリッシュを自動化しよう

はじめに

「AWS で CI 環境を構築」シリーズの第二弾です。

今回は Jenkins で GitHub のドキュメントを GitBook で PDF 化して、GitHub のリリースにアップロードすることで、ドキュメントのパブリッシュを自動化します。

準備

GitHub – Jenkins 連携

以下で、GitHub のイベントをトリガーに、Jenkins サーバにソースが展開されるようにしておきます。

「Jenkins で GitHub のイベントを受けてゴニョゴニョする準備」
http://qiita.com/exabugs/items/cc2c6f59da98cd81a06f

API トークン 作成

リリースの作成と成果物のアップロードで GitHub API を使用します。

そのため、Settings – Personal access tokens で API トークン を作成します。
screencapture-github-settings-tokens-1483809559363.png

scopes (権限) は repo をチェック
スクリーンショット_2017-01-08_2_24_46.png

作成したら、token をメモしておいて下さい
スクリーンショット_2017-01-08_2_25_33.png

インストール

Node.js

yum では、まだ 0.10 です。古いです。
ここは LTS の 6系 が望ましいです。

Node.js は環境の管理をしたいので nvm を使います。
https://github.com/creationix/nvm

以下で nvm をインストール。
NVM_DIR でインストール ディレクトリを指定できます。(指定しない場合は ~/.nvm にインストールされる)
全ユーザで使えるように、/usr/local/nvm にインストールします。

# curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.0/install.sh | NVM_DIR=/usr/local/nvm bash

インストール後、以下が ~/.bashrc に追記されます。しかし、このままではインストールしたユーザでしか使えない。
そのため、この部分を /etc/profile.d/nvm.sh (新規) に移動して、全ユーザで読み込まれるようにします。

/etc/profile.d/nvm.sh
export NVM_DIR="/usr/local/nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm

再ログインで、上記 nvm が使用可能になります。

# nvm ls-remote

         v6.9.0   (LTS: Boron)
         v6.9.1   (LTS: Boron)
         v6.9.2   (LTS: Boron)
         v6.9.3   (LTS: Boron)
         v6.9.4   (Latest LTS: Boron)

最新の v6.9.4 をインストールしましょうか。

# nvm install v6.9.4
# nvm use v6.9.4
# nvm alias default v6.9.4

以下になれば Node.js はインストール完了です。

# node -v
v6.9.4

GitBook

GitBook は以下でインストールできます

# sudo npm install gitbook-cli -g
# gitbook install

また、PDF出力のために、Calibre が必要です
https://calibre-ebook.com/download_linux

# sudo -v && wget -nv -O- https://download.calibre-ebook.com/linux-installer.py | sudo python -c "import sys; main=lambda:sys.stderr.write('Download failed\n'); exec(sys.stdin.read()); main()"

OS のライブラリも、不足があるようなのでインストールします

# yum install mesa-libGL

以下で、動作確認しておきましょうか。
(README.md と SUMMARY.md が必須)

# gitbook pdf

設定

Jenkins

GitBook で PDF を生成し、リリースにアップロードします。
上記を実行するタイミングとしては、リポジトリにSUMMARY.md が存在し、かつ tag が Push された時だけとします。

スクリーンショット 2017-01-08 3.03.09.png
スクリーンショット 2017-01-08 3.03.56.png

#!/bin/bash

export PATH=/usr/bin:/usr/local/bin:$PATH
. /etc/profile.d/nvm.sh

param() {
  echo $payload | jq -r $1
}

OWNER=`param '.repository.owner.name'`
BRANCH=`param '.ref' | sed "s:^refs/heads:remotes/origin:"`
NAME=`param '.repository.name'`
URL=`param '.repository.ssh_url'`
ID=`param '.after'`

echo $URL
echo $BRANCH

if [ ! -d $NAME ]; then
  git clone $URL
fi

cd $NAME

git fetch origin
git reset --hard ${BRANCH}


#GitBook
if [[  $BRANCH = refs/tags/*  ]]; then

  if [ -f SUMMARY.md ]; then
    gitbook pdf
    github_asset_upload.sh $OWNER $NAME ${BRANCH:10}
  fi

fi

Jenkins から呼び出しているスクリプト (github_asset_upload.sh) が以下になります。
gitbook コマンドで生成した book.pdf を、GitHub のリリースにアップロードする処理になります。
book.pdf が既に存在するなら、削除してアップロード (更新) になります。

github_asset_upload.sh
#!/bin/bash

TOKEN=<ここにトークンを記載>

OWNER=$1
REPO=$2
TAG=$3

FILE=book.pdf
TYPE=application/pdf

URL_API=https://api.github.com/repos/${OWNER}/${REPO}/releases
URL_UPLOAD=https://uploads.github.com/repos/${OWNER}/${REPO}/releases
CURL=(curl -s -H "Authorization: token ${TOKEN}")


# タグのリリースがあるか?
#  なければ、作成
#  あれば、何もしない
ID_RELEASE=`"${CURL[@]}" -X GET $URL_API | jq ".[] | select(.tag_name == \"$TAG\") | .id"`
if [ -z $ID_RELEASE ] ; then
  echo Release $TAG create.
  ID_RELEASE=`"${CURL[@]}" -X POST $URL_API -d "{\"tag_name\":\"$TAG\"}" | jq ".id"`
else
  echo Release $TAG exists.
fi
echo Release : $ID_RELEASE


# タグのリリースで、指定したファイル名のassetsがあるか?
#  なければ、何もしない
#  あれば、削除
ID_ASSET=`"${CURL[@]}" -X GET $URL_API/$ID_RELEASE/assets | jq ".[] | select(.name == \"$FILE\") | .id"`
if [ -z $ID_ASSET ] ; then
  echo Asset $FILE not exists.
else
  echo Asset $FILE exists : $ID_ASSET. so delete.
  "${CURL[@]}" -X DELETE $URL_API/assets/$ID_ASSET -d "{\"tag_name\":\"$TAG\"}"
fi


# ファイルのアップロード
echo Asset $FILE upload.
ID_ASSET=`"${CURL[@]}" -X POST -H "Content-type: $TYPE" -F file=@$FILE $URL_UPLOAD/$ID_RELEASE/assets?name=$FILE | jq ".id"`
echo Asset : $ID_ASSET

動作確認

タグを打ってPushしてみます。

$ git tag -a v1.9.0
$ get push origin v1.9.0

Release v1.9.0 が作成され、その中に book.pdf が追加されました。
スクリーンショット_2017-01-08_2_33_03.png

日本語対応

日本語に対応するためには、フォントが必要です。

以下のサイトを参考に「Adobe/Google謹製の源ノ角ゴシック(Noto Sans CJK / Source Han Sans)」をインストールしました。(ありがとうございます)
http://backport.net/blog/2016/09/06/pdf_embedded_japanese_font/

フォントは、book.json で “GenShinGothic” を指定します。

book.json
{
  "pdf": {
    "fontFamily": "GenShinGothic"
  }
}

さすが源ノ角!日本語がキレイにPDF化されました!
スクリーンショット 2017-01-12 10.06.38.png

カスタマイズ

PDF の見栄えをカスタマイズできるようです。
https://toolchain.gitbook.com/config.html

book.json
{
    "pluginsConfig": {
        "fontSettings": {
            "size": 2
        }
    },
    "pdf": {
        "fontFamily": "GenShinGothic",
        "fontSize": 12,
    }
}

リポジトリの小技

ドキュメント(マークダウン)は、ソースコードと同じツリーに入れる派と、いや、リポジトリも分けたいんだよ、という派があると思います。

ドキュメントの編集が頻繁にある場合、ソースと同じブランチだとコミットログが荒れてくるので、今回はドキュメント用のリポジトリを分離したいと思います。

その際、Git の オーファン(ルートなし) ブランチが便利です。
(コミット履歴やタグ等、master ブランチとは完全に切り離されたブランチになります。)

以下で、ルートなしの documents ブランチを作成し、そこに ドキュメント(マークダウン) をコミットしていきます。

$ git checkout --orphan documents

おわりに

  • これで、「ドキュメントはマークダウンで」活動が、一歩進みました。
  • GitHub の操作は、Jenkins の Git プラグインを上手く使えば、もっと簡単にできるかもしれないです。
  • HTML 形式で出力して、GitHub-Pages でパブリッシュしようか、とも思いましたが、Private リポジトリでも GitHub-Pages は Public になるのですね。危ない、危ない。

続きを読む