AnsibleでElastiCacheをセットアップしてRails5から利用する
下記の記事で紹介したスライド共有システム「nslides」ではAnsibleを用いてインフラ周りの設定をしています。今回はその中からElastiCacheでredisを立ち上げてRails5から利用する設定をしている部分を紹介します。
nslidesはこちらで公開中です。 公開終了しました (2017/02/27)
システム構成:
- アプリケーション
- インフラ
- Ops
- Ansible 1.9.4
- Capistrano 3.4.0
コードは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の解説は下記サイトなどを参照ください。
- 【Ansible】EC2 External Inventory Scriptを使った動的ホスト一覧生成 | Developers.IO
- Ansible Dynamic Inventoryを使ってAWSのEC2インスタンスに付与されたタグでプロビジョニング対象を決める - Qiita
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_groups
はVPCがない頃?の古いインスタンスたちのときに利用するオプションのようですが、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無料枠超えてしまうな。うーむ。。。