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

続きを読む

[JAWS-UG CLI] AWS Batch #3 環境の破棄(Unmanaged 編)

前提条件

EC2への権限

EC2、ECS、AWS Batch などに対してフル権限があること。

0. 準備

0.1. ジョブの作成と実行の完了

AWS Batch #2 ジョブの作成と実行 が終わっていること

0.2. 変数の確認

#1, #2 から引き続き利用する変数を確認します

コマンド
cat << ETX

    CFN_STACK_NAME:       ${CFN_STACK_NAME}
    COMPUTE_ENV_NAME:     ${COMPUTE_ENV_NAME}
    JOB_QUEUE_NAME:       ${JOB_QUEUE_NAME}
    JOB_DEFINITION_NAME:  ${JOB_DEFINITION_NAME}

    CONRAINER_PROPS_FILE: ${CONRAINER_PROPS_FILE}

ETX
結果(例)

    CFN_STACK_NAME:       aws-batch-xxxxxxxxxx
    COMPUTE_ENV_NAME:     aws-batch-refarch
    JOB_QUEUE_NAME:       aws-batch-job-queue-xxxxxxxxxx
    JOB_DEFINITION_NAME:  aws-batch-job-def-xxxxxxxxxx

    CONRAINER_PROPS_FILE: aws_batch_container_props.json

0.3. AWS CLIのバージョン

以下のバージョンで動作確認済

  • AWS CLI 1.11.36
コマンド
aws --version
結果(例)
 aws-cli/1.11.36 Python/2.7.5 Darwin/13.4.0 botocore/1.4.93

バージョンが古い場合は最新版に更新しましょう。

コマンド
sudo -H pip install -U awscli

0.4. AWS アカウントの属性

EC2-Classic が見えない AWS アカウントであること。

コマンド
AWS_SUPPORT_PLATFORMS=$( 
         aws ec2 describe-account-attributes 
           --query 'AccountAttributes[?AttributeName == `supported-platforms`].AttributeValues[].AttributeValue' 
           --output text 
) && echo ${AWS_SUPPORT_PLATFORMS}
結果
 VPC

1. リソースの削除

1.1. 定義ファイルの削除

コマンド
rm -f ${CFN_STACK_NAME}.yaml ${CFN_STACK_NAME}-ecs.yaml 
    ${CONRAINER_PROPS_FILE}

1.2. ジョブ定義の無効化

コマンド
for arn in $( aws batch describe-job-definitions 
    --job-definition-name ${JOB_DEFINITION_NAME} 
    --status ACTIVE 
    --query 'jobDefinitions[].jobDefinitionArn' 
    --output text)
do
  aws batch deregister-job-definition 
    --job-definition $arn
done
結果
返り値なし

1.3. ジョブキューの削除

ジョブキューを削除するには、まずキューを無効化します。

コマンド
aws batch update-job-queue 
  --job-queue ${JOB_QUEUE_NAME} 
  --state DISABLED
結果(例)
{
    "jobQueueArn": "arn:aws:batch:us-east-1:xxxxxxxxxxxx:job-queue/aws-batch-job-queue-xxxxxxxxxx", 
    "jobQueueName": "aws-batch-job-queue-xxxxxxxxxx"
}

その上で削除します

コマンド
aws batch delete-job-queue 
  --job-queue ${JOB_QUEUE_NAME}
結果
返り値なし

[ 重要!!]
AWS Batch にはまだ wait コマンドがなく不便なのですが、
順序よく消さないと INVALID な状態となりリソースが消せない
可能性があります。なので、確実に消えたことを確かめます。

コマンド
aws batch describe-job-queues 
  --job-queues ${JOB_QUEUE_NAME}
結果
{
    "jobQueues": []
}

1.4 ECS クラスタの削除

コマンド
aws cloudformation delete-stack 
  --stack-name ${CFN_STACK_NAME}-ecs

削除が完了するまで待機します

コマンド
aws cloudformation wait stack-delete-complete 
  --stack-name ${CFN_STACK_NAME}-ecs

1.5. コンピューティング環境の削除

コンピューティング環境も、削除するにはまず無効化が必要です。

コマンド
aws batch update-compute-environment 
  --compute-environment ${COMPUTE_ENV_NAME} 
  --state DISABLED
結果(例)
{
    "computeEnvironmentName": "aws-batch-refarch", 
    "computeEnvironmentArn": "arn:aws:batch:us-east-1:xxxxxxxxxxxx:compute-environment/aws-batch-refarch"
}

その上で削除します

コマンド
aws batch delete-compute-environment 
  --compute-environment ${COMPUTE_ENV_NAME}
結果
返り値なし

[ 重要!!] しばらく待ち、こちらも正常に消えたことを確認します。

コマンド
aws batch describe-compute-environments 
  --compute-environments ${COMPUTE_ENV_NAME}
結果
{
    "computeEnvironments": []
}

1.6. VPC や IAM の削除

コマンド
aws cloudformation delete-stack 
  --stack-name ${CFN_STACK_NAME}
結果
返り値なし

完了

お疲れさまでした

続きを読む

OpsWorks管理下のAmazon LinuxをCLIでアップグレードする

はじめに

