zaki work log

作業ログやら生活ログやらなんやら

Terraformをv0.15に更新してAWSにEC2を作成するサンプルを動作

Terraformがv0.15にアップデートされたので、手元の環境も更新してみた。
以前お試しで作ったv0.12で動いていたソースでプロバイダ設定を新しい書式に更新すれば動作した。
zaki-hmkc.hatenablog.com

qiita.com

サンプルレベルだとおそらくv0.14 -> v0.15固有の更新は無く、v0.12からの飛び級更新のためいくつか修正が必要だったので、その内容について作業メモ。

既存環境

[zaki@cloud-dev terraform (master)]$ which terraform 
/usr/local/bin/terraform
[zaki@cloud-dev terraform (master)]$ terraform --version
Terraform v0.12.28

Your version of Terraform is out of date! The latest version
is 0.15.0. You can update by downloading from https://www.terraform.io/downloads.html

インストールした時の状況は過去記事のTerraform CLIツールのインストールの通り。

install

TerraformのCLIは、以前はバイナリファイルをダウンロードしてOSに展開する方式だったが、現在はパッケージマネージャを使ってインストールできるようになっている。

https://learn.hashicorp.com/tutorials/terraform/install-cli

以下はCentOS 7の場合。

[zaki@cloud-dev terraform (master)]$ sudo yum-config-manager --add-repo https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
読み込んだプラグイン:fastestmirror
adding repo from: https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo
grabbing file https://rpm.releases.hashicorp.com/RHEL/hashicorp.repo to /etc/yum.repos.d/hashicorp.repo
repo saved to /etc/yum.repos.d/hashicorp.repo
[zaki@cloud-dev terraform (master)]$ sudo yum -y install terraform
読み込んだプラグイン:fastestmirror
Loading mirror speeds from cached hostfile
 * base: ftp.riken.jp
 * epel: ftp.riken.jp
 * extras: ftp.riken.jp
 * updates: ftp.riken.jp
hashicorp                                                                                         | 1.4 kB  00:00:00     
hashicorp/7/x86_64/primary                                                                        |  41 kB  00:00:00     
hashicorp                                                                                                        271/271
依存性の解決をしています
--> トランザクションの確認を実行しています。
---> パッケージ terraform.x86_64 0:0.15.0-1 を インストール
--> 依存性解決を終了しました。

依存性を解決しました

=========================================================================================================================
 Package                      アーキテクチャー          バージョン                    リポジトリー                  容量
=========================================================================================================================
インストール中:
 terraform                    x86_64                    0.15.0-1                      hashicorp                     25 M

トランザクションの要約
=========================================================================================================================
インストール  1 パッケージ

総ダウンロード容量: 25 M
インストール容量: 76 M
Downloading packages:
警告: /var/cache/yum/x86_64/7/hashicorp/packages/terraform-0.15.0-1.x86_64.rpm: ヘッダー V4 RSA/SHA512 Signature、鍵 ID a3219f7b: NOKEY
terraform-0.15.0-1.x86_64.rpm の公開鍵がインストールされていません
terraform-0.15.0-1.x86_64.rpm                                                                     |  25 MB  00:00:11     
https://rpm.releases.hashicorp.com/gpg から鍵を取得中です。
Importing GPG key 0xA3219F7B:
 Userid     : "HashiCorp Security (HashiCorp Package Signing) <security+packaging@hashicorp.com>"
 Fingerprint: e8a0 32e0 94d8 eb4e a189 d270 da41 8c88 a321 9f7b
 From       : https://rpm.releases.hashicorp.com/gpg
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  インストール中          : terraform-0.15.0-1.x86_64                                                                1/1 
  検証中                  : terraform-0.15.0-1.x86_64                                                                1/1 

インストール:
  terraform.x86_64 0:0.15.0-1                                                                                            

完了しました!
[zaki@cloud-dev terraform (master)]$ 

旧バージョンの/usr/local/bin/terraformは削除してシェルを起動し直す。

[zaki@cloud-dev terraform (master)]$ terraform --version
Terraform v0.15.0
on linux_amd64

AWSVPCからEC2まで作成するサンプル(0.15版)

元ネタ

zaki-hmkc.hatenablog.com

このときに作った定義ファイルをv0.15対応する。
ちなみにこの定義ファイルで作成されるリソースはクラウド上には既に無いので、最悪tfstateを消してリセットする、も可能。(運用時はそうもいかないだろうけど)

[zaki@cloud-dev practice (master)]$ find .terraform/
.terraform/
.terraform/plugins
.terraform/plugins/linux_amd64
.terraform/plugins/linux_amd64/terraform-provider-aws_v2.70.0_x4
.terraform/plugins/linux_amd64/lock.json

reinitialization

まずはそのまま実行してみる。

[zaki@cloud-dev practice (master)]$ terraform plan
╷
│ Warning: Version constraints inside provider configuration blocks are deprecated
│ 
│   on provider.tf line 2, in provider "aws":
│    2:   version = "~> 2.8"
│ 
│ Terraform 0.13 and earlier allowed provider version constraints inside the provider configuration block, but that is
│ now deprecated and will be removed in a future version of Terraform. To silence this warning, move the provider
│ version constraint into the required_providers block.
╵
╷
│ Error: Could not load plugin
│ 
│ 
│ Plugin reinitialization required. Please run "terraform init".
│ 
│ Plugins are external binaries that Terraform uses to access and manipulate
│ resources. The configuration provided requires plugins which can't be located,
│ don't satisfy the version constraints, or are otherwise incompatible.
│ 
│ Terraform automatically discovers provider requirements from your
│ configuration, including providers used in child modules. To see the
│ requirements and constraints, run "terraform providers".
│ 
│ failed to instantiate provider "registry.terraform.io/hashicorp/aws" to obtain schema: unknown provider
│ "registry.terraform.io/hashicorp/aws"
│ 
╵

警告はいったん置いといて、エラーがでてる。
terraform initの再実行が必要ということ。

[zaki@cloud-dev practice (master)]$ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 2.8"...
- Installing hashicorp/aws v2.70.0...
- Installed hashicorp/aws v2.70.0 (signed by HashiCorp)

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

╷
│ Warning: Version constraints inside provider configuration blocks are deprecated
│ 
│   on provider.tf line 2, in provider "aws":
│    2:   version = "~> 2.8"
│ 
│ Terraform 0.13 and earlier allowed provider version constraints inside the provider configuration block, but that is
│ now deprecated and will be removed in a future version of Terraform. To silence this warning, move the provider
│ version constraint into the required_providers block.
╵

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.

init自体は成功して以下のとおりファイルが追加される。
ディレクトリ構成が変わっており、旧バージョンで使っていたplugins/ではなくproviders/以下にファイルが作成される模様。

[zaki@cloud-dev practice (master)]$ find .terraform/
.terraform/
.terraform/plugins
.terraform/plugins/linux_amd64
.terraform/plugins/linux_amd64/terraform-provider-aws_v2.70.0_x4
.terraform/plugins/linux_amd64/lock.json
.terraform/providers
.terraform/providers/registry.terraform.io
.terraform/providers/registry.terraform.io/hashicorp
.terraform/providers/registry.terraform.io/hashicorp/aws
.terraform/providers/registry.terraform.io/hashicorp/aws/2.70.0
.terraform/providers/registry.terraform.io/hashicorp/aws/2.70.0/linux_amd64
.terraform/providers/registry.terraform.io/hashicorp/aws/2.70.0/linux_amd64/terraform-provider-aws_v2.70.0_x4

が、バージョンの警告が出ているのでそれを修正する。

Docs overview | hashicorp/aws | Terraform Registry

