AWS Elastic Beanstalk x Ruby on Rails で構築する

セットアップする環境

Ruby on Rails5
RDS (Mysql)
Puma
CloudFront
を用いて起動させる。

アプリケーション名は {app name} とする。

手順

RDS を起動

eb コマンドで、同時にRDS を起動させることも可能だが、アプリケーションの削除と同時に、RDSも消えてしまうため、別々に作成する。
マスターユーザー、マスターユーザーのパスワード、インスタンス名は、何でも良く、DBは作らなくて良い。

RDSを作るとき、新規で SecurityGroup を作成し、その名前は rds-launch-wizard に、インプットのソースに、自身を追加する。

f:id:unching-star:20161006082808p:plain

RDS で ユーザーの作成

権限を絞るため、Beanstalk から接続するためのユーザーを作成する。

GRANT ALTER, CREATE, DELETE, DROP, INDEX, SELECT, UPDATE, INSERT ON dbname_dayo.* TO username_dayo@'%' IDENTIFIED BY 'password_dayo';
Rails プロジェクトに設定を追加

1. 通常のRailsのセットアップを行う。

2. ENV を書く

環境構築時に変数として渡したい所は、ENV で指定する。

$ vi config/database.yml
...

production:
  <<: *default
  database: <%= ENV['RDS_DB_NAME'] %>
  username: <%= ENV['RDS_USERNAME'] %>
  password: <%= ENV['RDS_PASSWORD'] %>
  host: <%= ENV['RDS_HOSTNAME'] %>
  port: <%= ENV['RDS_PORT'] %>
.ebextensions

追加の設定を行う。

1. Rails Deploy 時、 rails db:seed が実行されないため、これを実装する。

container_commands:
  seeddb:
    command: 'export HOME=/root; rails db:seed --trace'
    leader_only: true

2. git が install されず、 bundle install に失敗するため、yum install git を追加する。

packages:
  yum:
    git: []

3. Healthcheck の URL を指定する

option_settings:
  - namespace:  aws:elasticbeanstalk:application
    option_name:  Application Healthcheck URL
    value:  /health_check.txt

4. Nginx の pubilc フォルダ設定が誤っているため、修正する

