本番環境をGCP/AWSで何個か作り、インフラについて少しわかったこと【GCP環境構築編】

Hakusan Mafiaアドベントカレンダー5日目を余語 [Qiita|facebook|github] が担当します!

本番環境をGCP/AWSで何個か作り、インフラについて少しわかったこと【SSL証明書編】
本番環境をGCP/AWSで何個か作り、インフラについて少しわかったこと【GCP環境構築編】 ←今これ
本番環境をGCP/AWSで何個か作り、インフラについて少しわかったこと【パフォーマンス改善編】

はじめに

以前はAWSでインフラ環境を整えるのがベストプラクティスかと思っていました。というのも、EC2は直感的に触って起動できるし、S3・RDS・ DynamoDMなどの記事も相当Webに溢れていたので簡単でした。ただ、プレスリリースを売ったりメディアに出したりする案件があった際にわざわざAmazonへ申請を出さなければならないといけないらしいということを聞いて少し疑問に感じた時もありました。

そんな時に、AWSと同等のものを用意してくれている。且つ、自動スケーリングが凄い(詳しくいうと、ロードバランサがGoogle検索と同じらしい)という記事を見た時に、少々衝撃を覚えました。

今回の記事では、Google Cloud Platform(GCP)を利用して本番環境を構築する方法を記述します。
基本的には、公式ドキュメント通りにやると全てうまくいくのですが、自分のターミナルから色々いじりたい人用に書いてます。

10分でGCP環境構築

1. ログイン、プロジェクト作成

https://cloud.google.com/?hl=ja の右上でログインし、その後「コンソール」へ入る
プロジェクト作成から、「プロジェクト名」を入力して、準備完了

2. GCEを選択し、VMインスタンス作成

https://gyazo.com/0581708139112c11db672d5cdcfb7ea6

名前は適当で、
- ゾーンを「asia-northeast1-a」
- ブートディスクに今回は、「CentOS」
- ファイアウォールの設定は二つともチェックを入れましょう。

https://gyazo.com/5cc259ec583d4635073f1876cbbd28e6

3. Terminalにてgcloudログイン

今回は、ミドルウェア、とりわけWebサーバーをNginx、ApサーバーをUnicornで実装します。
OSは先ほどCentOSを利用すると選択したので、webにあるDebianでの記事と比較しながらやると勉強になると思います。
(RoRアプリケーションを想定しています。)

ミドルウェアの設定は次の通りです。

ミドルウェア 項目
nginx conn./worker 1024
unicorn worker processes/cpu 2 or 3

インスタンス作成後に、そのVMインスタンスの「接続」から「glcloud コマンドを表示」という箇所でコマンドをコピーして、ローカルで接続して見てください。

local
$ gcloud compute --project "xxxxx-yyy-1111111" ssh --zone "asia-northeast1-a" "xxx"
Last login: Sun Dec  3 08:13:59 2017 from softbankxxxx.bbtec.net

server
[xxxx@yyyy ~]$ #こんな感じでログインできる

先ほどGoogle Compute Engineで作成したVMインスタンスにログインできる。
(ここで弾かれたら、permission関連なのでgithubのSSH and GPG keysという箇所に公開鍵を貼り、ログインしてください。)

gcloudがないと言われたら、下記でインストール&ログイン

local
$ curl https://sdk.cloud.google.com | bash
$ gcloud init

4. 権限管理・ミドルウェアのインストール

4.1 新規ユーザー作成

server

[user_name| ~ ]$ sudo adduser [新規ユーザー名]
[user_name| ~ ]$ sudo passwd [新規ユーザー名]
[user_name| ~ ]$ sudo visudo
----------------------------
root ALL=(ALL) ALL
[新規ユーザー名] ALL=(ALL) ALL # この行を追加
# ↑後々 wheelか何かで管理できるとなお良い
----------------------------
[user_name| ~ ]$ sudo su - [新規ユーザー名]

4.2 VMインスタンス内の環境構築

yum -> node.js -> rbenv/ruby-build -> rubyの順

4.2.1 yum (mysql関連含む)

server
[user_name| ~ ]$ sudo yum install 
                 git make gcc-c++ patch 
                 libyaml-devel libffi-devel libicu-devel 
                 zlib-devel readline-devel mysql-server mysql-devel  -y

4.2.2 node.js

server
[user_name| ~ ]$ sudo curl -sL https://rpm.nodesource.com/setup_6.x | sudo bash -
[user_name| ~ ]$ sudo yum install -y nodejs

4.2.3 rbenv/ruby-build

server
[user_name| ~ ]$ git clone https://github.com/sstephenson/rbenv.git ~/.rbenv
[user_name| ~ ]$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
[user_name| ~ ]$ echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile
[user_name| ~ ]$ source ~/.bash_profile
[user_name| ~ ]$ git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build
[user_name| ~ ]$ rbenv rehash

4.2.4 ruby

server
# centOSを選択した場合
[user_name| ~ ]$ sudo yum -y install bzip2

[user_name| ~ ]$ rbenv install -v 2.4.1
[user_name| ~ ]$ rbenv global 2.4.1
[user_name| ~ ]$ rbenv rehash
[user_name| ~ ]$ ruby -v
ruby 2.4.1p111 (2017-03-22 revision 58053) [x86_64-linux]

4.3 Gitとインスタンスの紐ずけ

ssh接続 → git clone

server
[user_name@vm_name .ssh]$ ssh-keygen -t rsa
[user_name@vm_name .ssh]$ cat config 
Host github
  Hostname github.com
  User git
  IdentityFile ~/.ssh/id_rsa #<- 追加
[user_name@vm_name .ssh]$ ssh -T github
# root権限だとgit cloneできないため
[user_name| ~ ]$ sudo chown user_name www/
[user_name@vm_name www]$ pwd
/var/www
[user_name@vm_name www]$ git clone git@github.com:~~~~

4.4 Nginxを積む

server
[ユーザー名|~]$ sudo yum install nginx
[ユーザー名|~]$ cd /etc/nginx/conf.d/
[ユーザー名|~]$ sudo vi <your_app_name>.conf (小文字でもok)
# default.conf/ssl.confなどと分けるとなお良い。(細分化)

Nginxの設定は最小限です。

/etc/nginx/conf.d/your_app_name.conf
upstream unicorn {
    server  unix:/var/www/<your_app_name>/tmp/sockets/unicorn.sock;
}

server {
    listen       80;
    server_name  <ip address and/or domain name>;

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

    root /var/www/<your_app_name>/public;

    client_max_body_size 100m;
    error_page  404              /404.html;
    error_page  500 502 503 504  /500.html;
    try_files   $uri/index.html $uri @unicorn;

    location @unicorn {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_pass http://unicorn;
    }
}
server
[user_name|~]$ cd /var/lib
[user_name|lib]$ sudo chmod -R 775 nginx #パーミッション調整

4.5 Unicornの導入/設定

server
[user_name@vm_name app_name]$  vi Gemfile
---------------------
group :production do
    gem 'unicorn'
end
---------------------
# gem: command not found
[user_name@vm_name app_name]$ gem install bundler
[user_name@vm_name app_name]$ bundle install
[user_name@vm_name app_name]$ vim config/unicorn.conf.rb
/var/www/app_name/config/unicorn.conf.rb
$worker = 2
$timeout = 30
$app_dir = '/var/www/<App_name>'
$listen  = File.expand_path 'tmp/sockets/unicorn.sock', $app_dir
$pid     = File.expand_path 'tmp/pids/unicorn.pid', $app_dir
$std_log = File.expand_path 'log/unicorn.log', $app_dir
worker_processes  $worker
working_directory $app_dir
stderr_path $std_log
stdout_path $std_log
timeout $timeout
listen  $listen
pid $pid
preload_app true

before_fork do |server, _worker|
  defined?(ActiveRecord::Base) && ActiveRecord::Base.connection.disconnect!
  old_pid = "#{server.config[:pid]}.oldbin"
  if old_pid != server.pid
    begin
      Process.kill 'QUIT', File.read(old_pid).to_i
    rescue Errno::ENOENT, Errno::ESRCH
    end
  end
end
after_fork do |_server, _worker|
  defined?(ActiveRecord::Base) && ActiveRecord::Base.establish_connection
end

4.6 CSS/JSをコンパイル

server
[user_name@vm_name app_name]$ bundle exec rake assets:precompile RAILS_ENV=production

4.7 Unicorn起動

server
[user_name@vm_name app_name]$ bundle exec unicorn_rails -c /var/www/<your_app_name>/config/unicorn.conf.rb -D -E production

4.7 Nginx再起動

server
[user_name@vm_name app_name]$ sudo service nginx reload

これで本番環境が構築できたかと思います。
エラーが出た際は、下記を参考にデバッグをすれば問題ないので、各自調べてください。

nginx   なら /var/log/nginx/error.log
unicornなら  /log/unicorn.log

AWSやVPSで普段環境構築してる人からしたら、ほとんど難しいところはないかと思います。
今後

GKE・k8sによるオートスケールの設定
Dockerによる環境開発改善・デプロイ改善
Cloud Storageによる外部ファイルサーバ利用

などをする際に、便利さを身にしみて感じるのではと思います。
では最後の投稿を楽しみにしていてください。

本番環境をGCP/AWSで何個か作り、インフラについて少しわかったこと【パフォーマンス改善編】

続きを読む

AWS FargateでとりあえずMastodonを動かしてみる

先日こんな記事を見かけて、

Web, Worker, Websocketと要素が揃っているMastodonのスタックをXXで動かしてみる、というのは感触をつかむのによいなと思いました。

で、じゃあAWS Fargateではどうかなと、このようにしてみました。

Amazon ECS .png

事前準備

Dockerコンテナ以外で、マネージドサービスで揃えられるものは優先的にそちらを使用します。

  • AWSで用意

    • VPS (既存でも新規でも)
    • RDS (Postgresで起動)
    • ACMで証明書 (ALBに適用する)
    • ALB
      • TargetGroup-web (HTTP/3000)
      • TargetGroup-stream (HTTP/4000)
    • S3バケット (PAPERCLIP Wikiのポリシー適用が楽)
  • Redis => Redis Labs / 30MBまでならFreeプランでOK
    • ElastiCacheが使いたければそちらでも
  • MailGunアカウント
    • 専用の認証情報をつくるとよいです

ALBは以下のルーティングを最後に割り当てます。

  • / (default)=> TargetGroup-web
  • /api/v1/streaming* TargetGroup-stream

