TerraformでEC2を作成する

今の環境

転職したらTerraformを使うから覚えてね。と言われた。

手元にはRails tutorialで作ったRailsアプリケーションのプロジェクトがある。Git管理(GitHubにpushしてる)されている。これをTerraformでAWSにデプロイできるようにしてみる。

…のが最後の目標だけど、そもそもTerraformわからん!なのでEC2インスタンスを作るところからスタートしてみる。

作成したいAmazonイメージを取得する

  1. AmazonナビゲーションコンソールからEC2を選択
  2. インスタンスの作成 からAMIイメージ一覧をみる
  3. 作成したいイメージのAMIを見つける

今回はami-04d3eb2e1993f679bを選択することにしてみた。

f:id:MofuMofu:20181013175155p:plain

Terraformファイルを作る

作業しているプロジェクトフォルダの直下に.tfファイルと.tfvarsファイルを作成する。

.tfファイルはTerraformのコンフィグレーションファイルとされていて、実際のクラウドリソースをどう組むかを設定する。 記載はTerraform独自のフォーマット形式っぽい。JSONも使えみたいだけど、そうすると拡張子が.tf.jsonになる。違いはあるのかな〜。

.tfvarsファイルはvariableを指定する。variable = 変数なので、環境情報やクレデンシャルキーを変数として登録し、.tfファイルでは生でデータを書かなくていいですってことらしい。これは.gitignoreに追加しておかないと、普通にアカウント乗っ取られちゃうやつ!

.tfvarsファイルをバージョン管理しないようにする

というわけで、ファイルを作成する前に.gitignoreを編集する。GitHubのgitignoreプロジェクトにTerraform用のファイルもあるので、それを参考にしてもいいかも。

