てくなべ (tekunabe)

ansible / network automation / 学習メモ

[Terraform/ACI] Terraform の Cisco ACI Provider で APIC を設定する

はじめに

本ブログでは、これまでいくつかのAnsible と ACI 対応についての記事を書いてきましたが、 Terraform も ACI に対応していることを最近知りました。

少しだけ試してみましたのでまとめます。

環境

  • Cisco DevNet Sandbox APIC 4.1(1k)
  • Terraform v0.12.19


■ Terraform の ACI 対応

Terraform の公式ドキュメントに詳細が記載されています。カテゴリとしては、Network ではなく、Cloudという扱いのようです。

https://www.terraform.io/docs/providers/aci/index.htmlwww.terraform.io

Tenant、Bridge Domain、Subnet、Contract などのオブジェクトを扱う Data Resources や Resources がそれぞれ 40以上用意されています。

aci_any Data Source や、aci_anyaci_rest Resource もあるので、専用のものがなくても、融通が効きそうです。


■ おためし

今回は、以下のような Tenant、Bridge Domain、VRF、Subnet を作ります。

f:id:akira6592:20200114152053p:plain
各オブジェクトの作成

tf ファイルの作成

main.tfと、bd.tf の 2つのファイルを作成します。(雰囲気で分けています)

main.tf の作成

provider の定義をします。

provider "aci" {
  username = "admin"
  password = "xxxdummyxxx"
  url      = "https://sandboxapicdc.cisco.com/"
  insecure = true
}

以前の記事でも書いたとおりAPIC の認証方式には、パスワードベース認証方式と、署名ベース認証方式の2種類あります。今回は手軽さ重視でパスワードベース認証方式を利用するように、privider を定義しています。

HTTPS 接続時の証明書の検証を無効化する場合は、insecuretrue に指定します。デフォルトも true です。

その他、パラメータの説明はCisco ACI Provider の公式ドキュメントを参照してください。

bd.tf の作成

Tenant、Bridge Domain、VRF、Subnet の Resource を定義をします。

resource "aci_tenant" "test_tenant1" {
  name        = "test_tenant1"
  description = "test tenant"
}

resource "aci_vrf" "test_vrf1" {
  tenant_dn   = aci_tenant.test_tenant1.id
  name        = "test_vrf1"
  description = "test vrf"
}

resource "aci_bridge_domain" "test_bd1" {
  tenant_dn          = aci_tenant.test_tenant1.id
  name               = "test_bd1"
  description        = "test bridge domain"
  relation_fv_rs_ctx = aci_vrf.test_vrf1.name
}

resource "aci_subnet" "test_subnet1" {
  bridge_domain_dn = aci_bridge_domain.test_bd1.id
  description      = "test_subnet1"
  ip               = "10.0.3.28/27"
} 

補足

少し調べるのにつまずいたのが、Bridge Domain から VRF への関連付けの方法です。以下の部分です。

  relation_fv_rs_ctx  = aci_vrf.test_vrf1.name

aci_bridge_domain を参照しても vrf という文字が見当たらないので、対応するパラメーターは無いのかもと思っていました。 ですが、ACI 用語としての VRF は Context とも呼ばれていて、クラス名?が fvCtx であることを思いだして、relation_fv_rs_ctx で設定を試したした。

なお、よく見かけるサンプルでは、id などの参照を "${}" で囲って

  tenant_dn = "${aci_tenant.test_tenant1.id}"

のようにしていますが、

Warning: Interpolation-only expressions are deprecated

という警告が表示されるので修正しました。

初回実行

terraform init コマンドの実行

terraform init コマンドで初期化します。

実行ログ(クリックして広げる)

$ terraform init

Initializing the backend...

Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "aci" (terraform-providers/aci) 0.1.4...

The following providers do not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* provider.aci: version = "~> 0.1"

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
$

terraform plan コマンドの実行

terraform plan コマンドで確認します。

実行ログ(クリックして広げる)

