hana_shinのLinux技術ブログ

Linuxの技術情報を掲載しています。特にネットワークをメインに掲載していきます。

コンテナ起動時、コンテナ間通信の処理概要(Podman編)

1 はじめに

コンテナ(test1、test2)を起動すると、ブリッジ(podman0)とveth(Virtual Ethernet)デバイスのペアが作成されます。vethデバイスの一方はコンテナに、もう一方はブリッジに接続されます。そして、test1からtest2へのpingを実行すると、 ICMP Echo Request パケットがブリッジを経由してtest2に到達します。次に、ICMP Echo Reply パケットが逆の経路を通ってtest1に戻ります。
この記事では、次の点を確認します。
・コンテナ起動時のブリッジデバイス、vethデバイスの生成過程
・ICMP echoパケットがやり取りされる際の処理概要

+------------+                    +------------+
|    test1   |                    |   test2    |
|            |                    |            |
+--- eth0 ---+                    +--- eth0 ---+
      |                                 |
      |                                 |
      | |                               | |
      | | ICMP Echo Request             | |ICMP Echo Reply
      | V                               | V
      |                                 |
      |                                 |
+--- veth0 --------------------------- veth1 ---+
|                                               |
|                    podman0                    |
|                                               |
+-----------------------------------------------+

2 検証環境

ホストのAlmaLinux版数は以下のとおりです。

[root@server ~]# cat /etc/redhat-release
AlmaLinux release 9.2 (Turquoise Kodkod)

カーネル版数は以下のとおりです。

[root@server ~]# uname -r
5.14.0-284.11.1.el9_2.x86_64

3 コンテナ起動時の処理概要

以下のコマンドを実行して、コンテナを起動します。なお、podmanコマンドの使い方は、podmanコマンドの使い方 - hana_shinのLinux技術ブログを参照してください。

[root@server ~]# podman start test1

コンテナを起動すると、コンテナはsocketシステムコールを実行します。この時、socketシステムコールの第一引数にはAF_NETLINKを指定します。AF_NETLINKはNetlinkインタフェースを操作するためのパラメータで、ユーザ空間とカーネル空間で情報をやり取りする時に使用されます。具体的には、コンテナはNetlinkインタフェースを通じて、カーネルに対してデバイス(podman0,veth)の作成や、デバイスに対するパラメータ設定などを指示します。他には、ipコマンドも同様にNetlinkインタフェースを使用して、カーネル空間に対して値の読み書きをします。Netlinkインタフェースについては、Netlinkプログラミングの書き方 - hana_shinのLinux技術ブログを参照してください。

netlink_socket = socket(AF_NETLINK, socket_type, netlink_family);

以下のSystemtapスクリプトで、socketシステムコール実行時のバックトレースを確認してみます。

[root@server ~]# cat tp.stp
#!/usr/bin/stap
probe module("veth").function("veth_newlink@drivers/net/veth.c")
{
  printf("%s\n",print_backtrace())
}

SystemTapスクリプト内でvethモジュール内の関数を参照しているため、SystemTapスクリプトを実行する際には、vethカーネルモジュールがロードされていることを確認してください。

[root@server ~]# lsmod |grep veth
veth                   36864  0

Systemtapスクリプトを実行すると、以下のバックトレースが得られます。
最終的にveth_newlink関数が呼び出されています。この関数のバックトレースを表示するようにした理由は、ソースコードを読んで、vethデバイスのペア(例:veth0とeth0)がこの関数で作成されると考えたからです。なお、SystemTapの使い方は、SystemTapの使い方 - hana_shinのLinux技術ブログを参照してください。

[root@server ~]# stap -vg tp.stp -d kernel
-snip
 0xffffffffc0bc677c : veth_newlink+0x20c/0x3d0 [veth]
 0xffffffff9fcf9d2f : __rtnl_newlink+0x72f/0x9c0 [kernel]
 0xffffffff9fcfa004 : rtnl_newlink+0x44/0x70 [kernel]
 0xffffffff9fcf6dce : rtnetlink_rcv_msg+0x13e/0x390 [kernel]
 0xffffffff9fd6cf8e : netlink_rcv_skb+0x4e/0x100 [kernel]
 0xffffffff9fd6c60b : netlink_unicast+0x23b/0x360 [kernel]
 0xffffffff9fd6c968 : netlink_sendmsg+0x238/0x480 [kernel]
 0xffffffff9fcb9a7f : sock_sendmsg+0x5f/0x70 [kernel]
 0xffffffff9fcbb5e0 : __sys_sendto+0xf0/0x160 [kernel]
 0xffffffff9fcbb670 : __x64_sys_sendto+0x20/0x30 [kernel]
 0xffffffff9ff25159 : do_syscall_64+0x59/0x90 [kernel]
 0xffffffffa000009b : entry_SYSCALL_64_after_hwframe+0x63/0xcd [kernel]

