ここ最近は既存のインフラを片っ端からコード化していた @sinsoku です。
インフラの魔物をコードに封印している感じがある。汝のあるべき姿に戻れ!
— 神速 (@sinsoku_listy) 2018年1月10日
やっとコード化が一段落したので、インフラ周りでやってきたことを技術ブログにまとめました。
作業をする前の状況
Forkwell のインフラ環境は2016年夏頃に「第1回 インフラがコード化されていないのはヤバい!」議論が起き、タスクの優先度が上がりました。 このときは @ta1kt0me が頑張ってくれて、既存 EC2 インスタンスを Ansible で作れるようにしてくれました。
しかし、弊社では AWS の ALB、EC2、RDS、ElastiCache、...など、いくつものサービスを使っています。 この AWS リソースはコード化されておらず、手作業で作っていました。
2017年12月末
サーバを Ruby 2.5.0 に上げる話が挙がり、そのとき「第2回 インフラが(以下略」の議論が起きました。
- 不要そうな AWS リソースがあるけど、消すのが怖い
- どこかで使われているかも...
- 既存の設定にした意図が残っていない
- デフォルト値と異なる箇所が分からない
- コード化されていないのでレビューできない
といった課題は前々からありましたし、2017年も終わろうとしているのにまだ Immutable Infrastructure になっていないのも微妙です。
そこで、この機に AWS リソースもコード化する方針を立てて、対応することにしました。
Terraform の導入
AWS リソースをコード管理できるツールは AWS CloudFormation、Terraform、Ansibleといくつか選択肢があります。
ただ、将来的には GitHub Organization などもコードで管理したいこともあって、Terraform を選択しました。*1
staging 環境のリソースをインポートする
いきなり production 環境のリソースを管理するのは怖いので、まずは staging 環境のリソースを管理することにしました。
- terraforming
terraform import
の機能terraform state
の機能
を駆使し、terraform.tfstate
を作っていきます。
EC2 インスタンスをインポートする例
まずは既存のインスタンスを全部 ec2_instances.tf
に出力します。
$ terraforming ec2 > ec2_instances.tf
次に、今回は管理対象外である production 環境の設定を頑張って削除します。
$ vim ec2_instances.tf
インポートをする前なので、 Terraform にはインスタンスの追加として認識されています。
$ terraform plan . . . Plan: xx to add, 0 to change, 0 to destroy.
1つずつ心をこめて import していきます。
$ terraform import aws_instance.web i-12345678
対応が終わったら、祈りながら plan を実行します。
$ terraform plan No changes. Infrastructure is up-to-date.
上手くインポートできれば差分が出ない状態になります。
Terraform のディレクトリ構成
インフラの設定を1つのディレクトリで管理すると plan の実行に時間がかかったり、 *.tfstate
が壊れた時のリスクが大きいため、ディレクトリを複数に分けました。
./infra app/ # Forkwell のVPC, EC2, ElastiCache, ... .terraform/ backend.tf main.tf vpc.tf ... auto_bundle_update/ # CodeBuild を使った自動bundle update設定 users/ # IAM 関係 s3_buckets/ # S3 バケット設定 metric_alarms/ # CloudWatch のアラーム関係
このディレクトリ構成が良いのかは自信ないですが、コード化さえできれば後からリファクタリングできるので、とりあえずこれで進めました。
ちなみに、他ディレクトリの値は terraform_remote_state を使って参照する作戦です。
セキュリティグループのテスト
セキュリティグループの設定はミスるとサービスが動かなくなったり、特定の機能がエラーになって非常に危険です。 このため、不要そうな設定であっても消しづらく、残ってしまいがちです。
そこで、既存のセキュリティグループに対するテストコードを書きました。
RSpec.describe 'staging' do let(:bastion_ip) { '123.456.789.1' } let(:bastion_user) { 'ec2-user' } let(:bastion) { Net::SSH::Proxy::Command.new("ssh #{bastion_user}@#{bastion_ip} -W %h:%p") } def mysql_config { host: '127.0.0.1', username: 'root', password: ENV['STAGING_DATABASE_PASSWORD'], database: 'forkwell_staging' } end describe 'in app server' do let(:server_ip) { '123.456.789.2' } let(:server_user) { 'forkwell' } let(:server) { Net::SSH::Gateway.new(server_ip, server_user, proxy: bastion) } it 'connects to MySQL server' do server.open(ENV['STAGING_DATABASE_HOST'], 3306) do |port| client = Mysql2::Client.new(mysql_config.merge(port: port)) results = client.query("SELECT VERSION();") expect(results).to include("VERSION()" => "5.6.34-log") end end it 'connects to Redis server' do server.open(ENV['STAGING_REDIS_HOST'], 6379) do |port| redis = Redis.new(url: "redis://127.0.0.1:#{port}") expect(redis.info['redis_version']).to eq '2.8.6' end end end end
このテストにより、全ての EC2 インスタンスで疎通確認するのが簡単になり、セキュリティグループを編集しやすくなります。
今後やっていくこと
今回は(staging環境の)既存リソースをコード化することを重視していたため、Terraform の変数や属性は使いませんでした。 やっとインポート作業が一段落したので、これからは少しずつリファクタリングしていきたいと思っています。
そして、メンバー全員が Terraform に慣れてきたら production 環境のコード化も進めていきたいですね。
(半分くらい) Immutable Infrastructure な環境で働きたい人
インフラ環境も改善されつつある弊社では一緒に働きたい方を募集しています!
興味ある方は下記URLからエントリーお願いします!
*1:あと HashiCorp 社が好きだったり、Terraform の名前がカッコいい、といった個人的な好みも選定に影響しています