$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aci_bridge_domain.test_bd1 will be created
  + resource "aci_bridge_domain" "test_bd1" {
      + annotation                  = (known after apply)
      + arp_flood                   = (known after apply)
      + bridge_domain_type          = (known after apply)
      + description                 = "test bridge domain"
      + ep_clear                    = (known after apply)
      + ep_move_detect_mode         = (known after apply)
      + host_based_routing          = (known after apply)
      + id                          = (known after apply)
      + intersite_bum_traffic_allow = (known after apply)
      + intersite_l2_stretch        = (known after apply)
      + ip_learning                 = (known after apply)
      + ipv6_mcast_allow            = (known after apply)
      + limit_ip_learn_to_subnets   = (known after apply)
      + ll_addr                     = (known after apply)
      + mac                         = (known after apply)
      + mcast_allow                 = (known after apply)
      + multi_dst_pkt_act           = (known after apply)
      + name                        = "test_bd1"
      + name_alias                  = (known after apply)
      + optimize_wan_bandwidth      = (known after apply)
      + relation_fv_rs_ctx          = "test_vrf1"
      + tenant_dn                   = (known after apply)
      + unicast_route               = (known after apply)
      + unk_mac_ucast_act           = (known after apply)
      + unk_mcast_act               = (known after apply)
      + v6unk_mcast_act             = (known after apply)
      + vmac                        = (known after apply)
    }

  # aci_subnet.test_subnet1 will be created
  + resource "aci_subnet" "test_subnet1" {
      + annotation       = (known after apply)
      + bridge_domain_dn = (known after apply)
      + ctrl             = (known after apply)
      + description      = "test_subnet1"
      + id               = (known after apply)
      + ip               = "10.0.3.28/27"
      + name_alias       = (known after apply)
      + preferred        = (known after apply)
      + scope            = (known after apply)
      + virtual          = (known after apply)
    }

  # aci_tenant.test_tenant1 will be created
  + resource "aci_tenant" "test_tenant1" {
      + annotation  = (known after apply)
      + description = "test tenant"
      + id          = (known after apply)
      + name        = "test_tenant1"
      + name_alias  = (known after apply)
    }

  # aci_vrf.test_vrf1 will be created
  + resource "aci_vrf" "test_vrf1" {
      + annotation             = (known after apply)
      + bd_enforced_enable     = (known after apply)
      + description            = "test vrf"
      + id                     = (known after apply)
      + ip_data_plane_learning = (known after apply)
      + knw_mcast_act          = (known after apply)
      + name                   = "test_vrf1"
      + name_alias             = (known after apply)
      + pc_enf_dir             = (known after apply)
      + pc_enf_pref            = (known after apply)
      + tenant_dn              = (known after apply)
    }

Plan: 4 to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

$ 

もろもろオブジェクトが作成されることが確認できます。plan なのでまだ実際に作成はされれいません。

terraform apply コマンドの実行

いよいよ terraform apply コマンドで、変更を適用します。

$ terraform apply

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aci_bridge_domain.test_bd1 will be created
  + resource "aci_bridge_domain" "test_bd1" {
      + annotation                  = (known after apply)
      + arp_flood                   = (known after apply)
      + bridge_domain_type          = (known after apply)
      + description                 = "test bridge domain"
      + ep_clear                    = (known after apply)
      + ep_move_detect_mode         = (known after apply)
      + host_based_routing          = (known after apply)
      + id                          = (known after apply)
      + intersite_bum_traffic_allow = (known after apply)
      + intersite_l2_stretch        = (known after apply)
      + ip_learning                 = (known after apply)
      + ipv6_mcast_allow            = (known after apply)
      + limit_ip_learn_to_subnets   = (known after apply)
      + ll_addr                     = (known after apply)
      + mac                         = (known after apply)
      + mcast_allow                 = (known after apply)
      + multi_dst_pkt_act           = (known after apply)
      + name                        = "test_bd1"
      + name_alias                  = (known after apply)
      + optimize_wan_bandwidth      = (known after apply)
      + relation_fv_rs_ctx          = "test_vrf1"
      + tenant_dn                   = (known after apply)
      + unicast_route               = (known after apply)
      + unk_mac_ucast_act           = (known after apply)
      + unk_mcast_act               = (known after apply)
      + v6unk_mcast_act             = (known after apply)
      + vmac                        = (known after apply)
    }

  # aci_subnet.test_subnet1 will be created
  + resource "aci_subnet" "test_subnet1" {
      + annotation       = (known after apply)
      + bridge_domain_dn = (known after apply)
      + ctrl             = (known after apply)
      + description      = "test_subnet1"
      + id               = (known after apply)
      + ip               = "10.0.3.28/27"
      + name_alias       = (known after apply)
      + preferred        = (known after apply)
      + scope            = (known after apply)
      + virtual          = (known after apply)
    }

  # aci_tenant.test_tenant1 will be created
  + resource "aci_tenant" "test_tenant1" {
      + annotation  = (known after apply)
      + description = "test tenant"
      + id          = (known after apply)
      + name        = "test_tenant1"
      + name_alias  = (known after apply)
    }

  # aci_vrf.test_vrf1 will be created
  + resource "aci_vrf" "test_vrf1" {
      + annotation             = (known after apply)
      + bd_enforced_enable     = (known after apply)
      + description            = "test vrf"
      + id                     = (known after apply)
      + ip_data_plane_learning = (known after apply)
      + knw_mcast_act          = (known after apply)
      + name                   = "test_vrf1"
      + name_alias             = (known after apply)
      + pc_enf_dir             = (known after apply)
      + pc_enf_pref            = (known after apply)
      + tenant_dn              = (known after apply)
    }

