1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【AWS】IaC未経験者がTerraformを触ってみた

1
Posted at

はじめに

これまで IaC を触る機会がなかったので、代表的なツールである Terraform を使用してAWSリソースを作成してみました。

作成するリソース構成

今回は以下の構成で「自分のPCからのみパブリックアクセスできる、nginx が動く EC2サーバー」を作成します。

事前準備

Terraform / AWS CLI のインストール

Terraform 本体をインストールします。

% brew tap hashicorp/tap
% brew install hashicorp/tap/terraform

AWS を操作するための AWS CLI もインストールします。

% brew install awscli

インストールできたらバージョンを確認します。

% terraform version
% aws --version

AWSにログイン

ブラウザベースの認証を行いAWSにログインします。aws loginを実行すると自動でブラウザが開くので、AWSアカウントでサインインします。

% aws login
Attempting to open your default browser.
If the browser does not open, open the following URL:
https://ap-northeast-1.signin.aws.amazon.com/v1/authorize?...

ブラウザで認証が完了したら、ログインできているか確認します。

% aws sts get-caller-identity

Account(アカウントID)と Arn が表示されればログイン成功です。

作業フォルダの作成

任意の場所に作業用フォルダを作成します。
今回フォルダ名は「terraform-aws-handson」としています。

SSHキーペアの作成

EC2にSSH接続するためのキーペアを作成しておきます。

% ssh-keygen -t ed25519 -f ~/.ssh/iac-handson -N ""

-N "" は秘密鍵にパスフレーズを設定しない(空にする)オプションです。これにより、SSH接続時にパスフレーズの入力を求められなくなります。~/.ssh/iac-handson(秘密鍵)と ~/.ssh/iac-handson.pub(公開鍵)の2ファイルが作られます。

実装

基本のブロックと書式

Terraform のコードは、以下のようなブロックの集まりとして記述します。

ブロックの種類 "ラベル1" "ラベル2" {
  引数 = 
}

例えば resource "aws_vpc" "vpc" { ... } なら、種類が resource、ラベル1がリソースの種類(aws_vpc)、ラベル2が自分で付ける名前(vpc)です。ラベルの数はブロックの種類によって異なります。

主なブロックは以下のとおりです。

ブロック 定義する情報
terraform Terraform 本体のバージョンや、使用するプロバイダとそのバージョン
provider プロバイダの設定
resource 作成するリソース
data 他で作成済みのリソースや外部の情報を参照して取得する値
variable 外部から受け取る入力値
output 実行後に出力する値(他のモジュールからの参照にも使う)
module 再利用できるようにひとまとめにしたリソース群の呼び出し
locals コード内で使い回すために名前を付けた値

フォルダ構成

Terraformでは、ディレクトリ内の全ての最上位ファイル(.tfおよび.tf.json)が1つのモジュールとして扱われます。
1つのファイルにまとめて記述することも可能ですが、可読性のため役割ごとにファイルを分けるのが慣例です。ただし特定のルールはなく、動作への影響もありません。

今回は用途別に、以下のファイルに分けて定義しました。

terraform-aws-handson/
├── providers.tf      # Terraform / AWS プロバイダのバージョンと設定
├── variables.tf      # 変数の定義
├── terraform.tfvars  # 変数の値
├── .gitignore
├── network.tf        # リソース定義(ネットワーク系)
├── security.tf       # リソース定義(セキュリティグループ)
├── ec2.tf            # リソース定義(EC2)
└── outputs.tf        # 出力したい値

providers.tf

使用する Terraform 及び プロバイダのバージョンの指定、AWSプロバイダの設定を行います。

プロバイダ
Terraform が外部サービスのAPIを操作するためのプラグイン。
Terraform 本体はリソースの状態管理と実行計画の作成を担当するのみ。「EC2 インスタンスを作る」等の実際の操作はプロバイダが行う。

terraform {
  required_version = ">= 1.9" # Terraformのバージョン指定

  required_providers {
    aws = {
      source  = "hashicorp/aws" # Terraform Registry 上の AWS用プロバイダ
      version = "~> 6.0"        # プロバイダのバージョン指定
    }
  }
}