何かにつけ、GUIが苦手です。WEBブラウザを開くと、トンデモなバナーをクリックしてしまうし、そもそもトンデモなバナーが出来てきてしまうネットワーク広告が怖い。
OpsWorks(AWSマネジメントコンソール)では、広告は表示されないのですが、いつか広告モデルになるかもしれない…
そんな不安から日々のオペレーションは、awscliを用いたCLIで完結したいというのがモチベーションです。
今回は、OpsWorksで唯一GUIなオペレーション(awscliにサブコマンドが用意されていない)となるUpgrade Operating SystemをCLIで実施するコネタを紹介します。

バージョン

動作を確認している環境とそのバージョンは、以下の通りです。

  • OpsWorks(リモート環境)

    • Cehf 11.10
    • Berkshelf 3.2.0
    • Amazon Linux 2016.03 → 2016.09
  • Mac(ローカル環境)
    • aws-cli 1.11.28 Python/2.7.10 Darwin/15.6.0 botocore/1.4.85

Custom JSONの準備

下ごしらえとして、OpsWorksに渡すattributeをCustom JSONとしてJSONフォーマットのファイルにまとめておきます。
OpsWorksのビルドインcookbookであるdependenciesのattributeを上書きします。詳細は、後述します。

$ mkdir ./aws/opsworks && vi $_/upgrade-amazonlinux.json

{
  "dependencies": {
    "os_release_version": "2016.09",
    "allow_reboot": false,
    "upgrade_debs": true
  }
}

コマンド

CLIでAmazon Linuxをアップグレードしていきます。

幸せなchefになるために、独自の味付けをしない、つまり可能な限りビルドインのcookbookを利用するべきです。
Upgrade Operating Systemで実施しているのは、update_dependenciesというdeployになり、dependencies::updateレシピを実行しています。該当のレシピを一読すると、yum -y updateコマンドを実行していました。前述のCustom JSONは、yum -y updateするために必要なattributeとなります。

  • os_release_versionは、アップグレードしたいAmazon Linuxのバージョンを指定します。
  • allow_reboot は、パッケージのアップデートの後に再起動するか指定します。今回は、インスタンスの停止を明示的に実施しますので、falseとしておきます。
  • upgrade_debsは、一見Debianぽいですがパッケージをアップデートするか否かのフラグとして実装されてます。今回は、アップデートするのでtrueとしておきます。

Upgrade Operating Systemの正体を把握できたので、awscliで以下のような一連コマンドを実行していきます。

# 1. Stackのリビジョンを指定
$ aws opsworks --region us-east-1 update-stack --stack-id STACK_ID --custom-cookbooks-source "{"Revision":"UgradeAmazonLinux"}"

# 2. Stackで管理している全EC2インスタンスに対して、update_custom_cookbooksの実行(最新版cookbookを配置)
$ aws opsworks --region us-east-1 create-deployment --stack-id STACK_ID --command "{"Name":"update_custom_cookbooks"}"

# 3. opsworks agentのバージョンアップ(最新版を利用する)
$ aws opsworks --region us-east-1 update-stack --stack-id STACK_ID --agent-version LATEST

# 4. Custom JSONとレシピを指定して、全パッケージをアップデート
$ aws opsworks --region us-east-1 create-deployment --stack-id STACK_ID --instance-ids INSTANCE_ID01 INSTANCE_ID02 --command "{"Name":"execute_recipes","Args":{"recipes":["dependencies::update"]}}" --custom-json file://./aws/opsworks/upgrade-amazonlinux.json

# 5. EC2インスタンスの停止
$ aws opsworks --region us-east-1 stop-instance --instance-id INSTANCE_ID01
$ aws opsworks --region us-east-1 stop-instance --instance-id INSTANCE_ID02

# 6. OpsWorksで保持しているOSのバージョン情報を更新
$ aws opsworks --region us-east-1 update-instance --instance-id INSTANCE_ID01 --os "Amazon Linux 2016.09"

# 7. EC2インスタンスの起動
$ aws opsworks --region us-east-1 start-instance --instance-id INSTANCE_ID01
$ aws opsworks --region us-east-1 start-instance --instance-id INSTANCE_ID02

4でビルドインのcookbookにCustom JSONでattributeを渡し、全パッケージのアップデートを実施します。
5でEC2インスタンスを停止するのは、以下の2つの理由があります。

  • OpsWorksが保持しているEC2インスタンスの情報を更新するためには、該当のEC2インスタンスを停止する必要がある
  • OSアップグレード後は、setupライフサイクルイベントを実施することを推奨されている

setup ライフサイクルイベントは、7の起動時に実行されます。

おわりに

AWSが広告モデルになったら嫌ですね。
Enjoy CLI!

参考

続きを読む

シンプルな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 Lambda】Amazon Linux の Docker イメージを使ってデプロイパッケージを作成する

AWS Lambda の実行環境に無いライブラリを利用する場合には、ローカル環境でライブラリのソース群をダウンロードしておいてそれをデプロイパッケージに含めて Lambda にアップロードする必要があります。

Python の例だとデプロイパッケージを作る際に例えば下記のような手順を踏みます。

$ virtualenv -p python2.7 venv
$ source venv/bin/activate
$ pip install -r requirements.txt
$ zip deploy_package.zip lambda_function.py # 実行スクリプトを zip にする
$ cd venv/lib/python2.7/site-packages
$ zip -r ../../../../deploy_package.zip * # pip でインストールしたライブラリを zip に追加