Plan: 4 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aci_tenant.test_tenant1: Creating...
aci_tenant.test_tenant1: Creation complete after 3s [id=uni/tn-test_tenant1]
aci_vrf.test_vrf1: Creating...
aci_vrf.test_vrf1: Still creating... [10s elapsed]
aci_vrf.test_vrf1: Creation complete after 18s [id=uni/tn-test_tenant1/ctx-test_vrf1]
aci_bridge_domain.test_bd1: Creating...
aci_bridge_domain.test_bd1: Creation complete after 5s [id=uni/tn-test_tenant1/BD-test_bd1]
aci_subnet.test_subnet1: Creating...
aci_subnet.test_subnet1: Creation complete after 2s [id=uni/tn-test_tenant1/BD-test_bd1/subnet-[10.0.3.28/27]]

Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
$ 

無事に Tenant、Bridge Domain、VRF、Subnet が作成されました。

APIC 画面でも確認します。

f:id:akira6592:20200114150045p:plain
一通り作成された


■ 宣言的であることの確認

宣言的と手続き的

ここまでで、一通りのオブジェクトが作成されました。 Terraform の特徴を掴むために、tf ファイルを少し修正して動作を確認します。

具体的に確認したい点は、Terraform は宣言的であるという点です。tf ファイルでは、あるべき状態を宣言的に定義します。そのため、定義を削除して apply すると、差分を検出して削除処理が実行されます。

一方、Ansible の Playook は全体としては手続き型です。そのため、タスク定義を削除した場合、そのタスクは実行されないというだけで、実態は残ったままになります。

確認1: Subnet 定義の削除

db.tf の修正

まず、db.tf から、Subnet の定義を削除します。

f:id:akira6592:20200114152138p:plain
Subnet の定義は削除

resource "aci_tenant" "test_tenant1" {
  name        = "test_tenant1"
  description = "test tenant"
}

resource "aci_vrf" "test_vrf1" {
  tenant_dn   = aci_tenant.test_tenant1.id
  name        = "test_vrf1"
  description = "test vrf"
}

resource "aci_bridge_domain" "test_bd1" {
  tenant_dn          = aci_tenant.test_tenant1.id
  name               = "test_bd1"
  description        = "test bridge domain"
  relation_fv_rs_ctx = aci_vrf.test_vrf1.name
}

# Subnet の定義は削除

実行

続いて、terraform planterraform apply します。

terraform plan 実行ログ(クリックして広げる)

$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

aci_subnet.test_subnet1: Refreshing state... [id=uni/tn-test_tenant1/BD-test_bd1/subnet-[10.0.3.28/27]]
aci_tenant.test_tenant1: Refreshing state... [id=uni/tn-test_tenant1]
aci_vrf.test_vrf1: Refreshing state... [id=uni/tn-test_tenant1/ctx-test_vrf1]
aci_bridge_domain.test_bd1: Refreshing state... [id=uni/tn-test_tenant1/BD-test_bd1]

------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # aci_subnet.test_subnet1 will be destroyed
  - resource "aci_subnet" "test_subnet1" {
      - bridge_domain_dn = "uni/tn-test_tenant1/BD-test_bd1" -> null
      - ctrl             = "nd" -> null
      - description      = "test_subnet1" -> null
      - id               = "uni/tn-test_tenant1/BD-test_bd1/subnet-[10.0.3.28/27]" -> null
      - ip               = "10.0.3.28/27" -> null
      - preferred        = "no" -> null
      - scope            = "private" -> null
      - virtual          = "no" -> null
    }

Plan: 0 to add, 0 to change, 1 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

terraform apply 実行ログ(クリックして広げる)

