My Home NW Lab

逸般の誤家庭のネットワーク

AWSにTerraformを用いてCatalyst 9800-CLをデプロイする

AWSCatalyst 9800-CLの検証環境を、必要なタイミングで即座に構築して、不要になったらコスト削減のために破棄したかったのでTerraformでデプロイする方法を書き留めます。

検証時の環境情報

筆者はAWS Cloud 9上のTerraformで検証を行いました。

ec2-user:~/environment $ terraform --version
Terraform v1.5.2
on linux_amd64

Your version of Terraform is out of date! The latest version
is 1.5.3. You can update by downloading from https://www.terraform.io/downloads.html
ec2-user:~/environment $ 

設計方針

構成の概要図

  • 検証用途であるためコスト削減を重視してシングル構成にしております。

  • 無線APからWLCであるCatalyst 9800-CLへのJoinはInternet経由を想定しております。

    無線APにはWLCのInternet側のPublic IP Addressを通知する必要があるため、デプロイ時のUser DataでPublic IP Addressの情報を渡しています。デプロイ後に手動設定する場合は、環境固有のPublic IP Addressの確認が必要になるため手間を省く意図です。

Terraformの設定

書き換えが必要な個所

  • SSH 公開鍵が public_key の値に指定されているので、使用者の公開鍵 (例: cat ~/.ssh/id_rsa.pub)に書き換えてください。
public_key = "ssh-rsa ABCDEF...."

環境に応じてチューニングが必要になる個所

  • Catalyst 9800-CLのVersionは v17.9.1 (C9800-CL-17-9-1.*)を明示的に指定しているため適宜書き換えてください。

  • 通信要件は使用する機能に合わせて精査してください。
    v17.9.1 の場合はRelease Noteの Network Protocols and Port Matrix から確認できます。

    www.cisco.com

ログイン情報

Username 認証方式 ログイン方式
ec2-user SSH公開鍵認証 SSH
admin パスワード認証 (ランダム文字列生成) SSH, Web UI

admin のパスワードはTerraformの実行時にランダムに生成する方針としています。 下記のように terraform output を実行するとパスワードの確認が可能です。

ec2-user:~/environment $ terraform output
output-admin-password = "<2]3SCqTN8zH9wN*pT"
output-wlc01-login-url = "https://***.***.***.***/"
output-wlc01-public-ip = "***.***.***.***"
ec2-user:~/environment $ 

設定ファイル全体

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
  default_tags {
    tags = {
      ENV = "TEST"
    }
  }
}

data "aws_region" "current" {}

resource "aws_key_pair" "keypair" {
  key_name = "keypair_for_terraform"
  # TODO: CHANGE KEYPAIR
  public_key = "ssh-rsa ABCDEF...."
}

resource "random_password" "login-password" {
  length  = 18
  special = true
  override_special = "!#$%&*()-_=+[]{}<>:" # Exclude question mark
}

data "aws_ami" "c9800-cl" {
  #most_recent = true

  owners = ["aws-marketplace"]

  filter {
    name = "name"
    #values = ["C9800-CL*"]
    values = ["C9800-CL-17-9-1.*"]
  }

  filter {
    name   = "product-code"
    values = ["38id0enmqvbjcm7sexk3394kx"]
  }

  filter {
    name   = "state"
    values = ["available"]
  }
}

resource "aws_vpc" "myvpc" {
  cidr_block           = "172.31.0.0/16"
  enable_dns_hostnames = true
  tags = {
    Name = "myvpc"
  }
}

resource "aws_subnet" "subnet-public-1a" {
  vpc_id                  = aws_vpc.myvpc.id
  cidr_block              = "172.31.0.0/24"
  availability_zone       = "ap-northeast-1a"
  map_public_ip_on_launch = true
  tags = {
    Name = "subnet-public-1a"
  }
}

resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.myvpc.id
}

resource "aws_route_table" "rt-public" {
  vpc_id = aws_vpc.myvpc.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }
  tags = {
    Name = "rt-public"
  }
}

