1. ホーム
  2. 記事一覧
  3. Terraformで構築するWeb2層構造 初心者向けハンズオン

2024.06.30

Terraformで構築するWeb2層構造 初心者向けハンズオン

1. はじめに

目的

このハンズオン記事では、Terraformを使用してAWS上に2層構造のWebアプリケーション環境を構築する手順を学びます。具体的には、VPC、パブリックサブネット、インターネットゲートウェイ、セキュリティグループ、EC2インスタンスをセットアップし、インターネット経由でアクセス可能なWebサーバーを作成します。

このハンズオンが役立つ場面

このハンズオンは、AWS上でインフラストラクチャをコードとして管理する方法を学びたい初心者に最適です。以下のような場面で役立ちます。

  • 開発環境の構築

    自動化された方法で簡単に再現可能な開発環境を構築する方法を学びます。

  • インフラストラクチャの管理

    Terraformを使用して、コードとしてインフラストラクチャを管理する手法を学びます。

このハンズオンが役立つ場面

このハンズオンは、実際の開発現場において、次のような場面で特に役立ちます。

  • リモートワーク環境の構築 開発者がどこからでもAWSリソースに安全にアクセスできるようにするための設定方法を学びます。

  • セキュリティ強化 公開されているリソースへの直接アクセスを避け、踏み台サーバーを介したアクセス方法を実装することでセキュリティを向上させます。

  • インフラストラクチャの自動化 Terraformを使用することで、インフラストラクチャの設定をコードとして管理し、再現性を持たせることができます。

  • チーム環境の統一 同じ設定を全てのチームメンバーが使用することで、環境の一貫性と設定ミスを減少させます。

前提条件

このハンズオンを始める前に、以下の準備が必要です。

  • AWSアカウント AWSのサービスを使用するために必要です。まだ持っていない場合は、こちらから作成してください。

  • 基本的なAWSの知識 EC2、VPC、IAMの基本概念についての理解が必要です。

  • 基本的なTerraformの知識 Terraformの基本的な操作(リソース定義、変数の使用、計画と適用など)を理解していることが望ましいです。こちらの記事を参考にして下さい。

    https://envader.plus/article/162

このハンズオンを通じて、AWS環境への安全なリモートアクセスの設定方法を実践的に学びましょう。それでは、始めていきます。

2. 準備

必要なツール

以下のツールをインストールしておいてください。

  • Terraform インフラストラクチャをコードとして管理するためのツールです。インストール方法は公式サイトのガイドに従ってください。

  • AWS CLI AWSのサービスをコマンドラインから操作するためのツールです。公式サイトからインストールパッケージをダウンロードし、インストールガイドに従ってください。

設定

このセクションでは、AWSアカウントの設定とAWS CLIの設定方法について説明します。準備としてローカルでキーペアも作成します。

  1. AWSアカウントの作成

    まだAWSアカウントを持っていない場合は、こちらから作成してください。アカウント作成にはクレジットカード情報が必要です。

  2. AWS CLIのインストール

    AWS CLIは、AWSのリソースを管理するためのコマンドラインツールです。以下のコマンドを使用してインストールします:

    • Windows MSIインストーラーを使用してインストールします。公式サイトからダウンロードできます。

    • macOS

      brew install awscli
    • Linux

      curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
      unzip awscliv2.zip
      sudo ./aws/install
  3. AWS CLIの設定

    AWS CLIをインストールしたら、以下のコマンドを実行して設定を行います。

    aws configure

    プロンプトに従い、以下の情報を入力します。

    • AWSアクセスキー: AWS Management Consoleの「IAM」セクションから取得できます。
    • AWSシークレットアクセスキー: 同上。
    • デフォルトのリージョン: 利用するリージョン(例: ap-northeast-1)。
    • 出力形式: json(デフォルトの出力形式)。
    AWS Access Key ID [None]: <your-access-key-id>
    AWS Secret Access Key [None]: <your-secret-access-key>
    Default region name [None]: ap-northeast-1
    Default output format [None]: json
  4. Terraformの設定

    Terraformの設定ファイル(.tfファイル)を作成するためのプロジェクトディレクトリを準備します。以下のコマンドでディレクトリを作成します。

    mkdir aws-2tier-web-infra
    cd aws-2tier-web-infra

    このディレクトリ内に、後のステップで使用するTerraform設定ファイルを配置します。

キーペアの作成

ここで、ローカルでSSHキーペアを作成します。これにより、踏み台サーバーにアクセスするためのキーが作成されます。

  1. ターミナル(またはコマンドプロンプト)を開き、以下のコマンドを実行してキーペアを生成します。

    ssh-keygen -t rsa -b 2048 -f my-key

    my-keyという名前でプライベートキー(my-key)とパブリックキー(my-key.pub)が生成されます。

  2. 生成されたパブリックキー(my-key.pub)をプロジェクトディレクトリにコピーします。

    cp ~/.ssh/my-key.pub /path/to/project/

