Ansibleを使ったAWS Direct Connectの自動化

# はじめに
こんにちは。小川と申します。
主にネットワークインフラを担当しています。
私が担当している箇所では、AWSで[Direct Connect](https://aws.amazon.com/jp/directconnect/)(以下DXと言います)を利用しています。
小さい単位でAWSアカウントを分けたいので、接続の構成は以下のようになります。
![構成図.png](https://qiita-image-store.s3.amazonaws.com/0/311023/3b669fc3-dbca-9b5f-9eec-dfb987f68fcb.png)
2018年12月現在、DXは、[Cloud Formation](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/cfn-supported-resources.html#cfn-supported-resources-networking)で自動化できる対象に入っていない点と、DXGWがクロスアカウントに対応していない点から、AWSアカウントが増えるたびに以下の作業が必要になります。

- AWS DX用アカウントで追加対象のアカウント向けVIFを追加
- AWS 追加対象のアカウントでVIFを承認
- オンプレ側スイッチにVLANを追加
- オンプレ側スイッチのインタフェースにVLAN Tagを追加
- オンプレ側スイッチにSVIを追加
- オンプレ側スイッチにBGP Peerを追加

※2019年になると、トランジットゲートウェイがDXに対応するようですので、そちらに期待です。
https://aws.amazon.com/jp/blogs/news/new-aws-transit-gateway/

これらの作業をAWSのコンソールやスイッチを手作業で行うのはとても手間なので、Ansibleで自動化できるようにしてみました。
動作している環境は以下になります。

| 項目 | 内容 | 
|:-----------|:------------|
| OS | CentOS7.5 |
| Ansibleバージョン | 2.6.1 |
| スイッチ | Arista EOS 4.18 |
# 準備
AnsibleのインストールとAnsibleからAWS CLIとネットワーク機器を操作する為の準備を行います。
## Ansibleのインストール
Ansibleのインストールを以下を参考に行います。
https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html
## AWS CLIのインストール
AWS CLIのインストールを以下を参考に行います。
https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/installing.html
## AWS CLIの設定
ansible-playbookを実行するユーザーで、aws configureを実行し、アクセスキーIDとシークレットアクセスキーを登録します。具体的な手順は以下になります。
https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-chap-getting-started.html
### コネクションIDの確認
`aws directconnect`コマンドを利用してコネクションIDを確認します。

```json:DXの設定
$ aws directconnect describe-connections
{
    "connections": [
        {
            "awsDevice": "DeviceName",
            "ownerAccount": "account ID",
            "connectionId": "【この値がコネクションID】dxcon-xxxxxxxx",
            "connectionState": "available",
            "bandwidth": "1Gbps",
            "location": "Location",
            "connectionName": "dx001",
            "loaIssueTime": 1532566140.0,
            "region": "ap-northeast-1",
            "awsDeviceV2": "DeviceName"
        },
        {
            "awsDevice": "DeviceName",
            "ownerAccount": "account ID",
            "connectionId": "【この値がコネクションID】dxcon-yyyyyyyy",
            "connectionState": "available",
            "bandwidth": "1Gbps",
            "location": "Location",
            "connectionName": "dx002",
            "loaIssueTime": 1532566166.0,
            "region": "ap-northeast-1",
            "awsDeviceV2": "DeviceName"
        }
    ]
}
```
## スイッチ側SSHキーの登録
Ansibleからスイッチ(Arista EOS)への接続は大人の事情からHTTP APIではなく、SSHを利用しています。
Ansibleを実行するユーザーからSSHでパスワードなしの公開鍵認証をできるようにスイッチ側で以下の設定を行います。

```configuration:show&nbsp;running-configから抜粋
aaa authorization exec default local
!
no aaa root
!
username [Ansibleを実行するユーザー名] privilege 15 secret [パスワード]
username [Ansibleを実行するユーザー名] sshkey [ssh-rsaで始まる公開鍵をそのまま記載]
!
management ssh
   vrf MGMT
      no shutdown
!
```
## インベントリの登録
``ansible-playbook``の-iオプションで指定するインベントリファイルを作成します。
インベントリファイルの詳細は以下をご確認ください。
https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html

```configuration:インベントリファイルの中身
# switch01,02をaristaというグループにまとめる
[arista]
# 操作対象のホスト名を記載
switch01
switch02

[arista:vars]
# Ansibleからの接続にnetwork_cli(SSH)を利用
ansible_connection=network_cli
# EOS(AristaのOS)を指定
ansible_network_os=eos
# SSHを実行するユーザーを指定
ansible_user=[ユーザー名]
# SSHの接続で利用する秘密鍵を指定
ansible_ssh_private_key_file=/home/[ユーザー名]/.ssh/id_rsa

# aws-cliをlocalhostで実行する為、インベントリに記載
[local]
localhost
```
正しく登録されているかをAnsibleのPingモジュールを使って確認します。

```console
$ ansible -i hosts -m ping arista
switch01 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
switch02 | SUCCESS => {
    "changed": false,
    "ping": "pong"
```
# Playbook、Roles、変数の準備
AnsibleはPlaybookを使ってタスクを実行しますが、今回のようにAWS側のVIF追加、オンプレ側のVLAN追加、BGP Peerの追加など項目が多く、場合によっては実行しないものなどがある為、Rolesを使って細かくタスクを分割します。Rolesの詳細を以下をご確認ください。
https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_roles.html
## Playbookの作成
Playbookを任意のディレクトリに作成します。Playbookの中身はrolesを記載するだけにして、作業に応じて必要なものを追加したりコメントアウトするだけの運用にしています。

```yaml:Playbookの中身
# AWS向けの設定
## インベントリのlocalグループに対して実行
- hosts: local
  connection: local
  gather_facts: no
## rolesの記載
  roles:
##### AWS DX VIFの追加
    - add_dxvif
# スイッチ向けの設定
## インベントリのaristaグループに対して実行
- hosts: arista
  gather_facts: no
## rolesの記載。必要に応じて追加、削除する
  roles:
    - add_vlans
    - add_vlantag
    - add_svi
    - add_bgppeer
```
## Rolesの作成
Rolesは、Playbookの配下にrolesディレクトリを作って、その中にPlaybookのrolesに記載したものと同じ名前のディレクトリを作成します。その下のディレクトリ構造は[Ansibleのマニュアル](https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_roles.html)にある「Role Directory Structure」を参考にしてください。
今回は、tasksとtemplatesだけを利用します。
ディレクトリ構造は以下になります。

```console:ディレクトリ構造
$ tree
.
#### arista.ymlがPlaybook
|-- arista.yml
|-- host_vars
|   |-- localhost
|   |-- switch01
|   `-- switch02
#### hostsがインベントリ
|-- hosts
`-- roles
    |-- add_bgppeer
    |   |-- tasks
    |   |   `-- main.yml
    |   |-- templates
    |   |   `-- add_bgppeer.j2
    |-- add_dxvif
    |   |-- tasks
    |   |   `-- main.yml
    |-- add_svi
    |   |-- tasks
    |   |   `-- main.yml
    |   |-- templates
    |    |   `-- add_svi.j2
    |-- add_vlans
    |   |-- tasks
    |   |   `-- main.yml
    |   |-- templates
    |   |   `-- vlans.j2
    `-- add_vlantag
        |-- tasks
        |   `-- main.yml
        |-- templates
            `-- add_vlantag.j2
```
### AWS VIF追加用のrolesの作成
タスクを定義した``main.yml``という名前のYAMLファイルをtasksの配下に設置します。ローカルで``aws direct-connect``コマンドを追加するVIF毎に実行します。繰り返し実行させるために、shellモジュールに[Jinja2テンプレート](https://docs.ansible.com/ansible/latest/user_guide/playbooks_templating.html?highlight=jinja2#templating-jinja2)を埋め込んでいます。

```yaml:main.yml
- name: Add VIF
  shell: "{% for i in dx %}aws directconnect allocate-private-virtual-interface --connection-id {{ i.connectionid }} --owner-account {{ i.account }} --new-private-virtual-interface-allocation virtualInterfaceName={{ i.name }},vlan={{ i.vlan }},asn=65534,authKey=password,amazonAddress={{ i.awsip }},customerAddress={{ i.localip }} --profile {{ i.profile }}\n{% endfor %}"
```

コマンドで設定する引数は以下になります。VIF毎に変更が必要な項目については後述の変数で値を設定します。

| 項目 | 内容 | 値 |
|:-----------|:------------|:------------|
| connection-id | 前述の手順で確認したDXのID | 変数で投入 |
| owner-account | VIFを払い出すAWSアカウント | 変数で投入 |
| virtualInterfacename | VIFの名前 | 変数で投入 |
| vlan | VIFのVLAN ID | 変数で投入 |
| asn | スイッチ側のAS番号 | 65534 |
| authKey | BGPのパスワード | password |
| amazonAddress | AWS側のIPアドレス | 変数で投入 |
| customerAddress | スイッチ側のIPアドレス | 変数で投入 |
※AWS側のAS番号は64512(デフォルト)、スイッチ側のAS番号は65534、BGPのパスワードはpasswordで固定しています。

### スイッチ用のrolesの作成
スイッチに対しては、tasksに[Jinja2テンプレート](https://docs.ansible.com/ansible/latest/user_guide/playbooks_templating.html?highlight=jinja2#templating-jinja2)を埋め込むのではなく、外部のファイルに記載します。その為、main.ymlへはJinja2テンプレートを呼び出すタスクだけを記載します。
全てのrolesのtasksディレクトリに以下のmain.ymlファイルを設置し、templatesディレクトリに[Jinja2テンプレート](https://docs.ansible.com/ansible/latest/user_guide/playbooks_templating.html?highlight=jinja2#templating-jinja2)を使って投入するConfigを記載します。
#### VLAN追加用のrolesの作成
../templates/vlans.j2を呼び出してスイッチへConfigを投入します。
``save_when: always``を記載しておくと毎回Configを保存します。

```yaml:roles/add_vlans/tasks/main.yml
- name: Add VLANs
  eos_config:
    src: vlans.j2
    save_when: always
```
VLAN IDとVLANの名前を変数で定義します。

```jinja:roles/add_vlans/templates/vlans.j2
{% for i in vlans %}
vlan {{ i.vlanid }}
   name {{ i.name }}
{% endfor %}
```
#### VLAN Tag追加用のrolesの作成
../templates/add_vlantag.j2を呼び出してスイッチへConfigを投入します。

```yaml:roles/add_vlantag/tasks/main.yml
- name: Add vlantag
  eos_config:
    src: add_vlantag.j2
    save_when: always
```
Tagを追加するインタフェースとVLAN Tagを変数で定義します。

```jinja:roles/add_vlantag/templates/add_vlantag.j2
{% for i in interfaces %}
interface {{ i.intname }}
   switchport trunk allowed vlan add {{ i.vlan }}
{% endfor %}
```
#### SVI追加用のrolesの作成
../templates/add_svi.j2を呼び出してスイッチへConfigを投入します。

```yaml:roles/add_svi/tasks/main.yml
- name: Add SVI
  eos_config:
    src: add_svi.j2
    save_when: always
```
SVIのインタフェース名、Desctiption、IPアドレスを変数で定義します。

```jinja:roles/add_svi/templates/add_svi.j2
{% for i in svi %}
interface {{ i.intname }}
  description {{ i.description }}
  ip address {{ i.address }}
{% endfor %}
```
#### BGP Peer追加用のrolesの作成
BGPの設定は共通化できる項目が多いので、peer-groupを利用して共通項目を設定します。
Arista EOSのBGPの詳細設定については以下をご確認ください。
https://www.arista.com/en/um-eos/eos-section-31-2-configuring-bgp

```config:peer-groupの設定
## 自ASは65534
router bgp 65534
   ### ピアグループ awsを使う
   neighbor aws peer-group
   ### AWS側のASは64512で固定
   neighbor aws remote-as 64512
   ### BFDを使って間接リンクの障害を検知する
   neighbor aws fall-over bfd
   ### パスワードの設定
   neighbor aws password [パスワード]
   ### AWSにはデフォルトルートを広告する
   neighbor aws default-originate
```
../templates/add_bgppeer.j2を呼び出してスイッチへConfigを投入します。

```yaml:roles/add_bgppeer/tasks/main.yml
- name: Add BGP Peer
  eos_config:
    src: add_bgppeer.j2
    save_when: always
```
peerのIPを変数で定義します。基本的な設定を読み込むためにpeer-groupを指定しています。

```jinja:roles/add_bgppeer/templates/add_bgppeer.j2
{% for i in bgppeers %}
router bgp 65534
   neighbor {{ i.peer }} peer-group aws
{% endfor %}
```
## 変数の作成
AWSの設定はlocalhostへ、スイッチの設定はswitch01,02に異なる設定を投入する必要があります。
それらの変数をプレイブックと同じディレクトリにhost_varsディレクトリを作成し、その中にファイル名をホスト名にしたファイルへ変数をYAML形式で記載します。ディレクトリ構成は以下のようになります。

```console:ディレクトリ構成
#### arista.ymlがプレイブック
|-- arista.yml
#### この下にホスト名をファイル名にした変数ファイルを設置
|-- host_vars
|   |-- localhost
|   |-- switch01
|   `-- switch02
`-- hosts
```
### AWS向け変数ファイル
VIFの名前、VIFを払い出すアカウント、VLAN、IPアドレス、コネクションIDを記載します。

```yaml:AWS向け変数
dx:
  - name: vif-a
    account: 123456789012
    vlan: 1111
    awsip: 1.1.1.1/30
    localip: 1.1.1.2/30
    connectionid: dxcon-xxxxxxxx
## リストにして複数のVIFを作成する事も可能
  - name: vif-b
    account: 123456789123
    vlan: 2221
    awsip: 2.2.2.1/30
    localip: 2.2.2.2/30
    connectionid: dxcon-yyyyyyyy
```
### スイッチ向け変数ファイル
VLAN、インタフェース、SVI、BGP Peerの設定内容を記載します。
2台ある場合は2台分記載します。

```yaml:スイッチ向け変数
### VLANの設定
vlans:
  - vlanid: 1111
    name: for-vif-a
  ## リストにして複数のVLANを一気に登録することも可能
  - vlanid: 2221
    name: for-vif-b

### インタフェースの設定
interfaces:
  - intname: Ethernet5
    description: for-dxcon-xxxxxxxx
    mode: trunk
    vlan: 1111
  ## リストにして複数のインタフェースを一気に登録することも可能
  - intname: Ethernet6
    description: for-dxcon-yyyyyyyy
    mode: trunk
    vlan: 2221

### SVIの設定
svi:
  - intname: Vlan1111
    description: vif-a
    address: 1.1.1.2/30
  ## リストにして複数のSVIを一気に登録することも可能
  - intname: Vlan2221
    description: vif-b
    address: 2.2.2.2/30

### BGP Peerの設定
bgppeers:
  - peer: 1.1.1.1
  ## リストにして複数のPeerを一気に登録することも可能
  - peer: 2.2.2.1
```

# Playbookの実行
``ansible-playbook``コマンドを利用してPlaybookを実行します。

```console:実行結果
## -iでインベントリファイルを指定
$ ansible-playbook -i hosts arista.yml

PLAY [local] *****************************************************************************************

TASK [add_dxvif : Add VIF] ************************************************
changed: [localhost]

PLAY [arista] **************************************************************************************

TASK [add_vlans : Add VLANs] *****************************************************************
changed: [switch01]
changed: [switch02]

TASK [add_vlantag : Add vlantag] *****************************************************************
changed: [switch01]
changed: [switch02]

TASK [add_svi : Add SVI] *****************************************************************
changed: [switch01]
changed: [switch02]

TASK [add_bgppeer : Add BGP Peer] *****************************************************************
changed: [switch01]
changed: [switch02]

PLAY RECAP *******************************************************************************************
localhost                  : ok=0    changed=1    unreachable=0    failed=0
switch01                : ok=0    changed=4    unreachable=0    failed=0
switch02                : ok=0    changed=4    unreachable=0    failed=0
```

# 運用方法
host_varsのファイルを都度更新してから``ansible-playbook``コマンドを実行します。これまでは手順書、Configファイルをレビューしてから作業を行っていましたが、変数が正しい事が確認できればコマンドを1回実行するだけで変更することができるようになりました。

# おわりに
今回はAWSのDXに関わる個所のみの変更でしたが、rolesで作業を細分化する事で、日々のネットワーク機器の運用に応用できるようになるかと思います。
又、今回Aristaの機器で自動化を行いましたが、CiscoやJuniperなどもAnsibleのモジュールが充実しているのでネットワークの自動化にどんどん取り組んでいこうと考えています！