こんな感じです。

EC2 Management Console 2017-12-03 14-04-04.png

Fargateを考慮したTask definitions

事前準備の内容が揃っていれば、Dockerのコンテナとして動かすのは次の3要素。

  • Web(Rails/puma)
  • Sidekiq(Ruby)
  • Streaming(Node.js)

これらをTask definitionsに起こすとして、全部のコンテナ定義を一つにまとめるとスケール変更が大げさになるので、それぞれを別のECSサービスとして登録できるようにバラします。
compose感はなくなりますが、Fargateならその方が良いように思います。

各タスクはmastodonリポジトリのdocker-compose.ymlをベースに、kubedonを参考にしつつアレンジ。

.env.production

大筋として本家のdocker-composeを使いまわすので、もともと用意されている.env.productionから環境変数という仕組みをほぼ踏襲します。
今回このようにしました。

.env.production
RAILS_ENV=production
NODE_ENV=production
REDIS_URL=redis://:<RedisLabsのパスワード>@<RedisLabsのエンドポイント>

DB_HOST=<RDSのエンドポイント>
DB_USER=<RDSのユーザ>
DB_NAME=<RDSのDB名>
DB_PASS=<RDSのパスワード>
DB_PORT=5432

LOCAL_DOMAIN=<ALBに向けるドメイン>
LOCAL_HTTPS=true

PAPERCLIP_SECRET=<rake secretで作成>
SECRET_KEY_BASE=<rake secretで作成>
OTP_SECRET=<rake secretで作成>

VAPID_PRIVATE_KEY=<rake mastodon:webpush:generate_vapid_key で作成>
VAPID_PUBLIC_KEY=<rake mastodon:webpush:generate_vapid_key で作成>

DEFAULT_LOCALE=ja

SMTP_SERVER=smtp.mailgun.org
SMTP_PORT=465
SMTP_LOGIN=< MAILGUNの認証ユーザ >
SMTP_PASSWORD=< MAILGUNのパスワード >
SMTP_FROM_ADDRESS=noreply@< MAILGUNのドメイン >
SMTP_TLS=true

S3_ENABLED=true
S3_BUCKET=< S3のバケット >
AWS_ACCESS_KEY_ID=< S3用のIAMユーザのキー >
AWS_SECRET_ACCESS_KEY=< S3用のIAMユーザのシークレットキー >
S3_REGION=< S3のリージョン >
S3_PROTOCOL=https
S3_HOSTNAME=< S3のリージョン別エンドポイント >

STREAMING_CLUSTER_NUM=1

web

web用にdocker-compose.web.yml, ecs-param.web.ymlを用意します。ecs-cliの取り回しを考えると、ディレクトリ分けて標準のファイル名を使う方が楽だったかも。
また、汎用性を考えたらloggingは分けて、–fileで追加指定してもよいはずです。

docker-compose.web.yml
version: '2'
services:
  web:
    image: gargron/mastodon:v2.0.0
    env_file: .env.production
    command: ["/bin/ash", "-c", "rails db:migrate && rails assets:precompile && rails s -p 3000 -b 0.0.0.0"]
    ports:
      - "3000:3000"
    mem_limit: 2GB
    logging:
      driver: awslogs
      options:
        awslogs-group: mstdn-fargate-demo
        awslogs-region: us-east-1
        awslogs-stream-prefix: web

assign_public_ipはイメージの取得にDockerhubを使う場合ENABLEDでないとPullできません。(※他にルートを用意できればおそらく可)
SGはALBとさえ通信できればOKなので、特にきにせずENABLEDで良いでしょう。

ecs-param.web.yml
version: 1
task_definition:
  ecs_network_mode: awsvpc
  task_execution_role: ecsTaskExecutionRole
  task_size:
    cpu_limit: 512
    mem_limit: 2GB
  services:
    web:
      essential: true
run_params:
  network_configuration:
    awsvpc_configuration:
      subnets:
        - <YOUR_SUBNETa>
        - <YOUR_SUBNETb>
      security_groups:
        - <YOUR_SG>
      assign_public_ip: ENABLED

webは他に比べてスペック高、ほぼasset:precompileのため(メモリが足りないと落ちる)です。

作成したファイルたちを指定してecs-cli composeを使ってサービス登録するにはこんな感じ。
CLUSTER_NAME=既存ECSクラスタの名前、TARGET_GROUP_WEB=参加させるALBのターゲットグループに置き換えで。

$ ecs-cli compose 
  --file docker-compose.web.yml 
  --ecs-params ecs-param.web.yml 
  --project-name mstdn-fargate-web 
  --cluster ${CLUSTER_NAME} 
  service up --launch-type FARGATE 
  --container-name web 
  --container-port 3000 
  --target-group-arn ${TARGET_GROUP_WEB}
  # --timeout 0

--target-group-arnは後から変更できないので、変更が必要な場合はサービスを一旦消すか別名で作成です。

なお実行の際、CPUをケチる(<1024)と--timeout 0を付与しておかないとデフォルト待ちの5分ではassets生成が終わりません。
このあたり考慮すると、実際に運用する場合は起動時間のことも考えて、よそでprecompileしてpublicを別に用意するのが得策でしょう。

streaming