3. リソースの作成

ディレクトリ構成

プロジェクトディレクトリの配下に、以下のファイルを用意します。

aws-2tier-web-infra/
├── main.tf
├── variables.tf
├── terraform.tfvars
├── vpc.tf
├── bastion.tf
├── private_server.tf
└── alb.tf

main.tf

provider "aws" {
  region = "ap-northeast-1"
}

# 最新のAmazon Linux 2023 AMIを取得
data "aws_ssm_parameter" "latest_amazon_linux_2023" {
name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-x86_64" # x86_64
}

variables.tf

variable "vpc_cidr" {
  description = "VPC CIDRブロック"
  default     = "10.0.0.0/16"
}

variable "public_subnet_cidrs" {
  description = "パブリックサブネットCIDRブロックのリスト"
  type        = map(string)
  default = {
    subnet1 = "10.0.1.0/24"
    subnet2 = "10.0.3.0/24"
  }
}

variable "private_subnet_cidrs" {
  description = "プライベートサブネットのCIDRブロックのリスト"
  type        = map(string)
  default = {
    subnet1 = "10.0.2.0/24"
    subnet2 = "10.0.4.0/24"
  }
}

variable "allowed_ssh_cidr" {
  description = "SSHアクセスを許可するCIDRブロック"
  default     = "0.0.0.0/0"
}

variable "instance_type" {
  description = "EC2インスタンスタイプ"
  default     = "t2.micro"
}

variable "key_name" {
  description = "SSHキーペアの名前"
}

terraform.tfvars

vpc_cidr = "10.0.0.0/16"
public_subnet_cidrs = {
  public1 = "10.0.1.0/24"
  public2 = "10.0.3.0/24"
}
private_subnet_cidrs = {
  private1 = "10.0.2.0/24"
  private2 = "10.0.4.0/24"
}
allowed_ssh_cidr = "0.0.0.0/0"
instance_type = "t2.micro"
key_name            = "my-key"  # ここにSSHキーペアの名前を入力

vpc.tf

# VPCの作成
resource "aws_vpc" "main" {
  cidr_block = var.vpc_cidr
  tags = {
    Name = "main-vpc"
  }
}

# インターネットゲートウェイの作成
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
  tags = {
    Name = "main-igw"
  }
}

# パブリックサブネットの作成
resource "aws_subnet" "public" {
  for_each = var.public_subnet_cidrs
  vpc_id                  = aws_vpc.main.id
  cidr_block              = each.value
  map_public_ip_on_launch = true
  availability_zone       = element(["ap-northeast-1a", "ap-northeast-1c"], index(keys(var.public_subnet_cidrs), each.key))
  tags = {
    Name = "public-subnet-${each.key}"
  }
}

# プライベートサブネットの作成
resource "aws_subnet" "private" {
  for_each = var.private_subnet_cidrs
  vpc_id     = aws_vpc.main.id
  cidr_block = each.value
  availability_zone = element(["ap-northeast-1a", "ap-northeast-1c"], index(keys(var.private_subnet_cidrs), each.key))
  tags = {
    Name = "private-subnet-${each.key}"
  }
}

# パブリックルートテーブルの作成
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }
  tags = {
    Name = "public-route-table"
  }
}

# パブリックサブネットとルートテーブルの関連付け
resource "aws_route_table_association" "public" {
  for_each       = aws_subnet.public
  subnet_id      = each.value.id
  route_table_id = aws_route_table.public.id
}

# NAT Gateway用のEIPの作成
resource "aws_eip" "nat" {
  domain = "vpc"
}

# NAT Gatewayの作成
resource "aws_nat_gateway" "main" {
  allocation_id = aws_eip.nat.id
  subnet_id     = aws_subnet.public["public1"].id
  tags = {
    Name = "main-nat-gateway"
  }
}

# プライベートルートテーブルの作成
resource "aws_route_table" "private" {
  vpc_id = aws_vpc.main.id
  route {
    cidr_block = "0.0.0.0/0"
    nat_gateway_id = aws_nat_gateway.main.id
  }
  tags = {
    Name = "private-route-table"
  }
}

# プライベートサブネットとルートテーブルの関連付け
resource "aws_route_table_association" "private" {
  for_each       = aws_subnet.private
  subnet_id      = each.value.id
  route_table_id = aws_route_table.private.id
}

# セキュリティグループの作成
resource "aws_security_group" "bastion_sg" {
  vpc_id = aws_vpc.main.id

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.allowed_ssh_cidr]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "bastion-sg"
  }
}

