Infrastructure as Code実践による3つのメリットとは? 〜Terraform0.7アップデート事例を紹介〜

はじめまして。インフラチームの恩田です。

 

今回は、成長期の会社におけるインフラリソース管理の課題と、先月リリースされたTerraform0.7を用いたレガシーコードのInfrastructure as Code推進による課題解決事例について書きたいと思います。

■ 会社の成長とインフラリソースの肥大化

エウレカではPairs / Couples始めとした各種プロダクト、コーポレートサイト、採用サイト、オウンドメディア、社内向けシステムなど、様々なWeb上のリソースをAWS上で運営しています。また弊社インフラチームは事業部横断で各種インフラリソース全般の管理をしています。

 

サービスの急成長に伴ってAWSのようなクラウドリソースを使っていると、構成の最適化よりまずはスケールアップ/アウトで逃げる、といった対応をしがちです。また、人の出入りに伴い属人化したサーバーリソースや、そもそも作成背景の不明なリソース、アドホックなリソース変更対応の残骸が散在する、といった状況が発生します。

 

これはサーバーリソースのコスト高やセキュリティリスクを放置する事になるので好ましい状況ではありません。

■ Infrastructure as Codeの実践によるメリットとは

ここで、近年のインフラ管理手法として常識となりつつあるInfrastructure as Codeという言葉について考えてみます。文字通りインフラレイヤのコンポーネントをアプリケーションと同じようにコード化、管理する手法を指す言葉です。では具体的になにが嬉しいのでしょうか。

エウレカでInfrastructure as Codeを実践する中で筆者が感じるメリットは以下の3点です。

  • 1 : セキュリティリスクの軽減
  • 2 : サーバーコスト削減
  • 3 : Githubフロー導入による非属人的なワークフローの確立

■ セキュリティリスクの軽減

network_1

セキュリティといっても様々なレイヤがありますが、今回は主にインターネット経由で接続されるアプリケーションのネットワークレイヤについての話をします。AWSはGUIベースで様々な変更を加えられる分、なぜこの修正が入ったのか正しく履歴を記録する術がないと、あっという間に構成が汚れていきます。以下、実際に弊社で起こっていた事例を上げてみます。

  • 謎のIPホワイトリストが定義されてるSecurity Group
  • 謎のポートがグローバルに解放されてるNetworkACL
  • VPC Peeringの接続先が、存在しないVPCを指したまま放置されてる

ネットワーク構成をコード化=現在発生中のセキュリティリクスが即解決、というわけでは
ありません。ですが、例えば社内+外部ベンダーのみアクセス可能な管理画面のIPホワイトリストがコード管理されていれば、pull-requestをたどって変更履歴と背景も分かるので安心感がありますし、以後の管理/変更のコストは激減します。

■ サーバコスト削減

broke

エウレカではPairs、Couples、コーポレートサイトを始め、Webに存在するほぼ全てのリソースはAWS上に構築しています。AWS上のリソース肥大化に伴い、例えば突発的なスパイク等の対応の名残でスケールアウトして急場をしのいだ後ストップされて放置されたサーバー。または、キャパシティプランニングを真面目に行われないまま検証用途で作成、実際には使わず稼働し続けてるリソース(主にEC2、EBS)といった不要なリソースが散在していました。以下、実際に弊社で起こっていた事例を上げてみます。

  • 使われてる形跡は無いが3ヶ月以上前から稼働してるEC2
  • 500GBのEBSがマウントされたままStop状態のEC2
  • 理由なく高Provisioned IOPSが付与されたままの検証用RDS

AWSのクラウドリソースは基本的に時間課金のため、対応放置 = コストの垂れ流しです。またコンポーネントによっては、Stop状態 ≠ お金がかからない、ので要注意です。これらの課題に対し、リソースのコード化を進めることで、稼働中のサーバを正確に把握することができます。また、新規リソース作成時も、後述するような第三者によるキャパシティプランニング妥当性をチェックする機会が設けられるので、結果不要なコストを削ることができます。

■ Githubフロー導入による非属人的なワークフローの確立

