🔖

素人が始めるTerraformとAWSを使ったサーバー構築

2023/10/11に公開

はじめに

昨今 AWS をはじめとしたクラウドを触れることはエンジニアにとって、必須となってきています。
ですが、いざ私に目を向けた時 AWS の存在は知っていても実際に触ったことはほとんどない素人でした。
これではいけないと思い、AWS の勉強を始め、今回ある程度ブログとして書けるくらいの分量にはなったので、投稿しました。
私と同じく AWS に馴染みのない方の参考になれば、幸いです。
なお、このブログはいつでも環境を再現できるように AWS を Terraform で IaC 化しています。
Terraform についてはほとんど解説しないので、その点はご留意ください。
また、Terraform のコードも保守性とか一切考えず、一つのファイルにほぼまとめて記載しています。
本当はサービスごとに分けて書くべきなのですが、一つのファイルにまとめた方がブログとして書きやすかったので、まとめています。
こちらの都合で申し訳ないですが、その点もご注意ください。
長いですが読んでいただけると幸いです。

Terraform の準備をする

ここでは AWS を Terraform で動かすための ID とシークレットキーを取得する手順を説明します。
ここはコンソールで準備する必要があるので、ご注意ください。
まず、IAM サービスにアクセスして、サイドメニューにある「ユーザー」をクリックします。
するとユーザーを作成するためのボタンが画面に表示されるので、クリックします。
クリックした際、「AWS マネジメントコンソールへのユーザーアクセスを提供する - オプション」というチェック項目が存在します。
Untitled
チェックをつけると上記画面が表示されますが、今回はキーを取得するためだけのユーザーを作成するので、ここにチェックは付ける必要はないです。
ただ、コンソールを触るためのユーザーを作成する場合などは、Identity Center でアカウント管理を行うことが推奨されているので、このチェック項目は設定するようにしましょう。
その他入力事項を埋めたら、次に進みます。
すると以下のような許可を設定する項目が表示されます。
2023-10-08_22h53_44.png
管理などを考えると、ポリシーを付与したグループを作成しそのグループにユーザーを割り当てる方が良いのですが、今回はキー用のユーザーを作るだけなのでポリシーをそのままアタッチすることにします。
「ポリシーをそのままアタッチする」を選択すると、AWS がデフォルトで用意しているポリシーが以下のように表示されます。
Untitled
今回は Terraform を実行するために管理者権限が欲しいので、「AdministratorAccess」を選択して、ユーザーを作成します。
作成した IAM ユーザーの詳細画面にアクセスすると、以下の画像のような項目があります。
Untitled
「アクセスキーを作成」をクリックすることで、Terraform で動かすために必要な ID とシークレットキーが表示されます。
この値を Terraform に設定することで、実行できるようになるので、値は控えておきます。
なお、一度作成した場合どこかにメモをしておかない限り再度シークレットキーを表示させることはできないので、ご注意ください。
ここまでで、AWS の準備が完了したので Terraform 側の設定を行います。
まず、terraform.tfvars ファイルを Linux 環境の任意のディレクトリ配下に作成し、以下のコードを記載します。

aws_access_key_id     = "IAMユーザーのアクセスキー"
aws_secret_access_key = "IAMユーザーのシークレットキー"

次に、variables.tf を作成して以下のコードを記載します。

variable "aws_access_key_id" {
  type = string
}
variable "aws_secret_access_key" {
  type = string
}

そして、main.tf を作成して以下のようにコードを記載します。

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.16"
    }
  }
  required_version = ">= 1.2.0"
}
provider "aws" {
  region     = "ap-northeast-1"
  access_key = var.aws_access_key_id
  secret_key = var.aws_secret_access_key
}

AWS のバージョンは 2023/10/06 時点で 5.2.0 なので、適宜必要なバージョンに設定しておいてください。
これで Terraform の準備が完了しました。
それでは実際に AWS でネットワーク環境を構築していきましょう。

AWS ではじめるサーバー構築

完成図

このブログでは以下の AWS の構成を作成するようにしていきます。
AWS.drawio.png
この構成が完了すれば、Private subnet に属する EC2 で作成した HTML ファイルが表示できるようになるので、それを今回のゴールとしています。
では早速作っていきます。

仮想ネットワーク(VPC)の作成

ここでは AWS 上で色々なサーバーを構築できるようにするためのネットワークを作成します。
この節を完了すれば AWS は以下の状態になります。
AWS_only_vpc.drawio.png

作成するまえにちょっとだけ概念のお話

VPC とは
VPC とは、 Virtual private Cloud の略で、ネットワークを構築する際に使用するサービスとなっています。
このサービスは、サーバーやネットワークが持つ機能を疑似的に再現するソフトを動かす AWS のデータセンターにある専用の機器によって実現されています。
そのため、ネットワークの追加や削除などはソフトの起動や停止と同じようにすることが可能です。
また、VPC はそれぞれが独立して存在しているので、複数ネットワークを作成したとしても互いが影響を及ぼすことはありません。
CIDR 表記について
AWS でネットワークを構築する際は VPC 領域にどのような IP アドレス範囲を割り当てるかを考える必要があります。
その際に使用する表記方法が CIDR(Classless Inter-Domain Routing)です。
CIDR の説明に入る前に、IP アドレスについて軽く説明します。
IP アドレスとは、TCP/IP で通信する機器を識別するためのアドレスのことです。
IP アドレスの長さは 32 ビットで、その中にネットワーク部とホスト部が存在します。
IPアドレスの基礎知識から引用
IP アドレスの基礎知識から引用
ネットワーク部はどのネットワークかを示す値となり、ホスト部はどのコンピューターかを表す値となります。
そして、CIDR はネットワーク部分をどこまでであるかを示す表記方法となります。
CIDR の書き方は「10.0.0.0/16」という形で記載します。
今回では、左から 16 ビットすなわち「10.0」の部分がネットワーク部分であり、それより後ろがそのコンピューターかを示すホスト部となります。
VPC を作る際は CIDR 表記でアドレスの範囲を記載する必要があるので、把握しておくのは大切です。
ただ、この後出てくるサブネットの都合上、基本的には「10.0.0.0/16」といった 16 ビットブロックでネットワーク部分を指定すれば良さそうです。

作成する VPC の内容

今回作成する VPC は以下の通りです。

  • 名前タグ →sample-vpc

これは私たちがどの VPC かを見分けることを可能にするためのタグ付けです。

  • CIDR ブロック

「10.0.0.0/16」を指定します。

Terraform のコード

VPC を作成する Terraform のコードは以下の通りです。