streaming用にdocker-compose.streaming.yml, ecs-param.streaming.ymlを用意します。ecs-cliの取り回し(以下略

docker-compose.streaming.yml
version: '2'
services:
  streaming:
    image: gargron/mastodon:v2.0.0
    env_file: .env.production
    command: ["npm", "run", "start"]
    ports:
      - "4000:4000"
    logging:
      driver: awslogs
      options:
        awslogs-group: mstdn-fargate-demo
        awslogs-region: us-east-1
        awslogs-stream-prefix: streaming

streamingのパラメータ、サイズは最小。FargateはCPUとメモリの組み合わせに制限があるようです。

ecs-param.streaming.yml
version: 1
task_definition:
  ecs_network_mode: awsvpc
  task_execution_role: ecsTaskExecutionRole
  task_size:
    cpu_limit: 256
    mem_limit: 512
  services:
    streaming:
      essential: true
run_params:
  network_configuration:
    awsvpc_configuration:
      subnets:
        - <YOUR_SUBNETa>
        - <YOUR_SUBNETb>
      security_groups:
        - <YOUR_SG>
      assign_public_ip: ENABLED

作成したファイルたちを指定してecs-cli composeを使ってサービス登録するにはこんな感じ。webとほぼおなじですね。
CLUSTER_NAME=既存ECSクラスタの名前、TARGET_GROUP_STREAMING=参加させるALBのターゲットグループに置き換えで。

$ ecs-cli compose 
  --file docker-compose.streaming.yml 
  --ecs-params ecs-param.streaming.yml 
  --project-name mstdn-fargate-streaming 
  --cluster ${CLUSTER_NAME} 
  service up --launch-type FARGATE 
  --container-name streaming 
  --container-port 4000 
  --target-group-arn ${TARGET_GROUP_STREAMING}

sidekiq

sidekiq用にdocker-compose.sidekiq.yml, ecs-param.sidekiq.ymlを用意します。ecs-cliの取り回し(以下略

docker-compose.sidekiq.yml
version: '2'
services:
  sidekiq:
    image: gargron/mastodon:v2.0.0
    env_file: .env.production
    command: ["sidekiq", "-q", "default", "-q", "mailers", "-q", "pull", "-q", "push"]
    logging:
      driver: awslogs
      options:
        awslogs-group: mstdn-fargate-demo
        awslogs-region: us-east-1
        awslogs-stream-prefix: sidekiq

こちらも最小でOK。

ecs-param.sidekiq.yml
version: 1
task_definition:
  ecs_network_mode: awsvpc
  task_execution_role: ecsTaskExecutionRole
  task_size:
    cpu_limit: 256
    mem_limit: 512
  services:
    sidekiq:
      essential: true
run_params:
  network_configuration:
    awsvpc_configuration:
      subnets:
        - <YOUR_SUBNETa>
        - <YOUR_SUBNETb>
      security_groups:
        - <YOUR_SG>
      assign_public_ip: ENABLED

CLUSTER_NAME=既存ECSクラスタの名前に置き換えで。

$ ecs-cli compose 
  --file docker-compose.sidekiq.yml 
  --ecs-params ecs-param.sidekiq.yml 
  --project-name mstdn-fargate-sidekiq 
  --cluster ${CLUSTER_NAME} 
  service up --launch-type FARGATE

ECS状況

$ ecs-cli ps --cluster ${CLUSTER_NAME}
Name                                            State                                                                               Ports                          TaskDefinition
775a399c-19e1-4e2a-bd3d-98f8b4d7304e/streaming  RUNNING                                                                             34.204.191.189:4000->4000/tcp  mstdn-fargate-streaming:1
921f2d1a-0e4f-49a6-a5b5-434e2b91cf11/sidekiq    RUNNING                                                                                                            mstdn-fargate-sidekiq:2
edb1ba2e-533d-4afd-aaee-39db59ee616a/web        RUNNING                                                                             34.204.174.122:3000->3000/tcp  mstdn-fargate-web:15

$ aws ecs list-services --cluster ${CLUSTER_NAME} 
{
    "serviceArns": [
        "arn:aws:ecs:us-east-1:xxxxxxxxxxxx:service/mstdn-fargate-web", 
        "arn:aws:ecs:us-east-1:xxxxxxxxxxxx:service/mstdn-fargate-sidekiq", 
        "arn:aws:ecs:us-east-1:xxxxxxxxxxxx:service/mstdn-fargate-streaming"
    ]
}

それぞれ別のECSサービスとして動かせたので、個別にコンテナ数を変更できて良い感じのはず。

Mastodon 2017-12-03 14-02-01.png

Mastdon admin登録

さて、Mastodonの管理者になるにはmastodon:make_adminタスクを実行すればよいのですが、Fargate上のDockerコンテナで何か実行(docker exec相当)できるのだろうか。

さっとアタッチはできないぽいので、これもtask definitionからやりますか。

yamlをかいてー

docker-compose.web_make_admin.yml
version: '2'
services:
  web:
    image: gargron/mastodon:v2.0.0
    env_file: .env.production
    command: ["/bin/ash", "-c", "rails mastodon:make_admin USERNAME=sawanoboly"]
    mem_limit: 512MB
    logging:
      driver: awslogs
      options:
        awslogs-group: mstdn-fargate-demo
        awslogs-region: us-east-1
        awslogs-stream-prefix: railstask

ecs-cli compose upと。

$ ecs-cli compose 
  --file docker-compose.web_make_admin.yml 
  --ecs-params ecs-param.web.yml 
  --project-name mstdn-fargate-makeadmin 
  --cluster ${CLUSTER_NAME} 
  up --launch-type FARGATE

で、管理メニューアクセスオープン。

レポート - Mastodon 2017-12-03 14-34-44.png

少々手間だが、できるのでよいです。

おわりに

これまではスケール変更を柔軟にしようと思ったら結局ECSクラスタなりk8s、docker swarmなどのノードを確保しておく必要があったことを考えると、リソース配分が目に見える分だけになるのでシンプルです。

Fargateで起動するコンテナと通常のEC2インスタンスを比較すると、数字上で同等のCPU/メモリのインスタンスを維持するよりFargateのほうがちょっと割高になるはずです。
しかし、EC2ではサービス用のプロセス以外にも色々動かしたりケアする場所も多いため、コンテナより余分に性能やらディスクやらが必要になります。Fargateは下限ギリギリを攻めてもよい分、トータルでどっちがよいかは動かしたいものによるでしょう。

スケールアウトの対応速度は比較にならないほどFargateの方がはやいです。※この記事の例での構成ではコンテナイメージの最適化をしてないので少々もたもたしますが。

Fargate専用のSpotFleetでないかなあ。

続きを読む

サーバ、インフラ経験ゼロの初心者がAWSで「【非公式】Unity JP Mastodon」を作ってみた

Mastodon Advent Calendar 2017 3日目の記事になります。

【非公式】Unity JP Mastodonの管理人、荻野です。
logo.svg

今回はサーバー、インフラの実装経験ゼロの超初心者が、ネットに掲載されている情報のみで生まれて初めて触ったAWSを利用して、Mastodonインスタンスを組み立てたことを書いていきます。

サーバー、インフラ初心者故に引っかかった苦労点や、AWSに全く詳しくなかったために頭から地雷にダイブするなど、バカバカしいお話でも書いていきたいと思います。

ソースだけ読みたい方はこちらから見れます。

Mastodonインスタンスを立てる上で参考にさせていただいたサイト

一番お世話になったのが以下のサイトです。インスタンス作成時は、これをそのまま使わせていただきました。

マストドンAWS構築チュートリアル完全版|初心者から大規模運用まで

簡単な自動メンテナンス処理も記載されているのが本当にありがたかったです。
5月のゴールデンウィークに当時v1.3.2だったMastodonインスタンスを立てたのですが、ほぼこのサイトに書いてある手順をほぼそのままなぞったらMastodonインスタンスがAWSで動きました。

※現状のv2.0以上だと、一部ソフトウェアのバージョンをアップしないとビルドが通らないことがあります

他に参考にさせていただいたサイトをいくつか掲載します
AWSの無料SSLを使ってmastodonインスタンスを立てる手順
AWSでMastodonサーバー立てました chitose.moe
AWSのEC2で最小限の努力でmastodonを構築する
マストドンのインスタンスを構築するドン
Mastodon インスタンスを運用しているサーバを HTTP/2 対応させた話 (CentOS7 + nginx + OpenSSL)

AWS設定

Mastodonインスタンスを立てるにあたり、使用したAWSサービスは以下の通りです。

・EC2インスタンス
OS:ubuntu16.04
タイプ:t2.micro

インスタンス作成時のタイプ:t2.medium
※t2.microだとアセットの初回ビルドでメモリの上限を超えて落ちるため

・RDS
エンジン:PostgreSQL
クラス:db.t2.micro
ストレージ:20GB

・ElastiChache
タイプ:cache.t2.micro

・VPC
・S3
・CloudWatch
・CloudFront
・Route 53
・Certificate Manager

※ほぼ全て無料枠に抑えています。

無料枠でおさめられれば、費用はだいたい100円以下となっています。
画像をのぞいて、Unityリンク集やお知らせ表示の簡単なカスタマイズを入れています。

スクリーンショット 2017-12-02 21.25.28.png

現バージョン(v2.0以上)でMastodonインスタンスをAWSで動かしたい場合

Mastodonは日々高速で改良が加えられているため、2017年4月前に記載されたマストドンAWS構築チュートリアル完全版を見て新規にMastodonインスタンスを組み立てる場合、いくつか引っかかる点と当時未実装だった機能があります。

dockerのバージョンが1.13以上であることが必須

v1.4.1からdockerが1.13以上でないと、ビルドが通らないように変更されています。

現在、非公式のUnity Mastodonのバージョンアップ中
どうやら1.4.1はdockerが1.13以上でないとビルドできないようになっているっぽいな

— 荻野雄季@デジゲー博X-03ab (@YuukiOgino) 2017年6月14日

バージョンが1.13より低い場合、公式ドキュメントを見て、docker ceをインストールしましょう。

プッシュ通知(VAPIDキー)

プッシュ通知はv1.5.0から新規に追加されました。
そのため、プッシュ通知に対応する場合は以下の作業が追加されます。

1.以下のコマンドを叩いて秘密鍵/変数を生成します
RAILS_ENV=production bundle exec rake mastodon:webpush:generate_vapid_key
(ドッカーの場合、docker-compose run –rm web rake mastodon:webpush:generate_vapid_key)

2.生成した秘密鍵/変数を設定ファイルにセット
大抵の人が.env.productionと名付けるファイルで、以下の定数に生成した秘密鍵と変数をセットします。

VAPID_PRIVATE_KEY =
VAPID_PUBLIC_KEY =

生成さえすれば、どちらがどのKEYに対応しているかはわかると思います。

AWS(インフラ)初心者ゆえやらかした失敗

AWSは2017年5月のGWで初めて触ったため、いくつかバカバカしい失敗をしました。
自分への戒めとして、晒していきます。

RDSとElastiCacheを本番稼動用のデフォルト設定のまま本番公開した

はい、Mastodonインスタンス作成時に初めてAWSに触ったため、ろくに調べずに無知故にやらかした失敗です。

これ、何が失敗かというと、最大でも一日あたりのアクティブユーザーが50人以下のコンテンツなのに、明らかに超オーバースペックな上に壮大に無駄な費用が発生するモンスタースペックな構成に知らず知らずにしてしまったことです。

1.ElastiCache
ElastiCacheのデフォルトだとシミュレーションができなかったので、大雑把な計算をしてみました。

ノードのタイプ:cache.r4.large(メモリ12.3GB) 時間で$0.288の費用発生
レプリケーション数:2

大雑把に料金を計算すると$0.288×2×24×31=$428.544(約48,083円)
明らかに不必要な投資です。

2.RDS
こちらは実際のデフォルト設定からある程度シミュレーションで計算ができました。
スクリーンショット 2017-12-02 19.07.27.png

なんと驚愕の月額$1386.07(約155,521円)!
明らかに過剰投資です。

これは現在の料金設定からの計算であり、当時は1日1万ずつ自動課金されるぐらいの消費をしていました。
構築時は設定した時間が3〜4時間程度な上、請求の欄を見ていなかったために異常な料金の上がり方に気づいていませんでした。

リリースしてから1日後、請求額を見たら明らかに想定と違う料金の上がり方をしていることに気づき、料金を調査して発覚しました。これをどうにかしないと毎月20万消える恐ろしいモノになるため、早期に構成を変更せざるを得ませんでした。

この時に初めてAWSの仕様として、容量は簡単に上げることは可能だが、容量を減らすことはできないことを知り、改めてインスタンスを作り直さないといけないことになりました。

ElastiCacheの再設定

Mastodonはサーバーキャッシュは全て捨てても、直近で影響が出るのがホームのページの表示ぐらいというのが検証してわかったため、完全に付け替える方針にしました。
超小規模であればレプリケーション数は1、メモリ0.5GBでも十分と判断し、ElastiCacheは無料枠に収めるインスタンスを改めて作成し、付け替えました。

無事に接続を確認後、金食い虫のインスタンスは即刻削除して料金を発生しないようにしました。

RDSの再設定

一番困ったのがDB、RDSの再構築です。一度世間に解放してしまった以上、当時90人弱のユーザー登録があったので破棄してしまうと再度登録させるという非常に面倒な手間を強いることになるため、DBをそのまま移植しないといけません。

仕事では基本的にSQLでの流し込みぐらいしかしてなかったため、時間が過ぎるごとに課金額があがる恐怖と戦いながらネット上でRDSのバックアップと復旧方法を検索しました。

最終的に以下の方法でDBの移植を行いました。

1.dumpファイル作成

まずdumpファイルを作成します。叩いたコマンドは以下の通りです。

psql -f [作成するdumpファイル名] --host=[RDSエンドポイント]  --port=[ポート] --username=[ユーザー名] --password --dbname=[DB名]

2.dumpから復旧

無料枠で収まるRDSインスタンスを作成後、以下のコマンドを実行しました。

sudo pg_restore -c -h [RDSエンドポイント] -U [ユーザ名] -d [DB名] [dumpファイル] 

詳しくはPostgreSQL 9.2.4文書をご覧ください。

あとは新規に作成したRDSに接続するように設定を変更し、無事トゥート一覧やユーザーリストが表示されていることを確認後、古いインスタンスを即時削除しました。

これでなんとかDBの復旧ができました。
頭ではDBはバックアップとって復旧する、ということをやらなければいけないとはわかってましたが、dumpファイルからの復旧は初めての作業だったため、非常にテンパった覚えがあります。

久しぶりに頭をフル回転させましたw

S3をダイレクトに接続していた結果、通信プロトコルがHTTP1.1形式でページの表示が遅かった

これもやらかしの一つです。ドキュメントを読めば明らかなのですが、S3の通信プロトコルはHTTP/1.1のため、せっかくHTTPSで接続しているのにHTTP/2.0にしていないために非常に無駄な通信コストが発生していました。

CloudFrontを経由すればS3でもHTTP/2.0対応ができることを知ったため、早速試してみました。

1.ドメイン&証明書発行

S3用のドメインと証明書を発行する必要があるため、AWSの証明書で作成します。
mastodonインスタンス作成時と同様、証明書のリクエストを行います。このドメインさえ確保できれば、あとはCloudFrontでインスタンスを作成するのみです。
スクリーンショット 2017-12-02 20.41.29.png

2.CloudFrontインスタンス作成

まずインスタンスを作成します。上のWebから作成します。
スクリーンショット 2017-12-02 20.34.50.png

Create Distribution画面が出てくるので、Origin Domain NameにS3のバケットをセットします。

スクリーンショット 2017-12-02 20.35.23.png

続いて、SSL CertificateでCustom SSL Certificateを選択し、先ほど発行したドメインをセットします。

スクリーンショット 2017-12-02 20.36.40.png

最後、HTTP VersionsがHTTP/2をサポートする設定になってるか確認し、インスタンスを作成します。
スクリーンショット 2017-12-02 20.35.59.png

最後、このドメインをS3_BUCKETに設定すれば、HTTP/2.0対応完了となります。
HTTP/2.0に変更後、画像等のアセットダウンロードが体感速度的に2倍ぐらい早くなった気がします。

参考に、画像がたくさん上がってくるPawooのインスタンスユーザーを表示した上での速度です。

スクリーンショット 2017-12-02 20.58.30.png

※すみません、データ取る前に切り替えてしまったので、変更前の具体的な数値が出せないです

33の画像を読み込んだ上での速度です

一見わかりにくくて申し訳ないですが、これはAWSで全て無料枠で収めてこの速度が出るということなので、EC2やElastiCacheの性能をあげればもっと早くなると思います。

Mastodonインスタンスを組み立てる上で役だったスキル

私は一応、会社で某ソーシャルゲームのWebフロントエンジニアのため、会社で(嫌でも)以下の経験をしたのがMastodonを立てる時に役立ちました。

・簡単なLinuxコマンドとコマンドライン

大体本番で使われているWebサーバーはLinuxのCUIベースだったので、フロントのソースをアップするために何回かviを編集していたこと、シンクコマンドを叩いていたのが役に立ちました。(GUIばっかり触ってると、CUIだとわかりにくい部分があるため)
Linuxでサーバーを立てる場合、ある程度Linuxコマンドを叩いて慣れておいたほうがいいと思います。

MastodonとAWSを触ったり、地雷を踏んだりして得られたスキル

最後のまとめです。
インスタンス作成時はAWSを触って4日、色々と失敗をやらかして得られた経験は以下の通りです。

・RDS(PostgreSQL)でのバックアップ復旧作業
・AWSにおけるサーバー環境構築
・Mastodonインスタンス構築の知見
・サーバーコストへの意識
・ユーザーがいる状況での本番環境のバージョンを更新するという恐怖と覚悟

とくによかったと思えるのはサーバーコストへの意識です。
実際に立てたからわかることですが、サーバーコストを適切にしないと無駄な費用が発生して回らなくなるというのをよく理解しました。
実質、フロント部分しか触ってないと、会社環境によってはこの辺りの意識がないところもあるので、改めてサーバーコストの意識が高まったのはやってよかったと思います。

本番環境の更新は毎回冷や汗を書きます。なぜなら会社では万が一が起きても頼れる人がいますが、このインスタンスで頼れるのは自分一人です。万が一が発生した場合、ネット上で情報がなければお手上げです。
謎のエラーでバージョンが更新できずに起動できない(だいたいコンフリクト放置が原因)ときは本当にあせります。

かなり必死で頭を回してるからこそ、動いた時に得られる経験値というのは相当なものだと感じています。
もし、Web系の会社に行きたいと考えている人は、mastodonインスタンスを立てて運営してみると、色々と実践的な経験を短期間で詰めるかもしれません。

明日はxserverさんによる「インスタンス運用アンチパターン」です。楽しみです!!

続きを読む

AWS Elastic BeanstalkでRails5アプリをデプロイする際の注意点(後編)

この記事

こちらの前編の続きです。

再デプロイエラー

上記の記事で初回デプロイまでは難なくできましたが、再デプロイが全然上手くいきませんでした。
出ていたエラーが下記のようなものです。(文字化けはご容赦ください。)

Webpacker is installed 脂 魂
  Using /var/app/ondeck/config/webpacker.yml file for setting up webpack paths
  Compiling窶ヲ
  Compilation failed:
  yarn run v1.2.1
  $ /var/app/ondeck/node_modules/.bin/webpack --config /var/app/ondeck/config/webpack/production.js
  info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

 warning From Yarn 1.0 onwards, scripts don’t require “--” for options to be forwarded. In a future version, any explicit “--” will be forwarded as-is to the scripts.
  fs.js:1331
      writeAll(fd, isUserFd, buffer, 0, buffer.length, position, callback);
                                              ^

 TypeError: Method get TypedArray.prototype.length called on incompatible receiver [object Object]
      at Buffer.get length [as length] (native)
      at writeFd (fs.js:1331:45)
      at fs.js:1322:7
      at FSReqWrap.oncomplete (fs.js:123:15)
  error Command failed with exit code 1.
   (Executor::NonZeroExitStatus)

状況としては書き込み時にメモリなどの問題で失敗しているようでした。
私の環境ではVue.jsを使ってほぼSPAに近い状態だったので.vueファイルなどをコンパイルしたファイルが数MBとかなり大きくなっていたことが原因のようでした。

暫定策

プレコンパイルをローカルで実施することにしました。

インスタンスサイズにもよるのでしょうが、この方が格段にデプロイが早かったです。
結果的には下記のようなファイルで落ち着きました。

01_yarn.config
commands:

  01_node_get:
    cwd: /tmp
    command: 'sudo curl --silent --location https://rpm.nodesource.com/setup_6.x | sudo bash -'

  02_node_install:
    cwd: /tmp
    command: 'sudo yum -y install nodejs'

  03_yarn_get:
    cwd: /tmp
    # don't run the command if yarn is already installed (file /usr/bin/yarn exists)
    test: '[ ! -f /usr/bin/yarn ] && echo "yarn not installed"'
    command: 'sudo wget https://dl.yarnpkg.com/rpm/yarn.repo -O /etc/yum.repos.d/yarn.repo'

  04_yarn_install:
    cwd: /tmp
    test: '[ ! -f /usr/bin/yarn ] && echo "yarn not installed"'
    command: 'sudo yum -y install yarn'

  05_mkdir_webapp_dir:
    command: mkdir /home/webapp
    ignoreErrors: true

  06_chown_webapp_dir:
    command: chown webapp:webapp /home/webapp
    ignoreErrors: true

  07_chmod_webapp_dir:
    command: chmod 700 /home/webapp
    ignoreErrors: true
03_conteiner_comand.config
container_commands:
  01-bundle_install:
    command: bundle install --path vendor/bundle
  02-db_migrate:
    command: bundle exec rake db:migrate
  03-db_seed:
    command: bundle exec rake db:seed

その上で、環境変数は下記のようにしています。(実行時は1行)
()の中は適宜自分の環境に読み変えてください。

eb setenv
RDS_PORT=3306
SECRET_KEY_BASE=<secret_key>
RAILS_SKIP_ASSET_COMPILATION=true
RDS_PASSWORD=<pass>
RDS_DB_NAME=ebdb
RACK_ENV=production
RDS_USERNAME=<user_name>
BUNDLE_WITHOUT=test:development
RAILS_SKIP_MIGRATIONS=true 
RDS_HOSTNAME=<hogehoge.rds.amazonaws.com>
-e <environment_name>

デプロイ手順は下記。

bundle exec rake assets:precompile assets:clean RAILS_ENV=production SECRET_KEY_BASE=<secret_key>
git add .
eb deploy <environment_name> --staged

最後に

AWS Elastic BeanstalkでRailsアプリをデプロイする際の参考になれば幸いです。
Railsはまだ初心者のため、色々ご指摘ありましたら優しく教えてください。

続きを読む

マストドンを2.0.0へバージョンアップを行う

概要

既に動いているインスタンスを2.0.0へ上げる
動かし方としてはEC2上から docker-compose up -d でそのまま使っている。
バージョンを上げるついでに画像データはS3へ、DBはRDSへ同時に移行させる。

環境

$ cat /proc/version
Linux version 4.4.0-1039-aws (buildd@lcy01-02) (gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.4) ) #48-Ubuntu SMP Wed Oct 11 15:15:01 UTC 2017
$ cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.2 LTS"
$ docker -v
Docker version 17.05.0-ce-rc3, build 90d35ab
$ docker-compose -v
docker-compose version 1.12.0, build b31ff33
$ git describe --tags #マストドン
v1.4rc3-24-gbbc3db8

やったこと

RDSの作成

  • 時間がかかるので一番最初に作成してしまう。
  • 何らかの方法で適宜RDS(PostgreSQL)を作成。
    • 特に設定はしていない。文字コードぐらい?

S3の作成

  • コンソールからS3を作成
  • バケットポリシーを変更
バケットポリシー
{
  "Version":"2012-10-17",
  "Statement":[
    {
      "Sid":"PublicS3Objects",
      "Effect":"Allow",
      "Principal": "*",
      "Action":["s3:GetObject"],
      "Resource":["arn:aws:s3:::<YOUR BUCKET NAME>/*"]
    }
  ]
}
  • S3へ既存の画像データをアップロード

    • $ aws s3 sync ./public/system/ s3://<YOUR BUCKET NAME>/

既存DBのダンプを取得

  • docker内からダンプを取得するため、 docker-compose.yml を書き換える

    • dockerとバージョンの合ったpsqlコマンドを取得するのが面倒なため中から実行する。
  db:
    restart: always
    image: postgres:9.6-alpine
### Uncomment to enable DB persistance
    volumes:
      - ./postgres:/var/lib/postgresql/data
      - ./dump:/dump
  • ダンプを取得する

    • $ docker-compose run --rm db pg_dump -h localhost -p 5432 -U <USER NAME> <DB NAME> > /dump/dump.sql
  • 取得したダンプをRDSへ流し込む
    • $ psql -h <RDS ENDPOINT> -U <USER NAME> <DB NAME> < ./dump/dump.sql

リポジトリをクローンし直す

  • 現在動いているマストドンを停止させる

    • $ docker-compose stop
  • 適当なディレクトリに移動し、マストドンをcloneする

    $ cd /opt
    $ git clone https://github.com/tootsuite/mastodon
    $ cd mastodon
    

.env.productionの編集

  • 運用していたマストドンディレクトリから .env.production を取得

    • $ cp /path/to/mastodon/.env.production ./
  • .env.production を編集

    • 編集するのは DB パラメータと、 S3 パラメータ。
    • S3はENDPOINTは以下の通り編集する必要あり
      • ex) S3_ENDPOINT=https://s3-ap-northeast-1.amazonaws.com
  • VAPIDを埋める
    • $ docker-compose run --rm web rake mastodon:webpush:generate_vapid_key を実行して出力されたパラメータをコピペ