弊社インフラチームではAWSのGUIからのリソース作成・変更を原則禁止しています。
リソースの作成、変更はコードベースによる修正とGithub-Flowによるレビュー、本番反映という流れを徹底しています。このルール化によって、以下のようなメリットが得られます。

  • 1:リソース操作の属人化解消
  • 2:キャパシティプランニングの徹底

1に関しては、チーム内でのナレッジの属人化を防ぐというメリットの他にも、非インフラチームのエンジニアでもコードの修正のみでリソースの作成/変更が可能になることを意味します。

 

2に関しては

  • なぜこの修正を行うのか
  • スペックは妥当か

上記観点について第三者の目を通すことで、サーバーコストの高騰化やヒューマンエラーを防ぎつつ、スピード感をもって本番への変更適用が可能になります。

 

■ 銀の弾丸ではない。だが、課題解決への有効な手段

Infrastructure as Codeの推進は、セキュリティリスク対策やコスト圧縮等のサーバーリソース
管理課題に対する銀の弾丸ではありません。ですが、長期的な目線で見れば間違いなく有効な手段です。

 

弊社では、ネットワークやDNSレコード、サーバーリソースなどの管理は全てTerraformに集約し、インフラツール用レポジトリで一元管理しています。

 

* ミドルウェアのレシピ等はAnsibleで管理しています。ご興味ある方は以下をご参照下さい。
Pairsのプロビジョニングフローについて

provisioning-flow

 

■ 課題・レガシーリソースのコード化

以下、Terraformを用いたリソースのコード化を前提に話を進めます。新サービスなど、インフラリソースを新規で作成する場合のコード化は簡単です。しかし、既にクラウドリソースが存在する長年運用してきたサービスの場合、以下のステップでコード化を進める必要があります。

  • 1 : 現リソースを完全に表現した状態ファイルを作成する
  • 2 : 状態ファイルと対になるレシピを作成する
  • 3 : 生成レシピと状態ファイルに差分がない状態にする
  • 4 : 不要リソースの断捨離や最適化を行う

1番の課題は、コードベースでの不要リソースの断捨離や最適化を行う前に、既存リソースを状態ファイル上で100%表現できている必要がある、ということです。

 

Terraformの場合、terraform.tfstateが状態ファイルにあたります。ですが、例えばEC21台を
コード化する場合を考えても状態ファイルは記述内容が多く、コード化作業は大変です。

例:EC2一台を表現するtfstate例


"aws_instance.ec_test_1": {
  "type": "aws_instance",
  "depends_on": [
    "aws_security_group.db",
    "aws_subnet.private_1a"
  ],
"primary": {
  "id": "i-xxxx",
  "attributes": {
    "ami": "ami-000000",
    "availability_zone": "ap-northeast-1a",
    "disable_api_termination": "false",
    "ebs_block_device.#": "1",
    "ebs_block_device.4023988449.delete_on_termination":  "true",
    "ebs_block_device.4023988449.device_name": "/dev/xvdf",
    "ebs_block_device.4023988449.encrypted": "false",
    "ebs_block_device.4023988449.iops": "xxxx",
    "ebs_block_device.4023988449.snapshot_id": "",
    "ebs_block_device.4023988449.volume_size": "xxxx",
    "ebs_block_device.4023988449.volume_type": "gp2",
    "ebs_optimized": "true",
    "ephemeral_block_device.#": "0",
    "iam_instance_profile": "Pairs-ec2-instance-role",
    "id": "i-3a4e769f",
    "instance_state": "running",
    "instance_type": "r3.large",
    "key_name": "",
    "monitoring": "false",
    "network_interface_id": "eni-xxxxxx",
    "private_dns": "ip-10-xxx.xxx.xxx.ap-northeast-1.compute.internal",
    "private_ip": "10.xxx.xxx.xxx",
    "public_dns": "",
    "public_ip": "",
    "root_block_device.#": "1",
    "root_block_device.0.delete_on_termination": "true",
    "root_block_device.0.iops": "xx",
    "root_block_device.0.volume_size": "xx",
    "root_block_device.0.volume_type": "gp2",
    "security_groups.#": "0",
    "source_dest_check": "true",
    "subnet_id": "subnet-xxxxx",
    "tags.%": "4",
    "tags.Name": "stage-jp-db-master",
    "tags.env": "stage",
    "tags.group": "stage-jp-db-master",
    "tags.region": "jp",
    "tenancy": "default",
    "vpc_security_group_ids.#": "1",
    "vpc_security_group_ids.2664158570": "sg-xxxxxx"
  },
"meta": {
  "schema_version": "1"
},
  "tainted": false
},
  "deposed": [],
  "provider": ""
},

