CloudShellでTerraformを実行してみる


はじめに

Cloudshellで使うメリットは、、、
個人の環境の差を気にしなくてよい!
CLI等のインストールがいらない!
です。
なので、とりあえずterraform触ってみたいとか、
社内勉強会とかにはいいかなと思います!

少しでも参考になる箇所があれば幸いです。

Terraformってなに?

Terraformは、何百ものクラウドサービスを管理するための一貫したCLIワークフローを提供するコードソフトウェアツールとしてのオープンソースインフラストラクチャです。

ほう…
ちなみによくわからん…

簡単に言えば、インフラ環境をコードで管理する(IaC)を実現するサービス!
とか言ってもわからんもんはわからん…

なんでもそうだけど、触ってみるのが早いので触ってニュアンス覚えたろ!
無料だし!
(筆者はAWSしか触ったことないので、AWSで…)

ちなみに触るだけならCloudShellがおすすめです!
https://www.terraform.io/

今回作る構築

今回は、ネットワークの基礎的なところを作成していきたいと思います。

CloudShellからTerraformを使ってみる

AWS CloudShellは、ブラウザベースのシェルで、AWSリソースの安全な管理、探索、操作を簡単に行うことができます。CloudShellは、お客様のコンソール認証情報で事前に認証されています。一般的な開発ツールや運用ツールがあらかじめインストールされているので、ローカルでのインストールや設定は必要ありません。CloudShellを使えば、AWSコマンドラインインターフェイス(AWS CLI)を使ってスクリプトを素早く実行したり、AWS SDKを使ってAWSサービスAPIを実験したり、その他様々なツールを使って生産性を高めることができます。CloudShellは、ブラウザからすぐに使用でき、追加費用もかかりません。

https://aws.amazon.com/jp/cloudshell/?nc1=h_ls
要するに、便利なツールいっぱい詰んでるTerminalですね。
裏はコンテナが動いてるみたいです。
試す分にはこいつがめちゃくちゃ便利です。
では、さっそくTerraformをインストールしましょう!
CloudShellはAmazon Linuxなので、このリンクのLinuxタブ→Amazon Linuxタブから手順通りに進めればインストールが可能です。
まずはAWSのマネジメントコンソールから、CloudShellを開きます。
この時、リージョンが東京であることを確認してください。
暫くたつとCloudShellが使用可能な状態になりますので、以下コードを実行します。

sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo
sudo yum -y install terraform

さて、インストールはこれで完了です。
ここからパスを通します。

sudo mkdir /home/app
sudo mv /bin/terraform /home/app/
export PATH="$PATH:/home/app/"

これでパスが通りました。
terraform versionなどを打って正常に反応が来るか確認しましょう!

$ terraform version
Terraform v0.14.9

こんなん帰ってきたらおk!
社内ハンズオンの場合はここで、リポジトリ用意しておいてクローンしましょう!

今回は一つずつ作成してみます!

事前準備

terraform.stateファイルを格納する場所が必要です。
1つS3バケットを作成しておきます!

aws s3 mb s3://terraform-handson-${ランダムな番号}

Terraformで使用するファイル

構成

今回はなーんにも考えずに全部一階層にぶち込みます!

terraform_handson
     ├ provider.tf
         ├ backend.tf
         ├ local.tf
         ├ variable.tf
         ├ output.tf
         ├ vpc.tf
         ├ subnet.tf
         ├ igw.tf
         ├ ngw.tf
         ├ rtb.tf
         ├ eip.tf

とりあえずterraform_handsonフォルダを作成して移動します。

mkdir terraform_handson && cd $_

provider.tf

Terraform構成では、Terraformがそれらをインストールして使用できるように、必要なプロバイダーを宣言する必要があります。さらに、一部のプロバイダーは、使用する前に構成(エンドポイントURLやクラウドリージョンなど)を必要とします。

ファイル作成

vi provider.tf

今回はAWSを使用するので、AWS Providerを宣言します。
ついでに、デフォルトは東京リージョンなので、オレゴンリージョンもエイリアスを付けて宣言ておきます!
ファイルの中身は以下の通りです。