DBのマイグレーションとアセットのコンパイル

  • DBのマイグレーションの実行とアセットのコンパイル

    • $ docker-compose run --rm web rake db:migrate; docker-compose run --rm web rake assets:precompile

マストドンの起動

  • $ docker-compose up -d

参考

続きを読む

AWS Elastic BeanstalkでRails5アプリをデプロイする際の注意点

この記事

Railsで作ったアプリをBeanstalkで公開する手順を後で見返せるようにメモ。

環境(RubyとBundlerは執筆時のBeanstalkが対応している最新版)

  • Rails5
  • Ruby 2.4.2
  • Bundler 1.15.0
  • webpaker 3.0.2

事前準備

IAMの設定やAWS CLIの設定とElastic Beanstalk用のCLIのダウンロードなどは終わっていることとします。
下記の記事が設定周りまでわかりやすかったです。
Ruby on Railsの環境構築をElastic Beanstalkで行う

手順

初期化

eb.initコマンドで初期化すると下記の項目を聞かれるので適宜設定。

  • default regionの選択
  • アクセスキーとシークレットキーを登録
  • アプリケーション名の登録(AWSコンソール画面に表示されるもの)
  • 環境の選択(ruby2.4(Puma)を選択しました)
  • CodeCommitを利用するか(最初はnoの方がやりやすいかと)
  • 立ち上げるEC2にssh接続するか(yesにするとkey pair nameとかパスを聞いてくれて自動で公開鍵のアップロードまでしてくれるので楽)

