見出し画像

TerraformでAWSリソースを作成してみる #482

Terraformでインフラをいじる機会が増えてきたので一度体系的に学びたく、以下の書籍を参考に勉強しています。

とても理解しやすく書かれており、良書だと思います。
ここで学んだことを少しずつアウトプットしていきます。

Terraformとは

Terraformは、IaC (Infrastructure as Code) を実現する手段の1つで、大きな括りではプロビジョニングツールという分類になります。プロビジョニングツールはサーバ、DB、キャッシュ、ロードバランサー、キュー、監視、サブネット設定、ファイフォール設定、SSL証明書など、インフラに関するほとんどあらゆるものを作成できます。

また、IaCツールの中でも手続き型宣言型という2つのスタイルがあり、Terraformは宣言型に該当します。両者の違いは、前者は実行する内容をコード化するのに対し、後者は最終的な状態をコード化します。

つまり、手続き型のツールを何度も実行すると同じ内容でいくつもリソースが作成されますが、宣言型では前回から変更箇所がなければ何もしません。チームで長く管理することを考えると、宣言型の方が扱いやすそうですね。

Terraformはどう動くのか

TerraformはHashicorpによって作られ、Go言語で書かれたオープンソースツールです。AWS、Azure、Google Cloud、DigitalOcean、OpenStackなどに対応していますが、これは各プロバイダに対してAPIコールを行うことで実現されています。

APIで各プロバイダにコマンドを送信し、プロバイダのエージェントが各サーバー上でそのコマンドを実行してくれるため、自分のローカルにエージェントを追加でインストールする必要はありません。

基本文法

最も基本的な文法についてメモしておきます。

リソース

作成するリソースは以下のように定義できます。

resource "<プロバイダ>_<タイプ>" "名前" {
  [設定...]
}

例えばAWSのEC2インスタンスを作成する場合は以下のようになります。

resource "aws_instance" "example" {
    ami = "ami-0fb653ca2d3203ac1"
    instance_type = "t2.micro"
    vpc_security_group_ids = [ aws_security_group.my_instance.id ]

    # インスタンスの最初の起動時に実行されるスクリプト
    user_data = <<-EOF
                #!/bin/bash
                echo "Hello, World" > index.html
                nohup busybox httpd -f -p ${var.server_port} &
                EOF
    
    # user_dataを変更してapplyした際、元のインスタンスをターミネートして全く新しいインスタンスを起動するための設定
    # user_dataは最初の起動時にしか実行されないため
    user_data_replace_on_change = true

    tags = {
      Name = "terraform-example"
    }
}

入力変数

他のプログラミング言語と同様に、Terraformでも変数に値を格納して使うことができます。

variable "名前" {
  [設定...]
}

例えばサーバーのポート番号を格納する場合は以下です。

variable "server_port" {
    description = "The port the server will use for HTTP requests"
    type = number
    default = 8080
}

出力変数

これはterraform applyでリソースを生成した時に表示されたり、Terraformモジュールを使っている場合の、値の引き渡しに使えます。(例えば、1つのモジュールで生成されたリソースのIDやARNを、別のモジュールで利用したい場合など。モジュールについては後日まとめます。)

output "名前" {
  value = <値>
  [設定...]
}

例えばELBを使っていて、そのDNS名を出力したい場合は以下です。

output "alb_dns_name" {
    value = aws_lb.my_example.dns_name
    description = "The domain name of the load balancer"
}

データソース

Terraformを動かすたびにプロバイダ(AWSなど)から取得する読み出し専用の情報です。これを定義しても何もリソースは作成されません。プロバイダのAPIでデータ取得し、それをTerraformコード内で使えるようにするだけです。

data "<プロバイダ>_<タイプ>" "名前" {
  [CONFIG...]
}

例えばデフォルトVPCのデータを取得する時は以下です。

data "aws_vpc" "default" {
    default = true  
}

以下のようにして取り出せます

data.aws_vpc.default.id

これを使って、重ねてVPC内のサブネットのデータを取得する時は以下です。

data "aws_subnets" "default" {
    filter {
        name = "vpc-id"
        values = [data.aws_vpc.default.id]
    }
}

基本コマンド

コードが書けたら、以下の手順でコマンドを実行してインフラを作成していけます。

$ terraform init
$ terraform plan
$ terraform apply
$ terraform destroy

terraform init

Terraformがコードをスキャンし、どのプロバイダを使うのか判断し、そのプロバイダに関するコードをダウンロードするためのコマンドです。デフォルトでは、プロバイダのコードは「.terraform」フォルダにダウンロードされ(.gitignoreすることを推奨)、.terraform.lock.hclファイルにもその情報を持ちます。