利用したいライブラリが純粋な Python コードであればこのパッケージは問題無く Lambda 上でも動作するのですが、OS の機能に依存しているようなライブラリの場合、ローカル環境でビルド・インストールされたものを Lambda 上にアップロードしても動きません。
上記の例で言うと pip install は Lambda の実行環境と同じ環境で行う必要があります。

失敗例

pycrypto というライブラリを使う Lambda Function を作って実行します。

requirements.txt
pycrypto==2.6.1
lambda_function.py
from Crypto.PublicKey import RSA


def lambda_handler(event, context):
    return {"messagge": "success"}

上記のファイルを用意してローカルの OS X 環境でデプロイパッケージを作成します。

$ virtualenv -p python2.7 venv
$ source venv/bin/activate
$ pip install -r requirements.txt
$ zip deploy_package.zip lambda_function.py
$ cd venv/lib/python2.7/site-packages
$ zip -r ../../../../deploy_package.zip *

作成した deploy_package.zip を AWS Lambda にアップロードして実行すると次のようなエラーが出ます。
Unable to import module 'lambda_function'

Unable to import module 'lambda_function'

エラーログ全文を見ると Unable to import module 'lambda_function': /var/task/Crypto/Util/_counter.so: invalid ELF header とあります。pycrypto が参照するファイルのヘッダ情報が不正であるようです。原因は pycrypto をインストールした環境が Lambda の実行環境と異なるところにあります。

対策

Amazon Linux の Docker イメージを用いてライブラリのインストールを行うことでこの問題を回避することができます。

先述のファイルと同じ階層にこのような Dockerfile を用意して

Dockerfile
FROM amazonlinux:latest

RUN yum -y update && yum -y install gcc python27-devel
RUN cd /tmp && 
    curl https://bootstrap.pypa.io/get-pip.py | python2.7 - && 
    pip install virtualenv
WORKDIR /app
CMD virtualenv -p python2.7 venv-for-deployment && 
    source venv-for-deployment/bin/activate && 
    pip install -r requirements.txt

このようにコマンドを実行すると venv-for-deployment という名前で、Amazon Linux でビルドされた Python ライブラリコードが作成されます。

$ docker build . -t packager
$ docker run --rm -it -v $(PWD):/app packager

その後は下記のようにデプロイパッケージの zip を作成して AWS Lambda にアップロードします。

$ zip deploy_package.zip lambda_function.py
$ cd venv-for-deployment/lib/python2.7/site-packages
$ zip -r ../../../../deploy_package.zip * .* # dotfile が含まれる場合は ".*" も

実行するとライブラリが import 出来て無事 "success" が表示されます。

success

自動化

ちょっと叩くコマンド量が多いのでこのような Makefile を作っておくと make だけで zip が生成されて便利です。

Makefile
package:
    docker build . -t packager
    docker run --rm -it -v $(PWD):/app packager
    zip deploy_package.zip lambda_function.py
    cd venv-for-deployment/lib/python2.7/site-packages && zip -r ../../../../deploy_package.zip * .*
    echo "Completed. Please upload deploy_package.zip to AWS Lambda"

サンプル

今回用いた Lambda Function のサンプルはこちらのリポジトリに置いています。
https://github.com/morishin/python-lambda-function-test

所感

AWS Lambda 便利なのですがちょっと凝ったことをしようとすると泣きそうになりますね。

続きを読む

AWS Lambda で GitHub の Projects 機能を Issue と連動させて Trello の代替とする

モチベーション

Trello が買収されたので、代替を探す。
昨年 9 月頃? リリースされた、GitHub の Projects 機能がよく出来ているが、カードの登録が手動で面倒臭い。

GitHub に Issue を登録したら、Projects のカードとして自動で登録して欲しい。
また、Issue がクローズされたら、Projects からは取り除いてくれると、カンバン運用が捗りそうだ。

完成イメージ
スクリーンショット 2017-01-15 12.16.47.png

スクリーンショット_2017-01-15_9_57_33.png

設定

GitHub

最初に、図の token A を取得します。

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

GitHub の API を実行するアカウントの、Personal Access Token を取得します。
repo の権限を ON にしてください。
screencapture-github-settings-tokens-new-1484453943940.png

token A が生成されたので、メモしておきます。
screencapture-github-settings-tokens-1484454286388.png

今回は、token A と token B は、同じトークンを使うことにします。

AWS Lambda

順番に、設定していきます。次は、Lambda 。

Node 4.3 「Blank Function」 を選択します。
スクリーンショット_2017-01-15_11_19_31.png

トリガーの設定で、API Gateway を選択します。
名前はお好みで。
Security は トークンを使って接続制限するので、Open でも大丈夫です。
screencapture-ap-northeast-1-console-aws-amazon-lambda-home-1484448339516.png

名前はお好みで。
Edit Code Inline を選択して、下記の Lambda コードをコピペすればOK。
Environment Variables (環境変数) に、token というキーで、先に取得した GitHub の Private Access Token を設定して下さい。
(Lambda 処理の中で、token A / token B の両方のトークンとして使用します。)
ロールは、Simple Microservice テンプレートで、新規作成しました。
タイムアウトは、デフォルトの3秒だと微妙に足らないので、10秒程度に設定します。
screencapture-ap-northeast-1-console-aws-amazon-lambda-home-1484447617614__1_.png

