読者です 読者をやめる 読者になる 読者になる

devひよこのあしあと

いつでもひよっこな気持ちで学びと挑戦を

AnsibleでElastiCacheをセットアップしてRails5から利用する

ActionCable AWS Rails Ansible ElastiCache Sidekiq Capistrano

下記の記事で紹介したスライド共有システム「nslides」ではAnsibleを用いてインフラ周りの設定をしています。今回はその中からElastiCacheでredisを立ち上げてRails5から利用する設定をしている部分を紹介します。

devchick.hatenablog.com

nslidesはこちらで公開中です。 公開終了しました (2017/02/27)

システム構成:

コードはGitHubにて公開しています。

https://github.com/devchick/nslides

事前作業

Ansibleが利用するAWSのアクセスキー&シークレットキーなどplaybook内には直書きしたくない情報を環境変数としてセットしておきます。 私の場合は普段direnvを利用しているので、ローカルの.envrcに登録して環境変数に反映されるようにしました。

$ cd REPO_ROOT
$ cat .envrc
export AWS_ACCESS_KEY_ID=XXXXXXXXXXXX
export AWS_SECRET_ACCESS_KEY=XXXXXXXXXXXXXXXXXX
export AWS_REGION=ap-northeast-1
export DB_USERNAME=XXXXXX
export DB_PASSWORD=XXXXXX
export APP_SERVER_IP=XXXXXXXX
export SECRET_KEY_BASE=XXXXXXXXXXXXXXXXXXXXXXXX

$ direnv allow

playbook

インベントリ

AWSリソースを用いますのでDynamic Inventoryを利用します。Dynamic Inventoryの解説は下記サイトなどを参照ください。

ec2.iniとec2.pyを取得します。

$ cd REPO_ROOT/provision/inventories
$ wget https://raw.githubusercontent.com/ansible/ansible/devel/contrib/inventory/ec2.ini
$ wget https://raw.githubusercontent.com/ansible/ansible/devel/contrib/inventory/ec2.py
$ chmod u+x ec2.py

site.yml

AWSリソースのセットアップタスクで、elasticacheロールとec2ロールを呼び出します。

# PLAYBOOK_ROOT/site.yml

---

- name: AWSリソースのセットアップ
  hosts: localhost
  connection: local
  roles:
    - ec2
    - elasticache
  vars:
    service_name: nslides01
    group_name: application
    region: ap-northeast-1

- name: アプリケーションサーバーのセットアップ
  hosts: tag_Name_application
  remote_user: ec2-user
  become: yes
  roles:
    - sudo
    - nginx
    - mysql
    - ruby
    - panel_base
  vars:
    db_host: "{{ groups.rds[0] }}"
    db_username: "{{ lookup('env', 'DB_USERNAME') }}"
    db_password: "{{ lookup('env', 'DB_PASSWORD') }}"
    redis_host: "{{ groups.elasticache_redis[0] }}"
    private_key: ~/.ssh/nslides01.pem

今回はElastiCacheの設定の話なので直接は関係ないですが、DBへ接続するためのユーザー名やパスワードは先述の.envrcで設定した環境変数値を読み込むようにしています。

ec2ロール

ec2ロールではセキュリティグループとEC2インスタンスを作成します。 EC2用セキュリティグループではSSH接続とHTTP/HTTPS接続のみを許可しておきます。

# PLAYBOOK_ROOT/roles/ec2/tasks/main.yml

---

- name: EC2用セキュリティグループを作成
  ec2_group:
    name: "{{ group_name }}"
    description: "{{ group_name }}"
    region: ap-northeast-1
    rules:
      - proto: tcp
        from_port: 22
        to_port:   22
        cidr_ip:   0.0.0.0/0
      - proto: tcp
        from_port: 80
        to_port:   80
        cidr_ip:   0.0.0.0/0
      - proto: tcp
        from_port: 443
        to_port:   443
        cidr_ip:   0.0.0.0/0
    rules_egress:
      - proto: all
        from_port: 0
        to_port:   65535
        cidr_ip:   0.0.0.0/0