provider "aws" {
  version = "= 3.14.1"
}

provider "aws" {
  version = "= 3.14.1"
  alias   = "oregon"
  region  = "us-west-2"
}

backend.tf

provider.tfと同じようにファイル作成してください。(以下省略)

各Terraform構成は、操作が実行される場所と方法、状態 スナップショットが保存される場所などを正確に定義するバックエンドを指定できます。ほとんどの重要なTerraform構成は、複数のユーザーが同じインフラストラクチャで作業できるようにリモートバックエンドを構成します。

terraform.stateファイルを置く場所を指定します!
S3バケットに格納するのが一般的です。

terraform {
  backend "s3" {
    bucket  = "terraform-handson-${ランダムな番号}"
    key     = "handson/terraform"
    region  = "ap-northeast-1"
    encrypt = true
  }
}

local.tf

ローカル値は、構成内で同じ値または式を複数回繰り返さないようにするのに役立ちますが、使いすぎると、使用される実際の値を非表示にすることで、将来のメンテナが構成を読みにくくする可能性があります。
ローカル値は、単一の値または結果が多くの場所で使用され、その値が将来変更される可能性がある状況で、適度にのみ使用してください。中央の場所で値を簡単に変更できることは、ローカル値の主な利点です。

今回はローカル値にサブネットを指定します。
ルートテーブルの関連付けをする際に、繰り返し処理で管理できるので!

locals {
    pri_subnet_id_tokyo = {
        pri-subnet-first-handson-tokyo = aws_subnet.pri-subnet-first-handson-tokyo.id,
        pri-subnet-second-handson-tokyo = aws_subnet.pri-subnet-second-handson-tokyo.id
    }
}

locals {
    pri_subnet_id_oregon = {
        pri-subnet-first-handson-oregon = aws_subnet.pri-subnet-first-handson-oregon.id,
        pri-subnet-second-handson-oregon = aws_subnet.pri-subnet-second-handson-oregon.id
    }
}

variable.tf

入力変数はTerraformモジュールのパラメーターとして機能し、モジュール自体のソースコードを変更せずにモジュールの側面をカスタマイズできるようにし、異なる構成間でモジュールを共有できるようにします。
構成のルートモジュールで変数を宣言する場合、CLIオプションと環境変数を使用してそれらの値を設定できます。子モジュールでそれらを宣言する場合、呼び出し元のモジュールはmoduleブロック内の値を渡す必要があります。

モジュールを使用する際に大活躍のvariableさんですね。
今回は試しにリソース名に変数でも入れてみます。

variable "number" {}

output.tf

出力値は、terraform apply時に出力したい場合や、他のアカウントでその値を参照したい場合に必要です。
(モジュールのリソースを上階層のアカウントで参照した場合も必要)
今回は特にそういった用途はないですが、何となくEIPでも見ようかなと…

output "eip-address-tokyo" {
  value = aws_eip.eip-handson-tokyo.public_ip
}

output "eip-address-oregon" {
  value = aws_eip.eip-handson-oregon.public_ip
}

Resource

ここからは各リソースファイルをサラッとご紹介

vpc.tf

resource "aws_vpc" "vpc-handson-tokyo" {
  cidr_block = "10.102.0.0/17"

  tags = {
    Name = "vpc-handson-tokyo-${var.number}"
  }
}

resource "aws_vpc" "vpc-handson-oregon" {
  provider = aws.oregon
  cidr_block = "10.103.0.0/17"

  tags = {
    Name = "vpc-handson-oregon-${var.number}"
  }
}

今回のようにString型の文字列の中に変数を入れる場合は${var.~~}という書き方が必要です。
例えば、変数が丸っと名前の場合は

Name = var.number

だけで出来ます!

Name = ${var.number}

としても通りますが、そのやり方古いよ?と怒られちゃいます。

subnet.tf

resource "aws_subnet" "pub-subnet-handson-tokyo" {
  vpc_id = aws_vpc.vpc-handson-tokyo.id

  availability_zone = "ap-northeast-1a"

  cidr_block = "10.102.0.0/24"

  tags = {
    Name = "pub-subnet-handson-tokyo-${var.number}"
  }
}

