diff --git a/COMMON_COMMANDS.md b/COMMON_COMMANDS.md deleted file mode 100644 index 6f7a54b..0000000 --- a/COMMON_COMMANDS.md +++ /dev/null @@ -1,3 +0,0 @@ -df -h - Check mount points -utop - system activity -lsblk diff --git a/MAINTENANCE.md b/MAINTENANCE.md deleted file mode 100644 index 4f4121a..0000000 --- a/MAINTENANCE.md +++ /dev/null @@ -1,85 +0,0 @@ -## Table of Contents - -- [Updating K3S on the Nodes](#updating-k3s-on-the-nodes) - ---- - -### Updating K3S on the Nodes - -To update k3s on your Raspberry Pis, you can follow these steps: - -1. **Backup your existing setup**: Always ensure you have backups, especially of your k3s server data and any critical configuration. - -2. **Drain the node**: If you're updating one node at a time in a cluster, drain the node to safely remove it from the cluster during the update. - -```bash -kubectl drain --ignore-daemonsets --delete-emptydir-data -``` - -3. **Stop k3s service**: Before updating, stop the k3s service on the node. - -```bash -sudo systemctl stop k3s -``` - -4. **Update k3s**: Download and install the latest version of k3s on the Raspberry Pi. You can use the installation script provided by k3s for updating it as well. - -```bash -curl -sfL https://get.k3s.io | sh - -``` - -5. **Start k3s service**: After the update, start the k3s service again. - -```bash -sudo systemctl start k3s -``` - -6. **Uncordon the node**: If you drained the node earlier, make it schedulable again by uncordoning it. - -```bash -kubectl uncordon -``` - -7. **Verify the update**: Check the version of k3s to confirm the update was successful. - -```bash -k3s --version -``` - -8. **Repeat for other nodes**: If you have multiple Raspberry Pis, repeat these steps for each node. - -Ensure each step is completed without errors before proceeding to the next. If managing multiple nodes, consider automating the process with scripts or using a configuration management tool like Ansible. - -### Explanation - -In the context of Kubernetes, "draining a node" involves safely evicting all the pods from the node so that it can be taken down for maintenance or updates. This is a critical step to ensure that the services continue to run smoothly on other nodes while one node is temporarily out of the cluster. - -#### Draining a Node - -When you drain a node, Kubernetes does the following: - -- **Evicts pods**: All the pods that are not part of the Kubernetes system (i.e., user pods) are safely evicted. System pods and those marked with a `PodDisruptionBudget` that cannot tolerate a disruption may remain unless specified otherwise. -- **Prevents new pods from being scheduled**: While a node is drained, it is marked as unschedulable, which means no new pods will be scheduled onto the node until it is uncordoned. - -The typical command to drain a node is: - -```bash -kubectl drain --ignore-daemonsets --delete-local-data -``` - -Options used: - -- `--ignore-daemonsets`: Allows the drain command to ignore DaemonSet-managed pods, which cannot be killed. -- `--delete-local-data`: Allows deleting pods with local storage, like EmptyDir volumes. - -### Uncordoning a Node - -Uncordoning a node reverses the draining process. This makes the node schedulable again, allowing Kubernetes to start placing new pods onto it as needed by the scheduler's normal behavior. This is done after maintenance or updates are completed and the node is ready to rejoin the cluster. - -The command to uncordon a node is: - -```bash -kubectl uncordon -``` - -Draining is an essential tool for cluster maintenance and upgrades, helping minimize disruptions in a production environment by gracefully handling pod migrations. diff --git a/RAID_1_SETUP.md b/RAID_1_SETUP.md deleted file mode 100644 index b09f255..0000000 --- a/RAID_1_SETUP.md +++ /dev/null @@ -1,91 +0,0 @@ -Using RAID (Redundant Array of Independent Disks) can help you achieve redundancy for data protection. For your requirements, you would use RAID 1, which mirrors data across both disks. - -Here's how you can configure RAID 1 on your Raspberry Pi using `mdadm`, a software RAID utility: - -### **1. Install mdadm** -First, you need to install `mdadm` if it's not already present. - -```bash -sudo apt update -sudo apt install mdadm -``` - -### **2. Create the RAID Array** -Assuming your disks are `/dev/sda1` and `/dev/sdb1`, you can create a RAID 1 array like this: - -```bash -sudo mdadm --create --verbose /dev/md0 --level=1 --raid-devices=2 /dev/sda1 /dev/sdb1 -``` - -- `/dev/md0` is the virtual disk for the RAID array. -- `--level=1` specifies RAID 1. -- `--raid-devices=2` means you'll be using two devices for the array. - -### **3. Verify RAID Array Creation** -Check the RAID status with the following command: - -```bash -cat /proc/mdstat -``` - -This should show the RAID array's status, indicating that it's syncing, initializing, or active. - -### **4. Create a Filesystem on the RAID Array** -Format the new RAID array with a filesystem, for instance, ext4: - -```bash -sudo mkfs.ext4 /dev/md0 -``` - -### **5. Mount the RAID Array** -Create a directory to mount the RAID array and mount it: - -```bash -sudo mkdir -p /mnt/raid1 -sudo mount /dev/md0 /mnt/raid1 -``` - -### **6. Configure Auto-Mounting on Boot** -Edit the `/etc/fstab` file to auto-mount the RAID array on boot: - -1. **Get the UUID of RAID Array:** - ```bash - sudo blkid /dev/md0 - ``` - -2. **Add to fstab:** - - Open fstab: - ```bash - sudo nano /etc/fstab - ``` - - Add the entry (replace `UUID=xxxx` with your actual UUID from the blkid command): - ``` - UUID=xxxx /mnt/raid1 ext4 defaults 0 0 - ``` - -### **7. Postgres Configuration** -You'll need to point PostgreSQL to use this RAID array for its data directory. Here's a basic outline: - -1. **Stop PostgreSQL Service:** - ```bash - sudo systemctl stop postgresql - ``` - -2. **Move Data Directory:** - ```bash - sudo rsync -av /var/lib/postgresql /mnt/raid1 - ``` - -3. **Adjust PostgreSQL Configuration:** - Open the PostgreSQL configuration file, usually located at `/etc/postgresql/*/main/postgresql.conf`, and update the data directory to `/mnt/raid1/postgresql`. - - ```bash - sudo nano /etc/postgresql/*/main/postgresql.conf - ``` - -4. **Start PostgreSQL Service:** - ```bash - sudo systemctl start postgresql - ``` - -With these steps, your PostgreSQL database should be storing its data on the RAID 1 array, ensuring redundancy. Make sure to test and validate the setup to ensure everything is working correctly. Let me know if you need further assistance! \ No newline at end of file diff --git a/README.md b/README.md index 0b7abe9..d1aefcf 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ **Goal**: By the end of this journey, aim to have the capability to rapidly instantiate new development and production environments and expose them to the external world with equal ease. ## Table of Contents -- [Hardware](#hardware) +- [Hardware](./docs/hardware-components.md#hardware) - [Hardware Components](./docs/hardware-components.md#hardware) - [Why These Choices?](./docs/hardware-components.md#why-these-choices) - [Raspberry Pi's Setup](./docs/raspberry-pi-setup.md#raspberry-pis-setup) @@ -24,7 +24,7 @@ - [Assign Static IP Addresses](./docs/raspberry-pi-setup.md#assign-static-ip-addresses) - [Set SSH Aliases](./docs/raspberry-pi-setup.md#set-ssh-aliases) -- [Automation with Ansible](./SETTING_UP_ANSIBLE.md) +- [Automation with Ansible](./docs/getting-started-with-ansible.md) - [K3S Setup](./docs/k3s-setup.md#k3s-setup) - [Enable Memory CGroups](./docs/k3s-setup.md#enable-memory-cgroups-ansible-playbook) @@ -49,4 +49,6 @@ - [Service Exposure](./docs/getting-started-with-kubernetes.md#service-exposure) - [Verify Deployment](./docs/getting-started-with-kubernetes.md#verify-deployment) - [Cleanup](./docs/getting-started-with-kubernetes.md#cleanup-wiping-everything-and-starting-over) - - [Basic Kubernetes Deployments](./docs/getting-started-with-kubernetes.md#basic-kubernetes-deployments) \ No newline at end of file + - [Basic Kubernetes Deployments](./docs/getting-started-with-kubernetes.md#basic-kubernetes-deployments) + - [K3S Maintenance](./docs/k3s-maintenance.md) + - [Common Commands](./docs/common-kubernetes-commands.md) diff --git a/ansible/host_vars/rp_1.yml b/ansible/host_vars/rp_1.yml deleted file mode 100644 index de99138..0000000 --- a/ansible/host_vars/rp_1.yml +++ /dev/null @@ -1,3 +0,0 @@ -ansible_host: 192.168.88.242 -ansible_user: aleksandar -k3s_master_node: true \ No newline at end of file diff --git a/ansible/host_vars/rp_2.yml b/ansible/host_vars/rp_2.yml deleted file mode 100644 index 15f17ca..0000000 --- a/ansible/host_vars/rp_2.yml +++ /dev/null @@ -1,2 +0,0 @@ -ansible_host: 192.168.88.243 -ansible_user: aleksandar \ No newline at end of file diff --git a/ansible/host_vars/rp_3.yml b/ansible/host_vars/rp_3.yml deleted file mode 100644 index 43084db..0000000 --- a/ansible/host_vars/rp_3.yml +++ /dev/null @@ -1,2 +0,0 @@ -ansible_host: 192.168.88.241 -ansible_user: aleksandar \ No newline at end of file diff --git a/ansible/host_vars/rp_4.yml b/ansible/host_vars/rp_4.yml deleted file mode 100644 index d3aabc7..0000000 --- a/ansible/host_vars/rp_4.yml +++ /dev/null @@ -1,2 +0,0 @@ -ansible_host: 192.168.88.240 -ansible_user: aleksandar \ No newline at end of file diff --git a/ansible/inventory.yml b/ansible/inventory.yml deleted file mode 100644 index 77b2a0a..0000000 --- a/ansible/inventory.yml +++ /dev/null @@ -1,16 +0,0 @@ -all_nodes: - hosts: - rp_1: - rp_2: - rp_3: - rp_4: - -worker_nodes: # A group for all worker nodes - hosts: - rp_2: - rp_3: - rp_4: - -postgres_and_redis: # A group for the PostgreSQL and Redis servers - hosts: - rp_2: \ No newline at end of file diff --git a/ansible/library/my_test.py b/ansible/library/my_test.py deleted file mode 100644 index c925129..0000000 --- a/ansible/library/my_test.py +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/python - -# Copyright: (c) 2018, Terry Jones -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -DOCUMENTATION = r''' ---- -module: my_test - -short_description: This is my test module - -# If this is part of a collection, you need to use semantic versioning, -# i.e. the version is of the form "2.5.0" and not "2.4". -version_added: "1.0.0" - -description: This is my longer description explaining my test module. - -options: - name: - description: This is the message to send to the test module. - required: true - type: str - new: - description: - - Control to demo if the result of this module is changed or not. - - Parameter description can be a list as well. - required: false - type: bool -# Specify this value according to your collection -# in format of namespace.collection.doc_fragment_name -# extends_documentation_fragment: -# - my_namespace.my_collection.my_doc_fragment_name - -author: - - Your Name (@yourGitHubHandle) -''' - -EXAMPLES = r''' -# Pass in a message -- name: Test with a message - my_namespace.my_collection.my_test: - name: hello world - -# pass in a message and have changed true -- name: Test with a message and changed output - my_namespace.my_collection.my_test: - name: hello world - new: true - -# fail the module -- name: Test failure of the module - my_namespace.my_collection.my_test: - name: fail me -''' - -RETURN = r''' -# These are examples of possible return values, and in general should use other names for return values. -original_message: - description: The original name param that was passed in. - type: str - returned: always - sample: 'hello world' -message: - description: The output message that the test module generates. - type: str - returned: always - sample: 'goodbye' -''' - -from ansible.module_utils.basic import AnsibleModule - - -def run_module(): - # define available arguments/parameters a user can pass to the module - module_args = dict( - name=dict(type='str', required=True), - new=dict(type='bool', required=False, default=False) - ) - - # seed the result dict in the object - # we primarily care about changed and state - # changed is if this module effectively modified the target - # state will include any data that you want your module to pass back - # for consumption, for example, in a subsequent task - result = dict( - changed=False, - original_message='', - message='' - ) - - # the AnsibleModule object will be our abstraction working with Ansible - # this includes instantiation, a couple of common attr would be the - # args/params passed to the execution, as well as if the module - # supports check mode - module = AnsibleModule( - argument_spec=module_args, - supports_check_mode=True - ) - - # if the user is working with this module in only check mode we do not - # want to make any changes to the environment, just return the current - # state with no modifications - if module.check_mode: - module.exit_json(**result) - - # manipulate or modify the state as needed (this is going to be the - # part where your module will do what it needs to do) - result['original_message'] = module.params['name'] - result['message'] = 'goodbye' - - # use whatever logic you need to determine whether or not this module - # made any modifications to your target - if module.params['new']: - result['changed'] = True - - # during the execution of the module, if there is an exception or a - # conditional state that effectively causes a failure, run - # AnsibleModule.fail_json() to pass in the message and the result - if module.params['name'] == 'fail me': - module.fail_json(msg='You requested this to fail', **result) - - # in the event of a successful module execution, you will want to - # simple AnsibleModule.exit_json(), passing the key/value results - module.exit_json(**result) - - -def main(): - run_module() - - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/ansible/playbooks/disable-swap.yml b/ansible/playbooks/disable-swap.yml deleted file mode 100644 index 13efbaf..0000000 --- a/ansible/playbooks/disable-swap.yml +++ /dev/null @@ -1,55 +0,0 @@ ---- -- name: Disable swap temporarily and configure permanently - hosts: all_nodes # Target all nodes by default - become: yes # Ensure the tasks are executed with sudo privileges - - tasks: - - # Step 1: Turn off swap temporarily (disable it for the current session) - - name: Disable swap temporarily - ansible.builtin.command: swapoff -a - ignore_errors: true # Continue even if swap is already off - - # Step 2: Set CONF_SWAPSIZE to 0 in /etc/dphys-swapfile to disable swap permanently - - name: Set CONF_SWAPSIZE to 0 in /etc/dphys-swapfile - ansible.builtin.lineinfile: - path: /etc/dphys-swapfile - regexp: '^CONF_SWAPSIZE=' - line: 'CONF_SWAPSIZE=0' - state: present # Ensure the line is present - - # Step 3: Delete the existing swap file as it is no longer needed - - name: Remove existing /var/swap file - ansible.builtin.file: - path: /var/swap - state: absent # Remove the file if it exists - - # Step 4: Stop the dphys-swapfile service immediately - - name: Stop dphys-swapfile service - ansible.builtin.service: - name: dphys-swapfile - state: stopped - - # Step 5: Disable dphys-swapfile service to prevent it from running on boot - - name: Disable dphys-swapfile service - ansible.builtin.service: - name: dphys-swapfile - enabled: no - - # Step 6: Verify swap is turned off and the removal has been successful - - name: Verify swap is turned off - ansible.builtin.command: free -m - register: memory_status - changed_when: false # This won't change any system state, just checking command output - - # Step 7: Display the memory status to confirm swap is turned off - - name: Display memory status (to verify swap is disabled) - ansible.builtin.debug: - var: memory_status.stdout_lines - - # Step 8: Reboot the machine to apply changes fully - - name: Reboot the machines to complete swap disabling - ansible.builtin.reboot: - reboot_timeout: 600 # Give the node 10 minutes to reboot and come back online - msg: "Rebooting the node to apply permanent swap configuration changes" - pre_reboot_delay: 5 # Delay 5 seconds before issuing the reboot command diff --git a/ansible/playbooks/join-worker-nodes.yml b/ansible/playbooks/join-worker-nodes.yml deleted file mode 100644 index fb5bef6..0000000 --- a/ansible/playbooks/join-worker-nodes.yml +++ /dev/null @@ -1,37 +0,0 @@ ---- -- name: Join Worker Nodes to K3s Cluster - hosts: all_nodes - become: true - vars: - k3s_token: "" - # Identify your master node - k3s_master_node: rp_1 - - tasks: - - - name: Retrieve join token from the master node - shell: cat /var/lib/rancher/k3s/server/token - register: join_token - delegate_to: "{{ k3s_master_node }}" - run_once: true # Retrieve the token only once on the master node - - - name: Set K3S_TOKEN variable with the join token - set_fact: - k3s_token: "{{ join_token.stdout }}" # Directly access stdout of token retrieval - - - name: Install K3s and join the cluster - shell: | - curl -sfL https://get.k3s.io | K3S_URL=https://192.168.88.242:6443 K3S_TOKEN={{ k3s_token }} sh - - args: - executable: /bin/bash - - - name: Verify that the node has joined the cluster - command: kubectl get nodes - register: node_status - retries: 5 - delay: 10 - until: node_status.stdout is search(ansible_hostname) - - - name: Show the status of the nodes - debug: - var: node_status.stdout diff --git a/ansible/playbooks/setup-postgres.yml b/ansible/playbooks/setup-postgres.yml deleted file mode 100644 index a995cd5..0000000 --- a/ansible/playbooks/setup-postgres.yml +++ /dev/null @@ -1,70 +0,0 @@ ---- -- name: Setup PostgreSQL Docker Container with Python Virtual Environment - hosts: postgres_and_redis - become: true - vars: - postgres_db: test_db # Replace with your database name - postgres_user: test_user # Replace with your username - postgres_password: test_password # Replace with your password - docker_network: test-pg-network # Name of the Docker network - postgres_container_name: test-postgres # Name of the PostgreSQL container - mount_point: /mnt/storage # External storage location for PostgreSQL data - pgdata_directory: "{{ mount_point }}/pgdata" # Directory on disk to bind mount - venv_path: /opt/venv_ansible_docker # Path to the Python virtual environment - - tasks: - - name: Ensure Python 3, venv, and Docker are installed - apt: - name: - - python3 - - python3-pip - - python3-venv # Ensure venv is installed for creating virtual environments - - docker.io # Install Docker - state: present - - - name: Create a Python Virtual Environment - command: python3 -m venv {{ venv_path }} - args: - creates: "{{ venv_path }}/bin/activate" # Idempotent task, only create if not exists - - - name: Install Docker SDK in the virtual environment (via pip) - command: "{{ venv_path }}/bin/pip install docker" - environment: - PATH: "{{ venv_path }}/bin:{{ ansible_env.PATH }}" # Use venv's pip - - - name: Start Docker service - systemd: - name: docker - state: started - enabled: true - - - name: Ensure Docker network exists - docker_network: - name: "{{ docker_network }}" - state: present - - - name: Ensure the pgdata directory exists on the mounted drive - ansible.builtin.file: - path: "{{ pgdata_directory }}" - state: directory - owner: root - group: root - mode: '0755' - - - name: Run PostgreSQL container - docker_container: - name: "{{ postgres_container_name }}" - image: postgres - restart_policy: always - network_mode: "{{ docker_network }}" - ports: - - "5432:5432" - volumes: - - "{{ pgdata_directory }}:/var/lib/postgresql/data" # Bind mount storage - env: - POSTGRES_DB: "{{ postgres_db }}" - POSTGRES_USER: "{{ postgres_user }}" - POSTGRES_PASSWORD: "{{ postgres_password }}" - state: started - environment: - PATH: "{{ venv_path }}/bin:{{ ansible_env.PATH }}" # Use venv's path for Docker SDK diff --git a/assets/diagrams/plan.drawio b/assets/diagrams/plan.drawio index e751e8f..9b7c031 100644 --- a/assets/diagrams/plan.drawio +++ b/assets/diagrams/plan.drawio @@ -1,6 +1,6 @@ - + @@ -129,6 +129,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/hardware-components.md b/docs/hardware-components.md deleted file mode 100644 index fd70caf..0000000 --- a/docs/hardware-components.md +++ /dev/null @@ -1,38 +0,0 @@ -## Hardware -### Hardware Components - -The setup illustrated here is not mandatory but reflects my personal choices based on both experience and specific requirements. I aimed for a setup that is not only robust but also relatively mobile. Therefore, I opted for a 4U Rack where all the components are neatly encapsulated, making it easy to plug and play. I plan to expand this cluster by adding another four Raspberry Pis once the prices are more accommodating. - -- **[Mikrotik RB3011UiAS-RM](https://mikrotik.com/product/RB3011UiAS-RM)**: I chose Mikrotik's router as it offers a professional-grade, feature-rich solution at an affordable price. This router allows for a myriad of configurations and functionalities that you'd typically find in higher-end solutions like Cisco. Its features like robust firewall options, VPN support, and advanced routing capabilities made it a compelling choice. - -- **[4x Raspberry Pi 4 B 8GB](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/)**: I opted for the 8GB variant of the Raspberry Pi 4 B for its performance capabilities. The 8GB RAM provides ample room for running multiple containers and allows for future scalability. - -- **[4U Rack Cabinet](https://www.compumail.dk/en/p/lanberg-rack-gra-993865294)**: A 4U Rack to encapsulate all components cleanly. It provides the benefit of space efficiency and easy access for any hardware changes or additions. - -- **[Rack Power Supply](https://www.compumail.dk/en/p/lanberg-pdu-09f-0300-bk-stromstodsbeskytter-9-stik-16a-sort-3m-996106700)**: A centralized power supply solution for the entire rack. Ensures consistent and reliable power distribution to all the components. - -- **[GeeekPi 1U Rack Kit for Raspberry Pi 4B, 19" 1U Rack Mount](https://www.amazon.de/-/en/gp/product/B0972928CN/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1)**: This 19 inch rack mount kit is specially designed for recording Raspberry Pi 4B boards and supports up to 4 units. - -- **[SanDisk Extreme microSDHC 3 Rescue Pro Deluxe Memory Card, Red/Gold 64GB](https://www.amazon.de/-/en/gp/product/B07FCMBLV6/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1)**: Up to 160MB/s Read speed and 60 MB/s. Write speed for fast recording and transferring - -- **[Vanja SD/Micro SD Card Reader](https://www.amazon.de/-/en/gp/product/B00W02VHM6/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1)**: Micro USB OTG Adapter and USB 2.0 Memory Card Reader - -- **[deleyCON 5 x 0.25 m CAT8.1](https://www.amazon.de/-/en/gp/product/B08WPJVGHR/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&th=1)**: deleyCON CAT 8.1 patch cable network cable as set // 2x RJ45 plug // S/FTP PIMF shielding - -- **[CSL CAT.8 Network Cable 40 Gigabit](https://www.amazon.de/-/en/gp/product/B08FCLHTH5/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&th=1)**: CSL CAT.8 Network Cable 40 Gigabit - -- **[2x Verbatim Vi550 S3 SSD](https://www.amazon.de/dp/B07LGKQLT5?ref=ppx_yo2ov_dt_b_fed_asin_title&th=1)** - -- **[2x JSAUX USB 3.0 to SATA Adapter](https://www.amazon.de/dp/B086W944YT?ref=ppx_yo2ov_dt_b_fed_asin_title)** - - - -### Why These Choices? - -**Mobility**: The 4U Rack allows me to move the entire setup easily, making it convenient for different scenarios, from a home office to a small business environment. - -**Professional-Grade Networking**: The Mikrotik router provides a rich feature set generally found in enterprise-grade hardware, offering me a sandbox to experiment with advanced networking configurations. - -**Scalability**: The Raspberry Pi units and the Rack setup are easily scalable. I can effortlessly add more Pis to the cluster, enhancing its capabilities. - -**Affordability**: This setup provides a balance between cost and performance, giving me a powerful Kubernetes cluster without breaking the bank. diff --git a/docs/k3s-setup.md b/docs/k3s-setup.md deleted file mode 100644 index 2377625..0000000 --- a/docs/k3s-setup.md +++ /dev/null @@ -1,132 +0,0 @@ -## K3S Setup - -### Enable Memory Cgroups ([Ansible Playbook](./ansible/playbooks/enable-memory-groups.yml)) - -```txt -Control Groups (Cgroups) are a Linux kernel feature that allows you to allocate resources such as CPU time, system memory, and more among user-defined groups of tasks (processes). K3s requires memory cgroups to be enabled to better manage and restrict the resources that each container can use. This is crucial in a multi-container environment where resource allocation needs to be as efficient as possible. - -Simple Analogy: Imagine you live in a house with multiple people (processes), and there are limited resources like time (CPU), space (memory), and tools (I/O). Without a system in place, one person might hog the vacuum cleaner all day (CPU time), while someone else fills the fridge with their stuff (memory). - -With a `"chore schedule"` (cgroups), you ensure everyone gets an allocated time with the vacuum cleaner, some space in the fridge, and so on. This schedule ensures that everyone can do their chores without stepping on each other's toes, much like how cgroups allocate system resources to multiple processes. -``` - -Before installing K3s, it's essential to enable memory cgroups on the Raspberry Pi for effective container resource management. - -Edit the `/boot/firmware/cmdline.txt` file on your Raspberry Pi. - -```bash -sudo vi /boot/firmware/cmdline.txt -``` - -Append the following to enable memory cgroups. - -```text -cgroup_memory=1 cgroup_enable=memory -``` - -Save the file and reboot your Raspberry Pi. - -```bash -sudo reboot -``` - -### Setup the Master Node - -Select one Raspberry Pi to act as the master node, and install K3S: - -```bash -curl -sfL https://get.k3s.io | sh - -``` - -**Copy and Set Permissions for Kubeconfig**: To avoid permission issues when using kubectl, copy the generated Kubeconfig to your home directory and update its ownership. - -```bash -# Create the .kube directory in the user's home directory if it doesn't already exist -mkdir -p ~/.kube - -# Copy the k3s.yaml file from its default location to the user's .kube directory as the default kubectl config file -sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config - -# Change the ownership of the copied config file to the current user and group, so kubectl can access it without requiring sudo -sudo chown $(id -u):$(id -g) ~/.kube/config -``` - -**Verify Cluster**: Ensure that `/etc/rancher/k3s/k3s.yaml` was created and the cluster is accessible. - -```bash -kubectl --kubeconfig ~/.kube/config get nodes -``` - -**Set KUBECONFIG Environment Variable**: To make it more convenient to run `kubectl` commands without having to specify the `--kubeconfig` flag every time, you can set an environment variable to automatically point to the kubeconfig file. - -```bash -export KUBECONFIG=~/.kube/config -``` - -To make this setting permanent across shell sessions, add it to your shell profile: - -```bash -echo "export KUBECONFIG=~/.kube/config" >> ~/.bashrc -source ~/.bashrc -``` - -By doing this, you streamline your workflow, allowing you to simply run `kubectl get nodes` instead of specifying the kubeconfig path each time. - ---- - -### Setup Worker Nodes - -**Join Tokens**: On the master node, retrieve the join token from `/var/lib/rancher/k3s/server/token`. - -```bash -vi /var/lib/rancher/k3s/server/token -``` - -**Worker Installation**: Use this token to join each worker node to the master. - -```bash -curl -sfL https://get.k3s.io | K3S_URL=https://:6443 K3S_TOKEN= sh - -``` - -**Node Verification**: Check that all worker nodes have joined the cluster. On your master node, run: - -```bash -kubectl get nodes -``` - ---- - -### Setup kubectl on your local machine - -#### Kubeconfig - -After setting up your cluster, it's more convenient to manage it remotely from your local machine. - -Here's how to do that: - -**Create the `.kube` directory on your local machine if it doesn't already exist.** - -```bash -mkdir -p ~/.kube -``` - -**Copy the kubeconfig from the master node to your local `.kube` directory.** - -```bash -scp @:~/.kube/config ~/.kube/config -``` -Replace `` with your username and `` with the IP address of your master node. - -**Note**: If you encounter a permissions issue while copying, ensure that the `~/.kube/config` on your master node is owned by your user and is accessible. You might have to adjust file permissions or ownership on the master node accordingly. - -**Update the kubeconfig server details (Optional)** - -Open your local `~/.kube/config` and make sure the `server` IP matches your master node's IP. If it's set to `127.0.0.1`, you'll need to update it. - -```yaml -server: https://:6443 -``` - -Replace `` with the IP address of your master node. - -After completing these steps, you should be able to run `kubectl` commands from your local machine to interact with your Kubernetes cluster. This avoids the need to SSH into the master node for cluster management tasks. \ No newline at end of file diff --git a/docs/kubernetes-theory.md b/docs/kubernetes-theory.md deleted file mode 100644 index 4d2d054..0000000 --- a/docs/kubernetes-theory.md +++ /dev/null @@ -1,63 +0,0 @@ -# Kubernetes Theory - -## What is Kubernetes? 🎥 -- [Kubernetes Explained in 6 Minutes | k8s Architecture](https://www.youtube.com/watch?v=TlHvYWVUZyc&ab_channel=ByteByteGo) -- [Kubernetes Explained in 15 Minutes](https://www.youtube.com/watch?v=r2zuL9MW6wc) -- Kubernetes is an open-source container orchestration platform that automates the deployment, scaling, and management of containerized applications. - -### Kubernetes Components Explained - -#### Control Plane Components - -- **API Server**: - - Acts as the front-end for the Kubernetes control plane. - -- **etcd**: - - Consistent and highly-available key-value store used as Kubernetes' backing store for all cluster data. - -- **Scheduler**: - - Responsible for scheduling pods onto nodes. - -- **Controller Manager**: - - Runs controllers, which are background threads that handle routine tasks in the cluster. - -#### Worker Node Components - -- **Worker Node**: - - Machines, VMs, or physical computers that run your applications. - -- **Pods**: - - The smallest deployable units of computing that can be created and managed in Kubernetes. - -- **kubelet**: - - An agent that runs on each worker node in the cluster and ensures that containers are running in a pod. - -- **kube-proxy**: - - Maintains network rules on nodes, allowing network communication to your Pods from network sessions inside or outside of your cluster. - - - -#### 2. Why Use Kubernetes? -- **Scaling**: Easily scale applications up or down as needed. -- **High Availability**: Ensure that your applications are fault-tolerant and highly available. -- **Portability**: Move workloads across different cloud providers or on-premises environments. -- **Declarative Configuration**: Describe what you want, and Kubernetes makes it happen. - -#### 3. Core Components and Concepts -- **Control Plane**: The set of components that manage the overall state of the cluster. -- **Nodes**: The worker machines that run containers. -- **Pods**: The smallest deployable units that can contain one or more containers. -- **Services**: A way to expose Pods to the network. -- **Ingress**: Manages external access to services within a cluster. -- **ConfigMaps and Secrets**: Manage configuration data and secrets separately from container images. - -#### 4. Architecture Overview -- **Bottom-Up View**: Understand Kubernetes from the infrastructure (Nodes) to Pods, to Services, and upwards. -- **Top-Down View**: Start from the user's perspective, breaking down what you want to deploy into services, pods, and the underlying infrastructure. - -#### 5. Read and Research -- Go through [Kubernetes' official documentation](https://kubernetes.io/docs/home/). -- Watch [beginner-friendly YouTube tutorials](https://www.youtube.com/watch?v=d6WC5n9G_sM&ab_channel=freeCodeCamp.org) or online courses. - -#### 6. Community and Ecosystem -- Get familiar with the wider Kubernetes ecosystem, including tooling, forums, and meetups. diff --git a/docusaurus/.gitignore b/docusaurus/.gitignore new file mode 100644 index 0000000..ec2327a --- /dev/null +++ b/docusaurus/.gitignore @@ -0,0 +1,22 @@ +# Dependencies +/node_modules + +# Production +/build + +# Generated files +.docusaurus +.cache-loader + +# Misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +package-lock.json diff --git a/docusaurus/.nvmrc b/docusaurus/.nvmrc new file mode 100644 index 0000000..156ca6d --- /dev/null +++ b/docusaurus/.nvmrc @@ -0,0 +1 @@ +22.16.0 \ No newline at end of file diff --git a/docusaurus/README.md b/docusaurus/README.md new file mode 100644 index 0000000..0c6c2c2 --- /dev/null +++ b/docusaurus/README.md @@ -0,0 +1,41 @@ +# Website + +This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. + +### Installation + +``` +$ yarn +``` + +### Local Development + +``` +$ yarn start +``` + +This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. + +### Build + +``` +$ yarn build +``` + +This command generates static content into the `build` directory and can be served using any static contents hosting service. + +### Deployment + +Using SSH: + +``` +$ USE_SSH=true yarn deploy +``` + +Not using SSH: + +``` +$ GIT_USER= yarn deploy +``` + +If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. diff --git a/SETTING_UP_ANSIBLE.md b/docusaurus/docs/ansible/automation-with-ansible.md similarity index 72% rename from SETTING_UP_ANSIBLE.md rename to docusaurus/docs/ansible/automation-with-ansible.md index ce101af..ed7d089 100644 --- a/SETTING_UP_ANSIBLE.md +++ b/docusaurus/docs/ansible/automation-with-ansible.md @@ -1,14 +1,17 @@ -# Getting Started with Ansible +--- +sidebar_position: 4 +title: Automation with Ansible +--- After setting up one of our Raspberry Pi devices, it's easy to see how tedious it would be to SSH into the other three devices and manually repeat each step. This process is not only time-consuming but also error-prone, given that each step is done manually. -To make things more efficient, we can turn to **Ansible**—a tool that allows us to automate tasks across multiple machines. To get started, refer to the official [Getting Started](https://docs.ansible.com/ansible/latest/getting_started/index.html) guide. +To make things more efficient, we can turn to **Ansible**, a tool that allows us to automate tasks across multiple machines. To get started, refer to the official [Getting Started](https://docs.ansible.com/ansible/latest/getting_started/index.html) guide. ## Installation and PATH Configuration -Once Ansible has been installed, you *might** encounter a warning indicating that some Ansible executables (like `ansible-doc`, `ansible-galaxy`, and others) are installed in `/home/YOUR_USER/.local/bin`, which is not included in your system’s PATH. +Once Ansible has been installed, you \*might\*\* encounter a warning indicating that some Ansible executables (like `ansible-doc`, `ansible-galaxy`, and others) are installed in `/home/YOUR_USER/.local/bin`, which is not included in your system’s PATH. -To resolve this, you will need to edit your shell profile. If you’re using Bash, open the `.bashrc` file with `nano ~/.bashrc`. For Zsh users, you should open `.zshrc` by running `nano ~/.zshrc`. +To resolve this, you will need to edit your shell profile. If you’re using Bash, open the `.bashrc` file with `nano ~/.bashrc`. For Zsh users, you should open `.zshrc` by running `nano ~/.zshrc`. At the end of the file, you should add this line: @@ -36,7 +39,7 @@ Ansible Vault is a tool that allows you to securely store sensitive information ansible-vault create secrets.yml ``` -When prompted, set a password—this password will be required every time you access or modify the vault file. After you’ve set the password, you can include sensitive data in the `secrets.yml` file using YAML format. For example, you might include the IP addresses and credentials for each Raspberry Pi: +When prompted, set a password, this password will be required every time you access or modify the vault file. After you’ve set the password, you can include sensitive data in the `secrets.yml` file using YAML format. For example, you might include the IP addresses and credentials for each Raspberry Pi: ```yaml all: @@ -86,4 +89,4 @@ ansible-vault edit secrets.yml For more complex setups, such as managing different environments, you can create separate encrypted inventory files, like `prod_secrets.yml` and `dev_secrets.yml`. You can also organize secrets by groups or hosts by creating encrypted files for each, stored in the `group_vars` and `host_vars` directories. This approach allows for fine-grained control over your environments while keeping sensitive data secure. -By following these steps, you can ensure both automation and security when working with multiple Raspberry Pi devices through Ansible. With the help of Ansible Vault, sensitive credentials like passwords and IP addresses are encrypted and protected from unauthorized access, while still being usable whenever Ansible tasks need them. \ No newline at end of file +In the context of our cluster, we won't be using Ansible in any complex way. We will be using it to automate the setup of our cluster. diff --git a/docusaurus/docs/databases/databases-within-kubernetes.md b/docusaurus/docs/databases/databases-within-kubernetes.md new file mode 100644 index 0000000..04393c8 --- /dev/null +++ b/docusaurus/docs/databases/databases-within-kubernetes.md @@ -0,0 +1,95 @@ +--- +title: Hosting Databases within Kubernetes +--- + +While researching and writing this Kubernetes series, I probably went through hundreds of articles, forum posts, and Reddit comments about a single core question: + +_Should I host a database within my Kubernetes cluster, or should I use a managed database service instead?_ + +It's a question that continues to pop up frequently, and honestly, as someone who's still relatively new to Kubernetes, it's not surprising why. The concern is valid and widely shared among both beginners and experts, not just in the Kubernetes community but also in modern DevOps and infrastructure contexts. + +## The Historical Context: Stateless vs. Stateful + +For a long time, the best practices around Kubernetes have revolved around the concept of "stateless" applications. The principles behind Kubernetes were designed to scale and recover from failures effortlessly. Because of its inherent design for self-healing and declarative state, Kubernetes excels at running stateless applications, where any pod or container can die, be recreated, and get back to running almost immediately, with no impact on the application's availability, given that these don’t carry persistent data within themselves. + +Here's a typical example: + +- Imagine a web server or an API service. If one of the replicas of a stateless service goes down or gets killed by the scheduler, Kubernetes just spins up another one somewhere else, connects it to the load balancer, and resumes traffic, all without anyone noticing. Simple! + +But when it comes to _stateful workloads_ like _databases_, it's a different story. This is where Kubernetes' stateless-first orientation starts to clash with the persistence and durability requirements of databases. The whole point of a database is to store data in a reliable and consistent way that survives pod failures, node restarts, or even an entire cluster shutting down. + +> The dilemma boils down to this simple point: **How do we reconcile stateless infrastructure with stateful services like databases?** + +### The Challenge of Stateful Applications + +When you introduce stateful services, such as databases, into a Kubernetes cluster, you encounter some key challenges: + +Persistent Storage: + +- Stateless apps don’t care about storage or data. In contrast, a database relies heavily on persistent storage to store and retrieve data without losing it. Fortunately, Kubernetes has matured in this area with components such as Persistent Volumes (PVs) and Persistent Volume Claims (PVCs), which allow pods to retain data even if they are recreated. But managing these can still be tricky, especially in scenarios of node failure or during cluster migrations. + +Data Consistency and Durability: + +- Databases are critical to maintaining data consistency and often need to replicate data across nodes to ensure durability and high availability. Any deployment failure or pod misplacement could lead to potential data corruption or downtime. Using stateful sets for databases helps address this, but it requires careful orchestration of failover, recovery, and scaling. + +Disaster Recovery and Backups: + +- When a database is managed independently of Kubernetes (e.g., through a cloud provider), backup and restore processes are simplified. In Kubernetes, organizations need to carefully define backup strategies to avoid data loss during disruptions. + +Performance and Resource Contention: + +- Applications running in a Kubernetes cluster often compete for shared resources (CPU, memory, I/O bandwidth). Large, resource-hungry databases may face performance bottlenecks, especially in clusters designed primarily for serving stateless microservices. Dedicated hosting of databases reduces the risk of congestion and performance hits. + +Scaling: + +- Scaling stateless applications in Kubernetes is trivial, up and down scaling is as simple as updating the replica count of a deployment. Scaling stateful applications, particularly relational databases, is much more complex. Horizontal scaling for databases often requires complex sharding or replication, each with its own intricacies. + +## When Does Database Hosting in Kubernetes Make Sense? + +Kubernetes has developed significantly since its stateless-first beginnings, and modern workloads have shown that stateful applications, including databases, can be successfully hosted in Kubernetes, but it comes with trade-offs that you need to carefully assess based on your use case. + +Here are situations where hosting a database in Kubernetes might make sense: + +### Portability Across Multiple Environments + +If you want consistency across your development, staging, and production environments, Kubernetes offers the advantage of running databases exactly the same way anywhere, whether that's on-premises, in the cloud, or even across hybrid-cloud setups. With the right configurations, you can move your entire application, including its database, as a single, unified package. + +### Cost-Efficiency with Self-Hosting + +Managed cloud databases provide convenience and reliability but come at a cost (often significant when scaling out). Running a database inside your Kubernetes cluster, especially in on-premise environments, can be much more cost-efficient. It allows for better utilization of server capacity, as you’ll be using the same resources to host both the application and database. + +### Advanced Kubernetes Features + +Kubernetes has introduced a variety of features that make running databases smoother: + +- StatefulSets: These provide ordered deployment, scaling, and self-healing of persistent pods used with your database. +- Persistent Volumes & Claims: Enable your pods to store data independently of their lifecycle, ensuring persistent data even if pods die. +- Operators: Kubernetes operators (e.g., for MySQL, PostgreSQL, MongoDB) have become more capable in simplifying the management of complex stateful apps such as databases, handling replication, failover, backups, and more automatically. + +## When Should You Use Managed Databases? + +Though running stateful services and databases in Kubernetes is possible, for many teams, the complexities may outweigh the benefits. In particular, managed databases (e.g., AWS RDS, Google Cloud SQL, Azure Database for PostgreSQL/MySQL, etc.) have remained a more popular choice in many production environments. + +Here are some reasons why you might opt for a managed database instead of self-hosting in Kubernetes: + +### Focused Reliability + +Managed database services are specifically optimized for uptime, with guarantees around availability, fault tolerance, and backups. Cloud providers take care of infrastructure management, including failover and hardware reliability, which is ideal for workloads requiring strong service level agreements (SLAs). + +### Simpler Setup and Maintenance: + +With managed databases, you don’t need to worry about keeping your database software up to date, scaling it as your system grows, ensuring data is backed up, or managing disaster recovery strategies. This level of automation around operational concerns can drastically reduce maintenance overhead. + +### Scalability Without Complexity: + +Cloud-managed databases allow you to scale up (vertically) or replicate databases easier, without having to configure sharding or complex replication setups typically required for self-hosted databases. + +## Conclusion + +The decision on whether to host your database on Kubernetes or use a managed database highly depends on your specific needs. From cost-efficiency to required complexity, there are legitimate use cases for both approaches. + +In general: + +If you don't want to manage the complexities of running and maintaining a database (backups, scaling, failover, etc.), a managed database service might be your best bet. + +However, if you require higher control, flexibility, or have specific portability needs (e.g., managing everything in Kubernetes, running on-premises, or multi-cloud without cloud-vendor lock-in), hosting a database within Kubernetes might make more sense. diff --git a/docusaurus/docs/databases/setup-cloudnative-pg.md b/docusaurus/docs/databases/setup-cloudnative-pg.md new file mode 100644 index 0000000..f7f9839 --- /dev/null +++ b/docusaurus/docs/databases/setup-cloudnative-pg.md @@ -0,0 +1,155 @@ +--- +title: CloudNativePG Operator +--- + +### Install the CloudNativePG Operator + +**Create the [CloudNativePG](https://cloudnative-pg.io) Namespace** +First, create a namespace for CloudNativePG. You don't have to do this, but it's good practice to separate operators into their own namespaces. + +```bash +kubectl create namespace cnpg-system +``` + +**Install the [CloudNativePG](https://cloudnative-pg.io) Operator using kubectl** + +The CloudNativePG team provides a manifest file that’s hosted publicly. You can fetch it using `kubectl` directly from their GitHub repository and apply it to your cluster. + +```bash +# Take the latest version from: https://cloudnative-pg.io/documentation/current/installation_upgrade/ + +kubectl apply --server-side -f \ + https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.24/releases/cnpg-1.24.1.yaml +``` + +This command applies all necessary resources such as CRDs, RBAC permissions, and the operator's Deployment. + +**Verify the Deployment** + +You can check if the CloudNativePG operator pod is running correctly in its namespace: + +```bash +kubectl get pods -n cnpg-system +``` + +You should see output like this: + +```bash +NAME READY STATUS RESTARTS AGE +cloudnative-pg-controller-manager 1/1 Running 0 1m +``` + +At this point, the CloudNativePG operator is installed, and you’re ready to create PostgreSQL clusters. + + +### Deploy a PostgreSQL Cluster + +Now that CloudNativePG is running, let's set up a simple PostgreSQL database cluster. + +**Create a Namespace for Your PostgreSQL Database** + +For better organization, create a namespace for your PostgreSQL cluster if needed: + +```bash +kubectl create namespace postgres-db +``` + +**Create a PostgreSQL Cluster YAML Definition** + +Save the following YAML into a file called `postgres-cluster.yaml`: + +```yaml +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: my-postgres-cluster + namespace: postgres-db +spec: + instances: 3 # Number of database instances + primaryUpdateMethod: switchover # Update strategy for the primary node + storage: + size: 1Gi # Storage size for persistent volumes + storageClass: longhorn +``` + +This YAML creates a PostgreSQL cluster with 3 instances managed by CloudNativePG. Note the `storageClass` is set to `longhorn`, assuming you have Longhorn installed and set up as the default backend. You might want to adjust the `size` value of the storage (`1Gi`) if needed. + +3 replicas of PostgreSQL pods will be created, providing High Availability. + +**Apply the PostgreSQL Cluster YAML** + +Run the following command to deploy the PostgreSQL cluster to your Kubernetes cluster: + +```bash +kubectl apply -f postgres-cluster.yaml +``` + +**Verify Running PostgreSQL Pods** + +After creating the cluster, confirm that the pods for your PostgreSQL cluster are created and running: + +```bash +kubectl get pods -n postgres-db +``` + +You should see something like: + +```bash +NAME READY STATUS RESTARTS AGE +my-postgres-cluster-1 1/1 Running 0 1m +my-postgres-cluster-2 1/1 Running 0 1m +my-postgres-cluster-3 1/1 Running 0 1m +``` + + +**Access PostgreSQL** + +To access PostgreSQL from your local machine, you'll need to port-forward one of the PostgreSQL services. + +First, let's list the services that have been exposed by the CloudNativePG operator: + +```bash +kubectl get svc -n postgres-db +``` + +You’ll see output similar to this: + +```bash +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +my-postgres-cluster-r ClusterIP 10.43.50.146 5432/TCP 22m +my-postgres-cluster-ro ClusterIP 10.43.103.161 5432/TCP 22m +my-postgres-cluster-rw ClusterIP 10.43.242.201 5432/TCP 22m +``` + +- `my-postgres-cluster-r`: Typically routes to the **read** replica. +- `my-postgres-cluster-ro`: Provides a **read-only** interface for **non-primary** nodes. +- `my-postgres-cluster-rw`: Connects to the current **primary** node for **read/write** operations. + +For example, to expose the `rw` service (which connects to the primary node), you can run: + +```bash +kubectl port-forward svc/my-postgres-cluster-rw 5432:5432 -n postgres-db +``` + +Then, on your machine, you can connect to PostgreSQL at `localhost:5432` using any PostgreSQL client or `psql`. + +For example: + +```bash +psql -h localhost -U postgres +``` + +By default, the `postgres` user is created, and you can set custom credentials by defining them in the cluster YAML under `spec.users`. + + +### Optional: Persistent Volumes with Longhorn + +To ensure the PostgreSQL data persists across node restarts, Kubernetes Persistent Volume Claims (PVCs) should use a proper storage class. + +We assumed in the YAML above that you've configured Longhorn as your storage solution: + +```yaml +storageClass: longhorn +``` + +This makes use of Longhorn's reliable storage and ensures that your PostgreSQL data is replicated and safe from node failures. \ No newline at end of file diff --git a/docusaurus/docs/hardware-raspberry-pi-setup/before-we-start.md b/docusaurus/docs/hardware-raspberry-pi-setup/before-we-start.md new file mode 100644 index 0000000..4654cc2 --- /dev/null +++ b/docusaurus/docs/hardware-raspberry-pi-setup/before-we-start.md @@ -0,0 +1,28 @@ +--- +title: Before We Start +--- + + + +Before we start, I want to mention that I've provided an [Ansible](../ansible/automation-with-ansible.md) playbook for most of the setup tasks. While I encourage you to use it, I also recommend doing things manually at first. Experience the process, let frustration build, and allow yourself to feel the annoyance. + +Just as I did, I want you to truly understand why we use certain tools. You'll only internalize this by initially experiencing the challenges and then resolving them by introducing the right tools. + +Once you feel the process, once you get tired of typing the same commands over and over again, you can then use the Ansible playbook to automate the same tasks across the other devices. This is really the only way to truly understand the process and the tools we use, and why we use them. + +While learning all this, I have failed countless times. + +- I had to reset my Raspberry Pi's multiple times. +- I had to re-flash the SD cards multiple times. +- I had to re-install the OS multiple times. +- I had to re-configure the SSH aliases multiple times. +- I had to re-configure the static IP addresses multiple times. +- I had to re-configure the Wi-Fi multiple times. +- I had to re-configure the swap multiple times. +- I had to re-configure the Bluetooth multiple times. +- I had to re-configure the fans multiple times. +- I had to re-configure the everything multiple times. + +All this is part of the learning process. And that very failure is what will lead you to the right tools and understanding. And additionally, it will enforce your learning by making you remember the process and the tools we use. + +The most beautiful part about this entire learning process is that you will be able to learn so many things. And the most fascinating part is that they are all interconnected, and necessary for our cluster to work. It can often feel confusing learning some of these things in isolation, whereas when you learn them in the context of the cluster and this entire setup, it becomes much easier to understand. diff --git a/docusaurus/docs/hardware-raspberry-pi-setup/hardware.mdx b/docusaurus/docs/hardware-raspberry-pi-setup/hardware.mdx new file mode 100644 index 0000000..afd3810 --- /dev/null +++ b/docusaurus/docs/hardware-raspberry-pi-setup/hardware.mdx @@ -0,0 +1,195 @@ +--- +sidebar_position: 3 +title: Hardware Components +--- + +import ImageGallery from "react-image-gallery"; + + + +import Alert from "@site/src/components/Alert/index.tsx"; + + + +While all the hardware is listed below, you can also get the full breakdown by opening the [excel sheet](https://docs.google.com/spreadsheets/d/17sQfTlpE3TCcj2Gz2uwkA2mW_AVzyXSE4EnZitLnAEY/edit?gid=0#gid=0). + +## Affordable Hardware (If you don't want to follow my setup) + +If you want to follow my k3s guide on a tight budget, here's the absolute cheapest Mikrotik combo I could find after some digging: + +**Cheapest Router:** + +- **[MikroTik hAP lite (RB941-2nD)](https://mikrotik.com/product/RB941-2nD-TC):** This is about as cheap as it gets. Prices jump around, but I've seen it listed for roughly 168 DKK (about $24 USD). +- **[MikroTik hEX lite (RB750r2)](https://mikrotik.com/product/RB750r2):** Another solid low-cost pick, usually around $40. You get 5 Ethernet ports, an 850MHz CPU, and 64MB RAM, plenty for a basic home lab. + +**Cheapest Switch:** + +- **[MikroTik RB260GS](https://mikrotik.com/product/RB260GS):** 5-port Gigabit smart switch, also about $40. Bonus: it has an SFP cage if you ever want to mess with fiber. + +So, if you're really trying to keep costs down, grab the hAP lite router and the RB260GS switch. This combo gives you all the routing and switching you need to follow along with my guide, without spending a fortune on higher-end gear. + +As of the Raspberry Pi and/or Mini PC, you can really use anything. For instance, if you have an old Intel laptop laying around, that is perfect. If you have an older version of Raspberry Pi, that is also perfect. No matter your hardware, you can still follow this guide and release your service, or services, by the end of it. + +## Raspberry Pi's + +**[4x Raspberry Pi 4 B 8GB](https://www.raspberrypi.com/products/raspberry-pi-4-model-b/)** + +_Note_: If you're looking to replicate my setup, opt for Raspberry Pi version 4 only if it's available at a significant discount. Otherwise, always go for the latest generation. Also, it's wise to select the model with the maximum RAM since we'll be running multiple services. More memory means better performance. + +## Mini PCs + +**[Lenovo Thinkcentre M900](https://www.ebay.com/sch/i.html?_nkw=Lenovo+thinkcentre+m900&_sacat=0&_from=R40&_trksid=m570.l1313)** - Slightly less powerful than the HP EliteDesk, but still a great choice. It came with 8GB of RAM, which I expanded to 24GB. + +**[HP EliteDesk 800 G3 Mini 65W](https://www.ebay.com/sch/i.html?_nkw=HP+EliteDesk+800+G3+Mini+65W&_sacat=0&_from=R40&_trksid=p2334524.m570.l1313&_odkw=Lenovo+thinkcentre+m900&_osacat=0)** - Expanded the HP EliteDesk with [32gb of RAM](https://www.amazon.de/dp/B07N1YBSPZ?ref=ppx_yo2ov_dt_b_fed_asin_title&th=1). As you'll be browing for those on websites such is e.g. ebay, you'll find them in slightly different configurations, so make sure to check the specs before you buy. This one came with 16GB of RAM, which I moved to Lenovo. + +Mini PCs are a great alternative to Raspberry Pis. They are more powerful, more reliable, and more affordable. They are also more suitable for running a production-grade service. When it comes to idle power consumption, they are often on pair with the Raspberry Pi's, and as I will show you throughout this guide, we can put them to sleep using [Intel Active Management Technology (AMT)](https://en.wikipedia.org/wiki/Intel_Active_Management_Technology) and wake them up using a simple HTTP request. + +You want to get those on the used market, and as shown in the images below, you want to do basic maintenance to them. In my case, I've cleaned the fans and the heat sinks, and I've also added some thermal paste to the CPU and the GPU. Better the cooling, better the performance, and less noise and power consumption. + +I'm personally using [BSFF Thermal paste](https://www.amazon.de/dp/B09NLXSP4S?ref=ppx_yo2ov_dt_b_fed_asin_title&th=1), but you can use whatever you want. To wipe the old thermal paste, you can use a simple paper towel or a microfiber cloth, preferably with some isopropyl alcohol. As you may see in the images below, I'm using [PURIVITA® Isopropanol 99.9%](https://www.amazon.de/-/en/dp/B0C4FKV9HY?ref_=ppx_hzsearch_conn_dt_b_fed_asin_title_1&th=1). Using isopropyl alcohol is a good idea, because it's a good solvent and it will help to remove the old thermal paste. + +As you can see on the images, these mini pc's often need a bit of maintenance, unless you purchase them from professional sellers. Ideally whatsoever, you want to buy them from regular people as they often undersell them out of incompetence. For instance, both of these PC's costed me less then a price of a new Raspberry Pi 4B. + +In mine, thermal paste was dry, and fans needed a bit of cleaning. Besides that, everything else was in a great shape. + +import Image from "@theme/IdealImage"; + + + +## Network + +~~**[Mikrotik RB3011UiAS-RM](https://mikrotik.com/product/RB3011UiAS-RM)**: I went with a MikroTik router because it offers professional-grade features at a price that's hard to beat. It's packed with options you'd usually only find in high-end gear like Cisco, but without the hefty price tag. The advanced routing, solid firewall, and built-in VPN support made it an easy choice for what I needed.~~ + +**[Lenovo M920q as our Router](/docs/networking/mikrotik/lenovo-m920q-roas)**: After extensive research, I decided to replace the [Mikrotik RB3011UiAS-RM](https://mikrotik.com/product/RB3011UiAS-RM) with the much more powerful Lenovo M920q, running MikroTik RouterOS. I wanted to avoid networking bottlenecks when migrating all my services from Hetzner to my home cluster, so I chose a solution that would ensure reliable, high-performance networking. + +**[Mikrotik CRS326-24G-2S+RM](https://mikrotik.com/product/CRS326-24G-2SplusRM)**: SwOS/RouterOS powered 24 port Gigabit Ethernet switch with two SFP+ ports. We need a switch with sufficient ports and SFP+ ports for future expansion, but also to do proper VLANs (network isolation) and QoS (quality of service) for different services. + +**[deleyCON 5 x 0.25 m CAT8.1](https://www.amazon.de/-/en/gp/product/B08WPJVGHR/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&th=1)**: deleyCON CAT 8.1 patch cable network cable as set // 2x RJ45 plug // S/FTP PIMF shielding + +**[CSL CAT.8 Network Cable 40 Gigabit](https://www.amazon.de/-/en/gp/product/B08FCLHTH5/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&th=1)**: CSL CAT.8 Network Cable 40 Gigabit + +**[deleyCON 5 x 0.25 m CAT6 Network Cable Set](https://www.amazon.de/dp/B079FYFZ96?ref=ppx_yo2ov_dt_b_fed_asin_title&th=1)** + +**[deleyCON 10 x 0.5 m CAT6 Network Cable Set](https://www.amazon.de/dp/B0DGKSTM37?ref=ppx_yo2ov_dt_b_fed_asin_title&th=1)** + +## Rack + +**[4U Rack Cabinet](https://www.compumail.dk/en/p/lanberg-rack-gra-993865294)**: A 4U Rack to encapsulate all components cleanly. It provides the benefit of space efficiency and easy access for any hardware changes or additions. + +**[2X Rack Power Supply](https://www.compumail.dk/en/p/lanberg-pdu-09f-0300-bk-stromstodsbeskytter-9-stik-16a-sort-3m-996106700)**: A centralized power supply solution for the entire rack. Ensures consistent and reliable power distribution to all the components. + +**[GeeekPi 1U Rack Kit for Raspberry Pi 4B, 19" 1U Rack Mount](https://www.amazon.de/-/en/gp/product/B0972928CN/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1)**: This 19 inch rack mount kit is specially designed for recording Raspberry Pi 4B boards and supports up to 4 units. + +**[DIGITUS Professional Extendible Shelf for 19-inch cabinets, Black](https://www.amazon.de/dp/B002KTE870?ref=ppx_yo2ov_dt_b_fed_asin_title&th=1)**: This shelf is perfect for the Mini PCs. It's extendible, so you can add more shelves if you need to. + +**[upHere Case Fan 120 mm](https://www.amazon.de/dp/B081SYD24Z?ref=ppx_yo2ov_dt_b_fed_asin_title&th=1)**: High-performance exhaust fans with adjustable speed and metal grill protection, used to efficiently remove heat from the rack and maintain optimal airflow for all components. + +## Storage + +Some of the storage choices were made based on a combination of overall research and a list of [Known Working Adapters](https://jamesachambers.com/best-ssd-storage-adapters-for-raspberry-pi-4-400/). + +**[4X UGREEN Hard Drive Housing](https://www.amazon.de/dp/B07D2BHVBD?ref=ppx_yo2ov_dt_b_fed_asin_title)** + +**[4X Crucial BX500 CT240BX500SSD1 240GB Internal SSD](https://www.amazon.de/dp/B07G3YNLJB?ref=ppx_yo2ov_dt_b_fed_asin_title&th=1)** + +**[2x Verbatim Vi550 S3 SSD](https://www.amazon.de/dp/B07LGKQLT5?ref=ppx_yo2ov_dt_b_fed_asin_title&th=1)** + +**[2x JSAUX USB 3.0 to SATA Adapter](https://www.amazon.de/dp/B086W944YT?ref=ppx_yo2ov_dt_b_fed_asin_title)** + +_During my learning journey with Raspberry Pi, I realized that purchasing microSD cards was a mistake. They perform significantly worse than solid-state drives (SSDs), are prone to random failures, and unfortunately, these microSD cards can be as expensive, or even more so, than buying SSDs. E.g. in comparison, [Verbatim Vi550 S3 SSD](https://www.amazon.de/dp/B07LGKQLT5?ref=ppx_yo2ov_dt_b_fed_asin_title) costs the same as [SanDisk Extreme microSDXC](https://www.amazon.de/dp/B09X7BK27V?ref=ppx_yo2ov_dt_b_fed_asin_title&th=1). In many instances in fact, microSD card is actually more expensive._ + +~~**[SanDisk Extreme microSDHC 3 Rescue Pro Deluxe Memory Card, Red/Gold 64GB](https://www.amazon.de/-/en/gp/product/B07FCMBLV6/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1)**: Up to 160MB/s Read speed and 60 MB/s. Write speed for fast recording and transferring~~ + +~~**[Vanja SD/Micro SD Card Reader](https://www.amazon.de/-/en/gp/product/B00W02VHM6/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1)**: Micro USB OTG Adapter and USB 2.0 Memory Card Reader~~ + +## Why These Choices? + +**Mobility**: The 4U Rack allows me to move the entire setup easily, making it convenient for different scenarios, from a home office to a small business environment + +**Professional-Grade Networking**: The Mikrotik router provides a rich feature set generally found in enterprise-grade hardware, offering me a sandbox to experiment with advanced networking configurations + +**Affordability**: This setup provides a balance between cost and performance, giving me a powerful Kubernetes cluster without breaking the bank diff --git a/docusaurus/docs/hardware-raspberry-pi-setup/mini-pcs-setup.md b/docusaurus/docs/hardware-raspberry-pi-setup/mini-pcs-setup.md new file mode 100644 index 0000000..62a96b4 --- /dev/null +++ b/docusaurus/docs/hardware-raspberry-pi-setup/mini-pcs-setup.md @@ -0,0 +1,29 @@ +--- +title: Mini PCs Setup +--- + +Setting up mini PCs is a bit different than setting up Raspberry Pi's. That is mainly because you might not have the same hardware available. Due to this, what I write here might not match your setup completely, but regardless of the hardware, worst case, it should be nearly identical. + +## Setup + +Typically, when thinking about PC's, we often think about the computer that one uses to surf the web, watch videos, play games, and do other things. Due to that, the way we setup the PC is a bit different. We focus on e.g. maximum performance, while paying the price of high power consumption, and high heat. + +In the context of servers, especially our home "mini data center", we want to focus on low power consumption, low noise, and low heat. All of these requirements (or goals) equal low cost. Since our computers will be running 24/7, we have to do all we can to reduce the cost of running them. Additionally, we want to extend the life of the hardware, and reduce the amount of maintenance we have to do. + +With that little "preface", let's get started. Whenever you get one of the used mini PCs (as I showed under [Hardware](../hardware-raspberry-pi-setup/hardware.mdx#mini-pcs)), you'll have to do some maintenance: + +- Clean the hardware, e.g. dust, +- Clean the computer +- Install a new OS +- Install a new OS + +### BIOS + +### Ubuntu Server + +For our Mini PCs, we'll be using [Ubuntu Server](https://ubuntu.com/download/server) as the OS. More specifically, throughout the setup process, we'll select the minimal (minimized) version of the OS. + +> Ubuntu Server Minimal is a version of Ubuntu Server with a reduced set of pre-installed packages, designed for cloud deployments and situations where a smaller footprint and faster boot times are desired. It provides the bare minimum to get to the command line, making it ideal for users who know what they're doing and prefer to install only necessary software. + +If you've never setup Ubuntu Server before, I recommend you to read the [How to install Ubuntu Server 22.04 +](https://systemwatchers.com/index.php/blog/ubuntu-server/how-to-install-ubuntu-server-22-04/). diff --git a/docs/raspberry-pi-setup.md b/docusaurus/docs/hardware-raspberry-pi-setup/raspberry-pi-setup.md similarity index 61% rename from docs/raspberry-pi-setup.md rename to docusaurus/docs/hardware-raspberry-pi-setup/raspberry-pi-setup.md index 2b53cf6..8fed003 100644 --- a/docs/raspberry-pi-setup.md +++ b/docusaurus/docs/hardware-raspberry-pi-setup/raspberry-pi-setup.md @@ -1,21 +1,30 @@ +--- +title: Raspberry Pi Setup +--- + # Raspberry Pi's Setup -For most steps, an [Ansible playbook](./ansible/playbooks/) is available. However, I strongly recommend that you initially set up the first Raspberry Pi manually. This hands-on approach will help you understand each step more deeply and gain practical experience. Once you've completed the manual setup, you can then use the [Ansible playbook](./ansible/playbooks/) to automate the same tasks across the other devices. +### Install the OS Using Pi Imager -#### Flash SD Cards with Raspberry Pi OS Using Pi Imager - Open [Raspberry Pi Imager](https://www.raspberrypi.com/software/). - - Choose the 'OS' you want to install from the list. The tool will download the selected OS image for you. - - Insert your SD card and select it in the 'Storage' section. + - Choose the `Raspberry Pi OS (other)` > `Raspberry Pi OS Lite (64-bit)` + - The tool will download the selected OS image for you. + - Plug in your SSD and select it in the 'Storage' section. + - - _Note_: If you're just unpacking brand new SSDs, there's a good chance you'll need to use a Disk Management tool on your operating system to initialize and allocate the available space. Otherwise, they might not appear in the Pi Imager. - Before writing, click on the cog icon for advanced settings. - Set the hostname to your desired value, e.g., `RP1`. - Enable SSH and select the "allow public-key authorization only" option. - Click on 'Write' to begin the flashing process. - -#### Initial Boot and Setup -- Insert the flashed SD card into the Raspberry Pi and power it on. + +### Initial Boot and Setup + +- Insert the flashed SSD into the USB 3 port on your Raspberry Pi and power it on - On the first boot, ssh into the Pi to perform initial configuration - -#### Update and Upgrade - ([Ansible Playbook](./ansible/playbooks/apt-update.yml)) + +### Update and Upgrade + +[Ansible Playbook](/ansible/playbooks/apt-update.yml) + - Run the following commands to update the package list and upgrade the installed packages: ```bash @@ -23,7 +32,45 @@ sudo apt update sudo apt upgrade ``` -#### Disable Wi-Fi ([Ansible Playbook](./ansible/playbooks/disable-wifi.yml)) +### Enable Memory Cgroups + +[Ansible Playbook](/ansible/playbooks/enable-memory-groups.yml) + +Before installing K3s, it's essential to enable memory cgroups on the Raspberry Pi for container resource management. + +For the Ubuntu Server, e.g. our mini-pcs, this is already enabled by default. + +[Control Groups (Cgroups)](https://en.wikipedia.org/wiki/Cgroups) are a Linux kernel feature that allows you to allocate resources such as CPU time, system memory, and more among user-defined groups of tasks (processes). + +K3s requires memory cgroups to be enabled to better manage and restrict the resources that each container can use. This is crucial in a multi-container environment where resource allocation needs to be as efficient as possible. + +**Simple Analogy**: Imagine you live in a house with multiple people (processes), and there are limited resources like time (CPU), space (memory), and tools (I/O). Without a system in place, one person might hog the vacuum cleaner all day (CPU time), while someone else fills the fridge with their stuff (memory). With a `"chore schedule"` (cgroups), you ensure everyone gets an allocated time with the vacuum cleaner, some space in the fridge, and so on. This schedule ensures that everyone can do their chores without stepping on each other's toes, much like how cgroups allocate system resources to multiple processes. + +Edit the `/boot/firmware/cmdline.txt` file on your Raspberry Pi. + +```bash +sudo vi /boot/firmware/cmdline.txt +``` + +Append the following to enable memory cgroups. + +```text +cgroup_memory=1 cgroup_enable=memory +``` + +Save the file and reboot your Raspberry Pi. + +```bash +sudo reboot +``` + +## Optimize our Pi's + +Since our Raspberry Pis are nodes in our cluster and will consistently be used when plugged into our Ethernet switch or router, we can optimize them by disabling unnecessary components. This reduces the number of services running on them, naturally lowering CPU and memory usage. More importantly, it reduces power consumption, leading to lower electricity bills. + +### Disable Wi-Fi + +[Ansible Playbook](/ansible/playbooks/disable-wifi.yml) ```sh sudo vi /etc/wpa_supplicant/wpa_supplicant.conf @@ -68,7 +115,11 @@ Reboot your Raspberry Pi: sudo reboot ``` -#### Disable Swap ([Ansible Playbook](./ansible/playbooks/disable-swap.yml)) +### Disable Swap + +[Ansible Playbook](/ansible/playbooks/disable-swap.yml) + +[Swap Memory](/terminology.md#swap-memory) Disabling swap in a K3s cluster is crucial because Kubernetes relies on precise memory management to allocate resources, schedule workloads, and handle potential memory limits. When swap is enabled, it introduces unpredictability in how memory is used. The Linux kernel may move inactive memory to disk (swap), giving the impression that there is available memory when, in reality, the node might be under significant memory pressure. This can lead to performance degradation for applications, as accessing memory from the swap space (on disk) is significantly slower than accessing it from RAM. In addition, Kubernetes, by default, expects swap to be off and prevents the kubelet from running unless explicitly overridden, as swap complicates memory monitoring and scheduling. @@ -83,7 +134,7 @@ sudo swapoff -a This command disables the swap immediately, but it will be re-enabled after a reboot unless further steps are taken. -##### Modify `/etc/dphys-swapfile` to Disable Swap Permanently +Modify `/etc/dphys-swapfile` to Disable Swap Permanently Open the swap configuration file `/etc/dphys-swapfile` in a text editor: @@ -100,33 +151,24 @@ CONF_SWAPSIZE=0 Save (Ctrl+O in `nano`) and exit the editor (Ctrl+X in `nano`). -##### Remove the Existing Swap File - Run the following command to remove the current swap file (`/var/swap`): ```bash sudo rm /var/swap ``` -##### Stop the `dphys-swapfile` service immediately - Stop the `dphys-swapfile` service, which manages swap: + ```bash sudo systemctl stop dphys-swapfile ``` -##### Disable the `dphys-swapfile` service to prevent it from running on boot - Prevent the `dphys-swapfile` service from starting during system boot by disabling it: ```bash sudo systemctl disable dphys-swapfile ``` ---- - -##### Verify swap is turned off - Run the following command to verify that swap is no longer in use: ```bash @@ -142,10 +184,6 @@ Mem: 2003 322 1681 18 12 129 Swap: 0 0 0 ``` ---- - -##### Reboot the system - Finally, reboot the system in order to apply all changes fully and ensure swap remains permanently disabled: ```bash @@ -154,20 +192,18 @@ sudo reboot After the system comes back online, run `free -m` again to confirm that swap is still disabled. +### Disable Bluetooth -#### Disable Bluetooth +[Ansible Playbook](/ansible/playbooks/disable-bluetooth.yml) When using Raspberry Pi devices in a Kubernetes-based environment like K3s, any unused hardware features, such as Bluetooth, can consume system resources or introduce potential security risks. Disabling Bluetooth on each Raspberry Pi optimizes performance by reducing background services and freeing up resources like CPU and memory. Additionally, by disabling an unused service, you reduce the attack surface of your Raspberry Pi-based K3s cluster, providing a more secure and streamlined operating environment. - -##### Stop and disable the bluetooth service - **Stop the Bluetooth service** that might be currently running on your Raspberry Pi: ```bash sudo systemctl stop bluetooth ``` - + **Disable the service** so it doesn't start automatically during system boot: ```bash @@ -176,17 +212,15 @@ sudo systemctl disable bluetooth This ensures that the Bluetooth service is not running in the background, conserving system resources. -##### Blacklist bluetooth kernel modules - To prevent the operating system from loading Bluetooth modules at boot time, you'll need to blacklist specific modules. -**Open the blacklist configuration file for editing (or create it)**: +Open the blacklist configuration file for editing (or create it) ```bash sudo nano /etc/modprobe.d/raspi-blacklist.conf ``` -**Add the following lines to disable Bluetooth modules**: +Add the following lines to disable Bluetooth modules: ```bash blacklist btbcm # Disables Broadcom Bluetooth module @@ -197,17 +231,15 @@ blacklist hci_uart # Disables hci_uart module specific to Raspberry Pi Bluetoo By blacklisting these modules, they won’t be loaded during boot, effectively preventing Bluetooth from running. -##### Disable bluetooth in the system configuration - Bluetooth can be disabled directly at the device level by editing specific Raspberry Pi system configurations. -**Open the boot configuration file for editing**: +Open the boot configuration file for editing: ```bash sudo nano /boot/config.txt ``` -**Add the following line to disable Bluetooth**: +Add the following line to disable Bluetooth: ```bash dtoverlay=disable-bt @@ -219,44 +251,45 @@ Ensure no Bluetooth device can wake up your Raspberry Pi by ensuring the line is This command ensures that the Raspberry Pi doesn’t enable Bluetooth at boot by making system-wide firmware adjustments. -**Reboot the Raspberry Pi** - To fully apply the changes (stopping the service, blacklisting modules, and adjusting system configuration), it’s recommended to reboot the system. -**Reboot the Raspberry Pi**: - ```bash sudo reboot ``` After rebooting, you can verify that Bluetooth has been disabled by checking the status of the service: - + ```bash sudo systemctl status bluetooth ``` It should indicate that the Bluetooth service is inactive or dead. +### Fan Control + +Unfortunately, due to the limitations of the [GeeekPi 1U Rack Kit for Raspberry Pi 4B, 19" 1U Rack Mount](https://www.amazon.de/-/en/gp/product/B0972928CN/ref=ppx_yo_dt_b_search_asin_title?ie=UTF8&psc=1), I couldn't optimize the fans for each Raspberry Pi. The fans included with this kit lack [PWM](https://en.wikipedia.org/wiki/Pulse-width_modulation) control and only come with a 2-pin cable. If you're using different fans that you can control, I highly recommend setting them to remain off below certain temperature thresholds. This will not only make your setup completely silent but also reduce power consumption. -#### Assign Static IP Addresses +## Assign Static IP Addresses -##### MikroTik Router +### MikroTik Router - Open the MikroTik Web UI and navigate to `IP > DHCP Server`. - Locate the `Leases` tab and identify the MAC addresses of your Raspberry Pi units. -- Click on the entry for each Raspberry Pi and change it from "dynamic" to "static". +- Click on the entry for each Raspberry Pi and change it from `Dynamic` to `Static`. + +If you're using a different router, the process should be similar. The only possible limitation is if you're using a consumer-grade router that doesn't offer these features. In that case, you'll need to set up a DHCP server. ## Set SSH Aliases Once you have assigned static IPs on your router, you can simplify the SSH process by setting up SSH aliases. Here's how to do it: -1. **Open the SSH config file on your local machine:** +Open the SSH config file on your local machine: ```bash vi ~/.ssh/config ``` - -2. **Add the following entries for each Raspberry Pi:** + +Add the following entries for each Raspberry Pi: ```bash Host rp1 @@ -276,11 +309,7 @@ Host rp4 User YOUR_USERNAME ``` -Replace ``, ``, ``, and `` with the actual static IP addresses of your Raspberry Pis. - -3. **Save and Close the File** - -5. **Test Your Aliases** +Replace ``, ``, ``, and `` with the actual static IP addresses of your Raspberry Pis. Save and close the file. You should now be able to SSH into each Raspberry Pi using the alias: @@ -288,4 +317,4 @@ You should now be able to SSH into each Raspberry Pi using the alias: ssh rp1 ``` -That's it! You've set up SSH aliases for your Raspberry Pi cluster. \ No newline at end of file +That's it! You've set up SSH aliases for your Raspberry Pi cluster. diff --git a/docusaurus/docs/kubernetes/anatomy-of-kubectl-command.mdx b/docusaurus/docs/kubernetes/anatomy-of-kubectl-command.mdx new file mode 100644 index 0000000..d7a071c --- /dev/null +++ b/docusaurus/docs/kubernetes/anatomy-of-kubectl-command.mdx @@ -0,0 +1,46 @@ +--- +title: Anatomy of a `kubectl` Command +--- + +[kubectl](https://kubernetes.io/docs/reference/kubectl/) is the command-line interface for Kubernetes. It allows us to run commands against Kubernetes clusters. It is the most important command in Kubernetes, and we'll use it a lot. + +I can't emphasize enough how important it is to write these commands manually until we internalize them. And, very importantly, make sure to set up [kubectl completion](https://kubernetes.io/docs/reference/kubectl/generated/kubectl_completion/), it will speed things up a lot. + +
+ +
+ $ + kubectl + + get + + pod + + my-pod + + -n my-namespace +
+ +
+ +
+

Command

+

Specifies the operation that you want to perform on one or more resources, for example create, get, describe, delete.

+
+ +
+

Resource Type

+

Specifies the __[resource type](https://kubernetes.io/docs/reference/kubectl/#resource-types)__. Resource types are case-insensitive and you can specify the singular, plural, or abbreviated forms.

+
+ +
+

Resource Name

+

Specifies the name of the resource. Names are case-sensitive. If the name is omitted, details for all resources are displayed, for example kubectl get pods.

+
+ +
+

Flags

+

Specifies optional flags. For example, you can use the -s or --server flags to specify the address and port of the Kubernetes API server.

+
+
+
diff --git a/docusaurus/docs/kubernetes/anatomy-of-kubernetes-yaml.mdx b/docusaurus/docs/kubernetes/anatomy-of-kubernetes-yaml.mdx new file mode 100644 index 0000000..2665b9a --- /dev/null +++ b/docusaurus/docs/kubernetes/anatomy-of-kubernetes-yaml.mdx @@ -0,0 +1,38 @@ +--- +title: Anatomy of Kubernetes YAML +--- + +Let's take a look at a couple of YAML files that our infrastructure will typically be composed of. Note that we don't have to fully understand what they do or instantly know how to write them, but by the time we finish this lesson, we should have a good idea of how we'll be defining our infrastructure. + + + +import Alert from "@site/src/components/Alert/index.tsx"; + + + +import KubernetesYAMLAnatomy from "@site/src/components/KubernetesYAMLAnatomy/index.tsx"; + +Understanding Kubernetes YAML files is essential for working with Kubernetes. These declarative files define nearly every aspect of our infrastructure: how many instances of an application to run, what type of storage to use, access controls, networking, and more. + +From now on, we should approach these concepts in a straightforward way, because that's really all there is to it. If we focus too much on terminology and fluff, it's easy to feel overwhelmed. + +> It's important to think about each of these "components" naturally. For example: we want to deploy an application with an API and supporting services (x, y, z). We might need 10GB of storage for our Postgres database, and we want Postgres to be accessible only by our API. Finally, we need to expose our API to the internet. + +Each major configuration area is typically represented by its own YAML file. For example: + +- **Ingress**: Describes how a service is accessed via HTTP and HTTPS. +- **Deployment**: Specifies how many instances (replicas) of an application to run, and resource allocations like RAM and CPU. +- **Service**: Defines networking and how other components or users can access our application. + +A typical Kubernetes YAML file is structured into several key sections ([Objects In Kubernetes](https://kubernetes.io/docs/concepts/overview/working-with-objects/)): + +- **apiVersion**: Specifies the Kubernetes API version to use for this object. +- **kind**: Indicates the type of Kubernetes object (e.g., Deployment, Service, Ingress). +- **metadata**: Provides identifying information such as the object's name, namespace, and labels. +- **spec**: Contains the desired state and configuration details for the object. + +Each section plays a specific role in telling Kubernetes what we want to run and how we want it managed. Mastering these files is key to effectively deploying and managing applications in Kubernetes. diff --git a/docusaurus/docs/kubernetes/common-kubernetes-commands.md b/docusaurus/docs/kubernetes/common-kubernetes-commands.md new file mode 100644 index 0000000..6f94c7d --- /dev/null +++ b/docusaurus/docs/kubernetes/common-kubernetes-commands.md @@ -0,0 +1,309 @@ +# Kubernetes Command Cheatsheet + +### Cluster Information and Health + +1. **Check cluster components (control plane availability):** +```bash +kubectl get componentstatuses +``` + +2. **Get general cluster information:** +```bash +kubectl cluster-info +``` + +3. **List all nodes in the cluster (health/status):** +```bash +kubectl get nodes +``` + +4. **Get detailed information about a node:** +```bash +kubectl describe node +``` + +5. **View the current Kubernetes version running:** +```bash +kubectl version --short +``` + +6. **Check any existing cluster issues or warning events globally:** +```bash +kubectl get events --all-namespaces --sort-by='.metadata.creationTimestamp' +``` + + +### Workload / Pod Management + +7. **View all pods across all namespaces:** +```bash +kubectl get pods --all-namespaces +``` + +8. **List the pods in a specific namespace (e.g., `default`, `longhorn-system`):** +```bash +kubectl get pods -n +``` + +9. **Get detailed information for a specific pod:** +```bash +kubectl describe pod -n +``` + +10. **Delete a pod (restarts the pod, useful for troubleshooting):** +```bash +kubectl delete pod -n +``` + +11. **Create or apply resources from a YAML file:** +```bash +kubectl apply -f .yaml +``` + +12. **View YAML/JSON configuration dump of a resource:** +- **Output YAML:** + ```bash + kubectl get -o yaml + ``` + +- **Output JSON:** + ```bash + kubectl get -o json + ``` + +13. **Get logs from a pod:** +```bash +kubectl logs -n +``` + +14. **Stream continuous logs from a pod:** +```bash +kubectl logs -f -n +``` + +15. **Get logs for a specific container in a multi-container pod:** +```bash +kubectl logs -c -n +``` + +16. **Launch a debug pod for troubleshooting (basic busybox container in interactive terminal):** +```bash +kubectl run debug --image=busybox -it --rm -- /bin/sh +``` + +17. **Forcefully delete a pod (if stuck in terminating or other strange states):** +```bash +kubectl delete pod --grace-period=0 --force -n +``` + + +### Service & Endpoint Management + +18. **List all services in a namespace:** +```bash +kubectl get svc -n +``` + +19. **Get detailed information about a service:** +```bash +kubectl describe svc -n +``` + +20. **Forward a local port to a pod (e.g., for local access to service, like database):** +```bash +kubectl port-forward : -n +``` + +21. **Test if a service is functioning by listing endpoints:** +```bash +kubectl get endpoints -n +``` + + +### Storage Management (Longhorn) + +22. **List Longhorn volumes:** +```bash +kubectl get volumes -n longhorn-system +``` + +23. **Describe a Longhorn volume:** +```bash +kubectl describe -n longhorn-system +``` + +24. **List PersistentVolumeClaims (PVCs) in a namespace:** +```bash +kubectl get pvc -n +``` + +25. **Delete a PersistentVolumeClaim (PVC) carefully:** +```bash +kubectl delete pvc -n +``` + +26. **Check the status of Longhorn-csi or other stateful sets:** +```bash +kubectl get statefulsets -n longhorn-system +``` + +27. **List all StorageClasses (to verify Longhorn's StorageClasses):** +```bash +kubectl get storageclass +``` + + +### Namespace Management + +28. **List all namespaces:** +```bash +kubectl get namespaces +``` + +29. **Switch context to a different namespace:** +```bash +kubectl config set-context --current --namespace= +``` + +30. **Create a new namespace:** +```bash +kubectl create namespace +``` + +31. **Delete a namespace (use caution):** +```bash +kubectl delete namespace +``` + + +### PostgreSQL Management (example provider) + +32. **List PostgreSQL-related resources (assuming you have CRDs or a PostgreSQL operator installed):** +```bash +kubectl get postgresql -n +``` + +33. **Describe a PostgreSQL instance:** +```bash +kubectl describe postgresql -n +``` + +34. **Connect to the PostgreSQL pod for database debugging:** +```bash +kubectl exec -it -n -- psql -U postgres +``` + + +### Resource & Utilization Monitoring + +35. **View resource usage (CPU/Memory) for nodes and pods (requires metrics-server):** +- **For nodes:** + ```bash + kubectl top nodes + ``` + +- **For pods (in a specific namespace):** + ```bash + kubectl top pods -n + ``` + +36. **Check events for troubleshooting issues in a namespace:** +```bash +kubectl get events -n +``` + +37. **Get details about a Deployment:** +```bash +kubectl describe deployment -n +``` + + +### Scale Deployments + +38. **Scale up/down the number of replicas in a Deployment:** +```bash +kubectl scale deployment --replicas= -n +``` + +39. **Autoscale a Deployment based on CPU usage:** +```bash +kubectl autoscale deployment --cpu-percent= --min= --max= -n +``` + + +### Debugging & Troubleshooting + +40. **Check recent events sorted by timestamp to diagnose issues:** +```bash +kubectl get events --sort-by='.metadata.creationTimestamp' -n +``` + +41. **Open a shell session inside a running container:** +```bash +kubectl exec -it -n -- /bin/bash +``` + +42. **Run one-off commands in a container (e.g., to run a curl command):** +```bash +kubectl exec -it -n -- curl +``` + +43. **Get the history of resource changes for a deployment (e.g., when scaling happens):** +```bash +kubectl rollout history deployment -n +``` + + +### Service Account Management (API & Permissions) + +44. **List all service accounts in a namespace:** +```bash +kubectl get serviceaccounts -n +``` + +45. **Get details about a specific service account:** +```bash +kubectl describe serviceaccount -n +``` + +46. **Create a service account:** +```bash +kubectl create serviceaccount -n +``` + +47. **Delete a service account:** +```bash +kubectl delete serviceaccount -n +``` + + +### Configuration Management + +48. **View all ConfigMaps in a namespace:** +```bash +kubectl get configmap -n +``` + +49. **Describe a specific ConfigMap:** +```bash +kubectl describe configmap -n +``` + +50. **List Secrets (API keys, credentials, etc.) in a namespace:** +```bash +kubectl get secrets -n +``` + +51. **Decode a base64-encoded Secret to reveal its true content:** +```bash +kubectl get secret -n -o jsonpath="{.data.}" | base64 --decode +``` + +--- + +### Additional Tips: +- **Backup critical configurations:** Before making any destructive operations like `delete`, always back up your resource configurations or use GitOps processes. +- **Use dry-run mode for testing deletions**: Use `--dry-run=client` to simulate applying or deleting things without actually making changes. + +Tools like **`kubectl krew`** can extend the functionality of `kubectl` and provide additional `kubectl` plugins for advanced features. + diff --git a/docs/getting-started-with-kubernetes.md b/docusaurus/docs/kubernetes/getting-started-with-kubernetes.md similarity index 71% rename from docs/getting-started-with-kubernetes.md rename to docusaurus/docs/kubernetes/getting-started-with-kubernetes.md index 14df13b..57b0d82 100644 --- a/docs/getting-started-with-kubernetes.md +++ b/docusaurus/docs/kubernetes/getting-started-with-kubernetes.md @@ -1,8 +1,17 @@ -# Gettting Started with Kubernetes +--- +title: Practice Makes Perfect 🥷🏻🚀 +--- + + +At this point, our Raspberry Pis should be configured, and we should have a basic understanding of Kubernetes. Most importantly, we know why we're learning all of this. Now, let's move into the practical side of things by using [`kubectl`](https://kubernetes.io/docs/reference/kubectl/) (pronounced "kube-control"). + +Until we start using tools like [`helm`](https://helm.sh/), [`kubectl`](https://kubernetes.io/docs/reference/kubectl/) will be our best friend. As I've mentioned before in previous sections or during my [live streams](https://www.twitch.tv/programmer_network), we should add tools and abstractions only **once** the work becomes repetitive and frustrating. + +In this case, we aren't going to use [`helm`](https://helm.sh/) until we've learned how to use [`kubectl`](https://kubernetes.io/docs/reference/kubectl/) thoroughly and memorized the key commands. Mastering the basics will help us build a strong foundation and make it clear when it's time to introduce new abstractions. ## Namespace Setup -1. **Create a new Kubernetes Namespace**: +**Create a new Kubernetes Namespace**: **Command:** ```bash @@ -27,7 +36,7 @@ kubectl apply -f namespace.yaml ## Basic Deployment -2. **Deploy a Simple App**: +**Deploy a Simple App**: **Command:** @@ -75,7 +84,7 @@ kubectl apply -f deployment.yaml ## Service Exposure -3. **Expose the Deployment**: +**Expose the Deployment**: **Command:** @@ -115,7 +124,7 @@ kubectl apply -f service.yaml ## Verify Deployment -4. **Verify Using Port-Forward**: +**Verify Using Port-Forward**: ```bash # This is only needed if service type is ClusterIP @@ -137,25 +146,23 @@ kubectl delete -f .yaml **Warning**: Deleting the namespace will remove all resources in that namespace. Ensure you're okay with that before running the command. ---- - ## Exercises ### Exercise 1: Create and Examine a Pod -1. Create a simple Pod running Nginx. +Create a simple Pod running Nginx. ```bash kubectl run nginx-pod --image=nginx --restart=Never ``` -2. Examine the Pod. +Examine the Pod. ```bash kubectl describe pod nginx-pod ``` -3. Delete the Pod. +Delete the Pod. ```bash kubectl delete pod nginx-pod @@ -163,23 +170,21 @@ kubectl delete pod nginx-pod **Objective**: Familiarize yourself with the Pod lifecycle. ---- - ### Exercise 2: Create a Deployment -1. Create a Deployment for a simple Node.js app (You can use a Docker image like `node:20`). +Create a Deployment for a simple Node.js app (You can use a Docker image like `node:20`). ```bash kubectl create deployment node-app --image=node:20 ``` -2. Scale the Deployment. +Scale the Deployment. ```bash kubectl scale deployment node-app --replicas=3 ``` -3. Rollback the Deployment. +Rollback the Deployment. ```bash kubectl rollout undo deployment node-app @@ -187,17 +192,15 @@ kubectl rollout undo deployment node-app **Objective**: Learn how to manage application instances declaratively using Deployments. ---- - ### Exercise 3: Expose the Deployment as a Service -1. Expose the Deployment as a ClusterIP service. +Expose the Deployment as a ClusterIP service. ```bash kubectl expose deployment node-app --type=ClusterIP --port=80 ``` -2. Access the service within the cluster. +Access the service within the cluster. ```bash kubectl get svc @@ -211,11 +214,9 @@ kubectl port-forward svc/node-app 8080:80 **Objective**: Learn how Services allow you to abstract and access your Pods. ---- - ### Exercise 4: Cleanup -1. Remove the service and deployment. +Remove the service and deployment. ```bash kubectl delete svc node-app diff --git a/docusaurus/docs/kubernetes/k3s-backup.md b/docusaurus/docs/kubernetes/k3s-backup.md new file mode 100644 index 0000000..81d2521 --- /dev/null +++ b/docusaurus/docs/kubernetes/k3s-backup.md @@ -0,0 +1,94 @@ +--- +title: K3S Backup +--- + +## Backup and Restore for Single-Node K3s Cluster Using SQLite + +[Ansible Playbook](/ansible/playbooks/backup-k3s.yml) + +When working with a single-node K3s cluster, the default datastore is [SQLite](https://docs.k3s.io/datastore/backup-restore#backup-and-restore-with-sqlite), which is a lightweight, file-based database. Unfortunately, K3s does not provide specialized tools for backing up SQLite in single-node configurations. + +In contrast, if you're running a multi-node (High Availability) cluster using etcd as the datastore, K3s offers a convenient [`k3s etcd-snapshot`](https://docs.k3s.io/cli/etcd-snapshot) command for backups and recovery. However, this tool is not applicable for single-node clusters where SQLite is the default datastore. + +### Why Manually Back Up? + +SQLite backups in K3s require manual steps because: + +* SQLite is a simple, file-based database, so backing it up is as easy as copying key directories. +* K3s doesn't provide automatic backup utilities for this. + +The good news is that manual backups are not too complicated. In this guide, we'll walk you through how to perform a manual backup and restore of K3s data using simple tools. + +## Backup and Restore for Single-Node K3s (SQLite) + +### Backup Process: + +1. **Identify Critical Files**: + +- SQLite Database: `/var/lib/rancher/k3s/server/db/` +- TLS Certificates: `/var/lib/rancher/k3s/server/tls/` +- Join Token: `/var/lib/rancher/k3s/server/token` + +2. Create Backup Folder on Local Machine: + +```bash +mkdir -p ~/k3s-backups/ +``` + +3. Copy Files from K3s Server to Local Machine: + +```bash +scp -r user@master_node:/var/lib/rancher/k3s/server/db ~/k3s-backups/ +scp -r user@master_node:/var/lib/rancher/k3s/server/tls ~/k3s-backups/ +scp user@master_node:/var/lib/rancher/k3s/server/token ~/k3s-backups/ +``` + +4. (Optional) Compress the Backup: + +```bash +tar -czf ~/k3s-backups/k3s-backup-$(date +%F_%T).tar.gz -C ~/k3s-backups db tls token +``` + +### Restore Process: + +1. Stop K3s: + +```bash +sudo systemctl stop k3s +``` + +2. Upload Backup from Local Machine to K3s Node: + +```bash +scp -r ~/k3s-backups/db user@master_node:/var/lib/rancher/k3s/server/ +scp -r ~/k3s-backups/tls user@master_node:/var/lib/rancher/k3s/server/ +scp ~/k3s-backups/token user@master_node:/var/lib/rancher/k3s/server/ +``` + +3. Ensure Correct Permissions: + +```bash +sudo chown -R root:root /var/lib/rancher/k3s/server/db /var/lib/rancher/k3s/server/tls +sudo chown root:root /var/lib/rancher/k3s/server/token +sudo chmod 0600 /var/lib/rancher/k3s/server/token +``` + +4. Start K3s: + +```bash +sudo systemctl start k3s +``` + +5. Verify Cluster Health: + +```bash +kubectl get nodes +kubectl get pods --all-namespaces +``` + +### Summary: + +- Backup: Copy `db/`, `tls/`, and `token` from `/var/lib/rancher/k3s/server/` to your local machine. + +- Restore: Stop K3s, upload those files back to the node, ensure permissions, and start K3s again. + diff --git a/docusaurus/docs/kubernetes/k3s-maintenance.md b/docusaurus/docs/kubernetes/k3s-maintenance.md new file mode 100644 index 0000000..fc67f25 --- /dev/null +++ b/docusaurus/docs/kubernetes/k3s-maintenance.md @@ -0,0 +1,85 @@ +--- +title: K3S Maintenance +--- + +### Steps for Updating K3S + +Updating K3S involves safely taking each node offline (one at a time), performing the update, then bringing the node back into the cluster. + +Before doing any updates, **backup your data**. Ensure you have backups of your K3S server data and important configuration files. This is especially crucial if something goes wrong during the update and you need to restore to a previous state. + +### Draining the Node +When performing maintenance (such as updating K3S), it’s important to **"drain"** the node to protect your workloads and avoid interruptions. + +#### What Does "Draining" a Node Mean? +- **Draining** safely evicts all non-essential pods from the node, allowing Kubernetes to reschedule them on other nodes. +- It also makes the node "unschedulable," ensuring no new pods can be assigned to the node while it’s offline. + +#### What does "evicting" a Pod Mean? + +- In Kubernetes, "evicts" refers to the process of safely terminating Pods on a node, typically to free up resources or for maintenance, allowing them to be rescheduled on other nodes. + +#### How to Drain a Node: +To drain a node, run the following command replacing `` with the name of the node you want to update: + +```bash +kubectl drain --ignore-daemonsets --delete-emptydir-data +``` + +**Explanation of Command Options:** +- `--ignore-daemonsets`: Prevents Kubernetes from evicting system-critical pods managed by DaemonSets (these won't be touched). +- `--delete-emptydir-data`: Deletes any storage associated with `EmptyDir` volumes (used for temporary data in pods). + +### Stopping the K3S Service + +To update K3S, we first need to stop the running K3S service on the Raspberry Pi: + +```bash +sudo systemctl stop k3s +``` + +This command stops K3S gracefully, which ensures everything halts correctly and there's no risk of corruption during the update. + +### Updating K3S + +Now, let's update K3S to its newest version. You can use the official K3S installation script to do this in a streamlined way. Running the script below will automatically detect the current installation and update it to the latest available version: + +```bash +curl -sfL https://get.k3s.io | sh - +``` + +The script will download, install, and configure the latest version of K3S while keeping all your configurations in place. + +### Starting the K3S Service Again + +Once the update finishes, restart the K3S service on the node to bring it back online: + +```bash +sudo systemctl start k3s +``` + +This will load the new K3S version and all services will resume. + +### Uncordoning the Node + +#### What Is "Uncordoning"? +After an update, we need to make the node available again for scheduling new pods, i.e., undo the "unschedulable" state created during the drain. + +#### How to Uncordon a Node: +To let Kubernetes know this node is now ready to schedule new pods again: + +```bash +kubectl uncordon +``` + +This command marks the node as "schedulable," meaning new pods can now be assigned to it. + +### Verifying the Update + +Once the node is back online, verify the K3S version to confirm that the update was successful: + +```bash +k3s --version +``` + +Check that the output shows the latest version installed. \ No newline at end of file diff --git a/docusaurus/docs/kubernetes/k3s-setup.md b/docusaurus/docs/kubernetes/k3s-setup.md new file mode 100644 index 0000000..d8439f3 --- /dev/null +++ b/docusaurus/docs/kubernetes/k3s-setup.md @@ -0,0 +1,178 @@ +--- +title: K3S Installation +--- + +### Before You Start + +Make sure you have: + +- [x] [Set up your Raspberry Pis](../hardware-raspberry-pi-setup/raspberry-pi-setup.md) +- [x] [Set up your Mini-PCs (if any)](../hardware-raspberry-pi-setup/mini-pcs-setup.md) +- [x] [Configured your Network](../networking/mikrotik/network-overview.mdx) + +Now, we are going to set up a Kubernetes cluster. You don't need to understand what Kubernetes is at this point, just follow the steps and you'll be able to use it. Once it's set up, you'll be able to deploy your applications and learn more about how it works. + +In this guide, we will set up a [HA (High Availability)](https://en.wikipedia.org/wiki/High-availability_cluster) cluster with 3 master nodes. If you are using different [hardware](../hardware-raspberry-pi-setup/hardware.mdx), you can set up your cluster accordingly. For example, if you are using a single machine (e.g., a single Raspberry Pi, a single Mini-PC, etc.), you can set up a single master node cluster. + +The official K3S documentation also explains both: + +- [Single Master Node](https://docs.k3s.io/quick-start) +- [High Availability Embedded etcd](https://docs.k3s.io/datastore/ha-embedded) + +### Set Up the Master Node(s) + +> **Note:** We disable the default installation of [Traefik](https://traefik.io/traefik/) because we will install it manually later using [Helm](https://helm.sh/). We also disable `servicelb` since we will use [MetalLB](https://metallb.io/) as our [load balancer](). Don't worry much about those right now. You will learn more about what they are and how to use them later. + +**Why Use DNS Names Instead of IPs?** + +We use static DNS names (not raw IP addresses) for our nodes, as configured in our [Network Device Configuration](../networking/mikrotik/device-configuration.mdx). + +If we use IP addresses directly when setting up K3S, and those IPs ever change (for example, due to network reconfiguration or moving to a different subnet), our cluster will likely break. This is because K3S (and Kubernetes in general) embeds the node addresses, including in SSL certificates and cluster configuration. Changing the IPs later would require us to tear down and completely recreate the cluster, as the certificates and internal references would no longer match. + +By using DNS names that always resolve to the correct node, we can change the underlying IPs in our network without having to rebuild our Kubernetes cluster. The nodes will continue to find and trust each other as long as the DNS names remain consistent. + +Select one Raspberry Pi to act as the master node, and install K3S: + +As you can see, we are using the static DNS names that we've set up in our [Network Device Configuration](../networking/mikrotik/device-configuration.mdx). This is really important to ensure that the nodes can communicate with each other, also that we can have our cluster running even if the subnet changes in the future. + +```bash title="k3s-server-1.cluster" +curl -sfL https://get.k3s.io | K3S_TOKEN=SECRET_TOKEN_HERE sh -s - server \ + --cluster-init \ + --disable servicelb \ + --disable traefik \ + --node-name k3s-server-1.cluster +``` + +If you have multiple master nodes (as in my case, with 3), run the following command on each additional master node: + +```bash title="k3s-server-2.cluster" +curl -sfL https://get.k3s.io | K3S_TOKEN=SECRET_TOKEN_HERE sh -s - server \ + --server https://k3s-server-1.cluster:6443 \ + --disable servicelb \ + --disable traefik \ + --node-name k3s-server-2.cluster +``` + +```bash title="k3s-server-3.cluster" +curl -sfL https://get.k3s.io | K3S_TOKEN=SECRET_TOKEN_HERE sh -s - server \ + --server https://k3s-server-1.cluster:6443 \ + --disable servicelb \ + --disable traefik \ + --node-name k3s-server-3.cluster +``` + +**Copy and Set Permissions for Kubeconfig:** To avoid permission issues when using `kubectl`, copy the generated kubeconfig to your home directory and update its ownership: + +```bash title="Copy and Set Permissions for Kubeconfig" +# Create the .kube directory in your home directory if it doesn't already exist +mkdir -p ~/.kube + +# Copy the k3s.yaml file from its default location to your .kube directory as the default kubectl config file +sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config + +# Change the ownership of the copied config file to the current user and group, so kubectl can access it without requiring sudo +sudo chown $(id -u):$(id -g) ~/.kube/config +``` + +> **Troubleshooting Tips:** +> +> - If `kubectl get nodes` hangs or fails, check that the K3S service is running: +> `sudo systemctl status k3s` +> - If you see certificate or permission errors, double-check the ownership and permissions of `~/.kube/config`. +> - Make sure your firewall allows traffic on port 6443 between nodes. + +**Verify Cluster:** Ensure that `/etc/rancher/k3s/k3s.yaml` was created and the cluster is accessible: + +```bash title="Verify Cluster" +kubectl --kubeconfig ~/.kube/config get nodes +``` + +**Quick Cluster Health Check:** + +```bash title="Quick Cluster Health Check" +kubectl get componentstatuses +kubectl get pods --all-namespaces +``` + +**Set KUBECONFIG Environment Variable:** To make it more convenient to run `kubectl` commands without specifying the `--kubeconfig` flag every time, set an environment variable to automatically point to the kubeconfig file: + +```bash title="Set KUBECONFIG Environment Variable" +export KUBECONFIG=~/.kube/config +``` + +To make this setting permanent across shell sessions, add it to your shell profile: + +```bash title="Set KUBECONFIG Environment Variable" +echo "export KUBECONFIG=~/.kube/config" >> ~/.bashrc +source ~/.bashrc +``` + +This streamlines your workflow, allowing you to simply run `kubectl get nodes` instead of specifying the kubeconfig path each time. + +### Set Up Worker Nodes + +[Ansible Playbook](/ansible/playbooks/join-worker-nodes-and-setup-kube-config.yml) + +**Join Tokens:** On the master node, retrieve the join token from `/var/lib/rancher/k3s/server/token`: + +```bash title="Join Tokens" +vi /var/lib/rancher/k3s/server/token +``` + +**Worker Installation:** Use this token to join each worker node to the master: + +```bash title="Worker Installation" +curl -sfL https://get.k3s.io | K3S_TOKEN=SECRET_TOKEN_HERE sh -s - agent \ + --server https://k3s-server-1.cluster:6443 \ + --node-name k3s-worker-rp4.cluster + +curl -sfL https://get.k3s.io | K3S_TOKEN=SECRET_TOKEN_HERE sh -s - agent \ + --server https://k3s-server-1.cluster:6443 \ + --node-name k3s-worker-hp.cluster + +curl -sfL https://get.k3s.io | K3S_TOKEN=SECRET_TOKEN_HERE sh -s - agent \ + --server https://k3s-server-1.cluster:6443 \ + --node-name k3-worker-lenovo.cluster +``` + +**Node Verification:** Check that all worker nodes have joined the cluster. On your master node, run: + +```bash title="Node Verification" +kubectl get nodes +``` + +### Set Up kubectl on Your Local Machine + +#### Kubeconfig + +After setting up your cluster, it's more convenient to manage it remotely from your local machine. + +Here's how to do that: + +**Create the `.kube` directory on your local machine if it doesn't already exist:** + +```bash title="Create the .kube directory on your local machine" +mkdir -p ~/.kube +``` + +**Copy the kubeconfig from the master node to your local `.kube` directory:** + +```bash title="Copy the kubeconfig from the master node to your local .kube directory" +scp @:~/.kube/config ~/.kube/config +``` + +Replace `` with your username and `` with the IP address of your master node. + +**Note:** If you encounter a permissions issue while copying, ensure that the `~/.kube/config` on your master node is owned by your user and is accessible. You might have to adjust file permissions or ownership on the master node accordingly. + +**Update the kubeconfig server details (Optional):** + +Open your local `~/.kube/config` and make sure the `server` IP matches your master node's IP. If it's set to `127.0.0.1`, you'll need to update it: + +```yaml title="Update the kubeconfig server details" +server: https://:6443 +``` + +Replace `` with the IP address of your master node. + +After completing these steps, you should be able to run `kubectl` commands from your local machine to interact with your Kubernetes cluster. This avoids the need to SSH into the master node for cluster management tasks. diff --git a/docusaurus/docs/kubernetes/kubernetes-80-20-rule.mdx b/docusaurus/docs/kubernetes/kubernetes-80-20-rule.mdx new file mode 100644 index 0000000..30c74c9 --- /dev/null +++ b/docusaurus/docs/kubernetes/kubernetes-80-20-rule.mdx @@ -0,0 +1,19 @@ +--- +title: Kubernetes 80/20 Rule +--- + +import Alert from "@site/src/components/Alert/index.tsx"; + + + +In our case of Kubernetes, that means that by focusing also upfront to a potential issues that we might face, we can save a +lot of time by being aware of these very issues, and how to best debug them. + +import KubernetesParetoPrinciple from "@site/src/components/KubernetesParetoPrinciple/index.tsx"; + + diff --git a/docusaurus/docs/kubernetes/kubernetes-yml-structure.md b/docusaurus/docs/kubernetes/kubernetes-yml-structure.md new file mode 100644 index 0000000..1661fdf --- /dev/null +++ b/docusaurus/docs/kubernetes/kubernetes-yml-structure.md @@ -0,0 +1,79 @@ +--- +title: Writing YAML files for Kubernetes +--- + +Writing YAML files for Kubernetes involves understanding the basic structure and key components used to define cluster objects. Here's a simple logical guide to help you write k3s YAML files manually: + +### Basic Structure of a Kubernetes YAML File + +#### API Version (`apiVersion`): + +Every YAML file starts with an API version. It's a string that indicates the version of the Kubernetes API you're using for the object. + +Common examples include: + +- `apiVersion: v1` for core objects like Services and Pods. +- `apiVersion: apps/v1` for objects like Deployments. +- Other versions might be `networking.k8s.io/v1` for Ingress. + +--- + +**Kind (`kind`)**: +- This represents the type of Kubernetes resource you're defining. + +Some common kinds are: + +- `Pod` +- `Service` +- `Deployment` +- `Ingress` + +--- + +**Metadata (`metadata`)**: + +This section includes basic metadata about the object, such as: + + - `name`: A unique identifier for the object within its namespace. + - `namespace`: (Optional) Defines the namespace where the object should be created or managed. + - `labels`: (Optional) Key-value pairs to organize and select groups of objects. + +--- + +**Spec (`spec`)**: + +This section contains the specifications of the object. + +It varies significantly between different kinds, but here are some general guidelines: + +**For Deployments**: + - Define `replicas` to set the desired number of pod copies. + - Use `selector` to match Pods with labels. + - Define a `template` for the Pod specification. + +**For Services**: + - Define `selector` to route traffic to the right Pods. + - Set `ports` to map incoming traffic to the target Pods. + +**For Ingress**: + - Define rules for routing external HTTP/S traffic to internal services. + +--- + +### Logical Steps to Write a k3s YAML File + +**Determine the Object Type**: + +Decide whether you need a Deployment, Service, Pod, etc. This dictates the fields you'll need. + +**Set the API Version and Kind**: + +Reference Kubernetes documentation or k3s-specific resources to know which API version to use and set the appropriate kind. + +**Add Metadata**: + +Assign a name to your object and optionally a namespace. Proper naming conventions help manage and track resources. + +**Define the Spec**: + +Tailor this section based on the object type. Carefully specify details like the number of replicas for Deployments, port mappings for Services, or routing rules for Ingress. diff --git a/docusaurus/docs/kubernetes/what-is-kubernetes.md b/docusaurus/docs/kubernetes/what-is-kubernetes.md new file mode 100644 index 0000000..77b956f --- /dev/null +++ b/docusaurus/docs/kubernetes/what-is-kubernetes.md @@ -0,0 +1,25 @@ +--- +sidebar_position: 5 +title: What is Kubernetes? 🎥 +--- + +As with anything in life, my experience has taught me that focusing on the essence of something and then going top-down is the best way to learn. In the context of Kubernetes, this means understanding it in a "teach me like I'm 6 years old" way. Kubernetes is a complex system, and trying to understand every component at the very beginning is overwhelming and will only lead to frustration. Plus, it won't be useful anyway, as this theory becomes important later, once things start failing (not working) and we need to debug. + +So, what we need to get out of this section is the main benefit of an orchestration platform like Kubernetes: what it does, and how it can help us as engineers. + +### So, what does Kubernetes actually do? + +Kubernetes is basically our super-organized friend who makes sure all our apps (and the stuff they need) are running smoothly, wherever we want them, cloud, our laptop, or a bunch of servers. We tell Kubernetes what we want ("run this app, keep it healthy, make sure it can handle lots of users"), and it figures out the rest. + +Basically, something like this: + +- **We give it instructions**: Like, "We want 3 copies of our app running." +- **It keeps things running**: If something crashes, Kubernetes restarts it. If we need more power, it adds more copies. If we want to update our app, it helps us do it without breaking things. +- **It works anywhere**: Cloud, on-prem, hybrid, etc. Kubernetes doesn't care. It just wants to run our stuff. +- **It's all about making life easier**: Less manual work, more time for us to build cool things. + +### How to Get the Most Out of It + +- Check out the [official docs](https://kubernetes.io/docs/home/) - This is probably the best resource out there. If you are patient enough to read it, you will learn a lot. +- [Docker Mastery: with Kubernetes + Swarm from a Docker Captain](https://www.udemy.com/course/docker-mastery) - This is a great course to get started with Docker and Kubernetes. +- Let's not stress about the details at first. We'll learn about each individual component as we go along. diff --git a/docusaurus/docs/networking/expose-traefik-dashboard-inside-the-k3s-cluster.md b/docusaurus/docs/networking/expose-traefik-dashboard-inside-the-k3s-cluster.md new file mode 100644 index 0000000..ab6af08 --- /dev/null +++ b/docusaurus/docs/networking/expose-traefik-dashboard-inside-the-k3s-cluster.md @@ -0,0 +1,137 @@ +--- +title: Expose Traefik Dashboard inside the K3s Cluster +--- + +As we have learned in the previous section ([Kubernetes Networking](understanding-network-components#ingress-controllers-traefik-nginx)), ingress controllers are responsible for managing HTTP and HTTPS traffic, enabling external access to internal Kubernetes services. In simpler terms, the ingress controller ensures that incoming traffic is directed to the appropriate services that we define. + +In K3s, [Traefik](https://doc.traefik.io/traefik/) comes preconfigured as the default ingress controller, which means we can also take advantage of the [Traefik Dashboard](https://doc.traefik.io/traefik/operations/dashboard/). However, since the dashboard is not fully set up by default, we will need to configure it ourselves. + +Let's proceed with setting that up. + +### Verify Traefik is Running + +First, let’s check if Traefik is installed and running in the cluster: +```bash +kubectl get pods -n kube-system +``` + +We’ll look for a pod with a name like `traefik-...`. If it’s there and running, we’re good to go. If not, we might need to revisit the K3s installation settings. + +## Objective + +You will be creating the required Kubernetes resources: + +1. A `ClusterIP` service to expose the Traefik dashboard. +2. An `Ingress` rule to route traffic to the dashboard service. + +## Create the Traefik Dashboard Service + +We'll create a `ClusterIP` Service to expose the Traefik dashboard. This service will make the Traefik dashboard's HTTP API, running on port `9000`, available to the cluster. + +Create a YAML file named `traefik-dashboard-service.yaml` with the following contents: + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: traefik-dashboard + namespace: kube-system + labels: + app.kubernetes.io/instance: traefik + app.kubernetes.io/name: traefik-dashboard +spec: + type: ClusterIP + ports: + - name: traefik + port: 9000 # Dashboard listens on port 9000 + targetPort: 9000 # Forward traffic to this port on Traefik pods + protocol: TCP + selector: + app.kubernetes.io/instance: traefik-kube-system + app.kubernetes.io/name: traefik +``` + +- **Explanation**: + - `ClusterIP`: Used for internal access only (within the cluster not externally exposed). + + - The service exposes port `9000`, which is the default port where Traefik serves its dashboard. + +```bash +kubectl apply -f traefik-dashboard-service.yaml +``` + +--- + +## Create the Traefik Ingress Resource + +Next, we need to create an Ingress that routes traffic to the `traefik-dashboard` service created in the previous step. This will allow external traffic to reach the dashboard by using a specific domain. + +Create a YAML file named `traefik-dashboard-ingress.yaml` with the following contents: + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: traefik-ingress + namespace: kube-system + annotations: + spec.ingressClassName: traefik +spec: + rules: + - host: YOUR_DOMAIN_NAME # Replace YOUR_DOMAIN_NAME with your own domain. + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: traefik-dashboard + port: + number: 9000 +``` + +- Ingress: The ingress resource defines rules that route HTTP requests to `traefik-dashboard` at port `9000` based on a specific host (`YOUR_DOMAIN_NAME`). + +- Replace `YOUR_DOMAIN_NAME` with the desired domain name where you want to expose your Traefik dashboard. + +- IngressClass: We're using the `traefik` ingress controller, as it's the default installed ingress controller for K3s. + +```bash +kubectl apply -f traefik-dashboard-ingress.yaml +``` + +--- + +## Update DNS or `/etc/hosts` + +To access the Traefik dashboard through your web browser, you'll need to ensure DNS resolves the host (`YOUR_DOMAIN_NAME`) to the correct IP address (either a load balancer IP, node IP, etc.). In the case of local development, you can update your **/etc/hosts** file. + +Suppose you're running a single-node K3s cluster accessible at the IP `192.168.1.100` and you want to use `traefik.example.com`. + +Edit `/etc/hosts` and add: + +```bash +192.168.1.100 traefik.example.com +``` + +## Access the Traefik Dashboard + +Once the service and ingress resources are in place, and DNS (or `/etc/hosts`) has been configured, you should be able to access the dashboard in your browser: + +``` +http://traefik.example.com/ +``` + +### Notes: + +- Deployment Security: The `Ingress` config above exposes the dashboard without authentication. For production deployments, consider securing the dashboard with basic authentication or other mechanisms. +- Dashboard Availability: By default, Traefik's dashboard is available via port 9000 and isn't exposed unless configured to be so. The steps above ensure it is properly exposed. + +## Clean-up + +When you no longer need the Traefik Dashboard exposed, you can remove the resources by using the following commands: + +```bash +kubectl delete -f traefik-dashboard-ingress.yaml +kubectl delete -f traefik-dashboard-service.yaml +``` \ No newline at end of file diff --git a/docusaurus/docs/networking/kubernetes-networking-explained.md b/docusaurus/docs/networking/kubernetes-networking-explained.md new file mode 100644 index 0000000..28e82e4 --- /dev/null +++ b/docusaurus/docs/networking/kubernetes-networking-explained.md @@ -0,0 +1,235 @@ +--- +title: Kubernetes Networking Explained +--- + +## Objectives: + +1. How **networking works in Kubernetes** (flat networking and DNS). +2. Types of Kubernetes **Service** resources and their usage. +3. How **Ingress** works and integrates with your cluster. +4. **Cross-namespace pod/service communication**. +5. How to **restrict communication** with Network Policies. +6. Bonus tips for optimizing and securing your cluster's networking. + +## The Basics of Kubernetes Networking + +Kubernetes networking is designed to be **simple and flat**: + +- Any pod can communicate with any other pod in the cluster, regardless of which namespace they're in. This communication works out of the box without additional configuration. +- Pods and services use **DNS** for service discovery instead of hardcoding IP addresses. + +### Pod-to-Pod Networking + +Every pod is assigned a unique IP address. All pods share a single, flat address space, so there’s no [Network Address Translation (NAT)](https://www.youtube.com/watch?v=FTUV0t6JaDA) when pods communicate. However, pod IPs are [ephemeral](https://www.google.com/search?q=ephemeral&oq=ephemeral&gs_lcrp=EgZjaHJvbWUyBggAEEUYOdIBBzExOWowajeoAgCwAgA&sourceid=chrome&ie=UTF-8), they change if a pod is restarted. + +### Pod-to-Service Networking with DNS + +Kubernetes provides a built-in DNS service that allows pods to resolve services using their names. For example: + +- A service called `nodejs-service` in the `default` namespace can be resolved by other pods in the same namespace as: + +``` +http://nodejs-service +``` + +- From another namespace, it might look like: + +``` +http://nodejs-service.default.svc.cluster.local +``` + +This DNS-based service discovery simplifies communication between pods and services, especially in complex setups. + +## Key Networking Components in Kubernetes + +### **A. Services** + +Services are used to expose a group of pods (selected using labels) over the network and provide a stable address for accessing them. + +Three key types of services: + +1. **ClusterIP** (default) + +- Accessible **within the cluster only**. +- Provides internal networking between pods. +- Example: A backend service used by a frontend within the same application stack. + +2. **NodePort** + +- Exposes a service on a static port across all cluster nodes. +- Mostly used for development purposes but not ideal for production due to limited network flexibility. + +3. **LoadBalancer** + +- Requests an external IP to expose the service outside your cluster. In K3s, this integrates with **MetalLB** to assign an IP from your private pool. + +> Tip: Minimize `LoadBalancer` usage by routing external traffic via an **Ingress Controller** for better efficiency. + +## Ingress: The Gateway to Your Cluster + +Ingress is responsible for **routing external HTTP / HTTPS traffic** to services within your cluster. It integrates seamlessly with **Traefik**, your Ingress Controller in K3s. + +### How It Works: + +1. Create your services (e.g., `ClusterIP` services for Node.js, backends, etc.). +2. Define an Ingress resource: + - Map hostnames (e.g., `nodejs.example.com`) or path prefixes (e.g., `/api`) to specific services. +3. Traefik manages incoming requests and routes them to the appropriate service. + +**Example Ingress Resource:** + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: my-ingress + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: web +spec: + rules: + - host: nodejs.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: nodejs-service + port: + number: 80 +``` + +### Benefits of Ingress: + +- Reduces the need for multiple `LoadBalancer` services, only Traefik’s load balancer requires an external IP. +- Simplifies DNS-based routing for multiple services. + +## Cross-Namespace Networking + +### **Default Behavior:** + +In K3s/Kubernetes, pods and services in one namespace can communicate with those in another **by default**. You can achieve this by: + +1. Using DNS: + + - `..svc.cluster.local` + - Example: `http://postgres-service.database.svc.cluster.local` + +2. Accessing services by IP/methods if service discovery is properly managed. + +### **Restricting Cross-Namespace Communication** + +To prevent unrestricted communication between namespaces, use **Network Policies** (see below). + +## Network Policies: Restricting Internal Communication + +By default, Kubernetes allows all traffic between pods and across namespaces. To secure your cluster, you can leverage **Network Policies** to restrict ingress (incoming) and/or egress (outgoing) traffic. + +### **How Network Policies Work** + +Network Policies let you: + +1. Define which pods are allowed to receive traffic (ingress). +2. Define which pods are allowed to send traffic (egress). +3. Use labels and selectors to control access between pods/services. + +### Examples: + +#### **Default-Deny All Traffic** + +The foundation of securing your cluster: + +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: deny-all + namespace: default +spec: + podSelector: {} + policyTypes: + - Ingress + - Egress +``` + +- Blocks all traffic to/from pods in the `default` namespace unless explicitly allowed. + +#### **Allow Specific Namespace Traffic** + +Allow only traffic originating from pods in a specific namespace (e.g., `frontend` namespace): + +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-namespace-frontend + namespace: backend +spec: + podSelector: {} + policyTypes: + - Ingress + ingress: + - from: + - namespaceSelector: + matchLabels: + role: frontend +``` + +- In the `backend` namespace, only pods from the `frontend` namespace (labeled `role: frontend`) can communicate. + +#### **Allow Specific Pod Communication** + +Allow only a specific pod to communicate with another (e.g., frontend → backend): + +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: allow-frontend-backend + namespace: default +spec: + podSelector: + matchLabels: + app: backend + ingress: + - from: + - podSelector: + matchLabels: + app: frontend +``` + +- Backend pods (`app: backend`) can only receive traffic from frontend pods (`app: frontend`). + +## Useful Tools for Debugging Networking in K3s + +1. **DNS Resolution** + +- Verify service discovery with DNS: + +```bash +kubectl exec -it -- nslookup +``` + +2. **Curl/HTTP Testing** + +- Use `curl` or similar tools to confirm connectivity between services: + +```bash +kubectl exec -it -- curl +``` + +3. **Logs for Ingress** + +- Check Traefik logs to diagnose routing issues. + +4. **Network Policy Debugging** + +- Use tools like **Cilium** (if installed) or **NetworkPolicy Viewer** addons for better visualization of applied policies. + +## Best Practices for K3s Networking + +- Use **ClusterIP** for internal services and restrict `NodePort` services. +- Depend on **Ingress** for external HTTP/S access, reduce the use of multiple `LoadBalancer` services. +- Enforce a **default-deny policy** and gradually allow necessary traffic. +- Use namespace labels and Network Policies to isolate and secure workloads. +- Monitor and audit your networking policies and Traefik configurations regularly. diff --git a/docusaurus/docs/networking/mikrotik/common-scenarios.mdx b/docusaurus/docs/networking/mikrotik/common-scenarios.mdx new file mode 100644 index 0000000..d3c49b6 --- /dev/null +++ b/docusaurus/docs/networking/mikrotik/common-scenarios.mdx @@ -0,0 +1,8 @@ +--- +sidebar_position: 6 +title: Common Scenarios +--- + +import Scenarios from "@site/src/components/MikrotikNetworking/Scenarios"; + +{" "} diff --git a/docusaurus/docs/networking/mikrotik/configure-email-on-mikrotik.md b/docusaurus/docs/networking/mikrotik/configure-email-on-mikrotik.md new file mode 100644 index 0000000..9a389d4 --- /dev/null +++ b/docusaurus/docs/networking/mikrotik/configure-email-on-mikrotik.md @@ -0,0 +1,28 @@ +--- +title: Configure Email on MikroTik +--- + +If your public IP changes or similar events occur, it's useful to receive notifications. + +There are many ways to achieve this, such as calling a webhook to send a Slack or Discord message. The simplest method, however, is to send an email to yourself. This guide uses Gmail as an example. + +Before configuring email on RouterOS, generate an app password for your Gmail account. + +You can do this by [generating an app password](https://myaccount.google.com/apppasswords). Store the password securely in a password manager like LastPass or 1Password. + +Next, configure the router’s SMTP settings: + +```rsc +/tool e-mail +set address=smtp.gmail.com port=587 start-tls=yes from=your.email@gmail.com user=your.email@gmail.com password=your-app-password +``` + +Test the configuration: + +```rsc +/tool e-mail send to="your.email@gmail.com" subject="MikroTik Test" body="Hello from the router" +``` + +This provides a general-purpose tool for sending emails from your MikroTik router. Use it for monitoring, alerts, or any scenario where email notifications are helpful, for example, when IP access is granted or denied, or when a specific event occurs. + +In the next section, we'll cover how to send an email when a particular event happens. diff --git a/docusaurus/docs/networking/mikrotik/core-concepts.mdx b/docusaurus/docs/networking/mikrotik/core-concepts.mdx new file mode 100644 index 0000000..32bdafc --- /dev/null +++ b/docusaurus/docs/networking/mikrotik/core-concepts.mdx @@ -0,0 +1,8 @@ +--- +sidebar_position: 2 +title: Core Concepts +--- + +import CoreConcepts from "@site/src/components/MikrotikNetworking/CoreConcepts"; + +{" "} diff --git a/docusaurus/docs/networking/mikrotik/device-configuration.mdx b/docusaurus/docs/networking/mikrotik/device-configuration.mdx new file mode 100644 index 0000000..c52e492 --- /dev/null +++ b/docusaurus/docs/networking/mikrotik/device-configuration.mdx @@ -0,0 +1,8 @@ +--- +sidebar_position: 4 +title: Device Configuration +--- + +import DeviceConfiguration from "@site/src/components/MikrotikNetworking/DeviceConfiguration"; + +{" "} diff --git a/docusaurus/docs/networking/mikrotik/dynamic-dns-with-cloudflare.md b/docusaurus/docs/networking/mikrotik/dynamic-dns-with-cloudflare.md new file mode 100644 index 0000000..aa89c28 --- /dev/null +++ b/docusaurus/docs/networking/mikrotik/dynamic-dns-with-cloudflare.md @@ -0,0 +1,202 @@ +--- +title: DDNS Using Cloudflare +--- + +## MikroTik Scripting + +After nearly two decades as programmers, I find MikroTik scripting to be one of RouterOS’s best features. It enables creativity and extensibility, allowing me to quickly write scripts whenever a new use case arises. The scripting language is simple, and if you have programming experience, it should feel straightforward. + +## Why do we need a DDNS? + +Without a static public IP address, setting our IP in Cloudflare (or any DNS provider) won’t work permanently. Our current public IP might work for now, but once our ISP changes it, our services will become inaccessible. To solve this, we use Dynamic DNS (DDNS): we create an A record in Cloudflare (e.g., something.example.com) and update it automatically whenever our public IP changes. This way, we can always point our ingress or other services to a consistent domain name. + +In our setup, we use two methods: + +- Utilize the `On Up` event under the `default` [PPP (Point-to-Point)](https://help.mikrotik.com/docs/spaces/ROS/pages/328072/PPP) profile +- Use the built-in [Scheduler](https://help.mikrotik.com/docs/spaces/ROS/pages/40992881/Scheduler) (essentially, a cron) to run every X minutes to update the DNS record in Cloudflare. + +Before using the script below, we’ll need to set up a few things in our Cloudflare account. Specifically, we’ll need some tokens and IDs so the script can update our DNS record via the API. This setup should only take a few minutes. + +## Get Our Cloudflare Credentials + +Before configuring the router, let’s gather these four pieces of information from our Cloudflare account: + +- Our Zone ID. +- The DNS Record Name we want to update (e.g., router.ourdomain.com). +- The DNS Record ID for that specific record. +- A special API Token. + +**Step A: Create the DNS Record (if it doesn't exist)** + +First, we need a placeholder 'A' record in Cloudflare that the script can target for updates. + +- Log in to our Cloudflare dashboard. +- Go to the DNS settings for our domain. +- Click Add record. +- Configure it as follows: + - Type: A + - Name: The subdomain we want to use (e.g., router, home, m920q). + - IPv4 address: Use a placeholder, such as our current public IP. The script will update this automatically as our IP changes. + - Proxy status: Our choice. If we want Cloudflare's protection (orange cloud), leave it Proxied. If we want a direct connection (e.g., for a VPN), set it to DNS only. Note our choice for later. + - Click Save. + +**Step B: Get our Zone ID and API Token** + +- Find our Zone ID: On the main `Overview page` for our domain in Cloudflare, scroll down. We'll find the Zone ID on the right-hand side. Copy it to a safe place. + +- Create an API Token: + - Click the user icon in the top right and go to My Profile. + - Select the API Tokens tab on the left. + - Click Create Token. + - Find the Edit zone DNS template and click Use template. + - Make sure to assign the following two permissions: `Zone - Zone - Read` and `Zone - DNS - Edit`. In simple terms, our script needs to read data from our DNS and update DNS records when needed. + - Under Zone Resources, ensure we select the specific domain we want this token to control. + - Click Continue to summary, then Create Token. + - Cloudflare will display the token only once. Copy it immediately and store it safely. + +**Step C: Get the DNS Record ID** + +This ID is not visible on the dashboard. The easiest way to get it is with a command on our PC (not the router). Open a Command Prompt (Windows) or Terminal (Mac/Linux) and run the following, replacing the capitalized parts with our info: + +```bash +curl -X GET "https://api.cloudflare.com/client/v4/zones/OUR_ZONE_ID/dns_records?name=OUR_DNS_RECORD_NAME" \ +-H "Authorization: Bearer OUR_API_TOKEN" \ +-H "Content-Type: application/json" +``` + +- `OUR_ZONE_ID`: The ID from Step B. +- `OUR_DNS_RECORD_NAME`: The full name, e.g., router.ourdomain.com. +- `OUR_API_TOKEN`: The token we just created. + +The command returns a response. Find and copy the "id" value from it. + +## DDNS Script for Cloudflare + +Now, let's program the router. + +**Step 1: Create the Script** + +- On our MikroTik router, go to `System > Scripts`. +- Click + to add a new script. Name it e.g. `cloudflare-ddns`. +- Copy the entire code block below and paste it into the Source box. + +```bash +:local cfApiToken "YOUR_API_TOKEN_HERE" +:local cfZoneId "YOUR_ZONE_ID_HERE" +:local cfDnsId "YOUR_DNS_ID_HERE" +:local cfDnsName "YOUR_DNS_NAME_HERE" +:local cfProxied true +:local currentIP +:local lastIP +:local wanInterface "YOUR_WAN_INTERFACE_HERE" # e.g. mine is named Telenor PPPoE + +:log info "--- Cloudflare DDNS Start ---" + +# Get current IP from the router interface +:do { + :local ipCIDR [/ip address get [/ip address find interface=$wanInterface] address] + :set currentIP [:pick $ipCIDR 0 [:find $ipCIDR "/"]] +} on-error={ + :log error "Cloudflare DDNS: Failed to get IP from interface '$wanInterface'. Aborting." + :set currentIP "0" +} + +# Only proceed if we successfully got a valid IP address +:if ($currentIP != "0") do={ + + :log info "Cloudflare DDNS: Current IP is $currentIP. Sending update..." + + :local apiURL ("https://api.cloudflare.com/client/v4/zones/$cfZoneId/dns_records/$cfDnsId") + + :local headers { + "Authorization: Bearer $cfApiToken" + "Content-Type: application/json" + } + + :local payload ("{\"type\":\"A\",\"name\":\"" . $cfDnsName . "\",\"content\":\"" . $currentIP . "\",\"ttl\":1,\"proxied\":" . $cfProxied . "}") + + /tool fetch url=$apiURL http-method=put http-header=$headers http-data=$payload output=none mode=https + + :log info "Cloudflare DDNS: Update command sent." +} + +:log info "--- Cloudflare DDNS End ---" + +``` + +Edit the five configuration lines at the top with our Cloudflare information, and update the WAN interface as needed. + +## Logging + +We’ll notice the script includes several log statements. Logging is a good engineering practice, so I recommend keeping them. + +Before proceeding to the next steps, let’s test the script we just created under `System > Scripts`. + +- Open the `cloudflare-ddns` script, and click `Run Script` +- Open the `Log` menu item in the sidebar to check if everything is working correctly. If there are errors, they will appear in red. Most errors are related to permissions or similar issues, such as Cloudflare returning an HTTP status 400. Once everything works, we can proceed to the next steps. + +**Step 2: Schedule the Script** + +Navigate to `System > Scheduler`. + +- Click + to add a new schedule. +- Name it Run-Cloudflare-Update. +- In the On Event box, type the script name: `cloudflare-ddns`. +- Set the interval, for example, to 00:05:00 (every 5 minutes), or adjust as needed. +- Click Apply and OK. + +## Using the PPPoE Client "On-Up" Script + +If your internet connection uses PPP (as mine does for Telenor ISP), we can use the `On Up` event, which triggers when the connection comes up. + +Typically, our public IP changes when our connection goes down and comes back up. + +- Navigate to the PPP menu on the left. +- Go to the Profiles tab. +- Open the profile that our PPPoE interface uses. This is typically the one named default or default-encryption. +- In the Profile settings, find the field named `On Up`. +- In this field, we'll run our previously defined script + +```bash +:log info "PPP UP - running DDNS update" + # The name here has to match the name of the + # script we previously created under System > Scripts +/system script run cloudflare-ddns +``` + +- Click Apply and OK. + +## For Other Connection Types + +For those of you who have a different type of internet connection, the same concept applies: + +**DHCP Client (Cable/Fiber)** + +If your WAN interface gets its IP via DHCP, we would: + +- Go to IP -> DHCP Client +- Open our WAN DHCP client entry +- Run a script inside the Script field, e.g. `/system script run cloudflare-ddns` + +## Sending emails for monitoring purposes + +If you have [configured email](./configure-email-on-mikrotik) as described earlier, you can receive notifications when certain events occur. Monitoring helps us understand patterns in our network, such as how often our public IP changes. + +Let's extend the above `On Up` and `On Down` scripts to log and send an email when these events occur: + +```bash +:log info "PPP UP - running DDNS update" +/system script run cloudflare-ddns + +:log info "PPP is UP - sending email" +/tool e-mail send to="hi@programmer.network" subject="PPP is UP" body=("Public IP: " . [/ip address get [/ip address find interface="Telenor PPPoE"] address]) +``` + +And of course, when it goes down as well: + +```bash +:log warning "PPP is DOWN - sending email" +/tool e-mail send to="hi@programmer.network" subject="PPP is DOWN" body="PPP connection lost" +``` + +In simple terms, we will want to do this in the appropriate place depending on the connection type. diff --git a/docusaurus/docs/networking/mikrotik/firewall-logic.mdx b/docusaurus/docs/networking/mikrotik/firewall-logic.mdx new file mode 100644 index 0000000..7a63e56 --- /dev/null +++ b/docusaurus/docs/networking/mikrotik/firewall-logic.mdx @@ -0,0 +1,8 @@ +--- +sidebar_position: 5 +title: Firewall Logic +--- + +import FirewallLogic from "@site/src/components/MikrotikNetworking/FirewallLogic"; + +{" "} diff --git a/docusaurus/docs/networking/mikrotik/lenovo-m920q-roas.mdx b/docusaurus/docs/networking/mikrotik/lenovo-m920q-roas.mdx new file mode 100644 index 0000000..f8cf7f8 --- /dev/null +++ b/docusaurus/docs/networking/mikrotik/lenovo-m920q-roas.mdx @@ -0,0 +1,144 @@ +--- +sidebar_position: 3 +title: MikroTik RouterOS on Lenovo M920q +--- + +You can have the greatest computers in the world, with insane amounts of CPU, RAM, and storage. But without proper networking, or if your network stack is a bottleneck, none of it will matter. Who cares if your NVMe can transfer data at lightning speed if your network is slow? Who cares if your API is written in binary and can process 17 trillion requests in negative time if your network adds four seconds of latency? Simply put, without top-notch networking, everything else will feel like a third-rate setup. That’s why I decided to step up my game and build this awesome machine as my dedicated router, the brain of the entire operation. + +As I mentioned in the [Hardware Overview](../../hardware-raspberry-pi-setup/hardware.mdx) section, you don't have to use the same hardware I did. If you have a different router running MikroTik RouterOS, you can skip this section. However, if you're looking for an enterprise-grade router for your home network at a much lower cost than buying hardware from MikroTik or other vendors, this section is for you. + +Many homelab setups I’ve seen on YouTube underestimate the importance of proper networking. Regardless of CPU, RAM, or storage, devices must communicate efficiently. Clients connect to the router, which means it must handle many firewall rules, NAT, VPN, etc. + +There are four things you need for this setup: + +1. [Lenovo M920q Mini PC](https://www.ebay.com/sch/i.html?_nkw=Lenovo+M920Q&_sacat=0&_from=R40&_trksid=p2332490.m570.l1313) + - Has a critical feature: a usable PCIe expansion slot, allowing installation of a server-grade network card. +2. [The Network Card](https://www.aliexpress.com/item/1005005920672631.html?spm=a2g0o.order_list.order_list_main.11.329f180254cPlG) (NIC) + - SFP+ model (Intel 82599ES chipset, X520-DA2) for direct fiber or DAC connectivity to your switch. +3. [PCIe Riser Card](https://www.aliexpress.com/item/1005007593015885.html?spm=a2g0o.order_list.order_list_main.5.329f180254cPlG) (to connect the NIC to the motherboard) + - Mandatory adapter needed to physically connect the network card to the Lenovo M920q's motherboard. +4. [MikroTik RouterOS License](https://help.mikrotik.com/docs/spaces/ROS/pages/328149/RouterOS+license+keys) + - You'll need a Level 4 (P1) RouterOS license to unlock the full speed of your hardware. + +
+ +import ImageGallery from "react-image-gallery"; + + + +
+ +For this build, we'll install MikroTik RouterOS v7 directly on bare metal. This avoids virtualization headaches and ensures maximum hardware performance. We'll use a 90W or higher power adapter to ensure the CPU, SSD, and NIC have enough power, even under heavy load. + +Switching from the RB3011 to this build is a huge leap forward. The RB3011 would struggle with high connection counts or heavy traffic, but this setup handles those demands easily, making it ideal for a busy K3s cluster. Compared to expensive MikroTik CCRs, this Intel-powered box outperforms many for CPU-intensive tasks like VPNs or complex firewall rules. + +We benefit from the flexibility and power of a general-purpose CPU, rather than being limited by a fixed-function router chip. For our services, this router removes the network as a bottleneck. APIs and applications will run at full speed, limited only by the server itself, not by the network path. + +## Lenovo M920q BIOS + +Before we can install RouterOS, we have to configure the BIOS on our Lenovo M920q. To save you time and effort, here are the BIOS settings you should enable or disable. + +Generally, disable features you don't use. In this context, two clear candidates are `bluetooth` and the `wireless` card. I went further and removed the wireless card from the Lenovo M920q, as I won't be using it. + +Assuming you've updated the BIOS to the latest version, do the following: + +- Restart the PC and keep pressing the F1 key to enter the BIOS. + +- Go to `Devices > USB Setup > Bluetooth` and set it to `disabled`. If you don't use the front USB ports, you can disable those too, leaving just one rear port enabled for keyboard or external screen setup. + +- Go to `Devices > Network Setup` and ensure all options except `Wireless LAN` are enabled. Disable `Wireless LAN`, this router won't connect to your switch via wireless. The `PXE` options must be enabled to install RouterOS via [Netinstall](https://help.mikrotik.com/docs/spaces/ROS/pages/24805390/Netinstall). + +- Go to `Power` and set `After Power Loss` to `Power On`. This is the most critical setting for all Mini PCs, ensuring the hardware automatically powers on after an outage. + +- Go to `Startup` and make sure the `CSM` option is `Enabled` and `Boot Mode` is set to `Auto`. `CSM` stands for `Compatibility Support Module` and allows loading of non-UEFI operating systems. + +- Go to `Startup > Primary Boot Sequence` and change the boot order. Since you'll install RouterOS using Netinstall (via ethernet), move `Network 1` to the top of the `Primary Boot Sequence` list. + +At this point, your BIOS should be fully set up, and we can proceed to the next section: setting up MikroTik's Netinstall. For now, you can turn the PC off. + +## Install MikroTik RouterOS on Lenovo M920q + +From experience, I must emphasize: installing RouterOS via USB stick is not possible. I tried burning [RouterOS](https://mikrotik.com/software) to a USB stick using [Rufus](https://rufus.ie/en/), set up the BIOS, and booted into the USB. Starting the RouterOS installation, I got a `no cdrom found` error. + + + +
+ +I thought it might be a corrupted ISO or USB stick, so I tried different sticks and Rufus settings, but nothing worked. Eventually, with help from AI and forums, I concluded you must follow MikroTik's [Netinstall guide](https://help.mikrotik.com/docs/spaces/ROS/pages/24805390/Netinstall). + +In short, install RouterOS over the network. The USB process fails because, once installation starts, RouterOS looks for a CD-ROM instead of recognizing the USB drive. + +Rather than duplicating all the content from MikroTik's original guide, simply follow the official [Netinstall guide](https://help.mikrotik.com/docs/spaces/ROS/pages/24805390/Netinstall). The installation process takes less than a minute, and the next time you boot, your Lenovo M920q will be ready to route. + +`Before finalizing the setup`, make sure to return to your BIOS setup, go to `Startup > Primary Boot Sequence`, and set your SSD or NVMe (depending on your hardware) back as the primary boot option. If you skip this, your Lenovo will try to boot from the network every time, causing long delays before it attempts another boot method. + +Your text is clear and mostly well-written. Here’s a proofread version with minor improvements for clarity and flow: + +## Purchase the MikroTik RouterOS License + +If everything has gone well up to this point, you’re ready for the final step: connect to your new router and purchase the license. + +Unless you plan to register a mini ISP company and sell internet, just like myself, you’ll want to get a RouterOS Level 4 license. You can compare the licenses at [RouterOS license key levels](https://help.mikrotik.com/docs/spaces/ROS/pages/328149/RouterOS+license+keys#RouterOSlicensekeys-RouterOSlicensekeylevels). In short, the main differences between level 4 and higher licenses are features like OVPN, tunnels, EoIP, user management, RADIUS, etc. For most users, these aren’t necessary. + +To purchase the license, create a MikroTik account at the [MikroTik Account](https://mikrotik.com/client/) page. Once your account is created, you’ll receive your initial login information via email (be sure to change your password after logging in). + +After purchasing the license, you’ll receive an email with the subject `MikroTik RouterOS Licensed Key`, which will contain your license in a `.key` file. Download the license, log in to your router (e.g., via Winbox), go to `System > License`, and click `Import Key...`. Once you import the key, your router will prompt you to restart. After restarting, your new level 4 license will be applied to your device. + +## Moving the Configuration from an Old MikroTik Router + +Until now, I was using an RB3011. I exported its configuration and, with minimal changes, imported it to the new Lenovo M920q router. This saved a lot of time, as I didn’t have to recreate everything from scratch. + +If your current (original) router, such as an RB3011 or similar, is still running, follow these steps: + +**Export the config on the RB3011:** + +```bash +# You can name the file however you like +/export file=rb3011-backup +``` + +This will generate a file named `rb3011-backup.rsc`. + +Download this file using Winbox, WebFig, or SCP. You’ll find it under the `Files` menu. + +Open it in your preferred text editor (e.g., Neovim, VSCode, Emacs). + +At this point, you’ll want to adjust the config to match the interfaces on the Lenovo M920q. In my case, I only needed to search for `sfp1` (from the RB3011) and replace it with `ether2`. This is because, after installing the [Intel X520-DA2](https://www.aliexpress.com/item/1005005920672631.html?spm=a2g0o.order_list.order_list_main.11.329f180254cPlG), MikroTik RouterOS identifies the two SFP+ ports as `ether2` and `ether3`. So, your router will appear to have three Ethernet ports: `ether1`, `ether2`, and `ether3`. Functionally, these are still SFP+ ports, only the naming is different. + +Make the absolute minimum changes needed to get the new router up and running. Avoid making lots of adjustments at once, as this can make troubleshooting difficult (e.g., is the Lenovo working? Is the new SFP cable good?). + +Once you’ve made the minimal necessary changes, restore the backup onto your Lenovo M920q. + +After confirming that the basic setup works, you can proceed with additional changes and experiment as needed. + +## Keep Your Old Router Around as a Backup + +If you’re not in desperate need of money and don’t have to sell your old router, I recommend keeping it. You can use it to practice MikroTik configurations and have a backup in case your Lenovo ever stops functioning. Since much of this setup is about Kubernetes and we’re calling it a `mini data center`, high availability (HA) should apply at every level, not just the Kubernetes nodes. So, stash your old router in a safe place as a backup plan. diff --git a/docusaurus/docs/networking/mikrotik/network-overview.mdx b/docusaurus/docs/networking/mikrotik/network-overview.mdx new file mode 100644 index 0000000..4912f32 --- /dev/null +++ b/docusaurus/docs/networking/mikrotik/network-overview.mdx @@ -0,0 +1,8 @@ +--- +sidebar_position: 1 +title: Network Overview +--- + +import NetworkOverview from "@site/src/components/MikrotikNetworking/NetworkOverview"; + +{" "} diff --git a/docusaurus/docs/networking/mikrotik/summary-and-checklist.mdx b/docusaurus/docs/networking/mikrotik/summary-and-checklist.mdx new file mode 100644 index 0000000..b9bc847 --- /dev/null +++ b/docusaurus/docs/networking/mikrotik/summary-and-checklist.mdx @@ -0,0 +1,8 @@ +--- +sidebar_position: 7 +title: Summary & Checklist +--- + +import Summary from "@site/src/components/MikrotikNetworking/Summary"; + +{" "} diff --git a/docusaurus/docs/networking/mikrotik/vlan-schema.mdx b/docusaurus/docs/networking/mikrotik/vlan-schema.mdx new file mode 100644 index 0000000..b0d84ad --- /dev/null +++ b/docusaurus/docs/networking/mikrotik/vlan-schema.mdx @@ -0,0 +1,8 @@ +--- +sidebar_position: 3 +title: VLAN Schema +--- + +import VlanSchema from "@site/src/components/MikrotikNetworking/VlanSchema"; + +{" "} diff --git a/docusaurus/docs/networking/mikrotik/why-mikrotik.mdx b/docusaurus/docs/networking/mikrotik/why-mikrotik.mdx new file mode 100644 index 0000000..7ca4961 --- /dev/null +++ b/docusaurus/docs/networking/mikrotik/why-mikrotik.mdx @@ -0,0 +1,29 @@ +# Why Mikrotik? + +import Alert from "@site/src/components/Alert/index.tsx"; + + + +## My experience with Mikrotik + +About 15 years ago (between 2008 and 2011), I was the lead network engineer for a small local ISP called "[SweaSAT](https://www.linkedin.com/company/sweasat/about/)" in [Brčko, Bosnia](https://maps.app.goo.gl/NSGmqRAiybxfKrPn6). + +Here is [a video of me putting a Mikrotik RB600 into a case](https://www.youtube.com/watch?v=-Sz4nHgGsD4&ab_channel=AleksandarGrbic), which we eventually installed on either a water tower or a very tall building in our town, I don't recall exactly where. Here is [another video of me](https://www.youtube.com/watch?v=fZy_0GY98l0&ab_channel=AleksandarGrbic) at "Beotel" in Belgrade, where I received mentorship in network engineering and Mikrotik. Beotel was a major ISP in Belgrade. + +At SweaSAT, I maintained 16 wirelessly connected routerboards within a 50-kilometer radius. We used [Mikrotik RouterBOARDs](https://mikrotik.com/products/group/routerboard) with Omni and Pacific antennas, and of course, Link antennas for point-to-point connections. Our target users were in rural areas or locations without access to ADSL or cable internet (which was unavailable in Bosnia at the time). We installed Mikrotik devices on the highest points in town, water towers, tall buildings, and even rooftops in nearby villages. + +Wherever we installed the routerboards, Mikrotik was always the backbone of our network. It was reliable and, at the time, offered an unbeatable price-to-performance ratio. Comparable equipment from companies like Cisco would have been prohibitively expensive. + +Fast forward to 2025, and I still use Mikrotik devices at home. My preference may be influenced by my past experience, but Mikrotik has never let me down. As this guide and its new use case show, Mikrotik remains an excellent choice. + +Fifteen years later, Mikrotik is even better, offering more features, better documentation, and strong community support. As a European company, Mikrotik also complies with all EU regulations. + +## Resources and Community + +Overall, Mikrotik is a great choice for both, enterprise and home use. Mikrotik has a great [YouTube channel](https://www.youtube.com/@mikrotik) with a lot of tutorials and guides. If you'd like to learn more about how to use Mikrotik, I can highly recommend a Udemy author [Maher Haddad](https://www.udemy.com/user/maherhaddad2/?kw=mikrotik&src=sac) who has a lot of great courses on Mikrotik. I personally have bought nearly all of his courses, as I had to re-learn things that I haven't used in 15 years. Additionally, there are many great content creators on YouTube who are sharing their knowledge about Mikrotik, make sure to check them out. One I can recommend is [The Network Berg](https://www.youtube.com/@TheNetworkBerg). I watched some of his videos and they are great. + +And before I forget, make sure to watch [5 reasons to choose MikroTik](https://www.youtube.com/watch?v=0DykSOLyU5Y&ab_channel=MikroTik), it's a great video. diff --git a/docusaurus/docs/networking/setup-metallb.md b/docusaurus/docs/networking/setup-metallb.md new file mode 100644 index 0000000..37e7f7f --- /dev/null +++ b/docusaurus/docs/networking/setup-metallb.md @@ -0,0 +1,282 @@ +--- +title: Setting up Metallb +--- + +To disable the default Service Load Balancer (ServiceLB) in a running K3s cluster, you can modify the K3s configuration to prevent it from automatically deploying the ServiceLB controller. This is necessary if you're installing a custom load balancer like MetalLB. + +Here are the steps to disable the ServiceLB in K3s: + +### 1. Edit the K3s Server Config + +You need to modify the K3s configuration to disable ServiceLB. To do this, you can either modify the `k3s.service` file (on systems using `systemd`) or provide the `--disable servicelb` flag when starting the K3s server binary. + +#### Option 1: Update the K3s systemd service + +1. Find the systemd service file for K3s. Typically, on a Linux system, it's located at `/etc/systemd/system/k3s.service`. You can confirm with: + +```sh +systemctl status k3s +``` + +2. Edit the service file: + +```sh +sudo nano /etc/systemd/system/k3s.service +``` + +3. Add the following option to the `ExecStart` line to disable the built-in ServiceLB: + +```sh +--disable servicelb +``` + +The modified `ExecStart` should look something like this: + +```sh +ExecStart=/usr/local/bin/k3s \ + server \ +--disable servicelb +``` + +4. Reload the `systemd` daemon, and restart K3s to apply the changes: + +```sh +sudo systemctl daemon-reload +sudo systemctl restart k3s + ``` + +#### Option 2: Edit `/etc/rancher/k3s/config.yaml` (Preferred for Persistent Changes) + +Alternatively, if you're using the K3s configuration file for persistent configuration, you can add the `--disable servicelb` flag into the `/etc/rancher/k3s/config.yaml` file: + +1. Edit the config file: + +```sh +sudo nano /etc/rancher/k3s/config.yaml +``` + +2. Add the following entry: + +```yaml +disable: + - servicelb +``` + +3. Save the file and restart K3s: + +```sh +sudo systemctl restart k3s +``` + +### 2. Remove Existing ServiceLB + +Once you've disabled the ServiceLB in your configuration and restarted K3s, you may also want to clean up any lingering instances of the ServiceLB already running. + +To do that, run the following command to remove the existing `svc/traefik` (default ServiceLB component) and `SvcLB` resources, if present: + +```sh +kubectl delete daemonset -n kube-system svclb-traefik +kubectl delete deployments -n kube-system traefik +``` + +Note: The resource name for the default load balancer may vary depending on your K3s setup, so check with the following command to get the precise resource names: + +```sh +kubectl get daemonsets -A +``` + +### 3. Install MetalLB + +Now that ServiceLB is disabled, you can safely install and configure MetalLB in your cluster. + +You can follow the official MetalLB documentation to deploy and configure it: + +```sh +kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.7/config/manifests/metallb-native.yaml +``` + +Afterward, make sure to configure an `IPAddressPool` and `L2Advertisement` to activate MetalLB for handling load balancer services. + + +### MetalLB with Traefik Ingress Controller + +In order to make **MetalLB** work with your **Traefik Ingress Controller** in a K3s cluster, you'll need to configure both components to properly handle `LoadBalancer` services for external traffic routing. Below is a step-by-step guide on how to do this: + +### Prerequisites: + +- K3s is running, and you’ve already disabled the default K3s **ServiceLB** as mentioned (with `--disable servicelb`). + +### Step 1: Configure MetalLB IP Address Pool + +1. **Identify an IP Range** that you want MetalLB to use for allocating external IP addresses. The IPs should come from a range within your local network that is not already in use by other devices. For example, you could designate a range like `192.168.1.240-192.168.1.250`. + +2. **Create the IPAddressPool and L2Advertisement** resources in Kubernetes**, so that MetalLB knows what IP addresses to manage. + +Create a YAML file (e.g., `metallb-config.yaml`) that defines the `IPAddressPool` and `L2Advertisement`: + +```yaml +apiVersion: metallb.io/v1beta1 +kind: IPAddressPool +metadata: + name: default-address-pool + namespace: metallb-system # Ensure this is the namespace where MetalLB is deployed +spec: + addresses: + - 192.168.1.240-192.168.1.250 # Example IP range, adjust to your network's range + +--- +apiVersion: metallb.io/v1beta1 +kind: L2Advertisement +metadata: + name: example + namespace: metallb-system +spec: + # This tells MetalLB to advertise the IPs at Layer 2 level (ARP/ND) +``` + +Apply the configuration: + +```sh +kubectl apply -f metallb-config.yaml +``` + +Make sure the assigned IP range is not being used by other devices on your local network, or you may run into IP conflicts. + +### Step 2: Update Traefik Service to Use `LoadBalancer` Type + +By default, Traefik may expose itself as a `ClusterIP` or a `NodePort` in some K3s setups. However, to leverage MetalLB for external access, you need to modify the Traefik service to be of type `LoadBalancer` so it can acquire an external IP. + +1. **Edit the Traefik Service**: + +You can modify the Traefik Service directly or update its Helm chart (if you used Helm for installation). + +If Traefik was installed as part of the K3s default installation, you can edit the `traefik` service directly: + +```sh +kubectl edit svc -n kube-system traefik +``` + +Look for the `type` field under the `spec` section in the service YAML definition and change it to `LoadBalancer`: + +```yaml +spec: + type: LoadBalancer +``` + +Your service update might look like: + +```yaml +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/instance: traefik + app.kubernetes.io/name: traefik + name: traefik + namespace: kube-system +spec: + externalTrafficPolicy: Cluster + ports: + - name: web + port: 80 + protocol: TCP + targetPort: 80 + - name: websecure + port: 443 + protocol: TCP + targetPort: 443 + selector: + app.kubernetes.io/instance: traefik + app.kubernetes.io/name: traefik + type: LoadBalancer # <-- This was changed from ClusterIP or NodePort +``` +2. **Save and Exit** the editor. + +### Step 3: Confirm Traefik Is Assigned a LoadBalancer IP From MetalLB + +After modifying Traefik’s service type to `LoadBalancer`, you need to check if MetalLB has assigned an external IP from the specified range. To do this, run the following command: + +```sh +kubectl get svc -n kube-system traefik +``` + +You should see something similar to: + +```bash +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +traefik LoadBalancer 10.43.123.45 192.168.1.240 80:31112/TCP,443:31777/TCP 12m +``` + +- `EXTERNAL-IP` should have a value from the IP pool you configured (e.g., `192.168.1.240`). + +- If the `EXTERNAL-IP` shows as ``, double-check that MetalLB has the correct IP pool configuration and that the nodes/pods can reach the network you specified. + +--- + +### Step 4: (Optional) Verify Traefik Ingress Functionality + +Test the Traefik ingress controller by creating an `Ingress` resource, which should be exposed externally via the LoadBalancer IP that MetalLB assigned. + +1. Create a simple test ingress: + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: test-ingress + namespace: default +spec: + rules: + - host: your.custom.domain # Or use an IP-based access + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: your-app-service # Replace this with a service that your app is running on + port: + number: 80 +``` + +2. Apply the `Ingress` resource: + +```sh +kubectl apply -f test-ingress.yaml +``` + +3. Ensure your `DNS` points `your.custom.domain` to the external IP (or access it with the IP address directly). + +4. You should be able to access your Traefik-ingressed service by navigating to `http://your.custom.domain` or `http://` in a browser. + +--- + +### Additional Considerations: +- **DNS**: Ensure you have your DNS properly configured to point the hostname you use in your Ingress definition to the external IP provided by MetalLB. +- **SSL/TLS**: If you plan to use `HTTPS`, you'll want to configure SSL termination on Traefik. This typically involves configuring Traefik with either self-signed certificates, **ACME Let's Encrypt**, or another certificate management setup. +- **Firewall**: Make sure your network firewall policies (if any) allow access to external clients for the allocated IP range in your MetalLB configuration. + +--- + +### Troubleshooting: + +1. **No External IP**: + - Make sure that MetalLB is configured correctly, and the IP range is valid in your local network. + - Verify that the MetalLB controller and speaker pods are running. + + ```bash + kubectl get pods -n metallb-system + ``` + + Check the logs of the MetalLB pods if you suspect issues: + + ```bash + kubectl logs -n metallb-system speaker-xxxxxxxxxxx + ``` + +2. **Invalid IP Range**: + - Double-check that the IP range you’ve reserved for MetalLB does not overlap with a DHCP-pool range or any IP address that’s already in use on your local network. + +3. **Ingress Routing Issues**: + - Verify the `Ingress` resource, and ensure that the service names and ports match correctly with your application. + - Validate Traefik's logs for any issues related to routing. \ No newline at end of file diff --git a/docusaurus/docs/networking/understanding-network-components.md b/docusaurus/docs/networking/understanding-network-components.md new file mode 100644 index 0000000..b0090ef --- /dev/null +++ b/docusaurus/docs/networking/understanding-network-components.md @@ -0,0 +1,119 @@ +--- +title: Kubernetes Networking +--- + +#### What is Network Load Balancing? + +Network load balancing is a critical process that distributes incoming network traffic across multiple backend servers or pods. This distribution ensures that no single server becomes overloaded, enhancing application response times, availability, and fault tolerance. It's like directing vehicles on a highway to different lanes to avoid congestion. + +#### Load Balancer + +A load balancer operates as a [gateway](https://en.wikipedia.org/wiki/Gateway_(telecommunications)) that receives incoming requests and decides how to distribute them across the available servers. This ensures [high availability](https://en.wikipedia.org/wiki/High_availability) and [reliability](https://en.wikipedia.org/wiki/Reliability_engineering) by balancing the load and increasing [failover](https://en.wikipedia.org/wiki/Failover) capabilities. + +#### MetalLB in Kubernetes + +- **Purpose**: Designed for environments like Raspberry Pi clusters, MetalLB provides network load balancing for bare-metal Kubernetes settings that lack a built-in cloud LoadBalancer service. +- **Functionality**: MetalLB assigns external IP addresses to services, enabling your Kubernetes cluster to be externally accessible and allowing traffic to reach your cluster services efficiently. + +#### Ingress Controllers (Traefik, NGINX) + +- **Purpose**: Ingress controllers manage HTTP and HTTPS traffic, facilitating external access to internal Kubernetes services. +- **Functionality**: They route incoming requests based on specified rules such as domain names or URL paths. For instance: + - Requests to `api.example.com` might be routed to a backend API service. + - Requests to `www.example.com` could be directed to a frontend service. + +#### Port Forwarding + +- **Purpose**: Port forwarding acts as a direct pathway from your local machine to a pod within the cluster, bypassing more complex routing like ingress. +- **Use Case**: It's particularly useful for development and debugging, allowing developers to connect directly to specific pods. + +### Integrating [MetalLB](https://metallb.universe.tf/) and Ingress Controllers + +[MetalLB](https://metallb.universe.tf/) is not mutually exclusive with ingress controllers. Instead, they can work together. [MetalLB](https://metallb.universe.tf/) can provide an external IP address for your services, allowing your ingress controller (like Traefik or NGINX) to route incoming traffic to various services in your cluster. + +1. **[MetalLB](https://metallb.universe.tf/)**: + - Provides external IPs to the LoadBalancer services, thereby making them accessible from outside the cluster. + +2. **Ingress Controller**: + - Utilizes the IPs provided by [MetalLB](https://metallb.universe.tf/) to manage routing of incoming HTTP/HTTPS requests. It's configured through ingress resources, which dictate traffic handling: + - **Domain-Based Routing**: Traffic can be directed to services based on the domain accessed. + - **Path-Based Routing**: Specific URL paths can point to distinct services. + +### Kubernetes ClusterIP vs NodePort vs LoadBalancer vs Ingress? + +Let's explore Kubernetes service types, ClusterIP, NodePort, LoadBalancer, and Ingress, and explain when to use each one and how they can work together. + +#### ClusterIP + +**What It Is:** +- ClusterIP is the default Kubernetes service type that exposes a service on a cluster-internal IP. This service type is only accessible within the cluster. + +**Use Cases:** +- **Internal Communication**: Ideal for services that only need to communicate with other services within the cluster (e.g., microservices architecture). +- **Backend Services**: Suitable for databases or back-end services that should not be accessed directly from the outside. + +**Pros:** +- Provides a simple way to manage internal services without exposing them to the outside. +- Reduces security risks by limiting external access. + +**Cons:** +- Not suitable for direct access from outside the cluster. + +#### NodePort + +**What It Is:** +- NodePort exposes a service on each node's IP at a specific port. It creates a static port on each node and forwards traffic to your service. + +**Use Cases:** +- **Local Development**: Ideal for development environments or testing purposes where simple access is needed. +- **Small-Scale Applications**: When running a small or non-critical setup where direct node access is required. +- **Debugging**: Suitable for scenarios where quick access to a service from an external source is necessary for troubleshooting. + +**Pros:** +- Simple to set up and does not require external infrastructure like a load balancer. + +**Cons:** +- Requires manual management of ports, which can become complex in larger environments. +- Not ideal for high availability since traffic can overwhelm a single node. + +#### LoadBalancer + +**What It Is:** +- LoadBalancer creates an external load balancer in supported environments, assigning an external IP address to your service. MetalLB can simulate this in bare metal environments like those with Raspberry Pis. + +**Use Cases:** +- **Production-Ready Applications**: When you need stable IP addresses and external access in production environments. +- **Auto-Scaling Needs**: In scenarios where you need automatic distribution of traffic across pods without manual management. + +**Pros:** +- Provides a single, stable point of access. +- Managed traffic distribution across multiple nodes. + +**Cons:** +- Can be costly in some cloud environments due to resource usage. +- Simpler than Ingress, hence does not support HTTP-level routing or SSL termination. + +#### Ingress + +**What It Is:** +- Ingress manages external access to services within a cluster, typically HTTP/HTTPS. Ingress controllers handle routing rules and can offer additional functionality like SSL termination and host/path-based routing. + +**Use Cases:** +- **Complex Applications**: Suitable for environments with multiple services that require sophisticated routing based on domains or paths. +- **Unified Entry Point**: When you want to consolidate access through a single, manageable endpoint. +- **Secure Connections**: Supports SSL termination, critical for secure communication over the web. + +**Pros:** +- Offers rich routing features and flexibility. +- Can manage multiple services through a single gateway. + +**Cons:** +- Requires additional configuration and management. +- Initial setup can be complex and requires understanding of ingress rules and controllers. + +### Recommendations + +- **Choose ClusterIP for**: Services only needing internal communication within the cluster. +- **Choose NodePort for**: Simplicity in small, non-production setups where each node's IP can handle incoming requests. +- **Choose LoadBalancer for**: Production needs in cloud environments or using MetalLB for external IP management in bare-metal scenarios. +- **Choose Ingress for**: Complex routing logic, SSL support, and environments requiring a unified interface for multiple services. \ No newline at end of file diff --git a/docusaurus/docs/storage/setup-longhorn-dashboard.md b/docusaurus/docs/storage/setup-longhorn-dashboard.md new file mode 100644 index 0000000..c8e6e7a --- /dev/null +++ b/docusaurus/docs/storage/setup-longhorn-dashboard.md @@ -0,0 +1,110 @@ +--- +title: Expose Longhorn Dashboard using Traefik Ingress +--- + +### Identify the Longhorn Dashboard Service + +When Longhorn is installed, it comes with a **Service** called `longhorn-frontend`. This service manages access to the Longhorn dashboard, which is used for monitoring and managing Longhorn volumes. + +You can verify the service by running: + +```bash +kubectl get svc -n longhorn-system +``` + +Look for the **`longhorn-frontend`** service in the output, which typically looks like this: + +```plaintext +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +longhorn-frontend ClusterIP 10.43.75.105 80/TCP 4m +``` + +### Create an Ingress Resource to Expose the Dashboard + +We will use **Traefik Ingress** to expose the Longhorn dashboard so that it is accessible via a browser. + +#### Ingress YAML Configuration + +Create a YAML file (e.g., `longhorn-ingress.yaml`) with the following configuration: + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: longhorn-ingress + namespace: longhorn-system +spec: + ingressClassName: traefik # We use Traefik as the ingress controller + rules: + - host: longhorn.local.host # The domain by which we'll access Longhorn UI + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: longhorn-frontend # Service managing Longhorn dashboard + port: + number: 80 # Service port where Longhorn UI runs +``` + +This configuration does the following: + +- It tells Traefik to expose the **`longhorn-frontend`** service (which runs the dashboard) under the host `longhorn.local.host`. + +- HTTP traffic to this host will be routed to the Longhorn dashboard running on port 80. + +#### Apply the Ingress Resource: + +After creating the YAML file, apply it to your cluster: + +```bash +kubectl apply -f longhorn-ingress.yaml +``` + +### Configure Your `/etc/hosts` File for Local Access + +Since this is for local testing, we need to map the domain `longhorn.local.host` to the IP address of your Kubernetes cluster. We'll achieve this by updating your **`/etc/hosts`** file to resolve requests for `longhorn.local.host` to your cluster node's IP. + +#### Get the Cluster Node's IP: + +Find the IP address of your cluster node (where Traefik or your load balancer is running). You can often use `kubectl get nodes -o wide` to retrieve the IP address. It might look something like this: + +```bash +kubectl get nodes -o wide +``` + +```plaintext +NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME +node-master Ready master,control-plane 12d v1.22.2 192.168.1.100 Ubuntu 20.04.3 LTS 5.4.0-89-generic docker://20.10.8 +node-worker Ready worker 11d v1.22.2 192.168.1.101 Ubuntu 20.04.3 LTS 5.4.0-89-generic docker://20.10.8 +``` + +In this example, suppose Traefik is running on the master node, which has the IP `192.168.1.100`. + +#### Update `/etc/hosts`: + +Edit the `/etc/hosts` file on your local machine using a text editor (e.g., `vim`, `nano`, etc.). + +```bash +sudo nano /etc/hosts +``` + +Add the following entry, replacing **`192.168.1.100`** with your node's IP: + +```plaintext +192.168.1.100 longhorn.local.host +``` + +This entry ensures that when you open `http://longhorn.local.host` in your browser, it will route the traffic to your cluster. + + +### Access the Longhorn Dashboard + +Now that your ingress is configured and your `/etc/hosts` is updated, you should be able to access the Longhorn dashboard by navigating to: + +``` +http://longhorn.local.host +``` + +The Longhorn UI should load in your browser, allowing you to manage your Longhorn volumes and nodes. \ No newline at end of file diff --git a/docusaurus/docs/storage/setup-longhorn.md b/docusaurus/docs/storage/setup-longhorn.md new file mode 100644 index 0000000..b29291f --- /dev/null +++ b/docusaurus/docs/storage/setup-longhorn.md @@ -0,0 +1,186 @@ +--- +title: Setup Longhorn +--- + +#### Download the Longhorn Manifest YAMLs + +Longhorn's manifest files are available in their GitHub repository. You can apply them directly to the Kubernetes cluster: + +```bash +kubectl apply -f https://raw.githubusercontent.com/longhorn/longhorn/master/deploy/longhorn.yaml +``` + +This command will pull the entire Longhorn deployment YAML, which configures everything Longhorn requires inside the `longhorn-system` namespace. + +#### Monitor the Deployment Progress + +After applying the manifest, you'll see various Kubernetes objects like Pods, Services, DaemonSets, and CRDs being created. You can monitor them with the following command: + +```bash +kubectl get all -n longhorn-system +``` + +Especially watch the status of the Pods. + +It will take a couple of minutes for all required components to pull the images from the Docker registry, configure themselves, and become ready. + +#### Verify Custom Resource Definitions (CRDs) + +Longhorn uses Custom Resource Definitions (CRDs) for managing and storing information about volumes, nodes, and engines. + +Check if the Longhorn CRDs have been installed properly: + +```bash +kubectl get crds | grep longhorn +``` + +You should see a list of Longhorn-related CRDs like: + +- `instancemanagers.longhorn.io` +- `volumes.longhorn.io` +- `nodes.longhorn.io` +- `replicas.longhorn.io` +- and others. + +These CRDs are the foundation of Longhorn's integration into your Kubernetes cluster. + +#### Verify Longhorn Components (Pods, DaemonSet) + +Ensure that all Longhorn components are running (Pods and DaemonSet) using: + +```bash +kubectl get pods -n longhorn-system +``` + +You should see Longhorn pods running, like: + +- `longhorn-manager-{pod-name}` +- `longhorn-instance-manager-{pod-name}` +- `longhorn-ui-{pod-name}` +- `longhorn-driver-deployer-{pod-name}` + +Additionally, verify that the `longhorn-manager` DaemonSet has pods on **every node** in your cluster, as it’s responsible for managing Longhorn processes on each node: + +```bash +kubectl get ds -n longhorn-system +``` + +Check that the DaemonSet has `Desired` pods on all your nodes, and `Current` matches the desired pod count. + +### Accessing the Longhorn UI + +Longhorn provides a web-based UI for managing your storage. To access it, you will need to expose its service. We cover this in the next section ["Expose Longhorn Dashboard using Traefik Ingress"](setup-longhorn-dashboard). + +### Configure Nodes for Longhorn Storage + +Longhorn automatically recognizes your Kubernetes nodes, but you may want to configure how disks on your nodes are used for storage. + +You can do this through the **Longhorn UI** under the **Node & Disk** section. Here you can: + +- Determine how much space is allocated on each node. +- Specify custom directories for disk storage (e.g., `/mnt/disk` instead of default paths). +- Set replication factors (i.e., how many copies of a volume will be stored across nodes). + +### Test Longhorn - Creating a PVC + +Let’s verify that Longhorn is working by creating a test Persistent Volume Claim (PVC). Here’s how you can create a StorageClass and a sample PVC. + +#### Create the Longhorn StorageClass + +To create a `StorageClass` for Longhorn, you need to define one so that Longhorn can dynamically provision volumes. You can use the default settings, but feel free to customize, especially the number of replicas depending on how many nodes you have. + +Create a file named `longhorn-storageclass.yaml`: + +```yaml +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: longhorn +provisioner: driver.longhorn.io +parameters: + numberOfReplicas: "2" + staleReplicaTimeout: "30" +allowVolumeExpansion: true +reclaimPolicy: Retain +volumeBindingMode: Immediate +``` + +Then apply this StorageClass: + +```bash +kubectl apply -f longhorn-storageclass.yaml +``` + +#### Create a PVC Using Longhorn + +Now create a sample Persistent Volume Claim (PVC) to test that Longhorn can provision volumes: + +Create a `longhorn-pvc.yaml` file: + +```yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: longhorn-pvc +spec: + accessModes: + - ReadWriteOnce + storageClassName: longhorn + resources: + requests: + storage: 2Gi +``` + +Apply the PVC: + +```bash +kubectl apply -f longhorn-pvc.yaml +``` + +Check the status of the PVC: + +```bash +kubectl get pvc +``` + +Once it’s `Bound`, you know Longhorn successfully provisioned your storage. + +#### Optionally Deploy a Pod Using the PVC + +To further verify the PVC is working, you can deploy a simple pod (for example, the NGINX web server) that mounts the Longhorn volume: + +Create a simple `nginx-pod.yaml` file: + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: nginx +spec: + containers: + - image: nginx + name: nginx + volumeMounts: + - name: data + mountPath: /usr/share/nginx/html + volumes: + - name: data + persistentVolumeClaim: + claimName: longhorn-pvc +``` + +Apply the pod: + +```bash +kubectl apply -f nginx-pod.yaml +``` + +Once the pod is running, Longhorn storage is working as expected. + +### Monitor Longhorn + +Longhorn offers monitoring and management tools (both in the UI and via the CLI) to track the status of volumes, nodes, and replicas. + +Key areas to check: +- **Volumes**: Make sure volumes are healthy and properly replicated. +- **Replicas**: Ensure replicas are collaborating across your cluster nodes to ensure data redundancy. \ No newline at end of file diff --git a/docusaurus/docs/storage/understanding-longhorn-concepts.md b/docusaurus/docs/storage/understanding-longhorn-concepts.md new file mode 100644 index 0000000..267fa1a --- /dev/null +++ b/docusaurus/docs/storage/understanding-longhorn-concepts.md @@ -0,0 +1,100 @@ +--- +title: Kubernetes Storage +--- + +### StorageClass - What is it? + +Think of a **StorageClass** in Kubernetes as a **"recipe"** or **blueprint** that dictates **how to create storage** for your application. + +In Kubernetes, you often need to store data (like database data, logs, etc.), and different applications might need different types of storage (some want fast, some want big, some want highly replicated storage). To solve this, Kubernetes uses **StorageClasses** to define how **storage should be provisioned**. + +- A **StorageClass** describes the **type of storage**: It can be based on the **speed**, **redundancy**, **storage provider/driver**, or **other properties**. +- Once defined, the StorageClass allows Kubernetes to automatically give the right kind of storage to any service (`Pods`) that asks for it. + +So, instead of worrying about *how* to create storage for your specific application, you just pick a StorageClass, and Kubernetes takes care of the rest (i.e., Kubernetes sends the request to the configured storage system). + +#### Simple Analogy for a StorageClass + +Imagine you're at a **pizza restaurant.** You want to order a pizza, but you don't care about how the kitchen makes it, you just describe the type of pizza you want by **selecting a predefined option** on the menu: + +- Regular crust +- Thin crust +- Extra cheese + +The kitchen (in this case, Kubernetes) **knows how to create** the pizza based on those instructions. + +In the Kubernetes world, the **"crust and cheese options"** represent different types of storage like Longhorn, AWS EBS, Google Persistent Disks, SSDs, etc. + +### PersistentVolumeClaim (PVC) - What does it do? + +A **PersistentVolumeClaim (PVC)** is your way of asking for a specific amount of storage from Kubernetes. It’s kind of like saying, "**Hey, I need 10 GB of storage that I can use reliably and persistently**." + +- A **PVC** is a request for storage: In the PVC, you specify **how much storage** you need (e.g., 10 GB or 50 GB), and **what kind of access** you need (e.g., read only or read/write). + +- The PVC gets "matched" to a **PersistentVolume** (an actual piece of storage) through the **StorageClass** you define. Once this happens, Kubernetes guarantees that the storage is reserved and available for your application (even if the pod is deleted or recreated). + +In simpler terms, imagine your PVC as a **rental request form**. You fill it out, specifying how much storage (like how much "house space" you need) and what type of house (StorageClass) you're asking for. Once Kubernetes finds matching storage (PersistentVolume), it gives you the key to that "house" (or disk) to use. + +So, the **PVC connects you to that storage**, and you can now use it for your application's data. + +#### Simple Analogy for a PVC + +Let's go back to the **pizza restaurant** analogy. Your **PVC** is kind of like saying: + +- "*I want a pizza that’s **12 inches large**, and it should be **thin crust**!*" + +When you make this request (PVC), the restaurant (Kubernetes) will: +1. Look at its "menu" (StorageClasses) and find the right recipe or profile that matches your request. +2. Bake a pizza based on that recipe (allocate PersistentVolume). +3. Serve it to you (PVC is *bound* to the actual storage). + +So, whenever you create a **PVC**, it will “claim” a matching **PersistantVolume** from Kubernetes, ensuring that your "requested storage" is available and bound to you for the data needs for your app. + +### Putting It Together + +1. **StorageClass** == A **blueprint (recipe)** that defines how to provision a specific type of storage (e.g., fast disk, replicated storage, etc.). + +2. **PersistentVolumeClaim (PVC)** == **A request** for storage. It says, "*I need X amount of storage handled in Y way*", and then Kubernetes matches it with the right type of storage based on the **StorageClass**. + +### Real Example + +Let's say you're deploying a **MySQL database** in your Kubernetes cluster. It's going to need some disk space to store data. + +1. First, you'll define a **StorageClass** to tell Kubernetes where the storage should come from and what kind it should be (e.g., using Longhorn for local replicated storage). + + ```yaml + apiVersion: storage.k8s.io/v1 + kind: StorageClass + metadata: + name: longhorn + provisioner: driver.longhorn.io + parameters: + numberOfReplicas: "2" + staleReplicaTimeout: "30" + allowVolumeExpansion: true + reclaimPolicy: Retain + volumeBindingMode: Immediate + ``` + +2. Next, you'll make a **PersistentVolumeClaim (PVC)** that asks for, say, **5 GB** of storage that uses this **longhorn** StorageClass. + + ```yaml + apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + name: mysql-data + spec: + accessModes: + - ReadWriteOnce + storageClassName: longhorn + resources: + requests: + storage: 5Gi + ``` + +Once the PVC is created, Kubernetes finds storage according to the `longhorn` recipe and gives you **5 GB** of storage. Now your MySQL pod can use that storage to save data files or your database. + +#### Summary: + +- **StorageClass**: The blueprint that defines what type of storage to give when storage is requested (e.g., fast SSD storage, networked storage, etc.). +- **PersistentVolumeClaim (PVC)**: A request for a specific amount of storage based on the criteria defined in the StorageClass (like *"I need 10 GB of disk space on this class of storage!"*). \ No newline at end of file diff --git a/docusaurus/docs/terminology.md b/docusaurus/docs/terminology.md new file mode 100644 index 0000000..a6033bf --- /dev/null +++ b/docusaurus/docs/terminology.md @@ -0,0 +1,17 @@ +--- +title: Terminology +--- + +This course covers much more than Kubernetes, touching on storage, networking, security, and many other topics. To fully understand the material, it's important to learn a wide range of concepts. While this might seem challenging, it also makes the process satisfying and rewarding. + +In my experience as a teacher and professional, a common mistake people make when learning is not taking the time to pause and understand new terms or concepts. Instead, they rush ahead, which can weaken their foundation and make it harder to build a deep understanding later. + +As you go through this course, give yourself the time and patience to pause whenever you come across a new protocol or term. At the very least, take a moment to understand it on a basic level so you can confidently move forward. + +In today’s world, it’s easy to copy and paste solutions, especially with tools like AI. However, this can lead us to skip the important step of going back to understand the basics. + +--- + +### Swap Memory + +[Swap memory](https://serverfault.com/questions/48486/what-is-swap-memory), also known as swap space, is a portion of a computer's hard drive or SSD that acts as an extension of RAM. When RAM is full, the operating system moves inactive data from RAM to the swap space, freeing up RAM for active tasks and improving system performance. Accessing data in swap is slower than accessing data in RAM, but it prevents the system from crashing when it runs out of RAM. diff --git a/docusaurus/docs/welcome.md b/docusaurus/docs/welcome.md new file mode 100644 index 0000000..3c1de14 --- /dev/null +++ b/docusaurus/docs/welcome.md @@ -0,0 +1,29 @@ +--- +title: Building a "Mini Data Center" with K3S +--- + + + +import DataCenterOverview from "@site/src/components/DataCenterOverview"; + + + +Welcome! I'm glad you're here. Hail to the SEO overlords! + +If we haven't met yet, my name is Alexander. I've been in IT for over 20 years and professionally active since 2007. Over the years, I've worked as a CISCO engineer, a senior full-stack developer, a startup co-founder, and, for the past few years, a team and tech lead. You can check out my [LinkedIn profile](https://www.linkedin.com/in/aleksandar-grbic-74670263/) if you want to know more about my background. + +I think it's important to share a bit about myself so you can decide if I'm the right person to guide you. + +I hope this guide to building a home "mini data center", from hardware and networking to K3s and real workloads is helpful. If you have any questions or suggestions, feel free to reach out on GitHub or join me on one of my live streams. + +Now, let's get started. + +With so much information available today, it's worth asking yourself, _"Why am I building this?"_ or _"Why am I learning these skills?"_ + +Why choose this project, setting up your own infrastructure, networking, and orchestration, when there are so many other things you could focus on? How will it help your career? Will it solve any real problems you're facing, or are you just doing it because it seems popular or trendy? These are the kinds of questions you should think about before committing to something new. + +Many of you might know me from [Twitch](https://www.twitch.tv/programmer_network) or [YouTube](https://www.youtube.com/@programmer-network). I prefer to keep things practical and honest. I'm not a fan of the influencer mindset where people recommend things just to fit their content strategy, especially when they don't actually use the tools or techniques they're promoting. + +So, when someone asks, _"Why should I build a home lab or learn Kubernetes?"_, it's a valid question. I could give you plenty of reasons, but not all of them might matter to you. Maybe this isn't the right time for you to dive into this. Maybe there are other skills that would be more useful for where you are in your career right now. + +It's important to understand the difference between things that are interesting to explore and things that are genuinely valuable to learn. Everyone has different needs depending on their goals, so take the time to think about whether this journey is what you need at this moment. diff --git a/docusaurus/docs/what-we-will-learn.md b/docusaurus/docs/what-we-will-learn.md new file mode 100644 index 0000000..92af6b5 --- /dev/null +++ b/docusaurus/docs/what-we-will-learn.md @@ -0,0 +1,77 @@ +--- +title: Outcome +--- + +Whenever I'm learning something, I try to think about the outcome first. That's where I usually begin. It's similar to building a product. As Steve Jobs once said, "Start with the customer, and the problem, then work backwards to the technology." + +I started learning Kubernetes because I wanted to enable myself to provision infrastructure with minimal effort. If I want to start building a new product and "ship fast, fail fast," infrastructure should be an afterthought. I don't want to spend time thinking about it. I should just write a few small YAML files, hit deploy, and be good to go. I shouldn't have to worry about networking, hard drives, CPUs, Logging etc. I just need an interface where I can say, "Hey, I want XYZ, give it to me. + +With that in mind, this will be the outcome of this course: By the end, we'll have the ability to provision a full-stack application infrastructure in under five minutes. And to top it off, it'll cost us next to nothing, as the only expenses will be the initial investment in the bare-metal server, plus about 5 euros per month for electricity. + +```mermaid +%%{init: { 'theme': 'dark' } }%% +flowchart TB + subgraph ControlPlane["K3s HA Control Plane"] + RPI1["Raspberry Pi 1 (Control)"] + RPI2["Raspberry Pi 2 (Control)"] + RPI3["Raspberry Pi 3 (Control)"] + end + subgraph RPI4["Raspberry Pi 4 (Worker)"] + api_pod1["API Pod"] + pg_pod1["CloudNative PG Pod"] + longhorn1["Longhorn (Storage)"] + end + subgraph HP["HP EliteDesk (Worker)"] + api_pod2["API Pod"] + pg_pod2["CloudNative PG Pod"] + longhorn2["Longhorn (Storage)"] + end + subgraph Lenovo["Lenovo ThinkCentre (Worker)"] + api_pod3["API Pod"] + pg_pod3["CloudNative PG Pod"] + longhorn3["Longhorn (Storage)"] + end + + external["Internet"] --> metallb["MetalLB (Load Balancer)"] + metallb --> traefik["Traefik (Ingress Controller)"] + + traefik --> api_pod1 & api_pod2 & api_pod3 + + api_pod1 -.-> pg_pod1 + api_pod2 -.-> pg_pod2 + api_pod3 -.-> pg_pod3 + + pg_pod1 -.-> longhorn1 + pg_pod2 -.-> longhorn2 + pg_pod3 -.-> longhorn3 + + ControlPlane -. "manages" .-> RPI4 + ControlPlane -. "manages" .-> HP + ControlPlane -. "manages" .-> Lenovo + + %% --- Styling to Match Screenshot --- + + %% Define reusable styles for nodes + classDef default fill:#121212,stroke:#ccc,color:#fff + classDef ingressNode fill:#121212,stroke:#9c27b0 + classDef controlNode fill:#121212,stroke:#e65100 + classDef workerGreenPod fill:#121212,stroke:#2e7d32 + classDef workerYellowPod fill:#121212,stroke:#f9a825 + classDef workerBluePod fill:#121212,stroke:#1565c0 + + %% Apply styles to nodes + class metallb,traefik ingressNode; + class RPI1,RPI2,RPI3 controlNode; + class api_pod1,pg_pod1,longhorn1 workerGreenPod; + class api_pod2,pg_pod2,longhorn2 workerYellowPod; + class api_pod3,pg_pod3,longhorn3 workerBluePod; + + %% Style the subgraph containers + style ControlPlane stroke:#e65100,stroke-width:2px,fill:transparent + style RPI4 stroke:#2e7d32,stroke-width:2px,fill:transparent + style HP stroke:#f9a825,stroke-width:2px,fill:transparent + style Lenovo stroke:#1565c0,stroke-width:2px,fill:transparent + + %% Style the dotted "manages" links + linkStyle 11,12,13 stroke:#e65100,stroke-width:2px,stroke-dasharray: 5 5 +``` diff --git a/docusaurus/docs/why-is-it-hard.md b/docusaurus/docs/why-is-it-hard.md new file mode 100644 index 0000000..1703795 --- /dev/null +++ b/docusaurus/docs/why-is-it-hard.md @@ -0,0 +1,17 @@ +--- +title: The Challenges of Building a Mini Data Center +--- + +One of the hardest things about building a mini data center isn't any single technology or component. Many people often find themselves learning individual pieces - like networking, hardware setup, or Kubernetes - without seeing how everything fits together. And while that fragmented approach is challenging, it's not the biggest obstacle. + +The biggest challenge is building something comprehensive and then letting it gather dust. In the context of our mini data center, if you end up going through this guide and then just abandoning your setup, you will find yourself forgetting crucial skills really fast - from MikroTik networking configurations to storage management and container orchestration. The advantage that I believe this entire guide enforces is that we are actually building an infrastructure that we intend to use daily. It's our mini data center that we are excited to maintain, covering everything from physical hardware and L2 networking to distributed storage, container orchestration, and running our applications. + +Clearly, you can also look at this complexity as a massive disadvantage, and to answer that concern, we really need to get back to [why](./why.md) and understand what we are getting out of this journey. + +So, to make this learning experience manageable, we need to ensure that: + +- We are building this as our primary infrastructure that we will actively use +- We understand that a mini data center requires regular maintenance across all layers - from hardware and networking to software - and we're enthusiastic about that +- We accept that components will fail at some point - whether it's a Raspberry Pi, a network switch, or a software deployment - and we see these as valuable learning opportunities +- We view this complete setup as an investment in our technical growth, keeping our full-stack infrastructure skills sharp +- We recognize that having hands-on experience with every layer of modern infrastructure, especially in the age of AI, helps us stay relevant and adaptable in the job market diff --git a/docusaurus/docs/why.md b/docusaurus/docs/why.md new file mode 100644 index 0000000..a75738b --- /dev/null +++ b/docusaurus/docs/why.md @@ -0,0 +1,41 @@ +--- +title: Why Build a Home Mini Data Center? +--- + +Answering this question honestly, for each of us individually, is fundamental because it will determine the outcome and likelihood of success or failure. If you're building something just because you heard about it from the latest content creator, or because "everyone seems to be doing it," you're already off to a poor start. + +What I've learned over the last two decades of my career is that if you're wondering whether you need something, you probably don't. These things usually come naturally as a result of the problems you encounter. The problem itself often guides you to the right tools, just like this one. + +Rather than getting too deep into philosophy, I'll share my personal reasons for building a complete mini data center at home - from setting up the physical rack and hardware (Raspberry Pis and Mini PCs), to configuring MikroTik networking equipment, and deploying Kubernetes: + +### End-to-End Infrastructure Knowledge + +As my career moves towards a CTO role, having **end-to-end** knowledge of infrastructure is essential - from physical hardware and networking to container orchestration and application deployment. Understanding the complete technology stack inside out will enable me to make better decisions and interface more effectively with stakeholders, investors, and engineering teams. + +### Efficient Infrastructure Provisioning + +I want to be able to provision infrastructure in under 5 minutes while thinking as little about it as possible. My focus should be on building products, not managing infrastructure. Additionally, the infrastructure needs to remain **cost-efficient**. Right now, I'm paying **24 euros a month** for Hetzner, which equals **about 300 euros a year**. Within two years, the hardware for my own mini data center (which I've already paid for upfront) will have effectively paid for itself through the savings. + +### Complete Control Over the Stack + +Building a mini data center gives me full control over every aspect of my infrastructure: + +- **Hardware**: Choice of Raspberry Pis and Mini PCs for different workloads +- **Networking**: Custom MikroTik setup for advanced networking capabilities +- **Storage**: Dedicated storage solutions +- **Orchestration**: Kubernetes (K3s) for container management +- **Security**: End-to-end control over security measures + +### Keeping My Skills Sharp + +Maintaining a complete data center forces me to regularly **maintain** and upgrade various components - from hardware and networking to software and orchestration. This helps me retain and sharpen the technical skills I've developed over time but might not frequently use in my day-to-day work. By continuing to use these skills, I ensure they remain active and relevant. + +### I see the future of infrastructure leaning towards on-premises + +As hardware becomes more efficient and cheaper to acquire, I predict that the future of infrastructure won't be `cloud-only` as it may seem now. While cloud solutions like GCP, AWS, and Azure are incredibly powerful, they come with high costs (even though they often save your engineers time) and may raise privacy concerns for some companies. As fiber-optic internet becomes more globally accessible and hardware costs continue to decrease, I believe that more companies will be confident enough to shift back to hosting on their own infrastructure, especially since modern tools make it significantly easier to manage and scale. + +### What Are Your Reasons? + +You now know my reasons for building a complete mini data center at home. It's essential for you to either find yourself in some of these points or come up with solid reasons of your own. I highly suggest having an internal discussion with yourself to figure out whether there's value for you in undertaking this journey. + +Learning new things should never be something we question; we should always strive to learn. The real question is: _Should I invest time in building and maintaining a mini data center_, or would my time be better spent learning another skill that will provide immediate value? diff --git a/docusaurus/docusaurus.config.ts b/docusaurus/docusaurus.config.ts new file mode 100644 index 0000000..65a5062 --- /dev/null +++ b/docusaurus/docusaurus.config.ts @@ -0,0 +1,91 @@ +import type * as Preset from "@docusaurus/preset-classic"; +import type { Config } from "@docusaurus/types"; +import { themes as prismThemes } from "prism-react-renderer"; + +const config: Config = { + title: "Learn K3S", + tagline: "Building a home mini data center using K3s, Mikrotik, and more", + favicon: "img/favicon.ico", + markdown: { + mermaid: true, + }, + themes: ["@docusaurus/theme-mermaid"], + + // Set the production url of your site here + url: "https://k3s.guide/", + // Set the // pathname under which your site is served + // For GitHub pages deployment, it is often '//' + baseUrl: "/", + presets: [ + [ + "classic", + { + docs: { + sidebarPath: "./sidebars.ts", + }, + theme: { + customCss: "./src/css/custom.css", + }, + } satisfies Preset.Options, + ], + ], + + plugins: [ + [ + "@docusaurus/plugin-ideal-image", + { + quality: 70, + max: 1030, // max resized image's size. + min: 640, // min resized image's size. if original is lower, use that size. + steps: 2, // the max number of images generated between min and max (inclusive) + disableInDev: false, + }, + ], + "./src/plugins/tailwind-config.js", + ], + + themeConfig: { + image: "img/mini-data-center-social-card.jpg", + navbar: { + title: "K3s.guide", + logo: { + alt: "K3s.guide", + src: "img/programmer-network-logo.svg", + }, + items: [ + { + href: "https://github.com/agjs", + label: "GitHub", + position: "right", + }, + { + href: "https://www.twitch.tv/programmer_network", + label: "Twitch", + position: "right", + }, + { + href: "https://www.youtube.com/@programmer-network", + label: "YouTube", + position: "right", + }, + { + href: "https://programmer.network", + label: "Programmer Network", + position: "right", + }, + ], + }, + prism: { + additionalLanguages: ["bash"], + theme: prismThemes.ultramin, + darkTheme: prismThemes.gruvboxMaterialDark, + }, + colorMode: { + defaultMode: "dark", // Set dark mode as the default + disableSwitch: false, // Keep the theme switcher toggle (optional) + respectPrefersColorScheme: true, // Ignore the user's system preference + }, + } satisfies Preset.ThemeConfig, +}; + +export default config; diff --git a/docusaurus/package.json b/docusaurus/package.json new file mode 100644 index 0000000..5370e80 --- /dev/null +++ b/docusaurus/package.json @@ -0,0 +1,61 @@ +{ + "name": "k-3-s", + "version": "0.0.0", + "private": true, + "scripts": { + "docusaurus": "docusaurus", + "start": "docusaurus start", + "build": "docusaurus build", + "swizzle": "docusaurus swizzle", + "deploy": "docusaurus deploy", + "clear": "docusaurus clear", + "serve": "docusaurus serve", + "write-translations": "docusaurus write-translations", + "write-heading-ids": "docusaurus write-heading-ids", + "typecheck": "tsc" + }, + "dependencies": { + "@docusaurus/core": "3.8.1", + "@docusaurus/plugin-ideal-image": "3.8.1", + "@docusaurus/preset-classic": "3.8.1", + "@docusaurus/theme-mermaid": "3.8.1", + "@heroicons/react": "2.2.0", + "@mdx-js/react": "3.1.0", + "chart.js": "4.5.0", + "clsx": "2.1.1", + "prism-react-renderer": "2.4.1", + "react": "19.1.0", + "react-dom": "19.1.0", + "react-image-gallery": "1.4.0", + "react-tooltip": "5.29.1", + "sharp": "0.34.2" + }, + "devDependencies": { + "@docusaurus/module-type-aliases": "3.8.1", + "@docusaurus/tsconfig": "3.8.1", + "@docusaurus/types": "3.8.1", + "@tailwindcss/postcss": "4.1.10", + "postcss": "8.5.6", + "tailwindcss": "4.1.10", + "typescript": "5.8.3" + }, + "browserslist": { + "production": [ + ">0.5%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 3 chrome version", + "last 3 firefox version", + "last 5 safari version" + ] + }, + "engines": { + "node": ">=22.16.0", + "npm": ">=11.4.2" + }, + "overrides": { + "postcss-selector-parser": "7.1.0" + } +} \ No newline at end of file diff --git a/docusaurus/sidebars.ts b/docusaurus/sidebars.ts new file mode 100644 index 0000000..5f0fb50 --- /dev/null +++ b/docusaurus/sidebars.ts @@ -0,0 +1,336 @@ +import type { SidebarsConfig } from "@docusaurus/plugin-content-docs"; + +// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) + +/** + * Creating a sidebar enables you to: + - create an ordered group of docs + - render a sidebar for each doc of that group + - provide next/previous navigation + + The sidebars can be generated from the filesystem, or explicitly defined here. + + Create as many sidebars as you want. + */ +const sidebars: SidebarsConfig = { + tutorialSidebar: [ + { + type: "category", + label: "Welcome", + items: [ + { + type: "doc", + label: "Welcome", + id: "welcome", + }, + { + type: "doc", + label: "Why?", + id: "why", + }, + { + type: "doc", + label: "The reason why it's hard", + id: "why-is-it-hard", + }, + { + type: "doc", + label: "The outcome", + id: "what-we-will-learn", + }, + ], + }, + { + type: "category", + label: "Hardware", + items: [ + { + type: "doc", + label: "Components", + id: "hardware-raspberry-pi-setup/hardware", + }, + { + type: "category", + label: "Setup", + items: [ + { + type: "doc", + label: "Before We Start", + id: "hardware-raspberry-pi-setup/before-we-start", + }, + { + type: "doc", + label: "Raspberry Pis", + id: "hardware-raspberry-pi-setup/raspberry-pi-setup", + }, + { + type: "doc", + label: "Mini PCs", + id: "hardware-raspberry-pi-setup/mini-pcs-setup", + }, + ], + }, + ], + }, + { + type: "category", + label: "Mikrotik", + items: [ + { + type: "doc", + label: "Why Mikrotik?", + id: "networking/mikrotik/why-mikrotik", + }, + { + type: "doc", + label: "Network Overview", + id: "networking/mikrotik/network-overview", + }, + { + type: "doc", + label: "Core Concepts", + id: "networking/mikrotik/core-concepts", + }, + { + type: "doc", + label: "MikroTik RouterOS on Lenovo M920q", + id: "networking/mikrotik/lenovo-m920q-roas", + }, + { + type: "doc", + label: "VLAN Schema", + id: "networking/mikrotik/vlan-schema", + }, + { + type: "doc", + label: "Device Configuration", + id: "networking/mikrotik/device-configuration", + }, + { + type: "doc", + label: "Firewall Logic", + id: "networking/mikrotik/firewall-logic", + }, + { + type: "doc", + label: "Configure Email", + id: "networking/mikrotik/configure-email-on-mikrotik", + }, + { + type: "doc", + label: "Dynamic DNS with Cloudflare", + id: "networking/mikrotik/dynamic-dns-with-cloudflare", + }, + { + type: "doc", + label: "Common Scenarios", + id: "networking/mikrotik/common-scenarios", + }, + { + type: "doc", + label: "Summary & Checklist", + id: "networking/mikrotik/summary-and-checklist", + }, + ], + }, + { + type: "category", + label: "Kubernetes", + items: [ + { + type: "doc", + label: "K3s Setup", + id: "kubernetes/k3s-setup", + }, + { + type: "doc", + label: "What Is Kubernetes", + id: "kubernetes/what-is-kubernetes", + }, + { + type: "doc", + label: "Anatomy of a kubectl Command", + id: "kubernetes/anatomy-of-kubectl-command", + }, + { + type: "doc", + label: "Anatomy of a Kubernetes YAML", + id: "kubernetes/anatomy-of-kubernetes-yaml", + }, + { + type: "doc", + label: "Kubernetes 80/20 Rule", + id: "kubernetes/kubernetes-80-20-rule", + }, + { + type: "doc", + label: "K3s Backup", + id: "kubernetes/k3s-backup", + }, + { + type: "doc", + label: "K3s Maintenance", + id: "kubernetes/k3s-maintenance", + }, + { + type: "category", + label: "Storage", + items: [ + { + type: "doc", + label: "Understanding Longhorn Concepts", + id: "storage/understanding-longhorn-concepts", + }, + { + type: "doc", + label: "Setup Longhorn", + id: "storage/setup-longhorn", + }, + { + type: "doc", + label: "Setup Longhorn Dashboard", + id: "storage/setup-longhorn-dashboard", + }, + ], + }, + { + type: "category", + label: "Databases", + items: [ + { + type: "doc", + label: "Databases Within Kubernetes", + id: "databases/databases-within-kubernetes", + }, + { + type: "doc", + label: "Setup CloudNative PG", + id: "databases/setup-cloudnative-pg", + }, + ], + }, + { + type: "category", + label: "Networking", + items: [ + { + type: "doc", + label: "Kubernetes Networking Explained", + id: "networking/kubernetes-networking-explained", + }, + { + type: "doc", + label: "Understanding Network Components", + id: "networking/understanding-network-components", + }, + { + type: "doc", + label: "Expose Traefik Dashboard Inside the K3s Cluster", + id: "networking/expose-traefik-dashboard-inside-the-k3s-cluster", + }, + { + type: "doc", + label: "Setup MetalLB", + id: "networking/setup-metallb", + }, + ], + }, + { + type: "category", + label: "Exercises", + items: [ + { + type: "doc", + label: "Kubernetes YML Structure", + id: "kubernetes/kubernetes-yml-structure", + }, + { + type: "doc", + label: "Getting Started With Kubernetes", + id: "kubernetes/getting-started-with-kubernetes", + }, + { + type: "doc", + label: "Common Kubernetes Commands", + id: "kubernetes/common-kubernetes-commands", + }, + ], + }, + ], + }, + { + type: "category", + label: "Tools", + items: [ + { + type: "category", + label: "Automation", + items: [ + { + type: "doc", + label: "Ansible", + id: "ansible/automation-with-ansible", + }, + ], + }, + ], + }, + + { + id: "terminology", + type: "doc", + label: "Terminology", + }, + ], +}; + +// Recursively number categories and doc items +function numberSidebar(items, prefix = "") { + let count = 1; + + return items.map(item => { + const number = `${prefix}${count}`; + count++; + + // Handle categories + if (item.type === "category") { + const newLabel = `${number}. ${item.label}`; + const numberedItems = numberSidebar(item.items, number + "."); + return { + ...item, + label: newLabel, + items: numberedItems, + }; + } + + // Handle string items (doc IDs) + if (typeof item === "string") { + return { + type: "doc", + id: item, + label: `${number}. ${humanizeId(item)}`, + }; + } + + // Handle doc objects + if (item.type === "doc") { + return { + ...item, + label: `${number}. ${item.label || humanizeId(item.id)}`, + }; + } + + return item; + }); +} + +// Helper to turn 'quick-start/basic' → 'Basic' +function humanizeId(id) { + const parts = id.split("/"); + const last = parts[parts.length - 1]; + return last.replace(/-/g, " ").replace(/\b\w/g, l => l.toUpperCase()); +} + +export default { + tutorialSidebar: numberSidebar(sidebars.tutorialSidebar), +}; diff --git a/docusaurus/src/components/Alert/index.tsx b/docusaurus/src/components/Alert/index.tsx new file mode 100644 index 0000000..edceda5 --- /dev/null +++ b/docusaurus/src/components/Alert/index.tsx @@ -0,0 +1,59 @@ +import { + AcademicCapIcon, + CheckCircleIcon, + ExclamationTriangleIcon, +} from "@heroicons/react/20/solid"; + +export default function Alert({ + title, + description, + variant = "warning", +}: { + title: string; + description: string; + variant?: "warning" | "error" | "success"; +}) { + // Color and icon mapping + const variantMap = { + warning: { + bg: "bg-[#ffab00]/10 dark:bg-[#ffab00]/5", + border: "border-[#ffab00] dark:border-[#ffab00]", + text: "text-[#ffab00] dark:text-[#ffab00]", + Icon: AcademicCapIcon, + }, + error: { + bg: "bg-red-100 dark:bg-red-900/20", + border: "border-red-500 dark:border-red-400", + text: "text-red-700 dark:text-red-400", + Icon: ExclamationTriangleIcon, + }, + success: { + bg: "bg-green-100 dark:bg-green-900/20", + border: "border-green-500 dark:border-green-400", + text: "text-green-700 dark:text-green-400", + Icon: CheckCircleIcon, + }, + }; + const { bg, border, text, Icon } = variantMap[variant] || variantMap.warning; + + return ( +
+
+ {title && ( +
+
+ )} +
+ {title &&

{title}

} +
+

{description}

+
+
+
+
+ ); +} diff --git a/docusaurus/src/components/CodeBlock/index.tsx b/docusaurus/src/components/CodeBlock/index.tsx new file mode 100644 index 0000000..7d52ba1 --- /dev/null +++ b/docusaurus/src/components/CodeBlock/index.tsx @@ -0,0 +1,35 @@ +import { Fragment } from "react"; +import CodeLine from "../CodeLine"; + +const CodeBlock = ({ + highlightedSection, + onHover, + sections, + sectionStyles, +}) => { + return ( +
+
+        
+          {sections.map(section => (
+            
+              {section.comment && (
+                
+                  {section.comment}
+                
+              )}
+              
+            
+          ))}
+        
+      
+
+ ); +}; + +export default CodeBlock; diff --git a/docusaurus/src/components/CodeLine/index.tsx b/docusaurus/src/components/CodeLine/index.tsx new file mode 100644 index 0000000..4f5ba43 --- /dev/null +++ b/docusaurus/src/components/CodeLine/index.tsx @@ -0,0 +1,20 @@ +const CodeLine = ({ section, styles, highlightedSection, onHover }) => { + const isHighlighted = + highlightedSection && section.title.startsWith(highlightedSection.title); + + return ( + onHover(section)} + onMouseLeave={() => onHover(null)} + > + {" ".repeat(section.indent || 0)} + {section.key} + {section.value} + + ); +}; + +export default CodeLine; diff --git a/docusaurus/src/components/Core/Accordion/index.tsx b/docusaurus/src/components/Core/Accordion/index.tsx new file mode 100644 index 0000000..99f4bed --- /dev/null +++ b/docusaurus/src/components/Core/Accordion/index.tsx @@ -0,0 +1,62 @@ +import { useState } from "react"; + +interface AccordionProps { + items: T[]; + getTitle: (item: T, idx: number) => React.ReactNode; + renderContent: (item: T, idx: number) => React.ReactNode; + initialActiveIndex?: number | null; + stepPrefix?: (idx: number) => React.ReactNode; // Optional, for Mikrotik style +} + +function Accordion({ + items, + getTitle, + renderContent, + initialActiveIndex = null, + stepPrefix, +}: AccordionProps) { + const [openIndex, setOpenIndex] = useState(initialActiveIndex); + + const handleToggle = (index: number) => { + setOpenIndex(openIndex === index ? null : index); + }; + + return ( +
+ {items.map((item, idx) => ( +
+ +
+ {openIndex === idx && ( +
{renderContent(item, idx)}
+ )} +
+
+ ))} +
+ ); +} + +export default Accordion; diff --git a/docusaurus/src/components/DataCenterOverview/index.tsx b/docusaurus/src/components/DataCenterOverview/index.tsx new file mode 100644 index 0000000..762ace8 --- /dev/null +++ b/docusaurus/src/components/DataCenterOverview/index.tsx @@ -0,0 +1,82 @@ +import React from "react"; + +interface DiagramBoxProps { + children: React.ReactNode; + className?: string; +} + +const DiagramBox: React.FC = ({ + children, + className = "", +}) => ( +
+ {children} +
+); + +const DataCenterOverview = () => ( +
+
+
+ {/* Hardware Setup */} +
+

1. Hardware Setup

+ +

🛠️ Physical Infrastructure

+
    +
  • 📦 Server Rack
  • +
  • 🍓 Raspberry Pi Cluster
  • +
  • 💻 Mini PCs
  • +
  • 🔌 Power Management
  • +
+
+
+ + {/* Network Setup */} +
+

2. Network Setup

+ +

🌐 Network Infrastructure

+
    +
  • 📡 Mikrotik Router
  • +
  • 🔄 Managed Switch
  • +
  • 🔒 VLANs & Security
  • +
  • 🌍 DNS & Load Balancing
  • +
+
+
+ + {/* K3s Setup */} +
+

3. K3s Setup

+ +

☸️ Kubernetes Layer

+
    +
  • 🎮 Control Plane
  • +
  • 👷 Worker Nodes
  • +
  • 💾 Storage (Longhorn)
  • +
  • 🔍 Monitoring
  • +
+
+
+ {/* K3s Setup */} +
+

+ 4. Applications & Services +

+ +

🚀 Applications & Services

+
    +
  • 🗄️ Databases - PostgreSQL, Redis
  • +
  • 🚪 Ingress - Traefik
  • +
  • 📊 Monitoring - Prometheus, Grafana
  • +
  • 🚀 Apps - Your Services
  • +
+
+
+
+
+
+); + +export default DataCenterOverview; diff --git a/docusaurus/src/components/ExplanationCard/index.tsx b/docusaurus/src/components/ExplanationCard/index.tsx new file mode 100644 index 0000000..45b35a0 --- /dev/null +++ b/docusaurus/src/components/ExplanationCard/index.tsx @@ -0,0 +1,27 @@ +import { Tooltip } from "react-tooltip"; + +const ExplanationCard = ({ + section, + styles, + isHighlighted, + onMouseEnter, + onMouseLeave, +}) => ( +
+

+ {section.title} +

+ +
+); + +export default ExplanationCard; diff --git a/docusaurus/src/components/HomepageFeatures/index.tsx b/docusaurus/src/components/HomepageFeatures/index.tsx new file mode 100644 index 0000000..50a9e6f --- /dev/null +++ b/docusaurus/src/components/HomepageFeatures/index.tsx @@ -0,0 +1,70 @@ +import clsx from 'clsx'; +import Heading from '@theme/Heading'; +import styles from './styles.module.css'; + +type FeatureItem = { + title: string; + Svg: React.ComponentType>; + description: JSX.Element; +}; + +const FeatureList: FeatureItem[] = [ + { + title: 'Easy to Use', + Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, + description: ( + <> + Docusaurus was designed from the ground up to be easily installed and + used to get your website up and running quickly. + + ), + }, + { + title: 'Focus on What Matters', + Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default, + description: ( + <> + Docusaurus lets you focus on your docs, and we'll do the chores. Go + ahead and move your docs into the docs directory. + + ), + }, + { + title: 'Powered by React', + Svg: require('@site/static/img/undraw_docusaurus_react.svg').default, + description: ( + <> + Extend or customize your website layout by reusing React. Docusaurus can + be extended while reusing the same header and footer. + + ), + }, +]; + +function Feature({title, Svg, description}: FeatureItem) { + return ( +
+
+ +
+
+ {title} +

{description}

+
+
+ ); +} + +export default function HomepageFeatures(): JSX.Element { + return ( +
+
+
+ {FeatureList.map((props, idx) => ( + + ))} +
+
+
+ ); +} diff --git a/docusaurus/src/components/HomepageFeatures/styles.module.css b/docusaurus/src/components/HomepageFeatures/styles.module.css new file mode 100644 index 0000000..b248eb2 --- /dev/null +++ b/docusaurus/src/components/HomepageFeatures/styles.module.css @@ -0,0 +1,11 @@ +.features { + display: flex; + align-items: center; + padding: 2rem 0; + width: 100%; +} + +.featureSvg { + height: 200px; + width: 200px; +} diff --git a/docusaurus/src/components/KubernetesParetoPrinciple/BareMetalSection/index.tsx b/docusaurus/src/components/KubernetesParetoPrinciple/BareMetalSection/index.tsx new file mode 100644 index 0000000..2788252 --- /dev/null +++ b/docusaurus/src/components/KubernetesParetoPrinciple/BareMetalSection/index.tsx @@ -0,0 +1,152 @@ +const BareMetalSection = () => ( +
+
+

+ The Bare-Metal Gauntlet +

+

+ In the cloud, networking and storage are managed services. On bare + metal, they are your direct responsibility and the source of the most + difficult challenges. This section provides a roadmap to tame them. +

+
+ +
+

+ Networking on Hard Mode +

+

+ Bare-metal networking is a three-layer problem. You must solve each in + order: Host Prep, CNI, and Load Balancing. Failure at any layer leads to + a non-functional cluster. +

+
+
+
1
+

+ Prepare the Host +

+

+ Disable swap, load kernel modules, and configure host firewalls. + Most `NotReady` nodes are caused by skipping these steps. +

+
+
+
2
+

+ Choose a CNI +

+

+ K3s defaults to Flannel (no Network Policies). For production + security, disable it and install Calico to enable network + segmentation. +

+
+
+
3
+

+ Expose Services +

+

+ `LoadBalancer` services will be `Pending` forever without a + controller. Install MetalLB to assign external IPs from your local + network. +

+
+
+
+ +
+

+ The Persistent Storage Quagmire +

+

+ Choosing your storage solution is a critical architectural decision. + There's no single best answer, only trade-offs. Compare the most common + options below. +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Solution + + Best For + + Ease of Setup + + Performance + + Resilience + + Key "Gotcha" +
+ NFS + Homelab, simple sharingVery EasyLowSPOFPerformance bottleneck
+ Longhorn + Small-to-medium prodEasyModerateReplicatedSlow rebuilds on node reboot
+ Ceph (Rook) + Large-scale prodComplexHighHighly ResilientHigh complexity & resource use
+ OpenEBS Mayastor + Performance-criticalModerateVery HighReplicatedVery high CPU usage
+
+
+
+); + +export default BareMetalSection; diff --git a/docusaurus/src/components/KubernetesParetoPrinciple/ContentRenderer.tsx b/docusaurus/src/components/KubernetesParetoPrinciple/ContentRenderer.tsx new file mode 100644 index 0000000..098927e --- /dev/null +++ b/docusaurus/src/components/KubernetesParetoPrinciple/ContentRenderer.tsx @@ -0,0 +1,74 @@ +import CodeBlock from "@theme/CodeBlock"; + +type ListItemProps = { + item: { + title?: string; + text: string; + }; +}; + +const ListItem = ({ item }: ListItemProps) => { + return ( +
  • + {item.title && {item.title} } + {item.text} +
  • + ); +}; + +export type ContentItem = + | { type: "paragraph"; text: string; title?: string } + | { type: "code"; code: string } + | { + type: "list" | "ordered-list"; + items: { title?: string; text: string }[]; + }; + +type ContentRendererProps = { + content: ContentItem[]; +}; + +const ContentRenderer = ({ content }: ContentRendererProps) => { + return ( + <> + {content.map((item, index) => { + if (item.type === "paragraph") { + return ( +

    + {item.title && {item.title} } + {item.text} +

    + ); + } + if (item.type === "code") { + return ( +
    + {item.code} +
    + ); + } + if (item.type === "list") { + return ( +
      + {item.items.map((listItem, i) => ( + + ))} +
    + ); + } + if (item.type === "ordered-list") { + return ( +
      + {item.items.map((listItem, i) => ( + + ))} +
    + ); + } + return null; + })} + + ); +}; + +export default ContentRenderer; diff --git a/docusaurus/src/components/KubernetesParetoPrinciple/DatastoreChart/index.tsx b/docusaurus/src/components/KubernetesParetoPrinciple/DatastoreChart/index.tsx new file mode 100644 index 0000000..192febb --- /dev/null +++ b/docusaurus/src/components/KubernetesParetoPrinciple/DatastoreChart/index.tsx @@ -0,0 +1,125 @@ +import { + BarController, + BarElement, + CategoryScale, + Chart, + LinearScale, + Title, + Tooltip, +} from "chart.js"; +import { useEffect, useRef } from "react"; + +Chart.register( + BarController, + BarElement, + LinearScale, + CategoryScale, + Title, + Tooltip +); + +const DatastoreChart = () => { + const chartRef = useRef(null); + const chartInstance = useRef(null); + const isDarkMode = + typeof window !== "undefined" && + document.documentElement.getAttribute("data-theme") === "dark"; + + useEffect(() => { + const fontColor = isDarkMode ? "#cbd5e1" : "#1e293b"; // slate-300 : slate-800 + const gridColor = isDarkMode ? "#334155" : "#e2e8f0"; // slate-700 : slate-200 + + if (chartRef.current) { + if (chartInstance.current) { + chartInstance.current.destroy(); + } + + const ctx = chartRef.current.getContext("2d"); + chartInstance.current = new Chart(ctx, { + type: "bar", + data: { + labels: ["API Response Time (s)", "CPU Usage (arbitrary units)"], + datasets: [ + { + label: "K3s with SQLite", + data: [1.4, 7], + backgroundColor: "rgba(245, 158, 11, 0.6)", // amber-500 + borderColor: "rgba(245, 158, 11, 1)", // amber-500 + borderWidth: 1, + }, + { + label: "K3s with Embedded etcd", + data: [0.35, 4], + backgroundColor: "rgba(59, 130, 246, 0.6)", // blue-500 + borderColor: "rgba(59, 130, 246, 1)", // blue-500 + borderWidth: 1, + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + title: { + display: true, + text: "Datastore Performance Under Load", + color: fontColor, + }, + tooltip: { + callbacks: { + label: function (context) { + let label = context.dataset.label || ""; + if (label) { + label += ": "; + } + if (context.parsed.y !== null) { + label += + context.parsed.y + + (context.label.includes("Time") ? "s" : ""); + } + return label; + }, + }, + }, + }, + scales: { + y: { + beginAtZero: true, + ticks: { color: fontColor }, + grid: { color: gridColor }, + }, + x: { + ticks: { color: fontColor }, + grid: { display: false }, + }, + }, + }, + }); + } + + return () => { + if (chartInstance.current) { + chartInstance.current.destroy(); + } + }; + }, [isDarkMode]); + + return ( +
    + +
    + ); +}; + +export default DatastoreChart; diff --git a/docusaurus/src/components/KubernetesParetoPrinciple/HomeSection/HomeSectionCard.tsx b/docusaurus/src/components/KubernetesParetoPrinciple/HomeSection/HomeSectionCard.tsx new file mode 100644 index 0000000..d805b67 --- /dev/null +++ b/docusaurus/src/components/KubernetesParetoPrinciple/HomeSection/HomeSectionCard.tsx @@ -0,0 +1,19 @@ +const HomeSectionCard = ({ + href, + title, + description, +}: { + href: string; + title: string; + description: string; +}) => ( + +

    {title}

    +

    {description}

    +
    +); + +export default HomeSectionCard; diff --git a/docusaurus/src/components/KubernetesParetoPrinciple/HomeSection/index.tsx b/docusaurus/src/components/KubernetesParetoPrinciple/HomeSection/index.tsx new file mode 100644 index 0000000..6eb87d8 --- /dev/null +++ b/docusaurus/src/components/KubernetesParetoPrinciple/HomeSection/index.tsx @@ -0,0 +1,39 @@ +import HomeSectionCard from "./HomeSectionCard"; + +const cardData = [ + { + href: "#troubleshooting", + title: "Decode Pod Errors", + description: + "Quickly diagnose `CrashLoopBackOff`, `ImagePullBackOff`, and `Pending` states.", + }, + { + href: "#bare-metal", + title: "Tame Networking & Storage", + description: + "Navigate the biggest bare-metal challenges: load balancing and persistent data.", + }, + { + href: "#production", + title: "Go to Production", + description: + "Follow an actionable checklist for security, monitoring, and backups.", + }, +]; + +const HomeSection = () => ( +
    +
    + {cardData.map(card => ( + + ))} +
    +
    +); + +export default HomeSection; diff --git a/docusaurus/src/components/KubernetesParetoPrinciple/K3sInternalsSection/index.tsx b/docusaurus/src/components/KubernetesParetoPrinciple/K3sInternalsSection/index.tsx new file mode 100644 index 0000000..b7ee246 --- /dev/null +++ b/docusaurus/src/components/KubernetesParetoPrinciple/K3sInternalsSection/index.tsx @@ -0,0 +1,90 @@ +import DatastoreChart from "../DatastoreChart"; + +const K3sInternalsSection = () => ( +
    +
    +

    + K3s In The Trenches +

    +

    + K3s has unique behaviors that can trip you up when moving from a homelab + to production. Understanding its datastore options and HA model is key + to building a stable cluster. +

    +
    + +
    +

    + Datastore Performance: SQLite vs. Embedded `etcd` +

    +

    + K3s defaults to SQLite for simplicity, but this is unsuitable for a + multi-server HA cluster. The `kine` translation layer introduces + overhead. For production, `etcd` is mandatory. This chart visualizes the + performance cliff. +

    + +

    + `etcd` is ~4x faster under load, with lower CPU usage. It demands faster + disks but is the only option for a stable, multi-server production + cluster. +

    +
    + +
    +

    + The Three Pillars of K3s High Availability +

    +

    + True HA is more than just adding servers. Neglecting any of these + pillars creates a hidden single point of failure and a false sense of + security. +

    +
    +
    +
    + 👥 +
    +

    + `etcd` Quorum +

    +

    + You must have an odd number of server nodes (3, 5, etc.). This + allows the Raft consensus algorithm to maintain a majority (quorum) + and tolerate node failures. A 3-node cluster can lose 1 server; a + 5-node cluster can lose 2. +

    +
    +
    +
    + 🎯 +
    +

    + Stable API Endpoint +

    +

    + Agents and clients need a fixed IP address that floats between + healthy servers. Without a Virtual IP (VIP), if the server you're + targeting fails, your connection breaks. Use a load balancer or + `keepalived` for the API server endpoint. +

    +
    +
    +
    + 💾 +
    +

    + Performant Hardware +

    +

    + `etcd` is extremely sensitive to disk I/O latency. Running an HA + cluster on slow storage like Raspberry Pi SD cards is a recipe for + instability and data corruption. Use SSDs for your server nodes. +

    +
    +
    +
    +
    +); + +export default K3sInternalsSection; diff --git a/docusaurus/src/components/KubernetesParetoPrinciple/ProductionChecklistSection/index.tsx b/docusaurus/src/components/KubernetesParetoPrinciple/ProductionChecklistSection/index.tsx new file mode 100644 index 0000000..259bd98 --- /dev/null +++ b/docusaurus/src/components/KubernetesParetoPrinciple/ProductionChecklistSection/index.tsx @@ -0,0 +1,36 @@ +import Accordion from "../../Core/Accordion"; +import ContentRenderer from "../ContentRenderer"; +import { contentData } from "../content"; + +const ProductionChecklistSection = () => ( +
    +
    +

    + Production-Readiness Crucible +

    +

    + A functional cluster is not a production cluster. Use this checklist to + systematically harden, monitor, and back up your system for + mission-critical workloads. +

    +
    + ( + + {item.icon} + + {item.title} + + + {item.description} + + + )} + renderContent={item => } + initialActiveIndex={0} + /> +
    +); + +export default ProductionChecklistSection; diff --git a/docusaurus/src/components/KubernetesParetoPrinciple/TroubleshootingSection/TabButton.tsx b/docusaurus/src/components/KubernetesParetoPrinciple/TroubleshootingSection/TabButton.tsx new file mode 100644 index 0000000..e252a86 --- /dev/null +++ b/docusaurus/src/components/KubernetesParetoPrinciple/TroubleshootingSection/TabButton.tsx @@ -0,0 +1,22 @@ +const TabButton = ({ + label, + isActive, + onClick, +}: { + label: string; + isActive: boolean; + onClick: () => void; +}) => ( + +); + +export default TabButton; diff --git a/docusaurus/src/components/KubernetesParetoPrinciple/TroubleshootingSection/TabContent.tsx b/docusaurus/src/components/KubernetesParetoPrinciple/TroubleshootingSection/TabContent.tsx new file mode 100644 index 0000000..bc5fe84 --- /dev/null +++ b/docusaurus/src/components/KubernetesParetoPrinciple/TroubleshootingSection/TabContent.tsx @@ -0,0 +1,27 @@ +import Accordion from "../../Core/Accordion"; +import ContentRenderer from "../ContentRenderer"; + +const TabContent = ({ + title, + description, + items, +}: { + title: string; + description: string; + items: { id: number; title: string; content: any }[]; +}) => ( +
    +

    + {title} +

    +

    {description}

    + item.title} + renderContent={item => } + initialActiveIndex={0} + /> +
    +); + +export default TabContent; diff --git a/docusaurus/src/components/KubernetesParetoPrinciple/TroubleshootingSection/index.tsx b/docusaurus/src/components/KubernetesParetoPrinciple/TroubleshootingSection/index.tsx new file mode 100644 index 0000000..7c17bd1 --- /dev/null +++ b/docusaurus/src/components/KubernetesParetoPrinciple/TroubleshootingSection/index.tsx @@ -0,0 +1,66 @@ +import { useState } from "react"; +import { contentData } from "../content"; +import TabButton from "./TabButton"; +import TabContent from "./TabContent"; + +const tabs = [ + { id: "crashloop", label: "CrashLoopBackOff" }, + { id: "imagepull", label: "ImagePullBackOff" }, + { id: "pending", label: "Pending" }, +]; + +const tabContentDetails = { + crashloop: { + title: "Diagnosing `CrashLoopBackOff`", + description: `The container starts, but exits with an error almost immediately. Kubernetes tries to restart it, creating a "crash loop." The problem is almost always inside your application or its configuration.`, + }, + imagepull: { + title: "Diagnosing `ImagePullBackOff`", + description: `The Kubelet on a node cannot pull the container image from the registry. The container will never start until this is resolved.`, + }, + pending: { + title: "Diagnosing `Pending` Pods", + description: `The pod has been accepted by the cluster, but the scheduler cannot find a suitable node to run it on. This is a resource or placement constraint issue.`, + }, +}; + +const TroubleshootingSection = () => { + const [openTab, setOpenTab] = useState("crashloop"); + + return ( +
    +
    +

    + Universal Kubernetes Annoyances +

    +

    + A huge amount of time is wasted on a few common pod errors. This + interactive troubleshooter helps you diagnose the root cause quickly + by treating the symptom to find the disease. +

    +
    +
    +
    + {tabs.map(tab => ( + setOpenTab(tab.id)} + /> + ))} +
    + + {openTab in tabContentDetails && ( + + )} +
    +
    + ); +}; + +export default TroubleshootingSection; diff --git a/docusaurus/src/components/KubernetesParetoPrinciple/content.ts b/docusaurus/src/components/KubernetesParetoPrinciple/content.ts new file mode 100644 index 0000000..7f061e3 --- /dev/null +++ b/docusaurus/src/components/KubernetesParetoPrinciple/content.ts @@ -0,0 +1,289 @@ +import { ContentItem } from "./ContentRenderer"; + +type TroubleshootingContent = { + id: number; + title: string; + content: ContentItem[]; +}; + +type ProductionContent = { + id: number; + title: string; + icon: string; + description: string; + content: ContentItem[]; +}; + +interface ContentData { + troubleshooting: { + crashloop: TroubleshootingContent[]; + imagepull: TroubleshootingContent[]; + pending: TroubleshootingContent[]; + }; + production: ProductionContent[]; +} + +export const contentData: ContentData = { + troubleshooting: { + crashloop: [ + { + id: 1, + title: "Step 1: Check the Logs", + content: [ + { + type: "paragraph", + text: "This is the most critical step. The container logs almost always contain the application error that caused the crash.", + }, + { type: "code", code: "kubectl logs " }, + ], + }, + { + id: 2, + title: "Step 2: Check Previous Logs", + content: [ + { + type: "paragraph", + text: "The container is restarting constantly. The original error might be in the logs of a *previous* instance.", + }, + { type: "code", code: "kubectl logs --previous" }, + ], + }, + { + id: 3, + title: "Step 3: Check ConfigMaps & Secrets", + content: [ + { + type: "paragraph", + text: "A very common cause is the application failing to start due to missing or incorrect configuration, like a database URL or password. Verify that all required ConfigMaps and Secrets are mounted correctly and contain valid data.", + }, + ], + }, + { + id: 4, + title: "Step 4: Check Resource Limits", + content: [ + { + type: "paragraph", + text: "If a container exceeds its memory limit, it will be `OOMKilled` (Exit Code 137), causing a crash loop. Check `kubectl describe pod` for the reason for the last termination.", + }, + ], + }, + ], + imagepull: [ + { + id: 1, + title: "Step 1: Describe the Pod", + content: [ + { + type: "paragraph", + text: 'This command is your source of truth. The `Events` section at the bottom will give a clear error message like "unauthorized" or "no such host".', + }, + { type: "code", code: "kubectl describe pod " }, + ], + }, + { + id: 2, + title: "Step 2: Check for Typos", + content: [ + { + type: "paragraph", + text: "The most common cause is a simple typo in the image name or tag in your YAML manifest (e.g., `my-app:latesst` instead of `my-app:latest`). Double-check it.", + }, + ], + }, + { + id: 3, + title: "Step 3: Verify Private Registry Secrets", + content: [ + { + type: "paragraph", + text: "If pulling from a private registry, ensure your pod spec includes the correct `imagePullSecrets` and that the secret itself contains valid, base64-encoded credentials.", + }, + ], + }, + { + id: 4, + title: "Step 4: Test Node Connectivity", + content: [ + { + type: "paragraph", + text: "SSH into the affected node and manually try to reach the registry with `ping` or `curl`. This can reveal DNS or firewall issues on the node itself.", + }, + ], + }, + ], + pending: [ + { + id: 1, + title: "Step 1: Describe the Pod", + content: [ + { + type: "paragraph", + text: 'Again, this is the most important command. The `Events` section will tell you exactly why the scheduler failed, e.g., "Insufficient cpu" or "node(s) had taint that the pod didnt tolerate".', + }, + { type: "code", code: "kubectl describe pod " }, + ], + }, + { + id: 2, + title: "Step 2: Check Resource Requests", + content: [ + { + type: "paragraph", + text: "Does the cluster have enough free CPU and memory to satisfy the pod's `resources.requests`? Use `kubectl top nodes` to see current usage.", + }, + ], + }, + { + id: 3, + title: "Step 3: Check Taints and Tolerations", + content: [ + { + type: "paragraph", + text: 'Nodes can have "taints" that repel pods. A pod can only be scheduled if it has a matching "toleration." Check for mismatches between `kubectl get nodes -o custom-columns=...` and your pod spec.', + }, + ], + }, + { + id: 4, + title: "Step 4: Check Persistent Volume Claims (PVCs)", + content: [ + { + type: "paragraph", + text: "If the pod requests a PVC, the pod will remain `Pending` until the storage system can provision the requested volume. Check the status of the PVC with `kubectl get pvc` and `kubectl describe pvc`.", + }, + ], + }, + ], + }, + production: [ + { + id: 1, + title: "Fortify the Fortress", + icon: "🛡️", + description: "Harden your cluster from the host OS up to the workloads.", + content: [ + { + type: "list", + items: [ + { + title: "Harden Host OS:", + text: "Set secure kernel parameters in /etc/sysctl.d/ and secure file permissions on K3s certs.", + }, + { + title: "Enable Audit Logging:", + text: "K3s disables this by default. Enable it via kube-apiserver-args to create a forensic trail.", + }, + { + title: "Use RBAC Least Privilege:", + text: "Avoid `cluster-admin`. Create narrowly-scoped Roles and RoleBindings for service accounts.", + }, + { + title: "Enforce Pod Security Standards (PSS):", + text: "Label production namespaces with `pod-security.kubernetes.io/enforce: restricted`.", + }, + { + title: "Implement Network Policies:", + text: "Start with a default-deny ingress policy in each namespace to prevent lateral movement.", + }, + ], + }, + ], + }, + { + id: 2, + title: "Achieve Observability", + icon: "📊", + description: + "You can't operate what you can't see. Set up monitoring and logging.", + content: [ + { + type: "paragraph", + title: "The K3s Gotcha:", + text: "Standard Prometheus setups fail to monitor the control plane because components are compiled into the K3s binary, not run as pods. You must manually configure scrape jobs.", + }, + { + type: "ordered-list", + items: [ + { + title: "Configure K3s:", + text: "Add args like `etcd-expose-metrics: true` to the K3s config on server nodes.", + }, + { + title: "Deploy Prometheus:", + text: "Use the `kube-prometheus-stack` Helm chart.", + }, + { + title: "Add Static Scrape Configs:", + text: "Add `additionalScrapeConfigs` to your Prometheus values to manually target the `etcd`, `kube-scheduler`, and `kube-controller-manager` metric endpoints on your server node IPs.", + }, + { + title: "Centralize Logs:", + text: "Deploy a logging stack like Promtail/Loki/Grafana (PLG) to ship all container and system logs to a central, searchable location.", + }, + ], + }, + ], + }, + { + id: 3, + title: 'The "Oops" Button', + icon: "⏪", + description: + "A system without a tested backup is a disaster waiting to happen.", + content: [ + { + type: "paragraph", + text: "A robust strategy requires a mandatory two-tiered approach:", + }, + { + type: "list", + items: [ + { + title: "Tier 1: Cluster State Backup:", + text: "This is the K3s datastore. Use the built-in `k3s etcd-snapshot` command. Schedule automatic snapshots and send them to off-site S3 storage. Critically, you MUST also back up the server token at /var/lib/rancher/k3s/server/token, or your restored snapshot will be unusable.", + }, + { + title: "Tier 2: Workload & Volume Backup:", + text: "This is for your applications and their persistent data. Use Velero to back up Kubernetes objects and take snapshots of Persistent Volumes via your storage provider's CSI plugin. Regularly test your restore process!", + }, + ], + }, + ], + }, + { + id: 4, + title: "Automate Everything", + icon: "🤖", + description: + "Adopt a GitOps workflow to manage your cluster declaratively.", + content: [ + { + type: "paragraph", + text: "Manual `kubectl apply` is error-prone and un-auditable at scale. GitOps treats your Git repository as the single source of truth for your cluster's state.", + }, + { + type: "list", + items: [ + { + title: "Use FluxCD or ArgoCD:", + text: "Deploy a GitOps agent in your cluster.", + }, + { + title: "Structure Your Repo:", + text: "Create a Git repository with a clear separation between cluster infrastructure (MetalLB, Longhorn) and applications.", + }, + { + title: "Reconcile Automatically:", + text: "The agent continuously compares the live state of the cluster to the 'desired state' in Git, automatically applying changes or correcting drift.", + }, + { + title: "Audit Trail:", + text: "Every change to your cluster is a version-controlled, auditable Git commit.", + }, + ], + }, + ], + }, + ], +}; diff --git a/docusaurus/src/components/KubernetesParetoPrinciple/index.tsx b/docusaurus/src/components/KubernetesParetoPrinciple/index.tsx new file mode 100644 index 0000000..abcf501 --- /dev/null +++ b/docusaurus/src/components/KubernetesParetoPrinciple/index.tsx @@ -0,0 +1,19 @@ +import BareMetalSection from "./BareMetalSection"; +import HomeSection from "./HomeSection"; +import K3sInternalsSection from "./K3sInternalsSection"; +import ProductionChecklistSection from "./ProductionChecklistSection"; +import TroubleshootingSection from "./TroubleshootingSection"; + +export default function App() { + return ( +
    +
    + + + + + +
    +
    + ); +} diff --git a/docusaurus/src/components/KubernetesYAMLAnatomy/configmap.ts b/docusaurus/src/components/KubernetesYAMLAnatomy/configmap.ts new file mode 100644 index 0000000..f80060f --- /dev/null +++ b/docusaurus/src/components/KubernetesYAMLAnatomy/configmap.ts @@ -0,0 +1,53 @@ +export const sectionStyles = { + apiVersion: { + keyColor: "text-blue-600 dark:text-blue-400", + cardColor: "border-2 border-blue-200 dark:border-blue-900", + titleColor: "text-blue-700 dark:text-blue-400", + }, + kind: { + keyColor: "text-cyan-600 dark:text-cyan-400", + cardColor: "border-2 border-cyan-200 dark:border-cyan-900", + titleColor: "text-cyan-700 dark:text-cyan-400", + }, + metadata: { + keyColor: "text-green-600 dark:text-green-400", + cardColor: "border-2 border-green-200 dark:border-green-900", + titleColor: "text-green-700 dark:text-green-400", + }, + data: { + keyColor: "text-purple-600 dark:text-purple-400", + cardColor: "border-2 border-purple-200 dark:border-purple-900", + titleColor: "text-purple-700 dark:text-purple-400", + }, +}; + +export const sections = [ + { + id: "apiVersion", + key: "apiVersion:", + value: "v1", + title: "apiVersion", + description: "The version of the Kubernetes API for ConfigMap resources.", + }, + { + id: "kind", + key: "kind:", + value: "ConfigMap", + title: "kind", + description: "Specifies the object type, here it is a ConfigMap.", + }, + { + id: "metadata", + key: "metadata:", + value: `\n name: my-app-config`, + title: "metadata", + description: "Metadata for the ConfigMap, such as its name.", + }, + { + id: "data", + key: "data:", + value: `\n APP_ENV: production\n LOG_LEVEL: info`, + title: "data", + description: "Key-value pairs of configuration data.", + }, +]; diff --git a/docusaurus/src/components/KubernetesYAMLAnatomy/deployment.ts b/docusaurus/src/components/KubernetesYAMLAnatomy/deployment.ts new file mode 100644 index 0000000..19b57ce --- /dev/null +++ b/docusaurus/src/components/KubernetesYAMLAnatomy/deployment.ts @@ -0,0 +1,140 @@ +export const sectionStyles = { + apiVersion: { + keyColor: "text-blue-600 dark:text-blue-400", + cardColor: "border-2 border-blue-200 dark:border-blue-900", + titleColor: "text-blue-700 dark:text-blue-400", + }, + kind: { + keyColor: "text-cyan-600 dark:text-cyan-400", + cardColor: "border-2 border-cyan-200 dark:border-cyan-900", + titleColor: "text-cyan-700 dark:text-cyan-400", + }, + metadata: { + keyColor: "text-green-600 dark:text-green-400", + cardColor: "border-2 border-green-200 dark:border-green-900", + titleColor: "text-green-700 dark:text-green-400", + }, + spec: { + keyColor: "text-purple-600 dark:text-purple-400", + cardColor: "border-2 border-purple-200 dark:border-purple-900", + titleColor: "text-purple-700 dark:text-purple-400", + }, + selector: { + keyColor: "text-orange-600 dark:text-orange-400", + cardColor: "border-2 border-orange-200 dark:border-orange-900", + titleColor: "text-orange-700 dark:text-orange-400", + }, + template: { + keyColor: "text-rose-600 dark:text-rose-400", + cardColor: "border-2 border-rose-200 dark:border-rose-900", + titleColor: "text-rose-700 dark:text-rose-400", + }, + "template-metadata": { + keyColor: "text-green-600 dark:text-green-400", + cardColor: "border-2 border-green-200 dark:border-green-900", + titleColor: "text-green-700 dark:text-green-400", + }, + "template-spec": { + keyColor: "text-purple-600 dark:text-purple-400", + cardColor: "border-2 border-purple-200 dark:border-purple-900", + titleColor: "text-purple-700 dark:text-purple-400", + }, + containers: { + keyColor: "text-yellow-600 dark:text-yellow-400", + cardColor: "border-2 border-yellow-200 dark:border-yellow-900", + titleColor: "text-yellow-700 dark:text-yellow-400", + }, + status: { + keyColor: "text-gray-600 dark:text-gray-400", + cardColor: "border-2 border-gray-200 dark:border-gray-400", + titleColor: "text-gray-700 dark:text-gray-400", + }, +}; + +export const sections = [ + { + id: "apiVersion", + key: "apiVersion:", + value: "apps/v1", + title: "apiVersion", + description: + "Which Kubernetes API version to use. Essential for compatibility.", + }, + { + id: "kind", + key: "kind:", + value: "Deployment", + title: "kind", + description: + "The type of object to create (e.g., `Deployment`, `Service`).", + }, + { + id: "metadata", + key: "metadata:", + value: ` + name: my-app-deployment`, + title: "metadata", + description: + "Unique identifiers for the object, like its `name` and `labels`.", + }, + { + id: "spec", + key: "spec:", + value: ` + replicas: 3`, + title: "spec", + description: + "The **desired state**. You tell Kubernetes what you want the object to look like.", + }, + { + id: "selector", + key: "selector:", + value: ` + matchLabels: + app: my-app`, + title: "spec.selector", + description: + "How a controller (like a Deployment) finds which Pods to manage. It matches the Pods' labels.", + indent: 2, + }, + { + id: "template", + key: "template:", + value: "", + title: "spec.template", + description: + "A blueprint for creating the Pods. It has its own `metadata` and `spec`.", + indent: 2, + }, + { + id: "template-metadata", + key: "metadata:", + value: ` + labels: + app: my-app`, + title: "spec.template.metadata", + description: "Metadata for the Pods created by the template.", + indent: 4, + }, + { + id: "template-spec", + key: "spec:", + value: "", + title: "spec.template.spec", + description: "Specification for the Pods created by the template.", + indent: 4, + }, + { + id: "containers", + key: "containers:", + value: ` + - name: my-app-container + image: nginx:latest + ports: + - containerPort: 80`, + title: "spec.template.spec.containers", + description: + "The heart of the Pod. A list of one or more containers to run, specifying the `image`, `ports`, etc.", + indent: 6, + }, +]; diff --git a/docusaurus/src/components/KubernetesYAMLAnatomy/index.tsx b/docusaurus/src/components/KubernetesYAMLAnatomy/index.tsx new file mode 100644 index 0000000..9f05863 --- /dev/null +++ b/docusaurus/src/components/KubernetesYAMLAnatomy/index.tsx @@ -0,0 +1,76 @@ +import { useState } from "react"; +import CodeBlock from "../CodeBlock"; +import ExplanationCard from "../ExplanationCard"; +import Tabs from "../Tabs"; +import * as configmapConfig from "./configmap"; +import * as deploymentConfig from "./deployment"; +import * as ingressConfig from "./ingress"; +import * as pvcConfig from "./pvc"; +import * as secretConfig from "./secret"; +import * as serviceConfig from "./service"; + +const yamls = { + deployment: deploymentConfig, + service: serviceConfig, + ingress: ingressConfig, + configmap: configmapConfig, + secret: secretConfig, + pvc: pvcConfig, +}; + +const tabs = [ + { id: "deployment", label: "Deployment" }, + { id: "service", label: "Service" }, + { id: "ingress", label: "Ingress" }, + { id: "configmap", label: "ConfigMap" }, + { id: "secret", label: "Secret" }, + { id: "pvc", label: "PVC" }, +]; + +export default function App() { + const [highlightedSection, setHighlightedSection] = useState(null); + const [activeTab, setActiveTab] = useState("deployment"); + + const handleHover = section => { + setHighlightedSection(section); + }; + + const { sections, sectionStyles } = yamls[activeTab]; + + return ( +
    + +
    +
    +
    + {/* Left Side: YAML Code */} +
    + +
    + + {/* Right Side: Explanations */} +
    + {sections.map(section => ( + handleHover(section)} + onMouseLeave={() => handleHover(null)} + /> + ))} +
    +
    +
    +
    + ); +} diff --git a/docusaurus/src/components/KubernetesYAMLAnatomy/ingress.ts b/docusaurus/src/components/KubernetesYAMLAnatomy/ingress.ts new file mode 100644 index 0000000..ffeae37 --- /dev/null +++ b/docusaurus/src/components/KubernetesYAMLAnatomy/ingress.ts @@ -0,0 +1,92 @@ +export const sectionStyles = { + apiVersion: { + keyColor: "text-blue-600 dark:text-blue-400", + cardColor: "border-2 border-blue-200 dark:border-blue-900", + titleColor: "text-blue-700 dark:text-blue-400", + }, + kind: { + keyColor: "text-cyan-600 dark:text-cyan-400", + cardColor: "border-2 border-cyan-200 dark:border-cyan-900", + titleColor: "text-cyan-700 dark:text-cyan-400", + }, + metadata: { + keyColor: "text-green-600 dark:text-green-400", + cardColor: "border-2 border-green-200 dark:border-green-900", + titleColor: "text-green-700 dark:text-green-400", + }, + spec: { + keyColor: "text-purple-600 dark:text-purple-400", + cardColor: "border-2 border-purple-200 dark:border-purple-900", + titleColor: "text-purple-700 dark:text-purple-400", + }, + rules: { + keyColor: "text-orange-600 dark:text-orange-400", + cardColor: "border-2 border-orange-200 dark:border-orange-900", + titleColor: "text-orange-700 dark:text-orange-400", + }, + http: { + keyColor: "text-pink-600 dark:text-pink-400", + cardColor: "border-2 border-pink-200 dark:border-pink-900", + titleColor: "text-pink-700 dark:text-pink-400", + }, + paths: { + keyColor: "text-yellow-600 dark:text-yellow-400", + cardColor: "border-2 border-yellow-200 dark:border-yellow-900", + titleColor: "text-yellow-700 dark:text-yellow-400", + }, +}; + +export const sections = [ + { + id: "apiVersion", + key: "apiVersion:", + value: "networking.k8s.io/v1", + title: "apiVersion", + description: "The version of the Kubernetes API for Ingress resources.", + }, + { + id: "kind", + key: "kind:", + value: "Ingress", + title: "kind", + description: "Specifies the object type, here it is an Ingress.", + }, + { + id: "metadata", + key: "metadata:", + value: `\n name: my-app-ingress`, + title: "metadata", + description: "Metadata for the Ingress, such as its name.", + }, + { + id: "spec", + key: "spec:", + value: "", + title: "spec", + description: "The desired state of the Ingress resource.", + }, + { + id: "rules", + key: "rules:", + value: "", + title: "spec.rules", + description: "Defines the rules for routing traffic.", + indent: 2, + }, + { + id: "http", + key: "http:", + value: "", + title: "spec.rules.http", + description: "HTTP-specific routing information.", + indent: 4, + }, + { + id: "paths", + key: "paths:", + value: `\n - path: /\n pathType: Prefix\n backend:\n service:\n name: my-app-service\n port:\n number: 80`, + title: "spec.rules.http.paths", + description: "Defines the paths and backend services for the Ingress.", + indent: 6, + }, +]; diff --git a/docusaurus/src/components/KubernetesYAMLAnatomy/pvc.ts b/docusaurus/src/components/KubernetesYAMLAnatomy/pvc.ts new file mode 100644 index 0000000..82899b2 --- /dev/null +++ b/docusaurus/src/components/KubernetesYAMLAnatomy/pvc.ts @@ -0,0 +1,80 @@ +export const sectionStyles = { + apiVersion: { + keyColor: "text-blue-600 dark:text-blue-400", + cardColor: "border-2 border-blue-200 dark:border-blue-900", + titleColor: "text-blue-700 dark:text-blue-400", + }, + kind: { + keyColor: "text-cyan-600 dark:text-cyan-400", + cardColor: "border-2 border-cyan-200 dark:border-cyan-900", + titleColor: "text-cyan-700 dark:text-cyan-400", + }, + metadata: { + keyColor: "text-green-600 dark:text-green-400", + cardColor: "border-2 border-green-200 dark:border-green-900", + titleColor: "text-green-700 dark:text-green-400", + }, + spec: { + keyColor: "text-purple-600 dark:text-purple-400", + cardColor: "border-2 border-purple-200 dark:border-purple-900", + titleColor: "text-purple-700 dark:text-purple-400", + }, + accessModes: { + keyColor: "text-orange-600 dark:text-orange-400", + cardColor: "border-2 border-orange-200 dark:border-orange-900", + titleColor: "text-orange-700 dark:text-orange-400", + }, + resources: { + keyColor: "text-pink-600 dark:text-pink-400", + cardColor: "border-2 border-pink-200 dark:border-pink-900", + titleColor: "text-pink-700 dark:text-pink-400", + }, +}; + +export const sections = [ + { + id: "apiVersion", + key: "apiVersion:", + value: "v1", + title: "apiVersion", + description: "The version of the Kubernetes API for PVC resources.", + }, + { + id: "kind", + key: "kind:", + value: "PersistentVolumeClaim", + title: "kind", + description: + "Specifies the object type, here it is a PersistentVolumeClaim.", + }, + { + id: "metadata", + key: "metadata:", + value: `\n name: my-app-pvc`, + title: "metadata", + description: "Metadata for the PVC, such as its name.", + }, + { + id: "spec", + key: "spec:", + value: "", + title: "spec", + description: "The desired state of the PVC resource.", + }, + { + id: "accessModes", + key: "accessModes:", + value: `\n - ReadWriteOnce`, + title: "spec.accessModes", + description: "Defines how the volume can be mounted (e.g., ReadWriteOnce).", + indent: 2, + }, + { + id: "resources", + key: "resources:", + value: `\n requests:\n storage: 1Gi`, + title: "spec.resources", + description: "Specifies the amount of storage requested.", + indent: 2, + }, +]; diff --git a/docusaurus/src/components/KubernetesYAMLAnatomy/secret.ts b/docusaurus/src/components/KubernetesYAMLAnatomy/secret.ts new file mode 100644 index 0000000..0ad526e --- /dev/null +++ b/docusaurus/src/components/KubernetesYAMLAnatomy/secret.ts @@ -0,0 +1,66 @@ +export const sectionStyles = { + apiVersion: { + keyColor: "text-blue-600 dark:text-blue-400", + cardColor: "border-2 border-blue-200 dark:border-blue-900", + titleColor: "text-blue-700 dark:text-blue-400", + }, + kind: { + keyColor: "text-cyan-600 dark:text-cyan-400", + cardColor: "border-2 border-cyan-200 dark:border-cyan-900", + titleColor: "text-cyan-700 dark:text-cyan-400", + }, + metadata: { + keyColor: "text-green-600 dark:text-green-400", + cardColor: "border-2 border-green-200 dark:border-green-900", + titleColor: "text-green-700 dark:text-green-400", + }, + data: { + keyColor: "text-purple-600 dark:text-purple-400", + cardColor: "border-2 border-purple-200 dark:border-purple-900", + titleColor: "text-purple-700 dark:text-purple-400", + }, + type: { + keyColor: "text-pink-600 dark:text-pink-400", + cardColor: "border-2 border-pink-200 dark:border-pink-900", + titleColor: "text-pink-700 dark:text-pink-400", + }, +}; + +export const sections = [ + { + id: "apiVersion", + key: "apiVersion:", + value: "v1", + title: "apiVersion", + description: "The version of the Kubernetes API for Secret resources.", + }, + { + id: "kind", + key: "kind:", + value: "Secret", + title: "kind", + description: "Specifies the object type, here it is a Secret.", + }, + { + id: "metadata", + key: "metadata:", + value: `\n name: my-app-secret`, + title: "metadata", + description: "Metadata for the Secret, such as its name.", + }, + { + id: "type", + key: "type:", + value: " Opaque", + title: "type", + description: + "The type of Secret. 'Opaque' is the default for arbitrary user-defined data.", + }, + { + id: "data", + key: "data:", + value: `\n PASSWORD: cGFzc3dvcmQ= # base64 for 'password'`, + title: "data", + description: "Key-value pairs of secret data, base64-encoded.", + }, +]; diff --git a/docusaurus/src/components/KubernetesYAMLAnatomy/service.ts b/docusaurus/src/components/KubernetesYAMLAnatomy/service.ts new file mode 100644 index 0000000..2ce3d0b --- /dev/null +++ b/docusaurus/src/components/KubernetesYAMLAnatomy/service.ts @@ -0,0 +1,88 @@ +export const sectionStyles = { + apiVersion: { + keyColor: "text-blue-600 dark:text-blue-400", + cardColor: "border-2 border-blue-200 dark:border-blue-900", + titleColor: "text-blue-700 dark:text-blue-400", + }, + kind: { + keyColor: "text-cyan-600 dark:text-cyan-400", + cardColor: "border-2 border-cyan-200 dark:border-cyan-900", + titleColor: "text-cyan-700 dark:text-cyan-400", + }, + metadata: { + keyColor: "text-green-600 dark:text-green-400", + cardColor: "border-2 border-green-200 dark:border-green-900", + titleColor: "text-green-700 dark:text-green-400", + }, + spec: { + keyColor: "text-purple-600 dark:text-purple-400", + cardColor: "border-2 border-purple-200 dark:border-purple-900", + titleColor: "text-purple-700 dark:text-purple-400", + }, + selector: { + keyColor: "text-orange-600 dark:text-orange-400", + cardColor: "border-2 border-orange-200 dark:border-orange-900", + titleColor: "text-orange-700 dark:text-orange-400", + }, + ports: { + keyColor: "text-pink-600 dark:text-pink-400", + cardColor: "border-2 border-pink-200 dark:border-pink-900", + titleColor: "text-pink-700 dark:text-pink-400", + }, +}; + +export const sections = [ + { + id: "apiVersion", + key: "apiVersion:", + value: "v1", + title: "apiVersion", + description: "The version of the Kubernetes API to use.", + }, + { + id: "kind", + key: "kind:", + value: "Service", + title: "kind", + description: "Specifies the object type, in this case, a Service.", + }, + { + id: "metadata", + key: "metadata:", + value: ` + name: my-app-service`, + title: "metadata", + description: + "Data that helps uniquely identify the object, including a name.", + }, + { + id: "spec", + key: "spec:", + value: "", + title: "spec", + description: + "The desired state of the Service, defining how it exposes an application.", + }, + { + id: "selector", + key: "selector:", + value: ` + app: my-app`, + title: "spec.selector", + description: + "Selects the Pods to which this Service will route traffic, based on their labels.", + indent: 2, + }, + { + id: "ports", + key: "ports:", + value: ` + - protocol: TCP + port: 80 + targetPort: 8080`, + title: "spec.ports", + description: + "Defines the port mapping. It forwards traffic from port 80 on the Service to port 8080 on the Pods.", + indent: 2, + }, +]; diff --git a/docusaurus/src/components/MikrotikNetworking/CodeBlock/index.tsx b/docusaurus/src/components/MikrotikNetworking/CodeBlock/index.tsx new file mode 100644 index 0000000..a92bbdd --- /dev/null +++ b/docusaurus/src/components/MikrotikNetworking/CodeBlock/index.tsx @@ -0,0 +1,42 @@ +import { Highlight, themes } from "prism-react-renderer"; +import { useState } from "react"; + +interface CodeBlockProps { + code: string; +} + +const CodeBlock = ({ code }: CodeBlockProps) => { + const [copied, setCopied] = useState(false); + + const handleCopy = () => { + navigator.clipboard.writeText(code); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
    + + {({ className, style, tokens, getLineProps, getTokenProps }) => ( +
    +            {tokens.map((line, i) => (
    +              
    + {line.map((token, key) => ( + + ))} +
    + ))} +
    + )} +
    + +
    + ); +}; + +export default CodeBlock; diff --git a/docusaurus/src/components/MikrotikNetworking/CoreConcepts/index.tsx b/docusaurus/src/components/MikrotikNetworking/CoreConcepts/index.tsx new file mode 100644 index 0000000..f57ea9e --- /dev/null +++ b/docusaurus/src/components/MikrotikNetworking/CoreConcepts/index.tsx @@ -0,0 +1,105 @@ +import DeviceChart from "../DeviceChart"; +import Section from "../Section"; + +const CoreConcepts = () => ( +
    +
    +
    +

    + The Big Picture: Why We Segment +

    +

    + The goal is to move from a "flat" network, where all devices can talk + to each other by default, to a segmented network built on a{" "} + + Zero Trust + {" "} + philosophy. In a flat network, if a less secure device (like a phone + or an IoT gadget) gets compromised, an attacker can immediately see + and attempt to attack critical K3S servers. +

    +

    + By segmenting, we assume{" "} + no device is inherently trustworthy. We build digital + walls between groups of devices. Traffic can't cross these walls + unless we create a specific, explicit firewall rule to allow it. This + approach drastically reduces the attack surface and contains potential + breaches, which is essential when self-hosting production-grade + services. +

    +
    + +
    +

    + + VLANs + {" "} + : The Digital Walls for Isolation +

    +

    + + VLANs + {" "} + (Virtual LANs) are the primary tool used to build these digital walls. + It's helpful to think of the CRS326 switch not as one big switch, but + as a box containing four completely separate, smaller virtual + switches. Each VLAN (HOME_NET, K3S_CLUSTER, etc.) is one of these + virtual switches. +

    +

    + Devices plugged into ports assigned to VLAN 10 can talk to each other + at full speed, but they are fundamentally unaware that devices on VLAN + 20 even exist. This is called Layer 2 Isolation. It's + the most basic and powerful form of network separation, and the switch + hardware (the CRS326) enforces it at wire-speed. +

    +
    + +
    +

    + Router-on-a-Stick: The Guarded Gate for Control +

    +

    + Since the{" "} + + VLANs + {" "} + are isolated, we need a way to let some traffic pass between + them in a controlled way. This is the job of the router (the Lenovo + M920q). The "Router-on-a-Stick" (RoaS) model uses a single physical + cable, configured as a VLAN Trunk, to connect the + switch to the router. +

    +

    + Every packet of data that travels over this trunk cable gets a digital + "passport stamp" called an 802.1Q tag. This tag tells the router which + VLAN the packet came from. The router can then inspect the packet, + check it against the firewall rules, and decide if it's allowed to go + to its destination VLAN. If it is, the router stamps it with a new + passport for the destination VLAN and sends it back to the switch. + This process provides + centralized control and security inspection for all + cross-network communication. +

    +
    +
    +
    + +
    +
    +); + +export default CoreConcepts; diff --git a/docusaurus/src/components/MikrotikNetworking/DeviceChart/index.tsx b/docusaurus/src/components/MikrotikNetworking/DeviceChart/index.tsx new file mode 100644 index 0000000..f713fe0 --- /dev/null +++ b/docusaurus/src/components/MikrotikNetworking/DeviceChart/index.tsx @@ -0,0 +1,115 @@ +import { Chart, registerables } from "chart.js"; +import { useEffect, useRef } from "react"; + +Chart.register(...registerables); + +const DeviceChart = () => { + const chartRef = useRef(null); + const canvasRef = useRef(null); + + useEffect(() => { + if (!canvasRef.current) return; + + if (chartRef.current) { + chartRef.current.destroy(); + } + + const ctx = canvasRef.current.getContext("2d"); + if (!ctx) return; + + chartRef.current = new Chart(ctx, { + type: "bar", + data: { + labels: ["Lenovo M920q (Router)", "CRS326 (Switch)", "RB2011 (AP)"], + datasets: [ + { + label: "CPU Power (Normalized)", + data: [100, 3.8, 2.9], + backgroundColor: "rgba(255, 171, 0, 0.6)", + borderColor: "rgba(255, 171, 0, 1)", + borderWidth: 1, + }, + { + label: "RAM (Normalized)", + data: [100, 3.2, 0.8], + backgroundColor: "rgba(96, 165, 250, 0.6)", + borderColor: "rgba(96, 165, 250, 1)", + borderWidth: 1, + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + y: { + beginAtZero: true, + grid: { color: "rgba(255, 255, 255, 0.1)" }, + ticks: { + color: "#9ca3af", + callback: (value: string | number) => value + "%", + }, + }, + x: { + grid: { display: false }, + ticks: { color: "#9ca3af" }, + }, + }, + plugins: { + legend: { labels: { color: "#d1d5db" } }, + title: { + display: true, + text: "Relative Hardware Capability", + color: "#f9fafb", + font: { size: 16 }, + }, + tooltip: { + callbacks: { + label: function (context: any) { + let label = context.dataset.label || ""; + if (label) { + label += ": "; + } + if (context.dataset.label === "CPU Power (Normalized)") { + if (context.label === "Lenovo M920q (Router)") + label += "Intel Core i5-8500T, 6C/6T, up to 3.5 GHz"; + if (context.label === "CRS326 (Switch)") + label += "800 MHz Single-Core"; + if (context.label === "RB2011 (AP)") + label += "600 MHz Single-Core"; + } else { + if (context.label === "Lenovo M920q (Router)") + label += "16 GB DDR4 2666 MHz"; + if (context.label === "CRS326 (Switch)") label += "512 MB"; + if (context.label === "RB2011 (AP)") label += "128 MB"; + } + return label; + }, + }, + }, + }, + }, + }); + + return () => { + if (chartRef.current) { + chartRef.current.destroy(); + } + }; + }, []); + + return ( +
    +

    Device Roles & Strengths

    +

    + Each device is assigned a role that plays to its hardware strengths, + ensuring optimal performance. +

    +
    + +
    +
    + ); +}; + +export default DeviceChart; diff --git a/docusaurus/src/components/MikrotikNetworking/DeviceConfiguration/index.tsx b/docusaurus/src/components/MikrotikNetworking/DeviceConfiguration/index.tsx new file mode 100644 index 0000000..ba15e9e --- /dev/null +++ b/docusaurus/src/components/MikrotikNetworking/DeviceConfiguration/index.tsx @@ -0,0 +1,50 @@ +import { useState } from "react"; +import Accordion from "../../Core/Accordion"; +import CodeBlock from "../CodeBlock"; +import { deviceConfigData } from "../data"; +import Section from "../Section"; +import { cx } from "../utils"; + +const DeviceConfiguration = () => { + const [activeTab, setActiveTab] = + useState("lenovoM920q"); + const activeDevice = deviceConfigData[activeTab]; + + return ( +
    +
    + +
    + item.title} + renderContent={item => ( + <> +

    {item.description}

    + + + )} + stepPrefix={idx => ( + Step {idx + 1}: + )} + /> +
    + ); +}; + +export default DeviceConfiguration; diff --git a/docusaurus/src/components/MikrotikNetworking/FirewallLogic/index.tsx b/docusaurus/src/components/MikrotikNetworking/FirewallLogic/index.tsx new file mode 100644 index 0000000..c5d6484 --- /dev/null +++ b/docusaurus/src/components/MikrotikNetworking/FirewallLogic/index.tsx @@ -0,0 +1,25 @@ +import Accordion from "../../Core/Accordion"; +import CodeBlock from "../CodeBlock"; +import { firewallConfigData } from "../data"; +import Section from "../Section"; + +const FirewallLogic = () => ( +
    + item.title} + renderContent={item => ( + <> +

    {item.description}

    + + + )} + stepPrefix={idx => Step {idx + 1}:} + /> +
    +); + +export default FirewallLogic; diff --git a/docusaurus/src/components/MikrotikNetworking/NetworkOverview/index.tsx b/docusaurus/src/components/MikrotikNetworking/NetworkOverview/index.tsx new file mode 100644 index 0000000..eea3c36 --- /dev/null +++ b/docusaurus/src/components/MikrotikNetworking/NetworkOverview/index.tsx @@ -0,0 +1,86 @@ +import Section from "../Section"; +import { cx } from "../utils"; + +const DiagramBox = ({ + children, + className = "", +}: { + children: React.ReactNode; + className?: string; +}) => ( +
    + {children} +
    +); + +const NetworkOverview = () => ( +
    +

    + As you may have noticed in the Hardware section, I'm a big fan of + Mikrotik. I've been using them for years and they've always been reliable. + If you would ask me besides that why, I couldn't tell you. I just like + them. +

    +
    +
    + +

    ☁️ Internet

    +
    +
    + +

    Lenovo M920q Router (The Brain)

    +

    Inter-VLAN Routing, Firewall, VPN, Scripting

    +
    +
    + VLAN Trunk (Single Cable) +
    +
    + +

    CRS326 Switch (The Muscle)

    +

    High-Speed Layer 2 Switching

    +
    +
    +
    +
    +

    + VLAN 20: K3S Cluster +

    +
    + +

    K3S Nodes

    +

    (RPis, Mini PCs)

    +
    +
    +
    +

    + VLAN 10: Home Network +

    +
    + +

    PCs & Laptops

    +

    (Wired)

    +
    +
    +
    +

    + Wireless Networks +

    +
    + +

    RB2011 AP

    +

    SSID: Home (VLAN 10)

    +

    SSID: Guest (VLAN 99)

    +
    +
    +
    +
    +
    +
    +); + +export default NetworkOverview; diff --git a/docusaurus/src/components/MikrotikNetworking/Scenarios/index.tsx b/docusaurus/src/components/MikrotikNetworking/Scenarios/index.tsx new file mode 100644 index 0000000..03ee8f4 --- /dev/null +++ b/docusaurus/src/components/MikrotikNetworking/Scenarios/index.tsx @@ -0,0 +1,25 @@ +import Accordion from "../../Core/Accordion"; +import CodeBlock from "../CodeBlock"; +import { scenariosConfigData } from "../data"; +import Section from "../Section"; + +const Scenarios = () => ( +
    + item.title} + renderContent={item => ( + <> +

    {item.description}

    + + + )} + stepPrefix={idx => Step {idx + 1}:} + /> +
    +); + +export default Scenarios; diff --git a/docusaurus/src/components/MikrotikNetworking/Section/index.tsx b/docusaurus/src/components/MikrotikNetworking/Section/index.tsx new file mode 100644 index 0000000..b6877b2 --- /dev/null +++ b/docusaurus/src/components/MikrotikNetworking/Section/index.tsx @@ -0,0 +1,19 @@ +import { ReactNode } from "react"; + +interface SectionProps { + title?: string; + description?: string; + children: ReactNode; +} + +const Section = ({ title, description, children }: SectionProps) => ( +
    + {" "} + {/* anchor for sidebar links */} + {title &&

    {title}

    } + {description &&

    {description}

    } + {children} +
    +); + +export default Section; diff --git a/docusaurus/src/components/MikrotikNetworking/Sidebar/index.tsx b/docusaurus/src/components/MikrotikNetworking/Sidebar/index.tsx new file mode 100644 index 0000000..cb8df3d --- /dev/null +++ b/docusaurus/src/components/MikrotikNetworking/Sidebar/index.tsx @@ -0,0 +1,48 @@ +import { cx } from "../utils"; + +interface SidebarProps { + activeSection: string; + setActiveSection: (id: string) => void; +} + +const Sidebar = ({ activeSection, setActiveSection }: SidebarProps) => { + const navItems = [ + { id: "overview", label: "Network Overview", icon: "🌐" }, + { id: "concepts", label: "Core Concepts", icon: "💡" }, + { id: "schema", label: "VLAN Schema", icon: "📋" }, + { id: "config", label: "Device Configuration", icon: "⚙️" }, + { id: "firewall", label: "Firewall Logic", icon: "🛡️" }, + { id: "scenarios", label: "Common Scenarios", icon: "🔧" }, + { id: "summary", label: "Summary & Checklist", icon: "✅" }, + ]; + + return ( + + ); +}; + +export default Sidebar; diff --git a/docusaurus/src/components/MikrotikNetworking/Summary/index.tsx b/docusaurus/src/components/MikrotikNetworking/Summary/index.tsx new file mode 100644 index 0000000..c2743ca --- /dev/null +++ b/docusaurus/src/components/MikrotikNetworking/Summary/index.tsx @@ -0,0 +1,48 @@ +import Accordion from "../../Core/Accordion"; +import CodeBlock from "../CodeBlock"; +import { hardeningConfigData } from "../data"; +import Section from "../Section"; + +const Summary = () => ( +
    +
    +

    + Key Achievements +

    +
      +
    • + Robust Segmentation: The K3S cluster + is now securely isolated. +
    • +
    • + Centralized Security: All traffic is + inspected by the Lenovo M920q's powerful firewall. +
    • +
    • + Optimized Performance: Each device is + used for its intended purpose. +
    • +
    • + Secure Management: A dedicated + management VLAN protects the network equipment. +
    • +
    +
    + item.title} + renderContent={item => ( + <> +

    {item.description}

    + + + )} + stepPrefix={idx => Step {idx + 1}:} + /> +
    +); + +export default Summary; diff --git a/docusaurus/src/components/MikrotikNetworking/VlanSchema/index.tsx b/docusaurus/src/components/MikrotikNetworking/VlanSchema/index.tsx new file mode 100644 index 0000000..ea9760a --- /dev/null +++ b/docusaurus/src/components/MikrotikNetworking/VlanSchema/index.tsx @@ -0,0 +1,94 @@ +import Section from "../Section"; +import { cx } from "../utils"; + +interface VlanCardProps { + vlan: string; + name: string; + subnet: string; + description: string; + tag: string; + borderColor: string; +} + +const VlanCard = ({ + vlan, + name, + subnet, + description, + tag, + borderColor, +}: VlanCardProps) => ( +
    +
    +
    + {vlan} +
    + {name} +
    +
    + + {subnet} + +
    + Subnet +
    +
    +

    {description}

    +
    +
    + + {tag} + +
    +
    +
    +); + +const VlanSchema = () => ( +
    +
    + + + + +
    +
    +); + +export default VlanSchema; diff --git a/docusaurus/src/components/MikrotikNetworking/data.ts b/docusaurus/src/components/MikrotikNetworking/data.ts new file mode 100644 index 0000000..0672340 --- /dev/null +++ b/docusaurus/src/components/MikrotikNetworking/data.ts @@ -0,0 +1,426 @@ +/* + Centralized data store for the MikroTik Networking view. + Keeps large static objects isolated from component logic. +*/ + +export const deviceConfigData = { + lenovoM920q: { + title: "Lenovo M920q Router", + steps: [ + { + title: "Internet Connection (PPPoE on VLAN)", + description: + "First, we establish the internet connection. My Telenor ISP requires a specific VLAN (101) for the connection, so we create a VLAN interface and then run the PPPoE client on top of it. Here in Denmark at least, to get the PPPoE credentials, you need to call the customer service. They will send you the credentials via E-mail. This step might be different for your ISP.", + code: `# Create the VLAN interface on the physical WAN port, I'm using ether1. +# This is the port that is connected to the Telenor modem. +/interface vlan +add name=vlan101-WAN vlan-id=101 interface=ether1 + +# Create the PPPoE client on the new VLAN interface +# Replace with your actual ISP username and password +/interface pppoe-client +add name="Telenor PPPoE" interface=vlan101-WAN user="your_telenor_username" password="your_telenor_password" add-default-route=yes use-peer-dns=no disabled=no + `, + }, + { + title: "DNS", + description: + "We need to set the DNS servers for the router. I'm using Cloudflare's DNS servers. This is my personal preference, you can use any DNS server you want. We might later in time setup our own DNS server, e.g. Technitium, but for now we will use Cloudflare.", + code: `# allow-remote-requests is set to yes to allow the router to resolve domain names. +# Without this, the router will not be able to resolve domain names. +/ip dns set servers=1.1.1.1,1.0.0.1 allow-remote-requests=yes + `, + }, + { + title: "Bridge & VLAN Interfaces", + description: + "Next, we create a bridge to group our internal networks and attach the virtual VLAN interfaces. The uplink to our switch uses ether2, which corresponds to the first SFP+ port on our Lenovo M920q. This is because, after manually installing MikroTik RouterOS, the Intel X520-DA2 SFP+ ports are detected as ether2 and ether3. If you use different hardware, your uplink port name may differ, just update the code accordingly.", + code: `/interface bridge +add name=main-bridge vlan-filtering=no +/interface bridge port +add bridge=main-bridge interface=ether2 comment="Trunk to CRS326" + +/interface vlan +add interface=main-bridge name=VLAN10_HOME vlan-id=10 +add interface=main-bridge name=VLAN20_K3S vlan-id=20 +add interface=main-bridge name=VLAN88_MGMT vlan-id=88 +add interface=main-bridge name=VLAN99_GUEST vlan-id=99`, + }, + { + title: "IP Addresses", + description: "Assign gateway IP addresses to each VLAN interface.", + code: `/ip address +add address=192.168.10.1/24 interface=VLAN10_HOME comment="Gateway for Home LAN" +add address=192.168.20.1/24 interface=VLAN20_K3S comment="Gateway for K3S Cluster" +add address=192.168.88.1/24 interface=VLAN88_MGMT comment="Gateway for Management" +add address=192.168.99.1/24 interface=VLAN99_GUEST comment="Gateway for Guest WiFi" +`, + }, + { + title: "Create IP Pools", + description: + "Create address pools that will be used by the DHCP servers. This is the range of IP addresses that will be assigned to the clients.", + code: `/ip pool +add name=pool_home ranges=192.168.10.100-192.168.10.254 +add name=pool_k3s ranges=192.168.20.100-192.168.20.254 +add name=pool_mgmt ranges=192.168.88.10-192.168.88.20 +add name=pool_guest ranges=192.168.99.100-192.168.99.254`, + }, + { + title: "DHCP Servers", + description: + "Create DHCP servers so clients on each VLAN receive IP addresses automatically.", + code: `/ip dhcp-server +add address-pool=pool_home interface=VLAN10_HOME name=dhcp_home +add address-pool=pool_k3s interface=VLAN20_K3S name=dhcp_k3s +add address-pool=pool_mgmt interface=VLAN88_MGMT name=dhcp_mgmt +add address-pool=pool_guest interface=VLAN99_GUEST name=dhcp_guest`, + }, + { + title: "DHCP Networks", + description: + "Create DHCP networks so clients receive IP addresses automatically, and also set the DNS server to the router's IP address. DNS is crucial for the clients to be able to resolve the domain names, especially for the K3S cluster. Our K3S machines will be using static DNS records, so the cluster has to be able to resolve the domain names. In simple words, during the setup we will not be using IP's but domain names.", + code: `/ip dhcp-server network +add address=192.168.10.0/24 gateway=192.168.10.1 dns-server=192.168.10.1 +add address=192.168.20.0/24 gateway=192.168.20.1 dns-server=192.168.20.1 +add address=192.168.88.0/24 gateway=192.168.88.1 dns-server=192.168.88.1 +add address=192.168.99.0/24 gateway=192.168.99.1 dns-server=192.168.99.1`, + }, + { + title: "VLAN Filtering on Router", + description: + "Finally for the router, we configure the bridge VLAN table and enable filtering.", + code: `/interface bridge vlan +add bridge=main-bridge tagged=main-bridge,sfp1 vlan-ids=10,20,88,99 + +/interface bridge +set main-bridge vlan-filtering=yes`, + }, + ], + }, + crs326: { + title: "CRS326 Switch", + steps: [ + { + title: "Create Main Bridge", + description: + "Create the main bridge interface to aggregate all switch ports. This is the foundation for VLAN-aware switching.", + code: `/interface bridge +add name=main-bridge`, + }, + { + title: "Add All Physical Ports", + description: + "Add all physical ports (ether1-24 and both SFP+ ports) to the bridge. This ensures all traffic is handled by the bridge.", + code: `/interface bridge port +add bridge=main-bridge interface=ether1 +add bridge=main-bridge interface=ether2 +add bridge=main-bridge interface=ether3 +add bridge=main-bridge interface=ether4 +add bridge=main-bridge interface=ether5 +add bridge=main-bridge interface=ether6 +add bridge=main-bridge interface=ether7 +add bridge=main-bridge interface=ether8 +add bridge=main-bridge interface=ether9 +add bridge=main-bridge interface=ether10 +add bridge=main-bridge interface=ether11 +add bridge=main-bridge interface=ether12 +add bridge=main-bridge interface=ether13 +add bridge=main-bridge interface=ether14 +add bridge=main-bridge interface=ether15 +add bridge=main-bridge interface=ether16 +add bridge=main-bridge interface=ether17 +add bridge=main-bridge interface=ether18 +add bridge=main-bridge interface=ether19 +add bridge=main-bridge interface=ether20 +add bridge=main-bridge interface=ether21 +add bridge=main-bridge interface=ether22 +add bridge=main-bridge interface=ether23 +add bridge=main-bridge interface=ether24 +add bridge=main-bridge interface=sfp-sfpplus1 +add bridge=main-bridge interface=sfp-sfpplus2`, + }, + { + title: "Management VLAN & IP", + description: + "Create the management VLAN interface, assign the switch's management IP, and set the default route. This allows management access on VLAN 88.", + code: `/interface vlan +add interface=main-bridge name=VLAN88_MGMT vlan-id=88 + +/ip address +add address=192.168.88.2/24 interface=VLAN88_MGMT +/ip route +add gateway=192.168.88.1`, + }, + { + title: "Set Port VLAN IDs (PVIDs)", + description: + "Assign the correct PVIDs to access ports. ether2 is where my PC is connected (VLAN 10), and ether17-24 are for the K3S cluster (VLAN 20). This might differ for you, but the gist of it is that you need to assign the correct PVIDs to the correct ports.", + code: `/interface bridge port +# Your PC is on ether2, its traffic belongs to VLAN 10. +set [find where interface=ether2] pvid=10 +# Your K3S Cluster is on ether17-24, its traffic belongs to VLAN 20. +set [find where interface=ether17] pvid=20 +set [find where interface=ether18] pvid=20 +set [find where interface=ether19] pvid=20 +set [find where interface=ether20] pvid=20 +set [find where interface=ether21] pvid=20 +set [find where interface=ether22] pvid=20 +set [find where interface=ether23] pvid=20 +set [find where interface=ether24] pvid=20`, + }, + { + title: "VLAN Table", + description: + "Build the VLAN table with explicit rules for tagged and untagged ports for each VLAN.", + code: `/interface bridge vlan +add bridge=main-bridge tagged=sfp-sfpplus1,ether1 untagged=ether2 vlan-ids=10 comment="PC on eth2, AP on eth1, Router on SFP1" +add bridge=main-bridge tagged=sfp-sfpplus1 untagged=ether17,ether18,ether19,ether20,ether21,ether22,ether23,ether24 vlan-ids=20 +add bridge=main-bridge tagged=main-bridge,sfp-sfpplus1,ether1 vlan-ids=88 comment="Management access for Switch CPU, Router, AP" +add bridge=main-bridge tagged=sfp-sfpplus1,ether1 vlan-ids=99`, + }, + { + title: "Enable VLAN Filtering", + description: + "Enable VLAN filtering on the bridge. This activates all VLAN rules and enforces isolation as configured.", + code: `/interface bridge set main-bridge vlan-filtering=yes`, + }, + { + title: "Recommended Enhancements", + description: + "Optional but recommended settings for improved performance, security, and reliability. These include protocol mode, edge/portfast, hardware offloading, loop protection, and snooping features.", + code: `/interface bridge +set main-bridge protocol-mode=rstp +set main-bridge loop-protect=yes +set main-bridge igmp-snooping=yes +set main-bridge dhcp-snooping=yes + +/interface bridge port +# Enable edge (portfast) on access ports +set [find where interface=ether2] edge=yes +set [find where interface=ether17] edge=yes +set [find where interface=ether18] edge=yes +set [find where interface=ether19] edge=yes +set [find where interface=ether20] edge=yes +set [find where interface=ether21] edge=yes +set [find where interface=ether22] edge=yes +set [find where interface=ether23] edge=yes +set [find where interface=ether24] edge=yes + +# Ensure hardware offloading is enabled on all ports +set [find] hw=yes`, + }, + ], + }, + rb2011: { + title: "RB2011 AP", + steps: [ + { + title: "Create Bridge & Add Ports", + description: + "Create a single bridge to link the wired uplink (ether1) and the wireless SSIDs.", + code: `/interface bridge add name=ap-bridge +/interface bridge port +add bridge=ap-bridge interface=ether1 +add bridge=ap-bridge interface=wlan_home_ssid +add bridge=ap-bridge interface=wlan_guest_ssid`, + }, + { + title: "Configure Wireless Radio & Security", + description: + "Set up the main wireless radio and security profiles for home and guest networks.", + code: `/interface wireless +set [find default-name=wlan1] mode=ap-bridge band=2ghz-b/g/n channel-width=20/40mhz-Ce frequency=auto country=denmark disabled=no hide-ssid=yes +/interface wireless security-profiles +set [find default=yes] mode=dynamic-keys authentication-types=wpa2-psk wpa2-pre-shared-key="Your_Strong_Home_Password" name=prof_home +add name=prof_guest mode=dynamic-keys authentication-types=wpa2-psk wpa2-pre-shared-key="Your_Simple_Guest_Password"`, + }, + { + title: "Create VLAN-Aware Virtual SSIDs", + description: + "Create virtual SSIDs for Home and Guest, each with their own VLAN tag. This is the key to proper network separation.", + code: `/interface wireless +add name=wlan_home_ssid master-interface=wlan1 ssid="MyHomeWiFi" security-profile=prof_home vlan-mode=use-tag vlan-id=10 +add name=wlan_guest_ssid master-interface=wlan1 ssid="MyGuestWiFi" security-profile=prof_guest vlan-mode=use-tag vlan-id=99`, + }, + { + title: "Management VLAN & IP Setup", + description: + "Add the management VLAN interface, assign the management IP, and set the default route.", + code: `/interface vlan add name=VLAN88_MGMT interface=ether1 vlan-id=88 +/ip address add address=192.168.88.3/24 interface=VLAN88_MGMT +/ip route add gateway=192.168.88.1`, + }, + { + title: "Recommended Enhancements", + description: + "Optional but recommended settings for improved security and reliability.", + code: `/interface bridge +# Use RSTP to prevent loops +set ap-bridge protocol-mode=rstp + +/interface bridge port +# Enable edge (portfast) on SSIDs for faster client connections +set [find interface=wlan_home_ssid] edge=yes +set [find interface=wlan_guest_ssid] edge=yes + +/ip service +disable [find name=telnet] +disable [find name=ftp] +disable [find name=api] +disable [find name=api-ssl]`, + }, + ], + }, +}; + +export const firewallConfigData = { + title: "Firewall Configuration (on Lenovo M920q)", + steps: [ + { + title: "Interface Lists", + description: + "We will use interface lists to create clean, scalable, and human-readable firewall rules. This is a best practice.", + code: `/interface list +add name=LAN comment="All internal interfaces" +add name=WAN comment="All external/internet interfaces" +add name=VLANs comment="All VLAN interfaces" +add name=TRUSTED comment="Trusted user networks" +add name=UNTRUSTED comment="Untrusted/isolated networks" + +/interface list member +# Ensure the WAN interface list points to the PPPoE connection +add list=WAN interface="Telenor PPPoE" +add list=VLANs interface=VLAN10_HOME,VLAN20_K3S,VLAN88_MGMT,VLAN99_GUEST +add list=LAN interface=VLAN10_HOME,VLAN20_K3S,VLAN88_MGMT,VLAN99_GUEST +add list=TRUSTED interface=VLAN10_HOME,VLAN88_MGMT +add list=UNTRUSTED interface=VLAN20_K3S,VLAN99_GUEST`, + }, + { + title: "Address Lists for Egress Control", + description: + "For core services like cert-manager to function, we need to allow specific outbound connections from our otherwise isolated K3S cluster. We use dynamic address lists to securely manage this.", + code: `# This example uses Cloudflare. Replace the FQDN with your DNS provider's API endpoint. +# The router will resolve and keep this IP list updated automatically. +/ip firewall address-list +add address=api.cloudflare.com list=dns-provider-apis comment="Cloudflare API for cert-manager" +# Add Cloudflare IPs for ingress filtering +add address=1.1.1.1 list=cloudflare-ips +add address=1.0.0.1 list=cloudflare-ips`, + }, + { + title: "Input Chain (Traffic to Router)", + description: + "These rules protect the router itself. The order of these rules is critical.", + code: `/ip firewall filter +add action=accept chain=input connection-state=established,related,untracked comment="Allow established/related" +add action=drop chain=input connection-state=invalid comment="Drop invalid" +add action=accept chain=input protocol=icmp comment="Allow Ping to router" +add action=accept chain=input protocol=udp in-interface-list=LAN dst-port=53 comment="Allow LAN DNS queries" +add action=accept chain=input protocol=tcp in-interface-list=LAN dst-port=53 comment="Allow LAN DNS queries (TCP)" +add action=accept chain=input src-address=192.168.10.253 comment="Allow my PC to manage router" +add action=drop chain=input in-interface-list=WAN comment="Drop all other input from WAN" +add action=drop chain=input comment="Drop all other input"`, + }, + { + title: "Forward Chain (Traffic through Router)", + description: + "Here we control traffic between VLANs and the internet, enforcing our isolation policies.", + code: `/ip firewall filter +add action=accept chain=forward connection-state=established,related,untracked comment="Allow established/related" +add action=drop chain=forward connection-state=invalid comment="Drop invalid" +add action=accept chain=forward connection-state=new connection-nat-state=dstnat protocol=tcp dst-address=192.168.20.241 src-address-list=cloudflare-ips in-interface-list=WAN dst-port=80,443 comment="Allow incoming K3S traffic from Cloudflare" +add action=drop chain=forward connection-state=new connection-nat-state=!dstnat in-interface-list=WAN comment="Drop new connections from WAN not DSTNATed" +add action=accept chain=forward in-interface-list=TRUSTED out-interface-list=WAN comment="Allow trusted VLANs to access internet" +add action=accept chain=forward protocol=tcp src-address=192.168.20.0/24 dst-address-list=dns-provider-apis dst-port=443 comment="Allow K3S to contact DNS provider for certs" +add action=accept chain=forward src-address=192.168.10.253 dst-address=192.168.20.0/24 comment="Allow my PC to access K3S" +add action=accept chain=forward src-address=192.168.10.253 dst-address=192.168.88.0/24 comment="Allow my PC to access Management VLAN" +add action=accept chain=forward src-address=192.168.20.0/24 out-interface-list=WAN comment="Allow K3S Cluster outbound internet" +add action=drop chain=forward comment="Drop all other forward"`, + }, + { + title: "NAT (Port Forwarding, Hairpin, and Masquerade)", + description: + "These rules handle DNS bypass for K3S, port forwarding for HTTP/HTTPS to K3S Ingress, hairpin NAT for internal access, and masquerade for internet access.", + code: `/ip firewall nat +add chain=dstnat action=accept protocol=udp src-address=192.168.20.0/24 dst-port=53 comment="Allow K3S DNS to bypass redirect" +add chain=dstnat action=dst-nat to-addresses=192.168.20.241 to-ports=80 protocol=tcp in-interface-list=WAN dst-port=80 comment="Forward HTTP to K3S Ingress" +add chain=dstnat action=dst-nat to-addresses=192.168.20.241 to-ports=443 protocol=tcp in-interface-list=WAN dst-port=443 comment="Forward HTTPS to K3S Ingress" +add chain=srcnat action=masquerade src-address=192.168.0.0/16 dst-address=192.168.20.241 comment="Correct Hairpin NAT" +add chain=srcnat action=masquerade src-address=192.168.0.0/16 out-interface-list=WAN comment="Main NAT rule for Internet Access"`, + }, + ], +}; + +export const scenariosConfigData = { + title: "Common Scenarios & Firewall Exceptions", + steps: [ + { + title: "Scenario 1: Granting Developer Access to K3S", + description: + "A common need is to allow a specific, trusted developer machine on the HOME_NET (VLAN 10) to have full access to the K3S_CLUSTER (VLAN 20) for management with tools like kubectl and ssh. We do this by adding a targeted 'accept' rule to the forward chain. This rule must be placed BEFORE the general 'Drop all inter-VLAN traffic' rule to be effective.", + code: `# First, give the developer PC a static IP via DHCP lease, e.g., 192.168.10.50 + +# Add this firewall rule. It allows the PC at 192.168.10.50 to access anything on the K3S network. +/ip firewall filter +add action=accept chain=forward src-address=192.168.10.50 out-interface=VLAN20_K3S \ +comment="Allow Dev PC full access to K3S Cluster" \ +place-before=[find where comment="Drop all inter-VLAN traffic by default"]`, + }, + { + title: "Scenario 2: Exposing a Service to the Internet (Port Forwarding)", + description: + "Our firewall correctly blocks all incoming connections from the internet by default. To expose a service (like a web server) from the K3S cluster, we must create an explicit Destination NAT (DNAT) rule. This rule tells the router to forward traffic arriving on a specific public port to an internal IP and port within the K3S network. We also need a corresponding filter rule to allow this forwarded traffic.", + code: `# Example: Expose a web service running at 192.168.20.150:30443 to the public on port 443 (HTTPS). + +# 1. The DNAT Rule: Forwards incoming internet traffic on port 443 to the internal K3S service. +/ip firewall nat +add action=dst-nat chain=dstnat dst-port=443 in-interface-list=WAN protocol=tcp \ +to-addresses=192.168.20.150 to-ports=30443 comment="Forward Web Traffic to K3S" + +# 2. The Filter Rule: Allows this specific forwarded traffic to pass through the firewall. +# This rule should be placed BEFORE the final 'Drop all other forward' rule. +/ip firewall filter +add action=accept chain=forward connection-nat-state=dstnat \ +in-interface-list=WAN out-interface=VLAN20_K3S protocol=tcp \ +comment="Allow DNAT web traffic to K3S" \ +place-before=[find where comment="Drop all other forward"]`, + }, + ], +}; + +export const hardeningConfigData = { + title: "Final Hardening Steps", + steps: [ + { + title: "Secure MAC Server (on ALL devices)", + description: + "The MAC server allows Layer 2 access via Winbox, bypassing IP firewall rules. We must restrict it to the management network.", + code: `# Run on Lenovo M920q, CRS326, and RB2011 +/tool mac-server +set allowed-interface-list=TRUSTED +/tool mac-server mac-winbox +set allowed-interface-list=TRUSTED`, + }, + { + title: "System Security (on ALL devices)", + description: + "Disable non-essential services and ensure strong user credentials are set.", + code: `# This is a checklist, not a single script. +# 1. Set a strong password for the 'admin' user or create a new admin user. +/user set [find name=admin] password="A_Very_Strong_Password" + +# 2. Disable unused services to reduce attack surface. +/ip service +disable [find name=telnet] +disable [find name=ftp] +disable [find name=api] +disable [find name=api-ssl] + +# 3. (Optional) Restrict management access to a specific IP or subnet. +# This adds another layer of security. +/ip service set [find name=winbox] address=192.168.88.0/24 +/ip service set [find name=ssh] address=192.168.88.0/24`, + }, + ], +}; diff --git a/docusaurus/src/components/MikrotikNetworking/index.tsx b/docusaurus/src/components/MikrotikNetworking/index.tsx new file mode 100644 index 0000000..071eca3 --- /dev/null +++ b/docusaurus/src/components/MikrotikNetworking/index.tsx @@ -0,0 +1,37 @@ +import { useState } from "react"; +import CoreConcepts from "./CoreConcepts"; +import DeviceConfiguration from "./DeviceConfiguration"; +import FirewallLogic from "./FirewallLogic"; +import NetworkOverview from "./NetworkOverview"; +import Scenarios from "./Scenarios"; +import Sidebar from "./Sidebar"; +import Summary from "./Summary"; +import VlanSchema from "./VlanSchema"; + +export default function MikrotikNetworkingApp() { + const [activeSection, setActiveSection] = useState("overview"); + + const sections: Record = { + overview: , + concepts: , + schema: , + config: , + firewall: , + scenarios: , + summary: , + }; + + return ( +
    +
    + +
    + {sections[activeSection]} +
    +
    +
    + ); +} diff --git a/docusaurus/src/components/MikrotikNetworking/utils.ts b/docusaurus/src/components/MikrotikNetworking/utils.ts new file mode 100644 index 0000000..0f71d4c --- /dev/null +++ b/docusaurus/src/components/MikrotikNetworking/utils.ts @@ -0,0 +1,2 @@ +export const cx = (...classes: (string | false | null | undefined)[]): string => + classes.filter(Boolean).join(" "); diff --git a/docusaurus/src/components/Tabs/index.tsx b/docusaurus/src/components/Tabs/index.tsx new file mode 100644 index 0000000..5a1446d --- /dev/null +++ b/docusaurus/src/components/Tabs/index.tsx @@ -0,0 +1,21 @@ +const Tabs = ({ tabs, activeTab, setActiveTab }) => { + return ( +
    + {tabs.map(tab => ( + + ))} +
    + ); +}; + +export default Tabs; diff --git a/docusaurus/src/css/custom.css b/docusaurus/src/css/custom.css new file mode 100644 index 0000000..d0697bd --- /dev/null +++ b/docusaurus/src/css/custom.css @@ -0,0 +1,53 @@ +@import "tailwindcss"; +@import "react-image-gallery/styles/css/image-gallery.css"; + +@custom-variant dark (&:is([data-theme="dark"] *)); + +/** + * Any CSS included here will be global. The classic template + * bundles Infima by default. Infima is a CSS framework designed to + * work well for content-centric websites. + */ + +/* You can override the default Infima variables here. */ +:root { + --ifm-color-primary: #ffab00; + --ifm-color-primary-dark: #ffab00; + --ifm-color-primary-darker: #ffab00; + --ifm-color-primary-darkest: #ffab00; + --ifm-color-primary-light: #818cf8; + --ifm-color-primary-lighter: #a5b4fc; + --ifm-color-primary-lightest: #c7d2fe; + --ifm-code-font-size: 95%; + --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); +} + +/* For readability concerns, you should choose a lighter palette in dark mode. */ +[data-theme="dark"] { + --ifm-color-primary: #ffab00; + --ifm-color-primary-dark: #ffab00; + --ifm-color-primary-darker: #ffab00; + --ifm-color-primary-darkest: #ffab00; + --ifm-color-primary-light: #818cf8; + --ifm-color-primary-lighter: #a5b4fc; + --ifm-color-primary-lightest: #c7d2fe; + --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); + --ifm-background-color: #242323 !important; + --ifm-navbar-background-color: #242323 !important; +} + +.navbar__item.navbar__link svg { + display: none; +} + +ul:not(.menu__list) li { + list-style-type: disc; +} + +.table-of-contents { + padding-left: 24px !important; +} + +.table-of-contents li { + list-style-type: none !important; +} diff --git a/docusaurus/src/pages/index.module.css b/docusaurus/src/pages/index.module.css new file mode 100644 index 0000000..9f71a5d --- /dev/null +++ b/docusaurus/src/pages/index.module.css @@ -0,0 +1,23 @@ +/** + * CSS files with the .module.css suffix will be treated as CSS modules + * and scoped locally. + */ + +.heroBanner { + padding: 4rem 0; + text-align: center; + position: relative; + overflow: hidden; +} + +@media screen and (max-width: 996px) { + .heroBanner { + padding: 2rem; + } +} + +.buttons { + display: flex; + align-items: center; + justify-content: center; +} diff --git a/docusaurus/src/pages/index.tsx b/docusaurus/src/pages/index.tsx new file mode 100644 index 0000000..92acf12 --- /dev/null +++ b/docusaurus/src/pages/index.tsx @@ -0,0 +1,6 @@ +import { Redirect } from '@docusaurus/router'; + +export default function Home(): JSX.Element { + return ; + +} diff --git a/docusaurus/src/plugins/tailwind-config.js b/docusaurus/src/plugins/tailwind-config.js new file mode 100644 index 0000000..158cca7 --- /dev/null +++ b/docusaurus/src/plugins/tailwind-config.js @@ -0,0 +1,9 @@ +module.exports = function tailwindPlugin(context, options) { + return { + name: "tailwind-plugin", + configurePostCss(postcssOptions) { + postcssOptions.plugins = [require("@tailwindcss/postcss")]; + return postcssOptions; + }, + }; +}; \ No newline at end of file diff --git a/docusaurus/static/.nojekyll b/docusaurus/static/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/docusaurus/static/ansible/host_vars/hp.yml b/docusaurus/static/ansible/host_vars/hp.yml new file mode 100644 index 0000000..ede82ac --- /dev/null +++ b/docusaurus/static/ansible/host_vars/hp.yml @@ -0,0 +1,2 @@ +ansible_host: 192.168.20.102 +ansible_user: agjs diff --git a/docusaurus/static/ansible/host_vars/lenovo.yml b/docusaurus/static/ansible/host_vars/lenovo.yml new file mode 100644 index 0000000..a0e15d9 --- /dev/null +++ b/docusaurus/static/ansible/host_vars/lenovo.yml @@ -0,0 +1,2 @@ +ansible_host: k3s-worker-lenovo.cluster +ansible_user: agjs diff --git a/docusaurus/static/ansible/host_vars/rp_1.yml b/docusaurus/static/ansible/host_vars/rp_1.yml new file mode 100644 index 0000000..a2654f7 --- /dev/null +++ b/docusaurus/static/ansible/host_vars/rp_1.yml @@ -0,0 +1,3 @@ +ansible_host: 192.168.20.101 +ansible_user: aleksandar +k3s_master_node: true diff --git a/docusaurus/static/ansible/host_vars/rp_2.yml b/docusaurus/static/ansible/host_vars/rp_2.yml new file mode 100644 index 0000000..ea8a4cd --- /dev/null +++ b/docusaurus/static/ansible/host_vars/rp_2.yml @@ -0,0 +1,2 @@ +ansible_host: 192.168.20.104 +ansible_user: aleksandar diff --git a/docusaurus/static/ansible/host_vars/rp_3.yml b/docusaurus/static/ansible/host_vars/rp_3.yml new file mode 100644 index 0000000..713901c --- /dev/null +++ b/docusaurus/static/ansible/host_vars/rp_3.yml @@ -0,0 +1,2 @@ +ansible_host: 192.168.20.103 +ansible_user: aleksandar diff --git a/docusaurus/static/ansible/host_vars/rp_4.yml b/docusaurus/static/ansible/host_vars/rp_4.yml new file mode 100644 index 0000000..077ec15 --- /dev/null +++ b/docusaurus/static/ansible/host_vars/rp_4.yml @@ -0,0 +1,2 @@ +ansible_host: 192.168.20.105 +ansible_user: aleksandar diff --git a/docusaurus/static/ansible/inventory.yml b/docusaurus/static/ansible/inventory.yml new file mode 100644 index 0000000..de67492 --- /dev/null +++ b/docusaurus/static/ansible/inventory.yml @@ -0,0 +1,25 @@ +master_node: + hosts: + rp_1: + rp_2: + rp_3: + +ubuntu_server: + hosts: + lenovo: + hp: + +all_nodes: + hosts: + rp_1: + rp_2: + rp_3: + rp_4: + lenovo: + hp: + +worker_nodes: # A group for all worker nodes + hosts: + rp_4: + lenovo: + hp: diff --git a/docusaurus/static/ansible/playbooks/PLAYBOOKS.md b/docusaurus/static/ansible/playbooks/PLAYBOOKS.md new file mode 100644 index 0000000..81a656f --- /dev/null +++ b/docusaurus/static/ansible/playbooks/PLAYBOOKS.md @@ -0,0 +1,9 @@ +Recommended order in which playbooks should be run: + +- [apt update and upgrade](./apt-update.yml) +- [disable wifi](./disable-wifi.yml) +- [disable bluetooth](./disable-bluetooth.yml.yml) +- [disable swap](./disable-swap.yml) +- [enable cgroups](./enable-memory-groups.yml) +- [install ISCI tools](./install-iscsi-tools.yml) +- [join worker nodes and setup kube config](./join-worker-nodes-and-setup-kube-config.yml) \ No newline at end of file diff --git a/ansible/playbooks/apt-update.yml b/docusaurus/static/ansible/playbooks/apt-update.yml similarity index 65% rename from ansible/playbooks/apt-update.yml rename to docusaurus/static/ansible/playbooks/apt-update.yml index 7f86334..5d0f537 100644 --- a/ansible/playbooks/apt-update.yml +++ b/docusaurus/static/ansible/playbooks/apt-update.yml @@ -1,7 +1,7 @@ --- - name: Update all Raspberry Pi hosts hosts: all_nodes - become: yes # Enables privilege escalation for a task or playbook, allowing it to run as another user (by default, root). + become: true # Enables privilege escalation for a task or playbook, allowing it to run as another user (by default, root). tasks: - name: Update package cache ansible.builtin.apt: diff --git a/docusaurus/static/ansible/playbooks/backup-k3s.yml b/docusaurus/static/ansible/playbooks/backup-k3s.yml new file mode 100644 index 0000000..f18bf71 --- /dev/null +++ b/docusaurus/static/ansible/playbooks/backup-k3s.yml @@ -0,0 +1,30 @@ +- name: Backup K3s cluster and fetch to local machine (including token) + hosts: rp_1 + become: yes + + tasks: + - name: Ensure backup directory exists on the remote node + file: + path: "/tmp/k3s-backups" + state: directory + mode: '0755' + + - name: Backup SQLite database, certificates and token on remote node + archive: + path: + - "/var/lib/rancher/k3s/server/db" + - "/var/lib/rancher/k3s/server/tls" + - "/var/lib/rancher/k3s/server/token" + dest: "/tmp/k3s-backups/k3s-backup-{{ ansible_date_time.date }}-{{ ansible_date_time.hour }}{{ ansible_date_time.minute }}.tar.gz" + format: gz + + - name: Fetch the backup archive to the local machine + fetch: + src: "/tmp/k3s-backups/k3s-backup-{{ ansible_date_time.date }}-{{ ansible_date_time.hour }}{{ ansible_date_time.minute }}.tar.gz" + dest: "./k3s-backups/" + flat: yes + + - name: Clean up remote backup files + file: + path: "/tmp/k3s-backups/k3s-backup-{{ ansible_date_time.date }}-{{ ansible_date_time.hour }}{{ ansible_date_time.minute }}.tar.gz" + state: absent diff --git a/docusaurus/static/ansible/playbooks/configure-firewall.yml b/docusaurus/static/ansible/playbooks/configure-firewall.yml new file mode 100644 index 0000000..a76fd56 --- /dev/null +++ b/docusaurus/static/ansible/playbooks/configure-firewall.yml @@ -0,0 +1,46 @@ +--- +- name: Configure Firewall on all K3s Nodes + hosts: all_nodes + become: true + tasks: + - name: Update package cache before installing + ansible.builtin.apt: + update_cache: yes + changed_when: false + + - name: Pre-configure answers for iptables-persistent to avoid prompts + ansible.builtin.debconf: + name: "{{ item }}" + question: "{{ item }}/autosave" # The question asked during installation + value: "true" # The answer we want to provide + vtype: "boolean" + loop: + - iptables-persistent + - ip6tables-persistent + + - name: Install required firewall packages + ansible.builtin.apt: + name: + - iptables + - iptables-persistent + state: present + + - name: Check if the cluster traffic rule already exists + ansible.builtin.command: + cmd: iptables -C INPUT -s 192.168.20.0/24 -j ACCEPT + register: iptables_check + changed_when: false + ignore_errors: true # The check returns a non-zero code if the rule doesn't exist, which is expected + + - name: Insert rule to allow all traffic from the K3s cluster subnet + ansible.builtin.command: + cmd: iptables -I INPUT 1 -s 192.168.20.0/24 -j ACCEPT -m comment --comment "Allow all traffic from K3s cluster nodes" + when: iptables_check.rc != 0 + notify: + - Save iptables rules + + handlers: + - name: Save iptables rules + ansible.builtin.command: + cmd: netfilter-persistent save + listen: "Save iptables rules" diff --git a/docusaurus/static/ansible/playbooks/configure-powertop.yml b/docusaurus/static/ansible/playbooks/configure-powertop.yml new file mode 100644 index 0000000..6e1ceee --- /dev/null +++ b/docusaurus/static/ansible/playbooks/configure-powertop.yml @@ -0,0 +1,43 @@ +--- +- name: Reduce Power Consumption with PowerTOP + hosts: all_nodes + become: true + tasks: + - name: Check if powertop service file exists + ansible.builtin.stat: + path: /etc/systemd/system/powertop.service + register: powertop_service_file_stat + + - name: Configure powertop if service file does not exist + block: + - name: Install powertop + ansible.builtin.apt: + name: powertop + state: present + update_cache: yes + + - name: Create powertop.service to apply optimizations on boot + ansible.builtin.copy: + dest: /etc/systemd/system/powertop.service + content: | + [Unit] + Description=PowerTOP auto-tuning + + [Service] + Type=oneshot + ExecStart=/usr/sbin/powertop --auto-tune + + [Install] + WantedBy=multi-user.target + mode: "0644" + + - name: Reload systemd daemon + ansible.builtin.systemd: + daemon_reload: yes + + - name: Enable and start the powertop service + ansible.builtin.systemd: + name: powertop.service + enabled: yes + state: started + when: not powertop_service_file_stat.stat.exists diff --git a/ansible/playbooks/disable-bluetooth.yml b/docusaurus/static/ansible/playbooks/disable-bluetooth.yml similarity index 99% rename from ansible/playbooks/disable-bluetooth.yml rename to docusaurus/static/ansible/playbooks/disable-bluetooth.yml index 714efd5..cbe313a 100644 --- a/ansible/playbooks/disable-bluetooth.yml +++ b/docusaurus/static/ansible/playbooks/disable-bluetooth.yml @@ -1,7 +1,7 @@ --- - name: Disable and turn off Bluetooth on Raspberry Pi hosts: all_nodes - become: yes + become: true tasks: diff --git a/docusaurus/static/ansible/playbooks/disable-swap-ubuntu-server.yml b/docusaurus/static/ansible/playbooks/disable-swap-ubuntu-server.yml new file mode 100644 index 0000000..155dd0c --- /dev/null +++ b/docusaurus/static/ansible/playbooks/disable-swap-ubuntu-server.yml @@ -0,0 +1,32 @@ +--- +- name: Ensure swap is disabled permanently + hosts: ubuntu_server + become: true + + tasks: + - name: Run swap disabling tasks only if swap is currently configured + block: + - name: Disable all active swap devices + ansible.builtin.command: + cmd: swapoff -a + # This command is not expected to change state in a way Ansible detects + changed_when: false + + - name: Comment out swap entries in /etc/fstab to make change permanent + ansible.builtin.replace: + path: /etc/fstab + # This regex finds any line containing the word "swap" that is not already commented out + regexp: '^(.*?\sswap\s.*)$' + replace: '# \1' + register: fstab_status + + # This 'when' condition applies to the entire block of tasks above. + # It checks the facts gathered by Ansible for the target machine. + when: ansible_facts.memory_mb.swap.total > 0 + + - name: Reboot the node if the fstab file was changed + ansible.builtin.reboot: + reboot_timeout: 120 # 120 seconds = 2 minutes + msg: "Rebooting node to finalize disabling swap." + # This condition ensures we only reboot if the fstab variable exists AND it reports a change. + when: fstab_status is defined and fstab_status.changed diff --git a/docusaurus/static/ansible/playbooks/disable-swap.yml b/docusaurus/static/ansible/playbooks/disable-swap.yml new file mode 100644 index 0000000..815d4f9 --- /dev/null +++ b/docusaurus/static/ansible/playbooks/disable-swap.yml @@ -0,0 +1,64 @@ +--- +- name: Disable swap temporarily and configure permanently + hosts: all_nodes + become: true + + tasks: + + - name: Disable swap temporarily + ansible.builtin.command: swapoff -a + failed_when: false # Ignore failure scenarios but capture output for debugging + + - name: Ensure dphys-swapfile is installed + ansible.builtin.package: + name: dphys-swapfile + state: present + + # Gather service facts so we can check for the existence of dphys-swapfile service + - name: Gather service facts + ansible.builtin.service_facts: + + - name: Check if /etc/dphys-swapfile exists + ansible.builtin.stat: + path: /etc/dphys-swapfile + register: swapfile_exists + + - name: Stop dphys-swapfile service + ansible.builtin.service: + name: dphys-swapfile + state: stopped + when: "'dphys-swapfile.service' in ansible_facts.services" + + - name: Set CONF_SWAPSIZE to 0 in /etc/dphys-swapfile + ansible.builtin.lineinfile: + path: /etc/dphys-swapfile + regexp: '^CONF_SWAPSIZE=' + line: 'CONF_SWAPSIZE=0' + when: swapfile_exists.stat.exists + + - name: Remove existing /var/swap file + ansible.builtin.file: + path: /var/swap + state: absent + when: swapfile_exists.stat.exists + + - name: Disable dphys-swapfile service + ansible.builtin.service: + name: dphys-swapfile + enabled: no + when: "'dphys-swapfile.service' in ansible_facts.services" + + - name: Verify swap is turned off + ansible.builtin.command: free -m + register: memory_status + changed_when: false + + - name: Display memory status + ansible.builtin.debug: + var: memory_status.stdout_lines + + - name: Reboot the machines to complete swap disabling + ansible.builtin.reboot: + reboot_timeout: 1.5 # 2 minutes + msg: "Rebooting the node to apply permanent swap configuration changes" + pre_reboot_delay: 5 diff --git a/ansible/playbooks/disable-wifi.yml b/docusaurus/static/ansible/playbooks/disable-wifi.yml similarity index 100% rename from ansible/playbooks/disable-wifi.yml rename to docusaurus/static/ansible/playbooks/disable-wifi.yml diff --git a/ansible/playbooks/enable-memory-groups.yml b/docusaurus/static/ansible/playbooks/enable-memory-groups.yml similarity index 100% rename from ansible/playbooks/enable-memory-groups.yml rename to docusaurus/static/ansible/playbooks/enable-memory-groups.yml diff --git a/docusaurus/static/ansible/playbooks/install-cryptsetup-and-dmsetup.yml b/docusaurus/static/ansible/playbooks/install-cryptsetup-and-dmsetup.yml new file mode 100644 index 0000000..8829af8 --- /dev/null +++ b/docusaurus/static/ansible/playbooks/install-cryptsetup-and-dmsetup.yml @@ -0,0 +1,18 @@ +--- +- name: Install cryptsetup and dmsetup packages on target hosts + hosts: all_nodes + become: true + tasks: + - name: Update the package cache (apt-get update) + apt: + update_cache: yes + + - name: Install cryptsetup + apt: + name: cryptsetup + state: present + + - name: Install dmsetup + apt: + name: dmsetup + state: present diff --git a/docusaurus/static/ansible/playbooks/install-iscsi-tools.yml b/docusaurus/static/ansible/playbooks/install-iscsi-tools.yml new file mode 100644 index 0000000..7924b81 --- /dev/null +++ b/docusaurus/static/ansible/playbooks/install-iscsi-tools.yml @@ -0,0 +1,20 @@ +- name: Install iSCSI initiator tools on all worker nodes + hosts: all_nodes # Or specify your Longhorn manager nodes, e.g., longhorn_manager_nodes, worker_nodes, etc. + become: true # Ensures the tasks run with sudo/root privileges + tasks: + - name: Install open-iscsi on Ubuntu/Debian + apt: + name: open-iscsi + state: present + update_cache: yes + when: ansible_facts['os_family'] == "Debian" + + - name: Verify iSCSI installation (check if iscsiadm is accessible) + command: which iscsiadm + register: iscsiadm_check + changed_when: false + + - name: Display error if iscsiadm is not found + fail: + msg: "iscsiadm not found on the host after installation!" + when: iscsiadm_check.stdout == "" diff --git a/docusaurus/static/ansible/playbooks/join-worker-nodes-and-setup-kube-config.yml b/docusaurus/static/ansible/playbooks/join-worker-nodes-and-setup-kube-config.yml new file mode 100644 index 0000000..0961638 --- /dev/null +++ b/docusaurus/static/ansible/playbooks/join-worker-nodes-and-setup-kube-config.yml @@ -0,0 +1,90 @@ +- name: Join Worker Nodes to K3s Cluster and Set Up Kubectl Access + hosts: worker_nodes + become: true + vars: + k3s_token: "" + k3s_master_node: rp_1 + kubeconfig_file_path: "/home/aleksandar/.kube/config" + api_server_url: "https://192.168.88.242:6443" + + tasks: + - name: Retrieve join token from the master node + shell: cat /var/lib/rancher/k3s/server/token + register: join_token + delegate_to: "{{ k3s_master_node }}" + run_once: true # Retrieve the token only once on the master node + + - name: Set K3S_TOKEN variable with the join token + set_fact: + k3s_token: "{{ join_token.stdout }}" # Directly access stdout of token retrieval + + - name: Install K3s and join the cluster + shell: | + curl -sfL https://get.k3s.io | K3S_URL={{ api_server_url }} K3S_TOKEN={{ k3s_token }} sh - + args: + executable: /bin/bash + +- name: Fetch kubeconfig from master node to the control node (or whatever you use) + hosts: master_node + become: true + tasks: + + - name: Fetch kubeconfig file from master node + fetch: + src: "/etc/rancher/k3s/k3s.yaml" + dest: "/tmp/kubeconfig_master" + flat: yes + validate_checksum: no + + +- name: Install kubeconfig on worker nodes + hosts: worker_nodes + become: true + vars: + kubeconfig_file_path: "/home/aleksandar/.kube/config" + api_server_url: "https://192.168.88.242:6443" + tasks: + + - name: Ensure .kube directory exists on worker node + file: + path: "/home/aleksandar/.kube" + state: directory + owner: "aleksandar" + group: "aleksandar" + mode: '0755' + + - name: Copy kubeconfig to worker node + copy: + src: "/tmp/kubeconfig_master" + dest: "{{ kubeconfig_file_path }}" + owner: "aleksandar" + group: "aleksandar" + mode: '0644' + + - name: Ensure the kubeconfig server IP is set correctly + lineinfile: + path: "{{ kubeconfig_file_path }}" + regexp: '^ server:' + line: " server: {{ api_server_url }}" + backrefs: yes + when: update_server_ip | default(True) + + - name: Clean up temporary kubeconfig_master file + file: + path: "/tmp/kubeconfig_master" + state: absent + +- name: Verify worker nodes using kubectl (from master node) + hosts: master_node + become: true + tasks: + + - name: Run kubectl get nodes from the master node to check status + command: kubectl get nodes + register: master_node_status + retries: 5 + delay: 10 + + - name: Display the status of nodes from the master node's perspective + debug: + var: master_node_status.stdout diff --git a/docusaurus/static/ansible/playbooks/k3s-backups/k3s-backup-2024-11-19-2221.tar.gz b/docusaurus/static/ansible/playbooks/k3s-backups/k3s-backup-2024-11-19-2221.tar.gz new file mode 100644 index 0000000..8c4f8c6 Binary files /dev/null and b/docusaurus/static/ansible/playbooks/k3s-backups/k3s-backup-2024-11-19-2221.tar.gz differ diff --git a/docusaurus/static/ansible/playbooks/load-dm-crypt-kernel-module.yml b/docusaurus/static/ansible/playbooks/load-dm-crypt-kernel-module.yml new file mode 100644 index 0000000..38557fb --- /dev/null +++ b/docusaurus/static/ansible/playbooks/load-dm-crypt-kernel-module.yml @@ -0,0 +1,36 @@ +--- +- name: Ensure dm_crypt kernel module is loaded on target nodes + hosts: all + become: true + tasks: + + - name: Load dm_crypt kernel module if it's not already loaded + command: modprobe dm_crypt + args: + warn: false + + - name: Ensure dm_crypt module is configured to load at boot + lineinfile: + path: /etc/modules-load.d/modules.conf + line: "dm_crypt" + create: yes + + - name: Ensure dmsetup is installed (device mapper support) + apt: + name: dmsetup + state: present + when: ansible_os_family == "Debian" + + - name: Ensure cryptsetup is installed + apt: + name: cryptsetup + state: present + when: ansible_os_family == "Debian" + + - name: Reboot the server if needed + reboot: + msg: "Rebooting to ensure dm_crypt module is loaded" + connect_timeout: 5 + reboot_timeout: 600 + pre_reboot_delay: 0 + post_reboot_delay: 30 diff --git a/ansible/playbooks/partition-and-format.yml b/docusaurus/static/ansible/playbooks/partition-and-format.yml similarity index 100% rename from ansible/playbooks/partition-and-format.yml rename to docusaurus/static/ansible/playbooks/partition-and-format.yml diff --git a/ansible/playbooks/setup-redis.yml b/docusaurus/static/ansible/playbooks/setup-redis.yml similarity index 100% rename from ansible/playbooks/setup-redis.yml rename to docusaurus/static/ansible/playbooks/setup-redis.yml diff --git a/docusaurus/static/ansible/playbooks/sysbench-cpu-test.yml b/docusaurus/static/ansible/playbooks/sysbench-cpu-test.yml new file mode 100644 index 0000000..91cd30f --- /dev/null +++ b/docusaurus/static/ansible/playbooks/sysbench-cpu-test.yml @@ -0,0 +1,92 @@ +# https://forums.raspberrypi.com/viewtopic.php?t=243567 + +- name: Run comprehensive benchmarks on Raspberry Pi cluster + hosts: raspberry_pis + become: true + vars: + sysbench_threads: 4 + sysbench_max_prime: 20000 + memory_block_size: 1K + memory_total_size: 1G + file_total_size: 1G + file_test_mode: rndrw + tasks: + + - name: Install sysbench and iperf3 + ansible.builtin.apt: + name: "{{ item }}" + state: present + loop: + - sysbench + - iperf3 + tags: install_tools + + - name: Run sysbench CPU test + ansible.builtin.command: > + sysbench --num-threads={{ sysbench_threads }} + --test=cpu + --cpu-max-prime={{ sysbench_max_prime }} + run + register: cpu_test_output + ignore_errors: true + + - name: Display CPU test results + ansible.builtin.debug: + msg: "CPU Test Result on {{ inventory_hostname }}: {{ cpu_test_output.stdout }}" + + - name: Run sysbench Memory test + ansible.builtin.command: > + sysbench --test=memory + --memory-block-size={{ memory_block_size }} + --memory-total-size={{ memory_total_size }} + run + register: memory_test_output + ignore_errors: true + + - name: Display Memory test results + ansible.builtin.debug: + msg: "Memory Test Result on {{ inventory_hostname }}: {{ memory_test_output.stdout }}" + + - name: Prepare files for Disk I/O test + ansible.builtin.command: > + sysbench --test=fileio --file-total-size={{ file_total_size }} + --file-test-mode={{ file_test_mode }} prepare + ignore_errors: true + + - name: Run sysbench Disk I/O test + ansible.builtin.command: > + sysbench --test=fileio + --file-total-size={{ file_total_size }} + --file-test-mode={{ file_test_mode }} + run + register: disk_test_output + ignore_errors: true + + - name: Cleanup files after Disk I/O test + ansible.builtin.command: > + sysbench --test=fileio --file-total-size={{ file_total_size }} + --file-test-mode={{ file_test_mode }} cleanup + ignore_errors: true + + - name: Display Disk I/O test results + ansible.builtin.debug: + msg: "Disk I/O Test Result on {{ inventory_hostname }}: {{ disk_test_output.stdout }}" + + - name: Run Network test with iperf3 as server on primary node + when: inventory_hostname == groups['raspberry_pis'][0] + ansible.builtin.command: iperf3 -s + async: 30 + poll: 0 + register: iperf_server_output + ignore_errors: true + + - name: Run Network test with iperf3 as client on secondary node + when: inventory_hostname == groups['raspberry_pis'][1] + ansible.builtin.command: iperf3 -c {{ hostvars[groups['raspberry_pis'][0]].ansible_host }} + register: network_test_output + ignore_errors: true + + - name: Display Network test results + when: inventory_hostname == groups['raspberry_pis'][1] + ansible.builtin.debug: + msg: "Network Test Result on {{ inventory_hostname }}: {{ network_test_output.stdout }}" diff --git a/docusaurus/static/ansible/secrets.yml b/docusaurus/static/ansible/secrets.yml new file mode 100644 index 0000000..fdaaf4c --- /dev/null +++ b/docusaurus/static/ansible/secrets.yml @@ -0,0 +1,6 @@ +$ANSIBLE_VAULT;1.1;AES256 +63386466616139346361396130356133636663393939316566623363373835323239623030336530 +6463663531396436633631343561623965366163386462660a653762653733393434393364396266 +35633464333431343764363661376461333363373331383033306463303531643764373134616434 +3461333631353638350a613062623762633563626432363736336330663463656132333337386430 +3631 diff --git a/docusaurus/static/img/android-chrome-192x192.png b/docusaurus/static/img/android-chrome-192x192.png new file mode 100644 index 0000000..d46e06c Binary files /dev/null and b/docusaurus/static/img/android-chrome-192x192.png differ diff --git a/docusaurus/static/img/android-chrome-512x512.png b/docusaurus/static/img/android-chrome-512x512.png new file mode 100644 index 0000000..120082a Binary files /dev/null and b/docusaurus/static/img/android-chrome-512x512.png differ diff --git a/docusaurus/static/img/apple-touch-icon.png b/docusaurus/static/img/apple-touch-icon.png new file mode 100644 index 0000000..56a8f0a Binary files /dev/null and b/docusaurus/static/img/apple-touch-icon.png differ diff --git a/docusaurus/static/img/developer-ripping-hair.png b/docusaurus/static/img/developer-ripping-hair.png new file mode 100644 index 0000000..7e71b79 Binary files /dev/null and b/docusaurus/static/img/developer-ripping-hair.png differ diff --git a/docusaurus/static/img/docusaurus.png b/docusaurus/static/img/docusaurus.png new file mode 100644 index 0000000..f458149 Binary files /dev/null and b/docusaurus/static/img/docusaurus.png differ diff --git a/docusaurus/static/img/favicon-16x16.png b/docusaurus/static/img/favicon-16x16.png new file mode 100644 index 0000000..5356608 Binary files /dev/null and b/docusaurus/static/img/favicon-16x16.png differ diff --git a/docusaurus/static/img/favicon-32x32.png b/docusaurus/static/img/favicon-32x32.png new file mode 100644 index 0000000..ed11163 Binary files /dev/null and b/docusaurus/static/img/favicon-32x32.png differ diff --git a/docusaurus/static/img/favicon.ico b/docusaurus/static/img/favicon.ico new file mode 100644 index 0000000..2930e56 Binary files /dev/null and b/docusaurus/static/img/favicon.ico differ diff --git a/docusaurus/static/img/hardware/cables-1.jpg b/docusaurus/static/img/hardware/cables-1.jpg new file mode 100644 index 0000000..781da61 Binary files /dev/null and b/docusaurus/static/img/hardware/cables-1.jpg differ diff --git a/docusaurus/static/img/hardware/cables-2.jpg b/docusaurus/static/img/hardware/cables-2.jpg new file mode 100644 index 0000000..abd65ef Binary files /dev/null and b/docusaurus/static/img/hardware/cables-2.jpg differ diff --git a/docusaurus/static/img/hardware/cables-3.jpg b/docusaurus/static/img/hardware/cables-3.jpg new file mode 100644 index 0000000..542763a Binary files /dev/null and b/docusaurus/static/img/hardware/cables-3.jpg differ diff --git a/docusaurus/static/img/hardware/exhaust-fans-k3-cluster-1.jpg b/docusaurus/static/img/hardware/exhaust-fans-k3-cluster-1.jpg new file mode 100644 index 0000000..8cebae6 Binary files /dev/null and b/docusaurus/static/img/hardware/exhaust-fans-k3-cluster-1.jpg differ diff --git a/docusaurus/static/img/hardware/exhaust-fans-k3-cluster-2.jpg b/docusaurus/static/img/hardware/exhaust-fans-k3-cluster-2.jpg new file mode 100644 index 0000000..0a947ce Binary files /dev/null and b/docusaurus/static/img/hardware/exhaust-fans-k3-cluster-2.jpg differ diff --git a/docusaurus/static/img/hardware/mikrotik-router-and-switch.jpg b/docusaurus/static/img/hardware/mikrotik-router-and-switch.jpg new file mode 100644 index 0000000..10670e3 Binary files /dev/null and b/docusaurus/static/img/hardware/mikrotik-router-and-switch.jpg differ diff --git a/docusaurus/static/img/hardware/mini-pcs-hardware.jpg b/docusaurus/static/img/hardware/mini-pcs-hardware.jpg new file mode 100644 index 0000000..9435a1f Binary files /dev/null and b/docusaurus/static/img/hardware/mini-pcs-hardware.jpg differ diff --git a/docusaurus/static/img/hardware/rack-empty.jpg b/docusaurus/static/img/hardware/rack-empty.jpg new file mode 100644 index 0000000..42009aa Binary files /dev/null and b/docusaurus/static/img/hardware/rack-empty.jpg differ diff --git a/docusaurus/static/img/hardware/rack.jpg b/docusaurus/static/img/hardware/rack.jpg new file mode 100644 index 0000000..542764d Binary files /dev/null and b/docusaurus/static/img/hardware/rack.jpg differ diff --git a/docusaurus/static/img/hardware/raspberry-pi-cluster.jpg b/docusaurus/static/img/hardware/raspberry-pi-cluster.jpg new file mode 100644 index 0000000..fdd2b69 Binary files /dev/null and b/docusaurus/static/img/hardware/raspberry-pi-cluster.jpg differ diff --git a/docusaurus/static/img/hardware/raspberry-pi-cluster2.jpg b/docusaurus/static/img/hardware/raspberry-pi-cluster2.jpg new file mode 100644 index 0000000..d079ecd Binary files /dev/null and b/docusaurus/static/img/hardware/raspberry-pi-cluster2.jpg differ diff --git a/docusaurus/static/img/hardware/roas/lenovo-m920q-roas-assembly.jpg b/docusaurus/static/img/hardware/roas/lenovo-m920q-roas-assembly.jpg new file mode 100644 index 0000000..269df75 Binary files /dev/null and b/docusaurus/static/img/hardware/roas/lenovo-m920q-roas-assembly.jpg differ diff --git a/docusaurus/static/img/hardware/roas/lenovo-m920q-roas-nic-2.jpg b/docusaurus/static/img/hardware/roas/lenovo-m920q-roas-nic-2.jpg new file mode 100644 index 0000000..0f17a0c Binary files /dev/null and b/docusaurus/static/img/hardware/roas/lenovo-m920q-roas-nic-2.jpg differ diff --git a/docusaurus/static/img/hardware/roas/lenovo-m920q-roas-nic-3.jpg b/docusaurus/static/img/hardware/roas/lenovo-m920q-roas-nic-3.jpg new file mode 100644 index 0000000..786c7e4 Binary files /dev/null and b/docusaurus/static/img/hardware/roas/lenovo-m920q-roas-nic-3.jpg differ diff --git a/docusaurus/static/img/hardware/roas/lenovo-m920q-roas-nic.jpg b/docusaurus/static/img/hardware/roas/lenovo-m920q-roas-nic.jpg new file mode 100644 index 0000000..3a6008b Binary files /dev/null and b/docusaurus/static/img/hardware/roas/lenovo-m920q-roas-nic.jpg differ diff --git a/docusaurus/static/img/hardware/roas/lenovo-m920q-roas-sticker.jpg b/docusaurus/static/img/hardware/roas/lenovo-m920q-roas-sticker.jpg new file mode 100644 index 0000000..7d5fb63 Binary files /dev/null and b/docusaurus/static/img/hardware/roas/lenovo-m920q-roas-sticker.jpg differ diff --git a/docusaurus/static/img/hardware/roas/lenovo-m920q-roas.jpg b/docusaurus/static/img/hardware/roas/lenovo-m920q-roas.jpg new file mode 100644 index 0000000..c4da6af Binary files /dev/null and b/docusaurus/static/img/hardware/roas/lenovo-m920q-roas.jpg differ diff --git a/docusaurus/static/img/hardware/router.jpg b/docusaurus/static/img/hardware/router.jpg new file mode 100644 index 0000000..134c1db Binary files /dev/null and b/docusaurus/static/img/hardware/router.jpg differ diff --git a/docusaurus/static/img/hardware/sfp-cable.jpg b/docusaurus/static/img/hardware/sfp-cable.jpg new file mode 100644 index 0000000..366dd4a Binary files /dev/null and b/docusaurus/static/img/hardware/sfp-cable.jpg differ diff --git a/docusaurus/static/img/hardware/single-raspberry-pi.jpg b/docusaurus/static/img/hardware/single-raspberry-pi.jpg new file mode 100644 index 0000000..bc15061 Binary files /dev/null and b/docusaurus/static/img/hardware/single-raspberry-pi.jpg differ diff --git a/docusaurus/static/img/hardware/switch.jpg b/docusaurus/static/img/hardware/switch.jpg new file mode 100644 index 0000000..b955508 Binary files /dev/null and b/docusaurus/static/img/hardware/switch.jpg differ diff --git a/docusaurus/static/img/k3s-cluster.png b/docusaurus/static/img/k3s-cluster.png new file mode 100644 index 0000000..6d7b5ca Binary files /dev/null and b/docusaurus/static/img/k3s-cluster.png differ diff --git a/assets/images/kubernetes.png b/docusaurus/static/img/kubernetes.png similarity index 100% rename from assets/images/kubernetes.png rename to docusaurus/static/img/kubernetes.png diff --git a/docusaurus/static/img/logo.svg b/docusaurus/static/img/logo.svg new file mode 100644 index 0000000..9db6d0d --- /dev/null +++ b/docusaurus/static/img/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docusaurus/static/img/mikrotik-no-cdrom-found.jpg b/docusaurus/static/img/mikrotik-no-cdrom-found.jpg new file mode 100644 index 0000000..f0f8e09 Binary files /dev/null and b/docusaurus/static/img/mikrotik-no-cdrom-found.jpg differ diff --git a/docusaurus/static/img/mini-data-center-social-card.jpg b/docusaurus/static/img/mini-data-center-social-card.jpg new file mode 100644 index 0000000..c346fc0 Binary files /dev/null and b/docusaurus/static/img/mini-data-center-social-card.jpg differ diff --git a/docusaurus/static/img/minipcs/minipc-assembled.jpg b/docusaurus/static/img/minipcs/minipc-assembled.jpg new file mode 100644 index 0000000..6df3679 Binary files /dev/null and b/docusaurus/static/img/minipcs/minipc-assembled.jpg differ diff --git a/docusaurus/static/img/minipcs/minipc-opened-1.jpg b/docusaurus/static/img/minipcs/minipc-opened-1.jpg new file mode 100644 index 0000000..bf7eea1 Binary files /dev/null and b/docusaurus/static/img/minipcs/minipc-opened-1.jpg differ diff --git a/docusaurus/static/img/minipcs/minipc-opened-2.jpg b/docusaurus/static/img/minipcs/minipc-opened-2.jpg new file mode 100644 index 0000000..7f238e5 Binary files /dev/null and b/docusaurus/static/img/minipcs/minipc-opened-2.jpg differ diff --git a/docusaurus/static/img/minipcs/minipc-opened-3.jpg b/docusaurus/static/img/minipcs/minipc-opened-3.jpg new file mode 100644 index 0000000..67b7853 Binary files /dev/null and b/docusaurus/static/img/minipcs/minipc-opened-3.jpg differ diff --git a/docusaurus/static/img/minipcs/minipc-opened-4.jpg b/docusaurus/static/img/minipcs/minipc-opened-4.jpg new file mode 100644 index 0000000..9873c47 Binary files /dev/null and b/docusaurus/static/img/minipcs/minipc-opened-4.jpg differ diff --git a/docusaurus/static/img/programmer-network-logo.svg b/docusaurus/static/img/programmer-network-logo.svg new file mode 100644 index 0000000..974027f --- /dev/null +++ b/docusaurus/static/img/programmer-network-logo.svg @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/docusaurus/static/img/safari-pinned-tab.svg b/docusaurus/static/img/safari-pinned-tab.svg new file mode 100644 index 0000000..d106903 --- /dev/null +++ b/docusaurus/static/img/safari-pinned-tab.svg @@ -0,0 +1,47 @@ + + + + +Created by potrace 1.14, written by Peter Selinger 2001-2017 + + + + + + + diff --git a/docusaurus/static/img/site.webmanifest b/docusaurus/static/img/site.webmanifest new file mode 100644 index 0000000..45dc8a2 --- /dev/null +++ b/docusaurus/static/img/site.webmanifest @@ -0,0 +1 @@ +{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/docusaurus/static/img/undraw_docusaurus_mountain.svg b/docusaurus/static/img/undraw_docusaurus_mountain.svg new file mode 100644 index 0000000..af961c4 --- /dev/null +++ b/docusaurus/static/img/undraw_docusaurus_mountain.svg @@ -0,0 +1,171 @@ + + Easy to Use + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docusaurus/static/img/undraw_docusaurus_react.svg b/docusaurus/static/img/undraw_docusaurus_react.svg new file mode 100644 index 0000000..94b5cf0 --- /dev/null +++ b/docusaurus/static/img/undraw_docusaurus_react.svg @@ -0,0 +1,170 @@ + + Powered by React + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docusaurus/static/img/undraw_docusaurus_tree.svg b/docusaurus/static/img/undraw_docusaurus_tree.svg new file mode 100644 index 0000000..d9161d3 --- /dev/null +++ b/docusaurus/static/img/undraw_docusaurus_tree.svg @@ -0,0 +1,40 @@ + + Focus on What Matters + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docusaurus/tsconfig.json b/docusaurus/tsconfig.json new file mode 100644 index 0000000..314eab8 --- /dev/null +++ b/docusaurus/tsconfig.json @@ -0,0 +1,7 @@ +{ + // This file is not used in compilation. It is here just for a nice editor experience. + "extends": "@docusaurus/tsconfig", + "compilerOptions": { + "baseUrl": "." + } +} diff --git a/drafts/COMMON_COMMANDS.md b/drafts/COMMON_COMMANDS.md new file mode 100644 index 0000000..8f47d12 --- /dev/null +++ b/drafts/COMMON_COMMANDS.md @@ -0,0 +1,4 @@ +- https://explainshell.com/explain?cmd=df+-hT +- https://explainshell.com/explain/8/lsblk +- https://explainshell.com/explain/1/htop +- https://explainshell.com/explain/8/lsblk diff --git a/MOUNT_AND_FORMAT_THE_DRIVE.md b/drafts/MOUNT_AND_FORMAT_THE_DRIVE.md similarity index 86% rename from MOUNT_AND_FORMAT_THE_DRIVE.md rename to drafts/MOUNT_AND_FORMAT_THE_DRIVE.md index 1dbadf6..0a8a8a7 100644 --- a/MOUNT_AND_FORMAT_THE_DRIVE.md +++ b/drafts/MOUNT_AND_FORMAT_THE_DRIVE.md @@ -3,7 +3,9 @@ To **reformat and recreate a partition** (in your case, **`sda1`**) using **`fdi Since you're working with an external device and the partition appears to be large (**931.5GB**), I assume it's an external hard drive or SSD attached via USB. ### ⚠️ **Important!** Before proceeding: + 1. **Backup Data**: Formatting and deleting the partition will erase all data on it, so ensure you have backed up any important data currently stored on **`sda1`**. + 2. **Unmount the Partition**: If the partition is currently mounted, you’ll need to unmount it before working on it. --- @@ -11,12 +13,15 @@ Since you're working with an external device and the partition appears to be lar ### Steps to Format and Recreate **`sda1`** Using `fdisk`: #### **1. Unmount the Partition** + Before modifying the partition, unmount it if it's mounted: + ```bash sudo umount /dev/sda1 ``` #### **2. Launch `fdisk` to Edit the Partition Table** + Run `fdisk` for the target device (`/dev/sda` in your case): ```bash sudo fdisk /dev/sda @@ -25,6 +30,7 @@ sudo fdisk /dev/sda This will start the interactive **`fdisk`** utility on the entire **`/dev/sda`** disk. #### **3. Delete the Existing Partition** + Once inside the `fdisk` tool, list the partitions to verify: ```bash p @@ -36,18 +42,20 @@ To delete the existing partition **`sda1`**: 1. Press `d` (to delete a partition). 2. If there is only one partition (`sda1`), it will automatically choose `1`. Otherwise, you may be asked to specify the partition number (enter `1` to select **`sda1`**). -Confirm that **`/dev/sda1`** has been deleted by pressing `p` again to view the partition table—it should now list no partitions. +Confirm that **`/dev/sda1`** has been deleted by pressing `p` again to view the partition table, it should now list no partitions. #### **4. Create a New Partition** + Now, create a new partition by doing the following: - Type `n` (to create a new partition). - When asked for the partition type, press `p` to create a **primary partition**. - When asked for the partition number, press `1` (to recreate it as **`sda1`**). - Choose the **default starting sector** by just pressing `Enter` (this will typically start at sector 2048 if you're using a GPT or MBR partition scheme). - - You will be asked for the last sector — press `Enter` to choose the default and use the rest of the available space from the starting sector, effectively recreating a partition that spans the entire disk. + - You will be asked for the last sector, press `Enter` to choose the default and use the rest of the available space from the starting sector, effectively recreating a partition that spans the entire disk. #### **5. Set the Partition's Filesystem (Optional)** + If you're partitioning for normal use (e.g., formatting to **ext4**), you can skip this step. But if you want to set a specific partition type (like Linux filesystem (`83`)), you'll be prompted to choose it. By default, **`fdisk`** will set it to **`83` (Linux Filesystem)** for most Linux machines. To explicitly set it: @@ -55,6 +63,7 @@ To explicitly set it: - Type `83` for Linux filesystem. #### **6. Write the Partition Table** + Once satisfied with the changes, write the new partition table to the disk by typing: ```bash w @@ -82,19 +91,21 @@ This will begin formatting the newly created partition `sda1` as an **ext4** vol Once the partition is formatted, you can mount it back for use: 1. Create a mount point (if it doesn’t exist yet): - ```bash - sudo mkdir -p /mnt/mydisk - ``` + +```bash +sudo mkdir -p /mnt/mydisk +``` 2. Mount the partition: - ```bash - sudo mount /dev/sda1 /mnt/mydisk - ``` +```bash +sudo mount /dev/sda1 /mnt/mydisk +``` 3. Verify the mount: - ```bash - df -h - ``` + +```bash +df -h +``` You should now see the newly mounted **`sda1`** partition, and it should be available in **`/mnt/mydisk`**. @@ -105,24 +116,28 @@ You should now see the newly mounted **`sda1`** partition, and it should be avai If you want this disk to mount automatically at boot, add an entry to **`/etc/fstab`**: 1. Find the **UUID** of the partition: - ```bash - sudo blkid /dev/sda1 - ``` - You will see an output that looks something like this: - ```bash - /dev/sda1: UUID="xxxx-xxxx-xxxx-xxxx" TYPE="ext4" - ``` +```bash +sudo blkid /dev/sda1 +``` + +You will see an output that looks something like this: + +```bash +/dev/sda1: UUID="xxxx-xxxx-xxxx-xxxx" TYPE="ext4" +``` 2. Open `/etc/fstab` in an editor: - ```bash - sudo nano /etc/fstab - ``` + +```bash +sudo nano /etc/fstab +``` 3. Add the following line to the end of the file to make the partition auto-mount at `/mnt/mydisk` on boot: - ```bash - UUID=xxxx-xxxx-xxxx-xxxx /mnt/mydisk ext4 defaults 0 0 - ``` + +```bash +UUID=xxxx-xxxx-xxxx-xxxx /mnt/mydisk ext4 defaults 0 0 +``` 4. Save (`Ctrl + O`) and exit (`Ctrl + X`). diff --git a/SETTING_UP_CLOUDFLARE_DNS.md b/drafts/SETTING_UP_CLOUDFLARE_DNS.md similarity index 100% rename from SETTING_UP_CLOUDFLARE_DNS.md rename to drafts/SETTING_UP_CLOUDFLARE_DNS.md diff --git a/SETTING_UP_CLOUDFLARE_SSL_VIA_API.md b/drafts/SETTING_UP_CLOUDFLARE_SSL_VIA_API.md similarity index 100% rename from SETTING_UP_CLOUDFLARE_SSL_VIA_API.md rename to drafts/SETTING_UP_CLOUDFLARE_SSL_VIA_API.md diff --git a/exercises/full-stack-example/.drawio b/exercises/full-stack-example/.drawio new file mode 100644 index 0000000..8cc5c81 --- /dev/null +++ b/exercises/full-stack-example/.drawio @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/exercises/full-stack-example/example.md b/exercises/full-stack-example/example.md new file mode 100644 index 0000000..19d994b --- /dev/null +++ b/exercises/full-stack-example/example.md @@ -0,0 +1,262 @@ +Great! Now that your K3s cluster is running with **MetalLB**, **Longhorn**, and the **Native PostgreSQL Operator**, you're ready to provision a full application stack that demonstrates: + +1. **Node.js API** deployment. +2. Node.js API connects to **PostgreSQL** running as a cluster service. +3. **Persistent Storage** allocated using **PVCs (Persistent Volume Claims)** from Longhorn. +4. **LoadBalancer access** for the Node.js API service via MetalLB. + +Let’s go step-by-step to set up a **Node.js API** that connects to a PostgreSQL database and uses Longhorn for storage. + +--- + +### Prerequisite: Ensure PostgreSQL Is Working with a `ClusterIP` Service + +You likely already have a **ClusterIP Service** for PostgreSQL through your native PostgreSQL Operator, so make sure you take note of: + +1. PostgreSQL **ClusterIP** address. +2. PostgreSQL **service name**. +3. **Database credentials** (user, password, database name). + +Let’s assume the following values (replace them with actual details from your setup): +- Postgres Service Name: `pg-cluster` +- Postgres Database Name: `mydb` +- Postgres User: `postgres` +- Postgres Password: `password123` +- Postgres Service Port: `5432` + +--- + +### Step 1: Create Persistent Volume Claim (PVC) for Node.js API (Longhorn) + +First, create a **PersistentVolumeClaim** (PVC) for the Node.js API to store any configuration, logs, or data it needs. This PVC will be backed by **Longhorn**. + +1. Create a file named `nodejs-api-pvc.yaml`: + +```yaml +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: nodejs-api-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + storageClassName: longhorn +``` + +2. Apply the PVC: + +```bash +kubectl apply -f nodejs-api-pvc.yaml +``` + +This will allocate 1Gi of persistent storage from Longhorn for your Node.js API. + +--- + +### Step 2: Create a ConfigMap for the Node.js Environment Variables + +Your Node.js API will need environment variables to connect to PostgreSQL, so let’s create a **ConfigMap** for that. + +1. Create a file named `nodejs-api-configmap.yaml`: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: nodejs-api-configmap +data: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: password123 + POSTGRES_DB: mydb + POSTGRES_HOST: pg-cluster + POSTGRES_PORT: "5432" +``` + +You can modify the values to match your PostgreSQL service details. + +2. Apply the ConfigMap: + +```bash +kubectl apply -f nodejs-api-configmap.yaml +``` + +--- + +### Step 3: Create a Deployment for the Node.js API with the PVC and ConfigMap + +1. Create a file named `nodejs-api-deployment.yaml`: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nodejs-api +spec: + replicas: 1 + selector: + matchLabels: + app: nodejs-api + template: + metadata: + labels: + app: nodejs-api + spec: + containers: + - name: nodejs-api + image: node:14 + ports: + - containerPort: 3000 + volumeMounts: + - name: data + mountPath: /app/data + envFrom: + - configMapRef: + name: nodejs-api-configmap + command: ["node"] + args: ["app.js"] + volumes: + - name: data + persistentVolumeClaim: + claimName: nodejs-api-pvc +``` + +In this deployment: +- The configuration for the PostgreSQL connection is coming from the **ConfigMap**. +- A **PersistentVolumeClaim** (PVC) mounted at `/app/data` is used for storing data. +- The `node:14` Docker image is used, and it's executing the `app.js` file within the container. + +2. Apply the deployment: + +```bash +kubectl apply -f nodejs-api-deployment.yaml +``` + +This creates a single replica (Pod) of the Node.js API and binds it to the PVC and ConfigMap. + +--- + +### Step 4: Expose the Node.js API Using MetalLB as a LoadBalancer + +Now we’ll expose the deployment as a `LoadBalancer` service using **MetalLB**, so you can access the Node.js API externally. + +1. Create a file named `nodejs-api-service.yaml`: + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: nodejs-api +spec: + selector: + app: nodejs-api + type: LoadBalancer + ports: + - protocol: TCP + port: 80 + targetPort: 3000 +``` + +2. Apply the service: + +```bash +kubectl apply -f nodejs-api-service.yaml +``` + +Once this service is applied, **MetalLB** will assign an external IP to the service, allowing you to access it from outside the cluster. + +3. Check the service to find the **external IP** assigned by MetalLB: + +```bash +kubectl get svc nodejs-api +``` + +You should see something like this: + +``` +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE +nodejs-api LoadBalancer 10.43.45.155 192.168.88.240 80:32515/TCP 1m +``` + +In this case, the **EXTERNAL-IP** is `192.168.88.240`, which you can use to access the Node.js API from any machine in the network. + +--- + +### Step 5: Example Node.js Application (Connecting to PostgreSQL) + +Now let’s define the **Node.js** app (`app.js`) that will connect to PostgreSQL based on the environment variables defined in the ConfigMap. + +Inside the `app.js` file, basic PostgreSQL connection code can look like this: + +```javascript +const express = require("express"); +const { Pool } = require("pg"); + +// Create the PostgreSQL connection pool +const pool = new Pool({ + user: process.env.POSTGRES_USER, + host: process.env.POSTGRES_HOST, + database: process.env.POSTGRES_DB, + password: process.env.POSTGRES_PASSWORD, + port: process.env.POSTGRES_PORT, +}); + +const app = express(); +const port = 3000; + +// Test route to query PostgreSQL +app.get("/db", async (req, res) => { + try { + const result = await pool.query("SELECT NOW()"); + res.json(result.rows); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Failed to connect to database" }); + } +}); + +// Main route +app.get("/", (req, res) => { + res.send("Welcome to the Node.js API connected to PostgreSQL!"); +}); + +app.listen(port, () => { + console.log(`API server running on http://localhost:${port}`); +}); +``` + +Make sure that this `app.js` file is included with your Node.js Docker image or mounted as part of the deployed Pod. + +--- + +### Step 6: Accessing Your Application + +Now that everything is set up: +1. Access the Node.js API from a browser or using `curl`: + +```bash +curl http:/// +``` + +If you configured everything correctly, you should see a "Welcome to the Node.js API..." message. + +2. Test the PostgreSQL connection: + +```bash +curl http:///db +``` + +This route will query the PostgreSQL database and return the current time from the database. + +--- + +### Summary: + +- You provisioned a **Node.js API** in your K3s cluster. +- The persistent storage is managed by **Longhorn** via a **PVC**. +- The Node.js API connects to your **PostgreSQL** database (provisioned by the PostgreSQL native operator). +- The API is exposed via a **LoadBalancer** service using **MetalLB**, and the API is accessible externally. + +This setup forms a fully scalable, cloud-native application architecture you can build upon in your K3s cluster. \ No newline at end of file diff --git a/exercises/full-stack-example/nodejs-api-configmap.yaml b/exercises/full-stack-example/nodejs-api-configmap.yaml new file mode 100644 index 0000000..c3c77f7 --- /dev/null +++ b/exercises/full-stack-example/nodejs-api-configmap.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: nodejs-api-configmap + namespace: nodejs +data: + POSTGRES_USER: appuser + POSTGRES_PASSWORD: appuser_password + POSTGRES_DB: app + POSTGRES_HOST: my-postgres-cluster-rw + POSTGRES_PORT: "5432" \ No newline at end of file diff --git a/exercises/full-stack-example/nodejs-api-deployment.yaml b/exercises/full-stack-example/nodejs-api-deployment.yaml new file mode 100644 index 0000000..4a6098a --- /dev/null +++ b/exercises/full-stack-example/nodejs-api-deployment.yaml @@ -0,0 +1,37 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nodejs-api + namespace: nodejs +spec: + replicas: 1 + selector: + matchLabels: + app: nodejs-api + template: + metadata: + labels: + app: nodejs-api + spec: + containers: + - name: nodejs-api + image: agjs/test + ports: + - containerPort: 3000 + volumeMounts: + - name: data + mountPath: /app/data + envFrom: + - configMapRef: + name: nodejs-api-configmap + resources: + limits: + memory: "512Mi" + cpu: "500m" + requests: + memory: "256Mi" + cpu: "250m" + volumes: + - name: data + persistentVolumeClaim: + claimName: nodejs-api-pvc diff --git a/exercises/full-stack-example/nodejs-api-ingress.yaml b/exercises/full-stack-example/nodejs-api-ingress.yaml new file mode 100644 index 0000000..587c936 --- /dev/null +++ b/exercises/full-stack-example/nodejs-api-ingress.yaml @@ -0,0 +1,18 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: traefik-ingress + namespace: nodejs +spec: + ingressClassName: traefik + rules: + - host: node-api.local.host + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: nodejs-api + port: + number: 3000 \ No newline at end of file diff --git a/exercises/full-stack-example/nodejs-api-pvc.yaml b/exercises/full-stack-example/nodejs-api-pvc.yaml new file mode 100644 index 0000000..594243e --- /dev/null +++ b/exercises/full-stack-example/nodejs-api-pvc.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: nodejs-api-pvc + namespace: nodejs +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + storageClassName: longhorn + diff --git a/exercises/full-stack-example/nodejs-api-service.yaml b/exercises/full-stack-example/nodejs-api-service.yaml new file mode 100644 index 0000000..64a5eb8 --- /dev/null +++ b/exercises/full-stack-example/nodejs-api-service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: nodejs-api + namespace: nodejs +spec: + selector: + app: nodejs-api + type: ClusterIP + ports: + - protocol: TCP + port: 80 + targetPort: 3000 \ No newline at end of file diff --git a/exercises/longhorn/longhorn-dashboard-ingress.yaml b/exercises/longhorn/longhorn-dashboard-ingress.yaml new file mode 100644 index 0000000..2a66640 --- /dev/null +++ b/exercises/longhorn/longhorn-dashboard-ingress.yaml @@ -0,0 +1,18 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: longhorn-ingress + namespace: longhorn-system +spec: + ingressClassName: traefik # Specify that Traefik handles this Ingress + rules: + - host: longhorn.local.host # The domain you'll use to access Longhorn UI + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: longhorn-frontend # Correct service name + port: + number: 80 # Service is using port 80 diff --git a/exercises/longhorn/longhorn-pvc.yaml b/exercises/longhorn/longhorn-pvc.yaml new file mode 100644 index 0000000..cc1689c --- /dev/null +++ b/exercises/longhorn/longhorn-pvc.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: longhorn-first-pvc +spec: + accessModes: + - ReadWriteOnce + storageClassName: my-first-storage-class + resources: + requests: + storage: 20Gi diff --git a/exercises/longhorn/longhorn-storageclass.yaml b/exercises/longhorn/longhorn-storageclass.yaml new file mode 100644 index 0000000..dfa6cb6 --- /dev/null +++ b/exercises/longhorn/longhorn-storageclass.yaml @@ -0,0 +1,11 @@ +apiVersion: storage.k8s.io/v1 # Specifies the API version for the StorageClass resource +kind: StorageClass # Defines the kind of resource, which is StorageClass +metadata: + name: my-first-storage-class # The name of the StorageClass, which is 'longhorn' +provisioner: driver.longhorn.io # The provisioner that will be used to provision volumes, in this case, Longhorn +parameters: + numberOfReplicas: "3" # The number of replicas to be created for each volume + staleReplicaTimeout: "30" # The timeout in minutes for a replica to be considered stale +allowVolumeExpansion: true # Allows the volume to be expanded after creation +reclaimPolicy: Retain # Specifies the reclaim policy, which determines what happens to the volume after it is released; 'Retain' keeps the volume +volumeBindingMode: Immediate # Specifies when the volume should be bound to a PVC; 'Immediate' means binding happens as soon as the PVC is created diff --git a/exercises/longhorn/nginx-pod.yaml b/exercises/longhorn/nginx-pod.yaml new file mode 100644 index 0000000..77aa7a0 --- /dev/null +++ b/exercises/longhorn/nginx-pod.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx + labels: + app: nginx # Add this Label so our Service can select this Pod +spec: + containers: + - image: nginx + name: nginx + ports: + - containerPort: 80 # Expose nginx's default container port (optional but recommended) + volumeMounts: + - name: data + mountPath: /usr/share/nginx/html + volumes: + - name: data + persistentVolumeClaim: + claimName: longhorn-pvc diff --git a/exercises/longhorn/nginx-service.yaml b/exercises/longhorn/nginx-service.yaml new file mode 100644 index 0000000..ae28ed9 --- /dev/null +++ b/exercises/longhorn/nginx-service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: nginx-service +spec: + selector: + app: nginx # This must match the pod's label + ports: + - protocol: TCP + port: 80 # This is the port within the cluster + targetPort: 80 # The container's port where nginx is running + nodePort: 30080 # External access port (if not specified, Kubernetes will assign one automatically) + type: NodePort diff --git a/exercises/metallb/metallb-config.yaml b/exercises/metallb/metallb-config.yaml new file mode 100644 index 0000000..990906b --- /dev/null +++ b/exercises/metallb/metallb-config.yaml @@ -0,0 +1,21 @@ +apiVersion: metallb.io/v1beta1 # Specifies the API version for the MetalLB resource +kind: IPAddressPool # Defines the kind of resource, which is IPAddressPool +metadata: + name: mikrotik-address-pool # The name of the IPAddressPool + namespace: metallb-system # The namespace where the IPAddressPool is created +spec: + addresses: # The list of IP addresses in the pool + - 192.168.88.15-192.168.88.99 # A range of IP addresses in the pool + autoAssign: true + avoidBuggyIPs: true +--- +apiVersion: metallb.io/v1beta1 # Specifies the API version for the MetalLB resource +kind: L2Advertisement # Defines the kind of resource, which is L2Advertisement +metadata: + namespace: metallb-system # The namespace where the L2Advertisement is created + name: config # The name of the L2Advertisement +spec: + ipAddressPools: + - mikrotik-address-pool + interfaces: + - eth0 \ No newline at end of file diff --git a/exercises/postgres/my-postgres-cluster.yaml b/exercises/postgres/my-postgres-cluster.yaml new file mode 100644 index 0000000..0a26eb3 --- /dev/null +++ b/exercises/postgres/my-postgres-cluster.yaml @@ -0,0 +1,16 @@ +apiVersion: postgresql.cnpg.io/v1 +kind: Cluster +metadata: + name: my-postgres-cluster + namespace: postgres-db +spec: + instances: 3 + primaryUpdateMethod: switchover + storage: + size: 1Gi + storageClass: my-first-storage-class + bootstrap: + initdb: + # Avoid creating the default app database + postInitSQL: + - CREATE USER appuser WITH PASSWORD 'appuser_password'; diff --git a/exercises/postgres/my-postgres-service.yaml b/exercises/postgres/my-postgres-service.yaml new file mode 100644 index 0000000..011f8d5 --- /dev/null +++ b/exercises/postgres/my-postgres-service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: postgres-external # A service name + namespace: postgres-db # Same namespace where PG cluster is running +spec: + type: LoadBalancer # Service type is NodePort, MetalLB + selector: # Targeting pods with the following labels + cnpg.io/cluster: my-postgres-cluster + role: primary + ports: + - protocol: TCP + port: 5432 # PostgreSQL port inside the cluster \ No newline at end of file diff --git a/exercises/traefik/traefik-dashboard-ingress.yaml b/exercises/traefik/traefik-dashboard-ingress.yaml new file mode 100644 index 0000000..756f0d6 --- /dev/null +++ b/exercises/traefik/traefik-dashboard-ingress.yaml @@ -0,0 +1,18 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: traefik-ingress + namespace: kube-system +spec: + ingressClassName: traefik + rules: + - host: traefik.local.host + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: traefik-dashboard + port: + number: 9000 \ No newline at end of file diff --git a/exercises/traefik/traefik-dashboard-service.yaml b/exercises/traefik/traefik-dashboard-service.yaml new file mode 100644 index 0000000..a201ccf --- /dev/null +++ b/exercises/traefik/traefik-dashboard-service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: traefik-dashboard + namespace: kube-system + labels: + app.kubernetes.io/instance: traefik + app.kubernetes.io/name: traefik-dashboard +spec: + type: ClusterIP + ports: + - name: traefik + port: 9000 # Dashboard listens on port 9000 + targetPort: 9000 # Forward traffic to this port on Traefik pods + protocol: TCP + selector: + app.kubernetes.io/instance: traefik-kube-system + app.kubernetes.io/name: traefik \ No newline at end of file