次に、veth_newlink関数の第二引数devのデバイス名と、1761行目のローカル変数peerのデバイス名を確認してみます。

   1711 static int veth_newlink(struct net *src_net, struct net_device *dev,
   1712                         struct nlattr *tb[], struct nlattr *data[],
   1713                         struct netlink_ext_ack *extack)
   1714 {
   1715         int err;
   1716         struct net_device *peer;
   -snip-
   1761         peer = rtnl_create_link(net, ifname, name_assign_type,
   1762                                 &veth_link_ops, tbp, extack);
   1763         if (IS_ERR(peer)) {

バイス名以外にも、以下の情報を表示するSystemtapスクリプトを作成しました。

  • veth_newlinkを実行するコンテキスト(プロセス名または割り込みコンテキスト名)
  • プローブを設定した関数名(veth_newlink)
  • 第一引数devのデバイス
  • ローカル変数peerのデバイス
  • ローカル変数ifnameに設定されているデバイス
[root@server ~]# cat tp.stp
#!/usr/bin/stap
probe module("veth").statement("veth_newlink@drivers/net/veth.c:1763")
{
  printf("proc=%s, pp=%s, dev->name=%s, peer->name=%s, ifname=%s\n",
    task_execname(task_current()), pp(), kernel_string($dev->name), kernel_string($peer->name), kernel_string($ifname))
}

上記SystemTapを実行すると、以下の結果が得られました。
第一引数devのデバイス名が"veth%d"であることがわかります。vethの後に続く"%d"は後で数値(0, 1, 2, ...)に展開されます。また、ifnameはeth0であることがわかります。ifnameは、コンテナがNetlinkインタフェースを通してカーネルに作成指示をするデバイス名です。1761行目でifnameを引数にしてrtnl_create_link関数を実行しています。この関数は、net_device構造体(デバイスeth0の実体)をメモリから取得して、net_device構造体へのポインタ(peer)を返します。

[root@server ~]# stap -vg tp.stp -d kernel
-snip
proc=netavark, pp=module("veth").statement("veth_newlink@drivers/net/veth.c:1763"), dev->name=veth%d, peer->name=eth0, ifname=eth0

さらにソースを確認すると、1817行目以降でveth%dデバイスとeth0デバイスが相互に参照できるように、互いのpeerメンバにポインタを設定します。しかし、1823行目にSystemTapのプローブを設定しても実行されなかったため、eth0デバイスのpeerメンバからveth%dデバイスへのポインタはここでは設定されていないことがわかりました。この後どこかで設定されると思われますが、そこまでは調べていません。

   1711 static int veth_newlink(struct net *src_net, struct net_device *dev,
   1712                         struct nlattr *tb[], struct nlattr *data[],
   1713                         struct netlink_ext_ack *extack)
   1714 {
  -snip-
   1813         /*
   1814          * tie the deviced together
   1815          */
   1816
   1817         priv = netdev_priv(dev);
   1818         rcu_assign_pointer(priv->peer, peer);
   1819         err = veth_init_queues(dev, tb);
   1820         if (err)
   1821                 goto err_queues;
   1822
   1823         priv = netdev_priv(peer);
   1824         rcu_assign_pointer(priv->peer, dev);
   1825         err = veth_init_queues(peer, tb);

最終的にvethデバイスのペア(veth0、eth0)は、以下のように、お互いのpeerメンバを使って相互に参照するようになります。vethデバイスの一方のデバイスからパケットを送信すると、peerメンバを参照して、もう一方のデバイスの受信キューに送信パケットをリンクします。

             net_device                                 net_device
              (veth0)                                     (eth0)
         +------------------+ <-- dev     peer --> +------------------+
         |                  |      |        |      |                  |
         |                  |      |        |      |                  |
         |                  |      |        |      |                  |
         |                  |      |        |      |                  |
         |                  |      |        |      |                  |
         |                  |      |        |      |                  |
 netdev_priv(dev)           |      |        |      |                  | netdev_priv(peer) 
  |      |                  |      |        |      |                  |     |
  V      |                  |      |        |      |                  |     V
 priv--> +------------------+      |        |      +------------------+ <--priv
         |      peer    -----------|--------+      |       peer       |
         |                  |      +---------------|----              |
         +------------------+                      +------------------+
         |                  |                      |                  |
         |                  |                      |                  |
         +------------------+                      +------------------+

なお、図中のnetdev_privはインライン関数です。include/linux/netdevice.hに定義されています。net_device構造体のサイズをNETDEV_ALIGN(=32バイト)の境界にアライメントしたサイズを、net_device構造体の先頭アドレスに加えています。

static inline void *netdev_priv(const struct net_device *dev)
{
        return (char *)dev + ALIGN(sizeof(struct net_device), NETDEV_ALIGN);
}

次に、rtnl_create_link関数の呼び出し元(caller())を確認するため、以下のSystemTapスクリプトを作成します。

[root@server ~]# cat tp.stp
#!/usr/bin/stap
probe kernel.function("rtnl_create_link@net/core/rtnetlink.c")
{
  printf("proc=%s, pp=%s, func=%s, ifname=%s\n",
    task_execname(task_current()), pp(), caller(), kernel_string($ifname))
}

SystemTapスクリプトを実行すると、rtnl_create_link関数を呼び出す関数は以下の2つあることがわかりました。

呼び出し元関数名 作成するデバイス
__rtnl_newlink 関数 ブリッジ(podman0)とブリッジに接続するデバイス(veth%d)
veth_newlink 関数 コンテナに接続するデバイス(eth0)
[root@server ~]# stap -vg tp.stp -d kernel
-snip-
proc=netavark, pp=kernel.function("rtnl_create_link@net/core/rtnetlink.c:3172"), func=__rtnl_newlink 0xffffffff9c0f9cdc, ifname=podman0
proc=netavark, pp=kernel.function("rtnl_create_link@net/core/rtnetlink.c:3172"), func=__rtnl_newlink 0xffffffff9c0f9cdc, ifname=veth%d
proc=netavark, pp=kernel.function("rtnl_create_link@net/core/rtnetlink.c:3172"), func=0xffffffffc09dd68d 0xffffffffc09dd68d, ifname=eth0

上記SystemTapの3つ目の実行結果は、呼び出し元関数がシンボル名ではなく16進数(0xffffffffc09dd68d)で表示されています。理由はカーネルモジュールの関数だからです。もしかしたら、SystemTapをうまく使えば、カーネルモジュールの関数をシンボル名で表示できるかもしれませんが、そこまで調べれなかったので、ここでは別の手段を使いました。crashコマンドのdisサブコマンドを使用して、このアドレスが該当するソースコードを確認してみました。結果は、veth_newlink関数であることがわかりました。つまり、veth_newlink関数がrtnl_create_link関数を呼び出していることが分かりました。なお、crashコマンドの使い方は、crashコマンドの使い方 - hana_shinのLinux技術ブログを参照してください。

crash> dis -l 0xffffffffc09dd68d
/usr/src/debug/kernel-5.14.0-284.11.1.el9_2/linux-5.14.0-284.11.1.el9_2.x86_64/drivers/net/veth.c: 1761
0xffffffffc09dd68d <veth_newlink+285>:  mov    %rax,%r14

次に、veth_newlink関数の呼び出し元を確認します。

   3278 static int __rtnl_newlink(struct sk_buff *skb, struct nlmsghdr *nlh,
   3279                           struct nlattr **attr, struct netlink_ext_ack *extack)
   3280 {
   -snip-
   3486         if (ops->newlink)
   3487                 err = ops->newlink(link_net ? : net, dev, tb, data, extack);
   3488         else
   3489                 err = register_netdevice(dev);

上記3487行目のnewlinkメソッドは、drivers/net/veth.cで以下のように定義されています。メソッドの実体は、veth_newlink関数です。

   1889 static struct rtnl_link_ops veth_link_ops = {
   1890         .kind           = DRV_NAME,
   1891         .priv_size      = sizeof(struct veth_priv),
   1892         .setup          = veth_setup,
   1893         .validate       = veth_validate,
   1894         .newlink        = veth_newlink,
   1895         .dellink        = veth_dellink,
   1896         .policy         = veth_policy,
   1897         .maxtype        = VETH_INFO_MAX,
   1898         .get_link_net   = veth_get_link_net,
   1899         .get_num_tx_queues      = veth_get_num_queues,
   1900         .get_num_rx_queues      = veth_get_num_queues,
   1901 };

次に、ブリッジやvethデバイスの実体(net_device構造体)の取得場所をSystemTapを使って確認しました。私の検証環境では、3208行目ではなく3203行目でブリッジおよびvethデバイスの実体をメモリから取得しました。

   3172 struct net_device *rtnl_create_link(struct net *net, const char *ifname,
   3173                                     unsigned char name_assign_type,
   3174                                     const struct rtnl_link_ops *ops,
   3175                                     struct nlattr *tb[],
   3176                                     struct netlink_ext_ack *extack)
   3177 {
   3178         struct net_device *dev;
  -snip-
   3202         if (ops->alloc) {
   3203                 dev = ops->alloc(tb, ifname, name_assign_type,
   3204                                  num_tx_queues, num_rx_queues);
   3205                 if (IS_ERR(dev))
   3206                         return dev;
   3207         } else {
   3208                 dev = alloc_netdev_mqs(ops->priv_size, ifname,
   3209                                        name_assign_type, ops->setup,
   3210                                        num_tx_queues, num_rx_queues);
   3211         }

4 コンテナ間通信の処理概要

コンテナでpingを実行したときの処理の流れを確認してみます。

ここでもソースコードを読んで大体の見当をつけてから、veth_xmit関数にプローブを設定しました。veth_xmit関数の第一引数skbは送信パケットへのポインタ、第二引数devはパケットを送信するデバイスです。そして、324行目のrcvは、ペアとなるvethデバイスです。veth_xmit関数は、送信パケットをデバイスdevから送信して、ペアとなるデバイスrcvにキューイングする処理を実行します。SystemTapスクリプトを作成して、この処理を確認してみます。

    319 static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev)
    320 {
    321         struct veth_priv *rcv_priv, *priv = netdev_priv(dev);
    322         struct netdev_queue *queue = NULL;
    323         struct veth_rq *rq = NULL;
    324         struct net_device *rcv;
    -snip-
    351         if (likely(veth_forward_skb(rcv, skb, rq, use_napi) == NET_RX_SUCCESS)) {
    352                 if (queue)
    353                         txq_trans_cond_update(queue);
    354                 if (!use_napi)
    355                         dev_lstats_add(dev, length);
    356         } else {
    357 drop:
    358                 atomic64_inc(&priv->dropped);
    359         }

SystemTapの実行結果の表示を絞り込むため、送信元アドレスがコンテナ(test1)のIPアドレス(10.88.0.2)のときのみ実行結果を表示するようにしました。このような絞り込みをしないと、SystemTapの実行結果が大量に出力され、目的とする情報がわからなくなります。そのため、SystemTapを使う場合は、情報の絞り込みが重要になります。

[root@server ~]#  cat tp.stp
#!/usr/bin/stap
probe module("veth").statement("veth_xmit@drivers/net/veth.c:351")
{
  if(is_skb_conn($skb) == 1){
    __get_skb_iphdr($skb)
    iphdr  = __get_skb_iphdr($skb)
    daddr  = format_ipaddr(__ip_skb_daddr(iphdr), AF_INET())
    saddr  = format_ipaddr(__ip_skb_saddr(iphdr), AF_INET())
    printf("pp=%s, saddr=%s, daddr=%s, dev->name=%s, rcv->name=%s\n",
      pp(), saddr, daddr, kernel_string($dev->name), kernel_string($rcv->name))
  }
}

function is_skb_conn(skb)
{
  iphdr  = __get_skb_iphdr(skb)
  daddr  = format_ipaddr(__ip_skb_daddr(iphdr), AF_INET())
  saddr  = format_ipaddr(__ip_skb_saddr(iphdr), AF_INET())
  if(saddr == "10.88.0.2") {
    return 1
  }
  return 0
}

SystemTapスクリプトを実行すると、以下の結果が得られました。

  • 1行目:コンテナ(test1)のeth0デバイスからブリッジのveth0へのパケット送信
  • 2行目:ブリッジのveth1からコンテナ(test2)のeth0へのパケット送信
[root@server ~]#  stap -vg tp.stp -d kernel
-snip-
pp=module("veth").statement("veth_xmit@drivers/net/veth.c:351"), saddr=10.88.0.2, daddr=10.88.0.3, dev->name=eth0, rcv->name=veth0
pp=module("veth").statement("veth_xmit@drivers/net/veth.c:351"), saddr=10.88.0.2, daddr=10.88.0.3, dev->name=veth1, rcv->name=eth0

次に351行目のveth_forward_skb関数について説明します。veth_forward_skb関数は次の処理を実行します。__dev_forward_skb関数でdevデバイスがアップしているかどうかや、送信パケットがdevデバイスのMTU長以下であるかどうかなどをチェックします。問題がなければ、veth_xdp_rx関数を呼び出して、送信パケットをキューイングします。具体的には、送信パケットのポインタをrcvデバイスのリングバッファに登録します。最後に__netif_rx関数を呼び出します。__netif_rx関数は、ハードウェア受信割り込みの中で呼び出される関数であり、ドライバとカーネルのインタフェース関数です。__netif_rx関数を呼び出すと、veth0デバイスでの受信処理が実行されます。

    292 static int veth_forward_skb(struct net_device *dev, struct sk_buff *skb,
    293                             struct veth_rq *rq, bool xdp)
    294 {
    295         return __dev_forward_skb(dev, skb) ?: xdp ?
    296                 veth_xdp_rx(rq, skb) :
    297                 __netif_rx(skb);
    298 }

受信処理の中でブリッジが実行されます。ブリッジとは、パケットの宛先MACアドレスMACアドレス学習テーブルで検索し、ブリッジの出力ポートを決定する処理です。この主な処理を実行するのがbr_forward関数です。br_forward関数の第一引数toは出力デバイス、第二引数skbは転送するパケットです。

    135 /**
    136  * br_forward - forward a packet to a specific port
    137  * @to: destination port
    138  * @skb: packet being forwarded
    139  * @local_rcv: packet will be received locally after forwarding
    140  * @local_orig: packet is locally originated
    141  *
    142  * Should be called with rcu_read_lock.
    143  */
    144 void br_forward(const struct net_bridge_port *to,
    145                 struct sk_buff *skb, bool local_rcv, bool local_orig)
    146 {

以下のSystemTapスクリプトを作成して、br_forward関数の引数を確認してみました。

[root@server ~]#  cat tp.stp
#!/usr/bin/stap

probe module("bridge").function("br_forward@net/bridge/br_forward.c")
{
  if(is_skb_conn($skb) == 1){
    __get_skb_iphdr($skb)
    iphdr  = __get_skb_iphdr($skb)
    daddr  = format_ipaddr(__ip_skb_daddr(iphdr), AF_INET())
    saddr  = format_ipaddr(__ip_skb_saddr(iphdr), AF_INET())
    printf("pp=%s, saddr=%s, daddr=%s, br->name=%s, dev->name=%s\n",
      pp(), saddr, daddr, kernel_string($to->br->dev->name), kernel_string($to->dev->name))
  }
}

function is_skb_conn(skb)
{
  iphdr  = __get_skb_iphdr(skb)
  daddr  = format_ipaddr(__ip_skb_daddr(iphdr), AF_INET())
  saddr  = format_ipaddr(__ip_skb_saddr(iphdr), AF_INET())
  if(saddr == "10.88.0.2") {
    return 1
  }
  return 0
}

SystemTapスクリプトを実行すると、以下の結果になりました。送信パケットがブリッジ(podman0)のveth1デバイスに転送されていることがわかります。

[root@server ~]#  stap -vg tp.stp -d kernel
-snip-
pp=module("bridge").function("br_forward@net/bridge/br_forward.c:144"), saddr=10.88.0.2, daddr=10.88.0.3, br->name=podman0, dev->name=veth1

参考までに、SystemTapスクリプトで参照しているnet_bridge_port構造体とnet_bridge構造体を示します。これらの構造体は、net/bridge/br_private.hで以下のように定義されています。

    349 struct net_bridge_port {
    350         struct net_bridge               *br;
    351         struct net_device               *dev;
   -snip-

    454 struct net_bridge {
    455         spinlock_t                      lock;
    456         spinlock_t                      hash_lock;
    457         struct hlist_head               frame_type_list;
    458         struct net_device               *dev;

5 まとめ

  • コンテナを起動すると、コンテナとブリッジを接続するvethデバイスのペアが作成されます。一方はコンテナに、もう一方はブリッジに接続されます。
  • vethデバイスの一方からパケットを送信すると、もう一方のデバイスで受信します。
  • ブリッジは、vethデバイスで受信したパケットを宛先MACアドレスに基づいてブリッジの出力先デバイスから送信します。
  • ブリッジの出力先デバイスから送信されるパケットは、コンテナに接続されている、もう一方のデバイスで受信されます。

X 参考情報

参考までに、Netlinkインタフェースでやりとりされるパケットを以下にしめします。Netlinkプロトコルの詳細は調べていませんが、以下はブリッジ(podman0)を作成するメッセージのようです。tcpdumpの使い方は、tcpdumpの使い方(基本編) - hana_shinのLinux技術ブログを参照してください。


Y 参考図書

単行本

電子書籍

podmanコマンドの使い方(Almalinux9)

1 podmanコマンドとは?

Podmanは、RedHat社が開発したコンテナ管理ツールです。
RHEL 8では、Dockerコンテナエンジンとdockerコマンドが削除され、その代わりにpodmanコマンドが使用されます。Podmanは、Dockerと異なり、サービス(docker.service)の起動が不要です。

2 検証環境

サーバのAlmaLinux版数は以下のとおりです。

[root@server ~]# cat /etc/redhat-release
AlmaLinux release 9.2 (Turquoise Kodkod)

カーネル版数は以下のとおりです。

[root@server ~]# uname -r
5.14.0-284.11.1.el9_2.x86_64

3 インストール方法

podmanパッケージをインストールします。

[root@server ~]# dnf -y install podman

podmanコマンドの版数を確認します。

[root@server ~]# podman -v
podman version 4.9.4-rhel

4 オプション一覧

オプションは以下のとおりです。

[root@server ~]# podman --help
Manage pods, containers and images

Usage:
  podman [options] [command]

Available Commands:
  attach      Attach to a running container
  auto-update Auto update containers according to their auto-update policy
  build       Build an image using instructions from Containerfiles
  commit      Create new image based on the changed container
  compose     Run compose workloads via an external provider such as docker-compose or podman-compose
  container   Manage containers
  cp          Copy files/folders between a container and the local filesystem
  create      Create but do not start a container
  diff        Display the changes to the object's file system
  events      Show podman system events
  exec        Run a process in a running container
  export      Export container's filesystem contents as a tar archive
  farm        Farm out builds to remote machines
  generate    Generate structured data based on containers, pods or volumes
  healthcheck Manage health checks on containers
  help        Help about any command
  history     Show history of a specified image
  image       Manage images
  images      List images in local storage
  import      Import a tarball to create a filesystem image
  info        Display podman system information
  init        Initialize one or more containers
  inspect     Display the configuration of object denoted by ID
  kill        Kill one or more running containers with a specific signal
  kube        Play containers, pods or volumes from a structured file
  load        Load image(s) from a tar archive
  login       Log in to a container registry
  logout      Log out of a container registry
  logs        Fetch the logs of one or more containers
  machine     Manage a virtual machine
  manifest    Manipulate manifest lists and image indexes
  mount       Mount a working container's root filesystem
  network     Manage networks
  pause       Pause all the processes in one or more containers
  pod         Manage pods
  port        List port mappings or a specific mapping for the container
  ps          List containers
  pull        Pull an image from a registry
  push        Push an image to a specified destination
  rename      Rename an existing container
  restart     Restart one or more containers
  rm          Remove one or more containers
  rmi         Remove one or more images from local storage
  run         Run a command in a new container
  save        Save image(s) to an archive
  search      Search registry for image
  secret      Manage secrets
  start       Start one or more containers
  stats       Display a live stream of container resource usage statistics
  stop        Stop one or more containers
  system      Manage podman
  tag         Add an additional name to a local image
  top         Display the running processes of a container
  unmount     Unmount working container's root filesystem
  unpause     Unpause the processes in one or more containers
  unshare     Run a command in a modified user namespace
  untag       Remove a name from a local image
  update      Update an existing container
  version     Display the Podman version information
  volume      Manage volumes
  wait        Block on one or more containers

Options:
      --cgroup-manager string       Cgroup manager to use ("cgroupfs"|"systemd") (default "systemd")
      --conmon string               Path of the conmon binary
  -c, --connection string           Connection to use for remote Podman service
      --events-backend string       Events backend to use ("file"|"journald"|"none") (default "journald")
      --help                        Help for podman
      --hooks-dir strings           Set the OCI hooks directory path (may be set multiple times) (default [/usr/share/containers/oci/hooks.d])
      --identity string             path to SSH identity file, (CONTAINER_SSHKEY)
      --imagestore string           Path to the 'image store', different from 'graph root', use this to split storing the image into a separate 'image store', see 'man containers-storage.conf' for details
      --log-level string            Log messages above specified level (trace, debug, info, warn, warning, error, fatal, panic) (default "warn")
      --module strings              Load the containers.conf(5) module
      --network-cmd-path string     Path to the command for configuring the network
      --network-config-dir string   Path of the configuration directory for networks
      --out string                  Send output (stdout) from podman to a file
  -r, --remote                      Access remote Podman service
      --root string                 Path to the graph root directory where images, containers, etc. are stored
      --runroot string              Path to the 'run directory' where all state information is stored
      --runtime string              Path to the OCI-compatible binary used to run containers. (default "crun")
      --runtime-flag stringArray    add global flags for the container runtime
      --ssh string                  define the ssh mode (default "golang")
      --storage-driver string       Select which storage driver is used to manage storage of images and containers
      --storage-opt stringArray     Used to pass an option to the storage driver
      --syslog                      Output logging information to syslog as well as the console (default false)
      --tmpdir string               Path to the tmp directory for libpod state content.

                                    Note: use the environment variable 'TMPDIR' to change the temporary storage location for container images, '/var/tmp'.
                                     (default "/run/libpod")
      --transient-store             Enable transient container storage
      --url string                  URL to access Podman service (CONTAINER_HOST) (default "unix:///run/podman/podman.sock")
  -v, --version                     version for podman
      --volumepath string           Path to the volume directory in which volume data is stored
[root@server ~]#

5 イメージをダウンロード/削除する方法(pull/rmi)

5.1 AlmaLinuxのイメージダウンロード方法(最新版)

下記ページに記載されているタグを参考に、AlmaLinuxのイメージをダウンロードしてみます。
https://hub.docker.com/_/almalinux

タグにlatestを指定して、AlmaLinuxイメージの最新版をダウンロードしてみます。

[root@server ~]# podman pull almalinux:latest

イメージを確認します。ダウンロードしたイメージが最新版(latest)であることが確認できます。

[root@server ~]# podman images
REPOSITORY                   TAG         IMAGE ID      CREATED      SIZE
docker.io/library/almalinux  latest      ce0e33343249  2 weeks ago  191 MB

イメージIDを指定して、イメージを削除します。

[root@server ~]# podman rmi ce0e33343249
Untagged: docker.io/library/almalinux:latest
Deleted: ce0e333432494fd17367f2d5aa1e361fc16259a03151f3f1e0843c90f695dc1a

イメージを確認します。イメージが削除されたことがわかります。

[root@server ~]# podman images
REPOSITORY  TAG         IMAGE ID    CREATED     SIZE

5.2 AlmaLinuxのイメージダウンロード方法(版数指定)

タグに9.4を指定して、AlmaLinuxイメージの9.4版をダウンロードしてみます。

[root@server ~]# podman pull almalinux:9.4
Resolved "almalinux" as an alias (/etc/containers/registries.conf.d/000-shortnames.conf)
Trying to pull docker.io/library/almalinux:9.4...
Getting image source signatures
Copying blob 2d51c66a149e done   |
Copying config ce0e333432 done   |
Writing manifest to image destination
ce0e333432494fd17367f2d5aa1e361fc16259a03151f3f1e0843c90f695dc1a

イメージを確認します。ダウンロードしたイメージが9.4版であることが確認できます。

[root@server ~]# podman images
REPOSITORY                   TAG         IMAGE ID      CREATED      SIZE
docker.io/library/almalinux  9.4         ce0e33343249  2 weeks ago  191 MB

イメージIDを指定して、イメージを削除します。

[root@server ~]# podman rmi ce0e33343249
Untagged: docker.io/library/almalinux:9.4
Deleted: ce0e333432494fd17367f2d5aa1e361fc16259a03151f3f1e0843c90f695dc1a

イメージを確認します。イメージが削除されたことがわかります。

[root@server ~]# podman images
REPOSITORY  TAG         IMAGE ID    CREATED     SIZE

5.3 openSUSEのイメージダウンロード方法(最新版)

DockerHubを確認すると、openSUSEイメージのタグの一覧が以下であることがわかります。

[root@server ~]# curl -s https://registry.hub.docker.com/v2/repositories/opensuse/leap/tags/ | jq '.results[].name'
"latest"
"15"
"15.5"
"15.6"
"15.4"
"15.3"
"15.2"
"15.1"
"15.2.1"
"15.0"

タグにlatestを指定して、openSUSEイメージの最新版をダウンロードしてみます。

[root@server ~]# podman pull docker.io/opensuse/leap:latest
Trying to pull docker.io/opensuse/leap:latest...
Getting image source signatures
Copying blob cd2d8e5c43d1 done   |
Copying config f8872388ad done   |
Writing manifest to image destination
f8872388adc38612a74bd8e234ecac71cfaf0c299dfc6c26ec86fc79dbcd879d

イメージを確認します。ダウンロードしたイメージが最新版であることが確認できます。

[root@server ~]# podman images
REPOSITORY               TAG         IMAGE ID      CREATED      SIZE
docker.io/opensuse/leap  latest      f8872388adc3  11 days ago  122 MB

イメージIDを指定して、イメージを削除します。

[root@server ~]# podman rmi f8872388adc3
Untagged: docker.io/opensuse/leap:latest
Deleted: f8872388adc38612a74bd8e234ecac71cfaf0c299dfc6c26ec86fc79dbcd879d

イメージを確認します。openSUSEのイメージが削除されたことがわかります。

[root@server ~]# podman images
REPOSITORY  TAG         IMAGE ID    CREATED     SIZE

5.4 openSUSEのイメージダウンロード方法(版数指定)

タグに15.5を指定して、openSUSEイメージの15.5版をダウンロードしてみます。

[root@server ~]# podman pull docker.io/opensuse/leap:15.5
Trying to pull docker.io/opensuse/leap:15.5...
Getting image source signatures
Copying blob cd2d8e5c43d1 done   |
Copying config f8872388ad done   |
Writing manifest to image destination
f8872388adc38612a74bd8e234ecac71cfaf0c299dfc6c26ec86fc79dbcd879d

イメージを確認します。ダウンロードしたイメージが15.5版であることが確認できます。

[root@server ~]# podman images
REPOSITORY               TAG         IMAGE ID      CREATED      SIZE
docker.io/opensuse/leap  15.5        f8872388adc3  11 days ago  122 MB

イメージIDを指定してイメージを削除します。

[root@server ~]# podman rmi f8872388adc3
Untagged: docker.io/opensuse/leap:15.5
Deleted: f8872388adc38612a74bd8e234ecac71cfaf0c299dfc6c26ec86fc79dbcd879d

イメージを確認します。イメージが削除されたことがわかります。

[root@server ~]# podman images
REPOSITORY  TAG         IMAGE ID    CREATED     SIZE

5.5 Ubuntuのイメージダウンロード方法(最新版)

下記ページに記載されているタグ一覧を参考に、Ubuntuの最新版イメージを取得してみます。
https://hub.docker.com/_/ubuntu

[root@server ~]# podman pull ubuntu:latest
Resolved "ubuntu" as an alias (/etc/containers/registries.conf.d/000-shortnames.conf)
Trying to pull docker.io/library/ubuntu:latest...
Getting image source signatures
Copying blob 49b384cc7b4a done   |
Copying config bf3dc08bfe done   |
Writing manifest to image destination
bf3dc08bfed031182827888bb15977e316ad797ee2ccb63b4c7a57fdfe7eb31d

イメージを確認します。

[root@server ~]# podman images
REPOSITORY                TAG         IMAGE ID      CREATED      SIZE
docker.io/library/ubuntu  latest      bf3dc08bfed0  3 weeks ago  78.7 MB

確認したイメージIDを指定して、イメージを削除します。

[root@server ~]# podman rmi bf3dc08bfed0
Untagged: docker.io/library/ubuntu:latest
Deleted: bf3dc08bfed031182827888bb15977e316ad797ee2ccb63b4c7a57fdfe7eb31d

イメージを確認します。イメージが削除されたことがわかります。

[root@server ~]# podman images
REPOSITORY  TAG         IMAGE ID    CREATED     SIZE

5.6 Ubuntuのイメージダウンロード方法(版数指定)

タグに24.04を指定して、Ubuntuイメージの24.04版をダウンロードしてみます。

root@server ~]# podman pull ubuntu:24.04
Resolved "ubuntu" as an alias (/etc/containers/registries.conf.d/000-shortnames.conf)
Trying to pull docker.io/library/ubuntu:24.04...
Getting image source signatures
Copying blob 49b384cc7b4a done   |
Copying config bf3dc08bfe done   |
Writing manifest to image destination
bf3dc08bfed031182827888bb15977e316ad797ee2ccb63b4c7a57fdfe7eb31d

イメージを確認します。ダウンロードしたイメージが24.04版であることが確認できます。

[root@server ~]# podman images
REPOSITORY                TAG         IMAGE ID      CREATED      SIZE
docker.io/library/ubuntu  24.04       bf3dc08bfed0  3 weeks ago  78.7 MB

確認したイメージIDを指定して、イメージを削除します。

[root@server ~]# podman rmi 8264e2ec2ece

イメージを確認します。イメージが削除されたことがわかります。

[root@server ~]# podman images
REPOSITORY  TAG         IMAGE ID    CREATED     SIZE

5.7 Nginxのイメージダウンロード方法(最新版)

下記ページに記載されているタグを参考に、Nginxのイメージを Docker Hubから取得してみます。
https://hub.docker.com/_/nginx

[root@server ~]# podman pull docker.io/library/nginx:latest
Trying to pull docker.io/library/nginx:latest...
Getting image source signatures
Copying blob 45337c09cd57 done   |
Copying blob 933cc8470577 done   |
Copying blob a11fc495bafd done   |
Copying blob 971bb7f4fb12 done   |
Copying blob 09f376ebb190 done   |
Copying blob 999643392fb7 done   |
Copying blob de3b062c0af7 done   |
Copying config e784f45604 done   |
Writing manifest to image destination
e784f4560448b14a66f55c26e1b4dad2c2877cc73d001b7cd0b18e24a700a070

イメージを確認します。

[root@server ~]# podman images
REPOSITORY               TAG         IMAGE ID      CREATED      SIZE
docker.io/library/nginx  latest      e784f4560448  3 weeks ago  192 MB

確認したイメージIDを指定して、イメージを削除します。

[root@server ~]# podman rmi e784f4560448
Untagged: docker.io/library/nginx:latest
Deleted: e784f4560448b14a66f55c26e1b4dad2c2877cc73d001b7cd0b18e24a700a070

イメージを確認します。イメージが削除されたことがわかります。

[root@server ~]# podman images
REPOSITORY  TAG         IMAGE ID    CREATED     SIZE

5.8 Nginxのイメージダウンロード方法(版数指定)

タグに1.25.5を指定して、Nginxイメージの1.25.5版をダウンロードしてみます。

[root@server ~]# podman pull docker.io/library/nginx:1.25.5
Trying to pull docker.io/library/nginx:1.25.5...
Getting image source signatures
Copying blob a11fc495bafd done   |
Copying blob 45337c09cd57 done   |
Copying blob 09f376ebb190 done   |
Copying blob 999643392fb7 done   |
Copying blob 933cc8470577 done   |
Copying blob 971bb7f4fb12 done   |
Copying blob de3b062c0af7 done   |
Copying config e784f45604 done   |
Writing manifest to image destination
e784f4560448b14a66f55c26e1b4dad2c2877cc73d001b7cd0b18e24a700a070

イメージを確認します。

[root@server ~]# podman images
REPOSITORY               TAG         IMAGE ID      CREATED      SIZE
docker.io/library/nginx  1.25.5      e784f4560448  3 weeks ago  192 MB

イメージIDを指定して、イメージを削除します。

[root@server ~]# podman rmi e784f4560448
Untagged: docker.io/library/nginx:1.25.5
Deleted: e784f4560448b14a66f55c26e1b4dad2c2877cc73d001b7cd0b18e24a700a070

イメージを確認します。イメージが削除されたことがわかります。

[root@server ~]# podman images
REPOSITORY  TAG         IMAGE ID    CREATED     SIZE

6 コンテナの起動/停止/削除する方法(run/stop/rm)

AlmaLinuxの最新版イメージを取得します。

[root@server ~]# podman pull almalinux:latest
Resolved "almalinux" as an alias (/etc/containers/registries.conf.d/000-shortnames.conf)
Trying to pull docker.io/library/almalinux:latest...
Getting image source signatures
Copying blob 2d51c66a149e done   |
Copying config ce0e333432 done   |
Writing manifest to image destination
ce0e333432494fd17367f2d5aa1e361fc16259a03151f3f1e0843c90f695dc1a

取得したイメージを確認します。

[root@server ~]# podman images
REPOSITORY                   TAG         IMAGE ID      CREATED      SIZE
docker.io/library/almalinux  latest      ce0e33343249  3 weeks ago  191 MB

IMAGE IDを指定して、testという名前のコンテナを起動します。

[root@server ~]# podman run -it --name test ce0e33343249
[root@31a5e80b3953 /]#

オプションの意味は以下のとおりです。

オプション 意味
--interactive(-i) 対話式プロセスの場合には、-i と -t を併用してコンテナプロセスに端末を割り当てる
--tty コンテナ内でシェルを実行する際に使用する
--name コンテナの名前を指定する
--detach(-d) コンテナをバックグラウンドで実行する時に指定する

ホストでもう1つターミナルを開いて、コンテナの状態を確認します。testという名前のコンテナが起動したことがわかります。

[root@server ~]# podman ps -a
CONTAINER ID  IMAGE                               COMMAND     CREATED         STATUS         PORTS       NAMES
31a5e80b3953  docker.io/library/almalinux:latest  /bin/bash   24 seconds ago  Up 24 seconds              test

少し話がそれますが、コンテナを起動すると、ネームスペースが作成されることがわかります。なお、ip netnsコマンドの使い方は、ip netnsコマンドの使い方(ネットワークの実験の幅が広がるなぁ~) - hana_shinのLinux技術ブログを参照してください。

[root@server ~]# ip netns
netns-793cbe84-d913-dfde-5e51-17154889d402 (id: 0)

コンテを停止します。

[root@server ~]# podman stop test
test

コンテナの状態を確認します。

[root@server ~]# podman ps -a
CONTAINER ID  IMAGE                               COMMAND     CREATED        STATUS                       PORTS       NAMES
31a5e80b3953  docker.io/library/almalinux:latest  /bin/bash   3 minutes ago  Exited (137) 13 seconds ago              test

コンテナを停止すると、ネームスペースが削除されることがわかります。

[root@server ~]# ip netns
[root@server ~]#

コンテナを削除します。

[root@server ~]# podman rm test
test

コンテナの状態を確認します。コンテナが削除されたことがわかります。

[root@server ~]# podman ps -a
CONTAINER ID  IMAGE       COMMAND     CREATED     STATUS      PORTS       NAMES

7 コンテナでコマンドを実行する方法(exec)

7.1 bashを実行する方法

タグに15.5を指定して、OpenSUSEイメージの15.5版をダウンロードしてみます。

[root@server ~]# podman pull docker.io/opensuse/leap:15.5
Trying to pull docker.io/opensuse/leap:15.5...
Getting image source signatures
Copying blob fc28d7a394e1 done   |
Copying config 007974651d done   |
Writing manifest to image destination
007974651d34f8a848ad692926e51fbb9382615e9838e43ee060b6160bc52b4c

取得したイメージを確認します。

[root@server ~]# podman images
REPOSITORY               TAG         IMAGE ID      CREATED     SIZE
docker.io/opensuse/leap  15.5        007974651d34  6 days ago  122 MB

IMAGE IDを指定して、testという名前のコンテナを起動します。

[root@server ~]# podman run -it --name test 007974651d34
af0c63c76e00:/ #

コンテナ内でOpenSUSE版数を確認します。OpenSUSE版数は15.5であることがわかります。

af0c63c76e00:/ # cat /etc/os-release
NAME="openSUSE Leap"
VERSION="15.5"
ID="opensuse-leap"
ID_LIKE="suse opensuse"
VERSION_ID="15.5"
PRETTY_NAME="openSUSE Leap 15.5"
ANSI_COLOR="0;32"
CPE_NAME="cpe:/o:opensuse:leap:15.5"
BUG_REPORT_URL="https://bugs.opensuse.org"
HOME_URL="https://www.opensuse.org/"
DOCUMENTATION_URL="https://en.opensuse.org/Portal:Leap"
LOGO="distributor-logo-Leap"

testコンテナから抜けます。

af0c63c76e00:/ # exit
exit
[root@server ~]#

コンテナの状態を確認します。

[root@server ~]# podman ps -a
CONTAINER ID  IMAGE                         COMMAND     CREATED             STATUS                     PORTS       NAMES
af0c63c76e00  docker.io/opensuse/leap:15.5  /bin/bash   About a minute ago  Exited (0) 17 seconds ago              test

コンテナを削除します。

[root@server ~]# podman rm test
test

コンテナの状態を確認します。コンテナが削除されたことがわかります。

[root@server ~]# podman ps -a
CONTAINER ID  IMAGE       COMMAND     CREATED     STATUS      PORTS       NAMES

7.2 catを実行する場合

AlmaLinuxの9.2版のイメージを取得します。

[root@server ~]# podman pull almalinux:9.2
Resolved "almalinux" as an alias (/etc/containers/registries.conf.d/000-shortnames.conf)
Trying to pull docker.io/library/almalinux:9.2...
Getting image source signatures
Copying blob 92cbf8f63752 done   |
Copying config 1851aa77ac done   |
Writing manifest to image destination
1851aa77ac58181083ae0e68711dc38e434bec08ad53ef6f42c66cb1d100a6ef

取得したイメージを確認します。

[root@server ~]# podman images
REPOSITORY                   TAG         IMAGE ID      CREATED        SIZE
docker.io/library/almalinux  9.2         1851aa77ac58  10 months ago  190 MB

IMAGE IDを指定して、testという名前のコンテナを起動します。

[root@server ~]# podman run -it -d --name test 1851aa77ac58
436f6e846043416113761d1d17b8fecf822a01a2840fa299fd7118732fdc1557

testコンテナで cat コマンドを実行します。

[root@server ~]# podman exec -it test cat /etc/redhat-release
AlmaLinux release 9.2 (Turquoise Kodkod)

8 Nginxコンテナを起動する方法

タグにlatestを指定して、nginxイメージの最新版をダウンロードしてみます。

[root@server ~]# podman pull nginx:latest
Resolved "nginx" as an alias (/var/cache/containers/short-name-aliases.conf)
Trying to pull docker.io/library/nginx:latest...
Getting image source signatures
Copying blob 09f376ebb190 done   |
Copying blob 999643392fb7 done   |
Copying blob 971bb7f4fb12 done   |
Copying blob a11fc495bafd done   |
Copying blob 933cc8470577 done   |
Copying blob 45337c09cd57 done   |
Copying blob de3b062c0af7 done   |
Copying config e784f45604 done   |
Writing manifest to image destination
e784f4560448b14a66f55c26e1b4dad2c2877cc73d001b7cd0b18e24a700a070

取得したイメージを確認します。

[root@server ~]# podman images
REPOSITORY               TAG         IMAGE ID      CREATED      SIZE
docker.io/library/nginx  latest      e784f4560448  3 weeks ago  192 MB

web1という名前でNginxコンテナを起動します。-pは、ホストの8080番ポートへのパケットをNginxがTCPパケットを待ち受けている80番ポートにDNATするオプションです。

[root@server ~]# podman run -d -p 8080:80 --name web1 e784f4560448
c8297889d408d1510ca62c22df1ddebd79db0324fd15eb7fb45f0b6785e57992

コンテナの状態を確認します。web1という名前のコンテナが動作していることがわかります。

[root@server ~]# podman ps -a
CONTAINER ID  IMAGE                           COMMAND               CREATED         STATUS         PORTS                 NAMES
c8297889d408  docker.io/library/nginx:latest  nginx -g daemon o...  13 seconds ago  Up 12 seconds  0.0.0.0:8080->80/tcp  web1

web1コンテナで動作しているプロセスを確認します。nginx プロセスが動作していることがわかります。

[root@server ~]# podman top web1
USER        PID         PPID        %CPU        ELAPSED        TTY         TIME        COMMAND
root        1           0           0.000       25.456279266s  ?           0s          nginx: master process nginx -g daemon off;
nginx       24          1           0.000       25.456595654s  ?           0s          nginx: worker process
nginx       25          1           0.000       25.456702218s  ?           0s          nginx: worker process
nginx       26          1           0.000       25.456799747s  ?           0s          nginx: worker process
nginx       27          1           0.000       25.457019033s  ?           0s          nginx: worker process

コンテナとホストのポート番号の対応関係を確認します。コンテナの80番ポートがホストの8080番ポートに対応していることがわかります。

[root@server ~]# podman port web1
80/tcp -> 0.0.0.0:8080

iptablesのターゲットを確認します。Nginxコンテナを起動すると、以下のターゲットがiptablesに作成されます。

[root@server ~]# iptables -t nat -nvL |grep 8080
    0     0 NETAVARK-HOSTPORT-SETMARK  tcp  --  *      *       10.88.0.0/16         0.0.0.0/0            tcp dpt:8080
    0     0 NETAVARK-HOSTPORT-SETMARK  tcp  --  *      *       127.0.0.1            0.0.0.0/0            tcp dpt:8080
    0     0 DNAT       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8080 to:10.88.0.6:80
    0     0 NETAVARK-DN-1D8721804F16F  tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8080 /* dnat name: podman id: c8297889d408d1510ca62c22df1ddebd79db0324fd15eb7fb45f0b6785e57992 */
[root@server ~]#

iptablesの使い方の詳細は、以下を参照してください。
iptablesコマンドの使い方 - hana_shinのLinux技術ブログ
iptablesコマンドの使い方(ターゲットの使い方) - hana_shinのLinux技術ブログ
iptables-extensionsの使い方 - hana_shinのLinux技術ブログ
tcpdumpのフックポイント、Netfilterのチェイン、Qdiscのカーネル内の場所 - hana_shinのLinux技術ブログ

curlコマンドでNginxにアクセスしてみます。なお。curlコマンドの使い方は、curlコマンドの使い方 - hana_shinのLinux技術ブログを参照してください。

[root@server ~]# curl -I http://192.168.122.87:8080
HTTP/1.1 200 OK
Server: nginx/1.25.5
Date: Tue, 28 May 2024 13:12:39 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Tue, 16 Apr 2024 14:29:59 GMT
Connection: keep-alive
ETag: "661e8b67-267"
Accept-Ranges: bytes

curlコマンドを1回実行すると、DNATターゲットが処理したパケット数が1増加していることがわかります。DNATでは、ホストが受信したTCPパケットの宛先IPアドレスを10.88.0.2、宛先ポート番号を8080から80に変更しています。

[root@server ~]# iptables -t nat -nvL |grep 8080
    0     0 NETAVARK-HOSTPORT-SETMARK  tcp  --  *      *       10.88.0.0/16         0.0.0.0/0            tcp dpt:8080
    0     0 NETAVARK-HOSTPORT-SETMARK  tcp  --  *      *       127.0.0.1            0.0.0.0/0            tcp dpt:8080
    1    60 DNAT       tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8080 to:10.88.0.6:80
    1    60 NETAVARK-DN-1D8721804F16F  tcp  --  *      *       0.0.0.0/0            0.0.0.0/0            tcp dpt:8080 /* dnat name: podman id: c8297889d408d1510ca62c22df1ddebd79db0324fd15eb7fb45f0b6785e57992 */

9 コンテナが出力するログを確認する方法(logs)

コンテナの状態を確認します。web1という名前のコンテナが動作しています。

[root@server ~]# podman ps -a
CONTAINER ID  IMAGE                           COMMAND               CREATED             STATUS             PORTS                 NAMES
c8297889d408  docker.io/library/nginx:latest  nginx -g daemon o...  About a minute ago  Up About a minute  0.0.0.0:8080->80/tcp  web1
[root@server ~]#

web1コンテナが出力するログを確認します。なお、-tオプションは、時刻を表示するオプションです。

[root@server ~]# podman logs -t web1
2024-05-28T22:11:21.257216000+09:00 /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
2024-05-28T22:11:21.262837000+09:00 /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
2024-05-28T22:11:21.264553000+09:00 /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
-snip-

-fオプションは、ログ末尾からメッセージを表示するオプションです。

[root@server ~]# podman logs -ft web1
-snip-
2024-05-28T22:11:21.378042000+09:00 2024/05/28 13:11:21 [notice] 1#1: start worker process 26
2024-05-28T22:11:21.379112000+09:00 2024/05/28 13:11:21 [notice] 1#1: start worker process 27
2024-05-28T22:12:39.324924000+09:00 192.168.122.87 - - [28/May/2024:13:12:39 +0000] "HEAD / HTTP/1.1" 200 0 "-" "curl/7.76.1" "-"

Y 参考図書

単行本

電子書籍

Ansibleの使い方(ansible.posixモジュール編)

1 はじめに

ansible.posixは、POSIXLinux/Unix系システム)ベースの操作をサポートするためのモジュールやプラグインが含まれているAnsibleコレクションです。Linuxのシステム管理に役立ちます。詳細は、Ansible.Posix — Ansible Community Documentationを参照してください。

モジュール名 概要
acl ファイルACL情報の設定と取得
at atコマンドを使用してコマンドまたはスクリプトファイルの実行をスケジュール
authorized_key SSHの認証キーの追加または削除
firewalld firewalldを使用して任意のポート/サービスを管理
firewalld_info firewalldに関する情報の収集
mount マウントポイントの制御
patch パッチツールを使用してパッチファイルを適用
rhel_facts RHEL固有のファクトを設定またはオーバーライドするためのファクトモジュール
rhel_rpm_ostree RHEL for Edge rpm-ostreeベースのシステムでパッケージが存在することを確認
rpm_ostree_upgrade rpm-ostreeアップグレードトランザクションの管理
seboolean SELinuxブール値の切り替え
selinux SELinuxのポリシーと状態の変更
synchronize rsyncコマンドを使って、ローカルホストとリモートホスト間でファイルやディレクトリを同期するためのモジュール
sysctl カーネルパラメータを操作するためのモジュール

2 検証環境

動作検証を行うための環境は以下のとおりです。enp1s0はNIC(ネットワークインターフェースカード)の名前です。

                          192.168.122.0/24
control(enp1s0) -------------------------------------(enp1s0) node1
               .220                               .87
ホスト名 役割
control コントロールノードとして動作します。Ansibleをインストールします
node1 ターゲットノードとして動作します

コントロールノード、ターゲットノードのAlmaLinux版数は以下のとおりです。

[root@server ~]# cat /etc/redhat-release
AlmaLinux release 9.2 (Turquoise Kodkod)

コントロールノード、ターゲットノードのカーネル版数は以下のとおりです。

[root@server ~]# uname -r
5.14.0-284.11.1.el9_2.x86_64

私の環境では、ansible.posixがデフォルトでインストールされていました。ansible.posixのバージョンは以下のとおりです

[root@control ~]# ansible-galaxy collection list

# /usr/lib/python3.9/site-packages/ansible_collections
Collection                    Version
----------------------------- -------
amazon.aws                    5.5.1
ansible.netcommon             4.1.0
ansible.posix                 1.5.4
-snip-

3 事前準備

Ansibleのインストールや公開鍵認証の設定方法などは、Ansibleの使い方(モジュール編) - hana_shinのLinux技術ブログを参照してください。

4 firewalldモジュールの使い方

firewalldモジュールは、firewalld ルールに対してサービス/ポート(TCPまたはUDPのいずれか)の追加/削除するモジュールです。詳細は、ansible.posix.firewalld module – Manage arbitrary ports/services with firewalld — Ansible Community Documentationを参照してください。

4.1 ポートを解放する方法

playbookを作成します。このplaybookは、nodesグループ内のホストで、publicゾーンにおいてポート80/tcpを許可します。設定は永続的(permanent: yes)かつ即時的(immediate: yes)に行われます。なお、firewalldモジュールに渡すパラメータの意味は以下のとおりです。

パラメータ 意味
zone ファイアウォールのゾーンを指定します。publicはpublicゾーンを指し、そこでポートの変更が行われます。ゾーンは他にdmz,internal等があります
permanent yesに設定することで、ファイアウォールの設定が永続的に保存されます。次回OS再起動後も設定が保持されます
immediate yesに設定することで、即時にファイアウォールの設定を反映します
port 許可またはブロックするポートとプロトコルを指定します。たとえば、80/tcpは80番ポートのTCPプロトコルを示しています
state enabledに設定することで、指定されたポートを有効にします。つまり、ポート80/tcpがpublicゾーンで解放されていることを意味します
[root@control ~]# vi test.yml
[root@control ~]# cat test.yml
- name: Sample Playbook
  hosts: nodes
  tasks:
    - name: Allow port 80/tcp in the public zone
      ansible.posix.firewalld:
        zone: public
        permanent: yes
        immediate: yes
        port: 80/tcp
        state: enabled

playbookの実行前に、ターゲットノードで開放されているポート番号を確認します。TCPの80番ポートが開放されていないことが確認できます。なお、firewall-cmdの使い方はfirewall-cmdの使い方 - hana_shinのLinux技術ブログを参照してください。

[root@node1 ~]# firewall-cmd --list-ports

[root@node1 ~]#

playbookを実行します。

[root@control ~]# ansible-playbook -i hosts.ini test.yml

PLAY [Sample Playbook] *************************************************************************

TASK [Gathering Facts] *************************************************************************
ok: [node1]

TASK [Allow port 80/tcp in the public zone] ****************************************************
changed: [node1]

PLAY RECAP *************************************************************************************
node1                      : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[root@control ~]#

playbookを実行すると、TCPの80番ポートが解放されたことが確認できます。

[root@node1 ~]# firewall-cmd --list-ports
80/tcp
[root@node1 ~]#

4.2 ポートをブロックする方法

playbookを作成します。このplaybookは、nodesグループ内のホストで、publicゾーンにおいてポート80/tcpを無効にします。設定は永続的(permanent: yes)かつ即時的(immediate: yes)に行われます。

[root@control ~]# vi test.yml
[root@control ~]# cat test.yml
- name: Sample Playbook
  hosts: nodes
  tasks:
    - name: Allow port 80/tcp in the public zone
      ansible.posix.firewalld:
        zone: public
        permanent: yes
        immediate: yes
        port: 80/tcp
        state: disabled

playbookの実行前にターゲットノードで解放されているポート番号を確認すると、TCPの80番ポートが開放されていることがわかります。

[root@node1 ~]# firewall-cmd --list-ports
80/tcp

playbookを実行します。

[root@control ~]# ansible-playbook -i hosts.ini test.yml

PLAY [Sample Playbook] *************************************************************************

TASK [Gathering Facts] *************************************************************************
ok: [node1]

TASK [Allow port 80/tcp in the public zone] ****************************************************
changed: [node1]

PLAY RECAP *************************************************************************************
node1                      : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[root@control ~]#

playbookを実行すると、TCPの80番ポートがブロックされていることが確認できます。

[root@node1 ~]# firewall-cmd --list-ports

[root@node1 ~]#

5 firewalld_infoモジュールの使い方

firewalld_infoモジュールは、firewalldのルールに関する情報を取得するモジュールです。詳細は、ansible.posix.firewalld_info module – Gather information about firewalld — Ansible Community Documentationを参照してください。

5.1 全ての情報を取得する方法

playbookを作成します。このplaybookは、nodesグループ内のホストについて、firewalldのアクティブなゾーンに関する情報を取得し、その結果をresultに格納します。そして、取得した情報をdebugモジュールを使って表示します。なお、debugモジュールについては、Ansibleの使い方(デバッグ編) - hana_shinのLinux技術ブログを参照してください。

[root@control ~]# vi test.yml
[root@control ~]# cat test.yml
- name: Sample Playbook
  hosts: nodes
  tasks:
    - name: Gather information about active zones
      ansible.posix.firewalld_info:
        active_zones: true
      register: result
    - name: Display the result
      ansible.builtin.debug:
        var: result

playbookを実行すると、firewalldの各種情報が表示されることがわかります。

[root@control ~]# ansible-playbook -i hosts.ini test.yml

PLAY [Sample Playbook] ************************************************************************

TASK [Gathering Facts] ************************************************************************
ok: [node1]

TASK [Gather information about active zones] **************************************************
ok: [node1]

TASK [Display the result] *********************************************************************
ok: [node1] => {
    "result": {
        "active_zones": true,
        "changed": false,
        "collected_zones": [
            "public"
        ],
        "failed": false,
        "firewalld_info": {
            "default_zone": "public",
            "version": "1.2.1",
            "zones": {
                "public": {
                    "forward": true,
                    "forward_ports": [],
                    "icmp_block_inversion": false,
                    "icmp_blocks": [],
                    "interfaces": [
                        "enp1s0"
                    ],
                    "masquerade": false,
                    "ports": [],
                    "protocols": [],
                    "rich_rules": [],
                    "services": [
                        "ssh",
                        "dhcpv6-client",
                        "cockpit"
                    ],
                    "source_ports": [],
                    "sources": [],
                    "target": "default"
                }
            }
        },
        "undefined_zones": []
    }
}

PLAY RECAP ************************************************************************************
node1                      : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[root@control ~]#

5.2 特定の情報を取得する方法

playbookを作成します。このplaybookは、nodesグループ内のホストで、firewalldのアクティブなゾーンに関する情報を取得し、その結果をresultに格納します。その後、firewalldのバージョン情報をdebugモジュールを使って表示します。

[root@control ~]# vi test.yml
[root@control ~]# cat test.yml
- name: Sample Playbook
  hosts: nodes
  tasks:
    - name: Gather information about active zones
      ansible.posix.firewalld_info:
        active_zones: true
      register: result
    - name: Display the result
      ansible.builtin.debug:
        var: result.firewalld_info.version

playbookを実行すると、firewalldのバージョンが1.2.1であることがわかります。

[root@control ~]# ansible-playbook -i hosts.ini test.yml

PLAY [Sample Playbook] ************************************************************************

TASK [Gathering Facts] ************************************************************************
ok: [node1]

TASK [Gather information about active zones] **************************************************
ok: [node1]

TASK [Display the result] *********************************************************************
ok: [node1] => {
    "result.firewalld_info.version": "1.2.1"
}

PLAY RECAP ************************************************************************************
node1                      : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[root@control ~]#

6 sysctlモジュールの使い方

sysctlモジュールは、カーネルパラメータを操作するためのモジュールです。カーネルパラメータは/proc/sys/配下のファイルに保存されます。たとえば、IPフォワーディングの有効化または無効化を制御するカーネルパラメータは、/proc/sys/net/ipv4/ip_forwardファイルに保存されています。また、sysctlモジュールのsysctl_fileパラメータを使用すると、指定したファイルにカーネルパラメータを設定できます。詳細は、ansible.posix.sysctl module – Manage entries in sysctl.conf. — Ansible Community Documentationを参照してください。

6.1 ルーティングを有効にする方法

playbookを作成します。このplaybookは、nodesグループ内のホストで、カーネルパラメータのnet.ipv4.ip_forwardを1に設定します。これにより、IPフォワーディングが有効になります。なお、sysctlモジュールに渡すパラメータの意味は以下のとおりです。

パラメータ 意味
name カーネルパラメータの名前を指定します
value カーネルパラメータに設定する値を指定します。'1'はIPフォワーディングを有効にすることを意味します
sysctl_set trueに設定することで、システムファイルにカーネルパラメータの変更を永続的に反映します。OS再起動後も設定を保持します
state このタスクで扱うリソースの状態を指定します。presentは、指定されたカーネルパラメータが存在していること、または期待値に設定されていることを確認します
reload trueに設定することで、変更が適用された後に設定ファイルをリロードし、システムに変更を反映します
sysctl_file カーネルパラメータを保存するファイルのパスを指定します
[root@control ~]# vi test.yml
[root@control ~]# cat test.yml
- name: Sample Playbook
  hosts: nodes
  tasks:
    - name: Set ip forwarding on in /proc and in the sysctl file and reload if necessary
      ansible.posix.sysctl:
        name: net.ipv4.ip_forward
        value: '1'
        sysctl_set: true
        state: present
        reload: true

playbookを実行する前に、ターゲットノードのip_forwardの値を確認すると、0であることがわかります。

[root@node1 ~]# sysctl -n net.ipv4.ip_forward
0

playbookを実行します。

[root@control ~]# ansible-playbook -i hosts.ini test.yml

PLAY [Sample Playbook] ************************************************************************

TASK [Gathering Facts] ************************************************************************
ok: [node1]

TASK [Set ip forwarding on in /proc and in the sysctl file and reload if necessary] ***********
changed: [node1]

PLAY RECAP ************************************************************************************
node1                      : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[root@control ~]#

playbookを実行すると、ターゲットノードのip_forwardの値が1に変更されたことが確認できます。

[root@node1 ~]# sysctl -n net.ipv4.ip_forward
1

6.2 カーネルパニックを有効にする方法

playbookを作成します。このplaybookは、nodesグループ内のホストで、/etc/sysctl.d/10-test.confファイルのkernel.panicを1に設定します。

[root@control ~]# vi test.yml
[root@control ~]# cat test.yml
- name: Sample Playbook
  hosts: nodes
  tasks:
    - name: Set kernel.panic to 1 in /etc/sysctl.d/10-test.conf
      ansible.posix.sysctl:
        name: kernel.panic
        value: '1'
        sysctl_file: /etc/sysctl.d/10-test.conf

playbookを実行します。

[root@control ~]# ansible-playbook -i hosts.ini test.yml

PLAY [Sample Playbook] ************************************************************************

TASK [Gathering Facts] ************************************************************************
ok: [node1]

TASK [Set ip forwarding on in /proc and in the sysctl file and reload if necessary] ***********
changed: [node1]

PLAY RECAP ************************************************************************************
node1                      : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[root@control ~]#

/etc/sysctl.d/10-test.confを確認すると、kernel.panicに1が設定されていることがわかります。

[root@node1 ~]# cat /etc/sysctl.d/10-test.conf
kernel.panic=1

7 atモジュールの使い方

atモジュールは、atコマンドを使ってコマンドやスクリプトファイルの実行をスケジュールするモジュールです。詳細は、ansible.posix.at module – Schedule the execution of a command or script file via the at command — Ansible Community Documentationを参照してください。

atモジュールを実行するためには、ターゲットノードにatコマンドがインストールされている必要があります。そのため、atパッケージを事前にインストールします。

[root@node1 ~]# dnf install at

playbookを作成します。

[root@control ~]# vi test.yml
[root@control ~]# cat test.yml
- name: Sample Playbook
  hosts: nodes
  tasks:
    - name: Get target node's time
      ansible.builtin.command:
        cmd: date
      register: node_time

    - name: Output target node's time to control node's stdout
      ansible.builtin.debug:
        msg: "Target node's time: {{ node_time.stdout }}"

    - name: Schedule a command using at module
      ansible.posix.at:
        command: echo "$(date)" > /tmp/time.txt
        count: 1
        units: minutes
      register: result

    - name:
      ansible.builtin.debug:
        msg: "Job scheduled with command: {{ result }}"

playbookを実行します。atモジュールを実行する直前のターゲットノードの時刻が21時38分13秒であることがわかります。

[root@control ~]# ansible-playbook -i hosts.ini test.yml

PLAY [Sample Playbook] ***********************************************************************

TASK [Gathering Facts] ***********************************************************************
ok: [node1]

TASK [Get target node's time] ****************************************************************
changed: [node1]

TASK [Output target node's time to control node's stdout] ************************************
ok: [node1] => {
    "msg": "Target node's time: 2024年  4月 17日 水曜日 21:38:13 JST"
}

TASK [Schedule a command using at module] ****************************************************
changed: [node1]

TASK [ansible.builtin.debug] *****************************************************************
ok: [node1] => {
    "msg": "Job scheduled with command: {'changed': True, 'state': 'present', 'script_file': '/tmp/atyq_8mt9r', 'count': 1, 'units': 'minutes', 'failed': False}"
}

PLAY RECAP ***********************************************************************************
node1                      : ok=5    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[root@control ~]#

ターゲットノードでatqコマンドを実行します。echoコマンドが21時39分00秒に実行されることがわかります。

[root@node1 ~]# atq
22      Wed Apr 17 21:39:00 2024 a root

ファイルの中身を確認すると、echoコマンドが21時39分00秒に実行されたことがわかります。

[root@node1 ~]# cat /tmp/time.txt
2024年  4月 17日 水曜日 21:39:00 JST

8 selinuxモジュールの使い方

selinuxモジュールは、SELinuxのモードを変更するモジュールです。

ターゲットノードのSELinuxのモードをEnforcingに変更するPlaybookを作成します。

[root@control ~]# vi test.yml
[root@control ~]# cat test.yml
- name: Sample Playbook
  hosts: nodes
  tasks:
    - name: Enable SELinux
      ansible.posix.selinux:
        policy: targeted
        state: enforcing

Playbookを実行する前にターゲットノードでSELinuxのモードを確認すると、Permissiveであることがわかります。

[root@node1 ~]# getenforce
Permissive

playbookを実行します。

[root@control ~]# ansible-playbook -i hosts.ini test.yml

PLAY [Sample Playbook] ***********************************************************************

TASK [Gathering Facts] ***********************************************************************
ok: [node1]

TASK [Enable SELinux] ************************************************************************
changed: [node1]

PLAY RECAP ***********************************************************************************
node1                      : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[root@control ~]#

Playbookを実行した後にターゲットノードでSELinuxのモードを確認すると、Enforcingに変更されたことがわかります。

[root@node1 ~]# getenforce
Enforcing

Ansibleの使い方(Handlersセクション編)

1 はじめに

1つのプレイブックは、1つ以上のプレイから構成されます。プレイは、以下のセクションから構成されます。本記事では、Handlersセクションについて動作確認をしてみます。

セクション名 概要
Targetsセクション ターゲットノードへの接続に関する情報を定義します
Tasksセクション 実行する処理を定義します
Handlersセクション notifyディレクティブで指定したタスクを実行するセクションです。サービスの再起動や設定の更新などに使用します
Varsセクション 変数を定義します

2 検証環境

動作検証をするための環境は以下のとおりです。enp1s0はNICの名前です。

                          192.168.122.0/24
control(enp1s0) -------------------------------------(enp1s0) node1
               .220                               .87
ホスト名 役割
control コントロールノードとして動作します。Ansibleをインストールします
node1 ターゲットノードとして動作します

コントロールノード、ターゲットノードのAlmaLinux版数は以下のとおりです。

[root@server ~]# cat /etc/redhat-release
AlmaLinux release 9.2 (Turquoise Kodkod)

コントロールノード、ターゲットノードのカーネル版数は以下のとおりです。

[root@server ~]# uname -r
5.14.0-284.11.1.el9_2.x86_64

3 事前準備

Ansibleのインストールや公開鍵認証の設定方法などは、Ansibleの使い方(モジュール編) - hana_shinのLinux技術ブログを参照してください。

4 インベントリファイルの作成

インベントリファイルを作成します。

[root@control ~]# cat hosts.ini
[nodes]
node1 ansible_host=192.168.122.220

5 notifyディレクティブの動作確認

notifyディレクティブあり/なしの場合の動作確認をしてみます。

5.1 notifyディレクティブありの場合

Playbookを作成します。このPlaybookは、Tasksセクションでfileモジュールを実行してファイルを作成します。ファイルが作成されると、notifyディレクティブが実行され、それによりHandlersセクション内のdebugモジュールが呼び出されます。debugモジュールが実行されると、指定されたメッセージが表示されます。

[root@control ~]# cat test.yml
- name: Sample Playbook
  hosts: nodes
  tasks:
    - name: Create file
      ansible.builtin.file:
        path: /tmp/test.txt
        state: touch
      notify: Display

  handlers:
    - name: Display
      ansible.builtin.debug:
        msg: The handlers section is called.

Playbookを実行すると、指定したメッセージが表示されることがわかります。

[root@control ~]# ansible-playbook -i hosts.ini test.yml

PLAY [Sample Playbook] **************************************************************************************

TASK [Gathering Facts] **************************************************************************************
ok: [node1]

TASK [Create file] ******************************************************************************************
changed: [node1]

RUNNING HANDLER [Display] ***********************************************************************************
ok: [node1] => {
    "msg": "The handlers section is called."
}

PLAY RECAP **************************************************************************************************
node1                      : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[root@control ~]#

5.2 notifyディレクティブなしの場合

Playbookを作成します。このPlaybookでは、リモートノードに対してファイルを作成するタスクを実行します。ただし、notifyディレクティブが定義されていないため、Tasksセクションでファイルを作成しても、Handlersセクションのdebugモジュールは実行されません。

[root@control ~]# vi test.yml
[root@control ~]# cat test.yml
- name: Sample Playbook
  hosts: nodes
  tasks:
    - name: Create file
      ansible.builtin.file:
        path: /tmp/test.txt
        state: touch

  handlers:
    - name: Display
      ansible.builtin.debug:
        msg: The handlers section is called.

Playbookを実行します。notifyディレクティブが定義されていないため、メッセージが表示されないことがわかります。

[root@control ~]# ansible-playbook -i hosts.ini test.yml

PLAY [Sample Playbook] **************************************************************************************

TASK [Gathering Facts] **************************************************************************************
ok: [node1]

TASK [Create file] ******************************************************************************************
changed: [node1]

PLAY RECAP **************************************************************************************************
node1                      : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[root@control ~]#

6 実践編(httpdの起動)

httpdパッケージをインストールしたら、httpdサービスを起動するplaybookを作成します。

[root@control ~]# vi test.yml
[root@control ~]# cat test.yml
- name: Sample Playbook
  hosts: nodes
  tasks:
    - name: Install Apache
      ansible.builtin.dnf:
        name: httpd
        state: present
      notify:
        - Start Apache

  handlers:
    - name: Start Apache
      ansible.builtin.service:
        name: httpd
        state: started

playbookを実行します。

[root@control ~]# ansible-playbook -i hosts.ini test.yml

PLAY [Sample Playbook] *******************************************************************

TASK [Gathering Facts] *******************************************************************
ok: [node1]

TASK [Install Apache] ********************************************************************
changed: [node1]

RUNNING HANDLER [Start Apache] ***********************************************************
changed: [node1]

PLAY RECAP *******************************************************************************
node1                      : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[root@control ~]#

httpdサービスの状態を確認するとactiveになっていることがわかります。

[root@node1 ~]# systemctl is-active httpd
active

Ansibleの使い方(配列、連想配列編)

1 はじめに

本記事では、配列、連想配列を使った簡単なplaybookを作成して、その動作確認をしてみます。

(1) 配列(シーケンス)
配列は、先頭に「-」(ハイフン)を付け、その後にスペースを空けて値を記述します。以下は、配列名がtest_arrayの例です。

test_array:
  - user1
  - user2
  - user3

(2) 連想配列マッピング
連想配列とは、配列の添え字にスカラー以外のデータ型(例:文字列)を使用できる配列のことです。Key(キー)のあとに「:」(コロン)を付け、その後スペースを空けてValue(バリュー)を記述します。以下は、配列名がtest_associative_arrayの例です。

test_associative_array:
  key1: value1
  key2: value2
  key3: value3

2 検証環境

動作検証をするための環境は以下のとおりです。enp1s0はNICの名前です。

                          192.168.122.0/24
control(enp1s0) -------------------------------------(enp1s0) node1
               .220                               .87
ホスト名 役割
control コントロールノードとして動作します。Ansibleをインストールします。
node1 ターゲットノードとして動作します。モジュールが実行されるホストです

コントロールノード、ターゲットノードのAlmaLinux版数は以下のとおりです。

[root@server ~]# cat /etc/redhat-release
AlmaLinux release 9.2 (Turquoise Kodkod)

コントロールノード、ターゲットノードのカーネル版数は以下のとおりです。

[root@server ~]# uname -r
5.14.0-284.11.1.el9_2.x86_64

3 事前準備

ansibleのインストールや公開鍵認証の設定方法などは、Ansibleの使い方(モジュール編) - hana_shinのLinux技術ブログを参照してください。

4 インベントリファイルの作成

管理対象となるサーバの一覧を記述したインベントリファイルを作成します。この後の動作確認でも、このインベントリファイルを使用します。node1 はターゲットノードのホスト名であり、ansible_host はターゲットノードのIPアドレスを定義しています。

[root@control ~]# cat hosts.ini
[nodes]
node1 ansible_host=192.168.122.220

5 配列の使い方

5.1 その1

Varsセクションで配列usersを定義します。Tasksセクションでは、loopディレクティブを使って配列に対して繰り返し処理を実行します。この繰り返し処理の中で、配列usersの要素であるuser1とuser2をuserモジュールのパラメータとして渡すことで、user1とuser2を作成します。なお、モジュールの使い方は、Ansibleの使い方(モジュール編) - hana_shinのLinux技術ブログを参照してください。

[root@control ~]# cat test.yml
- name: Sample Playbook
  hosts: nodes
  vars:
    users:
      - user1
      - user2
  tasks:
    - name: Add users
      ansible.builtin.user:
        name: "{{ item }}"
        state: present
      loop: "{{ users }}"

playbookを実行します。

[root@control ~]# ansible-playbook -i hosts.ini test.yml

PLAY [Sample Playbook] ********************************************************************************

TASK [Gathering Facts] ********************************************************************************
ok: [node1]

TASK [Add users] **************************************************************************************
changed: [node1] => (item=user1)
changed: [node1] => (item=user2)

PLAY RECAP ********************************************************************************************
node1                      : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[root@control ~]#

ターゲットノードで作成したユーザを確認すると、user1とuser2が作成されたことがわかります。

[root@node1 ~]# id user1
uid=1000(user1) gid=1000(user1) groups=1000(user1)
[root@node1 ~]# id user2
uid=1001(user2) gid=1001(user2) groups=1001(user2)

あと始末をします。

[root@node1 ~]# userdel user1
[root@node1 ~]# userdel user2

5.2 その2

Varsセクションで配列usersを定義します。Tasksセクションでは、loopディレクティブを使って配列に対して繰り返し処理を実行します。この繰り返し処理の中で、配列usersの要素であるユーザ名(user1,user2)とUID(5000,5001)をuserモジュールのパラメータとして渡すことで、user1(UID=5000)とuser2(UID=5001)を作成します。

[root@control ~]# cat test.yml
- name: Sample Playbook
  hosts: nodes
  vars:
    users:
      - name: user1
        uid: 5000
      - name: user2
        uid: 5001
  tasks:
    - name: Add users with specific UID.
      ansible.builtin.user:
        name: "{{ item.name }}"
        uid: "{{ item.uid }}"
        state: present
      loop: "{{ users }}"

playbookを実行します。

[root@control ~]# ansible-playbook -i hosts.ini test.yml

PLAY [Sample Playbook] ********************************************************************************

TASK [Gathering Facts] ********************************************************************************
ok: [node1]

TASK [Add users with specific UID.] *******************************************************************
changed: [node1] => (item={'name': 'user1', 'uid': 5000})
changed: [node1] => (item={'name': 'user2', 'uid': 5001})

PLAY RECAP ********************************************************************************************
node1                      : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[root@control ~]#

idコマンドを使って作成したユーザを確認すると、user1(uid=5000)とuser2(uid=5001)が作成されたことがわかります。

[root@node1 ~]# id user1
uid=5000(user1) gid=5000(user1) groups=5000(user1)
[root@node1 ~]# id user2
uid=5001(user2) gid=5001(user2) groups=5001(user2)

あと始末をします。

[root@node1 ~]# userdel user1
[root@node1 ~]# userdel user2

6 連想配列の使い方

6.1 その1

連想配列の使い方を確認するplaybookを作成します。Varsセクションで連想配列を定義します。Tasksセクションでは、loopディレクティブを使って連想配列に対して繰り返し処理を実行します。この繰り返し処理の中で、連想配列のキーとバリューをdebugモジュールのパラメータとして渡します。

[root@control ~]# vi test.yml
[root@control ~]# cat test.yml
- name: Sample Playbook
  hosts: nodes
  vars:
    test_associative_array:
      key1: value1
      key2: value2
      key3: value3
  tasks:
    - name: Print values from the associative array
      ansible.builtin.debug:
        msg: "{{ item.key }}: {{ item.value }}"
      loop: "{{ test_associative_array | dict2items }}"

playbookを実行すると、連想配列で定義してキーとバリューが出力されていることが確認できます。

[root@control ~]# ansible-playbook -i hosts.ini test.yml

PLAY [Sample Playbook] **********************************************************************************************************

TASK [Gathering Facts] **********************************************************************************************************
ok: [node1]

TASK [Print values from the associative array] **********************************************************************************
ok: [node1] => (item={'key': 'key1', 'value': 'value1'}) => {
    "msg": "key1: value1"
}
ok: [node1] => (item={'key': 'key2', 'value': 'value2'}) => {
    "msg": "key2: value2"
}
ok: [node1] => (item={'key': 'key3', 'value': 'value3'}) => {
    "msg": "key3: value3"
}

PLAY RECAP **********************************************************************************************************************
node1                      : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[root@control ~]#

6.2 その2

連想配列の使い方を確認するplaybookを作成します。Varsセクションで連想配列を定義します。Tasksセクションで、指定したキーに対するバリューを表示します。バリューをdebugモジュールのパラメータとして渡します。

[root@control ~]# cat test.yml
- name: Sample Playbook
  hosts: nodes
  vars:
    test_associative_array:
      key1: value1
      key2: value2
      key3: value3
  tasks:
    - name: Print value of key2 from the associative array
      ansible.builtin.debug:
        msg: "{{ test_associative_array['key2'] }}"

playbookを実行すると、キー (key2) に対応するバリュー (value2) が出力されていることが確認できます。

[root@control ~]# ansible-playbook -i hosts.ini test.yml

PLAY [Sample Playbook] ***************************************************************************************************

TASK [Gathering Facts] ***************************************************************************************************
ok: [node1]

TASK [Print value of key2 from the associative array] ********************************************************************
ok: [node1] => {
    "msg": "value2"
}

PLAY RECAP ***************************************************************************************************************
node1                      : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[root@control ~]#

Ansibleの使い方(マジック変数、ファクト変数編)

1 はじめに

本記事では、以下のドキュメントに記載されているマジック変数とファクト変数について動作確認をしてみます。
特別な変数 — Ansible Documentation

種類 概要
ファクト変数 ターゲットノード(リモートホスト)から収集したシステムに関する情報を保持する変数です。たとえば、ターゲットノードのホスト名やIPアドレスなどの情報を取得できます。ファクトはsetupというモジュールが実行されることにより、対象のターゲットノードでPythonスクリプトが実行され収集されます。
マジック変数 Ansible があらかじめ定義している変数です。マジック変数の中で、ターゲットノードに関する情報を設定した変数をファクト変数と呼びます。ユーザーはマジック変数に値を設定できません。

2 検証環境

動作検証をするための環境は以下のとおりです。enp1s0はNICの名前です。

                          192.168.122.0/24
control(enp1s0) -------------------------------------(enp1s0) node1
               .220                               .87
ホスト名 役割
control コントロールノードとして動作します。Ansibleをインストールします。
node1 ターゲットノードとして動作します。モジュールが実行されるホストです

コントロールノード、ターゲットノードのAlmaLinux版数は以下のとおりです。

[root@server ~]# cat /etc/redhat-release
AlmaLinux release 9.2 (Turquoise Kodkod)

コントロールノード、ターゲットノードのカーネル版数は以下のとおりです。

[root@server ~]# uname -r
5.14.0-284.11.1.el9_2.x86_64

3 事前準備

ansibleのインストールや公開鍵認証の設定方法などは、Ansibleの使い方(モジュール編) - hana_shinのLinux技術ブログを参照してください。

4 インベントリファイルの作成

管理対象となるサーバの一覧を記述したインベントリファイルを作成します。この後のモジュールの動作確認でも、このインベントリファイルを使用します。node1 はターゲットノードのホスト名であり、ansible_host はターゲットノードのIPアドレスを定義しています。

[root@control ~]# vi hosts.ini
[root@control ~]# cat hosts.ini
[nodes]
node1 ansible_host=192.168.122.220

5 ファクト変数を表示する方法

5.1 全てのファクト変数を表示する方法

全てのファクトを表示するplaybookを作成します。
playbook中のgather_facts,ansible_factsの意味は以下のとおりです。
・gather_facts:trueはファクトを収集する。falseはファクトを収集しない。
・ansible_facts:Ansibleが収集したファクトを格納する変数です。

[root@control ~]# vi test.yml
[root@control ~]# cat test.yml
- name: Sample Playbook
  hosts: nodes
  gather_facts: true
  tasks:
    - name: Display Gathered Facts
      debug:
        var: ansible_facts

playbookを実行すると、「Gathering Facts」と表示されるタイミングで、ターゲットノードからファクトが収集されていることがわかります。

[root@control ~]# ansible-playbook -i hosts.ini test.yml

PLAY [Sample Playbook] ************************************************************************************************

TASK [Gathering Facts] ************************************************************************************************
ok: [node1]

TASK [Display Gathered Facts] *****************************************************************************************
ok: [node1] => {
    "ansible_facts": {
        "all_ipv4_addresses": [
            "192.168.122.220"
        ],
        "all_ipv6_addresses": [
            "fe80::5054:ff:fe2a:749c"
        ],
        "ansible_local": {},
        "apparmor": {
            "status": "disabled"
        },
-snip-

5.2 特定のファクトを表示する方法

(1) カーネル版数を表示する方法

カーネル版数はansible_facts内で以下のように定義されています。したがって、カーネル版数は {{ ansible_facts.kernel }} で参照することができます。

    "ansible_facts": {

        -snip-

        "kernel": "5.14.0-284.11.1.el9_2.x86_64",
    }

カーネル版数を表示するplaybookを作成します。

[root@control ~]# vi test.yml
[root@control ~]# cat test.yml
- name: Sample Playbook
  hosts: node1
  gather_facts: true
  tasks:
    - name: Display Gathered Facts
      debug:
        msg: "The kernel version of the remote host is {{ ansible_facts.kernel }}"

playbookを実行すると、カーネル版数(5.14.0-284.11.1.el9_2.x86_64)が表示されていることがわかります。

[root@control ~]# ansible-playbook -i hosts.ini test.yml

PLAY [Sample Playbook] ************************************************************************************************

TASK [Gathering Facts] ************************************************************************************************
ok: [node1]

TASK [Display Gathered Facts] *****************************************************************************************
ok: [node1] => {
    "msg": "The kernel version of the remote host is 5.14.0-284.11.1.el9_2.x86_64"
}

PLAY RECAP ************************************************************************************************************
node1                      : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[root@control ~]#

(2) ホスト名を表示する方法
ホスト名はansible_facts内で以下のように定義されています。したがって、ホスト名は {{ ansible_facts.nodename }} で参照することができます。

    "ansible_facts": {

        -snip-

        "nodename": "node1",
    }

ターゲットノードのホスト名を表示するplaybookを作成します。

[root@control ~]# vi test.yml
[root@control ~]# cat test.yml
- name: Sample Playbook
  hosts: nodes
  gather_facts: true
  tasks:
    - name: Display Gathered Facts
      debug:
        msg: "The name of the remote host is {{ ansible_facts.nodename }}"

playbookを実行すると、ターゲットノードのホスト名(node1)が表示されていることがわかります。

[root@control ~]# ansible-playbook -i hosts.ini test.yml

PLAY [Sample Playbook] ************************************************************************************************

TASK [Gathering Facts] ************************************************************************************************
ok: [node1]

TASK [Display Gathered Facts] *****************************************************************************************
ok: [node1] => {
    "msg": "The name of the remote host is node1"
}

PLAY RECAP ************************************************************************************************************
node1                      : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[root@control ~]#

(3) インタフェース名を表示する方法
インタフェースはansible_facts内で以下のように定義されています。したがって、インタフェースは {{ ansible_facts.interfaces }} で参照することができます。

    "ansible_facts": {

        -snip-

        "interfaces": [
            "enp1s0",
            "lo"
        ],
    }

ターゲットノードが持っているインタフェース名を表示するplaybookを作成します。

[root@control ~]# vi test.yml
[root@control ~]# cat test.yml
- name: Sample Playbook
  hosts: nodes
  tasks:
    - name: Display Gathered Facts
      debug:
        msg: "The host has the interface {{ ansible_facts.interfaces }}"

playbookを実行すると、ターゲットノードは、2つのインタフェース(lo,enp1s0)を持っていることがわかります。

[root@control ~]# ansible-playbook -i hosts.ini test.yml

PLAY [Sample Playbook] ************************************************************************************************

TASK [Gathering Facts] ************************************************************************************************
ok: [node1]

TASK [Display Gathered Facts] *****************************************************************************************
ok: [node1] => {
    "msg": "The host has the interface ['enp1s0', 'lo']"
}

PLAY RECAP ************************************************************************************************************
node1                      : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[root@control ~]#

(4) IPアドレスを表示する方法

IPアドレスはansible_facts内で以下のように定義されています。したがって、IPアドレスは {{ ansible_facts.enp1s0.ipv4.address }}で参照することができます。

    "ansible_facts": {
        "enp1s0": {

            -snip-

            "ipv4": {
                "address": "192.168.122.220",
                "broadcast": "192.168.122.255",
                "netmask": "255.255.255.0",
                "network": "192.168.122.0",
                "prefix": "24"
            },

        },
    }

ターゲットノードのNIC(enp1s0)に設定されているIPアドレスを表示するplaybookを作成します。

[root@control ~]# vi test.yml
[root@control ~]# cat test.yml
- name: Sample Playbook
  hosts: nodes
  gather_facts: true
  tasks:
    - name: Display Gathered Facts
      debug:
        msg: "IP adress is {{ ansible_facts.enp1s0.ipv4.address }}"

playbookを実行すると、ターゲットノードのNIC(enp1s0)に設定されているIPアドレスが 192.168.122.220 であることがわかります。

[root@control ~]# ansible-playbook -i hosts.ini test.yml

PLAY [Sample Playbook] ************************************************************************************************

TASK [Gathering Facts] ************************************************************************************************
ok: [node1]

TASK [Display Gathered Facts] *****************************************************************************************
ok: [node1] => {
    "msg": "IP adress is 192.168.122.220"
}

PLAY RECAP ************************************************************************************************************
node1                      : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[root@control ~]#

6 マジック変数を表示する方法

マジック変数は、Ansible がシステム内の状態を反映する変数です。ユーザーは、マジック変数に値を設定することはできません。マジック変数の一覧は、特別な変数 — Ansible Documentationを参照してください。
https://docs.ansible.com/ansible/2.9_ja/user_guide/playbooks_variables.html

6.1 プレイブック名を表示する方法(ansible_play_name)

ansible_play_nameは、プレイブック名を表示するマジック変数です。ansible_play_nameを使ってplaybookを作成します。

[root@control ~]# vi test.yml
[root@control ~]# cat test.yml
- name: Sample Playbook
  hosts: nodes
  tasks:
    - name: Display Ansible Playbook Name
      debug:
        msg: "{{ ansible_play_name }}"

playbookを実行すると、プレイブックの名前(Sample Playbook)が表示されていることがわかります。

[root@control ~]# ansible-playbook -i hosts.ini test.yml

PLAY [Sample Playbook] ********************************************************************

TASK [Gathering Facts] ********************************************************************
ok: [node1]

TASK [Display Ansible Playbook Name] ******************************************************
ok: [node1] => {
    "msg": "Sample Playbook"
}

PLAY RECAP ********************************************************************************
node1                      : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[root@control ~]#

6.2 パッケージ管理ソフト名を表示する方法(ansible_pkg_mgr)

パッケージ管理ソフトウェアは、コンピュータにインストールされたソフトウェアの記録を管理し、新しいソフトウェアのインストール、既存ソフトウェアのアップデート、そして不要なソフトウェアの削除を容易に行えるソフトウェアです。具体例としては、yum(RHEL7など)、dnf(RHEL 8、9など)、およびyast(SLES)などがあります。ansible_pkg_mgrを使ってplaybookを作成します。

[root@control ~]# vi test.yml
[root@control ~]# cat test.yml
- name: Sample Playbook
  hosts: nodes
  tasks:
    - name: debug ansible_hostname
      debug:
        msg: "{{ansible_pkg_mgr}}"

playbookを実行すると、ターゲットノードのパッケージ管理ソフトがdnfであることがわかります。

[root@control ~]# ansible-playbook -i hosts.ini test.yml

PLAY [Sample Playbook] ************************************************************************************************

TASK [Gathering Facts] ************************************************************************************************
ok: [node1]

TASK [debug ansible_hostname] *****************************************************************************************
ok: [node1] => {
    "msg": "dnf"
}

PLAY RECAP ************************************************************************************************************
node1                      : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[root@control ~]#

6.3 インベントリファイル内のホスト名を表示する方法(inventory_hostname)

inventory_hostnameは、インベントリファイル内のホスト名を示すマジック変数です。この変数を使用してPlaybookを作成することができ、これにより特定のホストに対してロールを動的に適用することができます。なお、inventory_hostname_shortは、FQDNのホスト名の部分だけを示すマジック変数です。たとえば、インベントリファイル内でnode1.example.comと定義されていた場合、inventory_hostname_shortはnode1を示すことになります。

[root@control ~]# cat test.yml
- name: Sample Playbook
  hosts: nodes
  tasks:
    - name: debug ansible_hostname
      debug:
        msg: "{{inventory_hostname}}"

playbookを実行すると、インベントリファイル内のホスト名であるnode1が出力されていることがわかります。

[root@control ~]# ansible-playbook -i hosts.ini test.yml

PLAY [Sample Playbook] ************************************************************************************************

TASK [Gathering Facts] ************************************************************************************************
ok: [node1]

TASK [debug ansible_hostname] *****************************************************************************************
ok: [node1] => {
    "msg": "node1"
}

PLAY RECAP ************************************************************************************************************
node1

CPUQuotaの使い方

1 CPUQuotaとは?

CPUQuotaは、プロセスに割り当てられるCPUリソースの割合を制限するための機能です。CPUQuotaを設定すると、特定のプロセスがシステム全体のCPUリソースのうち、一定の割合しか利用できなくなります。例えば、CPUQuotaに20%を設定すると、そのユニットファイルで指定されたプロセスは、システム上のCPUリソースのうち20%まで利用できるようになります。これにより、他の重要なプロセスが十分なCPUリソースを利用できるようになり、システム全体の安定性やパフォーマンスを向上させることができます。

本記事では、CPUQuotaにCPU使用率を設定して、その効果を検証してみます。

2 検証環境

AlmaLinux版数は以下のとおりです。

[root@server ~]# cat /etc/redhat-release
AlmaLinux release 9.2 (Turquoise Kodkod)

カーネル版数は以下のとおりです。

[root@server ~]# uname -r
5.14.0-284.11.1.el9_2.x86_64

仮想マシンに搭載しているCPU数は4個です。lscpuの使い方は、lscpuコマンドの使い方 - hana_shinのLinux技術ブログを参照してください。

[root@server ~]# lscpu -ae
CPU NODE SOCKET CORE L1d:L1i:L2:L3 ONLINE
  0    0      0    0 0:0:0:0          yes
  1    0      1    1 1:1:1:1          yes
  2    0      2    2 2:2:2:2          yes
  3    0      3    3 3:3:3:3          yes

3 事前準備(stress-ngパッケージのインストール)

CPU負荷をかけるため、stress-ngパッケージをインストールします。stress-ngパッケージはepelリポジトリにあるので、まず、epel-releaseパッケージをインストールします。

[root@server ~]# yum -y install epel-release

次に、stress-ngパッケージをインストールします。

[root@server ~]# yum -y install stress-ng

stress-ngコマンドの版数を確認します。

[root@server ~]# stress-ng -V
stress-ng, version 0.15.00 (gcc 11.3, x86_64 Linux 5.14.0-284.11.1.el9_2.x86_64) ?諮

なお、stress-ngコマンドの使い方は、stress-ngコマンドの使い方 - hana_shinのLinux技術ブログを参照してください。

4 動作確認(CPUQuotaを設定しない場合)

4.1 実験1(1つのCPUに負荷をかけた場合)

1つのCPUにCPU使用率40%の負荷をかけるユニットファイルを作成します。

[root@server ~]# vi /etc/systemd/system/stress-ng.service
[root@server ~]# cat /etc/systemd/system/stress-ng.service
[Unit]
Description=CPUQuotaTest

[Service]
ExecStart=stress-ng -k -c 1 -l 40

[Install]
WantedBy=multi-user.target

ユニットファイルを変更したので、Systemdに設定ファイルの再読み込みを指示します。

[root@server ~]# systemctl daemon-reload

stress-ngサービスを起動します。

[root@server ~]# systemctl start stress-ng.service

psコマンドを使ってstress-ngプロセスのCPU使用率を確認すると、CPU1の使用率が約40%であることがわかります。なお、psコマンドの使い方は、psコマンドの使い方 - hana_shinのLinux技術ブログを参照してください。

[root@server ~]# ps -C stress-ng -o comm,pid,ppid,psr,%cpu,wchan
COMMAND             PID    PPID PSR %CPU WCHAN
stress-ng          2328       1   3  0.1 do_wait
stress-ng          2329    2328   1 39.7 -

stress-ngサービスを停止します。

[root@server ~]# systemctl stop stress-ng.service

4.2 実験2(2つのCPUに負荷をかけた場合)

2つのCPUにCPU使用率40%の負荷をかけるユニットファイルを作成します。

[root@server ~]# vi /etc/systemd/system/stress-ng.service
[root@server ~]# cat /etc/systemd/system/stress-ng.service
[Unit]
Description=CPUQuotaTest

[Service]
ExecStart=stress-ng -k -c 2 -l 40

[Install]
WantedBy=multi-user.target

ユニットファイルを変更したので、Systemdに設定ファイルの再読み込みを指示します。

[root@server ~]# systemctl daemon-reload

stress-ngサービスを起動します。

[root@server ~]# systemctl start stress-ng.service

psコマンドを使ってstress-ngプロセスのCPU使用率を確認すると、CPU2,CPU3の使用率が約40%であることがわかります。

[root@server ~]# ps -C stress-ng -o comm,pid,ppid,psr,%cpu,wchan
COMMAND             PID    PPID PSR %CPU WCHAN
stress-ng          1378       1   0  0.1 do_wait
stress-ng          1379    1378   2 39.2 -
stress-ng          1380    1378   3 39.1 -

stress-ngサービスを停止します。

[root@server ~]# systemctl stop stress-ng.service

4.3 実験3(3つのCPUに負荷をかけた場合)

3つのCPUにCPU使用率40%の負荷をかけるユニットファイルを作成します。

[root@server ~]# vi /etc/systemd/system/stress-ng.service
[root@server ~]# cat /etc/systemd/system/stress-ng.service
[Unit]
Description=CPUQuotaTest

[Service]
ExecStart=stress-ng -k -c 3 -l 40

[Install]
WantedBy=multi-user.target

ユニットファイルを変更したので、Systemdに設定ファイルの再読み込みを指示します。

[root@server ~]# systemctl daemon-reload

stress-ngサービスを起動します。

[root@server ~]# systemctl start stress-ng.service

psコマンドを使ってstress-ngプロセスのCPU使用率を確認すると、CPU1,CPU2,CPU3の使用率が約40%であることがわかります。

[root@server ~]# ps -C stress-ng -o comm,pid,ppid,psr,%cpu,wchan
COMMAND             PID    PPID PSR %CPU WCHAN
stress-ng          1416       1   0  0.0 do_wait
stress-ng          1417    1416   1 39.2 do_select
stress-ng          1418    1416   2 39.0 do_select
stress-ng          1419    1416   3 39.2 do_select

stress-ngサービスを停止します。

[root@server ~]# systemctl stop stress-ng.service

4.4 実験4(4つのCPUに負荷をかけた場合)

4つのCPUにCPU使用率40%の負荷をかけるユニットファイルを作成します。

[root@server ~]# vi /etc/systemd/system/stress-ng.service
[root@server ~]# cat /etc/systemd/system/stress-ng.service
[Unit]
Description=CPUQuotaTest

[Service]
ExecStart=stress-ng -k -c 4 -l 40

[Install]
WantedBy=multi-user.target

ユニットファイルを変更したので、Systemdに設定ファイルの再読み込みを指示します。

[root@server ~]# systemctl daemon-reload

stress-ngサービスを起動します。

[root@server ~]# systemctl start stress-ng.service

psコマンドを使ってstress-ngプロセスのCPU使用率を確認すると、CPU0,CPU1,CPU2,CPU3の使用率が約40%であることがわかります。

[root@server ~]# ps -C stress-ng -o comm,pid,ppid,psr,%cpu,wchan
COMMAND             PID    PPID PSR %CPU WCHAN
stress-ng          2365       1   1  0.0 do_wait
stress-ng          2366    2365   0 39.6 do_select
stress-ng          2367    2365   3 39.6 do_select
stress-ng          2368    2365   2 39.7 do_select
stress-ng          2369    2365   1 39.7 do_select

stress-ngサービスを停止します。

[root@server ~]# systemctl stop stress-ng.service

5 動作確認(CPUQuota=10%を設定した場合)

5.1 実験5(1つのCPUに負荷をかけた場合)

1つのCPUでCPU使用率が40%になるようにユニットファイルを作成します。このとき、CPUQuotaに10%を設定します。

[root@server ~]# vi /etc/systemd/system/stress-ng.service
[root@server ~]# cat /etc/systemd/system/stress-ng.service
[Unit]
Description=CPUQuotaTest

[Service]
ExecStart=stress-ng -k -c 1 -l 40
CPUQuota=10%

[Install]
WantedBy=multi-user.target

ユニットファイルを変更したので、Systemdに設定ファイルの再読み込みを指示します。

[root@server ~]# systemctl daemon-reload

stress-ngサービスを起動します。

[root@server ~]# systemctl start stress-ng.service

psコマンドを使用して、stress-ngプロセスのCPU使用率を確認しました。CPU0のCPU使用率が8.8%であるため、stress-ngプロセスはシステム全体でCPUを8.8%使用しています。CPUQuotaとして10%を指定しているため、ほぼ期待どおりのCPU使用率であることが分かります。

[root@server ~]# ps -C stress-ng -o comm,pid,ppid,psr,%cpu,wchan
COMMAND             PID    PPID PSR %CPU WCHAN
stress-ng          2105       1   3  0.0 do_wait
stress-ng          2106    2105   0  8.8 -

stress-ngサービスを停止します。

[root@server ~]# systemctl stop stress-ng.service

5.2 実験6(2つのCPUに負荷をかけた場合)

2つのCPUでCPU使用率が40%になるようにユニットファイルを作成します。このとき、CPUQuotaに10%を設定します。

[root@server ~]# vi /etc/systemd/system/stress-ng.service
[root@server ~]# cat /etc/systemd/system/stress-ng.service
[Unit]
Description=CPUQuotaTest

[Service]
ExecStart=stress-ng -k -c 2 -l 40
CPUQuota=10%

[Install]
WantedBy=multi-user.target

ユニットファイルを変更したので、Systemdに設定ファイルの再読み込みを指示します。

[root@server ~]# systemctl daemon-reload

stress-ngサービスを起動します。

[root@server ~]# systemctl start stress-ng.service

psコマンドを使用して、stress-ngプロセスのCPU使用率を確認しました。各CPUの使用率は以下の通りです:CPU1=5.0%、CPU3=4.6%。したがって、stress-ngプロセスはシステム全体でCPUを合計で9.6%使用しています。CPUQuotaとして10%を設定しているため、ほぼ期待どおりのCPU使用率であることがわかります。

[root@server ~]# ps -C stress-ng -o comm,pid,ppid,psr,%cpu,wchan
COMMAND             PID    PPID PSR %CPU WCHAN
stress-ng          2163       1   0  0.2 do_wait
stress-ng          2164    2163   3  4.6 -
stress-ng          2165    2163   1  5.0 -

stress-ngサービスを停止します。

[root@server ~]# systemctl stop stress-ng.service

5.3 実験7(3つのCPUに負荷をかけた場合)

3つのCPUでCPU使用率が40%になるようにユニットファイルを作成します。このとき、CPUQuotaに10%を設定します。

[root@server ~]# vi /etc/systemd/system/stress-ng.service
[root@server ~]# cat /etc/systemd/system/stress-ng.service
[Unit]
Description=CPUQuotaTest

[Service]
ExecStart=stress-ng -k -c 3 -l 40
CPUQuota=10%

[Install]
WantedBy=multi-user.target

ユニットファイルを変更したので、Systemdに設定ファイルの再読み込みを指示します。

[root@server ~]# systemctl daemon-reload

stress-ngサービスを起動します。

[root@server ~]# systemctl start stress-ng.service

psコマンドを使用して、stress-ngプロセスのCPU使用率を確認しました。各CPUの使用率は以下の通りです:CPU0=3.2%、CPU2=3.3%、CPU3=3.3%。したがって、stress-ngプロセスはシステム全体でCPUを合計で9.8%使用しています。CPUQuotaとして10%を設定しているため、ほぼ期待どおりのCPU使用率であることが分かります。

[root@server ~]# ps -C stress-ng -o comm,pid,ppid,psr,%cpu,wchan
COMMAND             PID    PPID PSR %CPU WCHAN
stress-ng          2198       1   2  0.0 do_wait
stress-ng          2199    2198   0  3.2 -
stress-ng          2200    2198   3  3.3 -
stress-ng          2201    2198   2  3.3 -

stress-ngサービスを停止します。

[root@server ~]# systemctl stop stress-ng.service

5.4 実験8(4つのCPUに負荷をかけた場合)

4つのCPUでCPU使用率が40%になるようにユニットファイルを作成します。このとき、CPUQuotaに10%を設定します。

[root@server ~]# vi /etc/systemd/system/stress-ng.service
[root@server ~]# cat /etc/systemd/system/stress-ng.service
[Unit]
Description=CPUQuotaTest

[Service]
ExecStart=stress-ng -k -c 4 -l 40
CPUQuota=10%

[Install]
WantedBy=multi-user.target

ユニットファイルを変更したので、Systemdに設定ファイルの再読み込みを指示します。

[root@server ~]# systemctl daemon-reload

stress-ngサービスを起動します。

[root@server ~]# systemctl start stress-ng.service

psコマンドを使用して、stress-ngプロセスのCPU使用率を確認しました。各CPUの使用率は以下の通りです:CPU0=2.4%、CPU1=2.4%、CPU2=2.1%、CPU3=2.2%。したがって、stress-ngプロセスはシステム全体でCPUを合計で9.1%使用しています。CPUQuotaとして10%を設定しているため、ほぼ期待どおりのCPU使用率であることが分かります。

[root@server ~]# ps -C stress-ng -o comm,pid,ppid,psr,%cpu,wchan
COMMAND             PID    PPID PSR %CPU WCHAN
stress-ng          2234       1   2  0.1 do_wait
stress-ng          2235    2234   3  2.2 -
stress-ng          2236    2234   2  2.1 -
stress-ng          2237    2234   1  2.4 -
stress-ng          2238    2234   0  2.4 -

stress-ngサービスを停止します。

[root@server ~]# systemctl stop stress-ng.service

6 まとめ

(1) CPUQuota未使用時のCPU使用率
CPUQuotaを指定していないため、stress-ngコマンドの引数で指定したCPU使用率(40%)までCPUが使用されていることが分かります。

CPU0 CPU1 CPU2 CPU3
実験1 - 39.7 - -
実験2 - - 39.2 39.1
実験3 - 39.2 39.0 39.2
実験4 39.6 39.7 39.7 39.6

(2) CPUQuota=10%を指定した時のCPU使用率
CPU使用率の合計がCPUQuotaで指定したCPU使用率(10%)未満になっていることがわかります。

CPU0 CPU1 CPU2 CPU3 CPU使用率の合計
実験5 8.8 - - - 8.8
実験6 - 5.0 - 4.6 9.6
実験7 3.2 - 3.3 3.3 9.8
実験8 2.4 2.4 2.1 2.2 9.1

Z 参考情報

私が業務や記事執筆で参考にした書籍を以下のページに記載します。
Linux技術のスキルアップをしよう! - hana_shinのLinux技術ブログ