resource "aws_subnet" "pri-subnet-first-handson-tokyo" {
  vpc_id = aws_vpc.vpc-handson-tokyo.id

  availability_zone = "ap-northeast-1a"

  cidr_block = "10.102.1.0/24"

  tags = {
    Name = "pri-subnet-first-handson-tokyo-${var.number}"
  }
}

resource "aws_subnet" "pri-subnet-second-handson-tokyo" {
  vpc_id = aws_vpc.vpc-handson-tokyo.id

  availability_zone = "ap-northeast-1c"

  cidr_block = "10.102.2.0/24"

  tags = {
    Name = "pri-subnet-second-handson-tokyo-${var.number}"
  }
}

resource "aws_subnet" "pub-subnet-handson-oregon" {
  provider = aws.oregon
  vpc_id = aws_vpc.vpc-handson-oregon.id

  availability_zone = "us-west-2a"

  cidr_block = "10.103.0.0/24"

  tags = {
    Name = "pub-subnet-handson-oregon-${var.number}"
  }
}

resource "aws_subnet" "pri-subnet-first-handson-oregon" {
  provider = aws.oregon
  vpc_id = aws_vpc.vpc-handson-oregon.id

  availability_zone = "us-west-2a"

  cidr_block = "10.103.1.0/24"

  tags = {
    Name = "pri-subnet-first-handson-oregon-${var.number}"
  }
}

resource "aws_subnet" "pri-subnet-second-handson-oregon" {
  provider = aws.oregon
  vpc_id = aws_vpc.vpc-handson-oregon.id

  availability_zone = "us-west-2b"

  cidr_block = "10.103.2.0/24"

  tags = {
    Name = "pri-subnet-second-handson-oregon-${var.number}"
  }
}

eip.tf

resource "aws_eip" "eip-handson-tokyo" {
  vpc      = true
}

resource "aws_eip" "eip-handson-oregon" {
  provider = aws.oregon
  vpc      = true
}

igw.tf

resource "aws_internet_gateway" "igw-handson-tokyo" {
  vpc_id = aws_vpc.vpc-handson-tokyo.id

  tags = {
    Name = "igw-handson-tokyo-${var.number}"
  }
}

resource "aws_internet_gateway" "igw-handson-oregon" {
  provider = aws.oregon
  vpc_id = aws_vpc.vpc-handson-oregon.id

  tags = {
    Name = "igw-handson-oregon-${var.number}"
  }
}

ngw.tf

resource "aws_nat_gateway" "ngw-handson-tokyo" {
  allocation_id = aws_eip.eip-handson-tokyo.id
  subnet_id     = aws_subnet.pub-subnet-handson-tokyo.id

  tags = {
    Name = "ngw-handson-tokyo-${var.number}"
  }
}

resource "aws_nat_gateway" "ngw-handson-oregon" {
  provider = aws.oregon
  allocation_id = aws_eip.eip-handson-oregon.id
  subnet_id     = aws_subnet.pub-subnet-handson-oregon.id

  tags = {
    Name = "ngw-handson-oregon-${var.number}"
  }
}

rtb.tf

resource "aws_route_table" "pub-rtb-handson-tokyo" {
  vpc_id = aws_vpc.vpc-handson-tokyo.id
  tags = {
    Name = "pub-rtb-handson-tokyo-${var.number}"
  }
}

resource "aws_route_table" "pri-rtb-handson-tokyo" {
  vpc_id = aws_vpc.vpc-handson-tokyo.id
  tags = {
    Name = "pri-rtb-handson-tokyo-${var.number}"
  }
}

resource "aws_route_table" "pub-rtb-handson-oregon" {
  provider = aws.oregon
  vpc_id = aws_vpc.vpc-handson-oregon.id
  tags = {
    Name = "pub-rtb-handson-oregon-${var.number}"
  }
}

resource "aws_route_table" "pri-rtb-handson-oregon" {
  provider = aws.oregon
  vpc_id = aws_vpc.vpc-handson-oregon.id
  tags = {
    Name = "pri-rtb-handson-oregon-${var.number}"
  }
}