- name: EC2インスタンスを作成
  ec2:
    image:         "{{ ami_image }}"
    instance_type: "{{ instance_type }}"
    region:        "{{ region }}"
    key_name:      "{{ service_name }}"
    group:         "{{ group_name }}"
    instance_tags:
      Name:        "{{ group_name }}"
    exact_count:   1
    count_tag:
      Name:        "{{ group_name }}"
    wait:          yes
    wait_timeout:  300
    volumes:
      - device_name: "{{ device_name }}"
        volume_type: "{{ volume_type }}"
        volume_size: "{{ volume_size }}"
        delete_on_termination: yes
    instance_profile_name: ap_servers
  register: ec2


- name: SSHで接続できるようになるまで待機
  wait_for: port=22 host="{{ item.public_ip }}" timeout=300 state=started
  with_items: ec2.instances

- name: 作成したEC2インスタンスをインベントリに追加
  add_host: hostname="{{ item.public_ip }}" groupname="tag_Name_{{ group_name }}"
  with_items: ec2.instances

上記で参照している変数にはAWSの無料枠で収まるように最低限のインスタンスサイズを下記のように指定しています。(節約♪節約♪)

# PLAYBOOK_ROOT/roles/ec2/defaults/main.yml

---

ami_image: ami-383c1956
instance_type: t2.micro
device_name: /dev/xvda
volume_type: gp2
volume_size: 30

elasticacheロール

elasticacheロールでは、セキュリティグループとElastiCacheを作成します。 先ほど作成したEC2用のセキュリティグループを適用してあるインスタンスからのみRedisへの接続を許可するように設定します。

# PALYBOOK_ROOT/roles/elasticache/tasks/main.yml

---

- name: ElastiCache用セキュリティグループを作成
  ec2_group:
    name: redis
    description: redis
    region: "{{ region }}"
    rules:
      - proto: tcp
        from_port: 6379
        to_port:   6379
        group_name: "{{ group_name }}"  # ← EC2用セキュリティグループ名を指定
    rules_egress:
      - proto: all
        from_port: 0
        to_port:   65535
        cidr_ip:   0.0.0.0/0
  register: sg

- name: AWS ElastiCacheのセットアップ
  elasticache:
    name: "{{ service_name }}"
    state: present
    engine: redis
    cache_engine_version: 2.8.24
    node_type: cache.t2.micro
    num_nodes: 1
    cache_port: 6379
    cache_security_groups: []   # ☆
    security_group_ids:         # ☆
      - "{{ sg.group_id }}"     # ☆
    region: "{{ region }}"
    zone: ap-northeast-1a
  register: result

- name: 作成したElastiCacheのエンドポイント
  debug: msg="The new ElastiCache endpoint is {{ result.elasticache.data.CacheNodes[0].Endpoint }}"

ハマったのは上記の☆の部分。cache_security_groupsVPCがない頃?の古いインスタンスたちのときに利用するオプションのようですが、VPC内に作成するからといって無視してはいけなくて、空配列を明示的に指定しておかなければならないようです。

A list of cache security group names to associate with this cache cluster. Must be an empty list if inside a vpc

security_group_idsの方はVPC内のセキュリティグループを指定するのですが、なぜかここはセキュリティグループ名だと抽出に失敗してしまってエラーになりました。なのでセキュリティグループIDを指定するために先に作成したセキュリティグループの結果をregister: sgしておき、そのデータを利用しています。

Ansible 1.9を使ってるからかなー? 2.0で直ってたらいいな。

Railsアプリデプロイ先を構成

nslidesシステムではRailsアプリのデプロイにはCapistrano3を使っています。Capistrano3用にデプロイ先のフォルダやデプロイ先固有の設定を構築するようにAnsibleを設定します。

# PALYBOOK_ROOT/roles/panel_base/tasks/main.yml

---

- name: アプリケーションディレクトリ作成
  file:
    path: "{{ item }}"
    state: directory
    owner: "{{ remote_user }}"
    group: rbenv
    mode: 0775
  with_items:
    - /app/nslides/panel/shared/config
    - /app/nslides/panel/shared/config/initializers
    - /app/nslides/panel/shared/tmp/pids
    - /app/nslides/panel/shared/tmp/sockets
    - /app/nslides/panel/shared/tmp/cache
    - /app/nslides/panel/shared/tmp/sessions
    - /app/nslides/panel/shared/log
    - /app/nslides/panel/shared/vendor/bundle

