** 서종호(가시다)님의 On-Premise K8s Hands-on Study 2주차 학습 내용을 기반으로 합니다. ** 

 

어제 포스팅에서 Ansible 설치를 해보고 Ansible의 기본적인 구성 요소들에 대한 설명을 했었다. 설치는 pip3를 통해 간단하게 진행할 수 있으니, 설치는 했다고 가정하고 진행해보려고 한다.

 

인벤토리 생성

인벤토리는 컨트롤 노드가 관리할 노드를 나열해둔 파일로 INI, YAML 모두 지원한다. 여기에서는 INI로 작성해보려고 한다. 

원래는 그냥 나열만 해도 된다.

$ cat inventory
10.10.1.1
10.10.1.2
10.10.1.3

그런데 위와 같이 하면 뭐가 어떤 노드인지 모른다. 그래서 이름을 /etc/hosts 기반으로 바꿔도 된다.

$ cat inventory
tnode1
tnode2
tnode3

이렇게 노드 이름을 붙일 수 있다. 그런데 관리할 노드가 엄청 많아지면 어떤 호스트가 무슨 역할의 노드인지 모를 수 있다. 그래서 그룹을 지어줄 수 있다.

$ cat inventory
[web]
tnode1 ansible_python_interpreter=/usr/bin/python3
tnode2 ansible_python_interpreter=/usr/bin/python3

[db]
tnode3 ansible_python_interpreter=/usr/bin/python3

[all:children]
web
db