provider "aws" {
  region = var.region # awsのリージョン指定 ※変数から受け取る

  # tags に対応するリソースへ共通タグを自動付与する。
  default_tags {
    tags = {
      Project   = var.project
      ManagedBy = "Terraform"
    }
  }
}

variables.tf

コード内で扱う変数を宣言します。
変数の値は後述の terraform.tfvars で指定しています。

なお、値が指定されなかった場合は、default 値があればそれが使用されます。なければ実行時に入力を求められます。

variable "region" {
  type    = string
  default = "ap-northeast-1"
}

variable "project" {
  type    = string
  default = "iac-handson"
}

variable "my_ip" {
  description = "接続元のグローバルIP (CIDR表記)" # 値が未指定のとき入力プロンプトに表示される説明
  type        = string
}

variable "instance_type" {
  type    = string
  default = "t2.micro"
}

terraform.tfvars

variables.tf で宣言した変数の値を設定します。

今回は、クライアントPCのグローバルIPを変数my_ipに設定します。

以下のコマンドで自分のグローバルIPを取得し、それを.tfvarsへ書き出します。

# checkip で自分のグローバルIPを取得して書き出す。
# このIPをセキュリティグループの許可対象にすることで、自分だけがアクセスできる状態にする。
echo "my_ip = \"$(curl -s https://checkip.amazonaws.com)/32\"" > terraform.tfvars

# 確認
cat terraform.tfvars # 例: my_ip = "203.0.113.10/32"

.gitignore

以下のファイルをGitから除外します。

.terraform/
*.tfstate
*.tfstate.*
*.tfvars
crash.log
ファイル名 用途 除外理由
.terraform/ terraform init でダウンロードしたプロバイダ本体(プラグイン)などが置かれるディレクトリ サイズが大きく、init すればいつでも再取得できるため
*.tfstate / *.tfstate.* terraform apply 時に自動生成される state 本体とそのバックアップ 機密値(生成されたパスワードや鍵など)が平文で入り、公開すると情報漏洩に繋がるため
crash.log Terraform がクラッシュしたときだけ自動生成されるログ 一時的なデバッグ用でコードと無関係なため
*.tfvars 変数に入れる具体的な値を書いたファイル(これのみ手動作成) 環境固有の値(今回は自分のIP)が入るため

state(tfstate)
Terraform が「どのコードが、実際のどのリソースに対応するか」を記録するファイル。Terraform はこれとコードを突き合わせて差分を検出し、実際のリソースへ反映する。

network.tf

ネットワーク一式(VPC・サブネット・ゲートウェイなど)を定義します。

# VPC
resource "aws_vpc" "vpc" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true          # VPC内のDNS解決(Amazon提供DNS)を有効化
  enable_dns_hostnames = true          # パブリックIPを持つEC2にパブリックDNS名を割り当て(dns_supportとの併用が前提)

  tags = { Name = "${var.project}-vpc" }
}

# IGW
resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.vpc.id # VPCに紐付け

  tags = { Name = "${var.project}-igw" }
}

# data = 既存情報の取得用途
# 利用可能なAZ一覧を取得する
data "aws_availability_zones" "available" {
  state = "available"
}

# サブネット
resource "aws_subnet" "public" {
  vpc_id                  = aws_vpc.vpc.id
  cidr_block              = "10.0.1.0/24"                                   
  availability_zone       = data.aws_availability_zones.available.names[0] # 取得したAZの先頭を使用
  map_public_ip_on_launch = true                                          # 起動したEC2に自動でパブリックIPを付与

  tags = { Name = "${var.project}-public-subnet" }
}

# ルーティングテーブル
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.vpc.id

  route {
    # インターネット宛の通信を全てIGWに送る
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }

  tags = { Name = "${var.project}-public-rt" }
}

# ルーティングテーブルをサブネットに関連付け
resource "aws_route_table_association" "public" {
  subnet_id      = aws_subnet.public.id
  route_table_id = aws_route_table.public.id
}

security.tf

セキュリティグループを定義します。