環境の作成

eb createで環境の作成が可能。

  • 環境名。デフォルトはアプリケーション名-devとなる。
  • DNS CNAME prefix(デフォルトは環境名と一緒)
  • ロードバランサーのタイプ(クラシック/アプリケーション/ネットワークを選べる)

デプロイ

eb deployでデプロイ可能。

→ただし、このままではDBも設定していないので動きません。

Ruby on Railsの環境構築をElastic Beanstalkで行う

こちらの記事を参考にRDSの作成をやりましょう。

エラーログなどを見たい場合はeb logsで見れます。
AWSのコンソール画面からも見れます。

注意点やハマったポイント

ebとgitの関係性

git commitしていないとデプロイには反映されないです。
ebのymlの設定でsc: gitとなっているため、変更の管理がgit配下と同じ扱いなんですね。

commandsの設定

自分で .ebextensionsフォルダを作成して、hogehoge.configのようなファイル名で保存し、そこの中にyamlで記述する必要があります。
option関連は、.elasticbeanstalk/config.ymlに追記しても実行されないです。
自分がうまくいった環境はこちらのような内容です。
Deploy on AWS Beanstalk
ここの情報のままでうまくyarnがinstallされました。

yarn.config
files:
  # If this file is edited, it must be removed from EC2 instance prior to deploy.
  "/opt/elasticbeanstalk/hooks/appdeploy/pre/09_yarn_install.sh" :
    mode: "000775"
    owner: root
    group: users
    content: |
      #!/usr/bin/env bash

      set -xe

      EB_APP_STAGING_DIR=$(/opt/elasticbeanstalk/bin/get-config container -k app_staging_dir)
      EB_APP_USER=$(/opt/elasticbeanstalk/bin/get-config container -k app_user)

      echo "I am: `whoami`"
      echo "App user is $EB_APP_USER"

      # If yarn is not detected, install it.
      if which yarn; then
        echo "Skipping installation of yarn -- yarn already installed."
        echo "yarn --version: `yarn --version`"
      else
        echo "which yarn: `which yarn`"
        echo "Yarn is not installed and accessible."
        echo "Installing yarn..."
        # Consider that the EC2 instance is managed by AWS Elastic Beanstalk.
        # Changes made via SSH WILL BE LOST if the instance is replaced by auto-scaling.
        # QUESTION: Will this script be run on new instances that are created by auto-scaling?
        # QUESTION: Should installation be moved to a rake task?

        # Download the yarn repo
        sudo wget https://dl.yarnpkg.com/rpm/yarn.repo -O /etc/yum.repos.d/yarn.repo
        # Confirm that it downloaded
        file /etc/yum.repos.d/yarn.repo

        # If node is not detected, install it.
        if [ `node --version` == 'v6.10.0' ]; then
          echo "Skipping installation of node -- node already installed."
          echo "node --version: `node --version`"
        else
          echo "Installing Node v6.10.0 ..."
          # Download the Node v6 setup script
          curl --location https://rpm.nodesource.com/setup_6.x > /home/ec2-user/node_install.sh
          # Confirm that it downloaded
          file /home/ec2-user/node_install.sh
          # Run the Node v6 setup script
          sudo bash /home/ec2-user/node_install.sh
          # Install nodejs
          sudo yum install -y nodejs
          node --version
          echo "... and finished installing Node v6.10.0"
        fi

        # install yarn
        sudo yum install -y yarn
        yarn --version

        echo "... and finished installing yarn."
      fi

      echo "Change directory to $EB_APP_STAGING_DIR"
      cd $EB_APP_STAGING_DIR

      # yarn install
      echo "Running yarn install."
      ./bin/yarn install

database名

上記で紹介した記事のように、ElasticBeanstalkのコンソール上からRDSを作成するとDB名は勝手にebdbになってしまいます。
変える方法はめんどくさそうなので試してません。(もしかするとできないかもしれないです。)

DBの文字コード

上記方法で作るとDBの文字コードが下記のようになってしまいます。

'character_set_client', 'utf8'
'character_set_connection', 'utf8'
'character_set_database', 'latin1'
'character_set_filesystem', 'binary'
'character_set_results', 'utf8'
'character_set_server', 'latin1'
'character_set_system', 'utf8'
'character_sets_dir', '/rdsdbbin/mysql-5.6.37.R1/share/charsets/'

日本語とかを入れようとするとエラーになってしまうので、手動でUTF-8に変更しました。

ALTER DATABASE ebdb default character set utf8;
ALTER TABLE テーブル名 CONVERT TO CHARACTER SET utf8;

dbのマイグレーションなど

bundleのinstallやdbのマイグレーションはcontainer_commandsというセクションで行います。
結果、configファイルは下記のような感じに落ち着きました。

container_commands.config
container_commands:
  01-bundle_install:
    command: bundle install --path vendor/bundle
  02-db_migrate:
    command: bundle exec rake db:migrate
  03-db_seed:
    command: bundle exec rake db:seed
  04-assets_precompile:
    command: bundle exec rake assets:precompile
  05-chown:
    command: chown -R webapp:webapp ./

画像のファイルパス

assets/imagesに入れている画像の参照パスはproduction環境とローカル環境で変わってしまいます。
nginxの設定にもよりますが、自分の環境では下記のようなwebpackerの設定でうまくいきました。

webpacker.yml
default: &default
  source_path: app/javascript
  source_entry_path: packs
  public_output_path: packs

production:
  <<: *default

  public_output_path: assets

最後に

AWS Elastic BeanstalkでRailsアプリをデプロイする際の参考になれば幸いです。
Railsはまだ初心者のため、色々ご指摘ありましたら優しく教えてください。

参考

続きを読む

ECS運用のノウハウ

概要

ECSで本番運用を始めて早半年。ノウハウが溜まってきたので公開していきます。

設計

基本方針

基盤を設計する上で次のキーワードを意識した。

Immutable infrastructure

  • 一度構築したサーバは設定の変更を行わない
  • デプロイの度に新しいインフラを構築し、既存のインフラは都度破棄する

Infrastructure as Code (IaC)

  • インフラの構成をコードで管理
  • オーケストレーションツールの利用

Serverless architecture

  • 非常中型プロセスはイベントごとにコンテナを作成
  • 冗長化の設計が不要

アプリケーションレイヤに関して言えば、Twelve Factor Appも参考になる。コンテナ技術とも親和性が高い。

ECSとBeanstalk Multi-container Dockerの違い

以前に記事を書いたので、詳しくは下記参照。

Beanstalk Multi-container Dockerは、ECSを抽象化してRDSやログ管理の機能を合わせて提供してくれる。ボタンを何度か押すだけでRubyやNode.jsのアプリケーションが起動してしまう。
一見楽に見えるが、ブラックボックスな部分もありトラブルシュートでハマりやすいので、素直にECSを使った方が良いと思う。

ALBを使う

ECSでロードバランサを利用する場合、CLB(Classic Load Balancer)かALB(Application Load Balancer)を選択できるが、特別な理由がない限りALBを利用するべきである。
ALBはURLベースのルーティングやHTTP/2のサポート、パフォーマンスの向上など様々なメリットが挙げられるが、ECSを使う上での最大のメリットは動的ポートマッピングがサポートされたことである。
動的ポートマッピングを使うことで、1ホストに対し複数のタスク(例えば複数のNginx)を稼働させることが可能となり、ECSクラスタのリソースを有効活用することが可能となる。

※1: ALBの監視方式はHTTP/HTTPSのため、TCPポートが必要となるミドルウェアは現状ALBを利用できない。

アプリケーションの設定は環境変数で管理

Twelve Factor Appでも述べられてるが、アプリケーションの設定は環境変数で管理している。
ECSのタスク定義パラメータとして環境変数を定義し、パスワードやシークレットキーなど、秘匿化が必要な値に関してはKMSで暗号化。CIによってECSにデプロイが走るタイミングで復号化を行っている。

ログドライバ

ECSにおいてコンテナはデプロイの度に破棄・生成されるため、アプリケーションを始めとする各種ログはコンテナの内部に置くことはできない。ログはイベントストリームとして扱い、コンテナとは別のストレージで保管する必要がある。

今回はログの永続化・可視化を考慮した上で、AWSが提供するElasticsearch Service(Kibana)を採用することにした。
ECSは標準でCloudWatch Logsをサポートしているため、当初は素直にawslogsドライバを利用していた。CloudWatchに転送してしまえば、Elasticsearch Serviceへのストリーミングも容易だったからである。

Network (3).png

しかし、Railsで開発したアプリケーションは例外をスタックトレースで出力し、改行単位でストリームに流されるためログの閲覧やエラー検知が非常に不便なものだった。
Multiline codec plugin等のプラグインを使えば複数行で構成されるメッセージを1行に集約できるが、AWS(Elasticsearch Service)ではプラグインのインストールがサポートされていない。
EC2にElasticsearchを構築することも一瞬考えたが、Elasticsearchへの依存度が高く、将来的にログドライバを変更する際の弊害になると考えて止めた。
その後考案したのがFluentd経由でログをElasticsearch Serviceに流す方法。この手法であればFluentdでメッセージの集約や通知もできるし、将来的にログドライバを変更することも比較的容易となる。