$ ansible-inventory -i ./inventory --list | more
{
    "_meta": {
        "hostvars": {
            "tnode1": {
                "ansible_all_ipv4_addresses": [
                    {
                        "__ansible_unsafe": "10.10.1.11"
                    },
                    {
                        "__ansible_unsafe": "10.0.2.15"
                    }
                ],
                "ansible_all_ipv6_addresses": [
                    {
                        "__ansible_unsafe": "fe80::a00:27ff:fe83:a1fc"
                    },
                    {
                        "__ansible_unsafe": "fd17:625c:f037:2:a00:27ff:fef8:c2eb"
                    },
                    {
                        "__ansible_unsafe": "fe80::a00:27ff:fef8:c2eb"
                    }
                ],
                "ansible_apparmor": {
                    "status": {
                        "__ansible_unsafe": "enabled"
                    }
                },
                "ansible_architecture": {
                    "__ansible_unsafe": "x86_64"
                },
                "ansible_bios_date": {
                    "__ansible_unsafe": "12/01/2006"
                },
                "ansible_bios_vendor": {
                    "__ansible_unsafe": "innotek GmbH"
                },
                "ansible_bios_version": {
                    "__ansible_unsafe": "VirtualBox"
                },
                "ansible_board_asset_tag": {
                    "__ansible_unsafe": "NA"
                },
                "ansible_board_name": {
                    "__ansible_unsafe": "VirtualBox"
                },
                "ansible_board_serial": {
                    "__ansible_unsafe": "0"
                },
                "ansible_board_vendor": {
                    "__ansible_unsafe": "Oracle Corporation"
                },
                "ansible_board_version": {
                    "__ansible_unsafe": "1.2"
                },
                "ansible_chassis_asset_tag": {
                    "__ansible_unsafe": "NA"
                },
                "ansible_chassis_serial": {
                    "__ansible_unsafe": "NA"
                ... < 중략 > ...

 

이렇게 하면 tnode1, tnode2는 web 그룹이고, tnode3이 db 노드이구나~ 하고 알 수 있다. 그리고 Ansible에서 inventory 작성할 때 다행히도 정규 표현식 같은 표현을 지원한다.

가령, 100개의 web 서버 이름을 넣으려고 하면

[webservers]
web[1:100].example.com

[db-servers]
db[01:09].example.com

## a.dns.example.com, b.dns.example.com, c.dns.example.com을 의미함.
[dns]
[a:c].dns.example.com

이런식으로 작성할 수도 있다고 한다. 와 편해.. 

인벤토리는 아래와 같이 간단하게 그래프 형태로도 프린트할 수 있다.

$ ansible-inventory -i ./inventory --graph
@all:
  |--@ungrouped:
  |--@web:
  |  |--tnode1
  |  |--tnode2
  |--@db:
  |  |--tnode3

이렇게 만든 inventory 파일을 실제로 적용하려고 하면 ansible.cfg 파일에 적용해주면 된다.

$ cat ansible.cfg
[defaults]
inventory = ./inventory
remote_user = root
ask_pass = false

[privilege_escalation]
become = true
become_method = sudo
become_user = root
become_ask_pass = false

"./inventory" 로 적은 부분으로 인해 inventory 파일을 적용할 수 있다. 

 

플레이북

이제 인벤토리를 작성했으니, 플레이북을 작성해보고 실행해보자. 그런데 그 전에 ad-hoc 커맨드에 대해서 먼저 알아보자.

 

ad-hoc 커맨드는 플레이북을 실행하지 않고 각각의 관리 노드에 간단하게 ansible을 통해 명령어를 실행해보는 것이다. 간단하게 ping 테스트를 해볼 수 있다.

$ ansible -m ping web
tnode2 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
tnode1 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

web은 인벤토리에서 설정한데로 tnode1, tnode2이다. 두 노드가 정상적으로 작동중이기 때문에 tnode1 | SUCCESS 와 같이 출력된다. CLI 명령어를 직접 실행하고 싶으면 아래와 같이 "-m shell"을 옵션으로 넣어 실행시키면 된다.

# db 그룹에만 실행
$ ansible -m shell -a uptime db
tnode3 | CHANGED | rc=0 >>
 13:01:48 up 16:28,  1 user,  load average: 0.00, 0.00, 0.00
 
# web 그룹에만 실행
$ ansible -m shell -a "free -h" web
tnode1 | CHANGED | rc=0 >>
               total        used        free      shared  buff/cache   available
Mem:           1.4Gi       347Mi       585Mi       1.0Mi       685Mi       1.1Gi
Swap:          2.9Gi          0B       2.9Gi
tnode2 | CHANGED | rc=0 >>
               total        used        free      shared  buff/cache   available
Mem:           1.4Gi       349Mi       590Mi       1.0Mi       678Mi       1.1Gi
Swap:          2.9Gi          0B       2.9Gi

# 모든 노드에 실행
$ ansible -m shell -a "tail -n 3 /etc/passwd" all
tnode3 | CHANGED | rc=0 >>
tcpdump:x:72:72::/:/sbin/nologin
vagrant:x:1000:1000::/home/vagrant:/bin/bash
vboxadd:x:991:1::/var/run/vboxadd:/bin/false
tnode1 | CHANGED | rc=0 >>
sshd:x:109:65534::/run/sshd:/usr/sbin/nologin
vagrant:x:1000:1000:vagrant:/home/vagrant:/bin/bash
vboxadd:x:999:1::/var/run/vboxadd:/bin/false
tnode2 | CHANGED | rc=0 >>
sshd:x:109:65534::/run/sshd:/usr/sbin/nologin
vagrant:x:1000:1000:vagrant:/home/vagrant:/bin/bash
vboxadd:x:999:1::/var/run/vboxadd:/bin/false

 

자 본격적으로 플레이북을 작성해보자.

$ cat first-playbook.yml
---
- hosts: all
  tasks:
    - name: Print message
      debug:
        msg: Hello CloudNet@ Ansible Study

$ ansible-playbook first-playbook.yml

PLAY [all] **********************************************************************************************************************************************************************************************************************************

TASK [Print message] ************************************************************************************************************************************************************************************************************************
ok: [tnode2] => {
    "msg": "Hello CloudNet@ Ansible Study"
}
ok: [tnode1] => {
    "msg": "Hello CloudNet@ Ansible Study"
}
ok: [tnode3] => {
    "msg": "Hello CloudNet@ Ansible Study"
}

PLAY RECAP **********************************************************************************************************************************************************************************************************************************
tnode1                     : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
tnode2                     : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
tnode3                     : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

참고로 작성한 플레이북의 yml 파일의 문법을 체크해볼 수도 있다.

# 정상적일 경우
$ ansible-playbook --syntax-check first-playbook.yml

playbook: first-playbook.yml


# 에러를 포함할 경우
$ ansible-playbook --syntax-check first-playbook-with-error.yml
[ERROR]: conflicting action statements: debug, msg
Origin: /root/my-ansible/first-playbook-with-error.yml:4:7

2 - hosts: all
3   tasks:
4     - name: Print message
        ^ column 7

이제 ssh 서비스를 재시작하는 플레이북을 만들어보자.

---
- hosts: all
  tasks:
    - name: Restart sshd service
      ansible.builtin.service:
        name: ssh # sshd
        state: restarted

위 플레이북을 실행하면 에러가 발생한다.

$ ansible-playbook restart-service-error.yml
PLAY [all] **********************************************************************************************************************************************************************************************************************************

TASK [Restart sshd service] *****************************************************************************************************************************************************************************************************************
[ERROR]: Task failed: Module failed: Could not find the requested service ssh: host
Origin: /root/my-ansible/restart-service-error.yml:4:7

2 - hosts: all
3   tasks:
4     - name: Restart sshd service
        ^ column 7

fatal: [tnode3]: FAILED! => {"changed": false, "msg": "Could not find the requested service ssh: host"}
changed: [tnode1]
changed: [tnode2]

PLAY RECAP **********************************************************************************************************************************************************************************************************************************
tnode1                     : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
tnode2                     : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
tnode3                     : ok=0    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

tnode3는 RedHat 계열의 노드이다. RedHat 계열에서는 ssh가 아니라 sshd이다. Ansible은 플레이북을 작성할 때 when이라는 키워드를 통해서 조건 값을 넣어줄 수 있다.

$ cat restart-service.yml
- hosts: all
  tasks:
    - name: Restart SSH on Debian
      ansible.builtin.service:
        name: ssh
        state: restarted
      when: ansible_facts['os_family'] == 'Debian'

    - name: Restart SSH on RedHat
      ansible.builtin.service:
        name: sshd
        state: restarted
      when: ansible_facts['os_family'] == 'RedHat'

$ ansible-playbook restart-service.yml

PLAY [all] **********************************************************************************************************************************************************************************************************************************

TASK [Restart SSH on Debian] ****************************************************************************************************************************************************************************************************************
skipping: [tnode3]
changed: [tnode1]
changed: [tnode2]

TASK [Restart SSH on RedHat] ****************************************************************************************************************************************************************************************************************
skipping: [tnode1]
skipping: [tnode2]
changed: [tnode3]

PLAY RECAP **********************************************************************************************************************************************************************************************************************************
tnode1                     : ok=1    changed=1    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0
tnode2                     : ok=1    changed=1    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0
tnode3                     : ok=1    changed=1    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

다음 절에서 설명할 것이지만, 미리 좀 말하면 팩트라는 값을 통해서 os 종류를 변수 형태로 가지고 있을 수 있다. ansible_facts['os_family']라고 하면 각 관리 노드가 RedHat 계열인지 Debian 계열인지 등을 알 수 있다.

 

팩트

팩트를 설명하기 전에 플레이북이 실행되는 과정을 먼저 봐보자.

https://juwon8891.github.io/2026/01/17/k8s-deploy-week2-ansible-basics/

 

[K8s-Deploy] Week 2 - Ansible 기초

주차 소개 Week 2에서는 Ansible의 기초를 학습합니다. Ansible은 에이전트리스(Agentless) 아키텍처를 기반으로 SSH를 통해 서버를 관리하는 자동화 도구입니다. 선언적 문법(YAML)과 멱등성(Idempotency)을

juwon8891.github.io

위 그림을 보면 SSH Connect 시도 후 Gather facts를 실행하는데, 컨트롤 노드가 관리 노드에 SSH를 붙은 후에 여러 정보들을 수집하는 과정임을 알 수 있다. 수집된 정보들은 ansible_facts를 통해 컨트롤 노드로 수집된다.  facts가 어떤 값을 가지고 있는지 알아보면 아래와 같다.

$ cat facts.yml
---

- hosts: db

  tasks:
  - name: Print all facts
    ansible.builtin.debug:
      var: ansible_facts

$ ansible-playbook facts.yml

PLAY [db] ***********************************************************************************************************************************************************************************************************************************

TASK [Print all facts] **********************************************************************************************************************************************************************************************************************
ok: [tnode3] => {
    "ansible_facts": {
        "all_ipv4_addresses": [
            "10.0.2.15",
            "10.10.1.13"
        ],
        "all_ipv6_addresses": [
            "fd17:625c:f037:2:a00:27ff:fe6f:16b4",
            "fe80::a00:27ff:fe6f:16b4",
            "fe80::a00:27ff:fec1:3efc"
        ],
        "ansible_local": {},
        "apparmor": {
            "status": "disabled"
        },
        "architecture": "x86_64",
        "bios_date": "12/01/2006",
        "bios_vendor": "innotek GmbH",
        "bios_version": "VirtualBox",
        "board_asset_tag": "NA",
        "board_name": "VirtualBox",
        "board_serial": "0",
        "board_vendor": "Oracle Corporation",
        "board_version": "1.2",
        "chassis_asset_tag": "NA",
        "chassis_serial": "NA",
        "chassis_vendor": "Oracle Corporation",
        "chassis_version": "NA",
        "cmdline": {
            "BOOT_IMAGE": "(hd0,msdos2)/boot/vmlinuz-5.14.0-570.52.1.el9_6.x86_64",
            "console": "ttyS0,115200n8",
            "no_timer_check": true,
            "resume": "UUID=2635f2a2-b00d-4ea8-b7ae-83a0dbab13b1",
            "ro": true,
            "root": "UUID=857c800e-4079-4023-b9be-38c4dc944806"
        },
        "date_time": {
            "date": "2026-01-18",
            "day": "18",
            "epoch": "1768671545",
            "epoch_int": "1768671545",
            "hour": "02",
            "iso8601": "2026-01-17T17:39:05Z",
            "iso8601_basic": "20260118T023905564860",
            "iso8601_basic_short": "20260118T023905",
            "iso8601_micro": "2026-01-17T17:39:05.564860Z",
            "minute": "39",
            "month": "01",
            "second": "05",
            "time": "02:39:05",
            "tz": "KST",
            "tz_dst": "KST",
            "tz_offset": "+0900",
            "weekday": "Sunday",
            "weekday_number": "0",
            "weeknumber": "02",
            "year": "2026"
        },
        "default_ipv4": {
            "address": "10.0.2.15",
            "alias": "enp0s3",
            "broadcast": "10.0.2.255",
            "gateway": "10.0.2.2",
            "interface": "enp0s3",
            "macaddress": "08:00:27:6f:16:b4",
            "mtu": 1500,
            "netmask": "255.255.255.0",
            "network": "10.0.2.0",
            "prefix": "24",
            "type": "ether"
        },
        .... <중략> ....
        "processor_cores": 2,
        "processor_count": 1,
        "processor_nproc": 2,
        "processor_threads_per_core": 1,
        "processor_vcpus": 2,
        "product_name": "VirtualBox",
        "product_serial": "VirtualBox-1741655f-ab00-4384-9aba-2fa3bd5cec6a",
        "product_uuid": "5f654117-00ab-8443-9aba-2fa3bd5cec6a",
        "product_version": "1.2",
        "python": {
            "executable": "/usr/bin/python3",
            "has_sslcontext": true,
            "type": "cpython",
            "version": {
                "major": 3,
                "micro": 21,
                "minor": 9,
                "releaselevel": "final",
                "serial": 0
            },
            "version_info": [
                3,
                9,
                21,
                "final",
                0
            ]
        },
        "python_version": "3.9.21",
        "real_group_id": 0,
        "real_user_id": 0,
        "selinux": {
            "config_mode": "permissive",
            "mode": "permissive",
            "policyvers": 33,
            "status": "enabled",
            "type": "targeted"
        },
        "selinux_python_present": true,
        "service_mgr": "systemd",
        "ssh_host_key_ecdsa_public": "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBGPshAekxOswu/JODuLJjSH6xr8XTV/HPnnNhle8uCRLOKIBQFDd0AtdFYpjSVO7U2APoNuwzWy0419qz6ftNC0=",
        "ssh_host_key_ecdsa_public_keytype": "ecdsa-sha2-nistp256",
        "ssh_host_key_ed25519_public": "AAAAC3NzaC1lZDI1NTE5AAAAIEWg0xPy7I/ESDGi6VpaQ1sTPpjxN0I2w/50a5rHwDm9",
        "ssh_host_key_ed25519_public_keytype": "ssh-ed25519",
        "ssh_host_key_rsa_public": "AAAAB3NzaC1yc2EAAAADAQABAAABgQCUlVAW8dXJHOVUIPotot1kKw5N3t9OOkn8z+UWWtCb9BJz8bshxz590cSxmxynbx+sNzPpF+h8JIirC7/AqKc4+gSWLm+SfyYV+zGZqmKf25rCic2bQ62hdtIk6liXOgfVuMvVWPeXM8L71jhKqQ27Zg9Mm4PwgIMVpMhc/jJkYbeZe5DGli4GoXmO33PTqA3MWY/hWKbhnCSIrkfCMOvQZX+4Gt+Xq+O1XZsvCWJ3Kspt1pxjpopg/vRaQi4Ke1CCbz46KRT1Oc37fUIpwkSEO5B4Ha+wkPJRc1CQwj7sJ4vQ4BCJbq7XUY2/4Q0Vei7TvT8RVK/+pmFgWcTrbNG2z1saKLAEFoADD2JhWPqTiXyQgKrZIyB0AnLfbW+Y2wM12d2ov3mWdV7NsCQpnlfvAGax01RkO0YrGv6kAJaafuxNqzDdcpCX4l5iSZp/mwMnYyBzrFbNJqizwKB3UBZLc+S8c4XllABAJIeBlZHeY0OSah5jlSXvOraLLUdLPZc=",
        "ssh_host_key_rsa_public_keytype": "ssh-rsa",
        "swapfree_mb": 3096,
        "swaptotal_mb": 3096,
        "system": "Linux",
        "system_capabilities": [],
        "system_capabilities_enforced": "False",
        "system_vendor": "innotek GmbH",
        "systemd": {
            "features": "+PAM +AUDIT +SELINUX -APPARMOR +IMA +SMACK +SECCOMP +GCRYPT +GNUTLS +OPENSSL +ACL +BLKID +CURL +ELFUTILS +FIDO2 +IDN2 -IDN -IPTC +KMOD +LIBCRYPTSETUP +LIBFDISK +PCRE2 -PWQUALITY +P11KIT -QRENCODE +TPM2 +BZIP2 +LZ4 +XZ +ZLIB +ZSTD -BPF_FRAMEWORK +XKBCOMMON +UTMP +SYSVINIT default-hierarchy=unified",
            "version": 252
        },
        "uptime_seconds": 48618,
        "user_dir": "/root",
        "user_gecos": "root",
        "user_gid": 0,
        "user_id": "root",
        "user_shell": "/bin/bash",
        "user_uid": 0,
        "userspace_architecture": "x86_64",
        "userspace_bits": "64",
        "virtualization_role": "guest",
        "virtualization_tech_guest": [
            "virtualbox"
        ],
        "virtualization_tech_host": [],
        "virtualization_type": "virtualbox"
    }
}

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

Ansible 팩트의 값을 활용하고 싶으면 아래와 같이 하면 된다. 예를 들어 IPv4 주소를 출력하고 싶다고 하면...

$ cat facts-ipv4.yml
---

- hosts: db

  tasks:
  - name: Print all facts
    ansible.builtin.debug:
      msg: >
        The default IPv4 address of {{ ansible_facts.hostname }}
        is {{ ansible_facts.default_ipv4.address }}
        
$ ansible-playbook facts-ipv4.yml

PLAY [db] ***********************************************************************************************************************************************************************************************************************************

TASK [Print all facts] **********************************************************************************************************************************************************************************************************************
ok: [tnode3] => {
    "msg": "The node's host name is tnode3 and the ip is 10.0.2.15\n"
}

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

위와 같이 출력이 된다. ansible_facts.default_ipv4.address 를 통해서 접근할 수 있게 된다.

 

루프

루프는 플레이북 yml 파일을 작성할 때 귀찮은 작업을 줄여주기 위해서 변수 등을 통해 특정 작업을 반복하도록 하는 것이다. 예를 들어보자. 

$ cat loop.yaml
---
- hosts: all
  vars:
    services:
      - vboxadd-service  # ssh
      - rsyslog

  tasks:
  - name: Check sshd and rsyslog state
    ansible.builtin.service:
      name: "{{ item }}"
      state: started
    loop: "{{ services }}"

위 yml 파일의 경우 services라는 변수를 vboxadd-service, rsyslog로 vars에 저장해두었다. 그러면 name: "{{ item }}" 의 item 부분에 차례로 vboxadd-service, rsyslog가 대입되어 실행된다. 

 

조건문

when절을 위에서도 잠깐 소개했었다. 조건문을 변수를 통해서도 아래와 같이 실행해볼 수 있다.

$ cat when.yml
---
- hosts: localhost
  vars:
    run_my_task: true

  tasks:
  - name: echo message
    ansible.builtin.shell: "echo test"
    when: run_my_task
    register: result

  - name: Show result
    ansible.builtin.debug:
      var: result

위 플레이북 yml을 해석해보면 run_my_task가 true일 경우에만 shell "echo test"를 실행하는 것이다. 그리고 결과를 result에 저장 하여 결과를 출력한다.

PLAY [localhost] ****************************************************************************************************************************************************************************************************************************

TASK [Print all facts] **********************************************************************************************************************************************************************************************************************
ok: [localhost] => {
    "ansible_local": {
        "my-custom": {
            "packages": {
                "db_package": "mariadb-server",
                "web_package": "httpd"
            },
            "users": {
                "user1": "ansible",
                "user2": "gasida"
            }
        }
    }
}

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

root@server:~/my-ansible# vi when.yml
$ ansible-playbook when.yml

PLAY [localhost] ****************************************************************************************************************************************************************************************************************************

TASK [echo message] *************************************************************************************************************************************************************************************************************************
changed: [localhost]

TASK [Show result] **************************************************************************************************************************************************************************************************************************
ok: [localhost] => {
    "result": {
        "changed": true,
        "cmd": "echo test",
        "delta": "0:00:00.015129",
        "end": "2026-01-18 13:49:13.705687",
        "failed": false,
        "msg": "",
        "rc": 0,
        "start": "2026-01-18 13:49:13.690558",
        "stderr": "",
        "stderr_lines": [],
        "stdout": "test",
        "stdout_lines": [
            "test"
        ]
    }
}

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

result 변수를 확인할 수 있다. 물론 run_my_task를 false로 수정 수 실행하면 echo가 실행이 안된다.

$ cat when.yml
---
- hosts: localhost
  vars:
    run_my_task: false

  tasks:
  - name: echo message
    ansible.builtin.shell: "echo test"
    when: run_my_task
    register: result

  - name: Show result
    ansible.builtin.debug:
      var: result


$ ansible-playbook when.yml
PLAY [localhost] ****************************************************************************************************************************************************************************************************************************

TASK [echo message] *************************************************************************************************************************************************************************************************************************
skipping: [localhost]

TASK [Show result] **************************************************************************************************************************************************************************************************************************
ok: [localhost] => {
    "result": {
        "changed": false,
        "false_condition": "run_my_task",
        "skip_reason": "Conditional result was False",
        "skipped": true
    }
}

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

 

참고로 when 조건절에 아래와 같이 조건 연산자를 통해서 조건을 부여할 수도 있다.

ansible_facts[’machine’] == “x86_64” ansible_facts[’machine’] 값이 x86_64와 같으면 true
max_memory == 512 max_memory 값이 512와 같다면 true
min_memory < 128 min_memory 값이 128보다 작으면 true
min_memory > 256 min_memory 값이 256보다 크면 true
min_memory <= 256 min_memory 값이 256보다 작거나 같으면 true
min_memory >= 512 min_memory 값이 512보다 크거나 같으면 true
min_memory != 512 min_memory 값이 512와 같지 않으면 true
min_memory is defined min_memory 라는 변수가 있으면 true
min_memory is not defined min_memory 라는 변수가 없으면 true
memory_available memory 값이 true이며 true, 이때 해당 값이 1이거나 True 또는 yes면 true
not memory_available memory 값이 false이며 true, 이때 해당 값이 0이거나 False 또는 no면 true
ansible_facts[’distribution’] in supported_distros ansible_facts[’distribution’]의 값이 supported_distros 라는 변수에 있으면 true

 

아래는 "in"에 대한 예시이다.

$ cat check-os.yml
---
- hosts: all
  vars:
    supported_distros:
      - Ubuntu
      - CentOS

  tasks:
    - name: Print supported os
      ansible.builtin.debug:
        msg: "This {{ ansible_facts['distribution'] }} need to use apt"
      when: ansible_facts['distribution'] in supported_distros

$ ansible-playbook check-os.yml

PLAY [all] **********************************************************************************************************************************************************************************************************************************

TASK [Print supported os] *******************************************************************************************************************************************************************************************************************
ok: [tnode1] => {
    "msg": "This Ubuntu need to use apt"
}
skipping: [tnode3]
ok: [tnode2] => {
    "msg": "This Ubuntu need to use apt"
}

PLAY RECAP **********************************************************************************************************************************************************************************************************************************
tnode1                     : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
tnode2                     : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
tnode3                     : ok=0    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

 

 

'Kubernetes' 카테고리의 다른 글

워커 노드 Join 하기  (0) 2026.01.25
kubeadm으로 k8s 구성하기  (0) 2026.01.25
Ansible: 에이전트 없이 완성하는 자동화  (0) 2026.01.18
On-Premise K8s Hands-on Study 1주차  (1) 2026.01.11
Posted by 빛나유
,
** 서종호(가시다)님의 On-Premise K8s Hands-on Study 2주차 학습 내용을 기반으로 합니다. ** 

 

오늘은 Ansible에 대해서 이야기해보려고 한다. 스터디를 하면서 처음 들었다. 공부를 하고 느낀 점은, 이거 엣지 디바이스 관리하는데 꽤 괜찮게 사용할 수 있겠다는 생각을 좀 했다. 

 

아무튼 Ansible에 대해서 공부해보자.

 

Ansible이란?

공식 홈페이지에서 Ansible을 아래와 같이 설명하고 있다.

Ansible is open-source technology that can perform virtually any IT task and remove complexity from workflows.

Code that reads like documentation
Ansible is an automation language that can describe any IT environment, whether homelab or large scale infrastructure. It is easy to learn, beautiful code that reads like clear documentation.

Freedom from repetitive tasks
As an automation engine, Ansible ensures that your IT environment stays exactly as you describe it, no matter the complexity. Not only that, you can automate any command with Ansible to eliminate drudgery from your daily routine. Ansible gives you tooling to be more productive and solve problems that really matter.

 

거의 모든 IT 작업들을 실행할 수 있는 오픈소스라고 한다. 그래서 무엇을 자동화하는 거냐? 가령 이런 식이다. 하나의 마스터 노드가 있다고 하면, 이 마스터 노드가 관리하고 있는 관리 노드들이 있는데, 이 관리 노드들에 원하는 작업을 실행시킬 수 있다. 이를 SSH 기반으로 작동시킨다. 즉, 마스터 노드에서 관리 노드들을 SSH로 접근하여 원하는 작업을 실행시켜 작동시키는 것이다. 마스터 노드는 Ansible에서는 컨트롤 노드라고 한다.

 

Ansible 구성 요소

Ansible에서 중요한 구성 요소들은 아래와 같다.

  • 컨트롤 노드 & 관리 노드
  • 인벤토리
  • 플레이북
  • 모듈
  • 핸들러
  • 플러그인

컨트롤 노드 & 관리 노드

하나하나 알아보자. 컨트롤 노드는 Ansible이 설치되어 있는 서버로, 관리 노드에 명령어를 보내는 역할을 한다. 관리 노드는 관리 노드에 의해 명령이 실행되는 Slave 같은 역할을 한다. 이 글의 제목에 "에이전트 없이"라고 했는데, 관리 노드에는 Ansible을 위한 어떠한 에이전트도 설치하지 않는다. 즉, "엇, 에이전트가 설치가 안되서요.. 아 에이전트랑 통신이 안되서요.." 이런 문제는 없다는 것이다. 그러면 어떻게 명령을 넘기냐? 그래서 SSH를 기반으로 움직인다고 하는 것이다.

$ ssh root@tnode1 ls -al /home/tnode-1/workspace
total 28
drwx------  5 root root 4096 Jan 14 01:49 .
drwxr-xr-x 23 root root 4096 Oct 24 07:40 ..
drwx------  3 root root 4096 Jan 14 01:49 .ansible
-rw-r--r--  1 root root 3106 Apr 22  2024 .bashrc
drwx------  2 root root 4096 Jan 14 00:51 .cache
-rw-r--r--  1 root root  161 Apr 22  2024 .profile
drwx------  2 root root 4096 Oct 24 07:42 .ssh

컨트롤 노드에서 tnode1이라고 하는 관리 노드에 대해서 ls 명령어 실행할 수 있다. 이것을 생각해보면, "아 저거 어떻게 잘 구조 작성해서 만들면 자동화 툴을 만들 수 있겠구나? 그게 Ansible이구나" 라고 생각할 수 있다. 자 백문이 불여일견, 일단 마스터 노드에 설치부터 해보자

$ pip3 install ansible

 

인벤토리

컨트롤 노드가 관리할 관리노드를 나열해둔 곳이다. 

[web]
tnode1 ansible_python_interpreter=/usr/bin/python3
tnode2 ansible_python_interpreter=/usr/bin/python3

[db]
tnode3 ansible_python_interpreter=/usr/bin/python3

[all:children]
web
db

위에서 [web], [db]는 그룹을 명시해둔 부분이다. tnode1~3은 IP 주소여도 되는데, /etc/hosts에 저장되어 있는 이름으로 바꿔쓴 것 뿐이다. 

 

플레이북

자, 이제 인벤토리를 통해 관리 노드들을 설정했으니, 관리 노드들에게 명령어를 실제로 실행하도록 해보자. 명령어를 실행하게 하기 위해서는 플레이북이라는 것을 만들어야 한다. 보통 yml 파일을 통해 만든다.

# check-os.yml
---
- hosts: all
  vars:
    supported_distros:
      - Ubuntu
      - CentOS

  tasks:
    - name: Print supported os
      ansible.builtin.debug:
        msg: "This {{ ansible_facts['distribution'] }} need to use apt"
      when: ansible_facts['distribution'] in supported_distros

위 yml 파일을 보면 알겠지만, Ansible에서 해석할 수 있는 key 들을 알아야 작성할 수 있다. 위 yml 파일을 해석해보면, supported_distros에는 Ubuntu와 CentOS가 있는데, 관리 노드가 Ubuntu이거나 CentOS라면 (when) debug 메세지로 "This {{ ansible_facts['distribution'] }} need to use apt"를 출력하라고 하는 것이다. 실행해 보면 아래와 같은 결과를 얻을 수 있다.

root@server:~/my-ansible# ansible-playbook check-os.yml

PLAY [all] **********************************************************************************************************************************************************************************************************************************

TASK [Gathering Facts] **********************************************************************************************************************************************************************************************************************
ok: [tnode3]
ok: [tnode2]
ok: [tnode1]

TASK [Print supported os] *******************************************************************************************************************************************************************************************************************
ok: [tnode1] => {
    "msg": "This Ubuntu need to use apt"
}
skipping: [tnode3]
ok: [tnode2] => {
    "msg": "This Ubuntu need to use apt"
}

PLAY RECAP **********************************************************************************************************************************************************************************************************************************
tnode1                     : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
tnode2                     : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
tnode3                     : ok=1    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

보면 tnode3은 스킵된 것을 확인할 수 있다. tonde3은 RedHat 계열이다. 

 

모듈 & 핸들러

Ansible에서 모듈은 관리 노드들에서 실행되는 작은 프로그램이다. Ansible의 모듈들은 수천개가 있다고 한다. 대표적으로 위에 check-os.yml에서 ansible.builtin.debug는 debug를 실행하는 빌트인 프로그램이라고 보면 된다. 

핸들러는 특정 플레이북의 테스크가 실행되어 관리 노드의 상태를 바꾸었을 때만 실행되는 조금 특별한 테스크라고 보면 된다.

예를 들어 특정 서버의 conf 파일이 변경되면 재시작을 해야 하는데, 이 과정에서 재시작이 핸들러를 통해 작성될 수 있다.

# handler-example.yml
---
- name: Deploy application and restart service
  hosts: webservers
  become: true  # Escalate privileges to root

  tasks:
    - name: Copy new configuration file
      ansible.builtin.copy: # Using the fully qualified collection name is a best practice
        src: files/myapp.conf
        dest: /etc/myapp/myapp.conf
        mode: '0644'
      notify:
        - Restart myapp service

  handlers:
    - name: Restart myapp service
      ansible.builtin.systemd_service: # Example using systemd service module
        name: myapp
        state: restarted
      listen:
        - "Restart myapp service" # Optional: Can use 'listen' for multiple notifications

 

'Kubernetes' 카테고리의 다른 글

워커 노드 Join 하기  (0) 2026.01.25
kubeadm으로 k8s 구성하기  (0) 2026.01.25
Ansible: 사용해보기  (0) 2026.01.18
On-Premise K8s Hands-on Study 1주차  (1) 2026.01.11
Posted by 빛나유
,

** 서종호(가시다)님의 On-Premise K8s Hands-on Study 1주차 학습 내용을 기반으로 합니다. **

 

도커는 매일 사용하지만 Kubernetes는 한번도 사용해본적이 없다. 물론, Kubernetes로 구성된 환경은 많이 사용해보지만, Kubernetes 자체는 사용해본적이 없다. 

 

우연히 링크드인을 돌아다니다가 서종호님께서 진행하시는 Kubernetes 스터디가 있어서 참여 신청했었다. 다행히 함께 할 수 있었고, 매주 과제를 수행하면서 2달간 진행하게 됐다. 

 

이번 글은 지난 주 첫번째 진행했던 스터디 후 과제를 진행한 글이다. 첫번째 과제는 Kubernetes 클러스터를 수동?으로 조금 어려운 방법으로 구성해보는 것이다.

 

아직. 솔직히 클러스터가 뭔지. pod는 뭔지 정확하게 모른다. 그런데 해보면서 공부하고, 공부하면서 정리하고, 정리해보면서 점점 개념을 세워가보려고 한다.

 

!! 여기 개념. 

쿠버네티스의 Pod가 무엇인가? 검색해보면 "쿠버네티스에서 생성하고 관리할 수 있는 배포 가능한 가장 작은 단위라고 한다. 음.. 도커 컨테이너인가? 라고 생각했는데, 한 개의 Pod당 컨테이너 1개로 쓸 수도 있고, 한 개의 Pod당 여러 개의 컨테이너를 묶어서 쓸 수도 있다고 한다. 아.. 그럼 뭔가 컨테이너는 아니고, 그거를 감싸는 무언가인가 보다.. 그래서 여러 개를 묶을 수도 있는건가보다..

 

과제에서는 쿠버네티스 클러스터를 손수 수동으로 구성해보는 것이다.

 

!! 여기 개념. 

쿠버네티스 클러스터란 무엇인가? 쿠버네티스 클러스터는 가상머신이든 물리적 서버이든 관계없이 이러한 것들의 그룹이고, 이 안에서 여러 애플리케이션들이 쿠버네티스에 의해 관리되고 배포된다고 한다. 마스터 노드와 워커 노드가 있는데, 마스터 노드가 실행할 애플리케이션을 스케쥴링하고 워커 노드들을 관리한다고 한다. 워커 노드는 컨테이너가 실제로 실행되는 물리 서버 또는 가상 머신이다. 

 

그렇게 하기 위해서 우선 4개의 가상 머신을 vagrant와 virtual machine을 통해 띄웠다. 

## 코드

 

 

4개의 가상 머신은 아래와 같다.

- jumpbox: 보안 강화를 위한 중개 서버

- server: 쿠버네티스 API 서버

- node-0: 워커 노드 0

- node-1: 워커 노드 1

 

이번 스터디에서 쿠버네티스를 온프리미스 환경에서 구성하는 것을 전제로 하기 때문에 내부 네트워크 통신 등이 암호화되어 있어야 한다. 그래서 이 jumpbox에서 인증서를 만들기도 하고 등등 여러 가지를 하는데, 그건 나중에 밑에서 좀 보자.

 

## 코드 02

## --> jumpbox 셋업

# ----> vagrant ssh jumpbox


# root 계정 확인
whoami
# root

## vagrant 계정 로그인 시 'sudo su -' 실행으로 root 계정 전환됨
cat /home/vagrant/.bashrc | tail -n 1
# sudo su -


# 툴 설치 : 이미 적용되어 있음
apt-get update && apt install tree git jq yq unzip vim sshpass -y

# Sync GitHub Repository
## --depth 1 : 최신 커밋만 가져오는 shallow clone을 의미한다. 전체 git 히스토리가 필요 없으므로 이 옵션을 사용하면 다운로드 시간과 용량을 절약할 수 있다.
## 출처 멤버 작성 글 : https://sirzzang.github.io/kubernetes/Kubernetes-Cluster-The-Hard-Way-02/
pwd
git clone --depth 1 https://github.com/kelseyhightower/kubernetes-the-hard-way.git
cd kubernetes-the-hard-way
tree
pwd


# Download Binaries : k8s 구성을 위한 컴포넌트 다운로드

# CPU 아키텍처 확인
dpkg --print-architecture
# arm64   # macOS 사용자
# amd64   # Windows 사용자

# CPU 아키텍처 별 다운로드 목록 정보 다름
ls -l downloads-*
# -rw-r--r-- 1 root root 839 Jan  4 10:30 downloads-amd64.txt
# -rw-r--r-- 1 root root 839 Jan  4 10:30 downloads-arm64.txt

# https://kubernetes.io/releases/download/
cat downloads-$(dpkg --print-architecture).txt
# https://dl.k8s.io/v1.32.3/bin/linux/arm64/kubectl
# https://dl.k8s.io/v1.32.3/bin/linux/arm64/kube-apiserver
# https://dl.k8s.io/v1.32.3/bin/linux/arm64/kube-controller-manager
# https://dl.k8s.io/v1.32.3/bin/linux/arm64/kube-scheduler
# https://dl.k8s.io/v1.32.3/bin/linux/arm64/kube-proxy
# https://dl.k8s.io/v1.32.3/bin/linux/arm64/kubelet
# https://github.com/kubernetes-sigs/cri-tools/releases/download/v1.32.0/crictl-v1.32.0-linux-arm64.tar.gz
# https://github.com/opencontainers/runc/releases/download/v1.3.0-rc.1/runc.arm64
# https://github.com/containernetworking/plugins/releases/download/v1.6.2/cni-plugins-linux-arm64-v1.6.2.tgz
# https://github.com/containerd/containerd/releases/download/v2.1.0-beta.0/containerd-2.1.0-beta.0-linux-arm64.tar.gz
# https://github.com/etcd-io/etcd/releases/download/v3.6.0-rc.3/etcd-v3.6.0-rc.3-linux-arm64.tar.gz

# wget 으로 다운로드 실행 : 500MB Size 정도
wget -q --show-progress \
  --https-only \
  --timestamping \
  -P downloads \
  -i downloads-$(dpkg --print-architecture).txt

# 확인
ls -oh downloads


# Extract the component binaries from the release archives and organize them under the downloads directory.
ARCH=$(dpkg --print-architecture)
echo $ARCH

mkdir -p downloads/{client,cni-plugins,controller,worker}
tree -d downloads

# 압축 풀기
tar -xvf downloads/crictl-v1.32.0-linux-${ARCH}.tar.gz \
  -C downloads/worker/ && tree -ug downloads

tar -xvf downloads/containerd-2.1.0-beta.0-linux-${ARCH}.tar.gz \
  --strip-components 1 \
  -C downloads/worker/ && tree -ug downloads

tar -xvf downloads/cni-plugins-linux-${ARCH}-v1.6.2.tgz \
  -C downloads/cni-plugins/ && tree -ug downloads

## --strip-components 1 : etcd-v3.6.0-rc.3-linux-amd64/etcd 경로의 앞부분(디렉터리)을 제거
tar -xvf downloads/etcd-v3.6.0-rc.3-linux-${ARCH}.tar.gz \
  -C downloads/ \
  --strip-components 1 \
  etcd-v3.6.0-rc.3-linux-${ARCH}/etcdctl \
  etcd-v3.6.0-rc.3-linux-${ARCH}/etcd && tree -ug downloads

# 확인
tree downloads/worker/
tree downloads/cni-plugins
ls -l downloads/{etcd,etcdctl}


# 파일 이동 
mv downloads/{etcdctl,kubectl} downloads/client/
mv downloads/{etcd,kube-apiserver,kube-controller-manager,kube-scheduler} downloads/controller/
mv downloads/{kubelet,kube-proxy} downloads/worker/
mv downloads/runc.${ARCH} downloads/worker/runc

# 확인
tree downloads/client/
tree downloads/controller/
tree downloads/worker/

# 불필요한 압축 파일 제거
ls -l downloads/*gz
rm -rf downloads/*gz

# Make the binaries executable.
ls -l downloads/{client,cni-plugins,controller,worker}/*
chmod +x downloads/{client,cni-plugins,controller,worker}/*
ls -l downloads/{client,cni-plugins,controller,worker}/*

# 일부 파일 소유자 변경
tree -ug downloads # cat /etc/passwd | grep vagrant && cat /etc/group | grep vagrant
chown root:root downloads/client/etcdctl
chown root:root downloads/controller/etcd
chown root:root downloads/worker/crictl
tree -ug downloads


# kubernetes client 도구인 kubectl를 설치
ls -l downloads/client/kubectl
cp downloads/client/kubectl /usr/local/bin/

# can be verified by running the kubectl command:
kubectl version --client

 

쿠버네티스를 수동으로 구성하기 위한 github을 클론하고 kubectl 등의 실행파일 등을 설치하는 과정이다. 

# ----> vagrant ssh jumpbox

# Machine Database (서버 속성 저장 파일) : IPV4_ADDRESS FQDN HOSTNAME POD_SUBNET
## 참고) server(controlplane)는 kubelet 동작하지 않아서, 파드 네트워크 대역 설정 필요 없음
cat <<EOF > machines.txt
192.168.10.100 server.kubernetes.local server
192.168.10.101 node-0.kubernetes.local node-0 10.200.0.0/24
192.168.10.102 node-1.kubernetes.local node-1 10.200.1.0/24
EOF
cat machines.txt

while read IP FQDN HOST SUBNET; do
  echo "${IP} ${FQDN} ${HOST} ${SUBNET}"
done < machines.txt


# Configuring SSH Access 설정
 
# sshd config 설정 파일 확인 : 이미 암호 기반 인증 접속 설정 되어 있음
grep "^[^#]" /etc/ssh/sshd_config

# Generate a new SSH key
ssh-keygen -t rsa -N "" -f /root/.ssh/id_rsa
ls -l /root/.ssh

# Copy the SSH public key to each machine
while read IP FQDN HOST SUBNET; do
  sshpass -p 'qwe123' ssh-copy-id -o StrictHostKeyChecking=no root@${IP}
done < machines.txt

while read IP FQDN HOST SUBNET; do
  ssh -n root@${IP} cat /root/.ssh/authorized_keys
done < machines.txt

# Once each key is added, verify SSH public key access is working
# 아래는 IP 기반으로 접속 확인
while read IP FQDN HOST SUBNET; do
  ssh -n root@${IP} hostname
done < machines.txt


# Hostnames 설정

# 확인 : init_cfg.sh 로 이미 설정되어 있음
while read IP FQDN HOST SUBNET; do
  ssh -n root@${IP} cat /etc/hosts
done < machines.txt

while read IP FQDN HOST SUBNET; do
  ssh -n root@${IP} hostname --fqdn
done < machines.txt

# 아래는 hostname 으로 ssh 접속 확인
cat /etc/hosts
while read IP FQDN HOST SUBNET; do
  sshpass -p 'qwe123' ssh -n -o StrictHostKeyChecking=no root@${HOST} hostname
done < machines.txt

while read IP FQDN HOST SUBNET; do
  sshpass -p 'qwe123' ssh -n root@${HOST} uname -o -m -n
done < machines.txt

 

위 코드 역시 jumpbox에서 실행하는데, server, node-0, node-1에 대한 hostname 등을 설정하는 과정이다. 그 다음 과정은 jumpbox 에서 openssl을 이용해서 개인 키를 만들고, 이 키를 이용해서 인증서를 만든다. 만든 인증서는 server, node-0, node-1에 배포한다.

 

# ----> vagrant ssh jumpbox

# Generate the CA configuration file, certificate, and private key

# Root CA 개인키 생성 : ca.key
openssl genrsa -out ca.key 4096
ls -l ca.key


cat ca.key
openssl rsa -in ca.key -text -noout # 개인키 구조 확인

# Root CA 인증서 생성 : ca.crt
## -x509 : CSR을 만들지 않고 바로 인증서(X.509) 생성, 즉, Self-Signed Certificate
## -noenc : 개인키를 암호화하지 않음, 즉, CA 키(ca.key)에 패스프레이즈 없음
## -config ca.conf : 인증서 세부 정보는 설정 파일에서 읽음 , [req] 섹션 사용됨 - DN 정보 → [req_distinguished_name] , CA 확장 → [ca_x509_extensions]
openssl req -x509 -new -sha512 -noenc \
  -key ca.key -days 3653 \
  -config ca.conf \
  -out ca.crt
ls -l ca.crt


# ca.conf 관련 내용
cat ca.conf


cat ca.crt
openssl x509 -in ca.crt -text -noout # 인증서 전체 내용 확인


# ==========================================


# Create Client and Server Certificates : admin
openssl genrsa -out admin.key 4096
ls -l admin.key

# ca.conf 에 admin 섹션
cat ca.conf


# csr 파일 생성 : admin.key 개인키를 사용해 'CN=admin, O=system:masters'인 Kubernetes 관리자용 클라이언트 인증서 요청(admin.csr) 생성
openssl req -new -key admin.key -sha256 \
  -config ca.conf -section admin \
  -out admin.csr
ls -l admin.csr
openssl req -in admin.csr -text -noout # CSR 전체 내용 확인


# ca에 csr 요청을 통한 crt 파일 생성
## -req : CSR를 입력으로 받아 인증서를 생성, self-signed 아님, CA가 서명하는 방식
## -days 3653 : 인증서 유효기간 3653일 (약 10년)
## -copy_extensions copyall : CSR에 포함된 모든 X.509 extensions를 인증서로 복사
## -CAcreateserial : CA 시리얼 번호 파일 자동 생성, 다음 인증서 발급 시 재사용, 기본 생성 파일(ca.srl)
openssl x509 -req -days 3653 -in admin.csr \
  -copy_extensions copyall \
  -sha256 -CA ca.crt \
  -CAkey ca.key \
  -CAcreateserial \
  -out admin.crt


ls -l admin.crt
openssl x509 -in admin.crt -text -noout

# ==========================================

# ca.conf 수정
cat ca.conf | grep system:kube-scheduler

sed -i 's/system:system:kube-scheduler/system:kube-scheduler/' ca.conf
cat ca.conf | grep system:kube-scheduler



# 변수 지정
certs=(
  "node-0" "node-1"
  "kube-proxy" "kube-scheduler"
  "kube-controller-manager"
  "kube-api-server"
  "service-accounts"
)

# 확인
echo ${certs[*]}


# 개인키 생성, csr 생성, 인증서 생성
for i in ${certs[*]}; do
  openssl genrsa -out "${i}.key" 4096

  openssl req -new -key "${i}.key" -sha256 \
    -config "ca.conf" -section ${i} \
    -out "${i}.csr"

  openssl x509 -req -days 3653 -in "${i}.csr" \
    -copy_extensions copyall \
    -sha256 -CA "ca.crt" \
    -CAkey "ca.key" \
    -CAcreateserial \
    -out "${i}.crt"
done


ls -1 *.crt *.key *.csr



# 인증서 정보 확인
openssl x509 -in node-0.crt -text -noout


openssl x509 -in node-1.crt -text -noout


openssl x509 -in kube-proxy.crt -text -noout


openssl x509 -in kube-scheduler.crt -text -noout


openssl x509 -in kube-controller-manager.crt -text -noout


# api-server : SAN 정보에 10.32.0.1 은 kubernetes (Service) ClusterIP. 다른 인증서와 다르게 SSL Server 역할 추가 확인
openssl x509 -in kube-api-server.crt -text -noout


# service-accounts
openssl x509 -in service-accounts.crt -text -noout



# ==========================================

# Copy the appropriate certificates and private keys to the node-0 and node-1 machines
for host in node-0 node-1; do
  ssh root@${host} mkdir /var/lib/kubelet/

  scp ca.crt root@${host}:/var/lib/kubelet/

  scp ${host}.crt \
    root@${host}:/var/lib/kubelet/kubelet.crt

  scp ${host}.key \
    root@${host}:/var/lib/kubelet/kubelet.key
done

# 확인
ssh node-0 ls -l /var/lib/kubelet
ssh node-1 ls -l /var/lib/kubelet


# Copy the appropriate certificates and private keys to the server machine
scp \
  ca.key ca.crt \
  kube-api-server.key kube-api-server.crt \
  service-accounts.key service-accounts.crt \
  root@server:~/

# 확인
ssh server ls -l /root

 

그 다음에 할 일들은 쿠버네티스 관련 configuration 세팅하는 과정이다.

# ----> vagrant ssh jumpbox

# apiserver 파드 args 정보
# kubectl describe pod -n kube-system kube-apiserver-myk8s-control-plane



# Generate a kubeconfig file for the node-0 and node-1 worker nodes

# config set-cluster
kubectl config set-cluster kubernetes-the-hard-way \
  --certificate-authority=ca.crt \
  --embed-certs=true \
  --server=https://server.kubernetes.local:6443 \
  --kubeconfig=node-0.kubeconfig && ls -l node-0.kubeconfig && cat node-0.kubeconfig

kubectl config set-cluster kubernetes-the-hard-way \
  --certificate-authority=ca.crt \
  --embed-certs=true \
  --server=https://server.kubernetes.local:6443 \
  --kubeconfig=node-1.kubeconfig && ls -l node-1.kubeconfig && cat node-1.kubeconfig

# config set-credentials
kubectl config set-credentials system:node:node-0 \
  --client-certificate=node-0.crt \
  --client-key=node-0.key \
  --embed-certs=true \
  --kubeconfig=node-0.kubeconfig && cat node-0.kubeconfig

kubectl config set-credentials system:node:node-1 \
  --client-certificate=node-1.crt \
  --client-key=node-1.key \
  --embed-certs=true \
  --kubeconfig=node-1.kubeconfig && cat node-1.kubeconfig
  
# set-context : default 추가
kubectl config set-context default \
  --cluster=kubernetes-the-hard-way \
  --user=system:node:node-0 \
  --kubeconfig=node-0.kubeconfig && cat node-0.kubeconfig

kubectl config set-context default \
  --cluster=kubernetes-the-hard-way \
  --user=system:node:node-1 \
  --kubeconfig=node-1.kubeconfig && cat node-1.kubeconfig


# use-context : current-context 에 default 추가
kubectl config use-context default \
  --kubeconfig=node-0.kubeconfig

kubectl config use-context default \
  --kubeconfig=node-1.kubeconfig


#
ls -l *.kubeconfig

# ==========================================

# Generate a kubeconfig file for the kube-proxy service
kubectl config set-cluster kubernetes-the-hard-way \
  --certificate-authority=ca.crt \
  --embed-certs=true \
  --server=https://server.kubernetes.local:6443 \
  --kubeconfig=kube-proxy.kubeconfig

kubectl config set-credentials system:kube-proxy \
  --client-certificate=kube-proxy.crt \
  --client-key=kube-proxy.key \
  --embed-certs=true \
  --kubeconfig=kube-proxy.kubeconfig

kubectl config set-context default \
  --cluster=kubernetes-the-hard-way \
  --user=system:kube-proxy \
  --kubeconfig=kube-proxy.kubeconfig

kubectl config use-context default \
  --kubeconfig=kube-proxy.kubeconfig

# 확인
cat kube-proxy.kubeconfig

# ==========================================

# Generate a kubeconfig file for the kube-controller-manager service
kubectl config set-cluster kubernetes-the-hard-way \
  --certificate-authority=ca.crt \
  --embed-certs=true \
  --server=https://server.kubernetes.local:6443 \
  --kubeconfig=kube-controller-manager.kubeconfig

kubectl config set-credentials system:kube-controller-manager \
  --client-certificate=kube-controller-manager.crt \
  --client-key=kube-controller-manager.key \
  --embed-certs=true \
  --kubeconfig=kube-controller-manager.kubeconfig

kubectl config set-context default \
  --cluster=kubernetes-the-hard-way \
  --user=system:kube-controller-manager \
  --kubeconfig=kube-controller-manager.kubeconfig

kubectl config use-context default \
  --kubeconfig=kube-controller-manager.kubeconfig

# 확인
cat kube-controller-manager.kubeconfig

# ==========================================

# Generate a kubeconfig file for the kube-scheduler service
kubectl config set-cluster kubernetes-the-hard-way \
  --certificate-authority=ca.crt \
  --embed-certs=true \
  --server=https://server.kubernetes.local:6443 \
  --kubeconfig=kube-scheduler.kubeconfig

kubectl config set-credentials system:kube-scheduler \
  --client-certificate=kube-scheduler.crt \
  --client-key=kube-scheduler.key \
  --embed-certs=true \
  --kubeconfig=kube-scheduler.kubeconfig

kubectl config set-context default \
  --cluster=kubernetes-the-hard-way \
  --user=system:kube-scheduler \
  --kubeconfig=kube-scheduler.kubeconfig

kubectl config use-context default \
  --kubeconfig=kube-scheduler.kubeconfig

# 확인
cat kube-scheduler.kubeconfig


# ==========================================

# Generate a kubeconfig file for the admin user
kubectl config set-cluster kubernetes-the-hard-way \
  --certificate-authority=ca.crt \
  --embed-certs=true \
  --server=https://127.0.0.1:6443 \
  --kubeconfig=admin.kubeconfig

kubectl config set-credentials admin \
  --client-certificate=admin.crt \
  --client-key=admin.key \
  --embed-certs=true \
  --kubeconfig=admin.kubeconfig

kubectl config set-context default \
  --cluster=kubernetes-the-hard-way \
  --user=admin \
  --kubeconfig=admin.kubeconfig

kubectl config use-context default \
  --kubeconfig=admin.kubeconfig

# 확인
cat admin.kubeconfig

# ==========================================

#
ls -l *.kubeconfig

# Copy the kubelet and kube-proxy kubeconfig files to the node-0 and node-1 machines
for host in node-0 node-1; do
  ssh root@${host} "mkdir -p /var/lib/{kube-proxy,kubelet}"

  scp kube-proxy.kubeconfig \
    root@${host}:/var/lib/kube-proxy/kubeconfig \

  scp ${host}.kubeconfig \
    root@${host}:/var/lib/kubelet/kubeconfig
done

# 확인
ssh node-0 ls -l /var/lib/*/kubeconfig
ssh node-1 ls -l /var/lib/*/kubeconfig


# Copy the kube-controller-manager and kube-scheduler kubeconfig files to the server machine
scp admin.kubeconfig \
  kube-controller-manager.kubeconfig \
  kube-scheduler.kubeconfig \
  root@server:~/

# 확인
ssh server ls -l /root/*.kubeconfig

 

위 코드를 보면 kube-control-manager, kube-scheduler, kube-proxy, node-0, node-1 등등을 이름으로 한 .kubeconfig 파일이 있다. 이 파일들을 마지막에 보면 scp 등을 통해 배포해준다.

 

그리고 마지막으로 etcd를 server에 설치해주도록 하는 코드가 아래에 있다.

# ----> vagrant ssh jumpbox



# The Encryption Key

# Generate an encryption key
export ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)
echo $ENCRYPTION_KEY



# The Encryption Config File

# Create the encryption-config.yaml encryption config file
# (참고) 실제 etcd 값에 기록되는 헤더 : k8s:enc:aescbc:v1:key1:<ciphertext>
cat configs/encryption-config.yaml


envsubst < configs/encryption-config.yaml > encryption-config.yaml
cat encryption-config.yaml

# Copy the encryption-config.yaml encryption config file to each controller instance:
scp encryption-config.yaml root@server:~/
ssh server ls -l /root/encryption-config.yaml

# ----> vagrant ssh jumpbox

# Prerequisites

# hostname 변경 : controller -> server
# http 평문 통신!
# Each etcd member must have a unique name within an etcd cluster. 
# Set the etcd name to match the hostname of the current compute instance:
cat units/etcd.service | grep controller

ETCD_NAME=server
cat > units/etcd.service <<EOF
[Unit]
Description=etcd
Documentation=https://github.com/etcd-io/etcd

[Service]
Type=notify
ExecStart=/usr/local/bin/etcd \\
  --name ${ETCD_NAME} \\
  --initial-advertise-peer-urls http://127.0.0.1:2380 \\
  --listen-peer-urls http://127.0.0.1:2380 \\
  --listen-client-urls http://127.0.0.1:2379 \\
  --advertise-client-urls http://127.0.0.1:2379 \\
  --initial-cluster-token etcd-cluster-0 \\
  --initial-cluster ${ETCD_NAME}=http://127.0.0.1:2380 \\
  --initial-cluster-state new \\
  --data-dir=/var/lib/etcd
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
cat units/etcd.service | grep server

# Copy etcd binaries and systemd unit files to the server machine
scp \
  downloads/controller/etcd \
  downloads/client/etcdctl \
  units/etcd.service \
  root@server:~/

 

!! 여기 개념

etcd란? 쿠버네티스에서 사용하고 있는 key:value 기반의 스토리지. 가령, 클러스터에 노드가 몇 개 있고, 어떤 Pod가 어떤 Node에서 실행되고 있는지 등의 정보이다.

 

그리고 server로 ssh 접근해서 server에서 etcd를 설치하고 서비스로 등록까지 한다.

# ----> vagrant ssh server


# 아래는 server 가상머신 접속 후 명령 실행
# The commands in this lab must be run on the server machine. Login to the server machine using the ssh command. Example:
# ssh root@server
# -------------------------------------------------------------------
# Bootstrapping an etcd Cluster

# Install the etcd Binaries
# Extract and install the etcd server and the etcdctl command line utility
pwd
mv etcd etcdctl /usr/local/bin/

# Configure the etcd Server
mkdir -p /etc/etcd /var/lib/etcd
chmod 700 /var/lib/etcd
cp ca.crt kube-api-server.key kube-api-server.crt /etc/etcd/

# Create the etcd.service systemd unit file:
mv etcd.service /etc/systemd/system/
tree /etc/systemd/system/

# Start the etcd Server
systemctl daemon-reload
systemctl enable etcd
systemctl start etcd

# 확인
systemctl status etcd --no-pager
ss -tnlp | grep etcd


# List the etcd cluster members
etcdctl member list


etcdctl member list -w table
etcdctl endpoint status -w table


# -------------------------------------------------------------------

 

그리고 server에 쿠버네티스 API를 실행하기 위한 서비스 설정 파일 등을 만든다.

# ---> jumpbox에서 실행

# ----> vagrant ssh jumpbox

# Prerequisites

# kube-apiserver.service 수정 : service-cluster-ip-range 추가
# https://github.com/kelseyhightower/kubernetes-the-hard-way/issues/905
# service-cluster-ip 값은 ca.conf 에 설정한 [kube-api-server_alt_names] 항목의 Service IP 범위
cat ca.conf | grep '\[kube-api-server_alt_names' -A2


cat units/kube-apiserver.service
cat << EOF > units/kube-apiserver.service
[Unit]
Description=Kubernetes API Server
Documentation=https://github.com/kubernetes/kubernetes

[Service]
ExecStart=/usr/local/bin/kube-apiserver \\
  --allow-privileged=true \\
  --apiserver-count=1 \\
  --audit-log-maxage=30 \\
  --audit-log-maxbackup=3 \\
  --audit-log-maxsize=100 \\
  --audit-log-path=/var/log/audit.log \\
  --authorization-mode=Node,RBAC \\
  --bind-address=0.0.0.0 \\
  --client-ca-file=/var/lib/kubernetes/ca.crt \\
  --enable-admission-plugins=NamespaceLifecycle,NodeRestriction,LimitRanger,ServiceAccount,DefaultStorageClass,ResourceQuota \\
  --etcd-servers=http://127.0.0.1:2379 \\
  --event-ttl=1h \\
  --encryption-provider-config=/var/lib/kubernetes/encryption-config.yaml \\
  --kubelet-certificate-authority=/var/lib/kubernetes/ca.crt \\
  --kubelet-client-certificate=/var/lib/kubernetes/kube-api-server.crt \\
  --kubelet-client-key=/var/lib/kubernetes/kube-api-server.key \\
  --runtime-config='api/all=true' \\
  --service-account-key-file=/var/lib/kubernetes/service-accounts.crt \\
  --service-account-signing-key-file=/var/lib/kubernetes/service-accounts.key \\
  --service-account-issuer=https://server.kubernetes.local:6443 \\
  --service-cluster-ip-range=10.32.0.0/24 \\
  --service-node-port-range=30000-32767 \\
  --tls-cert-file=/var/lib/kubernetes/kube-api-server.crt \\
  --tls-private-key-file=/var/lib/kubernetes/kube-api-server.key \\
  --v=2
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
cat units/kube-apiserver.service


# kube-apiserver가 kubelet(Node)에 접근할 수 있도록 허용하는 '시스템 내부용 RBAC' 설정
cat configs/kube-apiserver-to-kubelet.yaml ; echo


# api-server : Subject CN 확인
openssl x509 -in kube-api-server.crt -text -noout




# kube-scheduler
cat units/kube-scheduler.service ; echo
cat configs/kube-scheduler.yaml ; echo


# kube-controller-manager : cluster-cidr 는 POD CIDR 포함하는 대역, service-cluster-ip-range 는 apiserver 설정 값 동일 설정.
cat units/kube-controller-manager.service ; echo


# Connect to the jumpbox and copy Kubernetes binaries and systemd unit files to the server machine
scp \
  downloads/controller/kube-apiserver \
  downloads/controller/kube-controller-manager \
  downloads/controller/kube-scheduler \
  downloads/client/kubectl \
  units/kube-apiserver.service \
  units/kube-controller-manager.service \
  units/kube-scheduler.service \
  configs/kube-scheduler.yaml \
  configs/kube-apiserver-to-kubelet.yaml \
  root@server:~/

# 확인
ssh server ls -l /root

# ---------------------------------------------------------------

# ---> server에서 실행

# ----> vagrant ssh server

# Create the Kubernetes configuration directory:
pwd
mkdir -p /etc/kubernetes/config


# Install the Kubernetes binaries:
mv kube-apiserver \
  kube-controller-manager \
  kube-scheduler kubectl \
  /usr/local/bin/
ls -l /usr/local/bin/kube-*


# Configure the Kubernetes API Server
mkdir -p /var/lib/kubernetes/
mv ca.crt ca.key \
  kube-api-server.key kube-api-server.crt \
  service-accounts.key service-accounts.crt \
  encryption-config.yaml \
  /var/lib/kubernetes/
ls -l /var/lib/kubernetes/

## Create the kube-apiserver.service systemd unit file:
mv kube-apiserver.service \
  /etc/systemd/system/kube-apiserver.service
tree /etc/systemd/system


# Configure the Kubernetes Controller Manager

## Move the kube-controller-manager kubeconfig into place:
mv kube-controller-manager.kubeconfig /var/lib/kubernetes/

## Create the kube-controller-manager.service systemd unit file:
mv kube-controller-manager.service /etc/systemd/system/


# Configure the Kubernetes Scheduler

## Move the kube-scheduler kubeconfig into place:
mv kube-scheduler.kubeconfig /var/lib/kubernetes/

## Create the kube-scheduler.yaml configuration file:
mv kube-scheduler.yaml /etc/kubernetes/config/

## Create the kube-scheduler.service systemd unit file:
mv kube-scheduler.service /etc/systemd/system/


# Start the Controller Services : Allow up to 10 seconds for the Kubernetes API Server to fully initialize.
systemctl daemon-reload
systemctl enable kube-apiserver kube-controller-manager kube-scheduler
systemctl start  kube-apiserver kube-controller-manager kube-scheduler

# 확인
ss -tlp | grep kube
 

systemctl is-active kube-apiserver
systemctl status kube-apiserver --no-pager
journalctl -u kube-apiserver --no-pager

systemctl status kube-scheduler --no-pager
systemctl status kube-controller-manager --no-pager

# Verify this using the kubectl command line tool:
kubectl cluster-info dump --kubeconfig admin.kubeconfig
kubectl cluster-info --kubeconfig admin.kubeconfig
# Kubernetes control plane is running at https://127.0.0.1:6443

kubectl get node --kubeconfig admin.kubeconfig
kubectl get pod -A --kubeconfig admin.kubeconfig

kubectl get service,ep --kubeconfig admin.kubeconfig



# clusterroles 확인
kubectl get clusterroles --kubeconfig admin.kubeconfig

kubectl describe clusterroles system:kube-scheduler --kubeconfig admin.kubeconfig


# kube-scheduler subject 확인
kubectl get clusterrolebindings --kubeconfig admin.kubeconfig
kubectl describe clusterrolebindings system:kube-scheduler --kubeconfig admin.kubeconfig


# ==========================================


# api -> kubelet 접속을 위한 RBAC 설정
# Create the system:kube-apiserver-to-kubelet ClusterRole with permissions to access the Kubelet API and perform most common tasks associated with managing pods:
cat kube-apiserver-to-kubelet.yaml
kubectl apply -f kube-apiserver-to-kubelet.yaml --kubeconfig admin.kubeconfig
# clusterrole.rbac.authorization.k8s.io/system:kube-apiserver-to-kubelet created
# clusterrolebinding.rbac.authorization.k8s.io/system:kube-apiserver created

# 확인
kubectl get clusterroles system:kube-apiserver-to-kubelet --kubeconfig admin.kubeconfig
kubectl get clusterrolebindings system:kube-apiserver --kubeconfig admin.kubeconfig

# ---------------------------------------------------------------

 

위 명령어를 실행하면 server에서 쿠버네티스 API가 실행된다. jumpbox로 돌아와서 curl로 API call 하면 아래와 같이 실행되는 것을 볼 수 있다.

curl -s -k --cacert ca.crt https://server.kubernetes.local:6443/version | jq
{
  "major": "1",
  "minor": "32",
  "gitVersion": "v1.32.3",
  "gitCommit": "32cc146f75aad04beaaa245a7157eb35063a9f99",
  "gitTreeState": "clean",
  "buildDate": "2025-03-11T19:52:21Z",
  "goVersion": "go1.23.6",
  "compiler": "gc",
  "platform": "linux/amd64"
}

 

server는 이제 됐다. node-0과 node-1 등을 구성해보자.

# ----> vagrant ssh node-0

pwd
ls -l

# Install the OS dependencies : The socat binary enables support for the kubectl port-forward command.
apt-get -y install socat conntrack ipset kmod psmisc bridge-utils

# Disable Swap : Verify if swap is disabled:
swapon --show

# Create the installation directories
mkdir -p \
  /etc/cni/net.d \
  /opt/cni/bin \
  /var/lib/kubelet \
  /var/lib/kube-proxy \
  /var/lib/kubernetes \
  /var/run/kubernetes

# Install the worker binaries:
mv crictl kube-proxy kubelet runc /usr/local/bin/
mv containerd containerd-shim-runc-v2 containerd-stress /bin/
mv cni-plugins/* /opt/cni/bin/


# Configure CNI Networking

## Create the bridge network configuration file:
mv 10-bridge.conf 99-loopback.conf /etc/cni/net.d/
cat /etc/cni/net.d/10-bridge.conf 

## To ensure network traffic crossing the CNI bridge network is processed by iptables, load and configure the br-netfilter kernel module:
lsmod | grep netfilter
modprobe br-netfilter
echo "br-netfilter" >> /etc/modules-load.d/modules.conf
lsmod | grep netfilter

echo "net.bridge.bridge-nf-call-iptables = 1"  >> /etc/sysctl.d/kubernetes.conf
echo "net.bridge.bridge-nf-call-ip6tables = 1" >> /etc/sysctl.d/kubernetes.conf
sysctl -p /etc/sysctl.d/kubernetes.conf


# Configure containerd : Install the containerd configuration files:
mkdir -p /etc/containerd/
mv containerd-config.toml /etc/containerd/config.toml
mv containerd.service /etc/systemd/system/
cat /etc/containerd/config.toml ; echo


# Configure the Kubelet : Create the kubelet-config.yaml configuration file:
mv kubelet-config.yaml /var/lib/kubelet/
mv kubelet.service /etc/systemd/system/


# Configure the Kubernetes Proxy
mv kube-proxy-config.yaml /var/lib/kube-proxy/
mv kube-proxy.service /etc/systemd/system/


# Start the Worker Services
systemctl daemon-reload
systemctl enable containerd kubelet kube-proxy
systemctl start containerd kubelet kube-proxy


# 확인
systemctl status kubelet --no-pager
systemctl status containerd --no-pager
systemctl status kube-proxy --no-pager

# exit
# -----------------------------------------------------------
# 
# # jumpbox 에서 server 접속하여 kubectl node 정보 확인
# ssh server "kubectl get nodes node-0 -o yaml --kubeconfig admin.kubeconfig" | yq
# ssh server "kubectl get nodes -owide --kubeconfig admin.kubeconfig"
# NAME     STATUS   ROLES    AGE     VERSION   INTERNAL-IP      EXTERNAL-IP   OS-IMAGE                         KERNEL-VERSION   CONTAINER-RUNTIME
# node-0   Ready    <none>   2m48s   v1.32.3   192.168.10.101   <none>        Debian GNU/Linux 12 (bookworm)   6.1.0-40-arm64   containerd://2.1.0-beta.0
# 
# ssh server "kubectl get pod -A --kubeconfig admin.kubeconfig"


# ---------------------------------------------------------

# ----> vagrant ssh node-1

# -----------------------------------------------------------
# Install the OS dependencies : The socat binary enables support for the kubectl port-forward command.
apt-get -y install socat conntrack ipset kmod psmisc bridge-utils

# Create the installation directories
mkdir -p \
  /etc/cni/net.d \
  /opt/cni/bin \
  /var/lib/kubelet \
  /var/lib/kube-proxy \
  /var/lib/kubernetes \
  /var/run/kubernetes

# Install the worker binaries:
mv crictl kube-proxy kubelet runc /usr/local/bin/
mv containerd containerd-shim-runc-v2 containerd-stress /bin/
mv cni-plugins/* /opt/cni/bin/


# Configure CNI Networking

## Create the bridge network configuration file:
mv 10-bridge.conf 99-loopback.conf /etc/cni/net.d/
cat /etc/cni/net.d/10-bridge.conf 

## To ensure network traffic crossing the CNI bridge network is processed by iptables, load and configure the br-netfilter kernel module:
modprobe br-netfilter
echo "br-netfilter" >> /etc/modules-load.d/modules.conf
lsmod | grep netfilter

echo "net.bridge.bridge-nf-call-iptables = 1"  >> /etc/sysctl.d/kubernetes.conf
echo "net.bridge.bridge-nf-call-ip6tables = 1" >> /etc/sysctl.d/kubernetes.conf
sysctl -p /etc/sysctl.d/kubernetes.conf


# Configure containerd : Install the containerd configuration files:
mkdir -p /etc/containerd/
mv containerd-config.toml /etc/containerd/config.toml
mv containerd.service /etc/systemd/system/


# Configure the Kubelet : Create the kubelet-config.yaml configuration file:
mv kubelet-config.yaml /var/lib/kubelet/
mv kubelet.service /etc/systemd/system/


# Configure the Kubernetes Proxy
mv kube-proxy-config.yaml /var/lib/kube-proxy/
mv kube-proxy.service /etc/systemd/system/


# Start the Worker Services
systemctl daemon-reload
systemctl enable containerd kubelet kube-proxy
systemctl start containerd kubelet kube-proxy


# 확인
systemctl status kubelet --no-pager
systemctl status containerd --no-pager
systemctl status kube-proxy --no-pager

 

지금까지 한 과정을 따라하면 jumpbox에서 노드 2개가 실행되고 있음을 알 수 있다.

$ ssh server "kubectl get nodes -owide --kubeconfig admin.kubeconfig"
NAME     STATUS   ROLES    AGE   VERSION   INTERNAL-IP      EXTERNAL-IP   OS-IMAGE                         KERNEL-VERSION   CONTAINER-RUNTIME
node-0   Ready    <none>   93s   v1.32.3   192.168.10.101   <none>        Debian GNU/Linux 12 (bookworm)   6.1.0-40-arm64   containerd://2.1.0-beta.0
node-1   Ready    <none>   15s   v1.32.3   192.168.10.102   <none>        Debian GNU/Linux 12 (bookworm)   6.1.0-40-arm64   containerd://2.1.0-beta.0

 

그리고 jumpbox에서 deployment 과정도 봐보자.

kubectl get pod
kubectl create deployment nginx --image=nginx:latest
kubectl scale deployment nginx --replicas=2
kubectl get pod -owide
NAME                     READY   STATUS    RESTARTS   AGE    IP           NODE     NOMINATED NODE   READINESS GATES
nginx-54c98b4f84-pxp6c   1/1     Running   0          108s   10.200.1.2   node-1   <none>           <none>
nginx-54c98b4f84-qxpbn   1/1     Running   0          12s    10.200.0.2   node-0   <none>           <none>

ssh node-0 crictl ps
ssh node-1 crictl ps
ssh node-0 pstree -ap
ssh node-1 pstree -ap
ssh node-0 brctl show
ssh node-1 brctl show
ssh node-0 ip addr # 파드 별 veth 인터페이스 생성 확인
ssh node-1 ip addr # 파드 별 veth 인터페이스 생성 확인


# server 노드에서 파드 IP로 호출 확인
$ ssh server curl -s 10.200.1.2 | grep title
<title>Welcome to nginx!</title>

$ ssh server curl -s 10.200.0.2 | grep title
<title>Welcome to nginx!</title>

 

jumpbox에서 node-0과 node-1에 nginx 서버를 실행해봤다. 마지막에 Welcome to nginx!로 실행되고 있음을 알 수 있다. 

 

'Kubernetes' 카테고리의 다른 글

워커 노드 Join 하기  (0) 2026.01.25
kubeadm으로 k8s 구성하기  (0) 2026.01.25
Ansible: 사용해보기  (0) 2026.01.18
Ansible: 에이전트 없이 완성하는 자동화  (0) 2026.01.18
Posted by 빛나유
,