$ terraform apply
aci_subnet.test_subnet1: Refreshing state... [id=uni/tn-test_tenant1/BD-test_bd1/subnet-[10.0.3.28/27]]
aci_tenant.test_tenant1: Refreshing state... [id=uni/tn-test_tenant1]
aci_vrf.test_vrf1: Refreshing state... [id=uni/tn-test_tenant1/ctx-test_vrf1]
aci_bridge_domain.test_bd1: Refreshing state... [id=uni/tn-test_tenant1/BD-test_bd1]

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # aci_subnet.test_subnet1 will be destroyed
  - resource "aci_subnet" "test_subnet1" {
      - bridge_domain_dn = "uni/tn-test_tenant1/BD-test_bd1" -> null
      - ctrl             = "nd" -> null
      - description      = "test_subnet1" -> null
      - id               = "uni/tn-test_tenant1/BD-test_bd1/subnet-[10.0.3.28/27]" -> null
      - ip               = "10.0.3.28/27" -> null
      - preferred        = "no" -> null
      - scope            = "private" -> null
      - virtual          = "no" -> null
    }

Plan: 0 to add, 0 to change, 1 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aci_subnet.test_subnet1: Destroying... [id=uni/tn-test_tenant1/BD-test_bd1/subnet-[10.0.3.28/27]]
aci_subnet.test_subnet1: Destruction complete after 1s

Apply complete! Resources: 0 added, 0 changed, 1 destroyed.
$

確認

APIC の画面で確認します。

f:id:akira6592:20200114150615p:plain:w400
Subnet が削除された
Subnet の定義が削除され、宣言的な動作を確認できました。

Ansible の Playbook の場合は、Subnet 定義のタスクを削除しても、Subnet 定義は残ったままになるはずです。

確認2: Tenant 定義ごとの削除

db.tf の修正

今度は、bd.tf を空にして、Tenant の定義ごと削除します。

f:id:akira6592:20200114152217p:plain
Tenant 定義ごと削除

実行

続いて、terraform planterraform apply コマンドを実行します。

terraform plan 実行ログ(クリックして広げる)

$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

aci_tenant.test_tenant1: Refreshing state... [id=uni/tn-test_tenant1]
aci_vrf.test_vrf1: Refreshing state... [id=uni/tn-test_tenant1/ctx-test_vrf1]
aci_bridge_domain.test_bd1: Refreshing state... [id=uni/tn-test_tenant1/BD-test_bd1]

------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # aci_bridge_domain.test_bd1 will be destroyed
  - resource "aci_bridge_domain" "test_bd1" {
      - arp_flood                   = "no" -> null
      - bridge_domain_type          = "regular" -> null
      - description                 = "test bridge domain" -> null
      - ep_clear                    = "no" -> null
      - host_based_routing          = "no" -> null
      - id                          = "uni/tn-test_tenant1/BD-test_bd1" -> null
      - intersite_bum_traffic_allow = "no" -> null
      - intersite_l2_stretch        = "no" -> null
      - ip_learning                 = "yes" -> null
      - limit_ip_learn_to_subnets   = "yes" -> null
      - ll_addr                     = "::" -> null
      - mac                         = "00:22:BD:F8:19:FF" -> null
      - mcast_allow                 = "no" -> null
      - multi_dst_pkt_act           = "bd-flood" -> null
      - name                        = "test_bd1" -> null
      - optimize_wan_bandwidth      = "no" -> null
      - relation_fv_rs_ctx          = "test_vrf1" -> null
      - tenant_dn                   = "uni/tn-test_tenant1" -> null
      - unicast_route               = "yes" -> null
      - unk_mac_ucast_act           = "proxy" -> null
      - unk_mcast_act               = "flood" -> null
      - v6unk_mcast_act             = "flood" -> null
      - vmac                        = "not-applicable" -> null
    }

  # aci_tenant.test_tenant1 will be destroyed
  - resource "aci_tenant" "test_tenant1" {
      - description = "test tenant" -> null
      - id          = "uni/tn-test_tenant1" -> null
      - name        = "test_tenant1" -> null
    }

  # aci_vrf.test_vrf1 will be destroyed
  - resource "aci_vrf" "test_vrf1" {
      - bd_enforced_enable     = "no" -> null
      - description            = "test vrf" -> null
      - id                     = "uni/tn-test_tenant1/ctx-test_vrf1" -> null
      - ip_data_plane_learning = "enabled" -> null
      - knw_mcast_act          = "permit" -> null
      - name                   = "test_vrf1" -> null
      - pc_enf_dir             = "ingress" -> null
      - pc_enf_pref            = "enforced" -> null
      - tenant_dn              = "uni/tn-test_tenant1" -> null
    }