Network (4).png

ジョブスケジューリング

アプリケーションをコンテナで運用する際、スケジュールで定期実行したい処理はどのように実現するべきか。
いくつか方法はあるが、1つの手段としてLambdaのスケジュールイベントからタスクを叩く方法がある(Run task)。この方法でも問題はないが、最近(2017年6月)になってECSにScheduled Taskという機能が追加されており、Lambdaに置き換えて利用可能となった。Cron形式もサポートしているので非常に使いやすい。

運用

ECSで設定可能なパラメータ

ECSコンテナインスタンスにはコンテナエージェントが常駐しており、パラメータを変更することでECSの動作を調整できる。設定ファイルの場所は /etc/ecs/ecs.config
変更する可能性が高いパラメータは下表の通り。他にも様々なパラメータが存在する。

パラメータ名 説明 デフォルト値
ECS_LOGLEVEL ECSが出力するログのレベル info
ECS_AVAILABLE_LOGGING_DRIVERS 有効なログドライバの一覧 [“json-file”,”awslogs”]
ECS_ENGINE_TASK_CLEANUP_WAIT_DURATION タスクが停止してからコンテナが削除されるまでの待機時間 3h
ECS_IMAGE_CLEANUP_INTERVAL イメージ自動クリーンアップの間隔 30m
ECS_IMAGE_MINIMUM_CLEANUP_AGE イメージ取得から自動クリーンアップが始まるまでの間隔 1h

パラメータ変更後はエージェントの再起動が必要。

$ sudo stop ecs
$ sudo start ecs

クラスタのスケールアウトを考慮し、ecs.configはUserDataに定義しておくと良い。
以下はfluentdを有効にしたUserDataの記述例。

#!/bin/bash
echo ECS_CLUSTER=sandbox >> /etc/ecs/ecs.config
echo ECS_AVAILABLE_LOGGING_DRIVERS=["fluentd"] >> /etc/ecs/ecs.config

CPUリソースの制限

現状ECSにおいてCPUリソースの制限を設定することはできない(docker runの--cpu-quotaオプションがサポートされていない)。
タスク定義パラメータcpuは、docker runの--cpu-sharesにマッピングされるもので、CPUの優先度を決定するオプションである。従って、あるコンテナがCPUを食いつぶしてしまうと、他のコンテナにも影響が出てしまう。
尚、Docker 1.13からは直感的にCPUリソースを制限ができる--cpusオプションが追加されている。是非ECSにも取り入れて欲しい。

ユーティリティ

実際に利用しているツールを紹介。

graph.png

ルートボリューム・Dockerボリュームのディスク拡張

ECSコンテナインスタンスは自動で2つのボリュームを作成する。1つはOS領域(/dev/xvda 8GB)、もう1つがDocker領域(/dev/xvdcz 22GB)である。
クラスタ作成時にDocker領域のサイズを変更することはできるが、OS領域は項目が見当たらず変更が出来ないように見える。

Screen_Shot_2017-08-31_at_11_00_03.png

どこから設定するかというと、一度空のクラスタを作成し、EC2マネージメントコンソールからインスタンスを作成する必要がある。

また、既存ECSコンテナインスタンスのOS領域を拡張したい場合は、EC2マネージメントコンソールのEBS項目から変更可能。スケールアウトを考慮し、Auto scallingのLaunch Configurationも忘れずに更新しておく必要がある。

補足となるが、Docker領域はOS上にマウントされていないため、ECSコンテナインスタンス上からdf等のコマンドで領域を確認することはできない。

デプロイ

ECSのデプロイツールは色々ある(ecs_deployerは自分が公開している)。

社内で運用する際はECSでCIを回せるよう、ecs_deployerをコアライブラリとしたCIサーバを構築した。

デプロイ方式

  • コマンド実行形式のデプロイ
  • GitHubのPushを検知した自動デプロイ
  • Slackを利用したインタラクティブデプロイ

Screen_Shot_2017-08-31_at_11_31_15.png

デプロイフロー

ECSへのデプロイフローは次の通り。

  1. リポジトリ・タスクの取得
  2. イメージのビルド
    • タグにGitHubのコミットID、デプロイ日時を追加
  3. ECRへのプッシュ
  4. タスクの更新
  5. 不要なイメージの削除
    • ECRは1リポジトリ辺り最大1,000のイメージを保管できる
  6. サービスの更新
  7. タスクの入れ替えを監視
    • コンテナの異常終了も検知
  8. Slackにデプロイ完了通知を送信

現在のところローリングデプロイを採用しているが、デプロイの実行から完了までにおよそ5〜10分程度の時間を要している。デプロイのパフォーマンスに関してはまだあまり調査していない。

ログの分類

ECSのログを分類してみた。

ログの種別 ログの場所 備考
サービス AWS ECSコンソール サービス一覧ページのEventタブ APIで取得可能
タスク AWS ECSコンソール クラスタページのTasksタブから”Desired task status”が”Stopped”のタスクを選択。タスク名のリンクから停止した理由を確認できる APIで取得可能
Docker daemon ECSコンテナインスタンス /var/log/docker ※1
ecs-init upstart ジョブ ECSコンテナインスタンス /var/log/ecs/ecs-init.log ※1
ECSコンテナエージェント ECSコンテナインスタンス /var/log/ecs/ecs-agent.log ※1
IAMロール ECSコンテナインスタンス /var/log/ecs/audit.log タスクに認証情報のIAM使用時のみ
アプリケーション コンテナ /var/lib/docker/containers ログドライバで変更可能

※1: ECSコンテナインスタンス上の各種ログは、CloudWatch Logs Agentを使うことでCloudWatch Logsに転送することが可能(現状の運用ではログをFluentdサーバに集約させているので、ECSコンテナインスタンスにはFluentdクライアントを構築している)。

サーバレス化

ECSから少し話が逸れるが、インフラの運用・保守コストを下げるため、Lambda(Node.js)による監視の自動化を進めている。各種バックアップからシステムの異常検知・通知までをすべてコード化することで、サービスのスケールアウトに耐えうる構成が容易に構築できるようになる。
ECS+Lambdaを使ったコンテナ運用に切り替えてから、EC2の構築が必要となるのは踏み台くらいだった。

トラブルシュート

ログドライバにfluentdを使うとログの欠損が起きる

ログドライバの項に書いた通り、アプリケーションログはFluentd経由でElasticsearchに流していたが、一部のログが転送されないことに気付いた。
構成的にはアプリケーションクラスタからログクラスタ(CLB)を経由してログを流していたが、どうもCLBのアイドルタイムアウト経過後の最初のログ数件でロストが生じている。試しにCLBを外してみるとロストは起きない。

Network (1).png

ログクラスタ(ECSコンテナインスタンスの/var/log/docker)には次のようなログが残っていた。

time="2017-08-24T11:23:55.152541218Z" level=error msg="Failed to log msg "..." for logger fluentd: write tcp *.*.*.*:36756->*.*.*.*:24224: write: broken pipe"
3) time="2017-08-24T11:23:57.172518425Z" level=error msg="Failed to log msg "..." for logger fluentd: fluent#send: can't send logs, client is reconnecting"

同様の問題をIssueで見つけたが、どうも現状のECSログドライバはKeepAliveの仕組みが無いため、アイドルタイムアウトの期間中にログの送信が無いとELBが切断してしまうらしい(AWSサポートにも問い合わせた)。

という訳でログクラスタにはCLBを使わず、Route53のWeighted Routingでリクエストを分散することにした。

Network (2).png

尚、この方式ではログクラスタのスケールイン・アウトに合わせてRoute 53のレコードを更新する必要がある。
ここではオートスケールの更新をSNS経由でLambdaに検知させ、適宜レコードを更新する仕組みを取った。

コンテナの起動が失敗し続け、ディスクフルが発生する

ECSはタスクの起動が失敗すると数十秒間隔でリトライを実施する。この時コンテナがDockerボリュームを使用していると、ECSコンテナエージェントによるクリーンアップが間に合わず、ディスクフルが発生することがあった(ECSコンテナインスタンスの/var/lib/docker/volumesにボリュームが残り続けてしまう)。
この問題を回避するには、ECSコンテナインスタンスのOS領域(※1)を拡張するか、コンテナクリーンアップの間隔を調整する必要がある。
コンテナを削除する間隔はECS_ENGINE_TASK_CLEANUP_WAIT_DURATIONパラメータを使うと良い。

※1: DockerボリュームはDocker領域ではなく、OS領域に保存される。OS領域の容量はデフォルトで8GBなので注意が必要。

また、どういう訳か稀に古いボリュームが削除されず残り続けてしまうことがあった。そんな時は次のコマンドでボリュームを削除しておく。

# コンテナから参照されていないボリュームの確認
docker volume ls -f dangling=true

# 未参照ボリュームの削除
docker volume rm $(docker volume ls -q -f dangling=true)

ECSがELBに紐付くタイミング

DockerfileのCMDでスクリプトを実行するケースは多々あると思うが、コンテナはCMDが実行された直後にELBに紐付いてしまうので注意が必要となる。

bundle exec rake assets:precompile

このようなコマンドをスクリプトで実行する場合、アセットがコンパイル中であろうがお構いなしにELBに紐付いてしまう。
時間のかかる処理は素直にDockerfile内で実行した方が良い。

続きを読む

OpsWorks with Rails 環境でDB Migration問題をなんとかする

はじめに

AWS OpsWorksではChef11を選択すると、Railsスタックをデフォルトで提供しており、エンジニアが自前でChefなど用意しなくてもHeroku感覚でAWSをPaaSとして利用することが出来ます。

  • Herokuほどフルマネージドは要件上出来ないが、インフラはある程度管理したい
  • Chef実行準備が面倒くさい
  • コードのDeploy機能とデプロイ記録とりたい
  • CloudWatch Logs統合 (ログPath設定を画面から出来る)
  • AWSビジネスサポートに入っていると、困っている時に技術的なことを直接チケットを投げて聞ける

こんなメリットたちがあります。正直最後のメリットが一番のような気もしますが・・・

DB Migration問題?