これら一つひとつを手動で泥臭くコード化していくのは得策ではありません。また、Webサーバーなど状態を持たないリソースは、Terraformで新規作成して既存レガシーリソースとリプレイスするといった手段も取れますが、稼働中のVPCネットワークやDBだと大きな作業になりがちです。

 

1つ事例をあげると、弊社の決済機能を提供するマイクロサービスは比較的早い段階で作られたコンポーネントのため、ネットワークレイヤのコード化が進んでいませんでした。3 Tierで組んだアーキテクチャの場合、ネットワークレイヤのAWSコンポーネントだけでも下記のように8つあります。

  • VPC | VPC Association to Hostzone
  • Subnet(public/app/db)
  • Internet Gateway
  • Route Table | Association
  • Nat Gateway
  • NetworkACL(public/private)
  • Security Group(public/app/db/etc,,)
  • VPC Peering

こうした事情から、レガシーリソースのコード化はこれまで大きく進んでいませんでした。

■ Terraform0.7で既存のクラウドリソースをコード化する

2016年8月、Terraformのメジャーアップデートがありました。最大の特徴は、既存リソースの状態ファイル化機能であるimportコマンドが使用可能になったことです。これにより、クラウドリソースのコード化を推進する上で課題だった状態ファイル作成作業が、一気にはかどるようになりました。

 

実際にTerraformのimport機能を使ったレガシーリソースのコード化例を示していきます。

■ Terraform0.7のインストール

Terraformは頻繁にマイナーアップデートあるので、下記の要領でinitスクリプトを書いておくと更新が簡単です。

 

なお、0.7のアップデートに伴い0.6系で存在していたprovider別バイナリファイルが全てterraformバイナリに統合されています。0.6系からアップデートする場合は、公式ドキュメントにしたがって旧バイナリファイル群を削除した方がよいでしょう。

#!/bin/bash

# バージョン毎のインストール用ディレクトリ作成
VERSION="0.7.1"
INSTALL_PATH="/usr/local/terraform/${VERSION}"
[ ! -e ${INSTALL_PATH} ] && sudo mkdir -p ${INSTALL_PATH}

# upgraade用/terraform関連のsymlink削除
cd /usr/local/bin
for symlink in `ls -1a | grep -i terraform`; do
test -L ${symlink}  \&\& sudo unlink ${symlink}
done

cd ${INSTALL_PATH}
sudo wget https://releases.hashicorp.com/terraform/${VERSION}/terraform_${VERSION}_linux_amd64.zip
sudo unzip terraform_${VERSION}_linux_amd64.zip -d ${INSTALL_PATH}
for i in `ls -1 ${INSTALL_PATH}`; do
sudo ln -fs ${INSTALL_PATH}/${i} /usr/local/bin/${i}
done

exit;
$ which terraform
/usr/local/bin/terraform

$ terraform --version Terraform v0.7.1

Your version of Terraform is out of date! The latest version
is 0.7.3. You can update by downloading from www.terraform.io

$ terraform --help
usage: terraform [--version] [--help] [args]

The available commands for execution are listed below.
The most common, useful commands are shown first, followed by
less common or more advanced commands. If you're just getting
started with Terraform, stick with the common commands. For the
other commands, please read the help and docs before usage.