Plan: 0 to add, 0 to change, 3 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

terraform apply 実行ログ(クリックして広げる)

$ terraform apply
aci_tenant.test_tenant1: Refreshing state... [id=uni/tn-test_tenant1]
aci_vrf.test_vrf1: Refreshing state... [id=uni/tn-test_tenant1/ctx-test_vrf1]
aci_bridge_domain.test_bd1: Refreshing state... [id=uni/tn-test_tenant1/BD-test_bd1]

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # aci_bridge_domain.test_bd1 will be destroyed
  - resource "aci_bridge_domain" "test_bd1" {
      - arp_flood                   = "no" -> null
      - bridge_domain_type          = "regular" -> null
      - description                 = "test bridge domain" -> null
      - ep_clear                    = "no" -> null
      - host_based_routing          = "no" -> null
      - id                          = "uni/tn-test_tenant1/BD-test_bd1" -> null
      - intersite_bum_traffic_allow = "no" -> null
      - intersite_l2_stretch        = "no" -> null
      - ip_learning                 = "yes" -> null
      - limit_ip_learn_to_subnets   = "yes" -> null
      - ll_addr                     = "::" -> null
      - mac                         = "00:22:BD:F8:19:FF" -> null
      - mcast_allow                 = "no" -> null
      - multi_dst_pkt_act           = "bd-flood" -> null
      - name                        = "test_bd1" -> null
      - optimize_wan_bandwidth      = "no" -> null
      - relation_fv_rs_ctx          = "test_vrf1" -> null
      - tenant_dn                   = "uni/tn-test_tenant1" -> null
      - unicast_route               = "yes" -> null
      - unk_mac_ucast_act           = "proxy" -> null
      - unk_mcast_act               = "flood" -> null
      - v6unk_mcast_act             = "flood" -> null
      - vmac                        = "not-applicable" -> null
    }

  # aci_tenant.test_tenant1 will be destroyed
  - resource "aci_tenant" "test_tenant1" {
      - description = "test tenant" -> null
      - id          = "uni/tn-test_tenant1" -> null
      - name        = "test_tenant1" -> null
    }

  # aci_vrf.test_vrf1 will be destroyed
  - resource "aci_vrf" "test_vrf1" {
      - bd_enforced_enable     = "no" -> null
      - description            = "test vrf" -> null
      - id                     = "uni/tn-test_tenant1/ctx-test_vrf1" -> null
      - ip_data_plane_learning = "enabled" -> null
      - knw_mcast_act          = "permit" -> null
      - name                   = "test_vrf1" -> null
      - pc_enf_dir             = "ingress" -> null
      - pc_enf_pref            = "enforced" -> null
      - tenant_dn              = "uni/tn-test_tenant1" -> null
    }

Plan: 0 to add, 0 to change, 3 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

aci_bridge_domain.test_bd1: Destroying... [id=uni/tn-test_tenant1/BD-test_bd1]
aci_bridge_domain.test_bd1: Destruction complete after 2s
aci_vrf.test_vrf1: Destroying... [id=uni/tn-test_tenant1/ctx-test_vrf1]
aci_vrf.test_vrf1: Destruction complete after 0s
aci_tenant.test_tenant1: Destroying... [id=uni/tn-test_tenant1]
aci_tenant.test_tenant1: Destruction complete after 0s

Apply complete! Resources: 0 added, 0 changed, 3 destroyed. 

確認

APIC の画面で確認します。

f:id:akira6592:20200114151027p:plain
test_tenant1 ごと削除された

test_tenant1 ごと削除され、宣言的な動作を確認できました。 さすがに、他の Tenant は今回の tf ファイルの管理外のため残ったままです。


おわりに

Terraform の Cisco ACI Provider を使って、簡単な設定追加、削除をためしてみました。

Ansible と比較すると、やはり宣言的である点が特徴に感じました。

Terraform は Windows からでも実行できるため、手元の業務 windows 端末から実行できる点も特徴ではないかと思います。

参考

[2021/01/23 追記] - Introduction to Terraform with Cisco ACI, Part 1 - Cisco Blogs

余談

この記事を書くときに、はてなブログMarkdownシンタックスハイライトが tf に対応していることを知りました。

ソースコードを色付けして表示する(シンタックスハイライト) - はてなブログ ヘルプ