resource "aws_route_table_association" "rtassoc-public-1a" {
  subnet_id      = aws_subnet.subnet-public-1a.id
  route_table_id = aws_route_table.rt-public.id
}

# Network Communication Requirements
#
# Release Notes for Cisco Catalyst 9800 Series Wireless Controller, Cisco IOS XE Cupertino 17.9.x - Cisco
# https://www.cisco.com/c/en/us/td/docs/wireless/controller/9800/17-9/release-notes/rn-17-9-9800.html#Cisco_Reference.dita_fd1a1a9f-282c-4d1e-8948-166cb824fb8f
# Refer to "Network Protocols and Port Matrix"

resource "aws_security_group" "secgrp-public" {
  name        = "secgrp-public"
  description = "For Public Subnet"
  vpc_id      = aws_vpc.myvpc.id
  ingress {
    description = "ICMP Echo Request"
    from_port   = 8
    to_port     = 0
    protocol    = "icmp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    description = "SSH"
    from_port   = 0
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  # Ingress (Inbound)
  ingress {
    description = "CAPWAP Control"
    from_port   = 0
    to_port     = 5246
    protocol    = "udp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    description = "CAPWAP Data"
    from_port   = 0
    to_port     = 5247
    protocol    = "udp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    description = "CAPWAP MCAST"
    from_port   = 0
    to_port     = 5248
    protocol    = "udp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    description = "Mobility Control"
    from_port   = 16666
    to_port     = 16666
    protocol    = "udp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  #ingress {
  #  description = "Telnet"
  #  from_port   = 0
  #  to_port     = 23
  #  protocol    = "tcp"
  #  cidr_blocks = ["0.0.0.0/0"]
  #}
  #ingress {
  #  description = "HTTP"
  #  from_port   = 0
  #  to_port     = 80
  #  protocol    = "tcp"
  #  cidr_blocks = ["0.0.0.0/0"]
  #}
  ingress {
    description = "HTTPS & REST API"
    from_port   = 0
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    description = "Client Policies (AP-AP)"
    from_port   = 0
    to_port     = 16670
    protocol    = "udp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    description = "SNMP Agent (SNMP Polling)"
    from_port   = 0
    to_port     = 161
    protocol    = "udp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    description = "TFTP"
    from_port   = 0
    to_port     = 69
    protocol    = "udp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    description = "Mobility"
    from_port   = 16667
    to_port     = 16667
    protocol    = "udp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  #ingress {
  #  description = "NetConf"
  #  from_port   = 0
  #  to_port     = 830
  #  protocol    = "tcp"
  #  cidr_blocks = ["0.0.0.0/0"]
  #}
  ingress {
    description = "Device Discovery"
    from_port   = 0
    to_port     = 32222
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  # Egrerss (Outbound)
  #egress {
  #  description = "Mobility Control"
  #  from_port   = 16666
  #  to_port     = 16666
  #  protocol    = "udp"
  #  cidr_blocks = ["0.0.0.0/0"]
  #}
  #egress {
  #  description = "SNMP Trap"
  #  from_port   = 0
  #  to_port     = 162
  #  protocol    = "udp"
  #  cidr_blocks = ["0.0.0.0/0"]
  #}
  #egress {
  #  description = "RADIUS Authentication"
  #  from_port   = 0
  #  to_port     = 1812
  #  protocol    = "udp"
  #  cidr_blocks = ["0.0.0.0/0"]
  #}
  #egress {
  #  description = "RADIUS Accounting"
  #  from_port   = 0
  #  to_port     = 1813
  #  protocol    = "udp"
  #  cidr_blocks = ["0.0.0.0/0"]
  #}
  #egress {
  #  description = "TACACS+"
  #  from_port   = 0
  #  to_port     = 49
  #  protocol    = "tcp"
  #  cidr_blocks = ["0.0.0.0/0"]
  #}
  #egress {
  #  description = "Mobility"
  #  from_port   = 16667
  #  to_port     = 16667
  #  protocol    = "udp"
  #  cidr_blocks = ["0.0.0.0/0"]
  #}
  #egress {
  #  description = "NTP Server"
  #  from_port   = 0
  #  to_port     = 123
  #  protocol    = "udp"
  #  cidr_blocks = ["0.0.0.0/0"]
  #}
  #egress {
  #  description = "Syslog"
  #  from_port   = 0
  #  to_port     = 514
  #  protocol    = "udp"
  #  cidr_blocks = ["0.0.0.0/0"]
  #}
  #egress {
  #  description = "NetFlow"
  #  from_port   = 0
  #  to_port     = 9996
  #  protocol    = "udp"
  #  cidr_blocks = ["0.0.0.0/0"]
  #}
  #egress {
  #  description = "NMSP / Cisco Connected Mobile Experiences (CMX)"
  #  from_port   = 0
  #  to_port     = 16113
  #  protocol    = "udp"
  #  cidr_blocks = ["0.0.0.0/0"]
  #}
  egress {
    description = "Any"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = {
    Name = "secgrp-public"
  }
}

resource "aws_network_interface" "eni-wlc01-gi1" {
  subnet_id         = aws_subnet.subnet-public-1a.id
  security_groups   = [aws_security_group.secgrp-public.id]
  source_dest_check = false
  tags = {
    Name = "eni-wlc01-gi1"
  }
}

resource "aws_eip" "eip-wlc01" {
  vpc               = true
  network_interface = aws_network_interface.eni-wlc01-gi1.id
  depends_on        = [aws_internet_gateway.igw]
  tags = {
    Name = "eip-wlc01"
  }
}

resource "aws_instance" "wlc01" {
  ami = data.aws_ami.c9800-cl.image_id
  # Available Instance Types: c5.xlarge or c5.2xlarge or c5.4xlarge
  instance_type = "c5.xlarge"
  key_name      = aws_key_pair.keypair.id
  network_interface {
    network_interface_id = aws_network_interface.eni-wlc01-gi1.id
    device_index         = 0
  }
  user_data = <<EOF
ios-config-1="username admin privilege 15 password 0 ${random_password.login-password.result}"
ios-config-2="hostname wlc01"
ios-config-3="wireless management interface GigabitEthernet1"
ios-config-4=" public-ip ${aws_eip.eip-wlc01.public_ip}"
EOF
  tags = {
    Name = "wlc01"
  }
}

output "output-wlc01-public-ip" {
  description = "Public IP Address for wlc01"
  value       = aws_eip.eip-wlc01.public_ip
}

output "output-wlc01-login-url" {
  description = "Login URL for wlc01"
  value       = "https://${aws_eip.eip-wlc01.public_ip}/"
}

output "output-admin-password" {
  description = "admin user's password"
  value       = nonsensitive(random_password.login-password.result)
}

デプロイ方法

  • 設定ファイルを main.tf として保存します。

  • Terraformの実行環境を準備 (初期化)します。

terraform init
  • 構成が意図したものであり、エラーが出ないかを事前に確認します。
terraform plan
  • 実際にデプロイします。
terraform apply
  • ログイン情報が分からなくなったら output の情報を出力します。
terraform output
  • Web UIからログインすると Configuration Setup Wizard の状態になるため、検証に必要となる設定を続けてください。

    Configuration Setup Wizard

  • SSHでログインする際は ec2-user によるSSH公開鍵認証も可能です。

  • 2023年07月頃のAWSCatalyst 9800-CLでは、AWS対応Versionの全てがデプロイ時に選択できるわけではありません。そのため、APがWLCにJoinするにはUpgradeが必要になる可能性があります。

リソースの後始末 (検証環境の破棄)

  • 検証を終えたら不要なリソースは削除します。
terraform destroy
  • AWS Cloud 9を検証環境に利用している際は、AWS Cloud 9自体のリソース削除漏れに留意してください。

関連ドキュメント

www.cisco.com

www.cisco.com