Common commands:
apply Builds or changes infrastructure
destroy Destroy Terraform-managed infrastructure
fmt Rewrites config files to canonical format
get Download and install modules for the configuration
graph Create a visual graph of Terraform resources
import Import existing infrastructure into Terraform
init Initializes Terraform configuration from a module
output Read an output from a state file
plan Generate and show an execution plan
push Upload this Terraform module to Atlas to run
refresh Update local state file against real resources
remote Configure remote state storage
show Inspect Terraform state or plan
taint Manually mark a resource for recreation
untaint Manually unmark a resource as tainted
validate Validates the Terraform files,
version Prints the Terraform version

All other commands:
state Advanced state management

無事にTerraform0.7のインストールが確認できました。
次に、0.7からの新機能であるimport機能を試してみます。

■ import機能の使い方

import機能を用いて、下記の順序で既存リソースのコード化を進めます。

  • 1 : 現リソースを完全に表現した状態ファイルを作成
  • 2 : 状態ファイルと対になるレシピを作成する
  • 3 : 生成レシピと状態ファイルに差分がない状態にする
  • 4 : 不要リソースの断捨離や最適化を行う

1 : 現リソースを表現した状態ファイルを作成

弊社の事例ですが、こちらの記事で紹介したようなディレクトリ構成で各サービス、環境別にリソースを管理しています。

該当するディレクトリでimportコマンドを実行し、状態ファイルであるterraform.tfstateを新規作成(既に存在してれば追記)します。下記に、EC21台をimportする例を示します。

# EC2一台(test_uncoded_ec2)を状態ファイルに追記
$ terraform import aws_instance.test_uncoded_ec2 i-xxxxxx
provider.aws.region
The region where AWS operations will take place. Examples
are us-east-1, us-west-2, etc.

Default: us-east-1
Enter a value: ap-northeast-1

aws_instance.test_uncoded_ec2: Importing from ID "i-abxxxxxx"...
aws_instance.test_uncoded_ec2: Import complete!
Imported aws_instance (ID: i-xxxxxx)
aws_instance.test_uncoded_ec2: Refreshing state... (ID: i-abxxxxxx)

Import success! The resources imported are shown above. These are
now in your Terraform state. Import does not currently generate
configuration, so you must do this next. If you do not create configuration
for the above resources, then the next `terraform plan` will mark
them for destruction.
# 実行結果
$ git diff terraform.tfstate