- name: 環境設定
  become: yes
  template:
    src:  "{{ item.src }}"
    dest: "{{ item.dst }}"
    mode: 0644
  with_items:
    - { src: panel_envs.sh.j2, dst: /etc/profile.d/panel_envs.sh }

- name: 設定ファイル作成
  template:
    src: "{{ item.src }}"
    dest: "/app/nslides/panel/shared/{{ item.dst }}"
    owner: "{{ remote_user }}"
    group: rbenv
    mode: 0660
  with_items:
    - { src: database.yml.j2, dst: config/database.yml            }
    - { src: secrets.yml.j2,  dst: config/secrets.yml             }
    - { src: cable.yml.j2,    dst: config/cable.yml               }
    - { src: sidekiq.rb.j2,   dst: config/initializers/sidekiq.rb }

- name: Bitbucketアクセス用SSH鍵の配置
  copy:
    src: "{{ private_key }}"
    dest: "/home/ec2-user/.ssh/id_rsa"
    owner: "{{ remote_user }}"
    group: "{{ remote_user }}"
    mode: 0400

設定ファイルはAnsibleのテンプレート機能を使います。

例えばshared/config/cable.ymlの場合は下記のような内容になります。

# Action Cable uses Redis by default to administer connections, channels, and sending/receiving messages over the WebSocket.
production:
  adapter: redis
  url: "redis://{{ redis_host }}:6379"

development:
  adapter: async

test:
  adapter: async

shared/config/initializers/sidekiq.rbの場合は下記の内容になります。

Sidekiq.configure_server do |config|
  case Rails.env
    when 'production' then
      config.redis = { url: "redis://{{ redis_host }}:6379", namespace: 'sidekiq' }
    when 'staging' then
      config.redis = { url: "redis://{{ redis_host }}:6379", namespace: 'sidekiq' }
    else
      config.redis = { url: 'redis://127.0.0.1:6379', namespace: 'sidekiq' }
  end
end

Sidekiq.configure_client do |config|
  case Rails.env
    when 'production' then
      config.redis = { url: "redis://{{ redis_host }}:6379", namespace: 'sidekiq' }
    when 'staging' then
      config.redis = { url: "redis://{{ redis_host }}:6379", namespace: 'sidekiq' }
    else
      config.redis = { url: 'redis://127.0.0.1:6379', namespace: 'sidekiq' }
  end
end

Capistrano

RailsアプリをデプロイするCapistrano3の設定を下記のようにします。(関係ありそうな部分のみピックアップしてます)

# config valid only for current version of Capistrano
lock '3.4.0'

set :application, 'panel'
set :repo_url, 'git@bitbucket.org:tasaki-i3/nslides.git'

# Default deploy_to directory is /var/www/my_app_name
set :deploy_to, '/app/nslides/panel'

# Default value for :linked_files is []
set :linked_files, fetch(:linked_files, []).push('config/database.yml', 'config/secrets.yml', 'config/cable.yml', 'config/initializers/sidekiq.rb')

# Default value for linked_dirs is []
set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'tmp/sessions', 'vendor/bundle', 'public/system')

# capistrano/git/sub_directory_strategy
set :git_strategy, Capistrano::Git::SubDirectoryStrategy
set :project_root, 'panel'

Ansibleの方で/app/nslides/panel/sharedの下にファイルやフォルダをセットしてありますので、それらをリンクするようにしています。 なお、リポジトリ内のサブディレクトリにRAILS_ROOTがあるため、git_strategyを変更してます。

参考: capistrano 3 でサブディレクトリをデプロイするやつ · GitHub

実行

さて、準備ができたところでAnsibleを実行してインフラを整備します。

$ cd REPO_ROOT
$ ansible-playbook -i provision/inventories/ec2.py provision/site.yml --private-key=~/.ssh/nslides01.pem

PLAY [AWSリソースのセットアップ] *********************************************************

GATHERING FACTS ***************************************************************
ok: [localhost]

