(連載) HashiCorp活用例6: 一連のWebアプリ用のマルチベンダーインフラ展開を効率化
東京エレクトロンデバイスではHashiCorp社製品とその他各社製品とのマルチベンダー連携を通じて、お客様のWebアプリやクラウド、ITインフラなどの展開から運用までを効率化できるソリューション開発に取り組んでいます。さまざまな活用例をご紹介することで、HashiCorp社製品の提供する価値や可能性を発信していきたいと思っております。
はじめに
本連載最後の6記事目では、これまでの連載でご紹介してきた個々のパーツを組み合わせて構築したデモ環境のコードをご紹介します。
連載初回の記事で取り上げたユースケースに基づいて記載しておりますので、ユースケースの詳細につきましては過去の記事も併せてご参照ください。
連載記事一覧:
- Webアプリ用マルチベンダーインフラの自動構築
- 動的なバックエンドSSL証明書の管理
- 動的なDNSレコード管理
- 動的なロードバランサの構築
- プライベートネットワークでのIaC
- 一連のWebアプリ用のマルチベンダーインフラ展開を効率化 ←本記事
デモシナリオの構成
下図はデモシナリオ開始時の環境です。Terraform Cloudエージェント(以下 TFCエージェント)、Vaultサーバー、Infoblox仮想アプライアンスが動作しており、WebアプリとロードバランサであるBIG-IPはまだデプロイされていない状態です。
デモシナリオでは、一度のTerraform ApplyでWebアプリ用仮想マシン群とBIG-IPロードバランサをデプロイします。最終的に下図のように、クライアントへWebアプリが配信できる状態になるまでを自動化します。
デモシナリオのコードサンプル
デモシナリオを実現するために利用したTerraformコードやテンプレート等ファイルとディレクトリの構成、シナリオ実現のためのオペレーションフロー、実際のコードサンプルを紹介していきます。
コンテンツ一覧
- Terraformコードやファイルの構成
- オペレーションフローとコードサンプル
- Vaultから、Vaultエージェント用の新規AppRole IDとシークレットIDを発行
- Webアプリ用VMをAzure VMにデプロイ
- Azure VMに新規仮想マシンを構築する。ここでは3台のレプリカを配置する。
- Cloud-Initでセットアップスクリプトを実行する。セットアップスクリプトではアプリの展開、Vaultエージェントのインストールと設定を実施する。
- 各仮想マシンでWebアプリとVaultエージェントが起動する。
- VaultエージェントはVaultへAppRole IDで認証し、PKIシークレットから自サーバーのFQDNがCNに設定されているサーバー証明書と秘密鍵を取得する。またサーバー証明書の有効期限が切れる前に、自動的にPKIシークレットから新しいサーバー証明書と秘密鍵を取得・更新する。
- デプロイした仮想マシンを、Infoblox DNSサーバーにAレコードとして登録
- BIG-IPをAzure VMにデプロイ
- BIG-IPに各種設定を投入
- BIG-IPが参照するDNSサーバーにInfobloxのアドレスを登録し、Azure標準のDNSサーバーより優先度を高くする。
- クライアント向けに公的機関発行のサーバー証明書を登録する。
- クライアント向けSSLプロファイルを作成する。
- プールメンバーとしてWebアプリ用VMのFQDNを登録する。
- バーチャルサーバーを作成する。
Terraformコードやファイルの構成
以下はTerraformワークスペースに配置しているファイルやディレクトリ構成です。
[Terraform Workspace] + [certificates] | + ca.pub ← VaultサーバーへSSL接続時に使うCA証明書 | + [modules] | + [bigip_ltm_virtual_server] ← BIG-IPの設定を行う処理をまとめたカスタムモジュール | | + main.tf | | + outputs.tf | | + variables.tf | | + versions.tf | | | + [terraform-azurerm-linux-vm] ← AzureVMの作成を行う処理をまとめたカスタムモジュール | + [templates] | | + demo-vm.sh ← WebアプリVMのセットアップスクリプト | | + tfc-agent.sh ← TFCエージェントのセットアップスクリプト(前回記事のシェルスクリプトを参照) | | | + main.tf | + outputs.tf | + variables.tf | + versions.tf | + f5-bigip.tf ← bigip_ltm_virtual_serverモジュールを呼び出す内容を記述したTFファイル + infoblox.tf ← Infobloxの設定を行う処理をまとめたTFファイル + main.tf ← メインのTFファイル、主にterraform-azurerm-linux-vmモジュールを呼び出す内容を記述 + outputs.tf ← 実行後の出力を定義したTFファイル + providers.tf ← Provider設定をまとめたTFファイル + terraform.tfvars ← 引数パラメータの定義ファイル + variables.tf ← 引数宣言のTFファイル + vault-agent.tf ← Vaultエージェント関連でVaultサーバーの設定を行う処理をまとめたTFファイル + versions.tf ← TerraformやProviderのバージョン、バックエンド定義をまとめたTFファイル
1. Vaultエージェント用の新規AppRole IDとシークレットIDを発行
Vaultサーバーの認証に使います。ここで発行したIDをWebアプリ用VMに配置し、Vaultエージェントが利用します。
Terraform Workspace / vault-agent.tf
コードサンプル(クリックで表示/非表示)
resource "vault_approle_auth_backend_role" "fakeservice" {
backend = var.vault_approle_path
role_name = var.vault_approle_name
token_policies = [var.vault_policy_name]
}
resource "vault_approle_auth_backend_role_secret_id" "fakeservice" {
backend = var.vault_approle_path
role_name = vault_approle_auth_backend_role.fakeservice.role_name
}
2. Webアプリ用VMをAzure VMにデプロイ
Azure VM上にLinux仮想マシンを構築します。
- リソースグループ、VNetとSubnetは作成済みのものを使うので、dataリソースで情報を取り込んで参照しています。
- Azure VMのLinux仮想マシン作成処理はカスタムモジュール化し、使い回せるようにしています。
- コード的にはWebアプリ用VMとBIG-IP 仮想アプライアンスの両方の内容を記載しています。
Terraform Workspace / main.tf
コードサンプル(クリックで表示/非表示)
data "azurerm_resource_group" "rg" {
name = var.resource_group
}
data "azurerm_virtual_network" "vnet" {
name = var.virtual_network
resource_group_name = data.azurerm_resource_group.rg.name
}
data "azurerm_subnet" "subnet" {
name = var.subnet
virtual_network_name = data.azurerm_virtual_network.vnet.name
resource_group_name = data.azurerm_resource_group.rg.name
}
module "f5" {
source = "./modules/terraform-azurerm-linux-vm"
resource_group_name = data.azurerm_resource_group.rg.name
ssh_pub_key = ""
subnet_name = data.azurerm_subnet.subnet.name
network_name = data.azurerm_virtual_network.vnet.name
tags = var.tags
vm_size = "Standard_DS1_v2"
admin_username = var.admin_username
admin_password = var.admin_password
disable_password_authentication = false
marketplace = true
accept_marketplace_agreement = true
vm_name = "F5"
prefix = "ted-f5"
image_offer = "f5-big-ip-good"
image_publisher = "f5-networks"
image_sku = "f5-bigip-virtual-edition-25m-good-hourly"
image_version = "16.1.304000"
create_storage_account = true
rules = [
{
name = "https"
priority = "101"
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "*"
destination_address_prefix = "*"
description = "Allow HTTPS from the Internet to all VMs"
},
{
name = "https-8443"
priority = "102"
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "8443"
source_address_prefix = "x.x.x.x"
destination_address_prefix = "*"
description = "Allow https-8443 from x.x.x.x to all VMs"
},
{
name = "ssh"
priority = "103"
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "x.x.x.x"
destination_address_prefix = "*"
description = "Allow ssh from x.x.x.x to all VMs"
}
]
}
module "demovm" {
source = "./modules/terraform-azurerm-linux-vm"
count = var.demovm_count
resource_group_name = data.azurerm_resource_group.rg.name
ssh_pub_key = var.ssh_pub_key
subnet_name = data.azurerm_subnet.subnet.name
network_name = data.azurerm_virtual_network.vnet.name
tags = var.tags
vm_size = "Standard_B1s"
admin_username = var.admin_username
prefix = "ted-demo${count.index}"
create_storage_account = true
rules = [
{
name = "https-fake"
priority = "101"
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "10443"
source_address_prefix = "x.x.x.x"
destination_address_prefix = "*"
description = "Allow https-fake/10443 from x.x.x.x to all VMs"
},
{
name = "ssh"
priority = "102"
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "x.x.x.x"
destination_address_prefix = "*"
description = "Allow SSH from x.x.x.x to all VMs"
}
]
is_demovm = true
vault_url = var.vault_url
vault_ca_cert = file(var.ca_cert_file)
roleID = vault_approle_auth_backend_role.fakeservice.role_id
secretID = vault_approle_auth_backend_role_secret_id.fakeservice.secret_id
cert_ttl = var.cert_ttl
}
Terraform Workspace / modules / terraform-azurerm-linux-vm / main.tf
一般的なAzure VMのプロビジョニングに加え、本デモシナリオで利用するWebアプリ用VM、BIG-IP 仮想アプライアンス、TFCエージェントのデプロイも可能なモジュール構成になっています。(TFCエージェントのデプロイに利用するtfc-agent.shは前回の記事下部にあるシェルスクリプトですが、デモシナリオには含まれないため本ページには未記載です)
コードサンプル(クリックで表示/非表示)
locals {
# Create a list of adapters that have been created and associate them with the VM that will be provisioned
nic_list = flatten([for v in azurerm_network_interface_security_group_association.sgasc : v.network_interface_id])
}
data "azurerm_resource_group" "rg" {
name = var.resource_group_name
}
resource "random_id" "id" {
keepers = {
resource_group = var.resource_group_name
}
byte_length = 8
}
data "azurerm_subnet" "sub" {
name = var.subnet_name
virtual_network_name = var.network_name
resource_group_name = data.azurerm_resource_group.rg.name
}
resource "azurerm_network_security_group" "nsg" {
name = "${var.prefix}-sg"
location = data.azurerm_resource_group.rg.location
resource_group_name = data.azurerm_resource_group.rg.name
tags = var.tags
}
resource "azurerm_network_security_rule" "sgr" {
for_each = { for v in var.rules : v.name => v }
resource_group_name = data.azurerm_resource_group.rg.name
network_security_group_name = azurerm_network_security_group.nsg.name
direction = each.value.direction
name = each.value.name
priority = try(each.value.priority, null)
access = each.value.access
protocol = each.value.protocol
source_address_prefix = each.value.source_address_prefix
source_port_range = each.value.source_port_range
destination_address_prefix = each.value.destination_address_prefix
destination_port_range = each.value.destination_port_range
description = lookup(each.value, "description", null)
}
resource "azurerm_storage_account" "sa" {
count = var.create_storage_account ? 1 : 0
name = "bootdiag${random_id.id.hex}"
location = data.azurerm_resource_group.rg.location
resource_group_name = data.azurerm_resource_group.rg.name
account_tier = "Standard"
account_replication_type = "LRS"
tags = var.tags
}
resource "azurerm_network_interface_security_group_association" "sgasc" {
for_each = { for v in var.vm_nic_quantity : v.name => v }
network_interface_id = azurerm_network_interface.nic[each.key].id
network_security_group_id = azurerm_network_security_group.nsg.id
}
resource "azurerm_ssh_public_key" "sshpublickey" {
count = var.disable_password_authentication ? 1 : 0
name = "${var.prefix}-${random_id.id.hex}-sshpubkey"
location = data.azurerm_resource_group.rg.location
resource_group_name = data.azurerm_resource_group.rg.name
public_key = var.ssh_pub_key
tags = var.tags
}
resource "azurerm_public_ip" "pip" {
for_each = { for v in var.vm_nic_quantity : v.name => v if v.public }
name = "${var.prefix}-${random_id.id.hex}-${each.value.name}-pip"
location = data.azurerm_resource_group.rg.location
resource_group_name = data.azurerm_resource_group.rg.name
allocation_method = "Dynamic"
domain_name_label = "${var.prefix}-${random_id.id.hex}-${var.domain_name_label}"
tags = var.tags
}
resource "azurerm_marketplace_agreement" "ama" {
count = var.accept_marketplace_agreement ? 1 : 0
publisher = var.image_publisher
offer = var.image_offer
plan = var.image_sku
}
resource "azurerm_network_interface" "nic" {
for_each = { for v in var.vm_nic_quantity : v.name => v }
name = "${var.prefix}-${random_id.id.hex}-${var.vm_name}-${each.value.name}-nic"
location = data.azurerm_resource_group.rg.location
resource_group_name = data.azurerm_resource_group.rg.name
ip_configuration {
name = "${var.prefix}-${random_id.id.hex}-${var.vm_name}-${each.value.name}-ipconfig"
subnet_id = data.azurerm_subnet.sub.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = each.value.public ? azurerm_public_ip.pip[each.key].id : null
}
tags = var.tags
}
resource "azurerm_linux_virtual_machine" "vm" {
name = "${var.prefix}-${random_id.id.hex}-${var.vm_name}"
location = data.azurerm_resource_group.rg.location
resource_group_name = data.azurerm_resource_group.rg.name
size = var.vm_size
network_interface_ids = local.nic_list
computer_name = "${var.prefix}-${random_id.id.hex}-${var.vm_name}"
admin_username = var.admin_username
disable_password_authentication = var.disable_password_authentication
admin_password = var.disable_password_authentication ? null : var.admin_password
user_data = length(var.cloud_init) >= 1 || var.tfc_agent || var.is_demovm ? data.cloudinit_config.cic[0].rendered : null
source_image_reference {
publisher = var.marketplace && var.accept_marketplace_agreement ? azurerm_marketplace_agreement.ama[0].publisher : var.image_publisher
offer = var.marketplace && var.accept_marketplace_agreement ? azurerm_marketplace_agreement.ama[0].offer : var.image_offer
sku = var.marketplace && var.accept_marketplace_agreement ? azurerm_marketplace_agreement.ama[0].plan : var.image_sku
version = var.image_version
}
dynamic "plan" {
for_each = var.marketplace ? { "marketplace" : "" } : {}
content {
name = var.image_sku
product = var.image_offer
publisher = var.image_publisher
}
}
os_disk {
name = "${var.prefix}-${random_id.id.hex}-${var.vm_name}-osdisk"
storage_account_type = "Standard_LRS"
caching = "ReadWrite"
}
dynamic "boot_diagnostics" {
for_each = var.create_storage_account ? { "sa" : "" } : {}
content {
storage_account_uri = azurerm_storage_account.sa[0].primary_blob_endpoint
}
}
dynamic "admin_ssh_key" {
for_each = var.disable_password_authentication ? { "dpa" : "" } : {}
content {
username = var.admin_username
public_key = azurerm_ssh_public_key.sshpublickey[0].public_key
}
}
# Added to allow destroy to work correctly.
depends_on = [azurerm_network_interface_security_group_association.sgasc]
tags = var.tags
}
data "cloudinit_config" "cic" {
count = length(var.cloud_init) > 0 || var.tfc_agent || var.is_demovm ? 1 : 0
gzip = true
base64_encode = true
dynamic "part" {
for_each = { for v in var.cloud_init : v.filename => v }
content {
filename = part.value.filename
content_type = try(part.value.content_type, "text/x-shellscript")
content = file("${path.module}/${part.value.filename}")
}
}
dynamic "part" {
for_each = length(var.tfc_token) >= 1 ? { "tfc_agent" : "" } : {}
content {
filename = "tfc-agent.sh"
content_type = "text/x-shellscript"
content = templatefile("${path.module}/templates/tfc-agent.sh", {
tfc_token = var.tfc_token
tfc_agent_name = "${var.prefix}-${var.vm_name}"
})
}
}
dynamic "part" {
for_each = length(var.roleID) >= 1 && length(var.secretID) >= 1 ? { "demovm" : "" } : {}
content {
filename = "demo-vm.sh"
content_type = "text/x-shellscript"
content = templatefile("${path.module}/templates/demo-vm.sh", {
vault_url = var.vault_url,
vault_ca_cert = var.vault_ca_cert,
roleID = var.roleID,
secretID = var.secretID,
hostname = var.prefix,
ttl = var.cert_ttl
})
}
}
}
Terraform Workspace / modules / terraform-azurerm-linux-vm / outputs.tf
コードサンプル(クリックで表示/非表示)
output "prefix" {
value = var.prefix
description = "Prefix specified to the resource."
}
output "private_ip_address" {
value = {for k,v in azurerm_network_interface.nic : k => v.ip_configuration[0].private_ip_address}
description = "Map of private IP addresses that have been assigned to the VMs that have been deployed. {nic_name1=ip_address1, nic_name2=ip_address2}"
}
output "public_ip_address" {
value = {for k,v in azurerm_public_ip.pip : k => v.ip_address}
}
output "fqdn" {
value = {for k,v in azurerm_public_ip.pip : k => v.fqdn}
}
Terraform Workspace / modules / terraform-azurerm-linux-vm / variables.tf
コードサンプル(クリックで表示/非表示)
variable "prefix" {
type = string
description = "Prefix that will be assigned to all resources that will be created as a part of the Terraform run."
default = "ted"
}
# ------------------------------
# Azure Configuration
# ------------------------------
variable "resource_group_name" {
type = string
description = "Name of the resource group that will be used for the resource deployments"
}
variable "subnet_name" {
type = string
description = "Name of the Subnet where the resources will be deployed"
}
variable "network_name" {
type = string
description = "Name of the network where the subnets reside"
}
variable "ssh_pub_key" {
type = string
description = "Public key to associate with the instance"
}
variable "vm_size" {
type = string
description = "Size of the instance that will be deployed."
default = "Standard_B1ms"
}
variable "admin_username" {
type = string
description = "Name of the admin user that will be created on the instance."
default = "azureuser"
}
variable "admin_password" {
type = string
description = "Password of the admin user that will be created on the instance."
sensitive = true
}
variable "disable_password_authentication" {
type = bool
description = "Disable password authentication to log into the instance."
default = true
}
variable "image_publisher" {
type = string
description = "Name of the publisher of the image (az vm image list)"
default = "Canonical"
}
variable "image_offer" {
type = string
description = "Name of the offer (az vm image list)"
default = "0001-com-ubuntu-server-jammy"
}
variable "image_sku" {
type = string
description = "Image SKU to apply (az vm image list)"
default = "22_04-lts-gen2"
}
variable "image_version" {
type = string
description = "Version of the image to apply (az vm image list)"
default = "latest"
}
variable "vm_name" {
type = string
description = "Name that will be assigned to the VM that will be deployed"
default = "vm"
}
variable "vm_nic_quantity" {
type = list(object({
name = optional(string, "nic1")
public = optional(bool, true)
}))
description = "List of NICs that will be allocated to the VM."
default = [{
"name" = "nic1"
"public" = true
}]
}
variable "marketplace" {
type = bool
description = "Boolean that when true, will utilize the plan block for the image you are deploying. This assumes you have accepted the marketplace agreement already. If you haven't, please also set `accept_markeptlace_agreement` to true. Do not set this to true if not using OEM appliances from the marketplace."
default = false
}
variable "accept_marketplace_agreement" {
type = bool
description = "Boolean that when true, will generate the marketplace agreement for the image (if required). Do not set this to true if not using OEM appliances from the marketplace."
default = false
}
variable "create_storage_account" {
type = bool
description = "Boolean that when true, will create a storage account to be utilized by the VM for boot diagnostics."
default = false
}
variable "cloud_init" {
type = list(map(string))
default = []
description = <<DESC
List of maps that allow for the user to supply cloud-init shell scripts for the VM to execute during boot
{
filename = "myscript.sh"
content_type = "text/x-shellscript"
content = "./myscript.sh"
}
DESC
}
variable "rules" {
type = list(map(string))
default = []
description = <<DESC
Map of strings that represent rules that will be created in the security group.
{
name = "https"
prioriity = "101"
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "1.1.1.1"
destination_address_prefix = "*"
description = "Allow HTTPS from 1.1.1.1 to all VMs"
}
{
name = "ssh"
prioriity = "102"
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "1.1.1.1"
destination_address_prefix = "*"
description = "Allow SSH from 1.1.1.1 to all VMs"
}
DESC
}
variable "tags" {
type = map(string)
description = "Tag that will be associated with the resources that will be created as a part of the Terraform run."
default = null
}
variable "domain_name_label" {
type = string
description = "Label for the domain name that will be used"
default = "domainlabel"
}
# ------------------------------
# Terraform Cloud Agent Virtual Machine Configuration
# ------------------------------
variable "tfc_agent" {
type = bool
description = "Boolean that when true, will customize the target VM to "
default = false
}
variable "tfc_token" {
type = string
description = "TFC token that will be used if presented. If this variable is defined, then an agent VM will be provisioned."
default = ""
}
# ------------------------------
# Demo Web App Virtual Machine Configuration
# ------------------------------
variable "is_demovm" {
type = bool
description = "Boolean that when true, will customize the target VM to "
default = false
}
variable "vault_url" {
type = string
description = "The endpoint of Vault Server to be issued a private ssl certificate. ex. https://ip_address:8200"
default = ""
}
variable "vault_ca_cert" {
type = string
description = "CA certificate to access to Vault Server."
default = ""
}
variable "roleID" {
type = string
description = "Vault AppRole ID that will be used if presented. If this variable is defined, then an agent VM will be provisioned."
default = ""
}
variable "secretID" {
type = string
description = "Vault AppRole Secret ID that will be used if presented. If this variable is defined, then an agent VM will be provisioned."
default = ""
sensitive = true
}
variable "cert_ttl" {
type = string
description = "TTL of a private ssl certificate issued by Vault Server."
default = "7d"
}
Terraform Workspace / modules / terraform-azurerm-linux-vm / versions.tf
コードサンプル(クリックで表示/非表示)
terraform {
required_version = ">= 1.2.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~>2.0"
}
}
}
Terraform Workspace / modules / terraform-azurerm-linux-vm / templates / demo-vm.sh
コードサンプル(クリックで表示/非表示)
#!/bin/bash
set -x
# -----
# Set needrestart config for apt upgrade in case of Ubuntu 21.04 or later
# -----
cat << 'EOF' > ~/50_restart.conf
$nrconf{kernelhints} = '0';
$nrconf{restart} = 'l';
EOF
sudo chown root:root ~/50_restart.conf
sudo mv ~/50_restart.conf /etc/needrestart/conf.d/50_restart.conf
# -----
# Upgrade and install packages
# -----
sudo apt-get clean
# HashiCorp Repo
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
# Update & Upgrade
sudo apt-get update
sudo apt-get upgrade -y
# Install packages
sudo apt install -y python3-pip openssl vault
sudo pip3 install Flask
sudo pip3 install gunicorn
# Cleanup needrestart config
sudo rm -f /etc/needrestart/conf.d/50_restart.conf
# -----
# Flask and Gunicorn
# -----
# Create working directory
sudo mkdir -p /var/www/templates
sudo chown www-data:www-data /var/www
sudo chown www-data:www-data /var/www/templates
# fakeapp.py: flask application
cat << 'EOF' > ~/fakeapp.py
from flask import Flask, render_template, request
import socket
# Flask App
app = Flask(__name__)
@app.route('/')
def fakeapp():
hostname = socket.gethostname()
hostip = socket.gethostbyname(hostname)
return render_template("fakeapp_tmpl.html",
hostname=hostname,
hostip=hostip,
remoteip=request.remote_addr
)
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0")
EOF
sudo chown www-data:www-data ~/fakeapp.py
sudo mv ~/fakeapp.py /var/www/
# fakeapp_tmpl.html: template html
cat << 'EOF' > ~/fakeapp_tmpl.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>Fake Web App</title>
</head>
<body>
<header></header>
<main>
<h1>Fake Web App</h1>
<h2>This test web application is working on Hostname: {{hostname}}, IP Address: {{hostip}}.</h2>
<h2>And connected from IP Address: {{remoteip}}.</h2>
</main>
<footer>
Copyright Tokyo Electron Device, Ltd.
</footer>
</body>
</html>
EOF
sudo chown www-data:www-data ~/fakeapp_tmpl.html
sudo mv ~/fakeapp_tmpl.html /var/www/templates/
# Generate temporary ssl key and cert
SSL_CERTS_DIR=/usr/local/ssl/certs
sudo mkdir -p $SSL_CERTS_DIR
sudo chown vault:vault $SSL_CERTS_DIR
sudo openssl genrsa -out $SSL_CERTS_DIR/server.key 2048
sudo openssl req -new -key $SSL_CERTS_DIR/server.key -out $SSL_CERTS_DIR/server.csr -subj "/CN=test"
sudo openssl x509 -req -days 3650 -signkey $SSL_CERTS_DIR/server.key -in $SSL_CERTS_DIR/server.csr -out $SSL_CERTS_DIR/server.crt
sudo chown vault:vault $SSL_CERTS_DIR/*
sudo chmod 640 $SSL_CERTS_DIR/*
sudo usermod -aG vault www-data
# Create log directory
sudo mkdir /var/log/gunicorn
sudo chown www-data:www-data /var/log/gunicorn
# gunicorn.service: systemd unit file for Gunicorn
cat <<EOF > ~/gunicorn.service
[Unit]
Description="Gunicorn Deamon"
Requires=network-online.target
After=network-online.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www
ExecStart=/usr/local/bin/gunicorn fakeapp:app --bind=0.0.0.0:10443 --keyfile /usr/local/ssl/certs/server.key --certfile /usr/local/ssl/certs/server.crt
ExecReload=/bin/kill -HUP $MAINPID
KillMode=process
KillSignal=SIGTERM
Restart=on-failure
LimitNOFILE=65536
StandardOutput=append:/var/log/gunicorn/gunicorn.log
StandardError=append:/var/log/gunicorn/gunicorn.log
[Install]
WantedBy=multi-user.target
EOF
chmod 644 ~/gunicorn.service
sudo chown root:root ~/gunicorn.service
sudo mv ~/gunicorn.service /lib/systemd/system/gunicorn.service
# -----
# Vault Agent
# -----
VAULT_DIR=/etc/vault.d
# vault-agent.hcl: Vault agent config
cat <<EOF > ~/vault-agent.hcl
pid_file = "/etc/vault.d/pidfile"
vault {
address = "${vault_url}"
ca_cert = "/etc/vault.d/ca.pem"
}
auto_auth {
method "approle" {
mount_path = "auth/fakeservice-role"
config = {
role_id_file_path = "/etc/vault.d/roleID"
secret_id_file_path = "/etc/vault.d/secretID"
remove_secret_id_file_after_reading = false
}
}
sink "file" {
config = {
path = "/etc/vault.d/approleToken"
}
}
}
#------------------------------------------------------------------------------
# Web Certificate Templates
#------------------------------------------------------------------------------
template {
source = "/etc/vault.d/private-key.ctmpl"
destination = "/usr/local/ssl/certs/server.key"
}
template {
source = "/etc/vault.d/cert.ctmpl"
destination = "/usr/local/ssl/certs/server.crt"
}
EOF
sudo chown vault:vault ~/vault-agent.hcl
sudo mv ~/vault-agent.hcl $VAULT_DIR/vault-agent.hcl
# ca.pem: CA cert for accessing Vualt server
echo "${vault_ca_cert}" > ~/ca.pem
sudo chown vault:vault ~/ca.pem
sudo mv ~/ca.pem $VAULT_DIR/ca.pem
# roleID: Role ID file of AppRole auth
echo "${roleID}" > ~/roleID
sudo chown vault:vault ~/roleID
sudo chmod 600 ~/roleID
sudo mv ~/roleID $VAULT_DIR/roleID
# secretID: Secret ID file of AppRole auth
echo "${secretID}" > ~/secretID
sudo chown vault:vault ~/secretID
sudo chmod 600 ~/secretID
sudo mv ~/secretID $VAULT_DIR/secretID
# cert.ctmpl: Template file to generate ssl server cert dynamically
cat <<EOF > ~/cert.ctmpl
{{ with secret "ted_inter_ca_pki/issue/ted-fakeservice" "common_name=${hostname}.poc.domain.local" "ttl=${ttl}"}}
{{ .Data.certificate}}
{{ end }}
EOF
sudo chown vault:vault ~/cert.ctmpl
sudo mv ~/cert.ctmpl $VAULT_DIR/cert.ctmpl
# private-key.ctmpl: Template file to generate ssl server key dynamically
cat <<EOF > ~/private-key.ctmpl
{{ with secret "ted_inter_ca_pki/issue/ted-fakeservice" "common_name=${hostname}.poc.domain.local" "ttl=${ttl}"}}
{{ .Data.private_key }}
{{ end }}
EOF
sudo chown vault:vault ~/private-key.ctmpl
sudo mv ~/private-key.ctmpl $VAULT_DIR/private-key.ctmpl
# vault-agent.service: Vault Agent systemd unit file
cat <<EOF > ~/vault-agent.service
[Unit]
Description="HashiCorp Vault Agent - A tool for managing secrets"
Documentation=https://www.vaultproject.io/docs/
Requires=network-online.target
After=network-online.target
[Service]
User=vault
Group=vault
ExecStart=/usr/bin/vault agent -config=/etc/vault.d/vault-agent.hcl
ExecReload=/bin/kill -HUP $MAINPID
KillMode=process
KillSignal=SIGTERM
Restart=on-failure
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
EOF
chmod 644 ~/vault-agent.service
sudo chown root:root ~/vault-agent.service
sudo mv ~/vault-agent.service /lib/systemd/system/vault-agent.service
# Start gunicorn.service
sudo systemctl daemon-reload
sudo systemctl enable gunicorn.service
sudo systemctl enable vault-agent.service
sudo systemctl start gunicorn.service
sudo systemctl start vault-agent.service
3. Infoblox DNSサーバーにAレコードとして登録
Terraform Workspace / infoblox.tf
コードサンプル(クリックで表示/非表示)
#------------------------------------------------------------------------------
# DNS A-record Configuration
#------------------------------------------------------------------------------
locals {
records = { for v in module.demovm : "${v.prefix}.${var.domain}" => v.private_ip_address["nic1"] }
}
resource "infoblox_a_record" "a_rec" {
for_each = local.records
fqdn = each.key
ip_addr = each.value
}
4. BIG-IPをAzure VMにデプロイ
Terraform Workspace / main.tf (上述)
5. BIG-IPに各種設定を投入
Terraform Workspace / f5-bigip.tf
BIG-IP 仮想アプライアンスの仕様で、Azure VM上に仮想マシンとして構築されてからも数分間は内部処理のため管理アクセスが不可能になります。それを待つための処理も入っています。
BIG-IPの設定処理をモジュール化して使い回せるようにしています。
コードサンプル(クリックで表示/非表示)
# Wait until the BIG-IP instance is ready to operate after boot up. Sleep count starts at same time as start deployment.
# Since BIG-IP instance will take 3 - 5 minutes to be operational status after the status of virtual machine is Running.
# This time might be changed depends on environment.
resource "time_sleep" "sleep" {
create_duration = "6m"
triggers = {
mgmt_address = module.f5.private_ip_address["nic1"]
}
}
module "bigip_virtual_server" {
source = "./modules/bigip_ltm_virtual_server"
prefix = "fakeapp"
mgmt_address = time_sleep.sleep.triggers["mgmt_address"]
mgmt_port = "8443"
admin_username = var.admin_username
admin_password = var.admin_password
infoblox_server = var.infoblox_server
domain = var.domain
pool_members = { for v in module.demovm : v.prefix => "${v.prefix}.${var.domain}:10443" }
virtualserver_destination = time_sleep.sleep.triggers["mgmt_address"]
virtualserver_port = "443"
public_sslcert = var.public_sslcert
public_sslcert_privatekey = var.public_sslcert_privatekey
}
Terraform Workspace / modules / bigip_ltm_virtual_server / main.tf
コードサンプル(クリックで表示/非表示)
#------------------------------------------------------------------------------
# System Configuration
#------------------------------------------------------------------------------
resource "bigip_sys_dns" "dns" {
description = "DNS Servers, Infoblox and Azure built-in dns server."
name_servers = [var.infoblox_server, "168.63.129.16"]
search = [var.domain]
}
#------------------------------------------------------------------------------
# Certificate Management
#------------------------------------------------------------------------------
resource "bigip_ssl_certificate" "sslcert" {
name = "${var.prefix}_servercertkey"
content = var.public_sslcert
partition = var.partition
}
resource "bigip_ssl_key" "sslkey" {
name = bigip_ssl_certificate.sslcert.name
content = var.public_sslcert_privatekey
partition = bigip_ssl_certificate.sslcert.partition
}
#------------------------------------------------------------------------------
# Local Traffic > Virtual Servers
#------------------------------------------------------------------------------
resource "bigip_ltm_profile_client_ssl" "ClientSsl" {
name = "/${bigip_ssl_certificate.sslcert.partition}/${var.prefix}_ClientSsl"
cert = "/${bigip_ssl_certificate.sslcert.partition}/${bigip_ssl_certificate.sslcert.name}"
key = "/${bigip_ssl_certificate.sslcert.partition}/${bigip_ssl_certificate.sslcert.name}"
# Added to allow destroy to work correctly.
depends_on = [bigip_ssl_certificate.sslcert, bigip_ssl_key.sslkey]
}
resource "bigip_ltm_pool" "pool" {
name = "/${bigip_ssl_certificate.sslcert.partition}/${var.prefix}_pool"
load_balancing_mode = "round-robin"
monitors = ["/${bigip_ssl_certificate.sslcert.partition}/https"]
}
resource "bigip_ltm_pool_attachment" "attach_nodes" {
for_each = var.pool_members
pool = bigip_ltm_pool.pool.name
node = each.value
# Added to allow destroy to work correctly.
depends_on = [bigip_ltm_pool.pool]
}
resource "bigip_ltm_virtual_server" "virtualserver" {
name = "/${bigip_ssl_certificate.sslcert.partition}/${var.prefix}_virtualserver"
description = "VirtualServer for ${var.prefix}"
destination = var.virtualserver_destination
port = var.virtualserver_port
pool = bigip_ltm_pool.pool.name
client_profiles = [bigip_ltm_profile_client_ssl.ClientSsl.name]
server_profiles = ["/${bigip_ssl_certificate.sslcert.partition}/serverssl"]
source_address_translation = "automap"
# Added to allow destroy to work correctly.
depends_on = [bigip_ltm_profile_client_ssl.ClientSsl, bigip_ltm_pool.pool]
}
resource "bigip_command" "command" {
commands = ["save sys config"]
depends_on = [bigip_ltm_virtual_server.virtualserver]
}
Terraform Workspace / modules / bigip_ltm_virtual_server / outputs.tf
※アウトプットが無いため空ファイルになります。作成しなくても動作上問題ありませんが、必要になった場合のために残してあります。
Terraform Workspace / modules / bigip_ltm_virtual_server / variables.tf
コードサンプル(クリックで表示/非表示)
variable "prefix" {
type = string
description = "(Optional) Prefix to assign all of resouces in this module."
default = "bigip"
}
variable "mgmt_address" {
type = string
description = "(Required) IP Address of the BIG-IP."
}
variable "mgmt_port" {
type = string
description = "(Optional) TCP port number for the BIG-IP."
default = "443"
}
variable "admin_username" {
type = string
description = "(Required) Admin username of the BIG-IP."
}
variable "admin_password" {
type = string
description = "(Required) Admin password for the BIG-IP."
}
variable "partition" {
type = string
description = "(Option) Partition that will be used for the configuration"
default = "Common"
}
variable "pool_members" {
type = map(string)
description = <<DESC
(Required) Set of address and tcp port to add to the pool. Key is name of the node, value contains address and port of the node. Example:
{
"host0" = "192.168.0.10:80"
"host1" = "host1.domain.local:443"
}
DESC
}
variable "virtualserver_destination" {
type = string
description = "(Required) Destination IP for the client connecting to the BIG-IP. If single arm configuretion, it would be same as mgmt_address."
}
variable "virtualserver_port" {
type = string
description = "(Optional) Destination port for the client connecting to the BIG-IP. If single arm configuretion, it would be same as mgmt_address."
default = "443"
}
variable "public_sslcert" {
type = string
description = "Frontend Fullchain S/非表示SL Certificate for the BIG-IP issued by public certificate autholity. Should be defined on variables store such as Terraform Cloud or Vault."
}
variable "public_sslcert_privatekey" {
type = string
description = "Frontend SSL Private Key for the BIG-IP issued by public certificate autholity. Should be defined on variables store such as Terraform Cloud or Vault."
sensitive = true
}
variable "infoblox_server" {
type = string
description = "(Required) IP address for the Infoblox vNIOS Server."
}
variable "domain" {
type = string
description = "(Required) Domain name of the network."
default = "domain.local"
}
Terraform Workspace / modules / bigip_ltm_virtual_server / versions.tf
コードサンプル(クリックで表示/非表示)
terraform {
required_version = ">= 1.2.0"
required_providers {
bigip = {
source = "f5networks/bigip"
version = "~> 1.15"
}
}
}
ルートモジュールのその他ファイル
Terraform Workspace / outputs.tf
コードサンプル(クリックで表示/非表示)
output "bigip_public_ip" {
value = module.f5.public_ip_address
}
output "bigip_fqdn" {
value = module.f5.fqdn
}
Terraform Workspace / providers.tf
コードサンプル(クリックで表示/非表示)
provider "azurerm" {
subscription_id = var.ARM_SUBSCRIPTION_ID
client_id = var.ARM_CLIENT_ID
client_secret = var.ARM_CLIENT_SECRET
tenant_id = var.ARM_TENANT_ID
features {}
}
provider "vault" {
address = var.vault_url
ca_cert_file = var.ca_cert_file
}
provider "bigip" {
address = time_sleep.sleep.triggers["mgmt_address"]
port = var.bigip_mgmt_port
username = var.admin_username
password = var.admin_password
validate_certs_disable = true
}
provider "infoblox" {
server = var.infoblox_server
username = var.admin_username
password = var.admin_password
}
Terraform Workspace / terraform.tfvars
コードサンプル(クリックで表示/非表示)
demovm_count = 3
### Azure Configurations ###
ssh_pub_key = "ssh-rsa AAAAB3Nz..."
resource_group = "your-azure-resource-group"
virtual_network = "HC-vnet"
subnet = "develop"
### Vault Configurations ###
vault_url = "https://192.168.0.4:8200"
ca_cert_file = "./certificates/ca.pub"
vault_approle_name = "ted-fakeservice"
vault_approle_path = "fakeservice-role"
vault_policy_name = "fakeservice"
cert_ttl = "5m"
### F5 BIG-IP Configurations ###
bigip_mgmt_port = "8443"
### Infoblox vNIOS Configurations ###
infoblox_server = "192.168.0.7"
domain = "poc.domain.local"
Terraform Workspace / variables.tf
コードサンプル(クリックで表示/非表示)
variable "demovm_count" {
type = number
description = "Number of demo web application virtual machines that will be deployed."
default = 1
}
# Defined on Terraform Cloud
# Azure Service Principal
variable "ARM_SUBSCRIPTION_ID" {}
variable "ARM_CLIENT_ID" {}
variable "ARM_CLIENT_SECRET" {}
variable "ARM_TENANT_ID" {}
variable "resource_group" {
type = string
description = "Name of resource group existing on Azure"
}
variable "virtual_network" {
type = string
description = "Name of virtual network existing on Azure"
}
variable "subnet" {
type = string
description = "Name of subnet existing in the virtual network on Azure"
}
variable "ssh_pub_key" {
type = string
description = "Public key to associate with the instance"
}
variable "tags" {
type = map(string)
description = "Tag that will be associated with the resources that will be created as a part of the Terraform run."
default = {
Cost_Type = "hashicorp"
}
}
# ------------------------------
# Azure Virtual Machine Configuration
# ------------------------------
variable "admin_username" {
type = string
description = "Admin username for instances."
default = "azureuser"
}
variable "admin_password" {
type = string
description = "(Required) Admin user password for instances."
sensitive = true
}
variable "disable_password_authentication" {
type = bool
description = "Disable password authentication to log into instance."
default = true
}
# ------------------------------
# Vault Agent Configuration
# ------------------------------
variable "namespace" {
description = "Namespace that will be used for Vault authentication"
type = string
default = null
}
variable "vault_url" {
type = string
description = "(Required) URL for the Vault Server. This is used for all certificate issuing and CRLs. Eg. https://vault.bmrf.io:8200 or http://vault.bmrf.io:8200 or https://vault.bmrf.io (if using termination)"
}
variable "ca_cert_file" {
type = string
description = "(Required) CA Certification file"
}
variable "vault_approle_path" {
type = string
description = "(Required) - Path of the approle for Vault authentication"
}
variable "vault_approle_name" {
type = string
description = "(Required) - Name of the approle for Vault authentication"
}
variable "vault_policy_name" {
type = string
description = "(Required) - Name of the policy of the approle for Vault authentication"
}
variable "cert_ttl" {
type = string
description = "(Required) - TTL of the certification dynamically created by Vault"
default = "7d"
}
# ------------------------------
# F5 BIG-IP Configuration
# ------------------------------
variable "bigip_mgmt_port" {
type = string
description = "(Optional) TCP port number for the BIG-IP."
default = "443"
}
variable "public_sslcert" {
type = string
description = "Frontend Fullchain SSL Certificate for the BIG-IP issued by public certificate autholity. Should be defined on variables store such as Terraform Cloud or Vault."
}
variable "public_sslcert_privatekey" {
type = string
description = "Frontend SSL Private Key for the BIG-IP issued by public certificate autholity. Should be defined on variables store such as Terraform Cloud or Vault."
sensitive = true
}
# ------------------------------
# Infoblox vNIOS Configuration
# ------------------------------
variable "infoblox_server" {
type = string
description = "(Required) IP address for the Infoblox vNIOS Server."
}
variable "domain" {
type = string
description = "(Required) Domain name of the network."
default = "domain.local"
}
Terraform Workspace / versions.tf
コードサンプル(クリックで表示/非表示)
# Terraform Configurations
terraform {
required_version = ">= 1.2.0"
cloud {
organization = "your-organization"
workspaces {
name = "your-workspace"
}
}
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">=2.0"
}
vault = {
source = "hashicorp/vault"
version = ">=3.8.2"
}
bigip = {
source = "f5networks/bigip"
version = "~> 1.15"
}
infoblox = {
source = "infobloxopen/infoblox"
version = "~> 2.4.0"
}
}
}
おわりに
以上で全通信経路が暗号化されたWebアプリの自動デプロイをユースケースとした記事連載が終了となります。
今回のユースケースの作成と実際のデモ環境構築にはHashiCorp社の米国エンジニアにも協力してもらい作成したものです。この場を借りて御礼申し上げます。
東京エレクトロンデバイスでは今後もHashiCorp社やその他サービスプロバイダー・メーカーの方々と協力し、ITシステム運用のDXや自動化・効率化を支援する様々なユースケースとその実現方法まで含めたソリューション構築を進めてまいります。
次のユースケースとソリューションをお楽しみに!