Lambda の処理概要は、以下になります。

  1. Issue が更新されると、GitHub Webhook によって、ハンドラーが起動される
  2. Issue がマイルストーンにひもづいていない場合は、何もしない。
  3. マイルストーンにひもづくProjectを見つける
    存在しない場合は、新規作成
    存在する場合は、名前が変更されていればマイルストーン名に合わせる
  4. ToDo / Doing / Done のカラムが、なければ生成する
  5. Issue にひもづく Card を見つける
  6. Issue が
    Open なら、ToDo カラムに Card を追加。既に存在すれば何もしない。
    Close なら、カラムから Card を削除

Edit Code Inline に貼り付ける Lambda コードは、下記になります。
(GitHub はこちら https://github.com/exabugs/GitHubIssueProjectSync )

'use strict';

const crypto = require('crypto');
const https = require('https');

exports.handler = function(event, context) {

  console.log(JSON.stringify(event));
  console.log(JSON.stringify(event.headers));
  const headers = event.headers;

  // 認証
  const hmac = crypto.createHmac('sha1', process.env.token);
  hmac.update(event.body, 'utf8');
  const calculatedSignature = 'sha1=' + hmac.digest('hex');

  if (headers['X-Hub-Signature'] !== calculatedSignature) {
    console.log(`calculatedSignature : ${calculatedSignature}`);
    console.log(`req.X-Hub-Signature : ${headers['X-Hub-Signature']}`);
    return context.succeed({statusCode: 403});
  }

  const payload = JSON.parse(event.body);

  const REPO = payload.repository;
  if (!REPO) {
    console.log('Not exists Repository.');
    return context.succeed({statusCode: 200});
  }
  const REPO_OWNER = REPO.owner.login;
  const REPO_NAME = REPO.name;
  console.log(`REPOSITORY_OWNER : ${REPO_OWNER}`);
  console.log(`REPOSITORY_NAME : ${REPO_NAME}`);

  const ISSUE = payload.issue;
  if (!ISSUE) {
    console.log('Not exists Issue.');
    return context.succeed({statusCode: 200});
  }
  const ISSUE_STATE = ISSUE.state;
  const ISSUE_TITLE = ISSUE.title;
  const ISSUE_ID = ISSUE.id;
  const ISSUE_URL = ISSUE.url;
  console.log(`ISSUE_STATE : ${ISSUE_STATE}`);
  console.log(`ISSUE_TITLE : ${ISSUE_TITLE}`);
  console.log(`ISSUE_ID : ${ISSUE_ID}`);
  console.log(`ISSUE_URL : ${ISSUE_URL}`);

  const MILESTONE = ISSUE.milestone;
  if (!MILESTONE) {
    console.log('This issue is not assinged to milestone.');
    return context.succeed({statusCode: 200});
  }
  const MILESTONE_TITLE = MILESTONE.title;
  const MILESTONE_ID = MILESTONE.id;
  const MILESTONE_URL = MILESTONE.html_url;
  console.log(`MILESTONE_TITLE : ${MILESTONE_TITLE}`);
  console.log(`MILESTONE_ID : ${MILESTONE_ID}`);
  console.log(`MILESTONE_URL : ${MILESTONE_URL}`);

  request('GET', `/repos/${REPO_OWNER}/${REPO_NAME}/projects`).then(data => {
    console.log('Stage 1');
    return data.filter((milestone) => {
      // Description を編集できるように、最終行にIDが含まれていればよいことにする。
      if (!milestone.body) return false;
      const body = milestone.body.split('\n');
      return body[body.length - 1].indexOf(MILESTONE_ID) !== -1;
    })[0];
  }).then(project => {
    console.log('Stage 2');
    if (!project) {
      // Project 新規追加
      console.log('Create Project.');
      const path = MILESTONE_URL.split('/').slice(3).join('/');
      const url = `Milestone : <a href='/${path}'>${MILESTONE_ID}</a>`;
      const json = {name: MILESTONE_TITLE, body: url};
      return request('POST', `/repos/${REPO_OWNER}/${REPO_NAME}/projects`, json);
    } else if (project.name !== MILESTONE_TITLE) {
      // 名前だけアップデートする。(Description はそのまま)
      console.log(`Update Project Name.`);
      const json = {name: MILESTONE_TITLE, body: project.body};
      return request('PATCH', `/projects/${project.id}`, json);
    } else {
      console.log('Project exists.');
      return project;
    }
  }).then(project => {
    console.log('Stage 3');
    // ToDo / Doing / Done のカラム生成
    return request('GET', `/projects/${project.id}/columns`).then(columns => {
      return Promise.all(['ToDo', 'Doing', 'Done'].map(name => {
        const column = columns.filter(column => column.name === name);
        if (column.length) {
          // カードの配列を返す
          console.log(`Column : ${name} exists. Search cards.`);
          return request('GET', `/projects/columns/${column[0].id}/cards`).then(cards => {
            return {column: column[0], card: cards.filter(card => card.content_url === ISSUE_URL)[0]};
          });
        } else {
          // カラムを生成して、空配列を返す
          console.log(`Column : ${name} not exists. Create column.`);
          return request('POST', `/projects/${project.id}/columns`, {name: name}).then((column) => {
            return {column: column, card: undefined};
          });
        }
      }));
    });
  }).then(columns => {
    console.log('Stage 4');
    if (ISSUE_STATE === 'closed') {
      // 削除
      return Promise.all(columns.map(column => {
        console.log(`Find closed card in column ${column.column.name}.`);
        const card = column.card;
        if (card) {
          console.log('Remove card.');
          return request('DELETE', `/projects/columns/cards/${card.id}`);
        }
      }));
    } else {
      // Todo に追加 (他のカラムに存在した場合は何も変化なし)
      console.log(`Card is not closed.`);
      if (!columns.filter(column => column.card).length) {
        console.log('Card create.');
        const column = columns[0].column;
        const json = {content_id: ISSUE_ID, content_type: 'Issue'};
        return request('POST', `/projects/columns/${column.id}/cards`, json);
      }
    }
  }).then(() => {
    console.log('Success. Return 200.');
    return context.succeed({statusCode: 200});
  }).catch(e => {
    console.log(e);
    return context.fail(e);
  });
};

function request (method, path, json) {
  return new Promise(function(resolve, reject) {

    const options = {
      hostname: 'api.github.com',
      port: 443,
      path: path,
      method: method,
      headers: {
        'User-Agent': 'Awesome-Octocat-App',
        'Authorization': `token ${process.env.token}`,
        'Accept': 'application/vnd.github.inertia-preview+json'
      }
    };
    let data = undefined;
    if (json !== undefined) {
      data = JSON.stringify(json);
      options.headers['Content-Type'] = 'application/json; charser=UTF-8';
      options.headers['Content-Length'] = Buffer.byteLength(data);
    }
    const req = https.request(options, res => {
        let data = '';
        res.on('data', d => {
          data += d;
        });
        res.on('end', () => {
          console.log(`Status-Code : ${res.statusCode}.`);
          switch (res.statusCode) {
            case 200:
            case 201: // POST
              resolve(JSON.parse(data));
              break;
            case 204: // DELETE
            case 422: // Duplicate Insert
              resolve();
              break;
            default:
              reject(data);
              break;
          }
        });
      }
    );
    req.on('error', (e) => {
      console.error(e);
      reject(e);
    });
    req.end(data);
  });
}

Triggers タブで、API Gateway の URL を調べてメモしておきます。
後で、GitHub の Webhook に設定します。

screencapture-ap-northeast-1-console-aws-amazon-lambda-home-1484453085382.png

GitHub

最後に、GitHub の Webhook を設定します。
リポジトリ毎の設定ではなく、アカウントの設定で Webhook を設定すれば、全てのリポジトリが対象となります。
Payload URL は、先ほどの API Gateway の URL を設定。
Content-type は、application/json を指定。
Secret は、先ほど取得した Private Access Token を設定。

screencapture-github-organizations-DreamArts-settings-hooks-new-1484453509556.png

動作確認

Issue を登録します。
screencapture-github-DreamArts-SonarTest-issues-new-1484457757976.png

マイルストーンと同名の Project に、カードが追加されました!
screencapture-github-DreamArts-SonarTest-projects-45-1484458000795.png

Issue を クローズします。
screencapture-github-DreamArts-SonarTest-issues-26-1484458220168.png

カードが削除されました!
screencapture-github-DreamArts-SonarTest-projects-45-1484458247040.png

付録

ローカルテスト

ローカルで Lambda 関数を動作させて、挙動を確認できます。

test.js
// For Local Test
// Node.js 4.3.2
const lambda = require("./lambda");

process.env.token = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';

// 下記 X-Hub-Signature の値 sha1= は、GitHub Webhooks の Recent Deliveries 参照
const event = {
  headers: {
    'X-Hub-Signature': 'sha1=127ad3ca94f4e3520fb30fb18b6d36d0b6de28d'
  },
  body: JSON.stringify(require("./payload.json"))
};

const context = {
  succeed: response => {
    console.log(response);
  },
  fail: e => {
    console.log(e);
  }
};

// Lambda 実行
lambda.handler(event, context);
  • process.env.token は、Private Access Token を記載。
  • X-Hub-Signature は Headers の該当箇所をコピペ
  • paload.json は、Payload 部分をコピペしてファイル(payload.json)として保存

screencapture-github-organizations-DreamArts-settings-hooks-11536808-1484529851600.png

Node.js 4.3 で test.js を実行します

$ node test.js

GitHub API

今回使用した、Project関連のAPIをリストしておきます。
GitHub の REST API の特徴は、オブジェクト類は全ユーザでの通し番号になっており、Get と Create での URL のパスが微妙に違う、ということですかね。(少しハマりました。)

Projects

https://developer.github.com/v3/projects/

機能 API
List repository projects GET /repos/:owner/:repo/projects
List organization projects GET /orgs/:org/projects
Get a project GET /projects/:id
Create a repository project POST /repos/:owner/:repo/projects
Create an organization project POST /orgs/:org/projects
Update a project PATCH /projects/:id
Delete a project DELETE /projects/:id

Project columns

https://developer.github.com/v3/projects/columns/

機能 API
List project columns GET /projects/:project_id/columns
Get a project column GET /projects/columns/:id
Create a project column POST /projects/:project_id/columns
Update a project column PATCH /projects/columns/:id
Delete a project column DELETE /projects/columns/:id
Move a project column POST /projects/columns/:id/moves

Project cards

https://developer.github.com/v3/projects/cards/

機能 API
List project cards GET /projects/columns/:column_id/cards
Get a project card GET /projects/columns/cards/:id
Create a project card POST /projects/columns/:column_id/cards
Update a project card PATCH /projects/columns/cards/:id
Delete a project card DELETE /projects/columns/cards/:id
Move a project card POST /projects/columns/cards/:id/moves

おわりに

そもそも、なんで GitHub の機能として、無いの?

続きを読む

NATゲートウェイの設定 / AWSのプライベートサブネットから、インターネットに接続する

◆ 今日やること

AWSのプライベートサブネットから、インターネットに接続する場合、NATゲートウェイが必要です。
プライベートサブネットに配置したEC2インスタンスからyum installとか実行する時に必要ですよね。

NATゲートウェイの設定がないと、インターネットに出て行けません。


$ sudo yum install -y mongodb-org
Loaded plugins: priorities, update-motd, upgrade-helper
amzn-main/latest                                                                          | 2.1 kB     00:00
amzn-updates/latest                                                                       | 2.3 kB     00:00
https://repo.mongodb.org/yum/amazon/2013.03/mongodb-org/3.2/x86_64/repodata/repomd.xml: [Errno 12] Timeout on https://repo.mongodb.org/yum/amazon/2013.03/mongodb-org/3.2/x86_64/repodata/repomd.xml: (28, 'Connection timed out after 30000 milliseconds')
Trying other mirror.

failure: repodata/repomd.xml from mongodb-org-3.2: [Errno 256] No more mirrors to try.

◆ 実装編

> NATゲートウェイの作成

サブネットの選択は、Publicサブネットにしましょう

NATゲートウェイの作成.png

> ルートテーブルの設定をします

0.0.0.0/0の送信先ターゲットを上記で作成したNATテーブルに変更します。

ルートテーブル.png

> 確認

AWSのプライベートサブネットから、インターネットに接続できるようになったかと思います。
以下を実行して、インストールが完了すればOKです。


$ sudo yum install -y mongodb-org

続きを読む

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>

◆ 参考サイト

続きを読む

AWS ECSでDockerコンテナ管理入門(基本的な使い方、Blue/Green Deployment、AutoScalingなどいろいろ試してみた)

はじめに

Dockerを本番環境で利用するに当たり、私レベルではDockerのクラスタを管理することはなかなか難しい訳です。凄くめんどくさそうだし。
ということでAWS ECS(EC2 Container Service)ですよ。

記事書くまでも無いかなと思ったんですけど意外と手順がWEBにない(気がしました)。ということで、今回は社内でハンズオンでもやろうかと思って細かく書いてみました。

こんな感じのシナリオをやってみたいと思います。

  1. Dockerのイメージを用意する
  2. ECSの使い方の基本
  3. コンテナのリリース
  4. Blue/Green Deployment
  5. AutoScaling/ScaleIn

前準備:Dockerのイメージを用意する

FROM uzresk/docker-oracle-jdk
MAINTAINER uzresk

ADD demo-1.0.jar /root/demo-1.0.jar
CMD java -jar /root/demo-1.0.jar
  • Docker build
[root@centos7 build_demo_ver1.0]# docker build -t uzresk/demo:ver1.0 /vagrant/build_demo_ver1.0
Sending build context to Docker daemon 33.11 MB
Step 1 : FROM uzresk/docker-oracle-jdk
 ---> df2f575c2a0d
Step 2 : MAINTAINER uzresk
 ---> Using cache
 ---> 1995a4e99748
Step 3 : ADD demo-1.0.jar /root/demo-1.0.jar
 ---> 705df0209779
Removing intermediate container cd9ef33d8812
Step 4 : CMD java -jar /root/demo-1.0.jar
 ---> Running in b7bd939a8b5a
 ---> add0783a851f
Removing intermediate container b7bd939a8b5a
Successfully built add0783a851f
  • 起動します

    • アプリケーションは8080で起動しているのでポートフォワードしておきます。
[root@centos7 build_demo_ver1.0]# docker run -itd -p 80:8080 --name demo uzresk/demo:ver1.0
92bda2419bf7285d78f12be5877ae3242b5b13ac14409b3c47d38e2d74a06464
  • ブラウザでこんな画面がでれば成功です。

image

  • Dockerhubにコミットしてpushしておきます。
[root@centos7 build_demo_ver1.0]# docker commit -m "update ver1.0" demo uzresk/demo:ver1.0
[root@centos7 build_demo_ver1.0]# docker push uzresk/demo:ver1.0

ECSの使い方の基本

AWS ECSとはなんなのか?

  • 今回は利用手順について書こうと思うので割愛しますが、AWS Black Belt ECSを読むのがよろしいかと思います。

構成する順番を抑えよう

  • こんな感じの順番で構成していきます。大事なので押さえておきましょう。
  1. クラスタの作成

    • クラスタを動かすためのEC2インスタンスの設定を行います。具体的にはインスタンスタイプ、インスタンス数、VPC、サブネットの設定になります。
  2. タスク定義

    • クラスタ上で動かすコンテナの情報を登録します。コンテナイメージのURLやCPU、メモリのハード/ソフト制限、アプリケーションで利用する環境変数の定義などを行います。
  3. ロードバランサの作成

    • クラスタの上位に位置するロードバランサの設定を行います。スケールアウトやスケールインしてもロードバランサはサービスを見つけ出し配下に組み込むことができます。
  4. サービスの作成

    • クラスタとサービスを結びつけるのがサービスの役割です。タスクの最少数やAutoScalingの設定を行ったりできます。
    • 1つのクラスタに複数サービスを登録することももちろん可能です。

それではさっそくクラスタの作成からやってみましょう。

クラスタの作成

image

image

  • 正常に作成されると「クラスターの表示」ボタンが押せるようになります。

image

タスク定義

  • 次はタスクの定義です。タスクでは

image

  • タスク定義名を入力し、「コンテナの追加」をクリックします。

image

image

  • 作成を押せばタスク定義の作成が完了します。

ELBの作成

  • ELBは以下の設定で作っておきましょう

    • ELB名:app-demo-lb
    • 種類:アプリケーションロードバランサ
    • 2つのAZそれぞれのSubnetを指定
    • セキュリティグループで80/HTTPを通すように設定
  • ターゲットグループは以下のようにクラスタで設定したインスタンスIDをそれぞれ登録してください。

image

サービスの作成

  • クラスターのTOPからdemo-clusterを選択し、サービスタブで「作成」

image

  • タスク定義とクラスタ名は自動で埋まりますので、サービス名とタスクの数を設定します。
  • 今回はAZにそれぞれコンテナを作りたいので2としました。

image

  • 画面の下の方にあるELBの追加を選択します。

image

  • ELB名は作成したもの、リスナーポートは80、ターゲットグループ名は作成したものを選択します。

image

image

  • 「作成」を押して、サービスの画面をみるとPENDINGになっています。

image

  • 少し経つとRUNNINGになっている事が確認できると思います。

image

  • ELBのエンドポイント/app/をブラウザで叩くと画面が表示されるはずです。

image

コンテナを落としてみるとどうなるのか

  • タスクの一覧から、タスクを一つ消してみましょう。

image

  • 数十秒後に見てみると別のタスクIDのインスタンスが表示されているはずです。

image

  • コンテナが起動する数十秒間の間はアプリケーションロードバランサが生きているタスクの方にうまくルーティングしてくれるのかな?と思ったら「502BadGateway」というエラーが画面に返ってきました。
  • ここはALBのヘルスチェックの閾値を短くすることである程度は短くできそうです。
  • ここをさらに短くするには、コンテナ自体を軽くすることと、すぐに起動できるアプリケーションを利用するしかなさそうですね。

コンテナのリリース

  • 新しいコンテナをリリースするには、タスク定義に新しいリビジョンを登録し、サービスを更新することで実現可能です。さっそくやってみましょう。

image

  • コンテナのバージョンを2.0にして、新しいリビジョンを登録します。

image

  • 追加されたリビジョンを選択し、アクション→サービスの更新を押します。

image

  • タスク定義に新しいリビジョンが指定されていることを確認して、「サービスの更新」

image

  • サービスの「デプロイ」タブを覗くと、今はVer1.0が2つ動いていることが確認できます。

image

  • コンテナを一つ落としてみましょう

image

image

  • 実行中の数がそれぞれ1になり、タスクの一覧からもそれぞれが動いていることがわかりますね。
    image

image


Blue/Green Deployment

  • Blue/GreenDeploymentでは新しいリビジョンのアプリ用に、新しいインスタンスを構築して入れ替える必要があります。
  • この為のパラメータがサービスのデプロイメントオプションにある最大率(maximumPercent)です。2台の時にこの値を200%にしておくと、4台まで同時に動かしておくことができることを意味します。
  • 4台のインスタンス上で動かすにはECSのインスタンス台数を事前に追加しておくか、AutoScalingさせておく必要があります。もしECSインスタンスが2台の状態で4つのコンテナを動かそうとすると以下のようなメッセージがでてしまいます。(ポートかぶってるから上がらないよ。ってことですね)

  • さっそくやってみます

service demo-service was unable to place a task because no container instance met all of its requirements. The closest matching container-instance xxxxxxxxxxxxxxxxxxxx is already using a port required by your task. For more information, see the Troubleshooting section.

image

image

  • この状態でサービスの更新画面でタスク定義を新しいリビジョンに指定して「サービスの更新」を押してみます。
  • おお。4台分のコンテナが起動しましたね。

image

  • ちょっと経つと(3分ほど?)、古いタスクから削除されていきます・・・・

image

  • 最期は新しいタスク定義しか残らなくなりました。自動ですよ。自動。便利ですねー。

image


AutoScaling/ScaleIn

  • 次はオートスケールとスケールインを試してみます。
  • 通常のオートスケールではインスタンスだけでしたが、インスタンス上で動くコンテナもスケールする必要があります。
  • 今回は2つのインスタンスで2つのコンテナで動いていたものを、負荷をかけることにより4つのインスタンス上に4つのコンテナにスケールアウトさせて、スケールインさせたいと思います。

サービスのAutoScaling設定

  • タスクの最大数を4にして、スケーリングポリシーの追加を押します。

image

  • スケールアウトポリシーの設定を行います。
  • CPU使用率の1分間の平均が20%超えた場合2タスクスケールアウトする設定にしました。

image

  • スケールインポリシーの設定を行います。

image

  • ポリシーが追加できたら「保存」を押しましょう

image

  • ポリシーが追加されていることを確認して、「サービスの更新」を押します。

image

  • これでサービスの設定はおしまいです。

ClusterのAutoScaling/ScaleInの設定

  • ECSインスタンスのオートスケールのポリシーは、EC2インスタンスのAutoScalingGroupの設定で行います。
  • 最大数を4にします。

image

  • Scaleout/ScaleInのポリシーを設定します。
  • サービスの設定と同じく、クラスタのCPU使用率が20%以上だと2台スケールアウトするように設定しました。

image

うごかしてみる

  • ECSインスタンス上でCPU使用率を強引に(openssl speed -multi 1)あげてみたのですがうまく動きませんでした。
  • ありがちですけどabで負荷をかけてみます。
  • abをインストール
sudo yum install -y httpd24-tools
  • 負荷をかける
ab -n 100000 -c 100 http://localhost/app/loginForm
  • CloudWatch Alerm(CPUが20%以上)があがる

image

  • サービスの必要数が4に変更される

image

  • インスタンスがオートスケールする

image

  • タスクが起動する

image

  • 負荷を解除する

  • CloudWatch Alerm(CPUが20%より小さい)があがる

image

  • 必要数が2に変更される

image

  • コンテナとインスタンスが2ずつに変わる

  • ここまでやって思ったんですが、インスタンス→コンテナの順に起動されたんですからコンテナ→インスタンスの順に落としていった方がよさそうですね。それはスケーリングポリシーのクールダウン時間で調整ができそうです。

ECSインスタンスの起動を待つのか?

  • ECSのインスタンスの起動を行った後にコンテナが起動されるので結局時間が掛かってしまいます。負荷の時間が予測されるのであればECSインスタンスを事前に起動しておいた方がECSのスケールアウトが高速にできそうです。
  • Dockerのメリットの一つである起動の高速化のメリットを享受するにはこのスケジューリングがキーになりそうですね。

どのインスタンスがスケールインするのか?


さいごに

ここまでコンテナ管理のハードルが下がると、今後はアプリケーションをコンテナにして配布するのが普通になってくると思います。
そうなるときのためにDockerについてしっかりと理解し、良い設計を心がけたいものですね

続きを読む

Pythonの勉強 Part.1 環境作成編

はじめに

Pythonを仕事で使うことになりそうなので、メモとして残していきます。
既に多くのサイトにある情報ばかりですが、
記事を書くことで、身について行きやすいのでこの場を使用します。

環境作成

python 3.5.0を採用する予定のため、この環境づくりから実施します。

OSのインストール

1年間無料で使用できるAmazon Web Serviceで環境を作成します。
無料で環境構築が簡単なAmazon Linux AMIを選択して作成します。
このあたりことは、別の記事を参考にしてください。

まずは、EC2のインスタンスの作成を行ってください

インスタンス作成が終わったら

  1. ネットワーク & セキュリティ

    1. Elastic IPを選択
    2. 新しいアドレスの割り当てを実施してパブリックIPアドレスを取得
    3. “アクション”メニューのアドレスの関連付けを実行
    4. 作成したインスタンスとプライベートIPアドレスを設定して関連付けします
    5. この作業は、サーバー再起動時にグローバルIPアドレスが変更されないようにする対策です
  2. SSHでリモートログイン
    1. インスタンス作成時にダウンロードしたpemファイルを使ってSSH(私の場合は、teratermを使用しています)でログイン
    2. ec2-userユーザーでログインされます
    3. 念のため、# passwd ec2-userでパスワードを変更
    4. rootのパスワードの変更を実施 # sudo su -でroot化
    5. # passwdでrootのパスワードを変更
    6. 一旦、ログアウト
    7. # su -でログイン
  3. yum update
    1. # yum update -yでシステムのアップデートを実施

python環境を作成する

結構大変でした。。。

ですが、わかってしまえば簡単です。

前提条件:
 ec2-userでpython3.5.0を使用する
 pyenvでpythonを管理する

 ※ pyenvはpython環境を管理するツールです
 ※ pythonは、複数のバージョンを同じOS内で切り替えて使うことができます

  1. pyenvに必要な機能をインストール

    1. rootユーザーになります
    2. yumで必要なものをインストール 
    3. # yum install mlocate openssl-devel bzip2-devel zlib-devel bzip2 bzip2-devel readline-devel sqlite3 sqlite-devel openssl-devel gcc gcc-c++ git
    4. # exitで一旦元のユーザーに戻る
  2. pyenvをインストール # git clone https://github.com/yyuu/pyenv.git ~/.pyenv
  3. # vi .bash_profile でプロファイルに以下を追加

    export PYENV_ROOT=”${HOME}/.pyenv”
    if [ -d “${PYENV_ROOT}” ]; then
    export PATH=${PYENV_ROOT}/bin:$PATH
    eval “$(pyenv init -)”
    fi

  4. # source ~/.bash_profileで読み込み

  5. pythonをインストール # pyenv install 3.5.0

  6. # pyenv versionsで現在の状態を確認

    * system
     3.5.0 (set by /home/ec2-user/.pyenv/version)

  7. pythonのバージョンを変更 # pyenv global 3.5.0

  8. 再度# pyenv versionsで現在の状態を確認

     system
    * 3.5.0 (set by /home/ec2-user/.pyenv/version)

  9. バージョンが変更されていることを確認

  10. python自体でもバージョン確認 # python -V

    Python 3.5.0

  11. 一旦、リモートログアウトして再ログインして、pythonのバージョンを再確認

  12. 問題なければ、これで環境作成終わり

終わりに

インストールの行い方は色々ありますが、これが一番簡単だと思います。

続きを読む