resource "aws_vpc" "sample-vpc" {
  cidr_block = "10.0.0.0/16"
  tags = {
    Name = "sample-vpc"
  }
}

CIDR で IP アドレス範囲の指定して、識別しやすいようにタグに「sample-vpc」をつけたら、作成することができます。
場を設けるだけなので、細かい設定をしなければ非常に簡単にできますね。

アベイラビリティゾーンとサブネットの作成

この節を完了すれば AWS は以下の状態になります。
AWS4-3.drawio.png

作成するまえにちょっとだけ概念のお話

アベイラビリティゾーン
AWS はクラウドサービスを構築する環境・場所を設定する単位として、リージョンという概念があります。
そして、このリージョンは複数のデータセンターからなる「アベイラビリティゾーン」というもので構築されています。※
※アベイラビリティゾーンのみで構成されていると読み取れるこの書き方は適切ではなく、アベイラビリティゾーン以外にもリージョンを構成する要素は存在します。ですが、全く不正確なことを言っているわけではないと思うので、この書き方になることはご容赦ください。
このアベイラビリティゾーンはお互い完全に依存しておらず、一つのアベイラビリティゾーンが障害で動かなくなっても、別のアベイラビリティゾーンは通常通り動くことができます。
そのため、AWS で環境を構築する際は障害対策として、複数のアベイラビリティゾーンに同様の環境を作る場合が多々あります。
なお、そもそも複数のリージョンでサービスを構築することもできます。
が、正直そのアプリケーションが止まってしまうと国中、あるいは世界レベルで困る人が発生するものでない限り、複数のリージョンで設定する旨味はありません。
サブネット
サブネットは VPC のアドレス範囲をさらに分割する単位となります。
この章の最初に示した図で VPC 内に領域を作っていたと思います。
この領域がサブネットであり、領域の作り方が IP アドレスを分割することです。
IP アドレスを分割しサブネットを作成することで、各サブネットに対して個別の設定ができるようになります。
これによって、このリソースは公開したり、このリソースは基本的に公開せず特定の方法でしかアクセスできないようにしたりできます。
なお、サブネットを割り当てる表記方法は VPC の場合と同様に CIDR 表記となります。
注意点として、サブネットを CIDR 表記で書く場合 VPC で設定したネットワーク部分も考慮してい書く必要があります。
下記画像のように、VPC を「10.0.0.0/16」とネットワーク部分を 16 ビットにして、サブネット部分を 8 ビットとする場合、サブネットの CIDR は「10.0.0.0/24」とネットワーク部分を足した形で表現する必要があります。
クラスA~Cの問題を解消するための手法とは?  クラスとサブネッティングの関係を理解するから引用し、画像を改変しています。
クラス A ~ C の問題を解消するための手法とは? クラスとサブネッティングの関係を理解するから引用し、画像を改変しています。
サブネットはネットワーク部分が設定されてからしか、割り当てることができないので、落ち着いて見ると CIDR 表記に違和感はないと思います。
ただ、初見でみるといきなり 24 ビットもサブネットに割り当てているけど、ビット数足りなくない!?と驚いてしまうので、今回記載しました。

作成内容

今回作るサブネットは以下の通りです。

  • 先程作成した VPC 内にサブネットを作成する。
  • アベイラビリティゾーンとして、ap-northeast-1a と ap-northeast-1c を設定する。
  • 上記アベイラビリティゾーン内に、外部公開用のパブリックサブネットと内部でしかアクセスできないようにするプライベートサブネットをそれぞれ作成する。
  • ap-northeast-1a のパブリックサブネットの CIDR は「10.0.0.0/20」、プライベートサブネットの CIDR は「10.0.64.0/20」とする。
  • ap-northeast-1c のパブリックサブネットの CIDR は「10.0.16.0/20」、プライベートサブネットの CIDR は「10.0.80.0/20」とする。

なお、それぞれの CIDR を微妙に変えているのは、同じにするとどのサブネットか区別が出来なくなってしまうためです。

Terraform のコード

作成内容を反映した Terraform コードは以下の通りです。

resource "aws_subnet" "sample-subnet-public01" {
  vpc_id            = aws_vpc.sample-vpc.id
  availability_zone = "ap-northeast-1a"
  cidr_block        = "10.0.0.0/20"
  tags = {
    Name = "sample-subnet-public01"
  }
}
resource "aws_subnet" "sample-subnet-private01" {
  vpc_id            = aws_vpc.sample-vpc.id
  availability_zone = "ap-northeast-1a"
  cidr_block        = "10.0.64.0/20"
  tags = {
    Name = "sample-subnet-private01"
  }
}
resource "aws_subnet" "sample-subnet-public02" {
  vpc_id            = aws_vpc.sample-vpc.id
  availability_zone = "ap-northeast-1c"
  cidr_block        = "10.0.16.0/20"
  tags = {
    Name = "sample-subnet-public02"
  }
}
resource "aws_subnet" "sample-subnet-private02" {
  vpc_id            = aws_vpc.sample-vpc.id
  availability_zone = "ap-northeast-1c"
  cidr_block        = "10.0.80.0/20"
  tags = {
    Name = "sample-subnet-private02"
  }
}

大体やることはどのサブネットでも同じで、以下のことを行っています。

  1. 紐づける VPC を設定する。
  2. サブネットをおくアベイラビリティゾーンを定義する。
  3. CIDR を定義する
  4. 識別しやすいように名前を設定する

これを行えば、サブネットの作成までできます。

インターネットゲートウェイの設定

VPC とインターネットを接続できるようにインターネットゲートウェイを設定します。
この節を完了すれば AWS は以下の状態になります。
AWS4-4.drawio.png

作成するまえにちょっとだけ概念のお話

インターネットゲートウェイ
これまでで VPC にリソースを用意する場は完成しました。
しかし、現状 VPC は閉じられたままでインターネットと通信する穴がありません。
アプリなどを公開する場合、インターネットと通信できないといくら内部で環境を整えても、ユーザーにアプリを届けることができません。
なので、VPC にインターネット用の穴をあけることが往々にして求められます。
このインターネット用の穴のことをインターネットゲートウェイといいます。
インターネットゲートウェイを用意することで、インターネットとの通信を行うための準備ができます。

作成内容

今回作成するのはこれまで作成した VPC に対するインターネットゲートウェイです。
そして、インターネットゲートウェイを識別しやすいように「sample-igw」という名前を付与します。

Terraform のコード

resource "aws_internet_gateway" "gw" {
  vpc_id = aws_vpc.sample-vpc.id
  tags = {
    Name = "sample-igw"
  }
}