このサービスには致命的な問題があります。なんと複数インスタンスへのデプロイ時に、RailsのDB Migrationと、Unicornの再起動の同期が全く取れていません。 そのため、複数インスタンスを選択した状態で、DB Migrationを実行すると、Migration以外のノードはソース反映後すぐにUnicorn再起動がかかり、高確率で不完全な状態でデプロイされてしまいます。

OpsWorks上でのMigration実行の仕組み

OpsWorks経由で「Migrate YES」を選択してデプロイを行うと rake の db:migrate が実行されます。
https://github.com/aws/opsworks-cookbooks/blob/2209f35fc0e3fd7a75af2e3d0715d425c6a55783/deploy/attributes/deploy.rb#L79

複数ノードがある状態で、「Migrate YES」を選択してデプロイを行う場合、グループ内で任意のノード1個が選択され(基本的にはグループで一番最初のもの)、そのノードに対して内部的にchefの変数に default[:deploy][application][:migrate] = true が設定入り、その後、全ノードに同時にcookbookが実行されます。

デプロイ中は、基本的に全てのインスタンスはそれぞれ勝手にChefが実行されるだけです。そのため、これが原因でマイグレーション同期されない問題がおきています。小さなDBマイグレーションであれば問題なかったりしますが、ちょっとでも時間がかかるようになるとアウトです。規模によりますが、Railsのデプロイを1回実行するのに少なくとも3〜4分くらいかかりますので、ダウンタイム発生となります

不完全な状態でのUnicorn再起動を回避する方法 by AWS

AWSサポート等にもヒアリングした所、以下2点の方針が一般的との回答。

  1. Migration用のレイヤーを作っていただき、先にそちらでMigration込みで一旦ソースを反映して頂く
  2. Chefのデプロイフックがあるので、それを利用して何かしら処理を入れてもらう

うーむ、自前でやらねば・・・・ということで素直に上記を対応します。
(1)に関してはただの運用フロー寄りの問題なので、一旦今回は詳細を割愛します。

デプロイフック実装

OpsWorksはDocument Root配下に deploy ディレクトリ配下に before_symlink.rb (シンボリックリンク張替え直前) で以下のチェック入れます。

Chef::Log.info("Running deploy/before_symlink.rb")
current_release = release_path
env = node[:deploy][:app_rails][:rails_env]

execute "rake aws:verify_migration" do
  cwd current_release
  command "bundle exec rake aws:verify_migration"
  environment new_resource.environment.merge({ "RAILS_ENV" => env })
end

Rakeタスクを適当に作ります。

namespace :aws do
  task verify_migration: :environment do
    raise StandardError if 
  ActiveRecord::Migrator.needs_migration?
  end
end

これでとりあえず、不完全な状態でUnicornが再起動される問題だけ回避しました。before_symlinkの段階で、verify_migrationを実行し、マイグレーションが残っている状態の時は対象インスタンスの反映を途中中断させるようにします。「カラムの削除の時はどうするのか」みたいな懸念はありますが、一旦これだけでも入れておけば大きい所はカバーできそうです。

最後に

そもそもChefは諦めて、コンテナ運用(GKE + Spinnaker) に早く移行したほうがいいかもしれない

続きを読む

Rails5アプリケーションのAWSによるネットワーク構築 Nginx+Puma+Capistranoな環境とAWS構築(VPC EC2 RDS CloudFlont Route53 etc)

はじめに

数ヶ月前に書いたRails5のAWS構築をもう少し項目を増やしてから公開しようと思っていたのですが、なかなか出来ないのでもう公開します。(ELBとかCloudFlontとかもっとちゃんと書けそう、、)
項目の追加や修正は適宜入れると思います。

Railsは5.0を使っています。(記事を書いたときの最新)

4系は以前書きました↓

構築する環境

  • VPC
  • EC2
    • Nginx + Puma
    • Capistrano
  • (S3)
  • ELB
  • RDS
  • CloudFlont
  • Route53
  • (CloudWatch)

VPC、EC2のインスタンスの作成

AWSのデザインが多少変更はありますが、以下を参考に作成出来ます。

AWS VPCによるネットワーク構築とEC2によるサーバー構築

これに追加でElastic IPの設定もしておきました。

EC2内の環境構築

作成したEC2にログインして環境を構築します。
ec2-userでまずログインしましょう。

ユーザーの作成

デフォルトではec2-userなので新たなユーザーを追加し、そのユーザーでsshログイン出来るように設定します。

$ sudo adduser shizuma #ユーザーの追加 「shizuma」の部分は好きなユーザー名にします。以後「shizuma」の部分は各自のユーザー名になります。
$ sudo passwd shizuma
#ここで、新規ユーザーのパスワードを設定します。
$ sudo visudo
-----------------------------
#vimが起動するので新規ユーザーにroot権限を与える。
root    ALL=(ALL)       ALL
shizuma ALL=(ALL)       ALL #この行を追加
-----------------------------
$ sudo su - shizuma #ユーザー切り替え
#先ほど設定したパスワード

ここでローカルに一旦戻り、鍵を作成します。

$ cd .ssh
$ ssh-keygen -t rsa
-----------------------------
Enter file in which to save the key ():first_aws_rsa #ここでファイルの名前を記述して、エンター
Enter passphrase (empty for no passphrase): #何もせずそのままエンター
Enter same passphrase again: #何もせずそのままエンター
-----------------------------
$ vi config
-----------------------------
# 以下を追記
Host first_aws
  Hostname 54.64.22.197 #自分の設定に合わせて
  Port 22
  User shizuma #先ほどのユーザー名
  IdentityFile ~/.ssh/first_aws_rsa #秘密鍵の設定
-----------------------------

次に、サーバーに戻り作成した 公開鍵 をサーバーに設定します。

$ mkdir .ssh
$ chmod 700 .ssh
$ cd .ssh
$ vi authorized_keys
-----------------------------
#localの「first_aws_rsa.pub」の中身のコピペ。(localで $ cat first_aws_rsa.pubとかすると良い)
ssh-rsa sdfjerijgviodsjcIKJKJSDFJWIRJGIUVSDJFKCNZKXVNJSKDNVMJKNSFUIEJSDFNCJSKDNVJKDSNVJNVJKDSNVJKNXCMXCNMXNVMDSXCKLMKDLSMVKSDLMVKDSLMVKLCA shizuma@shizuma-no-MacBook-Air.local
-----------------------------
$ chmod 600 authorized_keys
$ exit
$ exit

これで設定が完了したので、以降作成したユーザーでアクセスするようにします。

基本ライブラリとrubyの環境構築

$ sudo yum install 
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 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 rehash
$ rbenv install -v 2.3.1
$ rbenv global 2.3.1
$ rbenv rehash
$ ruby -v

gitの設定

最低限の設定だけしておきます。

$ vi .gitconfig
.gitignore
[user]
  name = your_name #自分の名前
  email = hoge@hoge.com #自分のメアド

[alias] #これはお好きに
  a = add
  b = branch
  ch = checkout
  st = status

[color] #色付け
  ui = true

[url "github:"] #pull、pushのための設定
    InsteadOf = https://github.com/
    InsteadOf = git@github.com:

アプリケーションフォルダの設置

/var/www/rails にアプリケーションフォルダを設置します。

$ cd /
$ sudo mkdir -p /var/www/rails
$ sudo chown -R shizuma var/www

GitHubの接続とアプリケーションのclone

$ cd ~/.ssh
$ ssh-keygen -t rsa
-----------------------------
Enter file in which to save the key ():aws_git_rsa #ここでファイルの名前を記述して、エンター
Enter passphrase (empty for no passphrase): #何もせずそのままエンター
Enter same passphrase again: #何もせずそのままエンター
-----------------------------
$ chmod 744 config
$ vi config
-----------------------------
# 以下を追記
Host github github.com
  Hostname github.com
  User git
  Port 22
  IdentityFile ~/.ssh/aws_git_rsa #秘密鍵の設定
-----------------------------
$ cat aws_git_rsa.pub
-----------------------------
ssh-rsa sdfjerijgviodsjcIKJKJSDFJWIRJGIUVSDJFKCNZKXVNJSKDNVMJKNSFUIEJSDFNCJSKDNVJKDSNVJNVJKDSNVJKNXCMXCNMXNVMDSXCKLMKDLSMVKSDLMVKDSLMVKLCA shizuma@ip-10-0-1-10
-----------------------------

ここで、これをコピペしてgithubに公開鍵を登録する。
githubへの鍵の登録がよくわからない方は以下の記事を参考に。
gitHubでssh接続する手順~公開鍵・秘密鍵の生成から~

そして、git clone する。

$ cd /var/www/rails
$ git clone https://github.com/kuboshizuma/cheerfull # 自分のアプリケーション

RDSの設定

EC2の環境構築に一区切りついたのでRDSの設定を行います。

DBサブネットの登録

RDSで使うサブネットを登録します。
この設定には異なるアベイラビリティゾーンにあるサブネットが最低1つずつ計2つ必要になります。
また、Railsアプリケーションをおいたサブネットはゲートウェイに繋がったpublicなサブネットなので、privateなサブネットを異なるアベイラビリティゾーンに一つずつ作成します。

スクリーンショット_2017-03-04_11_56_01.png

パラメータグループの設定

パラメータグループを設定します。
mysqlを使用していると設定なので、ここではmysqlを選択します。バージョンは適宜選択。
「chara…」で検索すると出て来るcharasetをutf-8に変更。

スクリーンショット_2017-03-04_12_00_57.png

設定が完了した後に、パラメータの編集を押してパラメーター変更。

スクリーンショット_2017-03-04_12_04_29.png

セキュリティグループの作成

VPCでセキュリティグループを作成します。

スクリーンショット_2017-03-04_12_08_19.png

インバウンドルールにMySQLを設定し、MySQLのアクセスのみ許可します。
「送信元」は0.0.0.0/0で設定。特定のRailsアプリケーションサーバーがあるサブネットに限定するほうがよさそう。

スクリーンショット 2017-03-04 12.55.42.png

RDSインスタンスの作成

エンジンの選択

スクリーンショット_2017-03-04_12_12_05.png

本番稼働用?

スクリーンショット_2017-03-04_12_12_45.png

DB詳細設定の指定

無料枠はt2.micro

スクリーンショット_2017-03-04_12_17_35.png

詳細設定の設定

各種作成したもので設定。

スクリーンショット_2017-03-04_12_19_44.png

スクリーンショット_2017-03-04_12_19_53.png

接続確認

hostは各自の作成したRDSインスタンスのエンドポイントをみる。