# .gitignoreの一番下に追加
# Ignore .tfvars file
*.tfvars
# Ignore terraform logs
*.tfstate
*.tfstate.backup
# Local .terraform directories
**/.terraform/*

拡張子.tfvarsは全部バージョン管理しない、とした。こうしておけば間違えて接続情報をgit commitしなくてすむはず!

.tfvarsファイルを作る

さっそく.tfvarsファイルをつくる。環境情報ならみて写せばいいだけだし、やってる感が出るしね!1場所は先ほども書いたプロジェクト直下とした。Dockerfileとかと扱いが一緒なのかね。こういうのどうしてるのか気になるわ〜

➜  Rails-tutorial-sampleApp git:(terraform) ✗ ls -al
total 168
drwxr-xr-x  37 mallow  staff   1184 10 13 11:36 .
drwxr-xr-x  18 mallow  staff    576  9 25 10:17 ..
-rw-r--r--@  1 mallow  staff   6148  6 15 11:56 .DS_Store
drwxr-xr-x   3 mallow  staff     96  6  4 17:23 .bundle
-rw-r--r--   1 mallow  staff    192  7  2 09:56 .byebug_history
drwxr-xr-x   3 mallow  staff     96  8 31 14:38 .circleci
-rw-r--r--   1 mallow  staff     61  8 16 13:47 .dockerignore
drwxr-xr-x   4 mallow  staff    128  8 31 14:38 .ebextentions
drwxr-xr-x   3 mallow  staff     96  8 31 13:10 .elasticbeanstalk
drwxr-xr-x  16 mallow  staff    512 10 13 11:37 .git
-rw-r--r--   1 mallow  staff    710 10 13 11:28 .gitignore
-rw-r--r--   1 mallow  staff      5  6  4 17:01 .ruby-version
-rw-r--r--   1 mallow  staff    256  8 31 14:38 Dockerfile
-rw-r--r--   1 mallow  staff    374  8 20 11:27 Dockerrun.aws.json
-rw-r--r--   1 mallow  staff   1175  9  5 13:08 Gemfile
-rw-r--r--   1 mallow  staff  10417  9 16 13:07 Gemfile.lock
-rw-r--r--   1 mallow  staff   1935  6  4 22:07 Guardfile
-rw-r--r--@  1 mallow  staff     39  7  3 16:05 Procfile
-rw-r--r--   1 mallow  staff   1310  6  4 17:01 README.md
-rw-r--r--   1 mallow  staff    227  6  4 17:01 Rakefile
drwxr-xr-x  11 mallow  staff    352  7 13 22:27 app
drwxr-xr-x   9 mallow  staff    288  8 16 18:44 bin
drwxr-xr-x  15 mallow  staff    480  8 16 17:44 config
-rw-r--r--   1 mallow  staff    130  6  4 17:01 config.ru
drwxr-xr-x   7 mallow  staff    224  7 31 18:17 db
-rw-r--r--   1 mallow  staff    223  8 21 15:58 docker-compose.yml
-rw-r--r--   1 mallow  staff    508  6 15 12:35 example_user.rb
drwxr-xr-x   4 mallow  staff    128  6  4 17:01 lib
drwxr-xr-x   7 mallow  staff    224  8 30 14:26 log
-rw-r--r--   1 mallow  staff     68  6  4 17:01 package.json
drwxr-xr-x   9 mallow  staff    288  6  4 17:01 public
-rw-r--r--@  1 mallow  staff    198  8 21 17:50 start.sh
-rw-r--r--@  1 mallow  staff      0 10 13 11:36 terraform.tfvars ←これを新規に作成
drwxr-xr-x  11 mallow  staff    352  7  8 16:52 test
drwxr-xr-x   7 mallow  staff    224  6  4 18:09 tmp
drwxr-xr-x   3 mallow  staff     96  6  4 17:01 vendor

肝心のファイルの中身はこういう風に書く。

aws_access_key_id = "IAMの認証用ID"
aws_secret_access_key = "IAMの認証用キー"

変数を書いておくだけなので、aws_access_key_idのところは好きな名前にしておけば良い。プログラミング言語と似ているな〜と思った。

.tfファイルを作る

今度は実際の設定を書く。

variable "aws_access_key_id" {}
variable "aws_secret_access_key" {}
variable "region" {
  default = "ap-northeast-1"
}

provider "aws" {
  access_key = "${var.aws_access_key_id}"
  secret_key = "${var.aws_secret_access_key}"
  region     = "${var.region}"
}

resource "aws_instance" "hello_world_terraform" {
  ami           = "ami-04d3eb2e1993f679b"
  instance_type = "t2.micro"
  tags {
    Name = "rails-tutorial-application"
  }
}

変数の宣言はvariable "変数名" {代入したい値}とする。最近仕事でVue.js始めたので、この辺は「なるほど〜〜」感がある。「インフラエンジニアじゃなくてもリソース操作がしやすいんです!」って売りは、こういう風にプログラミング言語と似ている感を出しているところにあるのかもしれない。違うかもしれない。

AWSはリソースをどのリージョンで起動するか選択する必要があるので、変数にregionを追加しておく。tfvarsファイルに書いてもいいんだろうけど、そうするとGitでバージョン管理できないので「これどのリージョンで上がってるんですかねえ」とかいう話になっちゃうんだろうな。

provider "aws" {
  access_key = "${var.aws_access_key_id}"
  secret_key = "${var.aws_secret_access_key}"
  region     = "${var.region}"
}

providerの欄では、どのサービスを利用するか指定する。TerraformはAWS以外にもHerokuとかOpenStackとかGoogle Cloudも操作できるので指定しないといけないのだ。一覧はドキュメントに載っていた。いっぱいあるな〜。

変数を利用するときはJavaScript(ECMAScript2015)のテンプレートリテラルのように${変数名}とする。2この辺りはVSCodeでTerraform拡張を入れておくと、予測して入力できるので便利。

resource "aws_instance" "hello_world_terraform" {
  ami           = "ami-04d3eb2e1993f679b"
  instance_type = "t2.micro"
  tags {
    Name = "rails-tutorial-application"
  }
}

ここではAWSのどんなサービスを利用するのかを指定する。S3だったらaws_instanceaws_s3_bucketにする。一覧はAWS Providerのページに載っていた。

hello_world_terraformの部分はリソースにつけるお名前らしい。

あとはどのAMIイメージを利用するか、とかインスタンスタイプどうする?とかを記述する。

terraformコマンドでEC2を立てる

プロジェクトにTerraformの動作に必要なファイルをダウンロードする。

$ terraform init
➜  Rails-tutorial-sampleApp git:(terraform) terraform init

Initializing provider plugins...
- Checking for available provider plugins on https://releases.hashicorp.com...
- Downloading plugin for provider "aws" (1.40.0)...

The following providers do not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* provider.aws: version = "~> 1.40"

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

.tfファイルのproviderを読み込んで必要なファイルをダウンロードしてくれるんだね。

今度は.tfファイルが間違っていないか、などを確認する。

$ terraform plan
➜  Rails-tutorial-sampleApp git:(terraform) ✗ 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.


------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  + aws_instance.hello_world_terraform
      id:                           <computed>
      ami:                          "ami-04d3eb2e1993f679b"
      arn:                          <computed>
      associate_public_ip_address:  <computed>
      availability_zone:            <computed>
      cpu_core_count:               <computed>
      cpu_threads_per_core:         <computed>
      ebs_block_device.#:           <computed>
      ephemeral_block_device.#:     <computed>
      get_password_data:            "false"
      instance_state:               <computed>
      instance_type:                "t2.micro"
      ipv6_address_count:           <computed>
      ipv6_addresses.#:             <computed>
      key_name:                     <computed>
      network_interface.#:          <computed>
      network_interface_id:         <computed>
      password_data:                <computed>
      placement_group:              <computed>
      primary_network_interface_id: <computed>
      private_dns:                  <computed>
      private_ip:                   <computed>
      public_dns:                   <computed>
      public_ip:                    <computed>
      root_block_device.#:          <computed>
      security_groups.#:            <computed>
      source_dest_check:            "true"
      subnet_id:                    <computed>
      tags.%:                       "1"
      tags.Name:                    "rails-tutorial-application"
      tenancy:                      <computed>
      volume_tags.%:                <computed>
      vpc_security_group_ids.#:     <computed>


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

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

➜  Rails-tutorial-sampleApp git:(terraform) ✗

実際にリソースを作る

terraform applyコマンドでリソースを作成する。

$ terraform apply
➜  Rails-tutorial-sampleApp git:(terraform) ✗ terraform apply

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  + aws_instance.hello_world_terraform
      id:                           <computed>
      ami:                          "ami-04d3eb2e1993f679b"
      arn:                          <computed>
      associate_public_ip_address:  <computed>
      availability_zone:            <computed>
      cpu_core_count:               <computed>
      cpu_threads_per_core:         <computed>
      ebs_block_device.#:           <computed>
      ephemeral_block_device.#:     <computed>
      get_password_data:            "false"
      instance_state:               <computed>
      instance_type:                "t2.micro"
      ipv6_address_count:           <computed>
      ipv6_addresses.#:             <computed>
      key_name:                     <computed>
      network_interface.#:          <computed>
      network_interface_id:         <computed>
      password_data:                <computed>
      placement_group:              <computed>
      primary_network_interface_id: <computed>
      private_dns:                  <computed>
      private_ip:                   <computed>
      public_dns:                   <computed>
      public_ip:                    <computed>
      root_block_device.#:          <computed>
      security_groups.#:            <computed>
      source_dest_check:            "true"
      subnet_id:                    <computed>
      tags.%:                       "1"
      tags.Name:                    "rails-tutorial-application"
      tenancy:                      <computed>
      volume_tags.%:                <computed>
      vpc_security_group_ids.#:     <computed>


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

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aws_instance.hello_world_terraform: Creating...
  ami:                          "" => "ami-04d3eb2e1993f679b"
  arn:                          "" => "<computed>"
  associate_public_ip_address:  "" => "<computed>"
  availability_zone:            "" => "<computed>"
  cpu_core_count:               "" => "<computed>"
  cpu_threads_per_core:         "" => "<computed>"
  ebs_block_device.#:           "" => "<computed>"
  ephemeral_block_device.#:     "" => "<computed>"
  get_password_data:            "" => "false"
  instance_state:               "" => "<computed>"
  instance_type:                "" => "t2.micro"
  ipv6_address_count:           "" => "<computed>"
  ipv6_addresses.#:             "" => "<computed>"
  key_name:                     "" => "<computed>"
  network_interface.#:          "" => "<computed>"
  network_interface_id:         "" => "<computed>"
  password_data:                "" => "<computed>"
  placement_group:              "" => "<computed>"
  primary_network_interface_id: "" => "<computed>"
  private_dns:                  "" => "<computed>"
  private_ip:                   "" => "<computed>"
  public_dns:                   "" => "<computed>"
  public_ip:                    "" => "<computed>"
  root_block_device.#:          "" => "<computed>"
  security_groups.#:            "" => "<computed>"
  source_dest_check:            "" => "true"
  subnet_id:                    "" => "<computed>"
  tags.%:                       "" => "1"
  tags.Name:                    "" => "rails-tutorial-application"
  tenancy:                      "" => "<computed>"
  volume_tags.%:                "" => "<computed>"
  vpc_security_group_ids.#:     "" => "<computed>"
aws_instance.hello_world_terraform: Still creating... (10s elapsed)
aws_instance.hello_world_terraform: Still creating... (20s elapsed)
aws_instance.hello_world_terraform: Still creating... (30s elapsed)
aws_instance.hello_world_terraform: Creation complete after 37s (ID: i-0964794aa28ae38c5)

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
➜  Rails-tutorial-sampleApp git:(terraform) ✗

どんなものができたかはterraform.tsstateファイルで確認できる。これもgitignoreに入れといていいかも。

実際にコンソールで確認してみる。

f:id:MofuMofu:20181013175219p:plain

お!できている!

設定を確認すると、publicIPの割り当てがされていて、かつセキュリティグループもデフォルト(in/outオールオッケー)でできてしまう。これは細かく設定しないとダメだなー。

作ったものを消す

というわけで、早速リソースを削除する。

$ terraform destory
➜  Rails-tutorial-sampleApp git:(terraform) ✗ terraform destroy
aws_instance.hello_world_terraform: Refreshing state... (ID: i-0964794aa28ae38c5)

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  - aws_instance.hello_world_terraform


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

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

aws_instance.hello_world_terraform: Destroying... (ID: i-0964794aa28ae38c5)
aws_instance.hello_world_terraform: Still destroying... (ID: i-0964794aa28ae38c5, 10s elapsed)
aws_instance.hello_world_terraform: Still destroying... (ID: i-0964794aa28ae38c5, 20s elapsed)
aws_instance.hello_world_terraform: Still destroying... (ID: i-0964794aa28ae38c5, 30s elapsed)
aws_instance.hello_world_terraform: Still destroying... (ID: i-0964794aa28ae38c5, 40s elapsed)
aws_instance.hello_world_terraform: Still destroying... (ID: i-0964794aa28ae38c5, 50s elapsed)
aws_instance.hello_world_terraform: Destruction complete after 52s

Destroy complete! Resources: 1 destroyed.

f:id:MofuMofu:20181013175245p:plain

Terminatedなので、リソースの削除中ということがわかる。

詰まったところ

変数ファイルをどうやったら読み込めるのかがわからない

terraform planなどを実行する際、どうやって変数ファイルを読み込むのかわからなかった。

  • -var-file=変数名ファイルオプションをつける
  • ファイル名をterraform.tfvarsとしておく
  • ファイル名に.auto.tfvarsをつけて保存する

これらのどれかを利用すれば、読み込むことができる。ただし、tfファイルに記述した変数名とtfvarsファイルに記述した変数名が合わなければ、変数名のファイルが読み込まれても意味ないので注意(1敗)。

べんりだったもの

普段からVisual Studio Codeを使っているのだけど、Terraform拡張があったのでインストールした。シンタックスハイライトがつくだけで見通しがよくなるのですき。入力補完もやってくれる。あと、コンフィグの解説がポップアップで出てくる。大変便利です。

参考


  1. やってる感はとても大事だと思う。何も進まないとテンション下がるよね。

  2. Goにもテンプレートリテラルがあって、同じ書き方をするみたい。Rubyにもテンプレートリテラルがあるけど、#{}って書くみたい。他の言語はどうなんだろう?