インターネットゲートウェイは VPC と紐づけるだけで良いので、紐づける VPC の ID を指定しています。
そして、識別できるように名前を設定しています。

NAT ゲートウェイの構築

プライベートサブネットに設定したものは外部のネットワークと接続したくありません。
インターネットなどに出ていくいくことは求められても、インターネットからはアクセスされたくありません。
上記要求を達成するには NAT(ネットワークアドレス変換)を用意する必要があり、AWS においてはこれを NAT ゲートウェイで達成できます。
この節を完了すれば AWS は以下の状態になります。
AWS4-5.drawio.png

作成するまえにちょっとだけ概念のお話

NAT ゲートウェイ
先程作成したインターネットゲートウェイは VPC とインターネットを繋ぐための穴でした。
インターネットゲートウェイで通信の準備はできましたが、インターネットと VPC 内のリソースが直接通信を行うにはパブリック IP が必要となります。
ただ、プライベートサブネットに設置したリソースにパブリック IP を持たせたくありません。
パブリック IP を持つということはインターネットに公開されている状態となり、直接アクセスできてしまうのでせっかくプライベートにした意味がありません。
とはいえ、インターネットに、プライベートサブネット内のリソースで生成された情報を渡すことは往々にしてあります。
そこで使用するのが NAT ゲートウェイです。
NAT ゲートウェイにパブリック IP を持たせ、それ経由で行えば、プライベートサブネットと直接やり取りせずとも、インターネットに情報を流すことができます。
なお、NAT ゲートウェイはプライベートサブネットを内容を流す窓口なので、NAT ゲートウェイ自体はパブリックサブネットにおく必要があります。
プライベートサブネットに関わる内容なので、プライベートサブネットにおけばいいのかなと当時の自分は勘違いしていたので、同じように感じた方はご注意ください。
Elastic IP
NAT ゲートウェイは、プライベートサブネットにあるリソースのプライベート IP にパブリック IP を付け足すことでインターネットへ出ていくようにする仕組みでした。
そのため、NAT ゲートウェイはパブリック IP を持っている必要があるのですが、このパブリック IP を作成する機能 Elastic IP というものがあります。
Elastic IP は他の機能と関連していない静的なパブリック IP を作成できるので、作成した後任意のリソースに割り当てることができます。
今回は Elastic IP で作成したパブリック IP を NAT ゲートウェイに割り当てます。

作成内容

今回は二つのパブリックサブネットに対してそれぞれ NAT ゲートウェイを作成します。
一つ目の NAT ゲートウェイは以下の通りです。

  • 名前 →sample-ngw-01
  • 作成するサブネット →sample-subnet-public01
  • 接続タイプ → パブリック
  • パブリック IP→Elastic IP による自動生成された IP
    二つ目の NAT ゲートウェイは以下の通りです。
  • 名前 →sample-ngw-0
  • 作成するサブネット →sample-subnet-public012
  • 接続タイプ → パブリック
  • パブリック IP→Elastic IP による自動生成された IP

Terraform のコード

resource "aws_eip" "elastic-ip-01" {
  domain = "vpc"
}
resource "aws_nat_gateway" "sample-ngw-01" {
  allocation_id = aws_eip.elastic-ip-01.id
  subnet_id     = aws_subnet.sample-subnet-public01.id
  tags = {
    Name = "sample-ngw-01"
  }
}
resource "aws_eip" "elastic-ip-02" {
  domain = "vpc"
}
resource "aws_nat_gateway" "sample-ngw-02" {
  allocation_id = aws_eip.elastic-ip-02.id
  subnet_id     = aws_subnet.sample-subnet-public02.id
  tags = {
    Name = "sample-ngw-02"
  }
}

NAT ゲートウェイはパブリックサブネットに配置するので、先程作成したパブリックサブネットの ID を指定します。
そして、NAT ゲートウェイはパブリック ID を持っている必要があるので、resource "aws_eip" で Elastic IP で IP を作成し、その IP をそれぞれの NAT ゲートウェイに割り当てています。
最後にこれまでと同様に、識別しやすいように名前のタグを付与しています。

参考資料

NAT ゲートウェイと ALB の関係性の概要をぼんやりとつかめたページ
一番 Elastic IP の必要性を感じられたページ

ルートテーブルの作成

ここまでで、サブネットを作成し各種ゲートウェイも用意することで、インターネットとリソースやリソース同士のやり取りを行うための出入口が設定できました。
ですが、現状では実際に通信を行うための経路ができていません。
なので、ここでは通信のための経路を作成するためにルートテーブルを構築します。
この節を完了すれば AWS は以下の状態になります。
AWS4-6.drawio.png

作成するまえにちょっとだけ概念のお話

ルートテーブル
AWS において、サブネット間の通信経路を設定する機能をルートテーブルといいます。
ルートテーブルには「このサーバーに接続するときはここを経由する」といった接続のルールをテーブルの形式で設定できます。
設定方法は画像のように送信先に接続先の IP を設定します。
なお、この送信先は特定の IP でもいいですし、CIDR 表記で範囲を指定することもできます。
2023-10-09_19h35_09.png
ターゲットはどこを経由するかを指定します。
主なものは以下の通りです。

  • ローカル
    ⇒ 同一 VPC 内のリソースへアクセスする時
  • インターネットゲートウェイ
    ⇒ パブリックサブネット内のリソースがインターネットのサーバーと通信するとき
  • NAT ゲートウェイ
    ⇒ プライベートサブネット内のリソースがインターネットのサーバーと通信するとき
  • VPN ゲートウェイ
    ⇒VPN で接続された独自ネットワーク上のサーバーと通信するとき
  • VPC ピアリング
    ⇒ 接続を許可された別の VPC 内のリソースと通信するとき。

作成内容

今回は 3 つのルートテーブルを作成します。
同じルートテーブルを適用するのであれば、ルートテーブルは使いまわすことができます。
そのため、ルートテーブルの対象は各アベイラビリティゾーンのパブリックサブネットとプライベートサブネットの合計 4 つですが、ルートテーブルは 3 つのみ作成します。
具体的には以下のルートテーブルを作成します。
① パブリックサブネット用
対象のサブネット: sample-subnet-public01,sample-subnet-public02
ルート 1 の送信先: 10.0.0.0/16
ルート 1 の対象: Local
ルート 2 の送信先: 0.0.0.0/0
ルート 2 の対象: sample-igw(インターネットゲートウェイ)
② プライベートサブネット用 part1
対象のサブネット sample-subnet-private01
ルート 1 の送信先: 10.0.0.0/16
ルート 1 の対象: Local
ルート 2 の送信先: 0.0.0.0/0
ルート 2 の対象: sample-ngw-01(NAT ゲートウェイ)
② プライベートサブネット用 part2
対象のサブネット sample-subnet-private02
ルート 1 の送信先: 10.0.0.0/16
ルート 1 の対象: Local
ルート 2 の送信先: 0.0.0.0/0
ルート 2 の対象: sample-ngw-02(NAT ゲートウェイ)
なお、ゲートウェイに設定している CIDR の「0.0.0.0/0」はすべての送信先を意味します。
今回で言うと、10.0.0.0/16 以外の送信先は全てゲートウェイへ通信するということになります。