# セキュリティグループ
resource "aws_security_group" "web" {
  name        = "${var.project}-web-sg"
  description = "Allow SSH and HTTP from my IP"
  vpc_id      = aws_vpc.vpc.id

  tags = { Name = "${var.project}-web-sg" }
}

# inboundルール:自分のIPからのSSH通信を許可
resource "aws_vpc_security_group_ingress_rule" "ssh" {
  security_group_id = aws_security_group.web.id
  cidr_ipv4         = var.my_ip # 自分のIPのみ許可
  from_port         = 22        # SSH
  to_port           = 22
  ip_protocol       = "tcp"
  description       = "SSH from my IP"
}

# inboundルール:自分のIPからのHTTP通信を許可
resource "aws_vpc_security_group_ingress_rule" "http" {
  security_group_id = aws_security_group.web.id
  cidr_ipv4         = var.my_ip # 自分のIPのみ許可
  from_port         = 80        # HTTP
  to_port           = 80
  ip_protocol       = "tcp"
  description       = "HTTP from my IP"
}

# outboundルール:全て許可
resource "aws_vpc_security_group_egress_rule" "all" {
  security_group_id = aws_security_group.web.id
  cidr_ipv4         = "0.0.0.0/0"
  ip_protocol       = "-1" # -1 = 全プロトコル
}

ec2.tf

EC2 インスタンス本と、起動に必要なリソース(キーペア・AMI取得)を定義します。

# SSH接続用の公開鍵を AWS に登録
resource "aws_key_pair" "ssh" {
  key_name   = "${var.project}-key"
  public_key = file("~/.ssh/iac-handson.pub")
}

# 使うAMIを取得
data "aws_ami" "al2023" {
  most_recent = true       # 最新を選ぶ
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-2023.*-x86_64"] # Amazon Linux 2023
  }
}

resource "aws_instance" "web" {
  ami                    = data.aws_ami.al2023.id     # 取得したAMI
  instance_type          = var.instance_type          # インスタンスタイプ(デフォルト t2.micro)
  subnet_id              = aws_subnet.public.id        # 作成したサブネットに配置
  vpc_security_group_ids = [aws_security_group.web.id] # 作成したSGを適用
  key_name               = aws_key_pair.ssh.key_name  # 登録したキーペアを使用

  # 起動時に一度だけ実行されるスクリプト。nginxを入れて起動し、サンプルHTMLを配置
  user_data = <<-EOF
    #!/bin/bash
    dnf install -y nginx
    echo "<h1>Provisioned by Terraform</h1>" > /usr/share/nginx/html/index.html
    systemctl enable --now nginx
  EOF

  tags = { Name = "${var.project}-web" }
}

aws_key_pair では、事前準備で作成した鍵ペアのうち公開鍵~/.ssh/iac-handson.pub)だけを AWS に登録します。対になる秘密鍵(~/.ssh/iac-handson)はローカルに置いておき、後述の SSH で接続 で使います。

outputs.tf

リソースの作成後に、ターミナルに出力したい値を定義します。

output "instance_public_ip" {
  value = aws_instance.web.public_ip # 作成したEC2のIP
}

output "ssh_command" {
  # SSH接続できるコマンド文字列(コピペ用)を組み立てて表示
  value = "ssh -i ~/.ssh/iac-handson ec2-user@${aws_instance.web.public_ip}"
}

output "ami_id" {
  description = "data sourceで解決されたAMI ID"
  value       = data.aws_ami.al2023.id # 実際に使用AMIを確認用に出力
}

実行

記述したコードを元に、以下のコマンドを叩いて実際にリソースを作成していきます。

terraform init

最初に実行するコマンドです。
providers.tf で指定したプロバイダ(今回は AWS)がダウンロードされ、作業ディレクトリが初期化されます。

% terraform init
Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 6.0"...
- Installing hashicorp/aws v6.62.0...
- Installed hashicorp/aws v6.62.0 (signed by HashiCorp)

中略

Terraform has been successfully initialized!

完了すると、.terraformディレクトリが作成されてプロバイダ本体が格納され、選択されたバージョンを記録する .terraform.lock.hclも生成されます。