このコマンドは最初に実行する必要があり、何度実行しても問題ありません。

terraform plan

実際に変更を加える前にTerraformが何をする予定なのかを確認できます。インフラを触る前に最終確認ができます。ただ、ここで事前確認できる内容には限界があるみたいで、planでは無問題だったのに次のapplyでコケる、という事象がよくあります。

terraform apply

これを実行すると実際にリソースが作成・変更されます。前述の通り、planでは上手くいってもここでコケることがあります。例えば私は以下に直面しました。

AutoScaling groupの設定で、サンプルでは以下のようになっており、かつplanは無事に通っていました。

resource "aws_launch_configuration" "my_example" {
    image_id = "0fb653ca2d3203ac1"
    instance_type = "t2.micro"
    security_groups = [ aws_security_group.my_instance.id ]

    user_data = <<-EOF
                #!/bin/bash
                echo "Hello, World" > index.html
                nohup busybox httpd -f -p ${var.server_port} &
                EOF
    
    # Autoscaling Groupがある起動設定を使った場合に必須
    lifecycle {
        create_before_destroy = true
    }
}

ただしこれでは以下のエラーが出ました。

Error: creating Auto Scaling Launch Configuration (terraform-20240811124644967400000001): operation error EC2: DescribeImages, https response error StatusCode: 400, RequestID: b574174d-de24-4a77-8f1d-ca2562f56952, api error InvalidAMIID.Malformed: Invalid id: "0fb653ca2d3203ac1" (expecting "ami-...")
│ 
│   with aws_launch_configuration.my_example,
│   on main.tf line 36, in resource "aws_launch_configuration" "my_example":
│   36: resource "aws_launch_configuration" "my_example" {

どうもimage_tagの所に「ami-」を付ける必要があったみたいです。
以下で通過しました。

resource "aws_launch_configuration" "my_example" {
    image_id = "ami-0fb653ca2d3203ac1"
    instance_type = "t2.micro"
    security_groups = [ aws_security_group.my_instance.id ]

    user_data = <<-EOF
                #!/bin/bash
                echo "Hello, World" > index.html
                nohup busybox httpd -f -p ${var.server_port} &
                EOF
    
    # Autoscaling Groupがある起動設定を使った場合に必須
    lifecycle {
        create_before_destroy = true
    }
}

他にもELBのセキュリティグループの設定で、以下のようにしているとエラーが出ました。これは凡ミスですが、planでは検出されませんでした。

resource "aws_security_group" "alb" {
    name = "terraform-example-alb"

    # インバウンドHTTPリクエストを許可
    ingress {
        from_port = 80
        to_port = 80
        protocol = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
    }

    #  アウトバウンドHTTPリクエストを許可
    egress {
        from_port = 80
        to_port = 80
        protocol = "-1"
        cidr_blocks = ["0.0.0.0/0"]
    }
}

エラーの内容は以下の通りで、egressでprotocolを「-1 (なんでもOK)」にするならportは指定するな、という内容です。

Error: updating Security Group (sg-0ac573eac927d0d12) egress rules: updating rules: from_port (80) and to_port (80) must both be 0 to use the 'ALL' "-1" protocol!
│ 
│   with aws_security_group.alb,
│   on main.tf line 72, in resource "aws_security_group" "alb":
│   72: resource "aws_security_group" "alb" {
│ 

ポートを0にしたら通りました。

    #  アウトバウンドHTTPリクエストを許可
    egress {
        from_port = 0
        to_port = 0
        protocol = "-1"
        cidr_blocks = ["0.0.0.0/0"]
    }

ここまでやって、その後に再度実行すると以下のエラーが出ました。

Error: waiting for Auto Scaling Group (terraform-20240811125414427700000002) capacity satisfied: scaling activity (fa064506-040f-9f76-9710-266ba715c4f9): Failed: Access denied when attempting to assume role arn:aws:iam::345042987984:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling. Validating load balancer configuration failed.
│ 
│   with aws_autoscaling_group.my_example,
│   on main.tf line 53, in resource "aws_autoscaling_group" "my_example":
│   53: resource "aws_autoscaling_group" "my_example" {

ただしこれは原因が分からずで、、一度後述のdestroyを実行して、翌日に再度terraform applyしたら出ませんでした。再実行すると上手くいくケースもあるのかな。

terraform destroy

最後にdestroyコマンドです。実務でこれを使うことはほぼ無いと思います。これはここで作成したリソースを全て削除するコマンドです。所謂「前に戻る」機能は無いので、とてもシンプルに全削除してくれます。

チュートリアルで色々と試している時はとても便利です。


ここまでお読みいただきありがとうございました!!


いいなと思ったら応援しよう!