Terraform にてAWS、GCP、Azure を使ったマルチクラウドを構築してみました。
今回使うgithubレポジトリは以下になります。
https://github.com/vn-cdr/multicloud-docker
- 1. 何を検証したいか
- 2. Terraform 実行環境をコンテナで用意
- 3. 接続設定
- 4. 各クラウド間VPN接続
- 5. VMインスタンスの追加と動作確認
- 6. terraform 処理概要
- 7. 今後について
1. 何を検証したいか
Terraform にて自由な形式のマルチクラウド環境を短時間で用意し、様々なケースを想定したアプリケーション開発役立てたい。
以下クラウドをVPNで繋げてDirectory Serviceを設定するところまでを対象にしたいと思います。
2. Terraform 実行環境をコンテナで用意
Docker コンテナ上であれば、プラットフォームを気にすることなくTerraformが使えます。
AWS/Azure/GCP APIクライアントを一つのコンテナにて管理するようにする。複数にしても管理が大変なだけ。
※terraformコマンドをローカルホスト上で実行できるようにしたいが、今後別途考える
3. 接続設定
Terraform実行のためにAPIクライアントを準備します。 各クラウドでAPI用ユーザの作成方法がやや異なります。
3.1. AWS APIクライアントの準備
AWSでは一般ユーザと同じIAM画面で作成できます。
認証に必要なパラメータ
AWS_ACCESS_KEY_ID="<aws_access_key_id>" AWS_SECRET_ACCESS_KEY="<aws_secret_access_key>"
取得手順:
① IAMにてユーザを作成する
- ユーザタブへ移動
- ユーザ作成を押下
② ユーザ設定
- ユーザ名設定
- アクセス種類から「プログラムによるアクセス」をチェック
③ ユーザの権限設定
- アクセス許可の設定から「既存ポリシーを直接アタッチ」を選択
- ポリシー一覧からポリシーを選択(AdministratiorAccessを選べば全てのサービスのオーナー権限を設定できます)
④ タグ設定(オプション)
- 必要な場合はここでタグを作成することにより、ユーザ管理に役立てられます
- 最後に右下の設定確認をして完了
3.2. GCP APIクライアントの準備
GCPでは サービスアカウント にてCLIアクセスが可能になります。 サービスアカウントでは扱うアカウント情報が多いためか一般的にjsonファイルを指定してCLIアクセスするようです。
<account名>.json
※今回account名はgcp-service-accountで設定します。
取得手順: サービスアカウント作成
① サービスアカウントの作成
- 「IAMと管理」からサービスタブを選択
- 「サービスアカウントの作成」を押下
- サービスアカウント名の設定や権限の設定を入力し進めていく
- 完了を押下
② サービスアカウントのアカウント情報を取得
- サービスアカウント一覧からの作成したアカウントの行の操作 列を選択して「鍵を作成」を選択する
- そうするとjsonファイルがダウンロードされる
③ サービスアカウント権限の有効化
- 使用するサービスアカウントのAPIを有効化します
3.3. Azure APIクライアントの準備
Azureでは サービスプリンシパル というユーザにてCLIアクセスを可能にします。
- 認証に必要なパラメータ
export ARM_SUBSCRIPTION_ID="<arm_subscription_id>" export ARM_CLIENT_ID="<arm_client_id>" export ARM_CLIENT_SECRET="<arm_client_secret>" export ARM_TENANT_ID="<arm_tenant_id>"
- 取得手順: サービスプリンシパルの作成
① アプリの登録
- Azure Active Directoryから「アプリ登録」タブを押下
② 新規ユーザ作成
- 新規作成を押下
③ 必要事項入力
- 情報を入力すれば完了(名前入力して、"サポートされているアカウントの種類"はそのまま)
4. 各クラウド間VPN接続
今回Terraform で構築するマルチクラウド論理構成は以下のようになります。
4.1. AWS/Azureクレデンシャルの設定
cd multicloud-docker mkdir ./.secret cat << EOL > ./.secret/admin-rc.txt # AWS Credential AWS_ACCESS_KEY_ID="<aws_access_key_id>" AWS_SECRET_ACCESS_KEY="<aws_secret_access_key>" # Azure Credential export ARM_SUBSCRIPTION_ID="<arm_subscription_id>" export ARM_CLIENT_ID="<arm_client_id>" export ARM_CLIENT_SECRET="<arm_client_secret>" export ARM_TENANT_ID="<arm_tenant_id>" EOL # GCPサービスアカウントファイルは以下に配置 vi ./.secret/gcp-service-account.json
4.2. terraform変数の設定
# サンプルからコピー cp terraform.tfvars.sample terraform.tfvars # e.g. を参考に変数を埋める (詳しくは 5.3. terraform.tfvars を参照ください) vi terraform.tfvars
4.3. コンテナ起動
# コンテナビルド docker build . -t multicloud-docker:latest # コンテナ起動 docker run -it --name multicloud-docker --env-file .secret/admin-rc.txt multicloud-docker:latest # コンテナに入れたことを確認 root@9f2f313b91bc:~# ls aws azure google main.tf providers.tf terraform.tfvars variables.tf vpn
4.4. Terraform 実行
# Terraform 初期設定 terraform init # tfファイルのフォーマットチェック terraform fmt -check # Terraform 実行計画 terraform plan # Terraform 実行 time TF_LOG=debug TF_LOG_PATH="./terraform_log_`date "+%Y%m%d-%H%M%S"`" terraform apply
GCPのAPIが無効になってエラーになる場合は有効化しておいてください。
terrafrom上でも有効化、無効化は可能ですが、エラーなどで意図せず有効化さたままになってしまうことを防ぐため、今回terraformには含めませんでした。
以下のようなエラーになる場合は terraform applyを再実行してくさい.
Error: Error creating customer gateway: MissingParameter: The request must contain the parameter ipAddress status code: 400, request id: XXXXXXXXX on vpn/aws.tf line 49, in resource "aws_customer_gateway" "azure": 49: resource "aws_customer_gateway" "azure" {
4.5. terraform apply 実行結果
# エラーなく以下の通りメッセージが出ればデプロイ完了です Apply complete! Resources: 65 added, 0 changed, 0 destroyed. real 59m21.090s user 0m12.054s sys 0m22.781s root@99ae86e95c98:~#
5. VMインスタンスの追加と動作確認
最後に動作確認の手順を記載します。 AWS、GCP、Azureで同じ準備が必要になります。
5.1. 各クラウドにVMインスタンスを追加
①AWS、GCP、Azure、にて以下条件でインスタンスを立ち上げる
- VPCは今回terraformで作成したものを使うこと
- サブネットは作成したVPC内のprivate subnetを使うこと
- セキュリティグループはICMPとSSHのインバウンド、アウトバウンドを許可にすること
- 他OSやスペックは好みのものを選んでください
②ファイアウォールまたはACLの穴あけが必要ですので、各クラウドのVPCに紐づいているACLに穴を開けてください
③外部IP(GIP)を設定している場合はsshできることを確認してください
5.2. ネットワーク疎通
下記の通りprivate-ipにtracepathすると インターネットを経由せずにAWS-GCP間で疎通が通ることを確認できる。 これはVPNトンネルにて通信がフォワーディングされているためである。
vn-cdr@instance-1:~$ tracepath 10.0.21.84 1?: [LOCALHOST] pmtu 1390 1: no reply 2: 10.0.21.84 30.783ms reached Resume: pmtu 1390 hops 2 back 2 vn-cdr@instance-1:~$
※インターネットを通ってしまう場合や疎通できない場合はVPC周りのルートまたはファイアウォールを確認すること
6. terraform 処理概要
処理を担うtfファイルは、なるべくサービスごとに分けて作成
. ├── aws │ ├── dns │ │ ├── main.tf │ │ ├── outputs.tf │ │ └── variables.tf │ └── vpc │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── azure │ └── vnet │ ├── main.tf │ ├── output.tf │ └── variables.tf ├── google │ └── vpc │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── main.tf ├── providers.tf ├── terraform.tfstate ├── terraform.tfstate.backup ├── terraform.tfvars ├── terraform.tfvars.sample ├── variables.tf └── vpn ├── aws.tf ├── azure.tf ├── google.tf └── variables.tf
6.1. 処理フロー
変数群は./providors.tf ./terraform.tfvars を順次読み込み ↓ AWSのawsのvpc、dns作成(./main.tf) ↓ GCPのVPC作成(./google/vpc/main.tf) ↓ AzureのVNET作成(./azure/vnet/main.tf) ↓ VPNに必要なVGW作成をAWS GCP Azure順で作成(./vpn/{aws.tf, google.tf, azure.tf}) ↓ 設定に問題がなければここで各クラウドの VGW でステータスが BGP Established になる
6.2. Dockerfile
Dockerfileでは各クラウドでterraformを実行するために必要なパッケージをインストールするように設定している。
FROM python:3.8 ARG pip_installer="https://bootstrap.pypa.io/get-pip.py" ARG awscli_version="1.16.168" # install aws-cli RUN pip install awscli==${awscli_version} # install sam RUN pip install --user --upgrade aws-sam-cli ENV PATH $PATH:/root/.local/bin # install command & azure-cli. RUN apt update && apt install -y less vim wget unzip curl npm && npm install azure-cli # install terraform. RUN wget https://releases.hashicorp.com/terraform/0.14.4/terraform_0.14.4_linux_amd64.zip && \ unzip ./terraform_0.14.4_linux_amd64.zip -d /usr/local/bin/ # copy resources. COPY ./main.tf /root/ COPY ./providers.tf /root/ COPY ./variables.tf /root/ COPY ./aws /root/aws COPY ./google /root/google COPY ./azure /root/azure COPY ./vpn /root/vpn COPY ./.secret/gcp-service-account.json /root/.config/gcloud/gcp-service-account.json COPY ./terraform.tfvars /root/ # initialize command. ENTRYPOINT ["/bin/bash"] WORKDIR /root
6.3. terraform.tfvars
以下の通りTerraform実行に必要な引数を定義している。構築リージョンや各PWなどをここで設定します。
aws_dns_suffix = "" # e.g. "compute.internal" aws_directory_service_password = "" # e.g. "Password123!" google_region = "" # e.g. "us-central1" google_project_id = "" # e.g. "sample-project" google_service_account_json = "" # e.g. "gcp-service-account.json" azure_resource_group_name = "" # e.g. "resource_group" azure_location = "" # e.g. "westeurope"
項目 | 説明 |
---|---|
aws_dns_suffix | AWS Managed Microsoft AD に使うDNSサフィックスを設定してください |
aws_directory_service_password | AWS Managed Microsoft AD に使うパスワードを設定してください |
google_region | リソースを作成するGCPのリージョンを設定してください |
google_project_id | 使用する Project ID を設定してください |
google_service_account_json | 使用するサービスアカウントのjsonファイルを指定してください |
azure_resource_group_name | Azureで作成するリソースグループを設定してください |
6.4. メイン処理(一部抜粋)
AWS全体に必要な変数はここに書きます。VPC、DNS、VPNはモジュール化します。
./main.tf
module "aws_vpc" { source = "./aws/vpc" vpc_name = "aws-test" cidr_block = "10.0.0.0/16" subnet_count = 1 } module "dns" { source = "./aws/dns" vpc_id = module.aws_vpc.vpc_id directory_name = "test.internal" directory_password = var.aws_directory_service_password dns_subnet_cidr_prefix = "10.0.0.0/20" private_route_table_id = module.aws_vpc.private_route_table_id } module "vpn" { source = "./vpn" aws_vpc_id = module.aws_vpc.vpc_id aws_route_table_ids = [module.aws_vpc.private_route_table_id, module.aws_vpc.public_route_table_id] dns_network_acl_id = module.dns.dns_network_acl_id }
./providers.tf
terraform { required_providers { aws = "~> 2.39.0" } } provider "aws" { region = var.aws_region }
./variables.tf
variable "aws_region" { type = string description = "aws region to use" } variable "aws_dns_suffix" { type = string description = "DNS suffix Setting" } variable "aws_directory_service_password" { type = string description = "password to use for the aws directory service (enabling DNS)" }
VPC設定
VPC周りはここに書きます。VPC、subnet、セキュリティグループ、ルーティングテーブルなどを作成します。
resource "aws_vpc" "main" { cidr_block = var.cidr_block tags = { Name = var.vpc_name } } resource "aws_subnet" "private" { count = var.subnet_count vpc_id = aws_vpc.main.id cidr_block = cidrsubnet(var.cidr_block, 4, count.index * 2 + 1) tags = { Name = "private-subnet-${count.index}" } } resource "aws_subnet" "public" { count = var.subnet_count vpc_id = aws_vpc.main.id cidr_block = cidrsubnet(var.cidr_block, 4, count.index * 2 + 2) tags = { Name = "public-subnet-${count.index}" } } resource "aws_network_acl" "public" { vpc_id = aws_vpc.main.id subnet_ids = aws_subnet.public[*].id ingress { protocol = "tcp" rule_no = 100 action = "allow" cidr_block = "0.0.0.0/0" from_port = 80 to_port = 80 } ingress { protocol = "tcp" rule_no = 200 action = "allow" cidr_block = "0.0.0.0/0" from_port = 443 to_port = 443 } ingress { protocol = "tcp" rule_no = 400 action = "allow" cidr_block = "0.0.0.0/0" from_port = 1024 to_port = 65535 } ingress { protocol = "udp" rule_no = 500 action = "allow" cidr_block = "0.0.0.0/0" from_port = 1024 to_port = 65535 } ingress { protocol = -1 rule_no = 1000 action = "allow" cidr_block = aws_vpc.main.cidr_block from_port = 0 to_port = 0 } egress { protocol = -1 rule_no = 100 action = "allow" cidr_block = "0.0.0.0/0" from_port = 0 to_port = 0 } tags = { Name = "public-acl" } } resource "aws_network_acl" "private" { vpc_id = aws_vpc.main.id subnet_ids = aws_subnet.private[*].id ingress { protocol = "tcp" rule_no = 400 action = "allow" cidr_block = "0.0.0.0/0" from_port = 1024 to_port = 65535 } ingress { protocol = "udp" rule_no = 500 action = "allow" cidr_block = "0.0.0.0/0" from_port = 1024 to_port = 65535 } ingress { protocol = -1 rule_no = 1000 action = "allow" cidr_block = aws_vpc.main.cidr_block from_port = 0 to_port = 0 } egress { protocol = -1 rule_no = 100 action = "allow" cidr_block = "0.0.0.0/0" from_port = 0 to_port = 0 } tags = { Name = "private-acl" } } # Gateways resource "aws_internet_gateway" "gw" { vpc_id = aws_vpc.main.id tags = { Name = "${var.vpc_name}-internet-gateway" } } resource "aws_eip" "nat" { vpc = true tags = { Name = "nat-elastic-ip" } depends_on = [aws_internet_gateway.gw] } resource "aws_nat_gateway" "gw" { allocation_id = aws_eip.nat.id subnet_id = aws_subnet.public[0].id tags = { Name = "${var.vpc_name}-nat-gateway" } depends_on = [aws_internet_gateway.gw] } # Route Tables resource "aws_route_table" "public" { vpc_id = aws_vpc.main.id tags = { Name = "public-route-table" } } resource "aws_route" "public_igw" { route_table_id = aws_route_table.public.id destination_cidr_block = "0.0.0.0/0" gateway_id = aws_internet_gateway.gw.id } resource "aws_route_table" "private" { vpc_id = aws_vpc.main.id tags = { Name = "private-route-table" } } resource "aws_route" "private_nat" { route_table_id = aws_route_table.private.id destination_cidr_block = "0.0.0.0/0" nat_gateway_id = aws_nat_gateway.gw.id } resource "aws_route_table_association" "public" { count = var.subnet_count subnet_id = aws_subnet.public[count.index].id route_table_id = aws_route_table.public.id } resource "aws_route_table_association" "private" { count = var.subnet_count subnet_id = aws_subnet.private[count.index].id route_table_id = aws_route_table.private.id }
以下は ./main.tf
のローカル変数から取得します。
./aws/vpc/variables.tf
variable "vpc_name" { type = string description = "the name for the vpc" } variable "cidr_block" { type = string description = "the cidr block to use for the vpc" } variable "subnet_count" { type = number description = "how many private and private subnets to create" }
以下は ./main.tf
のモジュール呼び出しに返します。
./aws/vpc/outputs.tf
output "vpc_id" { value = aws_vpc.main.id description = "the id of the vpc" } output "vpc_cidr_block" { value = aws_vpc.main.cidr_block description = "the cidr block of the vpc" } output "private_route_table_id" { value = aws_route_table.private.id description = "the private route table of the vpc" } output "public_route_table_id" { value = aws_route_table.public.id description = "the public route table of the vpc" }
以下にはRoute53とSimpleADを設定します。 ./aws/dns/main.tf
data "aws_vpc" "main" { id = var.vpc_id } data "aws_availability_zones" "available" { state = "available" } locals { dns_subnet_availability_zones = slice(data.aws_availability_zones.available.names, 0, 2) } resource "aws_subnet" "dns" { count = length(local.dns_subnet_availability_zones) vpc_id = var.vpc_id cidr_block = cidrsubnet(var.dns_subnet_cidr_prefix, 4, count.index) availability_zone = local.dns_subnet_availability_zones[count.index] tags = { Name = "directory-service-${local.dns_subnet_availability_zones[count.index]}" } } resource "aws_route_table_association" "dns" { count = length(aws_subnet.dns) route_table_id = var.private_route_table_id subnet_id = aws_subnet.dns[count.index].id } resource "aws_network_acl" "dns" { vpc_id = var.vpc_id subnet_ids = aws_subnet.dns[*].id tags = { Name = "directory-service-acl" } } resource "aws_network_acl_rule" "ingress" { network_acl_id = aws_network_acl.dns.id egress = false protocol = -1 rule_number = 100 rule_action = "allow" cidr_block = data.aws_vpc.main.cidr_block from_port = 0 to_port = 0 } resource "aws_network_acl_rule" "egress" { network_acl_id = aws_network_acl.dns.id egress = true protocol = -1 rule_number = 100 rule_action = "allow" cidr_block = "0.0.0.0/0" from_port = 0 to_port = 0 } resource "aws_directory_service_directory" "dns" { name = var.directory_name description = "internal directory for dns forwarding over vpns" type = "SimpleAD" size = "Small" password = var.directory_password vpc_settings { vpc_id = var.vpc_id subnet_ids = aws_subnet.dns[*].id } }
variable "vpc_id" { type = string description = "the id of the aws vpc" } variable "private_route_table_id" { type = string description = "private route table id of the aws vpc" } variable "directory_name" { type = string description = "the name for the directory" } variable "directory_password" { type = string description = "the admin password for the directory" } variable "dns_subnet_cidr_prefix" { type = string description = "the cidr prefix for aws dns server subnets" }
output "dns_ip_addresses" { value = split(",", join(",", aws_directory_service_directory.dns.dns_ip_addresses)) } output "dns_network_acl_id" { value = aws_network_acl.dns.id }
7. 今後について
続きの投稿は以下を予定していますが、投稿日は未定です。気長にお願いします。