terraform plan

コードを適用すると作成・変更・削除されるリソースを、事前に確認します。

% terraform plan
data.aws_availability_zones.available: Reading...
data.aws_ami.al2023: Reading...
data.aws_availability_zones.available: Read complete after 0s [id=ap-northeast-1]
data.aws_ami.al2023: Read complete after 0s [id=ami-02aa60981f501ed4b]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_instance.web will be created
  + resource "aws_instance" "web" {
      + ami                    = "ami-02aa60981f501ed4b"
      + instance_type          = "t2.micro"
      + key_name               = "iac-handson-key"
      + subnet_id              = (known after apply)
      + vpc_security_group_ids = (known after apply)
      + user_data              = <<-EOT
            #!/bin/bash
            dnf install -y nginx
            echo "<h1>Provisioned by Terraform</h1>" > /usr/share/nginx/html/index.html
            systemctl enable --now nginx
        EOT
      + tags                   = {
          + "Name" = "iac-handson-web"
        }
      # ...(以下略)
    }

  中略 ※VPC・サブネット・IGW・ルートテーブル・キーペアなども同様に表示される


  # 自分のIPからのみ許可されていることが確認できる(tfvars で指定したIP)
  # aws_vpc_security_group_ingress_rule.ssh will be created
  + resource "aws_vpc_security_group_ingress_rule" "ssh" {
      + cidr_ipv4         = "126.123.223.56/32" # 自分のIP
      + description       = "SSH from my IP"
      + from_port         = 22
      + to_port           = 22
      + ip_protocol       = "tcp"
      + security_group_id = (known after apply)
      # ...(以下略)
    }

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

Changes to Outputs:
  + ami_id             = "ami-02aa60981f501ed4b"
  + instance_public_ip = (known after apply)
  + ssh_command        = (known after apply)

─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply"
now.

新規作成されるリソースには + create が付き、末尾の Plan: 11 to add, 0 to change, 0 to destroy. で今回は11個のリソースが作成されることが分かります。

terraform apply

plan と同じ実行計画が表示され、最後に実行の確認を求められます。yes を入力すると、実際に AWS 上へリソースが作成されます。

% terraform apply

中略 ※planと同じ出力

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_key_pair.ssh: Creating...
aws_vpc.vpc: Creating...
aws_key_pair.ssh: Creation complete after 0s [id=iac-handson-key]
aws_vpc.vpc: Creation complete after 2s [id=vpc-0f3ea994072d70803]
aws_internet_gateway.igw: Creating...
aws_security_group.web: Creating...
aws_subnet.public: Creating...
aws_internet_gateway.igw: Creation complete after 1s [id=igw-0993bd04c62568fba]
aws_route_table.public: Creating...
aws_route_table.public: Creation complete after 1s [id=rtb-02bdf8893955fedc6]
aws_security_group.web: Creation complete after 2s [id=sg-016c43f17e6145a8d]
aws_vpc_security_group_ingress_rule.http: Creating...
aws_vpc_security_group_ingress_rule.ssh: Creating...
aws_vpc_security_group_egress_rule.all: Creating...
aws_vpc_security_group_ingress_rule.http: Creation complete after 0s [id=sgr-0d4a4e76cfb36abde]
aws_vpc_security_group_egress_rule.all: Creation complete after 0s [id=sgr-07ee7d08fbc4acdcf]
aws_vpc_security_group_ingress_rule.ssh: Creation complete after 0s [id=sgr-0a514fef8aa241d5a]
aws_subnet.public: Still creating... [00m10s elapsed]
aws_subnet.public: Creation complete after 11s [id=subnet-00b9945414fb57d04]
aws_route_table_association.public: Creating...
aws_instance.web: Creating...
aws_route_table_association.public: Creation complete after 1s [id=rtbassoc-08a3987cfd632dea6]
aws_instance.web: Still creating... [00m10s elapsed]
aws_instance.web: Creation complete after 14s [id=i-09ac90ffc9ba1bc77]

Apply complete! Resources: 11 added, 0 changed, 0 destroyed.