Terraform のコード

ルートテーブルを設定するコードは以下の通りです。

resource "aws_route_table" "sample-rtb-public" {
  vpc_id = aws_vpc.sample-vpc.id
  route {
    cidr_block = "10.0.0.0/16"
    gateway_id = "local"
  }
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.gw.id
  }
  tags = {
    Name = "sample-rtb-public"
  }
}
resource "aws_route_table_association" "relate-public-route-table1" {
  subnet_id      = aws_subnet.sample-subnet-public01.id
  route_table_id = aws_route_table.sample-rtb-public.id
}
resource "aws_route_table_association" "relate-public-route-table2" {
  subnet_id      = aws_subnet.sample-subnet-public02.id
  route_table_id = aws_route_table.sample-rtb-public.id
}
resource "aws_route_table" "sample-rtb-private01" {
  vpc_id = aws_vpc.sample-vpc.id
  route {
    cidr_block = "10.0.0.0/16"
    gateway_id = "local"
  }
  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.sample-ngw-01.id
  }
  tags = {
    Name = "sample-rtb-private01"
  }
}
resource "aws_route_table_association" "relate-private-route-table1" {
  subnet_id      = aws_subnet.sample-subnet-private01.id
  route_table_id = aws_route_table.sample-rtb-private01.id
}
resource "aws_route_table" "sample-rtb-private02" {
  vpc_id = aws_vpc.sample-vpc.id
  route {
    cidr_block = "10.0.0.0/16"
    gateway_id = "local"
  }
  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.sample-ngw-02.id
  }
  tags = {
    Name = "sample-rtb-private02"
  }
}
resource "aws_route_table_association" "relate-private-route-table2" {
  subnet_id      = aws_subnet.sample-subnet-private02.id
  route_table_id = aws_route_table.sample-rtb-private02.id
}

セキュリティグループの設定

リソースの経路も設定し、実際に通信ができるようになりました。
しかし、このままではインターネットを通じてアクセスすることが可能な状態です。
そこで、外部からのアクセスを制限するためにセキュリティグループを設定します。
この節を完了すれば AWS は以下の状態になります。
AWS4-7.drawio.png

作成するまえにちょっとだけ概念のお話

セキュリティグループ
セキュリティグループはアクセス許可を行うソースやプロトコル、ポートのルールを定義します。
ポート番号や IP アドレスなどを設定することで、どこからならアクセスしてよいかを決めることができます。
そして、作成したルールを特定のリソースに関連づけることで、ファイアウォールのような役割を持たせることができます。
なお、セキュリティグループにはインバウンドルールとアウトバウンドルールを設定することができます。
これは名前の通り、セキュリティグループを割り当てたリソースの受信ルールと送信ルールを設定できます。

作成内容

以下のセキュリティコードを作成します。
踏み台サーバー(後述)用のセキュリティグループ
名前: sample-bg-bastion
説明: for bastion server
紐づける VPC: sample-vpc
インバウンドルール(タイプ): SSH
インバウンドルール(ソース): 0.0.0.0/0
⇒ これは SSH での接続方法であれば、どこからのリクエストも受け入れるということを意味します。
デフォルトセキュリティグループ
紐づける VPC: sample-vpc
インバウンドルール: 全てのリクエストを受け入れる。
アウトバウンドルール: 全ての対象に対してのリクエストを送ることを許可する。

Terraform のコード

セキュリティグループを作成する Terraform のコードは以下の通りです。

