zaki work log

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

[Terraform] countまたはfor_eachを使った複数リソース作成

countを使うことで簡単に同じリソースを複数作成できる。

learn.hashicorp.com

元ネタは以下で作成した定義ファイル。

zaki-hmkc.hatenablog.com

countを使った個数指定

www.terraform.io

例えばn人分の環境を作りたい場合で、とにかく数を指定してその個数のリソースを作るというパターン。

countの指定

resource "aws_instance" "bastion" {
  count  = 3

  ami                         = data.aws_ssm_parameter.amzn2_ami.value
  instance_type               = "t3.nano"
  key_name                    = aws_key_pair.my_key.id
  subnet_id                   = aws_subnet.prac_public.id
  security_groups             = [aws_security_group.allow_ssh_icmp.id]
  associate_public_ip_address = true

  tags = {
    Name = "bastion"
  }
}

count = 3 を追加している。

[zaki@cloud-dev practice (main)]$ ssh ec2-user@**.**.**.** -i ~/.ssh/id_aws_terraform 
The authenticity of host '**.**.**.** (**.**.**.**)' can't be established.
ECDSA key fingerprint is SHA256:lWRWcx/vBbr3QHTyJf10zxImxSfxY0l1V41k/u/wrqM.
ECDSA key fingerprint is MD5:7f:bd:48:2f:44:3a:8c:c3:cf:e3:6b:88:63:da:0e:c6.
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/
[ec2-user@ip-172-25-10-145 ~]$ 

ログインもちゃんとできる。

ただ、一覧で見てもわかるとおり、全て同じtag名になっている。

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

インデックス値参照

count.indexでいわゆるループ内のインデックス値を参照できる。
これをtag名に付与してやればOK。

  tags = {
    Name = "bastion-${count.index}"
  }

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

これで連番をsuffixに付けることができた。

for_eachを使ったリスト指定

www.terraform.io

作成するリソースは同じだけど、用途が異なるためそれぞれ別の名前を付けたい、などのパターン。
例として「app」と「db」というEC2を作る場合。

リスト定義

ホスト一覧として、appdbという文字列型のリスト(配列)を定義。

variable "host_list" {
    description = "host count"
    type        = list(string)
    default     = [
        "app",
        "db",
    ]
}

for_eachの指定

resource "aws_instance" "server" {
  for_each = toset(var.host_list)

  ami                         = data.aws_ssm_parameter.amzn2_ami.value
  instance_type               = "t3.nano"
  key_name                    = aws_key_pair.my_key.id
  subnet_id                   = aws_subnet.prac_priv2.id
  security_groups             = [aws_security_group.allow_ssh_icmp.id]
  associate_public_ip_address = false

  tags = {
    Name = each.value
  }
}

countと同じような要領で、for_eachを使ってホスト一覧を指定する。
for_eachを使うことで、リストの要素ごとにリソースが作成される。

また、それぞれホスト毎の名前を指定したいtag名については、each.valueを指定することで要素の内容に置き換わる。

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

残課題

「count数の環境ごとにfor_eachの要素を作成 (bastion/app/dbのセットをcount数分作る)」をやりたくて、

resource "aws_instance" "server" {
  for_each = toset(var.host_list)
  count = var.host_count

  ami                         = data.aws_ssm_parameter.amzn2_ami.value
  instance_type               = "t3.nano"
  key_name                    = aws_key_pair.my_key.id
  subnet_id                   = aws_subnet.prac_priv2.id
  security_groups             = [aws_security_group.allow_ssh_icmp.id]
  associate_public_ip_address = false

  tags = {
    Name = "${each.value}-${count.index}"
  }
}

みたいに書いてみたけど、Terraformはcountfor_eachを併用できなかった。

[zaki@cloud-dev practice (main)]$ terraform plan
╷
│ Error: Invalid combination of "count" and "for_each"
│ 
│   on ec2.tf line 26, in resource "aws_instance" "server":
│   26:   for_each = toset(var.host_list)
│ 
│ The "count" and "for_each" meta-arguments are mutually-exclusive, only one should be used to be explicit about the number of resources to be
│ created.
╵

これはまた別のやり方で作る必要がある(未調査)

つかいわけ