resource "aws_security_group" "private_sg" {
  vpc_id = aws_vpc.main.id

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    security_groups = [aws_security_group.bastion_sg.id]
  }

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/16"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "private-sg"
  }
}

resource "aws_security_group" "alb_sg" {
  vpc_id = aws_vpc.main.id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "alb-sg"
  }
}

output "vpc_id" {
  value = aws_vpc.main.id
}

output "public_subnet_ids" {
  value = [for subnet in aws_subnet.public : subnet.id]
}

output "private_subnet_ids" {
  value = [for subnet in aws_subnet.private : subnet.id]
}

output "bastion_sg_id" {
  value = aws_security_group.bastion_sg.id
}

output "private_sg_id" {
  value = aws_security_group.private_sg.id
}

output "alb_sg_id" {
  value = aws_security_group.alb_sg.id
}

bastion.tf

resource "aws_key_pair" "generated_key" {
  key_name   = var.key_name
  public_key = file("${path.module}/${var.key_name}.pub")
}

resource "aws_instance" "bastion" {
  ami                    = data.aws_ssm_parameter.latest_amazon_linux_2023.value
  instance_type          = var.instance_type
  subnet_id              = aws_subnet.public["public1"].id
  vpc_security_group_ids = [aws_security_group.bastion_sg.id]
  associate_public_ip_address = true
  key_name = aws_key_pair.generated_key.key_name
  tags = {
    Name = "BastionHost"
  }
  user_data = <<-EOF
              #!/bin/bash
              yum install -y amazon-ssm-agent
              systemctl start amazon-ssm-agent
              systemctl enable amazon-ssm-agent
              EOF
}

output "bastion_instance_id" {
  value = aws_instance.bastion.id
}

output "bastion_public_ip" {
  value = aws_instance.bastion.public_ip
}

private_server.tf

resource "aws_instance" "private" {
  ami                    = data.aws_ssm_parameter.latest_amazon_linux_2023.value
  instance_type          = var.instance_type
  subnet_id              = aws_subnet.private["private1"].id
  vpc_security_group_ids = [aws_security_group.private_sg.id]
  key_name = aws_key_pair.generated_key.key_name
  tags = {
    Name = "PrivateServer"
  }
  user_data = <<-EOF
              #!/bin/bash
              yum update -y
              yum install -y httpd
              systemctl start httpd
              systemctl enable httpd
              echo "Hello, World from Private Server" > /var/www/html/index.html
              EOF
}

output "private_instance_id" {
  value = aws_instance.private.id
}

output "private_instance_private_ip" {
  value = aws_instance.private.private_ip
}

resource "aws_lb_target_group" "main" {
  name        = "main-target-group"
  port        = 80
  protocol    = "HTTP"
  vpc_id      = aws_vpc.main.id
  health_check {
    path = "/"
    port = "80"
  }
  tags = {
    Name = "main-target-group"
  }
}

resource "aws_lb_target_group_attachment" "main" {
  target_group_arn = aws_lb_target_group.main.arn
  target_id        = aws_instance.private.id
  port             = 80
}

output "target_group_arn" {
  value = aws_lb_target_group.main.arn
}

alb.tf

resource "aws_lb" "main" {
  name               = "main-lb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb_sg.id]
  subnets            = [aws_subnet.public["public1"].id, aws_subnet.public["public2"].id]
  tags = {
    Name = "main-lb"
  }
}

resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.main.arn
  port              = "80"
  protocol          = "HTTP"
  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.main.arn
  }
}

output "alb_dns_name" {
  value = aws_lb.main.dns_name
}

4. デプロイ手順

初期化とプラン作成

まず、プロジェクトディレクトリに移動し、Terraformを初期化します。これにより、Terraformが使用するプロバイダーのプラグインがダウンロードされます。

cd aws-2tier-web-infra
terraform init

次に、Terraformプランを作成し、どのリソースが作成されるかを確認します。

terraform plan -var-file="terraform.tfvars"
# 変数ファイルがterraform.tfvarsという名称の場合、-var-fileは省略可能
terraform plan

計画を確認して問題がないか確認します。

リソースのデプロイ

Terraformプランを適用し、リソースをデプロイします。

terraform apply -var-file="terraform.tfvars"
# 変数ファイルがterraform.tfvarsという名称の場合、-var-fileは省略可能
terraform apply

プロンプトが表示されたら、yesと入力してリソースの作成を承認します。 タイミングにより下記エラーがでた場合は、再度デプロイして下さい。

Error: creating EC2 Subnet: InvalidSubnet.Conflict

5. Webサーバーの確認

