以前の記事で、Terraform の基本的な使い方を紹介しました。ここから何回かに分けて、ECS を使ってアプリを動かす環境を Terraform で構築する方法を紹介していきます。
はじめに:最終的なゴールと今回の目標
- 最終的なゴールは下の図のような環境を構築することです。
- 今回は、ネットワーク部分構築を行います。以下の図の構成を目標に構築します。
main.tf
- 以前の記事で説明した、プロバイダの設定などを記載する
main.tf
を以下のように作成しておきます。
provider "aws" { version = "~> 2.0" profile = "terraformer" region = "ap-northeast-1" } terraform { required_version = "0.12.29" }
VPC
- VPC (Virtual Private Cloud) は他のネットワークから論理的に切り離された仮想ネットワークを提供するサービスです。
- 以下のように定義します。
- なお、リソースによっては、
Name
タグでリソースの名前を決められます。
resource "aws_vpc" "vpc" { cidr_block = "10.0.0.0/16" enable_dns_support = true enable_dns_hostnames = true tags = { Name = "example" } }
- 以下、VPC を更に分割してサブネットを作成していきます。
パブリックネットワーク
パブリックサブネット
- インターネットからアクセス可能なパブリックサブネットを以下のように定義します。
cidr_block
(必須): IPv4 アドレスの範囲を CIDR 形式で指定- 特にこだわりがなければ、VPC では
/16
単位、サブネットでは/24
単位にするとわかりやすい vpc_id
(必須): サブネットを作成する VPC の ID を指定- 先程作成した VPC の ID を参照します
- Terraform では「TYPE.NAME.ATTRIBUTE」の形式で書けば、他のリソースの値を参照できます。
map_public_ip_on_launch
:true
にすると、そのサブネットで起動したインスタンスにパブリック IP を自動的に割り当ててくれるavailability_zone
: サブネットを作成するアベイラビリティゾーンを指定する
resource "aws_subnet" "public_a" { vpc_id = aws_vpc.example.id cidr_block = "10.0.0.0/24" availability_zone = "ap-northeast-1a" map_public_ip_on_launch = true tags = { Name = "sn-pub-a" } } resource "aws_subnet" "public_c" { vpc_id = aws_vpc.example.id cidr_block = "10.0.1.0/24" availability_zone = "ap-northeast-1c" map_public_ip_on_launch = true tags = { Name = "sn-pub-c" } }
インターネットゲートウェイ
resource "aws_internet_gateway" "igw" { vpc_id = aws_vpc.example.id tags = { Name = "example-igw" } }
ルートテーブル
- ネットワークにデータを流すために、ルーティング情報を管理するルートテーブルを以下のように定義します。
- こちらも、VPC の ID を指定するだけです。
resource "aws_route_table" "public" { vpc_id = aws_vpc.example.id tags = { Name = "example-rt-pub" } }
- ルートテーブルを作成すると、 VPC 内の通信を有効にするため、ローカルルートが自動的に作成されるという特殊な仕様があるため注意が必要です。
ルート
- ルートテーブルの 1 レコードに該当するルートを定義します。
resource "aws_route" "public" { route_table_id = aws_route_table.public.id gateway_id = aws_internet_gateway.igw.id destination_cidr_block = "0.0.0.0/0" }
ルートテーブルの関連付け
- どのルートテーブルを使ってルーティングするかはサブネット単位で判断するため、以下のようにルートテーブルとサブネットを関連付けます。
- 関連付けを忘れた場合、デフォルトルートテーブルが自動的に使用されます。ただし、デフォルトルートテーブルの利用はアンチパターンであるため、関連付けは忘れないよう注意します。
resource "aws_route_table_association" "public_a" { route_table_id = aws_route_table.public.id subnet_id = aws_subnet.public_a.id } resource "aws_route_table_association" "public_c" { route_table_id = aws_route_table.public.id subnet_id = aws_subnet.public_c.id }
プライベートネットワーク
- インターネットから隔離されたプライベートネットワークには、データベースサーバのようなインターネットからアクセスしないリソースを配置します。
- システムをセキュアにするため、パブリックネットワークには必要最低限のリソースのみ配置し、それ以外はプライベートネットワークに置くのが定石
プライベートサブネット
- パブリックサブネットと同様にプライベートサブネットを定義します。
- パブリックサブネットとは異なる CIDR ブロックを指定します。
- パブリック IP アドレスは不要なため、
map_public_ip_on_launch
はfalse
に設定します。
resource "aws_subnet" "private_a" { vpc_id = aws_vpc.example.id cidr_block = "10.0.128.0/24" availability_zone = "ap-northeast-1a" map_public_ip_on_launch = false tags = { Name = "eks-sn-pri-a" } } resource "aws_subnet" "private_c" { vpc_id = aws_vpc.example.id cidr_block = "10.0.129.0/24" availability_zone = "ap-northeast-1c" map_public_ip_on_launch = false tags = { Name = "eks-sn-pri-c" } }
ルートテーブルとルートテーブルの関連付け
- プライベートネットワーク用のルートテーブルとその関連付けを以下のように実装します。
- インターネットゲートウェイに対するルーティング定義は不要です。
resource "aws_route_table" "private_a" { vpc_id = aws_vpc.example.id tags = { Name = "example-rt-pri-a" } } resource "aws_route_table" "private_c" { vpc_id = aws_vpc.example.id tags = { Name = "example-rt-pri-c" } } resource "aws_route_table_association" "private_a" { route_table_id = aws_route_table.private_a.id subnet_id = aws_subnet.private_a.id } resource "aws_route_table_association" "private_c" { route_table_id = aws_route_table.private_c.id subnet_id = aws_subnet.private_c.id }
NAT ゲートウェイ
- NAT (Network Address Translation) サーバは、プライベートネットワークからインターネットへアクセス出来るようにする役割を担います。
- AWS では、 NAT ゲートウェイを利用することで、自力で NAT サーバを構築せずともプライベートネットワークからインターネットへアクセスできるようになります。
EIP (Elastic IP Address)
- EIP は静的なパブリック IP アドレスを付与するサービスです。
- NAT ゲートウェイには必要なものです。
- AWS では、インスタンスを起動するたびに異なる IP アドレスが動的に割り当てられますが、 EIP を使うとパブリック IP アドレスが固定できます。
- EIP は以下のように定義します。
resource "aws_eip" "nat_a" { vpc = true depends_on = [aws_internet_gateway.igw] tags = { Name = "example-eip-a" } } resource "aws_eip" "nat_c" { vpc = true depends_on = [aws_internet_gateway.igw] tags = { Name = "example-eip-c" } }
NAT ゲートウェイ
- NAT ゲートウェイは以下のように定義します。
allocation_id
: 先ほど作成した EIP を指定subnet_id
: NAT ゲートウェイを配置するパブリックサブネットを指定(プライベートサブネットではない点に注意)
- NAT ゲートウェイも暗黙的にインターネットゲートウェイに依存するため、
depends_on
に明示します。
resource "aws_nat_gateway" "nat_a" { allocation_id = aws_eip.nat_a.id subnet_id = aws_subnet.public_a.id depends_on = [aws_internet_gateway.igw] tags = { Name = "example-nat-gw-a" } } resource "aws_nat_gateway" "nat_c" { allocation_id = aws_eip.nat_c.id subnet_id = aws_subnet.public_c.id depends_on = [aws_internet_gateway.igw] tags = { Name = "example-nat-gw-c" } }
ルート
- プライベートネットワークからインターネットに通信を行うためのルートを定義します。
- 以下のように、プライベートサブネットのルートテーブルに追加します。
destination_cidr_block
: デフォルトルートを指定nat_gateway_id
: ルーティングする NAT ゲートウェイを指定
resource "aws_route" "private_a" { route_table_id = aws_route_table.private_a.id nat_gateway_id = aws_nat_gateway.nat_a.id destination_cidr_block = "0.0.0.0/0" } resource "aws_route" "private_c" { route_table_id = aws_route_table.private_c.id nat_gateway_id = aws_nat_gateway.nat_c.id destination_cidr_block = "0.0.0.0/0" }
- ここまでで、必要なリソースは揃いました。
ここまでのまとめ
- ここまでのリソースをまとめて、
network.tf
に記載しておきます。
network.tf
の内容(クリックで展開)
# ----------------------------------------- # VPC # ----------------------------------------- resource "aws_vpc" "example" { cidr_block = "10.0.0.0/16" enable_dns_hostnames = true enable_dns_support = true instance_tenancy = "default" tags = { Name = "example-vpc" } } # ----------------------------------------- # Public Network # ----------------------------------------- # Subnet resource "aws_subnet" "public_a" { availability_zone = "ap-northeast-1a" cidr_block = "10.0.0.0/24" vpc_id = aws_vpc.example.id map_public_ip_on_launch = true tags = { Name = "sn-pub-a" } } resource "aws_subnet" "public_c" { availability_zone = "ap-northeast-1c" cidr_block = "10.0.1.0/24" vpc_id = aws_vpc.example.id map_public_ip_on_launch = true tags = { Name = "sn-pub-c" } } # Internet Gateway resource "aws_internet_gateway" "igw" { vpc_id = aws_vpc.example.id tags = { Name = "example-igw" } } # Route Table resource "aws_route_table" "public" { vpc_id = aws_vpc.example.id tags = { Name = "example-rt-pub" } } # Route resource "aws_route" "public" { route_table_id = aws_route_table.public.id gateway_id = aws_internet_gateway.igw.id destination_cidr_block = "0.0.0.0/0" } # Route Table Association resource "aws_route_table_association" "public_a" { route_table_id = aws_route_table.public.id subnet_id = aws_subnet.public_a.id } resource "aws_route_table_association" "public_c" { route_table_id = aws_route_table.public.id subnet_id = aws_subnet.public_c.id } # ----------------------------------------- # Private Network # ----------------------------------------- # Subnet resource "aws_subnet" "private_a" { availability_zone = "ap-northeast-1a" cidr_block = "10.0.128.0/24" vpc_id = aws_vpc.example.id map_public_ip_on_launch = false tags = { Name = "eks-sn-pri-a" } } resource "aws_subnet" "private_c" { availability_zone = "ap-northeast-1c" cidr_block = "10.0.129.0/24" vpc_id = aws_vpc.example.id map_public_ip_on_launch = false tags = { Name = "eks-sn-pri-c" } } # Route Table resource "aws_route_table" "private_a" { vpc_id = aws_vpc.example.id tags = { Name = "example-rt-pri-a" } } resource "aws_route_table" "private_c" { vpc_id = aws_vpc.example.id tags = { Name = "example-rt-pri-c" } } # Route resource "aws_route" "private_a" { route_table_id = aws_route_table.private_a.id nat_gateway_id = aws_nat_gateway.nat_a.id destination_cidr_block = "0.0.0.0/0" } resource "aws_route" "private_c" { route_table_id = aws_route_table.private_c.id nat_gateway_id = aws_nat_gateway.nat_c.id destination_cidr_block = "0.0.0.0/0" } # Route Table Association resource "aws_route_table_association" "private_a" { route_table_id = aws_route_table.private_a.id subnet_id = aws_subnet.private_a.id } resource "aws_route_table_association" "private_c" { route_table_id = aws_route_table.private_c.id subnet_id = aws_subnet.private_c.id } # Elastic IP Address resource "aws_eip" "nat_a" { vpc = true depends_on = [aws_internet_gateway.igw] tags = { Name = "example-eip-a" } } resource "aws_eip" "nat_c" { vpc = true depends_on = [aws_internet_gateway.igw] tags = { Name = "example-eip-c" } } # Nat Gateway resource "aws_nat_gateway" "nat_a" { allocation_id = aws_eip.nat_a.id subnet_id = aws_subnet.public_a.id depends_on = [aws_internet_gateway.igw] tags = { Name = "example-nat-gw-a" } } resource "aws_nat_gateway" "nat_c" { allocation_id = aws_eip.nat_c.id subnet_id = aws_subnet.public_c.id depends_on = [aws_internet_gateway.igw] tags = { Name = "example-nat-gw-c" } }
セキュリティグループの設定とモジュール化
セキュリティグループの定義
- セキュリティグループ本体は以下のように定義します
resource "aws_security_group" "example" { name = "example" vpc_id = aws_vpc.example.id }
セキュリティグループルール:インバウンド
- 外からのアクセスの許可・不許可のルールを設定するインバウンドルールは以下のように設定します
type
をingress
にすることでインバウンドルールになります。
- 今回は HTTP 通信ができるように 80 番ポートを許可します。
resource "aws_security_group_rule" "ingress" { type = "ingress" from_port = "80" to_port = "80" protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] security_group_id = aws_security_group.default.id }
セキュリティグループルール:アウトバウンド
- 外へのアクセスの許可・不許可のルールを設定するアウトバウンドルールは以下のように設定します
type
をegress
にすることでインバウンドルールになります。
- 今回は、すべての通信を許可しています。
resource "aws_security_group_rule" "egress" { type = "egress" from_port = 0 to_port = 0 protocol = "all" cidr_blocks = ["0.0.0.0/0"] security_group_id = aws_security_group.default.id }
ここまでの内容適用と確認
- ここまでの作業が完了したら、プロジェクトのルートディレクトリで、
terraform apply
を実行します。 - 完了後、マネジメントコンソールで VPC やサブネット、ルートテーブルが作成されていれば成功です。
$ terraform fmt $ terraform validate $ terraform plan $ terraform apply
セキュリティグループのモジュール化
- 頻繁に利用するものはモジュール化しておくと便利です。セキュリティグループをモジュール化します。
- モジュールを定義するファイルは、別ディレクトリ配下に置く必要があるため、
security_group
ディレクトリを作成し、その中にmain.tf
,variable.tf
,output.tf
の 3 つを作成します。
. |-- main.tf |-- network.tf `-- security_group // 新しく作成したディレクトリ |-- main.tf |-- output.tf `-- variable.tf
- 作成したファイルにはそれぞれ以下の設定を記載します:
- 入力パラメータは以下のとおりです:
name
: セキュリティグループ名vpc_id
: VPC の IDport
: 通信を許可するポート番号cidr_blocks
: 通信を許可する CIDR ブロック
- 入力パラメータは以下のとおりです:
main.tf
の内容(クリックで展開)
resource "aws_security_group" "default" { name = var.name vpc_id = var.vpc_id } resource "aws_security_group_rule" "ingress" { type = "ingress" from_port = var.port to_port = var.port protocol = "tcp" cidr_blocks = var.cidr_blocks security_group_id = aws_security_group.default.id } resource "aws_security_group_rule" "egress" { type = "egress" from_port = 0 to_port = 0 protocol = "all" cidr_blocks = ["0.0.0.0/0"] security_group_id = aws_security_group.default.id }
variable.tf
の内容(クリックで展開)
variable "name" { type = string } variable "vpc_id" { type = string } variable "port" { type = string } variable "cidr_blocks" { type = list(string) }
output.tf
の内容(クリックで展開)
output "security_group_id" { value = aws_security_group.default.id }
- このモジュールは以下のようにして利用します。
module "example_sg" { source = "./security_group" name = "module-sg" vpc_id = aws_vpc.example.id port = 80 cidr_blocks = ["0.0.0.0/0"] }
- このモジュール自体は次回以降利用することになります。
- 定義内容の適用は、Terraform プロジェクトのルートディレクトリで行います。
apply
をする前に、terraform get
を実行してモジュールを事前に取得することで、適用可能になります。
まとめ
- ここから何回かに分けて、ECS のサービス環境を Terraform で構築していきます。
- 今回は AWS のネットワーク環境の構築を行いました。
- ECS での利用を想定していますが、EKS や EC2 でも利用できる内容です。
- 今後も利用するモジュールの定義と、簡単に使い方を説明しました。
- 次回以降で利用する予定です。