同じサイズのコンピュートインスタンスを指定数分だけ並べたい程度であればcountでよさそうだが、たとえばサブネットの定義みたいに、CIDRとかのパラメタが異なるものを複数作る必要がある場合は、ListやMapで定義してfor_eachを使う方がよさそう。

[Terraform] 変数を定義・参照する

ハードコーディングしていた変数を別ファイルに定義してみる。

記事中のCIDRが172.26.だったり172.25.だったりしてるのはあまり意味なくて例示用。

元の定義ファイル

zaki-hmkc.hatenablog.com

上記では、こんな内容のVPCを定義したTerraformのファイルを作成していた。

resource "aws_vpc" "practice" {
  cidr_block = "172.26.0.0/16"
  enable_dns_hostnames = true

  tags = {
    Name = "vpc-aws-study-example"
  }
}

なのですが、VPCに設定している、"172.26.0.0/16"というネットワークのCIDRは定義内に直接指定するより、変数ファイルに記述した方が再利用性高いよね、ということで変数を定義してみる。

variableで変数定義

learn.hashicorp.com

拡張子が.tfであれば(変数定義とか関係なくTerraformの定義ファイルとして)実行時に読み込まれるので特にファイル名は何でもよいが、ここではvariables.tfで以下の内容のファイルを作成。
定義ファイル内でvariableを使用することで変数定義ができる。

書式はvariable "変数名" { ... }

variable "vpc_cidr_block" {
    description = "CIDR Block of VPC"
    type        = string
    default     = "172.26.0.0/16"
}

変数を参照するVPCの定義部分は以下の通り。

resource "aws_vpc" "practice" {
  cidr_block = var.vpc_cidr_block
  enable_dns_hostnames = true

  tags = {
    Name = "vpc-aws-study-example"
  }
}

これでplan実行すると、