resource "aws_route" "pub-route-handson-tokyo" {
  route_table_id         = aws_route_table.pub-rtb-handson-tokyo.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.igw-handson-tokyo.id
  depends_on             = [aws_route_table.pub-rtb-handson-tokyo]
}

resource "aws_route" "pri-route-handson-tokyo" {
  route_table_id         = aws_route_table.pri-rtb-handson-tokyo.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_nat_gateway.ngw-handson-tokyo.id
  depends_on             = [aws_route_table.pri-rtb-handson-tokyo]
}

resource "aws_route" "pub-route-handson-oregon" {
  provider = aws.oregon
  route_table_id         = aws_route_table.pub-rtb-handson-oregon.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.igw-handson-oregon.id
  depends_on             = [aws_route_table.pub-rtb-handson-oregon]
}

resource "aws_route" "pri-route-handson-oregon" {
  provider = aws.oregon
  route_table_id         = aws_route_table.pri-rtb-handson-oregon.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_nat_gateway.ngw-handson-oregon.id
  depends_on             = [aws_route_table.pri-rtb-handson-oregon]
}

resource "aws_route_table_association" "pub-rtb-assoc-handson-tokyo" {
  route_table_id = aws_route_table.pub-rtb-handson-tokyo.id
  subnet_id      = aws_subnet.pub-subnet-handson-tokyo.id
}

resource "aws_route_table_association" "pri-rtb-assoc-handson-tokyo" {
  for_each = local.pri_subnet_id_tokyo

  route_table_id = aws_route_table.pri-rtb-handson-tokyo.id
  subnet_id      = each.value
}

resource "aws_route_table_association" "pub-rtb-assoc-handson-oregon" {
  provider = aws.oregon
  route_table_id = aws_route_table.pub-rtb-handson-oregon.id
  subnet_id      = aws_subnet.pub-subnet-handson-oregon.id
}

resource "aws_route_table_association" "pri-rtb-assoc-handson-oregon" {
  provider = aws.oregon
  for_each = local.pri_subnet_id_oregon

  route_table_id = aws_route_table.pri-rtb-handson-oregon.id
  subnet_id      = each.value
}

ここでfor_each急に出てきてなんなん?
と思われたかもしれません。

サブネットって実際はもっと大量にあると思います。
そいつらをいちいちルートテーブルに関連付けるだけで新しいリソースセクション作成してるとコードが肥大化してしまうので、for_eachを使って繰り返し処理をしています。
for_each = local.pri_subnet_id_oregonでmap型を代入しておきます。

例えば

for_each =  {
  a = apple
  b = banana
}

のとき、
each.key = [a, b]
each.value = [apple, banana]
のようなイメージで繰り返されていきます。

構築してみる

さてここまで長かったですね。
ちなみにCloudshellは接続が一度切れるとHomeより上の階層がリセットされてしまうので、パスを通しなおす必要があります。
実践向きではないですね…

飛ばしている方がほとんどだと信じています。

それでは、まずはinitから!
terraform init

準備が整ったところで、terraform plan!!
variableに指定していた値を入力しろ!と言われました

適当に入力してEnter押します!

いろいろ表示されますが、
Plan: 28 to add, 0 to change, 0 to destroy.
になっていればとりあえずオッケー!

terraform applyしましょう!

ほんとにApplyしちゃうよ?
ってファイナルアンサー?が来るので
yesと入力し、Enter!!

リソースの作成が開始されます!
nat_gatewayがちょっと時間食いますね…

Applyが成功して、outputに指定していたEIPのアドレスが出力されました!
実際の中身をマネコンで確認してみてください!

ではterraform destroyします!

また聞かれるので、答えてあげてください。

そしてまたも、みのさん、、、ファイナルアンサー???
yes!!!!

削除が始まりました!

削除できました!!

最後にS3の削除をお忘れなきよう…

aws s3 rb s3://terraform-handson-${ランダムな番号} --force

まとめ

今回は、CloudShellでTerraformを実行しました。
主なユースケースは社内勉強会とかになるかなと思います。
個人の環境の違いを考慮しなくてよくなるので、非常に楽ですね!
Cloud9と違って無料ですし…

それではまた!