resource "aws_security_group" "sample-sg-bastison-terraform" {
  name        = "sample-sg-bastion-terraform"
  description = "for bastion server"
  vpc_id      = aws_vpc.sample-vpc.id
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
resource "aws_default_security_group" "default" {
  vpc_id = aws_vpc.sample-vpc.id
  ingress {
    from_port        = 0
    to_port          = 0
    protocol         = "-1"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }
  egress {
    from_port        = 0
    to_port          = 0
    protocol         = "-1"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }
}

なお、デフォルトのセキュリティグループはそもそも存在しているのですが、この後の EC2 などでそのセキュリティグループと紐づける方法がわからなかったので、ここでデフォルトのセキュリティグループを作成しています。

EC2 でサーバーの構築

ネットワークの構築が概ね完了したので、ここではサーバーを構築します。
パブリックサブネットには踏み台サーバー用の EC2 を、プライベートサブネットには Web サーバーを想定して作成します。
この節を完了すれば AWS は以下の状態になります。
AWS0.drawio.png

作成するまえにちょっとだけ概念のお話

EC2
EC2 は Elastic Compute Cloud の略で、仮想的なサーバー環境を提供するサービスです。
アプリケーションのデプロイをはじめとした、様々なソフトウェアの実行基盤として使用されます。
AMI
EC2 で仮想サーバとして動かすゲスト OS のことを指します。
AWS が提供する Amazon Linux を筆頭に、CentOS や Ubuntu などの Linux ディストリビューションも使用できます。
キーペア
EC2 のインスタンスに接続するのに必要な公開鍵を指します。
これを AWS 側に設定しておくことで、秘密鍵を使用して SSH 接続ができるようになります。
なお、System Manager や Session Manager を使用すると、このキーペアは設定する必要がなくなるそうですが、ここではこれ以上言及はしません。
踏み台サーバー
踏み台サーバーとは、目的のサーバーにログインするために中継するサーバーのことです。
踏み台サーバー運用のリスクと証跡取得の必要性より引用
踏み台サーバー運用のリスクと証跡取得の必要性より引用
プライベートサブネットにある EC2 へのアクセスを踏み台サーバー経由のみに限定することで以下の利点があります。

  • 直接のアクセスを制限することで、不正にアクセスされるリスクを減らせる。
  • 踏み台サーバー経由にすることで、アクセス元、アクセス時間を制御を踏み台サーバーに施せばよく、制限の管理が簡単になる。
  • 誰がどのリソースへアクセスしたといった操作ログの管理がしやすくなる。
    • これは踏み台サーバーを設定すれば、踏み台サーバーさえ見ればログが分かるようになるためです。
  • 踏み台サーバー以外からの不正なアクセスが検知しやすい

その他にも利点があるので、各 EC2 に直接アクセスさせず踏み台サーバーを経由することは大切です。
ただ、必ずしも直接アクセスさせないことを踏み台サーバーで行う必要はありません。
AWS にはEC2 Instance Connectといった直接アクセスされるのを防ぐ機能はあるので、適宜プロジェクトにあったものを選ぶのが良さそうです。
多段接続
先程プライベートサブネットにある EC2 を踏み台サーバー経由で接続すると言いました。
そのため、プライベートサブネットの EC2 にアクセスするには、まず踏み台サーバーに SSH 接続したあと、再度対象の EC2 に SSH 接続する必要があります。
この時踏み台サーバーへの SSH 接続は、秘密鍵をユーザーの端末が持っているので問題なく接続できますが、その先の EC2 に接続するには踏み台サーバーに秘密鍵を渡して、それを使って SSH 接続をする手順が必要となります。
これは手間がかかりますし、何より重要な情報を踏み台サーバーになるべく置きたくありません。
そこで使用するのが多段接続です。
多段接続を行うことで、「サーバ A に接続し、そこからサーバ B に接続する」といった動作を一つのコマンドで行うことができます。
また、踏み台サーバーに秘密鍵を渡さずに踏み台サーバー経由の SSH 接続が可能となります。
多段接続の意義などを説明したので、簡単に書き方も記載します。

Host 192.168.0.85
        HostName        192.168.0.85
        Port            22
        IdentityFile    ~/.ssh/id_rsa.aws
        User            centos
        ProxyCommand    ssh -W %h:%p 192.168.0.84
Host 192.168.0.84
        HostName        192.168.0.84
        Port            22
        IdentityFile    ~/.ssh/id_rsa.aws
        User            centos
        ProxyCommand    ssh -W %h:%p 52.40.178.xxx

それぞれの項目は以下の通りです。

  • Host
    ⇒SSH 接続する際に利用する名前。IP アドレスである必要はなく、任意の名前でよい。
  • HostName
    ⇒ 接続先のアドレスや省略せずに書いた完全なドメイン(FQDN)を記載する。
  • Port
    ⇒ 接続先のポート番号
  • IdentityFile
    ⇒ 秘密鍵のパス
  • User
    ⇒SSH 接続の際のユーザー名
  • ProxyCommand
    ⇒ 接続先ホストで自動実行するコマンドを指定する

特に注目したいのは ProxyCommand です。
これを指定すると接続した時にコマンドを実行できるので、プライベートサブネットにある EC2 を指定すれば、まず踏み台サーバーに接続して、接続したら自動でその先への EC2 へ接続するようになります。
これによって、一回のコマンドで踏み台サーバーを経由しつつ、プライベートサブネットにある EC2 を接続する多段接続が実行できます。

作成内容

踏み台サーバー
名前: sample-ec2-bastion
Amazon マシンイメージ(AMI): Amaxon Linux 2
インスタンスタイプ: t2.micro
キーペア: Terraform 内で作成したもの
サブネット: sample-subnet-public01(パブリックサブネット)
セキュリティグループ: デフォルト、踏み台サーバー用のセキュリティグループ
そして、パブリック IP を割り当てます。
プライベートサブネットにある Web サーバー1
名前: sample-ec2-web01
Amazon マシンイメージ(AMI): Amaxon Linux 2
インスタンスタイプ: t2.micro
キーペア: Terraform 内で作成したもの
サブネット: sample-subnet-private01(プライベートサブネット)
セキュリティグループ: デフォルトセキュリティグループ
プライベートサブネットにある Web サーバー 2
名前: sample-ec2-web02
Amazon マシンイメージ(AMI): Amaxon Linux 2
インスタンスタイプ: t2.micro
キーペア: Terraform 内で作成したもの
サブネット: sample-subnet-private02(プライベートサブネット)
セキュリティグループ: デフォルトセキュリティグループ
なお、Web サーバーにはパブリック IP は割り当てません。

Terraform のコード

踏み台サーバーをはじめとした EC2 を作成する Terraform のコードは以下の通りです。

variable "key_name" {
  type        = string
  description = "keypair name"
  #キーペア名はここで指定
  default = "hoge-key"
}
locals {
  public_key_file  = "./.key_pair/${var.key_name}.id_rsa.pub"
  private_key_file = "./.key_pair/${var.key_name}.id_rsa"
}
#privateキーのアルゴリズム設定
resource "tls_private_key" "keygen" {
  algorithm = "RSA"
  rsa_bits  = 2048
}
#秘密鍵の生成
resource "local_file" "private_key_pem" {
  filename = local.private_key_file
  content  = tls_private_key.keygen.private_key_pem
  provisioner "local-exec" {
    command = "chmod 600 ${local.private_key_file}"
  }
}
#公開鍵の生成
resource "local_file" "public_key_openssh" {
  filename = local.public_key_file
  content  = tls_private_key.keygen.public_key_openssh
  provisioner "local-exec" {
    command = "chmod 600 ${local.public_key_file}"
  }
}
resource "aws_key_pair" "key_pair" {
  key_name   = var.key_name
  public_key = tls_private_key.keygen.public_key_openssh
  provisioner "local-exec" {
    command = <<-EOT
     echo "${tls_private_key.keygen.private_key_pem}" > /mnt/c/Users/ユーザー名/.ssh/${var.key_name}.pem
    EOT
  }
}
data "aws_ami" "amzlinux2" {
  most_recent = true
  owners      = ["amazon"]
  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }
}
resource "aws_instance" "sample-ec2-bastion" {
  ami             = data.aws_ami.amzlinux2.id
  instance_type   = "t2.micro"
  subnet_id       = aws_subnet.sample-subnet-public01.id
  security_groups = [aws_security_group.sample-sg-bastion-terraform.id, aws_default_security_group.default.id]
  associate_public_ip_address = true
  key_name                    = aws_key_pair.key_pair.key_name
  tags = {
    Name = "sample-ec2-bastion"
  }
  lifecycle {
    ignore_changes = [ami]
  }
}
resource "aws_instance" "sample-ec2-web01" {
  ami             = data.aws_ami.amzlinux2.id
  instance_type   = "t2.micro"
  subnet_id       = aws_subnet.sample-subnet-private01.id
  security_groups = [aws_security_group.sample-sg-bastion-terraform.id, aws_default_security_group.default.id]
  associate_public_ip_address = false
  key_name                    = aws_key_pair.key_pair.key_name
  tags = {
    Name = "sample-ec2-web01"
  }
  lifecycle {
    ignore_changes = [ami]
  }
}
resource "aws_instance" "sample-ec2-web02" {
  ami             = data.aws_ami.amzlinux2.id
  instance_type   = "t2.micro"
  subnet_id       = aws_subnet.sample-subnet-private02.id
  security_groups = [aws_default_security_group.default.id]
  associate_public_ip_address = false
  key_name                    = aws_key_pair.key_pair.key_name
  tags = {
    Name = "sample-ec2-web02"
  }
  lifecycle {
    ignore_changes = [ami]
  }
}
#local_fileのリソースを指定するとterraformを実行するディレクトリ内でファイル作成やコマンド実行が出来る。
resource "local_file" "config_file" {
  filename = "/mnt/c/Users/ユーザー名/.ssh/config"
  content  = <<-EOT
Host bastion
    Hostname ${aws_instance.sample-ec2-bastion.public_ip}
    User ec2-user
    IdentityFile ~/.ssh/hoge-key.pem
Host web01
    Hostname ${aws_instance.sample-ec2-web01.private_ip}
    User ec2-user
    IdentityFile ~/.ssh/hoge-key.pem
    ProxyCommand ssh.exe -W %h:%p bastion
Host web02
    Hostname ${aws_instance.sample-ec2-web02.private_ip}
    User ec2-user
    IdentityFile ~/.ssh/hoge-key.pem
    ProxyCommand ssh.exe -W %h:%p bastion
    EOT
}