TASK: [elasticache | ElastiCache用セキュリティグループを作成] ***
changed: [localhost] => {"changed": false, "group_id": "sg-bdcf08d9"}

TASK: [elasticache | AWS ElastiCacheのセットアップ] ********************
changed: [localhost] => {"changed": false, "elasticache": {"data": {"AutoMinorVersionUpgrade": true, "CacheClusterCreateTime": 1456835365.451, "CacheClusterId": "nslides01", "CacheClusterStatus": "available", "CacheNodeType": "cache.t2.micro", "CacheNodes": [{"CacheNodeCreateTime": 1456835365.451, "CacheNodeId": "0001", "CacheNodeStatus": "available", "Endpoint": {"Address": "nslides01.xxxxxx.0001.apne1.cache.amazonaws.com", "Port": 6379}, "ParameterGroupStatus": "in-sync", "SourceCacheNodeId": null}], "CacheParameterGroup": {"CacheNodeIdsToReboot": [], "CacheParameterGroupName": "default.redis2.8", "ParameterApplyStatus": "in-sync"}, "CacheSecurityGroups": [], "CacheSubnetGroupName": "default", "ClientDownloadLandingPage": "https://console.aws.amazon.com/elasticache/home#client-download:", "ConfigurationEndpoint": null, "Engine": "redis", "EngineVersion": "2.8.24", "NotificationConfiguration": null, "NumCacheNodes": 1, "PendingModifiedValues": {"CacheNodeIdsToRemove": null, "EngineVersion": null, "NumCacheNodes": null}, "PreferredAvailabilityZone": "ap-northeast-1a", "PreferredMaintenanceWindow": "thu:17:00-thu:18:00", "ReplicationGroupId": null, "SecurityGroups": [{"SecurityGroupId": "sg-bdcf08d9", "Status": "active"}]}, "name": "nslides01", "status": "available"}}

TASK: [elasticache | 作成したElastiCacheのエンドポイント] *********
changed: [localhost] => {
    "msg": "The new ElastiCache endpoint is {'Port': 6379, 'Address': 'nslides01.xxxxx.0001.apne1.cache.amazonaws.com'}"
}

...

上記のようなのがずらずらーっと出力されて実行が完了します。(一発でうまくはいきませんけどね (^^;

続いてRailsアプリのデプロイです。

$ cd REPO_ROOT/panel
$ bundle exec cap production deploy
INFO [91dc5ead] Running /usr/bin/env mkdir -p /tmp/panel/ as ec2-user@xx.xx.xx.xx
INFO [91dc5ead] Finished in 0.099 seconds with exit status 0 (successful).
INFO Uploading /tmp/panel/git-ssh.sh 100.0%
INFO [ba2628eb] Running /usr/bin/env chmod +x /tmp/panel/git-ssh.sh as ec2-user@xx.xx.xx.xx
INFO [ba2628eb] Finished in 0.100 seconds with exit status 0 (successful).
INFO [0a23f6b1] Running /usr/bin/env mkdir -p /tmp/panel/ as ec2-user@xx.xx.xx.xx
INFO [0a23f6b1] Finished in 0.099 seconds with exit status 0 (successful).
INFO Uploading /tmp/panel/git-ssh.sh 100.0%
INFO [ecc80eb7] Running /usr/bin/env chmod +x /tmp/panel/git-ssh.sh as ec2-user@xx.xx.xx.xx
INFO [ecc80eb7] Finished in 0.100 seconds with exit status 0 (successful).
Please enter branch (release):

...

上記のようなのがずらずらーっと出力されて実行完了です。

おわりに

AnsibleでElastiCacheをセットアップしてRails5のActionCableやSidekiqから利用するための設定を紹介しました。

Ansibleでばばーっとインフラが立ち上がっていくのは爽快感がありますね。誰かが「ピタゴラスイッチ感」って名づけてましたがそれを実感できました。

ただ、なかなか一発で全部が動くことはなく試行錯誤の上でやっとここまでたどり着いた感じです。なので、既に本番稼働しているシステムに対してplaybookを修正して再度実行するときはかなーりドキドキしますね。ステージング環境用意した方がいいかなぁ。あー、でもAWS無料枠超えてしまうな。うーむ。。。