Forkwell のインフラをコード化するためにやったこと

ここ最近は既存のインフラを片っ端からコード化していた @sinsoku です。

やっとコード化が一段落したので、インフラ周りでやってきたことを技術ブログにまとめました。

作業をする前の状況

Forkwell のインフラ環境は2016年夏頃に「第1回 インフラがコード化されていないのはヤバい!」議論が起き、タスクの優先度が上がりました。 このときは @ta1kt0me が頑張ってくれて、既存 EC2 インスタンスを Ansible で作れるようにしてくれました。

しかし、弊社では AWS の ALB、EC2、RDS、ElastiCache、...など、いくつものサービスを使っています。 この AWS リソースはコード化されておらず、手作業で作っていました。

2017年12月末

サーバを Ruby 2.5.0 に上げる話が挙がり、そのとき「第2回 インフラが(以下略」の議論が起きました。

  • 不要そうな AWS リソースがあるけど、消すのが怖い
    • どこかで使われているかも...
  • 既存の設定にした意図が残っていない
    • デフォルト値と異なる箇所が分からない
  • コード化されていないのでレビューできない

といった課題は前々からありましたし、2017年も終わろうとしているのにまだ Immutable Infrastructure になっていないのも微妙です。

f:id:sinsoku:20180119104029p:plain

そこで、この機に AWS リソースもコード化する方針を立てて、対応することにしました。

Terraform の導入

AWS リソースをコード管理できるツールは AWS CloudFormationTerraform、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からエントリーお願いします!

jobs.forkwell.com

*1:あと HashiCorp 社が好きだったり、Terraform の名前がカッコいい、といった個人的な好みも選定に影響しています