鍵を OpenSSH で生成し、公開鍵をキーペアに設定し、秘密鍵をローカルに設定しています。
そして、EC2 で動かす Amazon Linux を作成しています。
その後、それぞれ作成内容で示した条件で EC2 インスタンスを作成しています。
最後に多段接続用の config ファイルを指定したローカルパスに出力しています。

動作確認

プライベートサブネットに作成した EC2 へアクセスしてみます。
まず Power Shell を開き、任意の場所でssh web01 を実行します。
電子指紋が hosts に追加するかを聞かれるので、yes と答えます。
問題がなければ EC2 インスタンス内にアクセスすることができます。
なお、ssh web01を実行して、config ファイルもしくは秘密鍵に権限エラーが出たら、参考資料をもとに権限の付け直しを行ってください。

参考資料

踏み台サーバー経由の接続確認時発生した権限エラー解消に役立った
Terraform で kye-pair を生成するのに参考とした
多段接続のサンプルコードを拝借した

ロードバランサーの設定

ここまでで、Web サーバーも用意できました。
ですが、このままの状態では、まだ Web サーバーをインターネットに公開されていません。
そこで、ロードバランサーを用意し、ブラウザ上で Web サーバーで設定したアプリケーションを閲覧できるようにします。
この節を完了すれば AWS は以下の状態になります。
AWS.drawio.png

作成するまえにちょっとだけ概念のお話

ロードバランサー
リクエスト数が増加すると、一台のサーバーでは対処しきれなくなります。
その場合、複数のサーバーを用意して性能を上げる手法がとられます。
ただ、単純に複数のサーバーを用意してもそれを活かすことはできません。
そこで、用いられるのがロードバランサーです。
ロードバランサーを設定することで複数のリクエストがあった場合、各サーバーへ均等に分散します。
それによって複数サーバーを適切に動かすことができ、リクエスト数の増加に対応できます。
他にも、ロードバランサーで SSL 処理を行うことで、Web サーバーで暗号化を行う必要がなくなり、Web サーバーなどが暗号や複合する処理にリソースを取られることがなくなります。
また、リクエストが全てロードバランサーに集約されるので、不正なリクエストの対策をサーバーごとで行う必要がなく、ロードバランサーに集中できます。
【図で理解】ロードバランサーとは?負荷分散の仕組みを解説より引用
【図で理解】ロードバランサーとは?負荷分散の仕組みを解説より引用
ターゲットグループ
アベイラビリティゾーンをまたいでインスタンスやコンテナなどをまとめたグループとなります。
ターゲットグループを設定することで、一つの条件で異なるアベイラビリティゾーンのリソースにリクエストを振り分けることができます。
これによって、片方に障害が発生しても、もう片方のアベイラビリティゾーンにあるリソースへリクエストを流すことで、アプリなどが完全に止まってしまうことを防げます。

作成内容

ロードバランサー
紐づける VPC: sample-vpc
アベイラビリティゾーン: sample-subnet-public01,sample-subnet-public02 ※
※アベイラビリティゾーンと言っていますが、指定する値は各アベイラビリティゾーンにあるパブリックサブネットです。
セキュリティグループ: デフォルト、ポート番号 80 と 443 のリクエストを受け入れるセキュリティグループ
ターゲットグループ
名前: sample-tg
プロトコル: HTTP
ポート番号: 3000
Availabel instances(紐づけるインスタンス): sample-ec2-web01,sample-ec2-web02(共に Web サーバー)

Terraform のコード

//ロードバランサー用のセキュリティグループ
resource "aws_security_group" "sample-sg-elb-terraform" {
  name        = "sample-sg-elb-terraform"
  description = "for load balancer"
  vpc_id      = aws_vpc.sample-vpc.id
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
//ALBの設定
resource "aws_lb" "sample-elb-terraform" {
  name               = "sample-elb-terraform"
  internal           = false
  load_balancer_type = "application"
	ip_address_type = "ipv4"
  //セキュリティグループとサブネットを紐づけている
  security_groups = [module.vpc-module.default-security-groups-id, module.vpc-module.sample-security-group-elb-id]
  subnets = [module.vpc-module.sample-subnet-public01-id,
  module.vpc-module.sample-subnet-public02-id]
}
// ターゲットグループ
resource "aws_lb_target_group" "sample-tg-terraform" {
  name     = "sample-tg-terraform"
  port     = 3000
  protocol = "HTTP"
  vpc_id   = module.vpc-module.sample-vpc-id
}
// ターゲットグループにEC2インスタンスを登録
//1台目のEC2インスタンスに登録
resource "aws_lb_target_group_attachment" "sample-target_ec01" {
  target_group_arn = aws_lb_target_group.sample-tg-terraform.arn
  target_id        = aws_instance.sample-ec2-web01.id
}
//2台目のEC2インスタンスに登録
resource "aws_lb_target_group_attachment" "sample-target_ec02" {
  target_group_arn = aws_lb_target_group.sample-tg-terraform.arn
  target_id        = aws_instance.sample-ec2-web02.id
}
// リスナー設定
resource "aws_lb_listener" "sample-tg" {
  load_balancer_arn = aws_lb.sample-elb-terraform.arn
  port              = "80"
  protocol          = "HTTP"
  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.sample-tg-terraform.arn
  }
}