{
"version": 3,
"terraform_version": "0.7.1",
- "serial": 46,
+ "serial": 47,
"lineage": "206d8d90-bdce-46c2-93d1-1b9e7db85aeb",
"modules": [
{
@@ -304,6 +304,51 @@
"deposed": [],
"provider": ""
},
+ "aws_instance.test_uncoded_ec2": {
+ "type": "aws_instance",
+ "depends_on": [],
+ "primary": {
+ "id": "i-xxxxx",
+ "attributes": {
+ "ami": "ami-xxxxx",
+ "availability_zone": "ap-northeast-1a",
+ "disable_api_termination": "false",
+ "ebs_block_device.#": "0",
+ "ebs_optimized": "true",
+ "ephemeral_block_device.#": "0",
+ "iam_instance_profile": "test-role",
+ "id": "i-xxxxx",
+ "instance_state": "running",
+ "instance_type": "m4.xlarge",
+ "key_name": "eureka",
+ "monitoring": "false",
+ "network_interface_id": "eni-xxxxxx",
+ "private_dns": "ip-10-xxx-xxx-xx.ap-northeast-1.compute.internal",
+ "private_ip": "10.xxx.xxx.xx",
+ "public_dns": "ec2-xx-xx-x-xxx.ap-northeast-1.compute.amazonaws.com",
+ "public_ip": "xx.xx.xx.xx",
+ "root_block_device.#": "1",
+ "root_block_device.0.delete_on_termination": "true",
+ "root_block_device.0.iops": "100",
+ "root_block_device.0.volume_size": "8",
+ "root_block_device.0.volume_type": "gp2",
+ "security_groups.#": "0",
+ "source_dest_check": "false",
+ "subnet_id": "subnet-xxxxx",f
+ "tags.%": "1",
+ "tags.Name": "test_uncoded_2",
+ "tenancy": "default",
+ "vpc_security_group_ids.#": "1",
+ "vpc_security_group_ids.715766139": "sg-xxxxx"
+ },
+ "meta": {
+ "schema_version": "1"
+ },
+ "tainted": false
+ },
+ "deposed": [],
+ "provider": "aws"
+ },

2 : 状態ファイルと対になるレシピを作成する

この状態でterraform planを実行すると、既存リソースが破壊されてしまうので要注意です。
状態ファイル(terraform.tfstate)には記述があるのに、レシピ(xxx.tf)に対になる該当リソースの記述がないと、terraformはリソースが存在しない状態を正として動作するためです。

# この時点でレシピを流すとtest_uncoded_ec2が破壊される
$ terraform plan

~~ 中略 ~~

Note: You didn't specify an "-out" parameter to save this plan, so when
"apply" is called, Terraform can't guarantee this is what will execute.

- aws_instance.test_uncoded_ec2

Plan: 0 to add, 0 to change, 1 to destroy.

aws_instance.test_uncoded_ec2をコード管理下に置くために、importコマンドで生成(追記)
したterraform.tfstateと対になるレシピを作成します。以下の要領でレシピを作成します。

# EC2一台(test_uncoded_ec2)を作成するレシピを.tfファイルに追記
$ git diff ec2.tf

+resource "aws_instance" "test_uncoded_2" {
+ ami = "ami-xxxxxx"
+ instance_type = "m4.xlarge"
+ availability_zone = "ap-northeast-1a"
+ vpc_security_group_ids = ["sg-xxxxx"]
+ subnet_id = "subnet-xxxxxx"
+ ebs_optimized = "true"
+ iam_instance_profile = "xxx-role"
+ monitoring = "false"
+ count = 1
+
+ tags {
+ Name = "test_uncoded_2"
+ }
+}

3 : 生成レシピと状態ファイルに差分がない状態にする

この状態でdry-runを実行します。生成レシピ(ec2.tfファイル)と状態ファイル(terraform.tfstate)に乖離がない状態であることを確認します。

$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but
will not be persisted to local or remote state storage.

~~ 中略

No changes. Infrastructure is up-to-date. This means that Terraform
could not detect any differences between your configuration and
the real physical resources that exist. As a result, Terraform
doesn't need to do anything.

これでEC21台のコード化が完了しました。後はterraform.tfstate、ec2.tfそれぞれのファイルをmasterブランチへcommitします。以後、レシピ修正と実行(terraform apply)でリソースに対して変更を加えることが可能になります。

 

今回はEC2のコード化事例を取り上げましたが、他のAWSリソースでも同様の手順でインフラリソースをコード化できます。先の弊社事例で紹介した決済マイクロサービスのネットワークレイヤのコード化においても、同じ手順で実施。現在はTerraform経由でリソースの管理/修正を行っています。

■ 終わりに

今回の話をまとめると以下です。

  • レガシーリソースの放置はコスト高やセキュリティリスクに繋がる
  • Infrastructure as Codeの実践は上記問題の解決につながる
  • Terraform0.7のimport機能でレガシーリソースのコード化を進めた

ここで紹介したTerraformはあくまでツールであり、目標達成への手段にすぎません。インフラレイヤを主として仕事をしていると、どうしても目線がシステムへ向きがちですが、あくまで考えるべきは事業への価値提供です。インフラレイヤからの観点で目指すべき目標は以下だと私は考えます。

  • 高可用性を保ちつつコストを抑え(大前提)
  • サービス全体のパフォーマンス向上へ貢献し
  • デリバリサイクル高速化によるサービス改善を後ろ押しする

この3点からブレず、日々泥臭く今後も改善を進めていきたいと思います。

  • このエントリーをはてなブックマークに追加

エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!

Recommend

「どんな価値観を持った人も活躍できる会社に」 新人事制度baniera(バニエラ)を作った理由

デザインレビューやエンジニアとのやりとりに役立つ!デザイナーでも簡単にGitで画像管理する方法