ユースケースクラウドセキュリティネットワーク

(連載) HashiCorp活用例6: 一連のWebアプリ用のマルチベンダーインフラ展開を効率化

東京エレクトロンデバイスではHashiCorp社製品とその他各社製品とのマルチベンダー連携を通じて、お客様のWebアプリやクラウド、ITインフラなどの展開から運用までを効率化できるソリューション開発に取り組んでいます。さまざまな活用例をご紹介することで、HashiCorp社製品の提供する価値や可能性を発信していきたいと思っております。

はじめに

本連載最後の6記事目では、これまでの連載でご紹介してきた個々のパーツを組み合わせて構築したデモ環境のコードをご紹介します。
連載初回の記事で取り上げたユースケースに基づいて記載しておりますので、ユースケースの詳細につきましては過去の記事も併せてご参照ください。

連載記事一覧:

  1. Webアプリ用マルチベンダーインフラの自動構築
  2. 動的なバックエンドSSL証明書の管理
  3. 動的なDNSレコード管理
  4. 動的なロードバランサの構築
  5. プライベートネットワークでのIaC
  6. 一連のWebアプリ用のマルチベンダーインフラ展開を効率化                            ←本記事

デモシナリオの構成

下図はデモシナリオ開始時の環境です。Terraform Cloudエージェント(以下 TFCエージェント)、Vaultサーバー、Infoblox仮想アプライアンスが動作しており、WebアプリとロードバランサであるBIG-IPはまだデプロイされていない状態です。

デモシナリオでは、一度のTerraform ApplyでWebアプリ用仮想マシン群とBIG-IPロードバランサをデプロイします。最終的に下図のように、クライアントへWebアプリが配信できる状態になるまでを自動化します。

デモシナリオのコードサンプル

デモシナリオを実現するために利用したTerraformコードやテンプレート等ファイルとディレクトリの構成、シナリオ実現のためのオペレーションフロー、実際のコードサンプルを紹介していきます。

コンテンツ一覧

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や自動化・効率化を支援する様々なユースケースとその実現方法まで含めたソリューション構築を進めてまいります。

次のユースケースとソリューションをお楽しみに!

この記事に関連する製品・サービス

この記事に関連する記事