Outputs:

ami_id = "ami-02aa60981f501ed4b"
instance_public_ip = "x.x.x.x"
ssh_command = "ssh -i ~/.ssh/iac-handson ec2-user@52.196.113.92"

完了すると outputs.tf で定義した値が表示されます。

動作確認

まず AWS コンソールでリソースが構成図どおりに作られているかを確認し、
続いてブラウザと SSH で、実際にサーバーへアクセスできることを確認します。

コンソール

コードどおりにリソースが作られているかを確認していきます。

VPC / サブネット / ルートテーブル / IGW

VPC コンソールで、iac-handson-vpc10.0.0.0/16)が作成されていることを確認します。
スクリーンショット 2026-09-02 12.24.50.png

合わせて、リソースマップで サブネット、Internet Gateway、ルートテーブルも確認します。
スクリーンショット 2026-09-02 12.26.00.png

セキュリティグループ

iac-handson-web-sg が作成され、インバウンドルールとアウトバウンドルールが設定されていることを確認します。
スクリーンショット 2026-09-02 12.27.21.png
スクリーンショット 2026-09-02 12.28.40.png

EC2

EC2 コンソールで iac-handson-webrunning になっていることを確認します。パブリック IP が outputsinstance_public_ip と一致していれば OK です。
スクリーンショット 2026-09-02 12.32.02.png

ブラウザで確認

instance_public_ip をブラウザで開きます。user_data で入れた nginx が起動し、Provisioned by Terraform が表示されれば成功です。
スクリーンショット 2026-09-02 12.35.00.png

SSH で接続

最後に、outputsssh_command をそのまま実行して SSH 接続してみます。初回は接続先の確認を求められるので yes を入力します。Amazon Linux 2023 のロゴが表示されれば接続成功です。

% ssh -i ~/.ssh/iac-handson ec2-user@52.196.113.92
The authenticity of host '52.196.113.92 (52.196.113.92)' can't be established.
ED25519 key fingerprint is: SHA256:V+5iU1t3YcmMo+7v/yENr0sqY5tMlT0EyRaLCH3HCB8
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '52.196.113.92' (ED25519) to the list of known hosts.
** WARNING: connection is not using a post-quantum key exchange algorithm.
** This session may be vulnerable to "store now, decrypt later" attacks.
** The server may need to be upgraded. See https://openssh.com/pq.html
   ,     #_
   ~\_  ####_        Amazon Linux 2023
  ~~  \_#####\
  ~~     \###|
  ~~       \#/ ___   https://aws.amazon.com/linux/amazon-linux-2023
   ~~       V~' '->
    ~~~         /
      ~~._.   _/
         _/ _/
       _/m/'

これで、コードから作ったサーバーに実際にアクセスできることを確認できました。

terraform destroy

動作確認が終わったら、作成したリソースを削除します。terraform destroy は、state に記録されているリソースをまとめて削除するコマンドです。

apply と同様に確認を求められるので、yes を入力すると削除が実行されます。

% terraform 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
...
Destroy complete! Resources: 11 destroyed.

今回使用しなかった機能

今回は使いませんでしたが、Terraform には以下のような機能もあります。

リモートstate

今回は state をローカル(作業フォルダの terraform.tfstate)で管理していましたが、チームで同じ構成を扱う場合は、state を共有しないと各自の手元で変更が競合したり食い違いが起きます。
そこで state を S3 などのリモートに置くのが backend ブロック(慣例的に backend.tf に書く)の役割です。リモートに置くと state を共有できるうえ、ロックによって同時変更の競合も防げます。

モジュール

今回は .tf ファイルに直接リソースを書きましたが、同じ構成を何度も作成するケース(例: dev / prod で同じネットワークを複製)では、リソースのまとまりを modules/ に切り出して再利用します。

おわりに

Terraform を使って、コードから AWS 上に nginx が動く EC2 サーバーを立てるところまでを一通り体験しました。
今回のシンプルな構成でもコードで管理できるIaCの恩恵を感じられましたが、実務でより複雑な構成を扱うほど、その恩恵は大きくなりそうだと感じました。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?