[zaki@cloud-dev practice (main)]$ terraform plan
:
:
  # aws_vpc.practice will be created
  + resource "aws_vpc" "practice" {
      + arn                              = (known after apply)
      + assign_generated_ipv6_cidr_block = false
      + cidr_block                       = "172.26.0.0/16"
      + default_network_acl_id           = (known after apply)
      + default_route_table_id           = (known after apply)
      + default_security_group_id        = (known after apply)
:
:

今回はvariables.tfという変数定義用のファイルを別途作成したが、定義場所は別にどこでもよいので、変数を参照する同じファイルの先頭とかでも問題ない。

変数のセット

terraform.tfvarsで変数セット

variableに「デフォルト値」を定義できるけど、このデフォルト値を上書きする変数定義ファイルを別途使うこともできる。
ファイル名はterraform.tfvars固定。
(※ オプションでファイル名指定可能)

以下参照。
Assign values with a terraform.tfvars file

あくまでvariableを使って変数定義を行った変数に対して、指定の値をセットするというものなので、variableで定義していない変数は扱えないので注意。

追加で以下のvariableを定義。

variable "prac_public_cidr_block" {
    description = "CIDR Block of Public Subnet"
    type        = string
    default     = "172.26.10.0/24"
}
variable "prac_private1_cidr_block" {
    description = "CIDR Block of Private1 Subnet"
    type        = string
    default     = "172.26.20.0/24"
}
variable "prac_private2_cidr_block" {
    description = "CIDR Block of Private2 Subnet"
    type        = string
    default     = "172.26.30.0/24"
}

サブネットの変数の参照箇所は以下の通り。

resource "aws_subnet" "prac_public" {
  vpc_id            = aws_vpc.practice.id
  cidr_block        = var.prac_public_cidr_block
  availability_zone = "ap-northeast-1a"

  tags = {
    Name = "public-subnet-aws-study"
  }
}

で、terraform.tfvarsには以下のように変数セット。

vpc_cidr_block = "172.25.0.0/16"
prac_public_cidr_block = "172.25.10.0/24"

実行すると以下の通り。

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

:
:

  # aws_subnet.prac_public will be created
  + resource "aws_subnet" "prac_public" {
      + arn                             = (known after apply)
      + assign_ipv6_address_on_creation = false
      + availability_zone               = "ap-northeast-1a"
      + availability_zone_id            = (known after apply)
      + cidr_block                      = "172.25.10.0/24"

:
:

  # aws_vpc.practice will be created
  + resource "aws_vpc" "practice" {
      + arn                              = (known after apply)
      + assign_generated_ipv6_cidr_block = false
      + cidr_block                       = "172.25.0.0/16"
      + default_network_acl_id           = (known after apply)
      + default_route_table_id           = (known after apply)

コマンドライン引数でセット

以下参照。
Variables on the Command Line

terraform.tfvarsファイル以外に、-varオプションで変数セットもできる。
Ansibleでいうextra varsみたいな感じかな?

$ terraform plan -var='prac_public_cidr_block=172.26.60.0/24'

:
:

  # aws_subnet.prac_public will be created
  + resource "aws_subnet" "prac_public" {
      + arn                             = (known after apply)
      + assign_ipv6_address_on_creation = false
      + availability_zone               = "ap-northeast-1a"
      + availability_zone_id            = (known after apply)
      + cidr_block                      = "172.26.60.0/24"
      + id                              = (known after apply)

:
:

こんな感じ。

環境変数でセット

以下参照。
Set values with environment variables

環境変数も使用可能。
その場合、環境変数名はTF_VAR_をprefixに付与しておけば、そのあとの変数名をTerraform定義ファイル内で参照できる。

[zaki@cloud-dev practice (main)]$ export TF_VAR_prac_private2_cidr_block="172.26.120.0/24"
[zaki@cloud-dev practice (main)]$ terraform plan

:
:

  # aws_subnet.prac_priv2 will be created
  + resource "aws_subnet" "prac_priv2" {
      + arn                             = (known after apply)
      + assign_ipv6_address_on_creation = false
      + availability_zone               = "ap-northeast-1c"
      + availability_zone_id            = (known after apply)
      + cidr_block                      = "172.26.120.0/24"
      + id                              = (known after apply)
      + ipv6_cidr_block_association_id  = (known after apply)

変数参照の優先順位

以下参照。
Variable Definition Precedence

  1. 環境変数
  2. terraform.tfvarsファイル
  3. terraform.tfvars.jsonファイル
  4. *.auto.tfvarsファイルか*.auto.tfvars.jsonファイルを辞書順
  5. -var-var-fileによる指定

環境変数が一番弱く、設定されていてもterraform.tfvarsファイルがあれば上書きされる。

-varはやっぱAnsibleのextra varsと同じって認識でよさげね。

環境

[zaki@cloud-dev practice (main)]$ terraform -version
Terraform v0.15.1
on linux_amd64
+ provider registry.terraform.io/hashicorp/aws v3.37.0

サンプルコード

github.com

フライパンでご飯を炊く (炊飯器持ってないけど米を買った)

うちって炊飯器が無いのでご飯つぶを食べたいときは近所の弁当屋さんでライス単品を買ってきてたんだけど、フライパンで米炊けると聞いて試してみました。

環境

  • ガスコンロ (1ノード)
  • フライパン (20cm)
  • フライパン用の蓋 (蒸気穴あったほうが良さそうなイメージだけど無いタイプ)
  • ボウルかザル (米を研ぐときに使用)

蓋はガラスで中が見えるヤツじゃないと難易度上がるかも。

米と水の量

米1合に対して水どれだけ?ってやると、まず米1合の量を計る必要があった。(うちにあるカップは200cc用しかない)
2回試した結果、米の量1に対して水の量1.2にすればだいたい丁度よかったので、米が何合かどうかはあまり気にしないでいいかも。

手順

以下は米200ccの場合。
蓋は最後までしたまま。

  1. 米を研いで水気を切る
  2. 米と水をフライパンへ投入
  3. 30min浸水させる
  4. 蓋をして沸騰するまで強火で加熱
  5. 沸騰したら弱火で5min
  6. 30secくらい一度強火にするとチリチリ音がしだすので火を止める (これの効果はわかってない)
  7. 火を止めたら60secほどsleep

成果物

昼 (1回戦)

夜 (2回戦)

※ 夕食は昼に炊いたご飯を食べたので、2回戦は明日食べる分です笑

参考文献

www.kurashiru.com

oceans-nadia.com

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で検索パスのリストを確認できる。