動作確認

それでは EC2 にファイルを作成して、ブラウザで表示されることを確認してみます。
まず、SSH 接続で Web サーバー 1 にアクセスして、Vim などで以下のコードを記載した HTML ファイルを作成します。

<body>
  Hello AWS Terraform
</body>

作成したらpython -m SimpleHTTPServer 3000を実行して、HTTP サーバーを起動します。
同様に以下の HTML ファイルを Web サーバー 2 に作成して、python -m SimpleHTTPServer 3000で HTTP サーバーを起動します。

<body>
  Hello AWS Terraform2
</body>

その後 EC2 のコンソールにアクセスして、サイドメニューからロードバランサーを選択します。
すると作成したロードバランサーの一覧が表示されるので、作成したロードバランサー内の「DNS 名」をコピーします。
2023-10-10_22h49_03.png
コピーした DNS 名をブラウザでアクセスすると、先程 Web サーバー 1 で作成した HTML ファイルが表示されています。
2023-10-11_00h01_39.png
ブラウザを更新すると、今度は Web サーバー 2 で作成した HTML ファイルが表示されます。
2023-10-11_00h01_59.png
これらが確認できたら、ロードバランサーの設定と Web サーバーへの接続は完了です。

参考資料

ALB 設定方法のコードを参考にした

Terraform の全体像

最後にこれまで作成した Terraform の全体像を載せます。