前回はv0.12でやっていたので「awsプロバイダ指定とバージョン・リージョン指定」をセットで書く書式だったが、v0.13以降は以下のように「プロバイダ指定とバージョン」「awsプロバイダの指定とリージョン指定」と別々に記述するようになっている。

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

provider "aws" {
  region  = "ap-northeast-1"
}

これでinitを再実行すると、

[zaki@cloud-dev practice (master)]$ terraform init

Initializing the backend...

Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
╷
│ Error: Failed to query available provider packages
│ 
│ Could not retrieve the list of available versions for provider hashicorp/aws: locked provider
│ registry.terraform.io/hashicorp/aws 2.70.0 does not match configured version constraint ~> 3.0; must use terraform
│ init -upgrade to allow selection of new versions
╵

ぬぁ、アップグレードは-upgradeオプションが必要。

[zaki@cloud-dev practice (master)]$ terraform init -upgrade

Initializing the backend...

Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 3.0"...
- Installing hashicorp/aws v3.37.0...
- Installed hashicorp/aws v3.37.0 (signed by HashiCorp)

Terraform has made some changes to the provider dependency selections recorded
in the .terraform.lock.hcl file. Review those changes and commit them to your
version control system if they represent changes you intended to make.

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.

正常に動作しました。

plan

プロバイダ定義以外の定義ファイルはそのままで、terraform plan(dry run)を実行。