$ mysql -h hogepoge.ap-northeast-1.rds.amazonaws.com -u shizuma -P 3306 -p

接続出来たら完了!

絵文字の扱い

絵文字も登録出来るようにする場合。
以下を参考に utfmb4を採用する。

ActiveRecordをutf8mb4で動かす

MAMPでは /Applications/MAMP/conf/my.cnfmy.cnf を設置した。

Railsアプリケーションの起動のための準備

puma setting

以下を追記する。

cofig/puma.rb
# add setting for production
_proj_path = "#{File.expand_path("../..", __FILE__)}"
_proj_name = File.basename(_proj_path)
_home = ENV.fetch("HOME") { "/home/#{ENV['PUMA_USER']}" }
pidfile "#{_home}/run/#{_proj_name}.pid"
bind "unix://#{_home}/run/#{_proj_name}.sock"
directory _proj_path
# add end

ENV['PUMA_USER'] にサーバーのユーザー名が入るように環境変数を設定。

database setting

以下のように編集。

config/database.yml
production:
  <<: *default
  database: cheerfull
  username: <%= ENV['DATABASE_USER_NAME_PRODUCTION'] %>
  password: <%= ENV['DATABASE_PASSWORD_PRODUCTION'] %>
  host: <%= ENV['DATABASE_HOST_PRODUCTION'] %>

それぞれ該当の環境変数を設定。

rake secret

以下のsecret keyの箇所に値が入るように環境変数を設定。
rake secret とコマンドを打つと出て来る文字列を設定する。

config/secrets.yml
production:
  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
$ gem install bundler
$ bundle install
$ rake db:create RAILS_ENV=production
$ rake db:migrate RAILS_ENV=production

PumaとNginxの起動

Pumaの起動

アプリケーションディレクトリにて以下を実行。

$ RAILS_SERVE_STATIC_FILES=true RAILS_ENV=production puma -w 4

「Ctr+c」でプロセス消さなければ生き残る。
プロセス消す必要があれば以下のようにする。

$ ps aux | grep puma # プロセスIDを探す
$ kill -9 (ID)

Nginx の起動

「cheerfull」の部分は自分のアプリケーションディレクトリ名に変更する。

/etc/nginx/conf.d/cheerfull.conf
  # log directory
  error_log  /var/www/rails/cheerfull/log/nginx.error.log;
  access_log /var/www/rails/cheerfull/log/nginx.access.log;
  upstream app_server {
    # for UNIX domain socket setups
    server unix:/home/shizuma/run/cheerfull.sock fail_timeout=0;
  }
  server {
    listen 80;
    server_name 12.134.156.178; # 自分のIP
    # nginx so increasing this is generally safe...
    # path for static files
    root /var/www/rails/cheerfull/public;
    # page cache loading
    try_files $uri/index.html $uri @app_server;
    location / {
      # HTTP headers
      proxy_pass http://app_server;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_redirect off;
    }
    # Rails error pages
    error_page 500 502 503 504 /500.html;
    location = /500.html {
      root /var/www/rails/cheerfull/public;
    }
    client_max_body_size 4G;
    keepalive_timeout 5;
  }

ユーザーをnginxから自分のユーザー名に変更しておく。

/etc/nginx/nginx.conf
#user nginx;
user shizuma;

nginxを起動します。

$ sudo service nginx restart

これで、IPアドレスでアクセスするとアプリケーションが表示されるようになりました。

Capistranoの設定

諸々動くことが確認出来たのでデプロイが出来るように設定します。
デプロイのためにCapistranoの設定をします。
慣れてきたら、いきなりCapistranoの設定をしていけばいいと思います。

socketの場所もアプリケーションディレクトリに変更するのでnginxの設定もそれに合わせて変更します。

「cheerfull」の部分は自分のアプリケーション名に変更する。

deploy.rb
require 'dotenv'
Dotenv.load

lock "3.7.1"

set :application, "cheerfull"
set :repo_url, "git@github.com:your_gitname/cheerfull.git"
set :branch, 'master'
set :deploy_to, '/var/www/rails/protospacce'
set :linked_files, fetch(:linked_files, []).push('.env')
set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'vendor/bundle', 'public/system')
set :keep_releases, 5
set :rbenv_ruby, '2.3.1'

# puma setting
set :puma_threads,    [4, 16]
set :puma_workers,    0
set :pty,             true
set :use_sudo,        false
set :stage,           :production
set :deploy_via,      :remote_cache
set :deploy_to,       "/var/www/rails/#{fetch(:application)}"
set :puma_bind,       "unix://#{shared_path}/tmp/sockets/#{fetch(:application)}-puma.sock"
set :puma_state,      "#{shared_path}/tmp/pids/puma.state"
set :puma_pid,        "#{shared_path}/tmp/pids/puma.pid"
set :puma_access_log, "#{shared_path}/log/puma.error.log"
set :puma_error_log,  "#{shared_path}/log/puma.access.log"
set :puma_preload_app, true
set :puma_worker_timeout, nil
set :puma_init_active_record, true

namespace :deploy do
  desc 'Create database'
  task :db_create do
    on roles(:db) do |host|
      with rails_env: fetch(:rails_env) do
        within current_path do
          execute :bundle, :exec, :rake, 'db:create'
        end
      end
    end
  end
end

実際にdeployする前に、共有ファイルは用意しておきましょう。この場合、.env

ここでpumaのセッティングをしたので、config/puma.rb はもう不要になります。
また、dotenvを使用する場合は変更を読み込むために config/deploy/templates/puma.rb.erb を用意します。以下を用意。

config/deploy/templates/puma.rb.erb
#!/usr/bin/env puma

directory '<%= current_path %>'
rackup "<%=fetch(:puma_rackup)%>"
environment '<%= fetch(:puma_env) %>'
<% if fetch(:puma_tag) %>
  tag '<%= fetch(:puma_tag)%>'
<% end %>
pidfile "<%=fetch(:puma_pid)%>"
state_path "<%=fetch(:puma_state)%>"
stdout_redirect '<%=fetch(:puma_access_log)%>', '<%=fetch(:puma_error_log)%>', true


threads <%=fetch(:puma_threads).join(',')%>

<%= puma_bind %>
<% if fetch(:puma_control_app) %>
activate_control_app "<%= fetch(:puma_default_control_app) %>"
<% end %>
workers <%= puma_workers %>
<% if fetch(:puma_worker_timeout) %>
worker_timeout <%= fetch(:puma_worker_timeout).to_i %>
<% end %>

<% if puma_preload_app? %>
preload_app!
<% else %>
prune_bundler
<% end %>

on_restart do
  puts 'Refreshing Gemfile'
  ENV["BUNDLE_GEMFILE"] = "<%= fetch(:bundle_gemfile, "#{current_path}/Gemfile") %>"
  ENV.update Dotenv::Environment.new('.env')
end

<% if puma_preload_app? and fetch(:puma_init_active_record) %>
before_fork do
  ActiveRecord::Base.connection_pool.disconnect!
end

on_worker_boot do
  ActiveSupport.on_load(:active_record) do
    ActiveRecord::Base.establish_connection
  end
end
<% end %>
$ bundle exec production cap puma:config 

これでこの設定を本番に反映。

あとはデプロイコマンドを打つ。

$ bundle exec cap production deploy

デプロイdone!

nginxのlogの位置やsocketの位置を変更します。

/etc/nginx/conf.d/cheerfull.conf
  # log directory
  error_log  /var/www/rails/cheerfull/shared/log/nginx.error.log;
  access_log /var/www/rails/cheerfull/shared/nginx.access.log;
  upstream app_server {
    # for UNIX domain socket setups
    server unix:/var/www/rails/cheerfull/shared/tmp/sockets/cheerfull-puma.sock fail_timeout=0;
  }
  server {
    listen 80;
    server_name 12.134.156.178; # 自分のIP
    # nginx so increasing this is generally safe...
    # path for static files
    root /var/www/rails/cheerfull/current/public;
    # page cache loading
    try_files $uri/index.html $uri @app_server;
    location / {
      # HTTP headers
      proxy_pass http://app_server;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_redirect off;
    }
    # Rails error pages
    error_page 500 502 503 504 /500.html;
    location = /500.html {
      root /var/www/rails/cheerfull/current/public;
    }
    client_max_body_size 4G;
    keepalive_timeout 5;
  }
$ sudo service nginx restart

これで、デプロイ環境が整いました。
以前起動したpumaのプロセスはきっておきましょう。

複数サーバーへのデプロイ

multiconfig を使用します。

Gemfile
gem 'capistrano-multiconfig', '~> 3.1', require: false

Capfileのcapistrano/setupから変更。

Capfile
# require "capistrano/setup"
require 'capistrano/multiconfig'

config/deploy/production.rb から config/deploy/app1/production.rb に変更する。
以下のようにdeploy出来るようになる。

$ bundle exec cap app1:production deploy

違うサーバーにデプロイするときは同じように config/deploy/app2/production.rb を準備すればオーケー。

$ bundle exec cap app2:production deploy

これで違うサーバーにデプロイ出来る。

これまでと同様にec2サーバーをもう一度用意して2箇所にデプロイ出来るようにする。

ロードバランサー ELB

ロードバランサーをhttpsで運用したいときadmin@hoge.comのようなアドレスへのメールを受信可能のしないといけない。

以下でとりあえずzoneファイルのインポートまでする。
Amazon Route53 ネームサーバへの移行手順(お名前.comからの)

その後にこの記事にあるようにお名前.com側の設定をする。
お名前.com + Route53で独自ドメインのメールをGmailに転送する

メールの転送が出来ていることを確認する。
そして、ELBの指示に従って設定。
下記参考。
【初心者向け】ELBにSSL証明書をインストールする

そこまでしたら
Amazon Route53 ネームサーバへの移行手順(お名前.comからの)
の続きでELBとつなげる。

注意点は、ターゲットグループにトラフィックをルーティングするときのプロトコルをHTTPにすること。これでELBへはHTTPSでアクセス出来、それをアプリケーションではHTTPで扱うことが出来る。

また、以下のように nginxのサーバーネームにドメイン名を追加する必要がある。

server_name hogepoge.com 00.000.00.00 sakamichi-app-elb-000000000.ap-northeast-1.elb.amazonaws.com

CloudFront

Amazon CloudFront + ACM 独自ドメインで HTTPS (SSL) 配信設定メモ

CloudWatchとか

必要の応じて加筆。

続きを読む