デプロイが完了したら、ALBのDNS名を使用してWebサーバーにアクセスできます。以下のコマンドでALBのDNS名を取得します。

terraform output alb_dns_name

ブラウザで取得したDNS名にアクセスし、"Hello, World from Private Server"と表示されることを確認します。

6. Bastion Hostを使用したWebサーバーの更新

SSHで踏み台サーバーに接続

まず、AWS Management Consoleから作成したSSHキーペアをダウンロードします。このキーペアは踏み台サーバーとプライベートサーバーの両方で使用します。

次に、以下のコマンドでローカルマシンからBastion Hostに接続します。

ssh -i <path-to-key-pair.pem> ec2-user@<bastion-public-ip>

キーペアを踏み台サーバーにアップロード

踏み台サーバーに接続後、プライベートサーバーにアクセスするために、キーペアを踏み台サーバーにアップロードします。ローカルマシンから踏み台サーバーにキーペアをコピーします。

scp -i <path-to-key-pair.pem> <path-to-key-pair.pem> ec2-user@<bastion-public-ip>:/home/ec2-user/

プライベートサーバーに接続

踏み台サーバーに接続したら、次にプライベートサーバーにSSHで接続します。

ssh -i <path-to-key-pair.pem> ec2-user@<private-instance-private-ip>

Webサーバーの更新

プライベートサーバーに接続したら、以下のコマンドでWebサーバーの内容を更新します。

echo "Updated Content from Private Server" | sudo tee /var/www/html/index.html

ブラウザでALBのDNS名にアクセスし、更新された内容が表示されることを確認します。

7. まとめと後片付け

まとめ

このハンズオンでは、Terraformを使用してAWS環境にセキュアなリモートアクセスを設定する方法を学びました。具体的には、VPC、パブリックサブネット、プライベートサブネット、Bastion Host、およびロードバランサーを作成し、Bastion Hostを経由してプライベートサーバーにアクセスする手順を実践しました。この方法は、開発環境や本番環境でのセキュアなアクセスに非常に役立ちます。

リソースのクリーンアップ

作成したリソースを削除するには、以下のコマンドを実行します。

terraform destroy

プロンプトが表示されたら、yesと入力してリソースの削除を承認します。

参考資料

【番外編】USBも知らなかった私が独学でプログラミングを勉強してGAFAに入社するまでの話

IT未経験者必見 USBも知らなかった私が独学でプログラミングを勉強してGAFAに入社するまでの話

プログラミング塾に半年通えば、一人前になれると思っているあなた。それ、勘違いですよ。「なぜ間違いなの?」「正しい勉強法とは何なの?」ITを学び始める全ての人に知って欲しい。そう思って書きました。是非読んでみてください。

「フリーランスエンジニア」

近年やっと世間に浸透した言葉だ。ひと昔まえ、終身雇用は当たり前で、大企業に就職することは一種のステータスだった。しかし、そんな時代も終わり「優秀な人材は転職する」ことが当たり前の時代となる。フリーランスエンジニアに高価値が付く現在、ネットを見ると「未経験でも年収400万以上」などと書いてある。これに釣られて、多くの人がフリーランスになろうとITの世界に入ってきている。私もその中の1人だ。数年前、USBも知らない状態からITの世界に没入し、そこから約2年間、毎日勉学を行なった。他人の何十倍も努力した。そして、企業研修やIT塾で数多くの受講生の指導経験も得た。そこで私は、伸びるエンジニアとそうでないエンジニアをたくさん見てきた。そして、稼げるエンジニア、稼げないエンジニアを見てきた。

「成功する人とそうでない人の違いは何か?」

私が出した答えは、「量産型エンジニアか否か」である。今のエンジニア市場には、量産型エンジニアが溢れている!!ここでの量産型エンジニアの定義は以下の通りである。

比較的簡単に学習可能なWebフレームワーク(WordPress, Rails)やPython等の知識はあるが、ITの基本概念を理解していないため、単調な作業しかこなすことができないエンジニアのこと。

多くの人がフリーランスエンジニアを目指す時代に中途半端な知識や技術力でこの世界に飛び込むと返って過酷な労働条件で働くことになる。そこで、エンジニアを目指すあなたがどう学習していくべきかを私の経験を交えて書こうと思った。続きはこちらから、、、、

note記事3000いいね超えの殿堂記事 今すぐ読む

エンベーダー編集部

エンベーダーは、ITスクールRareTECHのインフラ学習教材として誕生しました。 「遊びながらインフラエンジニアへ」をコンセプトに、インフラへの学習ハードルを下げるツールとして運営されています。

RareTECH 無料体験授業開催中! オンラインにて実施中! Top10%のエンジニアになる秘訣を伝授します! RareTECH講師への質疑応答可

関連記事