#初期設定
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.16"
    }
  }
  required_version = ">= 1.2.0"
}
provider "aws" {
  region     = "ap-northeast-1"
  access_key = var.aws_access_key_id
  secret_key = var.aws_secret_access_key
}
#VPCの作成
resource "aws_vpc" "sample-vpc" {
  cidr_block = "10.0.0.0/16"
  tags = {
    Name = "sample-vpc"
  }
}
#以下の作業を行った。
#①アベイラビリティゾーンの設定
#②アベイラビリティゾーンにそれぞれ、パブリックサブネットとプライベートサブネットの割り当て
resource "aws_subnet" "sample-subnet-public01" {
  vpc_id            = aws_vpc.sample-vpc.id
  availability_zone = "ap-northeast-1a"
  cidr_block        = "10.0.0.0/20"
  tags = {
    Name = "sample-subnet-public01"
  }
}
resource "aws_subnet" "sample-subnet-private01" {
  vpc_id            = aws_vpc.sample-vpc.id
  availability_zone = "ap-northeast-1a"
  cidr_block        = "10.0.64.0/20"
  tags = {
    Name = "sample-subnet-private01"
  }
}
resource "aws_subnet" "sample-subnet-public02" {
  vpc_id            = aws_vpc.sample-vpc.id
  availability_zone = "ap-northeast-1c"
  cidr_block        = "10.0.16.0/20"
  tags = {
    Name = "sample-subnet-public02"
  }
}
resource "aws_subnet" "sample-subnet-private02" {
  vpc_id            = aws_vpc.sample-vpc.id
  availability_zone = "ap-northeast-1c"
  cidr_block        = "10.0.80.0/20"
  tags = {
    Name = "sample-subnet-private02"
  }
}
#インターネットゲートウェイの作成
resource "aws_internet_gateway" "gw" {
  vpc_id = aws_vpc.sample-vpc.id
  tags = {
    Name = "sample-igw"
  }
}
#NATゲートウェイの作成とNATゲートウェイに使用するElastic IPの設定
resource "aws_eip" "elastic-ip-01" {
  domain = "vpc"
}
resource "aws_nat_gateway" "sample-ngw-01" {
  allocation_id = aws_eip.elastic-ip-01.id
  subnet_id     = aws_subnet.sample-subnet-public01.id
  tags = {
    Name = "sample-ngw-01"
  }
}
resource "aws_eip" "elastic-ip-02" {
  domain = "vpc"
}
resource "aws_nat_gateway" "sample-ngw-02" {
  allocation_id = aws_eip.elastic-ip-02.id
  subnet_id     = aws_subnet.sample-subnet-public02.id
  tags = {
    Name = "sample-ngw-02"
  }
}
#ルートテーブルの作成とサブネットとの紐づけ
resource "aws_route_table" "sample-rtb-public" {
  vpc_id = aws_vpc.sample-vpc.id
  route {
    cidr_block = "10.0.0.0/16"
    gateway_id = "local"
  }
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.gw.id
  }
  tags = {
    Name = "sample-rtb-public"
  }
}
resource "aws_route_table_association" "relate-public-route-table1" {
  subnet_id      = aws_subnet.sample-subnet-public01.id
  route_table_id = aws_route_table.sample-rtb-public.id
}
resource "aws_route_table_association" "relate-public-route-table2" {
  subnet_id      = aws_subnet.sample-subnet-public02.id
  route_table_id = aws_route_table.sample-rtb-public.id
}
resource "aws_route_table" "sample-rtb-private01" {
  vpc_id = aws_vpc.sample-vpc.id
  route {
    cidr_block = "10.0.0.0/16"
    gateway_id = "local"
  }
  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.sample-ngw-01.id
  }
  tags = {
    Name = "sample-rtb-private01"
  }
}
resource "aws_route_table_association" "relate-private-route-table1" {
  subnet_id      = aws_subnet.sample-subnet-private01.id
  route_table_id = aws_route_table.sample-rtb-private01.id
}
resource "aws_route_table" "sample-rtb-private02" {
  vpc_id = aws_vpc.sample-vpc.id
  route {
    cidr_block = "10.0.0.0/16"
    gateway_id = "local"
  }
  route {
    cidr_block     = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.sample-ngw-02.id
  }
  tags = {
    Name = "sample-rtb-private02"
  }
}
resource "aws_route_table_association" "relate-private-route-table2" {
  subnet_id      = aws_subnet.sample-subnet-private02.id
  route_table_id = aws_route_table.sample-rtb-private02.id
}
#セキュリティグループの作成
resource "aws_security_group" "sample-sg-bastion-terraform" {
  name        = "sample-sg-bastion-terraform"
  description = "for bastion server"
  vpc_id      = aws_vpc.sample-vpc.id
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
resource "aws_security_group" "sample-sg-elb-terraform" {
  name        = "sample-sg-elb-terraform"
  description = "for load balancer"
  vpc_id      = aws_vpc.sample-vpc.id
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
#デフォルトのセキュリティグループの作成
resource "aws_default_security_group" "default" {
  vpc_id = aws_vpc.sample-vpc.id
  ingress {
    from_port        = 0
    to_port          = 0
    protocol         = "-1"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }
  egress {
    from_port        = 0
    to_port          = 0
    protocol         = "-1"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }
}
#踏み台サーバー用とWebサーバー用のEC2作成とキーペアの設定
#さらに、踏み台サーバー経由による接続のためのconfigの作成
variable "key_name" {
  type        = string
  description = "keypair name"
  #キーペア名はここで指定
  default = "hoge-key"
}
locals {
  public_key_file  = "./.key_pair/${var.key_name}.id_rsa.pub"
  private_key_file = "./.key_pair/${var.key_name}.id_rsa"
}
#privateキーのアルゴリズム設定
resource "tls_private_key" "keygen" {
  algorithm = "RSA"
  rsa_bits  = 2048
}
#local_fileのリソースを指定するとterraformを実行するディレクトリ内でファイル作成やコマンド実行が出来る。
resource "local_file" "private_key_pem" {
  filename = local.private_key_file
  content  = tls_private_key.keygen.private_key_pem
  provisioner "local-exec" {
    command = "chmod 600 ${local.private_key_file}"
  }
}
resource "local_file" "public_key_openssh" {
  filename = local.public_key_file
  content  = tls_private_key.keygen.public_key_openssh
  provisioner "local-exec" {
    command = "chmod 600 ${local.public_key_file}"
  }
}
resource "aws_key_pair" "key_pair" {
  key_name   = var.key_name
  public_key = tls_private_key.keygen.public_key_openssh
  provisioner "local-exec" {
    command = <<-EOT
     echo "${tls_private_key.keygen.private_key_pem}" > /mnt/c/Users/tihou/.ssh/${var.key_name}.pem
    EOT
  }
}
data "aws_ami" "amzlinux2" {
  most_recent = true
  owners      = ["amazon"]
  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }
}
resource "aws_instance" "sample-ec2-bastion" {
  ami             = data.aws_ami.amzlinux2.id
  instance_type   = "t2.micro"
  subnet_id       = aws_subnet.sample-subnet-public01.id
  security_groups = [aws_security_group.sample-sg-bastion-terraform.id, aws_default_security_group.default.id]
  associate_public_ip_address = true
  key_name                    = aws_key_pair.key_pair.key_name
  tags = {
    Name = "sample-ec2-bastion"
  }
  lifecycle {
    ignore_changes = [ami]
  }
}
resource "aws_instance" "sample-ec2-web01" {
  ami             = data.aws_ami.amzlinux2.id
  instance_type   = "t2.micro"
  subnet_id       = aws_subnet.sample-subnet-private01.id
  security_groups = [aws_security_group.sample-sg-bastion-terraform.id, aws_default_security_group.default.id]
  associate_public_ip_address = false
  key_name                    = aws_key_pair.key_pair.key_name
  tags = {
    Name = "sample-ec2-web01"
  }
  lifecycle {
    ignore_changes = [ami]
  }
}
resource "aws_instance" "sample-ec2-web02" {
  ami             = data.aws_ami.amzlinux2.id
  instance_type   = "t2.micro"
  subnet_id       = aws_subnet.sample-subnet-private02.id
  security_groups = [aws_default_security_group.default.id]
  associate_public_ip_address = false
  key_name                    = aws_key_pair.key_pair.key_name
  tags = {
    Name = "sample-ec2-web02"
  }
  lifecycle {
    ignore_changes = [ami]
  }
}
resource "local_file" "config_file" {
  # filename = "/home/tihoutaikai2011/.ssh/config"
  filename = "/mnt/c/Users/tihou/.ssh/config"
  content  = <<-EOT
Host bastion
    Hostname ${aws_instance.sample-ec2-bastion.public_ip}
    User ec2-user
    IdentityFile ~/.ssh/hoge-key.pem
Host web01
    Hostname ${aws_instance.sample-ec2-web01.private_ip}
    User ec2-user
    IdentityFile ~/.ssh/hoge-key.pem
    ProxyCommand ssh.exe -W %h:%p bastion
Host web02
    Hostname ${aws_instance.sample-ec2-web02.private_ip}
    User ec2-user
    IdentityFile ~/.ssh/hoge-key.pem
    ProxyCommand ssh.exe -W %h:%p bastion
    EOT
}
#Applicatoin Load Balancerの作成
#バランサー部分
resource "aws_lb" "sample-elb-terraform" {
  name               = "sample-elb-terraform"
  internal           = false
  load_balancer_type = "application"
  //アプリケーションタイプのロードバランサーに対してのみ有効
  security_groups = [aws_security_group.sample-sg-elb-terraform.id, aws_default_security_group.default.id]
  subnets         = [aws_subnet.sample-subnet-public01.id, aws_subnet.sample-subnet-public02.id]
  ip_address_type = "ipv4"
}
#ターゲットグループ
resource "aws_lb_target_group" "sample-tg-terraform" {
  name     = "sample-tg-terraform"
  port     = 3000
  protocol = "HTTP"
  vpc_id   = aws_vpc.sample-vpc.id
}
#ターゲットグループにEC2インスタンスを登録
resource "aws_lb_target_group_attachment" "sample-target_ec01" {
  target_group_arn = aws_lb_target_group.sample-tg-terraform.arn
  target_id        = aws_instance.sample-ec2-web01.id
}
resource "aws_lb_target_group_attachment" "sample-target_ec02" {
  target_group_arn = aws_lb_target_group.sample-tg-terraform.arn
  target_id        = aws_instance.sample-ec2-web02.id
}
#リスナー設定
resource "aws_lb_listener" "sample-tg" {
  load_balancer_arn = aws_lb.sample-elb-terraform.arn
  port              = "80"
  protocol          = "HTTP"
  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.sample-tg-terraform.arn
  }
}

おわりに

今回は AWS と Terraform を使って、サーバーを作成しました。
素人ながらここまで作成できたのは良かったなと素直に思います。
ただ、概念の説明については読み直してみると、理解の浅さを感じました。
次に S3 など、他のリソースについてブログにするときはもう少しちゃんと調べて、理解して展開したいと思います。
ここまで読んでいただきありがとうございました。

全体的に参照した資料

AWS ドキュメント
AWS ではじめるインフラ構築入門 第 2 版 安全で堅牢な本番環境のつくり方
AWS の基本・仕組み・重要用語が全部わかる教科書
Amazon Web Services 基礎からのネットワーク&サーバー構築 改訂 3 版

Discussion