files:
  "/etc/nginx/conf.d/webapp_healthd.conf":
    mode: "000644"
    owner: root
    group: root
    content: |
      upstream my_app {
        server unix:///var/run/puma/my_app.sock;
      }

      log_format healthd '$msec"$uri"'
                      '$status"$request_time"$upstream_response_time"'
                      '$http_x_forwarded_for';

      server {
        listen 80;
        server_name _ localhost; # need to listen to localhost for worker tier

        if ($time_iso8601 ~ "^(\d{4})-(\d{2})-(\d{2})T(\d{2})") {
          set $year $1;
          set $month $2;
          set $day $3;
          set $hour $4;
        }

        access_log  /var/log/nginx/access.log  main;
        access_log /var/log/nginx/healthd/application.log.$year-$month-$day-$hour healthd;

        root /var/app/current/public;

        location / {
          try_files  $uri @app;

          gzip_static on;
          gzip on;
          expires 60s;
          add_header Cache-Control public;
        }

        location @app {
          proxy_pass http://my_app; # match the name of upstream directive which is defined above
          proxy_set_header Host $host;
          proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
          proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

      }

container_commands:
  01_reload_nginx:
    command: "service nginx reload"


5. RDSに繋ぐためのセキュリティーグループを追加する

option_settings:
  - namespace:  aws:autoscaling:launchconfiguration
    option_name:  SecurityGroups
    value:
      - rds-launch-wizard

6. ELB のポートを変更する

デフォルトのままだと、攻撃されやすいため、ポートを変更します。
ポートの追加を行うと、セキュリティーグループも自動で書き換えてくれます。
ここの設定も、追加の設定となるので、デフォルトの80ポートは、無効にします。

option_settings:
  aws:elb:listener:8080:
    ListenerProtocol: HTTP
    InstanceProtocol: HTTP
    InstancePort: 80

  aws:elb:listener:80:
    ListenerEnabled: false


7. コミットする

コミットしなければ、deploy に反映されない。

$ git commit -m 'hoge'
Elastic Beanstalk を作成する。
$ eb create {app name} \
-i t2.medium \
-r ap-northeast-1 \
--vpc.elbpublic \
--scale 2 \
--envvars RACK_ENV=production,SECRET_KEY_BASE=dafb6ff6cb0952c8842fd8c9419d3333ec5054ce7a9edbf8bc0c2abd012fc26ed94d0,S3_ACCESS_KEY_ID=AWDWDWDWDWDWW2A,S3_SECRET_ACCESS_KEY=UW/mKLUWm/fr4fFggs+5r39l5,S3_BUCKET=hoge-data,S3_ASSET_HOST=https://hoge.com/,RDS_DB_NAME=dbname,RDS_HOSTNAME=db.hoge.ap-northeast-1.rds.amazonaws.com,RDS_PORT=3306,RDS_USERNAME=username,RDS_PASSWORD=password
Cloud Front を使用する

beanstalk の ホスト名を Origin に指定するだけで良い。
キャッシュの時間設定は、Rails 側で指定する。

トラブルシューティング

起動に失敗する場合

ログを確認する

$ eb logs {app name}

AWS Lambda で、 python から mysql に接続する

aws lambda で python から mysql が使いたい場合は、

$ pip install mysql-python -t .
$ vi lambda_function.py
...

さらに、`libmysqlclient.so.*` がないと起動できない。

http://www.filewatcher.com/ ここなどで適当なファイルをダウンロードし、同じフォルダに保存すれば、利用できます。

MiniJSON で text の Json.Deserialize が null になる場合

Json のフォーマットがおかしいことが考えられるが、
textは見かけ上問題ない場合がある。

そんな場合によくあるのが、BOM (バイトオーダーマーク)という情報がテキストデータの先頭に含まれている場合。

windows 標準のメモ帳など、UTF-8には、通常はつけるべきでないとされている、BOMがついてしまう。
この BOM が原因で、読み込みエラーが起きている可能性がある。

json ファイル自体を別のエディタで作りなおすか、プログラム側で、先頭の無駄な文字を消すと、読み込めるようになる。

WWW www = new WWW ("https://xxx/sample.json");

yield return www;

string v = www.text;
v = v.Trim();

var o = Json.Deserialize (v);

プログラムに関わるテキストは、メモ帳で開かないようにしましょう。

Certificate Manager で取得した証明書が、CloudFront で選択できない

Certificate Manager が 東京リージョンでも取得できるようになりましたが、
CloudFrontは、バージニア北部 (us-east-1) で取得した証明書でないと設定できません。

よくある質問 - AWS Certificate Manager(簡単に SSL/TLS 証明書を作成、管理、配置) | AWS

米国東部 (バージニア北部) リージョンでプロビジョンされ、Amazon CloudFront ディストリビューションに関連付けられた証明書が、お客様のディストリビューションに設定された地理的場所に配信されます。

Lineのメッセージで、URLが展開されない

Line の チャット画面上で、 OG情報を展開させるには、
http または、httpsプロトコルsslv3(脆弱性があり利用停止が推奨されている)に対応させるひつようがあり、
httpsプロトコルTLSだと表示されません。

おそらく数年以内に改善されるはずです。

NginxでUnicornが別Portへリダイレクトしてしまう

$http_host は ポート含む
$host は、ポート含まない

たとえば、このような経路でサイトを表示させるとき、
ブラウザ =(80)=> Cloud Front =(8000)=> ELB =(80)=> Nginx =(socket)=> Unicorn
このようにポート8000にして通過させると、Unicorn 側に 8000ポートでアクセスしたことになってしまう。

location @unicorn {
    set $proto $scheme;
    if ($http_cloudfront_forwarded_proto = "https")  { set $proto "https"; }

    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $proto;
    proxy_set_header Host $host;
    # proxy_set_header Host $http_host;
    proxy_pass http://app_sock;
}

Rails で Base64 エンコードされた文字を Carrierwave で保存する最も良い方法

File クラスを使うと、ストレージの読み書きが発生し、速度が落ちるため、メモリ上だけで処理を行う方針。
File クラスと同じ処理をするため、メソッドを定義する。

class StringFileIO < StringIO

  def self.create_from_canvas_base64 str
    return nil if str.nil?

    head, data = str.split ",", 2

    return nil if data.nil?

    _, mime_type = head.split /:|;/

    bin = Base64.decode64 data

    self.new bin, mime_type
  end

  def initialize blob, content_type
    super(blob)
    @content_type = content_type
    self
  end

  def original_filename
    "image"
  end

  def content_type
    @content_type
  end

end


あとは、代入するのみ

class User
  def image_str= str
    self.image = StringFileIO.create_from_canvas_base64(str)
  end
end