[zaki@cloud-dev practice (master)]$ terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # aws_instance.bastion will be created
  + resource "aws_instance" "bastion" {
      + ami                          = (sensitive)
      + arn                          = (known after apply)
      + associate_public_ip_address  = true
      + availability_zone            = (known after apply)

:
:

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

────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if
you run "terraform apply" now.

とくに問題無し。

apply

[zaki@cloud-dev practice (master)]$ time terraform apply -auto-approve
aws_key_pair.my_key: Creating...
aws_vpc.practice: Creating...
aws_key_pair.my_key: Creation complete after 0s [id=deployer-key]
aws_vpc.practice: Still creating... [10s elapsed]
aws_vpc.practice: Creation complete after 12s [id=vpc-033ac6df503e91040]
aws_internet_gateway.gw: Creating...
aws_subnet.prac_priv2: Creating...
aws_security_group.allow_ssh_icmp: Creating...
aws_subnet.prac_priv1: Creating...
aws_subnet.prac_public: Creating...
aws_subnet.prac_priv2: Creation complete after 1s [id=subnet-0623f3cc8dca00879]
aws_internet_gateway.gw: Creation complete after 1s [id=igw-08d2b30675b10dbd5]
aws_route_table.public_route: Creating...
aws_subnet.prac_priv1: Creation complete after 1s [id=subnet-0761a961e0049ac32]
aws_subnet.prac_public: Creation complete after 1s [id=subnet-023d30a65d6a5fe9f]
aws_route_table.public_route: Creation complete after 1s [id=rtb-038e5ea5f48545c62]
aws_route_table_association.public_subnet: Creating...
aws_security_group.allow_ssh_icmp: Creation complete after 2s [id=sg-043174961474ad348]
aws_security_group_rule.egress: Creating...
aws_security_group_rule.icmp: Creating...
aws_security_group_rule.ssh: Creating...
aws_instance.bastion: Creating...
aws_route_table_association.public_subnet: Creation complete after 0s [id=rtbassoc-09bb5298f44359358]
aws_security_group_rule.egress: Creation complete after 1s [id=sgrule-3339993613]
aws_security_group_rule.icmp: Creation complete after 1s [id=sgrule-366981081]
aws_security_group_rule.ssh: Creation complete after 2s [id=sgrule-3333037129]
aws_instance.bastion: Still creating... [10s elapsed]
aws_instance.bastion: Creation complete after 13s [id=i-0a64360f7c3300544]

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

real    0m31.964s
user    0m2.115s
sys     0m0.280s

できた。

ssh access

はい。

[zaki@cloud-dev practice (master)]$ ssh ec2-user@*.*.*.* -i ~/.ssh/id_aws_sample
The authenticity of host '*.*.*.* (*.*.*.*)' can't be established.
ECDSA key fingerprint is SHA256:gUKCdxBnkvEt1R0K0DjgiASoiH2XdpnVCccgH5kH+vU.
ECDSA key fingerprint is MD5:1d:df:5e:a7:55:9a:a2:5a:15:64:e9:63:0b:ff:e8:a2.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '*.*.*.*' (ECDSA) to the list of known hosts.

       __|  __|_  )
       _|  (     /   Amazon Linux 2 AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-2/
No packages needed for security; 2 packages available
Run "sudo yum update" to apply all updates.
[ec2-user@ip-172-26-10-12 ~]$ uptime
 00:35:04 up 3 min,  1 user,  load average: 0.02, 0.04, 0.01

後始末

terraform destroy -auto-approve

追加ファイル

www.terraform.io

バージョンが新しくなり、新たに .terraform.lock.hcl というファイルが作成されるようになっている。
以下の記事で詳しく解説されている。
参考: Terraform職人再入門2020 - Qiita

なるほど、init-upgradeが必要だったのはこれも影響してたっぽい。

このロックファイルが存在すると、 terraform init が前回と同じバージョンを選択します。更新する場合は terraform init -upgrade を実行します。

また、このファイルもバージョン管理の対象にするとのこと。

サンプルコード

github.com

手元はmasterなのにGitHubリポジトリ作ったらmainになってて差異を解消できずに朝からgit push --forceを発動してしまった()

[VyOS] NAT設定を使ったネットワーク間のルーティング (手動 & Ansible / vyos_config)

VyOS自体のIPアドレス設定は前回までにできたので、ようやく(?)ネットワーク間でNATを使ったルーターとして動作するための設定をやってみる。

(試行錯誤でやってるんで「こうした方がいい」とか「いや、そのりくつはおかしい」とかあったらぜひ教えて欲しい…)

f:id:zaki-hmkc:20210415085905p:plain

  • オレンジの"network A"(172.16.0.0/23)と、グリーンの"network B"(172.29.0.0/24)があり、VyOSが両方のネットワークに接続されている
  • VyOSのIPアドレス設定は、"network A"が172.16.1.3/23(eth0)で、"network B"が172.29.0.3/24(eth1)
  • VyOSに対して設定を行うAnsibleがインストールされているホストは"network A"の左端の172.16.1.119/23
  • 今回設定するのは、"network A"にあるFedoraのホスト(172.16.1.17/23)と、"network B"にあるCentOSのホスト(172.29.0.89/24)の相互通信
  • Fedoraのホスト名はclient-dev
  • CentOSのホスト名はrestricted-node

VyOSの簡単なNAT設定はQuick Startに載っている。

docs.vyos.io

あとはNATのページ。

docs.vyos.io

NAT44とNAT66ってあるけど、66の方はIPv6用なのかな?(無知)

環境

restricted-node (CentOSホスト)

NICは1つ

[zaki@restricted-node ~]$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:0c:29:77:f6:c4 brd ff:ff:ff:ff:ff:ff
    inet 172.29.0.89/24 brd 172.29.0.255 scope global noprefixroute ens192
       valid_lft forever preferred_lft forever
    inet6 fe80::20c:29ff:fe77:f6c4/64 scope link 
       valid_lft forever preferred_lft forever
[zaki@restricted-node ~]$ nmcli c s "System ens192" | grep ipv4.gateway
ipv4.gateway:                           172.29.0.3

Fedoraのホストへはpingは届かない。

[zaki@restricted-node ~]$ ping -c 4 172.16.1.17
PING 172.16.1.17 (172.16.1.17) 56(84) bytes of data.

--- 172.16.1.17 ping statistics ---
4 packets transmitted, 0 received, 100% packet loss, time 2999ms

client-dev (Fedoraホスト)

図には描いてないけどNICは2つで上位ネットワークにもつながってる。(今回は関係ない)

[root@client-dev ~]# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:0c:29:22:fb:ed brd ff:ff:ff:ff:ff:ff
    altname enp11s0
    inet 192.168.0.17/24 brd 192.168.0.255 scope global noprefixroute ens192
       valid_lft forever preferred_lft forever
3: ens224: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:0c:29:22:fb:f7 brd ff:ff:ff:ff:ff:ff
    altname enp19s0
    inet 172.16.1.17/23 brd 172.16.1.255 scope global noprefixroute ens224
       valid_lft forever preferred_lft forever

デフォルトゲートウェイは上位ネットワークに向いている。

[root@client-dev ~]# nmcli c s ens192 | grep ipv4.gateway
ipv4.gateway:                           192.168.0.1
[root@client-dev ~]# nmcli c s ens224 | grep ipv4.gateway
ipv4.gateway:                           --
[root@client-dev ~]# ping -c 4 172.29.0.89
PING 172.29.0.89 (172.29.0.89) 56(84) bytes of data.

--- 172.29.0.89 ping 統計 ---
送信パケット数 4, 受信パケット数 0, 100% packet loss, time 3078ms

CentOSのホストへpingは届かない。

Ansible

(a2.10) [zaki@cloud-dev vyos (master)]$ ansible --version
ansible 2.10.7
  config file = /home/zaki/.ansible.cfg
  configured module search path = ['/home/zaki/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/zaki/src/ansible-sample/venv/a2.10/lib64/python3.6/site-packages/ansible
  executable location = /home/zaki/src/ansible-sample/venv/a2.10/bin/ansible
  python version = 3.6.8 (default, Nov 16 2020, 16:55:22) [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]

VyOS

show versionで確認できたのね。
以下の通り。

vyos@vyos:~$ show version 

Version:          VyOS 1.4-rolling-202104061641
Release Train:    sagitta

Built by:         autobuild@vyos.net
Built on:         Wed 07 Apr 2021 01:17 UTC
Build UUID:       467750e8-bd03-4562-a102-c2e895328517
Build Commit ID:  9c63b99198c829

Architecture:     x86_64
Boot via:         installed image
System type:      VMware guest

Hardware vendor:  VMware, Inc.
Hardware model:   VMware Virtual Platform
Hardware S/N:     VMware-56 4d 5d 6e 4f 25 4a d1-64 a3 ff d0 e1 3b 48 8c
Hardware UUID:    6e5d4d56-254f-d14a-64a3-ffd0e13b488c

Copyright:        VyOS maintainers and contributors

ネットワークの状態

vyos@vyos:~$ show interfaces 
Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down
Interface        IP Address                        S/L  Description
---------        ----------                        ---  -----------
eth0             172.16.1.3/23                     u/u  
eth1             172.29.0.3/24                     u/u  
lo               127.0.0.1/8                       u/u  
                 ::1/128       

network Bからnetwork AへのNAT (手動)

まずは手動で設定してみる。

VyOS設定

'172.16.0.0/23'宛のパケットでeth0からのアウトバウンドをIPマスカレード設定、という内容。
(Quick Startは「入力」でルール定義してたけど、今回は「出力」を基準にしてる)

vyos@vyos:~$ configure 
[edit]
vyos@vyos# set nat source rule 100 outbound-interface eth0
[edit]
vyos@vyos# set nat source rule 100 destination address 172.16.0.0/23
[edit]
vyos@vyos# set nat source rule 100 translation address masquerade
[edit]

これでshowを実行すると投入される↑の設定内容を確認できるので問題無ければcommitを実行。

確認

[zaki@restricted-node ~]$ ping -c 4 172.16.1.17
PING 172.16.1.17 (172.16.1.17) 56(84) bytes of data.
64 bytes from 172.16.1.17: icmp_seq=1 ttl=63 time=0.481 ms
64 bytes from 172.16.1.17: icmp_seq=2 ttl=63 time=0.501 ms
64 bytes from 172.16.1.17: icmp_seq=3 ttl=63 time=0.256 ms
64 bytes from 172.16.1.17: icmp_seq=4 ttl=63 time=0.357 ms

--- 172.16.1.17 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 2999ms
rtt min/avg/max/mdev = 0.256/0.398/0.501/0.102 ms

CentOS -> Fedoraへのpingが繋がるようになった。

save

再起動後も有効になるよう設定をほじするため、saveを実行。

network Aからnetwork BへのNAT (Ansible)

同じ要領で172.29.0.0/24宛のeth1からのアウトバウンド設定を今度はAnsibleで。

playbook

なんだけど、NATを設定するためのモジュールがどれかわからず…なさそう。
ということで、vyos.vyos.vyos_configモジュールを使って設定する。

---
- hosts: vyos
  gather_facts: false

  tasks:
    - name: configure nat (A to B)
      vyos.vyos.vyos_config:
        lines:
          - set nat source rule 110 outbound-interface eth1
          - set nat source rule 110 destination address 172.29.0.0/24
          - set nat source rule 110 translation address masquerade

この内容のplaybookでansible-playbook-v付けて実行すると以下の通り。

changed: [172.16.1.3] => changed=true 
  ansible_facts:
    discovered_interpreter_python: /usr/bin/python
  commands:
  - set nat source rule 110 outbound-interface eth1
  - set nat source rule 110 destination address 172.29.0.0/24
  - set nat source rule 110 translation address masquerade
  filtered: []

vyos_factsを使っても下記の情報を確認できる。

    set nat source rule 110 destination address '172.29.0.0/24'
    set nat source rule 110 outbound-interface 'eth1'
    set nat source rule 110 translation address 'masquerade'

configureモードでshowしても、natに関しては以下のように設定を確認できる。

 nat {
     source {
         rule 100 {
             destination {
                 address 172.16.0.0/23
             }
             outbound-interface eth0
             translation {
                 address masquerade
             }
         }
         rule 110 {
             destination {
                 address 172.29.0.0/24
             }
             outbound-interface eth1
             translation {
                 address masquerade
             }
         }
     }
 }

確認

デフォルトゲートウェイ設定が別ネットワークになってるので、これをVyOSに向くように変更する。

ens192の設定をいったん削除

[root@client-dev ~]# nmcli c m ens192 ipv4.gateway ""
[root@client-dev ~]# nmcli c m ens224 ipv4.gateway "172.16.1.3"

ens224にVyOS向きの設定追加

[root@client-dev ~]# nmcli c m ens192 ipv4.gateway ""
[root@client-dev ~]# nmcli c m ens224 ipv4.gateway "172.16.1.3"

設定の有効化
※ CentOS7のときはsystemctl restart networkで反映できたけどFedora33は対応するサービスがなかったのでupしてる。(reloadだけでもダメだった)

[root@client-dev ~]# nmcli c up ens192
接続が正常にアクティベートされました (D-Bus アクティブパス: /org/freedesktop/NetworkManager/ActiveConnection/3)
[root@client-dev ~]# nmcli c up ens224
接続が正常にアクティベートされました (D-Bus アクティブパス: /org/freedesktop/NetworkManager/ActiveConnection/4)
[root@client-dev ~]# ping -c 4 172.29.0.89
PING 172.29.0.89 (172.29.0.89) 56(84) bytes of data.
64 バイト応答 送信元 172.29.0.89: icmp_seq=1 ttl=63 時間=0.371ミリ秒
64 バイト応答 送信元 172.29.0.89: icmp_seq=2 ttl=63 時間=0.219ミリ秒
64 バイト応答 送信元 172.29.0.89: icmp_seq=3 ttl=63 時間=0.333ミリ秒
64 バイト応答 送信元 172.29.0.89: icmp_seq=4 ttl=63 時間=0.293ミリ秒

--- 172.29.0.89 ping 統計 ---
送信パケット数 4, 受信パケット数 4, 0% packet loss, time 3058ms
rtt min/avg/max/mdev = 0.219/0.304/0.371/0.056 ms

ちなみにこのnmcli c mによる設定変更はリブートでリセットします。今度はこの辺をまとめたい ← 勘違い。リブートしてもリセットはしない。(手動で戻したのを勘違いしてたかも)

save

(しつこいけどw)ただしこれだけのplaybookだと、VyOSをリブートしたら設定が(ry

Ansibleのvyos.vyos.vyos_configモジュールの場合は、saveパラメタをtrueに指定しておけば保存される。

    - name: configure nat (A to B)
      vyos.vyos.vyos_config:
        lines:
          - set nat source rule 110 outbound-interface eth1
          - set nat source rule 110 destination address 172.29.0.0/24
          - set nat source rule 110 translation address masquerade
        save: true

ただしこのsaveは、指定タスクの設定内容だけをsaveするのではなく、これまでの未saveの設定全てが保存される。

vyos_configの冪等性

OSコマンドを実行するためのshellcommandと異なり、ネットワークOS用のconfig系モジュールは、冪等性がある。 というのも、コマンド投入前に同じコマンドが設定済みか無いかどうかを確認しているため。(言いかえると確認する術があるため)

ただしそのとき、taskに定義した投入予定のコマンドが省略形で記載されていると比較ができなくなるため、再実行されてしまうので(taskに定義するコマンド名は省略しないように)注意が必要。

docs.ansible.com

この辺の処理をコードから追いたかったけどちょっと時間かかりそうだったので覚えてたらまた別途。


CentOSをfirewalld使ってルーター設定するのに比べると、さすがにネットワークOSは設定のコマンドやパラメタが直観的でわかりやすいな。。

zaki-hmkc.hatenablog.com


そういえば、Fedora 33のNetworkManager、設定スクリプトって/etc/NetworkManager/system-connections以下にあるのね。
/etc/sysconfig/network-scripts 以下にあるものと思い込んでたから探しちゃいましたw

あと中を見ると書式も変わってた。バージョンかなぁ。

Ansibleを使ってVyOSのIPアドレスを設定する

前回セットアップしたVyOSの、まだ設定を残しておいたeth1のIPアドレスをAnsibleを使って設定してみる。

zaki-hmkc.hatenablog.com

VyOSのホストとAnsibleを実行するホストの関係は以下の通り。
172.16.1.119のホストにAnsibleがインストールされており、前回設定した172.16.1.3(eth0)のVyOSに接続してもう一つのNICであるeth1の設定を行う、というもの。

f:id:zaki-hmkc:20210412080307p:plain

AnsibleでVyOSに接続して設定サンプルとしてIPアドレスを設定する、くらいの内容。

inventory

[vyos]
172.16.1.3  ansible_user=vyos ansible_password=vyos

[vyos:vars]
ansible_connection=network_cli
ansible_network_os=vyos

VyOS本体のアドレスと、ログイン用アカウント情報をansible_useransible_passwordで設定している。
また、コネクションプラグインとしてnetwork_cliを、そして接続先ネットワークOS種別としてansible_network_os=vyosを指定。

playbook

現在の設定を取得

設定前の状態をAnsibleで取得してみる。

docs.ansible.com

---
- hosts: vyos
  gather_facts: false

  tasks:
    - name: vyos_fact
      vyos.vyos.vyos_facts:
        gather_subset: config
      # register: result

    - name: print config
      debug:
        # msg: "{{ result.ansible_facts.ansible_net_config }}"
        msg: "{{ ansible_net_config }}"

出力はこんな感じ。(一部マスク済み)

ok: [172.16.1.3] => 
  msg:
  - |-
    set interfaces ethernet eth0 address '172.16.1.3/23'
    set interfaces ethernet eth0 hw-id '00:0c:29:3b:48:8c'
    set interfaces ethernet eth1 hw-id '00:0c:29:3b:48:96'
    set interfaces loopback lo
    set service ssh
    set system config-management commit-revisions '100'
    set system console device ttyS0 speed '115200'
    set system host-name 'vyos'
    set system login user vyos authentication encrypted-password '........'
    set system login user vyos authentication plaintext-password ''
    set system ntp server 0.pool.ntp.org
    set system ntp server 1.pool.ntp.org
    set system ntp server 2.pool.ntp.org
    set system syslog global facility all level 'info'
    set system syslog global facility protocols level 'debug'
  - |-
    0   2021-04-08 00:19:38 by vyos via cli
    1   2021-04-08 00:13:38 by vyos via cli
    2   2021-04-08 00:11:38 by vyos via cli
    3   2021-04-08 00:10:39 by root via vyos-boot-config-loader
    4   2021-04-08 00:09:09 by vyos via cli
    5   2021-04-07 23:48:18 by root via vyos-boot-config-loader
    6   2021-04-07 23:48:18 by root via init

show interfaces ethernetすると以下の通り。

vyos@vyos:~$ show interfaces ethernet 
Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down
Interface        IP Address                        S/L  Description
---------        ----------                        ---  -----------
eth0             172.16.1.3/23                     u/u  
eth1             -          

IPアドレス設定

IPアドレスの設定はvyos_l3_interfacesを使用する。

docs.ansible.com

    - name: set ipv4 to eth1
      vyos.vyos.vyos_l3_interfaces:
        config:
          - name: eth1
            ipv4:
              - address: 172.29.0.3/24
        state: merged

この内容のplaybookでAnsibleを実行すると、

ok: [172.16.1.3] => 
  msg:
  - |-
    set interfaces ethernet eth0 address '172.16.1.3/23'
    set interfaces ethernet eth0 hw-id '00:0c:29:3b:48:8c'
    set interfaces ethernet eth1 address '172.29.0.3/24'
    set interfaces ethernet eth1 hw-id '00:0c:29:3b:48:96'
    set interfaces loopback lo
    set service ssh

show interfacesすると以下の通り。

vyos@vyos:~$ show interfaces ethernet 
Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down
Interface        IP Address                        S/L  Description
---------        ----------                        ---  -----------
eth0             172.16.1.3/23                     u/u  
eth1             172.29.0.3/24                     u/u  

設定の保存

ただしこれだけだと、例によってrebootによって設定は消えるので、保存する処理を追加する。
保存するにはvyos_configモジュールでsaveを指定する。

docs.ansible.com

    - name: save config
      vyos.vyos.vyos_config:
        save: true

これでsaveの実行が行われ、rebootしても設定が保持される。

vyos_l3_interfacesにsaveするパラメタがあったら楽だったのに…と思ったけど、サーバー構築自動化でもサービスのrestartは最後にやるし、notifyとhandler使って「変更があったときだけ」もできるからこっちの方がいいのかも。

環境

Ansibleバージョンは以下の通り。
VyOSのモジュール/コレクションは付属のものを使用。

(a2.10) [zaki@cloud-dev vyos (master)]$ ansible --version
ansible 2.10.7
  config file = /home/zaki/.ansible.cfg
  configured module search path = ['/home/zaki/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/zaki/src/ansible-sample/venv/a2.10/lib64/python3.6/site-packages/ansible
  executable location = /home/zaki/src/ansible-sample/venv/a2.10/bin/ansible
  python version = 3.6.8 (default, Nov 16 2020, 16:55:22) [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]
(a2.10) [zaki@cloud-dev vyos (master)]$ ansible-galaxy collection list vyos.vyos

# /home/zaki/src/ansible-sample/venv/a2.10/lib/python3.6/site-packages/ansible_collections
Collection Version
---------- -------
vyos.vyos  1.1.1  

# /home/zaki/src/ansible-sample/venv/a2.10/lib64/python3.6/site-packages/ansible_collections
Collection Version
---------- -------
vyos.vyos  1.1.1  

playbook類は以下にあります。

github.com


次こそルーティング設定をやろうな…

[Linux] systemdのユニットファイルを自分で書いてプログラムをdaemon動作させてみる

systemdのユニットファイルを作成してみる。
rcスクリプト育ちだったのでsystemctlコマンドは慣れるのに時間はかかったといえさすがにもう息を吸うように使うようになったけど、systemdのユニットファイル周りは基本的な部分があまりわかってないのでお試し。

記載については、Red Hatのドキュメントを確認すれば必要な情報は揃う。

access.redhat.com

access.redhat.com

本エントリ中の作業ホストは意表をついてUbuntu 20.04で確認。

root@ubuntu-node:~# cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=20.04
DISTRIB_CODENAME=focal
DISTRIB_DESCRIPTION="Ubuntu 20.04.2 LTS"

お題

daemonで動く適当なアプリケーションとして、以下で作ったPerlのソケットサーバーを使う。

qiita.com

ベースのソースはこちら。

github.com

実行するとTCP/8080でListenしてなんちゃってHTTPを喋るサーバーとして動作する。

コードの修正

systemdで停止を行う際に使用するための、実行プロセスのPIDをファイル出力するようにする。

open W, "> /var/run/sockserv.pid" or die;
print W $$;
close W;

Perlでは$$でプロセスIDを取得できる。
この値をファイル出力するコードを、for()で接続待ちのループに入る前に追加。

(forkしないスクリプトなので、実はこのファイルは不要だった)

ユニットファイルの作成

コアな部分の最低限必要そうな記述を行うと以下の通り。

[Unit]
Description=Perl Socket Server Sample
After=network.target

[Service]
Type=simple
PIDFile=/var/run/sockserv.pid
ExecStart=/usr/local/tcp-probe-example/src/sockserv.pl

[Install]
WantedBy=multi-user.target

パラメタ

man systemd.directivesで(どのmanを見ればいいかが)確認できる。

Description

概要。
systemctl status hogehogeとかの出力に使用される。

After

依存するユニット。
ここで指定したユニットがactiveになってから起動される。
ネットワークを使うプログラムであればnetworkを指定しておけばよさげ。

Type

今回のお題のスクリプトのように、実行するとフォアグラウンドで動作するスクリプトであればsimpleを指定する。
スクリプト自体がバックグラウンドで動作するようなものはforkingを使う。

詳しくは表10.10 [Service] セクションの重要なオプションを参照。

PIDFile

ユニットファイルで指定するプログラムが出力するPIDが記録されるファイルのパス。
停止時にこのPIDに対してkillする。

なんだけど、forkしない1プロセスで処理するスクリプトだとドキュメント見る限り不要っぽい。

ExecStart

プログラムの起動コマンド。
引数がある場合もここに全部書ける模様。

WantedBy

WantedByというか、[Install]セクションの設定は、systemctl enableを使った時の動作を定義。
WantedBy=multi-user.targetだと/etc/systemd/system/multi-user.target.wants/以下にsymlinkを作成する動作になる。

ユニットファイルの作成場所

どこにファイルを作ればいいかはman systemdで確認できる。

System unit directories
The systemd system manager reads unit configuration from various directories. Packages that want to install unit files shall place them in the directory returned by pkg-config systemd --variable=systemdsystemunitdir. Other directories checked are /usr/local/lib/systemd/system and /usr/lib/systemd/system. User configuration always takes precedence. pkg-config systemd --variable=systemdsystemconfdir returns the path of the system configuration directory. Packages should alter the content of these directories only with the enable and disable commands of the systemctl(1) tool. Full list of directories is provided in systemd.unit(5).

平たく言うと、「どこのディレクトリに置くと有効かはman systemd.unitで確認できる。主に使うのは/usr/lib/systemd/system/usr/local/lib/systemd/system」とのこと(超意訳)。

ということでパッケージインストールするやつじゃなくて自前のプログラムなので、/usr/local/lib/systemd/system以下に作ってみる。

# mkdir -p /usr/local/lib/systemd/system
# vi /usr/local/lib/systemd/system/sockserv.service

ここに前述の内容のユニットファイルを作成。

今回はsockservという名前のサービスとして登録してみるということで、sockserv.serviceというファイル名でファイル作成。

systemctl

status

root@ubuntu-node:~# systemctl status sockserv
● sockserv.service - Perl Socket Server Sample
     Loaded: loaded (/usr/local/lib/systemd/system/sockserv.service; disabled; vendor preset: enabled)
     Active: inactive (dead)

この時点でstatusで内容確認できる。

start

root@ubuntu-node:~# systemctl start sockserv
root@ubuntu-node:~# echo $?
0
root@ubuntu-node:~# systemctl status sockserv
● sockserv.service - Perl Socket Server Sample
     Loaded: loaded (/usr/local/lib/systemd/system/sockserv.service; disabled; vendor preset: enabled)
     Active: failed (Result: exit-code) since Sat 2021-04-10 11:39:21 UTC; 6s ago
    Process: 198244 ExecStart=/usr/local/tcp-probe-example/src/sockserv.pl (code=exited, status=203/EXEC)
   Main PID: 198244 (code=exited, status=203/EXEC)

Apr 10 11:39:21 ubuntu-node systemd[1]: Started Perl Socket Server Sample.
Apr 10 11:39:21 ubuntu-node systemd[1]: sockserv.service: Main process exited, code=exited, status=203/EXEC
Apr 10 11:39:21 ubuntu-node systemd[1]: sockserv.service: Failed with result 'exit-code'.

あら?

journalctl -xeで確認すると以下の通り。
スクリプトchmod 755してなかった笑

Apr 10 11:39:21 ubuntu-node systemd[1]: Started Perl Socket Server Sample.
-- Subject: A start job for unit sockserv.service has finished successfully
-- Defined-By: systemd
-- Support: http://www.ubuntu.com/support
-- 
-- A start job for unit sockserv.service has finished successfully.
-- 
-- The job identifier is 7009.
Apr 10 11:39:21 ubuntu-node systemd[198244]: sockserv.service: Failed to execute command: Permission denied
Apr 10 11:39:21 ubuntu-node systemd[198244]: sockserv.service: Failed at step EXEC spawning /usr/local/tcp-probe-example/src/sockserv.pl: Permission denied

修正

root@ubuntu-node:~# chmod 755 /usr/local/tcp-probe-example/src/sockserv.pl 

再実行

root@ubuntu-node:~# systemctl start sockserv
root@ubuntu-node:~# echo $?
0
root@ubuntu-node:~# systemctl status sockserv
● sockserv.service - Perl Socket Server Sample
     Loaded: loaded (/usr/local/lib/systemd/system/sockserv.service; disabled; vendor preset: enabled)
     Active: active (running) since Sat 2021-04-10 11:42:29 UTC; 17s ago
   Main PID: 198358 (sockserv.pl)
      Tasks: 1 (limit: 4619)
     Memory: 3.0M
     CGroup: /system.slice/sockserv.service
             └─198358 /usr/bin/perl /usr/local/tcp-probe-example/src/sockserv.pl

Apr 10 11:42:29 ubuntu-node systemd[1]: Started Perl Socket Server Sample.
Apr 10 11:42:29 ubuntu-node sockserv.pl[198358]: [DBG] server listening ... 0.0.0.0:8080
root@ubuntu-node:~# curl http://localhost:8080
Running Socket Server.
root@ubuntu-node:~# cat /var/run/sockserv.pid 
198358

systemctl stop sockservすることでこのプロセスIDを見てkillしてくれる。
停止時はこのファイルは削除される。

ちなみにPIDFileの指定が無い場合でも、systemctlが把握してるPIDは面倒みてくれるっぽくてちゃんと停止する模様。
今回はsimpleなので不要だったけど、forkingタイプだと、無いとkillされないかも。(未確認)

enable

デフォルトでは以下の通り。

root@ubuntu-node:~# systemctl is-enabled sockserv
disabled

OSブート時にも起動するにはenableを使う。

root@ubuntu-node:~# systemctl enable sockserv
Created symlink /etc/systemd/system/multi-user.target.wants/sockserv.service → /usr/local/lib/systemd/system/sockserv.service.

この通り、/etc/systemd/system/multi-user.target.wants/sockserv.serviceにsymlinkが作成される。

daemon-reload

動作に影響しないけど、ユニットファイルのDescriptionの内容をちょっと変更してみる。
(末尾に!追加)

[Unit]
Description=Perl Socket Server Sample!

この状態で、systemctl status sockservを実行すると、

root@ubuntu-node:~# systemctl status sockserv
Warning: The unit file, source configuration file or drop-ins of sockserv.service changed on disk. Run 'systemctl daemon-reload' to reload units.
● sockserv.service - Perl Socket Server Sample
     Loaded: loaded (/usr/local/lib/systemd/system/sockserv.service; enabled; vendor preset: enabled)

:
:

このように「ディスク上のunitファイルの内容が更新されとるで」と、systemctl daemon-reloadするように警告が表示さる。

root@ubuntu-node:~# systemctl daemon-reload 
root@ubuntu-node:~# systemctl status sockserv
● sockserv.service - Perl Socket Server Sample!
     Loaded: loaded (/usr/local/lib/systemd/system/sockserv.service; enabled; vendor preset: enabled)
:
:

更新されました。


ということで、自前のプログラムを自作のユニットファイルでdaemonとして動作させることができた。

というか実は、このユニットファイルのパスがどこかを知れたのが今回の最大の収穫だった。
今までなんとなく/etc/systemd/system/multi-user.target.wants/にあるファイルをチェックしてたけど、これはenableになってる分だけで、実体は(基本的に)/lib/systemd/system/にあって、他の場所もman systemd.unitで検索パスのリストを確認できる。

VyOSをインストールしてIPアドレスとsshの初期設定

OSSで開発されているネットワークOS(Wikipediaの説明より)であるVyOSを使って、仮想環境上にルーターを構築してみる。
VyOSはDebianベースで動作するOSで、VMにインストールすることができる。

vyos.io

ちなみにLinux(CentOS)をルーターにするには以下参照。

zaki-hmkc.hatenablog.com

社内のハンズオンイベントで扱ったりひよこ本にも載ってたりしててVyOS触っておきたいなーと思いつつタスク積んでいたけど、Linuxにスタティックルートの設定をする際にこれあった方が絶対によさそうだったのでようやく自宅ラボに導入しようとしてます。。
ちなみに本記事ではVyOSのイメージの入手とOSインストール、OSインストール後のIPアドレス設定と外部からssh接続できるようにする設定までです。

Rolling Release版のVyOS イメージの入手

f:id:zaki-hmkc:20210408094742p:plain

「Rolling Release」であればISOのダウンロードページにすぐたどり着く。

なんだけど、初めてなのでLTS(2021.04.07時点でv1.2.7)を使いたい。

vyos.io

なんだけど、ここを見ると全部有償版に見える。。。
ハテ…と思ってドキュメントのinstallationのページを見てみると、

docs.vyos.io

登録すればダウンロードできるっぽい?

Registered subscribers can log into https://support.vyos.io/ to have access to a variety of different downloads via the “Downloads” link. These downloads include LTS (Long-Term-Support) and associated hot-fix releases, early public access releases, pre-built VM images, as well as device specific installation ISOs.

というわけで、ここからユーザー登録。

support.vyos.io

・・・してみたけど、やっぱりLTSのダウンロードページを見つけられない。

ということであきらめて、現在の最新版を使ってみる。
最初にも書いたけど「Rolling Release」->「Downloading Rolling Release」で1.4のISOファイルダウンロードページにたどり着く。

downloads.vyos.io

「latest」をダウンロードするとあとからわからなくなるので、2021.04.07時点で最新の「vyos-1.4-rolling-202104061641-amd64.iso」をゲット。(中身はlatest)

ちなみにOVAファイルも配布されてたけど、1.1.8で妙に古かったのでスルー。

VMの作成

インストールのドキュメントは以下。

docs.vyos.io

The minimum system requirements are 512 MiB RAM and 2 GiB storage. Depending on your use you might need additional RAM and CPU resources e.g. when having multiple BGP full tables in your system.

マシンスペックは掲載されてるけど、ESXiで作る際の「ゲストOSのバージョン」はいまいち。ベースはDebianだけどバージョンがわからず。
あとで変更できるし、とりあえず「Debian GNU/Linux 10 (64bit)」で作成。 (ちなみに以下の通り10.9なので最新版のイメージであれば選択するDebianバージョンも最新でたぶん大丈夫)

vyos@vyos:~$ cat /etc/debian_version 
10.9

今回はルーターとして動かしたいのでNICを2個セット。
マシンスペックはそんなに必要ないみたいだけど、一応こんな感じ。

f:id:zaki-hmkc:20210408095328p:plain

install

CDブートするとLive Linux状態で使用可能になる。
VMにインストールするには「Permanent installation」の項を参照

docs.vyos.io

電源投入するとログイン待ち状態になるので、ユーザー名・パスワードはどちらもvyosを入力しコンソールログインする。

インストールするにはinstall imageを実行。
聞かれる質問は以下の通り。基本デフォルトで問題ない。

  • Would you like to continue? (Yes/No) [Yes]:
  • Partition (Auto/Parted/Skip) [Auto]:
  • Install the image on? [sda]:
  • This will destroy all data on /dev/sda. Continue? (Yes/No) [No]: Yes
  • How big of a root partition should I create? (2000MB - 4294MB) [4294]MB:

これでCreating filesystem on /dev/sda1 と表示されパーティションが作成される。

  • What would you like to name this image? [1.4-rolling-202104061641]:
  • Which one should I copy to sda? [/opt/vyatta/etc/config/config.boot]:
  • Enter password for user 'vyos': <管理ユーザー"vyos"のパスワードを入力する>
  • Retype password for user 'vyos':
  • Which drive should GRUB modify the boot partition on? [sda]:

Setting up grub: と出てOKと表示されれば処理完了。
poweroffしてシャットダウンし、isoファイルをVMから取り外す。

設定

ディスクから起動したらvyosユーザーでログイン。
主要な設定はQuick Startを参照。

docs.vyos.io

IPアドレス設定

NICを2個乗せているので、ip aで2つあることを確認(このコマンドでいいんだっけ 後述)し、eth0eth1がそれぞれVMのどのNICかをESXiのネットワークアダプタMACアドレスを見て確認する。

configureを実行して設定モードに変更。

vyos@vyos:~$ configure
[edit]
vyos@vyos# 

これはプロンプトが以下の2行になるって感じかな。。

[edit]
vyos@vyos#

で、eth0のアドレス設定。
172.16.1.3でサブネットは/23
DHCPはオフになってるので特にdeleteの必要はない。

set interfaces ethernet eth0 address 172.16.1.3/23

eth1は別途設定するので未設定のまま残しておく。

setを実行したらshowで内容を確認できる。
問題無ければ、commitで適用し、saveで保存する。

(commitを実行した時点で設定が反映されるので他のホストからpingなどで反応できる。ただしsaveしないとrebootで設定は消える)

設定確認

最初ip aを使って確認してた…

vyos@vyos:~$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 fe80::200:ff:fe00:0/64 scope link 
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:0c:29:3b:48:8c brd ff:ff:ff:ff:ff:ff
    inet 172.16.1.3/23 brd 172.16.1.255 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::20c:29ff:fe3b:488c/64 scope link 
       valid_lft forever preferred_lft forever
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:0c:29:3b:48:96 brd ff:ff:ff:ff:ff:ff
    inet6 fe80::20c:29ff:fe3b:4896/64 scope link 
       valid_lft forever preferred_lft forever

けどここはネットワークOSらしく、show interfacesを使おう。

vyos@vyos:~$ show interfaces 
Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down
Interface        IP Address                        S/L  Description
---------        ----------                        ---  -----------
eth0             172.16.1.3/23                     u/u  
eth1             -                                 u/u  
lo               127.0.0.1/8                       u/u  
                 ::1/128     

sshサーバー

以降の作業用にsshサーバーを有効にする。
IPアドレスと同様configureで設定モードで実行する。

vyos@vyos:~$ configure
[edit]
vyos@vyos# 
set service ssh

これを実行し、IPアドレスの設定と同様にcommitsaveすれば、他のホストからsshで接続できるようになる。

zaki@ubuntu-node:~$ ssh vyos@172.16.1.3
The authenticity of host '172.16.1.3 (172.16.1.3)' can't be established.
ECDSA key fingerprint is SHA256:O03Yp97VsV5iOpw+BrW/tUvQH6l3ritn1+njB8rR9CE.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '172.16.1.3' (ECDSA) to the list of known hosts.
Welcome to VyOS

vyos@172.16.1.3's password: 
Linux vyos 5.10.27-amd64-vyos #1 SMP Sun Apr 4 22:05:07 UTC 2021 x86_64

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
vyos@vyos:~$ 

というわけで、VyOSをインストールしてeth0のみアドレス設定完了。
現在の構成はこんな感じ。

f:id:zaki-hmkc:20210412080027p:plain

ルーターの設定などは次回で。。

[Ansible] asyncとpollを使った非同期処理とループの並列実行

Ansibleのループ処理はデフォルトでは逐次処理で動作します。
例えばループの回数が5回で、1回の処理に10秒ずつかかる場合は合計50秒はかかる計算になります。

Ansibleではループによって複数回実行される処理がお互いに干渉せずに並列に実行可能なのであれば、asyncpollの指定で並列に処理することができます。
1回10秒(x5)の処理を並行で同時に実行することで10秒で完了する計算になります。

docs.ansible.com

デフォルト(逐次処理)の動作

まずは普通の逐次処理されるループ処理のplaybookです。

---
- hosts: localhost
  gather_facts: false

  tasks:
    - name: clear log
      file:
        path: async.log
        state: absent

    - name: loop sample
      shell: echo "$(date +%Y.%m.%d_%H.%M.%S); {{ item }} begin" >> async.log; sleep "{{ item }}"; echo "$(date +%Y.%m.%d_%H.%M.%S); {{ item }} end" >> async.log
      loop:
        - 2
        - 3
        - 4

ごちゃごちゃ書いてるけど、shellでやってるのは以下の3つのコマンドを実行している。

  • 現在日時とsleepする時間とbeginをファイル書き込み
  • 指定秒数sleep
  • 現在日時とsleepした時間とendをファイル書き込み

loopを使って[2, 3, 4]のリストを渡すことで、「2秒スリープ」「3秒スリープ」「4秒スリープ」が順番に処理されることを出力されるファイルで確認できます。
出力されるファイルはこんな感じ。

2021.04.06_20.06.50; 2 begin
2021.04.06_20.06.52; 2 end
2021.04.06_20.06.53; 3 begin
2021.04.06_20.06.56; 3 end
2021.04.06_20.06.56; 4 begin
2021.04.06_20.07.00; 4 end

shellで実行される処理が並行して実行されていないことが確認できました。

今回は、このスリープ処理を同時に実行させてみます。

並列処理

docs.ansible.com

Ansibleの並列処理に必要な非同期の処理は基本全部ここにのってる。はず。
ちなみにループの場合の応用もこのページに載ってます。

asyncとpollによる非同期処理

今回使うのはasyncpoll。どちらも整数(秒)を引数に取ります。

async

asyncを単体で使用すると、「指定秒数以内にtaskが完了しない場合はエラー」となります。

    - name: async sample
      shell: echo "start" >> async-poll.log; sleep 10; echo "end" >> async-poll.log
      async: 4

これを実行すると、sleep 10の最中にasync: 4によるタイムアウトが発動し、以下のようにエラーとなります。

fatal: [localhost]: FAILED! => changed=false 
  msg: async task did not complete within the requested time - 4s

async + poll

pollの指定が無い場合は、デフォルト値は15のため、15秒経ってエラーとして動作します。
なので、asyncに指定した値より大きい値はあまり意味がない(と思う)ので、おそらくasyncの秒数の約数にしておくとベターと思う。

    - name: async sample
      shell: echo "start" >> async-poll.log; sleep 10; echo "end" >> async-poll.log
      async: 4
      poll: 1

このようにpoll: 1にしておくと、実行しているモジュールが完了するかasyncで指定した4秒が経過するかを、pollに指定した1秒毎にチェックする動作になります。

poll:0

poll: 0は特殊で、asyncで指定しているtaskを非同期で処理しつつ、その完了を待たずに次のtaskへ処理が進むという動作になります。

    - name: async sample
      shell: echo "start" >> async-poll.log; sleep 10; echo "end" >> async-poll.log
      async: 11
      poll: 0
      # shellで実行している処理がタイムアウトしないように`async`の値は11秒に変更

上記のtask定義でansible-playbookを実行すると、shellの完了を待たずに非同期で処理が継続され、taskが完了します。
また、他にtaskがなければansible-playbookの実行は終了しシェルに処理が戻りますが、バックグラウンドで非同期処理用のプロセスが動作し続けます。

ansible-playbookは終了しても処理が実行してる最中にps auxfwするとこんな感じ。

zaki     110284  0.0  0.1 215616 11040 ?        S    21:08   0:00 /home/zaki/src/ansible-sample/venv/a2.10/bin/python3 /home/zaki/.a
zaki     110285  0.0  0.1 215616 11412 ?        S    21:08   0:00  \_ /home/zaki/src/ansible-sample/venv/a2.10/bin/python3 /home/zak
zaki     110286  5.0  0.1 207000 14188 ?        S    21:08   0:00      \_ /home/zaki/src/ansible-sample/venv/a2.10/bin/python3 /home
zaki     110298  0.0  0.0 113288  1420 ?        S    21:08   0:00          \_ /bin/sh -c echo "start" >> async-poll.log; sleep 10; e
zaki     110299  0.0  0.0 108056   356 ?        S    21:08   0:00              \_ sleep 10

ループ処理に応用

ここまでの async + poll:0 設定を踏まえて、ループ処理とコンボすることで、ループで実行される各処理を逐次処理でなく並列に同時に実行することができます。

    - name: async loop
      shell: echo "{{ item }} begin" >> async.log; sleep "{{ item }}"; echo "{{ item }} end" >> async.log
      loop:
        - 2
        - 3
        - 4
      async: 5
      poll: 0

冒頭の逐次処理を行っていたtaskにasync+pollを追加。
これを実行した結果出力されるasync.logファイルは以下の通り。

2021.04.06_21.12.22; 2 begin
2021.04.06_21.12.22; 3 begin
2021.04.06_21.12.22; 4 begin
2021.04.06_21.12.24; 2 end
2021.04.06_21.12.25; 3 end
2021.04.06_21.12.26; 4 end

beginが同時に書き込まれ、endは各時間のスリープが終わったあとに書き込まれることを確認でき、ループ内の各処理が並列に処理されています。

非同期で実行した処理を待つ

asyncpoll:0の指定で並列処理できるようになったけど、実際のplaybookでは「並列処理したtaskの結果がどうだったか」というのを以降の処理を実行する前に結果を確認したうえで次の処理を行うことが多いと思います。
Ansibleのasyncを使った非同期処理はその結果を保持した上で、ansible.builtin.async_statusモジュールを使ってwaitできます。

docs.ansible.com

    # ループの並列処理
    - name: async loop
      shell: echo "$(date +%Y.%m.%d_%H.%M.%S); {{ item }} begin" >> async.log; sleep "{{ item }}"; echo "$(date +%Y.%m.%d_%H.%M.%S); {{ item }} end" >> async.log
      loop:
        - 2
        - 3
        - 4
      async: 1
      poll: 0
      register: result_async

    # 並列処理の完了を待つ
    - name: wait
      ansible.builtin.async_status:
        jid: "{{ item.ansible_job_id }}"
      loop: "{{ result_async.results }}"
      register: async_poll_results
      until: async_poll_results.finished
      retries: 10
      delay: 1

並列ループ処理はregisterを使って結果を保持。
そしてその結果にはansible_job_idという値が含まれており、これをjid(Job identifier)パラメタに指定します。

このときに「ループとregisterを組み合わせた場合はresultsにリスト形式で各ループごとの結果が保持される」という動作に合わせて、async_statusモジュールを使ったJob identifierの指定もloopディレクティブと{{ item.ansible_job_id }}を使ってループごとに指定します。

zaki-hmkc.hatenablog.com

そしてこのtaskは、前の並列ループ処理が非同期で処理される結果、まだ未完了の状態で起動されるため、「並列で動作している非同期処理がすべて完了するまで」リトライし続けるように記述します。
これは、このtaskの結果をregisterで保持し、その保持した結果をuntilretriesを使って指定の状態(registerで保持した辞書型の変数のfinishedの値)が真になるまで繰り返し実行します。

非同期処理を待つplaybook自体は以下のエントリを参考。

zaki-hmkc.hatenablog.com

これで、並列実行のループ処理を実現しつつ、後続のtaskではその結果を待ったうえで処理を続けることができます。

環境

確認した環境は以下の通り。

ansible 2.10.5
  config file = /home/zaki/.ansible.cfg
  configured module search path = ['/home/zaki/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/zaki/src/ansible-sample/venv/a2.10/lib64/python3.6/site-packages/ansible
  executable location = /home/zaki/src/ansible-sample/venv/a2.10/bin/ansible
  python version = 3.6.8 (default, Nov 16 2020, 16:55:22) [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]

[Ansible] loopとモジュール実行の結果を保持するregisterを併用する

registerを使うことでモジュールの実行結果(Return Values)を保持し、後続のタスクでその値を参照することができるので、処理の実行結果を次のタスクのパラメタにすることができます。
ただし、その結果のデータ構造がループでの実行時は通常とは異なるのでそれについての覚え書き。

通常時のサンプル

指定コマンドを実行するplaybookです。

---
- hosts: localhost
  gather_facts: false

  tasks:
    # コマンド実行
    - name: exec command
      ansible.builtin.command: hostname
      register: result_command

    - name: print result
      debug:
        msg: '{{ result_command }}'

1つ目のtaskでコマンド実行しつつ、その結果を2つ目のtaskで出力。内容は以下の通り。
commandのReturn Valueについてはドキュメントのこちら

ok: [localhost] => 
  msg:
    changed: true
    cmd:
    - hostname
    delta: '0:00:00.001591'
    end: '2021-04-06 18:53:48.144296'
    failed: false
    rc: 0
    start: '2021-04-06 18:53:48.142705'
    stderr: ''
    stderr_lines: []
    stdout: cloud-dev
    stdout_lines:
    - cloud-dev

この通り、モジュールの実行結果をregisterで指定した名称の変数にセットでます。
commandの場合は、コマンドの戻り値がrc・標準出力がstdoutに保持されます、ディクショナリ型データになっているのでキーを指定して参照できます。

ループ併用時

loopを使って以下の3つのコマンドを実行するように変更。

  • hostname
  • pwd
  • ls /opt
    - name: exec command
      ansible.builtin.command: "{{ item }}"
      loop:
        - hostname
        - pwd
        - ls /opt
      register: result_command

    - name: print result
      debug:
        msg: '{{ result_command }}'

この時の実行結果は以下の通り。
(長いので一部省略)

ok: [localhost] => 
  msg:
    changed: true
    msg: All items completed
    results:
    - ansible_loop_var: item
      changed: true
      cmd:
      - hostname
      delta: '0:00:00.001672'
      end: '2021-04-06 18:54:28.461799'
      failed: false
      invocation:
        module_args:
          ...
      item: hostname
      rc: 0
      start: '2021-04-06 18:54:28.460127'
      stderr: ''
      stderr_lines: []
      stdout: cloud-dev
      stdout_lines:
      - cloud-dev
    - ansible_loop_var: item
      changed: true
      cmd:
      - pwd
      delta: '0:00:00.001590'
      end: '2021-04-06 18:54:28.630725'
      failed: false
      invocation:
        module_args:
          ...
      item: pwd
      rc: 0
      start: '2021-04-06 18:54:28.629135'
      stderr: ''
      stderr_lines: []
      stdout: /home/zaki/src/ansible-sample/file
      stdout_lines:
      - /home/zaki/src/ansible-sample/file
    - ansible_loop_var: item
      changed: true
      cmd:
      - ls
      - /opt
      delta: '0:00:00.001935'
      end: '2021-04-06 18:54:28.798976'
      failed: false
      invocation:
        module_args:
          ...
      item: ls /opt
      rc: 0
      start: '2021-04-06 18:54:28.797041'
      stderr: ''
      stderr_lines: []
      stdout: |-
        cni
        containerd
      stdout_lines:
      - cni
      - containerd

loopwith_itemsなどのループとの併用時は、各ループ毎の実行結果はresultsというキー以下にリスト型でセットされるようにデータ構造が変化します。

該当ドキュメント

ループのドキュメントに載っています。

docs.ansible.com

When you use register with a loop, the data structure placed in the variable will contain a results attribute that is a list of all responses from the module. This differs from the data structure returned when using register without a loop:

環境

ansible 2.10.5
  config file = /home/zaki/src/ansible-sample/file/ansible.cfg
  configured module search path = ['/home/zaki/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/zaki/src/ansible-sample/venv/a2.10/lib64/python3.6/site-packages/ansible
  executable location = /home/zaki/src/ansible-sample/venv/a2.10/bin/ansible
  python version = 3.6.8 (default, Nov 16 2020, 16:55:22) [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]

ちょっと古いな…

Return Valueをカジュアルに確認したい場合

個人的には普段はansible-playbook実行時に-vを付けて実行してます。
verboseレベル1でモジュールの実行結果は標準出力へprintされるので、開発中は基本的にこのレベルで作業してます。

    - name: exec command
      ansible.builtin.command: hostname

このtaskを-v無しで実行してもchangedしか出力されないですが、-vを付けると以下のようにcommandの結果が出力されます。

changed: [localhost] => changed=true 
  cmd:
  - hostname
  delta: '0:00:00.001589'
  end: '2021-04-06 18:58:07.337019'
  rc: 0
  start: '2021-04-06 18:58:07.335430'
  stderr: ''
  stderr_lines: <omitted>
  stdout: cloud-dev
  stdout_lines: <omitted>