クラウド時代のサーバ管理手法。エウレカでのサーバリソース調達,プロビジョニング,テストへの取り組みについて

こんにちは!Pairs事業部の恩田です。今回は、弊社エウレカで実践しているサーバリソースの管理手法、また実際のリソース調達やプロビジョニング、テストへの取り組みを紹介します。

はじめに

弊社エウレカで運営しているサービス”Pairs”はAWS上にサービスを構築しています。おかげさまでユーザ数360万人を突破するまでに成長し、現在も順調にユーザ数を伸ばしています。それに伴い、快適なユーザ体験を届ける為に当然サーバ台数は増え、また各種機能改修に伴いバックエンドの構成も複雑化してきています。

これらサーバ群の整備を省力化、運用する手法としてInfrastcucture as a codeに代表されるようなサーバ調達やプロビジョニングのコード化、自動化が上げられます。上記を実現するツール(terraform/ansible/chef等)の台頭もあり、手順書ベースでの人力プロビジョニングは機会が減りつつあります。ですが、これらレシピを適切にグルーピング化されたサーバ群に対して適用するのは、また1段階別の苦労があります(開発環境用の設定が本番サーバに適用されたのでは、大事故ですよね )

クラウドネイティブな時代のサーバ管理

AWSに代表されるようなクラウドサービスは、サーバ調達や各種操作がCLI等でプログラマブルに操作できる事が最大の特徴です。これらを用いる事で、各種プロビジョニング操作をIPアドレスベースではなく抽象化されたrole単位で実行する事が可能になります。ここではAWSの例を上げたいと思います。AWSにはタグ付けに機能があり、これを用いる事でサーバリソースに一意な命名、また役割に応じたタグを複数設定する事が可能です。このタグ名をキーにしてAWS CLIを用いてプロビジョニング対象のサーバ一覧を取得、プロビジョニングレシピなりテストを流す事が可能です。

Infrastructure as a codeの体現と、実現の為のツール

サーバのサービスインまで、時系列でいうと大きく以下3ステップに分類されます。

  • 1:リソース調達
  • 2:プロビジョニング
  • 3:テスト

弊社では、リソース作成にTerraform、プロビジョニングにansible、テストの実行にはserverspecを用いています。Terraformでリソース作成時にroleに応じたタグを付与。ansibleやserverspec実行時にはaws cliを用いてサーバ群のtag名ベースでホストを取得、サーバの所属するグループに対して各種操作/テストの実行可能にしています

実例・webアプリケーション稼働環境構築

以前書かせて頂いた記事、”これだけ押さえておけば大丈夫!Webサービス向けVPCネットワークの設計指針”の例を基に、ここでは以下のようなサーバ構成を考えます。

  • ELB x 1
  • webサーバ x 2
  • DBサーバ x 2(マスタ + スレーブ )

また、弊社では台湾にもサービス展開している為、日台でそれぞれ同一の環境が1つづつ、また社内検証環境として
ステージング環境を用意しています。(それぞれ、サーバのスペックや台数、構成等は等は異なります)

terraformによるリソース作成

弊社では以下のような構成でリソースを管理しています。ディレクトリ単位で環境(prod/stage)、リージョン(日本/台湾)を表現し、その中にサービス構成で必要なコンポーネントのレシピを押し込みます。

$ tree                                                                                                               
.
├── prod
│ ├── jp
│ │ ├── ebs.tf
│ │ ├── ec2.tf
│ │ ├── elasticache.tf
│ │ ├── elb.tf
│ │ ├── route53.tf
│ │ ├── sqs.tf
│ │ ├── terraform.tfstate
│ │ ├── terraform.tfstate.backup
│ │ ├── variables.tf
│ │ └── vpc.tf
│ └── tw
│ ├── ebs.tf
│ ├── ec2.tf
│ ├── elasticache.tf
│ ├── elb.tf
│ ├── route53.tf
│ ├── sqs.tf
│ ├── terraform.tfstate
│ ├── terraform.tfstate.backup
│ ├── variables.tf
│ └── vpc.tf
└── stage
├── ebs.tf
├── ec2.tf
├── elb.tf
├── route53.tf
├── sqs.tf
├── terraform.tfstate
├── terraform.tfstate.backup
├── variables.tf
└── vpc.tf

各種レシピ詳細は割愛しますが、ここでサーバ作成部分のレシピを上げてみます。

# Appサーバ
resource "aws_instance" "web_1" {
ami                   = "${var.ec2.app.ami_id}"
instance_type = "${var.ec2.app.instance_type}"
availability_zone = "${var.vpc.region_1a}"
security_groups = ["${aws_security_group.app.id}"]
subnet_id = "${aws_subnet.app_1a.id}"
ebs_optimized = "${var.ec2.app.ebs_optimized}"
iam_instance_profile = "${var.ec2.app.iam_instance_profile}"
monitoring = "true"
count = 1
tags {
Name = "Pairs-jp-web1"
region = "jp"
env = "prod"
group = "Pairs-jp-web"
}
}
# DBサーバ(マスタ)
resource "aws_instance" "db_master" {
ami                   = "${var.ec2.db_master.ami_id}"
instance_type = "${var.ec2.db_master.instance_type}"
availability_zone = "${var.vpc.region_1a}"
security_groups = ["${aws_security_group.db.id}"]
subnet_id = "${aws_subnet.db_1a.id}"
ebs_optimized = "${var.ec2.db_master.ebs_optimized}"
iam_instance_profile = "${var.ec2.db_master.iam_instance_profile}"
monitoring = "true"
count = 1
tags {
Name = "Pairs-jp-db-master"
region = "jp"
env = "prod"
role = "Pairs-jp-db-master"
}
}
# DBサーバ(スレーブ)
resource "aws_instance" "db_slave" {
ami                   = "${var.ec2.db_master.ami_id}"
instance_type = "${var.ec2.db_master.instance_type}"
availability_zone = "${var.vpc.region_1a}"
security_groups = ["${aws_security_group.db.id}"]
subnet_id = "${aws_subnet.db_1a.id}"
ebs_optimized = "${var.ec2.db_master.ebs_optimized}"
iam_instance_profile = "${var.ec2.db_master.iam_instance_profile}"
monitoring = "true"
count = 1
tags {
Name = "Pairs-jp-db-slave1"
region = "jp"
env = "prod"
role = "Pairs-jp-db-slave"
}
}
各サーバリソースを一意に命名する"Name"タグと、プロビジョニンググループを表す"role"をそれぞれ定義しています。管理上、各リソースにはユニークなタグ名を当てたい(dnsやhostname用途)が、プロビジョニングやテスト対象としては複数台をまとめて扱いたいので、roleという単位でグルーピングしています。
# 実行
cd /path/to/terraform/prod/jp
terraform plan
terraform apply
ansibleによるプロビジョニング
次にansibleです。弊社では各種ホスト情報を管理するディレクティブを以下のような構成にしています(playbook等は割愛)
$ tree                                                                                                               
.
├── ec2.ini
├── ec2.py
├── Pairs-jp
│ ├── prod
│ │ ├── ec2.ini
│ │ ├── ec2.py
│ │ ├── group_vars
│ │ │ ├── all.yml
│ │ │ ├── db-master.yml
│ │ │ └── web.yml
│ │ └── inventory
│ └── stage
│ ├── ec2.ini
│ ├── ec2.py
│ ├── group_vars
│ │ ├── all.yml
│ │ ├── db-master.yml
│ │ └── web.yml
│ └── inventory
└── Pairs-tw
├── prod
│ ├── ec2.ini
│ ├── ec2.py
│ ├── group_vars
│ │ ├── all.yml
│ │ ├── db-master.yml
│ │ └── web.yml
│ └── inventory
└── stage
├── ec2.ini
├── ec2.py
├── group_vars
│ ├── all.yml
│ ├── db-master.yml
│ └── web.yml
└── inventory
ansibleにはdynamic inventoryという仕組みがあり、レシピ実行時に対象ホストの情報をaws cli等用いて取得、json形式で渡してやる事で事前にIPアドレスのリストを定義する事なく対象ホストを動的に決定する事ができます。terraformと同じくディレクトリ単位で環境とリージョンを表現し、その中にサービス構成で必要なコンポーネントの設定ファイルを押し込みます。プロビジョニング用レシピは全環境共通のplaybookを用い、各種ミドルウェア等の設定値等はこれらディレクトリ以下に配置しています。各環境ごとにディレクトリ直下にinventoryというファイルを設定し、対象グループを管理します。
[tag_group_Pairs-jp-web]
[tag_group_Pairs-jp-db-master]
[tag_group_Pairs-jp-db-slave]
[common:children]
tag_role_Pairs-jp-web
tag_role_Pairs-jp-db-master
tag_role_Pairs-jp-db-slave
[web:children]
tag_group_Pairs-jp-web
[db-master:children]
tag_group_Pairs-jp-db-master
[db-master:children]
tag_group_Pairs-jp-db-slave
DBのslaveサーバやwebサーバは将来的に複数台構成になる事が考えられるので、一意な命名であるNameタグを用いるのではなく、
プロビジョニング対象のグループを表したroleを用いてグループ化されるのがミソです。次に、role毎のplaybookを用意します。以下のようなイメージです。
# For web-server
# Usage:ansible-playbook -i hosts/Pairs-jp/prod web.yml
- hosts: web
roles:
- { role: common, tags: common }
- { role: redis, tags: redis }
- { role: nginx, tags: nginx }
- { role: mackerel, tags: mackerel }
- { role: circus, tags: circus }
- { role: td-agent, tags: td-agent }
- { role: haproxy, tags: haproxy }
# For db-master-server
# Usage:ansible-playbook -i hosts/Pairs-jp/prod db-master.yml
- hosts: db-master
roles:
- { role: common, tags: common }
- { role: mysql, tags: mysql }
# For db-slave-server
# Usage:ansible-playbook -i hosts/Pairs-jp/prod db-slave.yml
- hosts: db-slave
roles:
- { role: common, tags: common }
- { role: mysql, tags: mysql }
実行時のインタフェースは上記usage通りです。また、ansibleはワンライナーで叩く事もできるので、適切にinventoryファイル内で
サーバがグループ化されていれば、以下のように各種shellコマンドを叩きたい場合も簡単です
# 日本 x prod環境全サーバ
$ ansible common -i host/Pairs-jp/prod -m shell -a "echo test"
# 日本 x prod環境webサーバ
$ ansible web -i host/Pairs-jp/prod -m shell -a "echo test"
# 日本 x prod環境DB slaveサーバ
$ ansible db-slave -i host/Pairs-jp/prod -m shell -a "echo test"
# 日本 x stage環境DB webサーバ
$ ansible web -i host/Pairs-jp/stage -m shell -a "echo test"
$ ansible web -i hosts/Pairs-jp/prod -m shell -a"echo test"
10.0.0.1 | SUCCESS | rc=0 >>
test
10.0.1.1 | SUCCESS | rc=0 >>
test
10.0.0.2 | SUCCESS | rc=0 >>
test
10.0.1.2 | SUCCESS | rc=0 >>
test
$ ansible db-slave -i hosts/Pairs-jp/prod -m shell -a"echo test"
10.0.2.1 | SUCCESS | rc=0 >>
test
10.0.3.1 | SUCCESS | rc=0 >>
test
10.0.2.2 | SUCCESS | rc=0 >>
test
10.0.3.2 | SUCCESS | rc=0 >>
test
これで、ansibleでregion x env x role毎にタスクが実行可能になりました
serverspec
serverspecも同様に、AWS CLIで取得したtag名ベースでtaskを実行します。terraform/ansibleと同じくディレクトリ単位で環境とリージョンを表現し、その中にテストを定義していきます。
$ tree                                                                                                          [04/01 14:13][master|rebase]
.
├── Gemfile
├── Gemfile.lock
├── Rakefile
└── spec
├── common
│ └── common_spec.rb
├── Pairs-jp
│ └── prod
│ ├── Pairs-jp-db-master_spec.rb
│ ├── Pairs-jp-db-slave_spec.rb
│ └── Pairs-jp-web_spec.rb
└── spec_helper.rb
テストタスクを定義するRakeFileは以下のようなイメージです。aws sdkを用いて各ホストのtagを取得、
タグ名からenv/region/roleを取得し、実行タスクを定義しています。
require 'rake'
require 'rspec/core/rake_task'
require 'aws-sdk-v1'
# AWS SDKで稼働中のホスト一覧を取得する
if ENV['AWS_ACCESS_KEY_ID'] && ENV['AWS_SECRET_ACCESS_KEY']
AWS.config(
{
access_key_id: ENV['AWS_ACCESS_KEY_ID'],
secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'],
region: 'ap-northeast-1'
}
)
# 何かフィルター入れたければ自由に
ec2_hosts = AWS.ec2.instances.select { |i| i.status == :running}
end
task :spec    => 'spec:all'
task :default => :spec
namespace :spec do
targets = []
Dir.glob('./spec/*').each do |dir|
next unless File.directory?(dir)
target = File.basename(dir)
target = "_#{target}" if target == "default"
targets << target
end
task :all     => targets
task :default => :all
# AWS SDKで取得した値を元にテストタスクを作成
if ec2_hosts
ec2_hosts.each do |host|
host_env = host.tags.env
host_region = host.tags.region
host_role = host.tags.role
host_name = host.tags.Name
host_ip = host.public_ip_address || host.private_ip_address
host_user = ENV['SSH_USER']
host_key = ENV['SSH_KEY']
task_name = "#{host_env}:#{host_region}:#{host_role}"
spec_pattern = "spec/Pairs-#{host_region}/#{host_env}/#{host_role}_spec.rb"
# テストタスクを定義
# ex:rake spec:prod:jp:web
desc "Run serverspec tests to ec2 #{host_name} (PATH=#{spec_pattern},IP=#{host_ip})"
RSpec::Core::RakeTask.new(task_name.to_sym) do |t|
ENV['TARGET_HOST'] = host_ip
ENV['TARGET_HOST_NAME'] = host_name
ENV['TARGET_HOST_USER'] = host_user
ENV['TARGET_IDENTITYFILE'] = host_key
t.pattern = "spec/Pairs-#{host_region}/#{host_env}/#{host_role}_spec.rb,spec/common/*_spec.rb"
end
end
end
end
各ホストのenv/regionタグを元に実行テストの配置ディレクトリを決定、role毎のタスクを発射可能にしています。
また、全環境共通のテストはcommonという独自ディレクトリに配置し、どのテスト実行時も実行されるようにします。
他にも、webサーバのテスト項目で環境関係なく共通のテスト等もあれば、適宜web_commonといった形で切り出していってもいいでしょう。
# テストタスク一覧
$ rake -T
$ rake spec:prod:jp:Pairs-jp-web
$ rake spec:prod:jp:Pairs-jp-db-master
$ rake spec:prod:jp:Pairs-jp-db-slave
ここまでの実装で、コマンドラインベースで各環境ごとのリソース調達/プロビジョニング/テスト実行が可能になりました。あとは、hubotと組み合わせてchatops的な運用にするなり、様々な応用も効きます。
終わりに
今回は弊社でのサーバリソース管理についてお話しました。次回はセキュリティ管理についての記事を書こうかなーと画策中です。では、皆様よいGWを!