DEV Community

Cristiano Lemes
Cristiano Lemes

Posted on

Isolando cargas de trabalho no k8s com kata container

Introdução

Containers revolucionaram a forma como implantamos e gerenciamos aplicações, oferecendo portabilidade, escalabilidade e eficiência no uso de recursos. No entanto, apesar dessas vantagens, containers tradicionais não são projetados para fornecer isolamento total entre cargas de trabalho. Eles compartilham o kernel do sistema operacional do host, o que significa que, em cenários de ataque, vulnerabilidades no kernel ou nos próprios mecanismos de conteinerização, como namespaces e cgroups, podem ser exploradas para comprometer o ambiente do host ou outros containers.

Além disso, ataques como container escape permitem que um invasor rompa as barreiras de isolamento e obtenha acesso a recursos do host. Essa preocupação é ainda mais relevante em ambientes multi-tenant, onde múltiplas cargas de trabalho de diferentes equipes ou clientes podem estar sendo executadas lado a lado.

Por conta dessas limitações, surge a necessidade de soluções mais robustas para isolamento de cargas de trabalho. Tecnologias como Kata Containers e Firecracker oferecem maior segurança ao combinarem a leveza dos containers com o isolamento robusto de máquinas virtuais (VMs), criando uma camada adicional de proteção sem sacrificar a eficiência operacional.

Neste guia, exploraremos como integrar essas tecnologias ao Kubernetes para isolar cargas de trabalho de forma eficaz, reduzindo riscos de segurança em ambientes sensíveis.

Ferramentas Utilizadas

  • Cilium
  • Terraform
  • Ansible
  • Equinix Metal
  • Kata Container
  • Firecraker
  • Helm

Ambiente Local
Ferramentas instaladas

  • Metal Cli (Ferramenta de linha de comando da Equinix Metal)
go install github.com/equinix/metal-cli/cmd/metal@latest 
Enter fullscreen mode Exit fullscreen mode
  • Ansible no Linux com pipx ou uv:
pipx install ansible-core uvx ansible 
Enter fullscreen mode Exit fullscreen mode
  • Ansible no Windows com Docker:
  • Crie os alias no seu $PROFILE
function runansible { docker run -ti --rm `  -v "$HOME\.ssh:/root/.ssh" `  -v "$HOME\.aws:/root/.aws" `  -v "${PWD}:/apps" `  -w /apps `  alpine/ansible ansible @args } New-Alias -Name ansible -Value runansible function playbook { docker run -ti --rm `  -v "$HOME\.ssh:/root/.ssh" `  -v "$HOME\.aws:/root/.aws" `  -v "${PWD}:/apps" `  -w /apps alpine/ansible `  bash -c `  "chmod -R 700 /root/.ssh && ansible-playbook $($args -join ' ')" } New-Alias -Name ansible-playbook -Value playbook 
Enter fullscreen mode Exit fullscreen mode
  • Terraform siga a instalação para sua plataforma

Parte 1: Criando servidores

Nessa guia eu vou usar a Equinix Metal para o ambiente de servidores, você também pode usar seu provedor de Bare metal preferido, ou ambiente de virtualização local se seu ambiente suportar virtualização aninhada (Nested VT-x/AMD-V).
os requisitos são mínimo 2 servidores com 2CPU e 4GB de Ram .

Criando ambiente na Equinix Metal

1. Crie uma conta na Equinix Metal , ou caso já tenha faça login.

  • No momento eles estão oferendo um crédito de $250,00 para testar a plataforma, o suficiente para seguir esse tutorial. O uso é cobrado por hora em instancias on demand, com cobrança mínima de 1 hora (Não adianta desligar depois de 1 minuto, vai cobrar 1 hora).

2. Crie uma chave de API para acessar a Equinix.

  • No console da equinix, selecione o projeto em qual você vai criar suas maquinas, vá em project settings, e em api keys, adicione uma chave com permissão readwrite.

Api Equinix

Criando scripts Terraform para a implantação das máquinas

Siga as instruções abaixo para criar os arquivos e configurar a infraestrutura.

1. Estrutura de Arquivos

📂 k8s-metal-fire/
├── 📂 terraform/
│ ├── 📄 main.tf
│ ├── 📄 output.tf
│ ├── 📄 providers.tf
│ ├── 📄 terraform.tfvars
│ ├── 📄 variables.tf
│ └── 📝 inventory.sh
├── 📂 ansible/
....

Crie a estrutura de pastas e arquivos. Use os comandos abaixo:

mkdir -p k8s-metal-fire/terraform mkdir -p k8s-metal-fire/ansible/{build,inventory,scripts/devmapper} touch k8s-metal-fire/terraform/{main.tf,output.tf,providers.tf,terraform.tfvars,variables.tf} touch k8s-metal-fire/ansible/{build/firecracker,inventory/hosts.yml,scripts/devmapper/{create.sh,reload.sh}} touch k8s-metal-fire/{cluster_bootstrap.yml,k8s_environment.yml,k8s_firecracker.yml,main.yml} 
Enter fullscreen mode Exit fullscreen mode

2. Crie o arquivo main.tf

Esse arquivo define os recursos que serão provisionados no Equinix Metal.

Conteúdo:

resource "equinix_metal_device" "k8s_master" { count = var.k8s_master.num_instances hostname = "k8s-master-${count.index + 1}" plan = var.k8s_master.plan metro = var.em_region operating_system = var.k8s_master.operating_system billing_cycle = var.billing_cycle project_id = var.em_project_id tags = ["kubernetes", "master"] } resource "equinix_metal_device" "k8s_worker" { count = var.k8s_nodes.num_instances hostname = "k8s-worker-${count.index + 1}" plan = var.k8s_nodes.plan metro = var.em_region operating_system = var.k8s_nodes.operating_system billing_cycle = var.billing_cycle project_id = var.em_project_id tags = ["kubernetes", "worker"] } 
Enter fullscreen mode Exit fullscreen mode

O que faz:

  • Define dois tipos de máquinas:
    • Masters: Controlam o cluster Kubernetes.
    • Workers: Executam os workloads (cargas de trabalho).

3. Crie o arquivo output.tf

Esse arquivo define as saídas dos recursos provisionados.

Conteúdo:

output "master_ips" { value = { for device in equinix_metal_device.k8s_master : device.hostname => { "public_ip" = device.access_public_ipv4 "private_ip" = device.access_private_ipv4 } } description = "IP addresses of master nodes" } output "worker_ips" { value = { for device in equinix_metal_device.k8s_worker : device.hostname => { "public_ip" = device.access_public_ipv4 "private_ip" = device.access_private_ipv4 } } description = "IP addresses of worker nodes" } 
Enter fullscreen mode Exit fullscreen mode

O que faz:

  • Mostra os IPs públicos e privados dos masters e workers após a execução do Terraform.

4. Crie o arquivo providers.tf

Esse arquivo configura o provedor do Terraform.

Conteúdo:

terraform { required_providers { equinix = { source = "equinix/equinix" version = "2.11.0" } } # backend "gcs" { # bucket = "cslemes-terraform" #} } provider "equinix" { auth_token = var.em_api_token } 
Enter fullscreen mode Exit fullscreen mode

O que faz:

  • Configura o provedor Equinix Metal para gerenciar os recursos.
  • Inclui um exemplo comentado de backend remoto para armazenar o estado do Terraform.

5. Crie o arquivo terraform.tfvars

Esse arquivo define os valores das variáveis.

Conteúdo:

em_api_token = "xxxxxxxxxxxxxxxxxxxxxxxxx" em_project_id = "xxxxxxxxxxxxxxxxxxxxxxxxxx" em_region = "da" billing_cycle = "hourly" k8s_master = { plan = "c3.small.x86" ipxe_script_url = "" operating_system = "ubuntu_24_04" num_instances = 3 tags = ["k8s_master"] } k8s_nodes = { plan = "c3.small.x86" ipxe_script_url = "" operating_system = "ubuntu_24_04" num_instances = 2 tags = ["k8s-nodes"] } 
Enter fullscreen mode Exit fullscreen mode

O que faz:

  • Define as credenciais (API token e ID do projeto).
  • Configura os planos e características das máquinas para os masters e workers.

6. Crie o arquivo variables.tf

Esse arquivo declara as variáveis usadas no projeto.

Conteúdo:

variable "em_api_token" { description = "Equinix Metal API Key" type = string } variable "em_project_id" { description = "Equinix Metal Project ID" type = string } variable "em_region" { description = "Equinix Metal region to use" type = string } variable "billing_cycle" { description = "value of billing cycle" type = string } variable "k8s_master" { description = "k8s master" type = object({ plan = string ipxe_script_url = optional(string) operating_system = string num_instances = number tags = optional(list(string), []) }) } variable "k8s_nodes" { description = "k8s nodes" type = object({ plan = string ipxe_script_url = optional(string) operating_system = string num_instances = number tags = optional(list(string), []) }) } 
Enter fullscreen mode Exit fullscreen mode

O que faz:

  • Declara as variáveis obrigatórias, como token, projeto, região, e configurações dos nós.

Com esses arquivos criados, você pode iniciar a implantação executando os seguintes comandos no diretório terraform:

terraform init # Inicializa o projeto terraform plan # Exibe o plano de execução terraform apply # Aplica as configurações e provisiona os recursos 
Enter fullscreen mode Exit fullscreen mode

7. Crie o arquivo inventory.sh

  • Esse script vai pegar o output do terraform e gerar o arquivo de inventory para o ansible.
terraform output -json | jq -r ' .master_ips.value as $masters | .worker_ips.value as $workers | { all: { children: { k8s_master: { hosts: ( $masters | to_entries | map({ (.key): { ansible_host: .value.public_ip, ansible_user: "root" } }) | add ) }, k8s_workers: { hosts: ( $workers | to_entries | map({ (.key): { ansible_host: .value.public_ip, ansible_user: "root" } }) | add ) } } } } ' | yq -P > ../ansible/hosts.yaml 
Enter fullscreen mode Exit fullscreen mode

Criando manifestos Ansible para configuração do cluster Kubernetes

Você pode criar os arquivos necessários para o Ansible em uma estrutura organizada. Aqui está um guia passo a passo para criar e organizar os arquivos mencionados:


1. Estrutura de diretórios

Crie os seguintes diretórios e arquivos no seu projeto:

📂 ansible/
├── 📂 group_vars/
├── 📂 host_vars/
├── 📂 roles/
│ ├── 📂 k8s_environment/
│ │ ├── 📂 scripts/
│ │ │ ├── 📂 devmapper/
│ │ │ │ ├── 📄 create.sh
│ │ │ │ └── 📄 reload.sh
│ │ │ └── 📄 devmapper_reload.service
│ │ ├── 📂 tasks/
│ │ │ └── 📄 main.yml
│ ├── 📂 k8s_bootstrap/
│ │ ├── 📂 build/
│ │ │ └── 📄 firecraker
│ │ ├── 📂 tasks/
│ │ │ └── 📄 main.yml
│ ├── 📂 k8s_firecracker/
│ │ ├── 📂 tasks/
│ │ │ └── 📄 main.yml
│ ├── 📂 apply_kata/
│ │ ├── 📂 tasks/
│ │ │ └── 📄 main.yml
├── 📄 hosts.yml
└── 📄 playbook.yml

2. Arquivo hosts.yml

Define os grupos de hosts (master e workers), vamos cria-lo a partir do output do terraform aqui é um exemplo:

all: children: k8s_master: hosts: node1: ansible_host: 147.75.45.67 ansible_user: root workers: hosts: node: ansible_host: 147.28.197.223 ansible_user: root 
Enter fullscreen mode Exit fullscreen mode

3. Arquivo playbook.yml

O ponto de entrada principal do Ansible:

- name: Setup Kubernetes cluster hosts: all become: yes tasks: - name: Install Kubernetes dependencies import_role: name: k8s_environment - name: Configure Firecracker hosts: all become: yes tasks: - name: Configure Firecracker import_role: name: k8s_firecracker - name: Add kube-vip hosts: k8s_master become: yes tasks: - name: Generate kube-vip manifest include_tasks: kube-vip.yaml - name: Bootstrap Kubernetes cluster hosts: k8s_master become: yes tasks: - name: Bootstrap cluster import_role: name: k8s_bootstrap - name: Apply Kata manifests hosts: k8s_master become: yes tasks: - name: Apply Kata import_role: name: apply_kata - name: Join worker nodes hosts: k8s_workers become: yes tasks: - name: Join cluster command: "{{ hostvars[groups['k8s_master'][0]]['join_command'] }}" args: creates: /etc/kubernetes/kubelet.conf 
Enter fullscreen mode Exit fullscreen mode

4. Arquivo roles/k8s_environment/tasks/main.yml

Responsável por configurar o ambiente do Kubernetes:

--- - name: Update apt cache ansible.builtin.apt: update_cache: yes - name: Install required packages ansible.builtin.apt: name: - apt-transport-https - ca-certificates - curl - gnupg - lsb-release - thin-provisioning-tools - lvm2 - bc state: present - name: Install Container runtime ansible.builtin.apt: name: - containerd state: present - name: Enable and start container runtime ansible.builtin.systemd: name: containerd state: started enabled: yes - name: Create Containerd Directory ansible.builtin.file: path: /etc/containerd state: directory mode: "0755" - name: Configure containerd default ansible.builtin.shell: | mkdir -p /etc/containerd containerd config default > /etc/containerd/config.toml sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml - name: Reload containerd ansible.builtin.systemd: name: containerd state: started - name: Download Kubernetes GPG key ansible.builtin.get_url: url: https://pkgs.k8s.io/core:/stable:/v1.31/deb/Release.key dest: /tmp/kubernetes-release.key - name: Crege keyring directory ansible.builtin.file: path: /etc/apt/keyrings state: directory - name: Convert and move Kubernetes GPG key ansible.builtin.command: cmd: gpg --yes --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg /tmp/kubernetes-release.key - name: Add Kubernetes repository ansible.builtin.lineinfile: path: /etc/apt/sources.list.d/kubernetes.list line: "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.31/deb/ /" create: yes - name: Update apt cache apt: update_cache: yes - name: Install Kubernetes packages apt: name: - kubelet - kubeadm - kubectl state: present - name: Hold Kubernetes packages dpkg_selections: name: "{{ item }}" selection: hold loop: - kubelet - kubeadm - kubectl - name: Disable swap command: swapoff -a when: ansible_swaptotal_mb > 0 - name: Remove swap from /etc/fstab lineinfile: path: /etc/fstab regexp: '^[^#].*\sswap\s.*' state: absent - name: Enable kernel modules modprobe: name: "{{ item }}" state: present loop: - overlay - br_netfilter - name: Add kernel modules to load on boot copy: dest: /etc/modules-load.d/k8s.conf content: | overlay br_netfilter - name: Set kernel parameters for Kubernetes sysctl: name: "{{ item.name }}" value: "{{ item.value }}" state: present reload: yes loop: - { name: "net.bridge.bridge-nf-call-iptables", value: "1" } - { name: "net.bridge.bridge-nf-call-ip6tables", value: "1" } - { name: "net.ipv4.ip_forward", value: "1" } - name: Set extra args for kubelet ansible.builtin.lineinfile: path: /etc/default/kubelet regexp: "^KUBELET_EXTRA_ARGS=" line: 'KUBELET_EXTRA_ARGS="--cloud-provider=external"' state: present 
Enter fullscreen mode Exit fullscreen mode

5. Arquivo roles/k8s_bootstrap/tasks/main.yml

Responsável pelo bootstrap do cluster Kubernetes:

--- - name: Get the IP address of the master node set_fact: advertise_address: "{{ ansible_default_ipv4.address }}" # advertise_address: "10.70.191.131" - name: Initialize Kubernetes cluster command: kubeadm init --skip-phases=addon/kube-proxy --pod-network-cidr=10.244.0.0/16 --apiserver-advertise-address {{ advertise_address }} register: kubeadm_init args: creates: /etc/kubernetes/admin.conf - name: Create .kube directory file: path: /root/.kube state: directory mode: "0755" - name: Copy admin.conf to root's kube config copy: src: /etc/kubernetes/admin.conf dest: /root/.kube/config remote_src: yes owner: root group: root mode: "0644" - name: Deploy Calico command: kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.29.1/manifests/calico.yaml - name: Get join command command: kubeadm token create --print-join-command register: join_command - name: Store join command set_fact: join_command: "{{ join_command.stdout }}" 
Enter fullscreen mode Exit fullscreen mode

6. Arquivo roles/k8s_firecracker/tasks/main.yml

Configura o Firecracker:

--- - name: Copy Firecracker binary copy: src: build/firecracker dest: /usr/local/bin/firecracker mode: "0755" - name: Create DevMapper directories ansible.builtin.file: path: /var/lib/containerd/io.containerd.snapshotter.v1.devmapper state: directory mode: "0755" - name: Move and set permissions for DevMapper scripts copy: src: "{{ item.src }}" dest: "{{ item.dest }}" mode: "0755" with_items: - { src: scripts/devmapper/create.sh, dest: /usr/local/bin/devmapper-create.sh, } - { src: scripts/devmapper/reload.sh, dest: /usr/local/bin/devmapper-reload.sh, } - name: Run initial DevMapper creation script ansible.builtin.command: /usr/local/bin/devmapper-create.sh ignore_errors: yes - name: Containerd Configuration Firecracker ansible.builtin.shell: | CONFIG_FILE="/etc/containerd/config.toml" sudo cp "$CONFIG_FILE" "${CONFIG_FILE}.bak" sudo sed -i '/\[plugins."io.containerd.snapshotter.v1.devmapper"\]/,/^$/d' "$CONFIG_FILE" sudo sed -i '/\[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata-fc\]/,/^$/d' "$CONFIG_FILE" cat <<EOF >> /etc/containerd/config.toml [plugins."io.containerd.snapshotter.v1.devmapper"] pool_name = "devpool" root_path = "/var/lib/containerd/io.containerd.snapshotter.v1.devmapper" base_image_size = "40GB" [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.kata-fc] snapshotter = "devmapper" runtime_type = "io.containerd.kata-fc.v2" EOF - name: Create DevMapper reload systemd service copy: dest: /lib/systemd/system/devmapper-reload.service content: | [Unit] Description=Devmapper reload script After=network.target [Service] Type=oneshot ExecStart=/usr/local/bin/devmapper-reload.sh RemainAfterExit=yes [Install] WantedBy=multi-user.target - name: Enable and reload systemd daemon systemd: name: devmapper-reload.service enabled: yes daemon_reload: yes - name: Restart containerd systemd: name: containerd.service state: restarted 
Enter fullscreen mode Exit fullscreen mode

7. Arquivo roles/apply_kata/tasks/main.yml

Aplica os manifestos Kata:

--- - name: Install Kata RBAC ansible.builtin.command: kubectl apply -f https://raw.githubusercontent.com/kata-containers/kata-containers/main/tools/packaging/kata-deploy/kata-rbac/base/kata-rbac.yaml - name: Install Kata Deploy ansible.builtin.command: kubectl apply -f https://raw.githubusercontent.com/kata-containers/kata-containers/main/tools/packaging/kata-deploy/kata-deploy/base/kata-deploy.yaml - name: Install Kata Runtime ansible.builtin.command: kubectl apply -f https://raw.githubusercontent.com/kata-containers/kata-containers/main/tools/packaging/kata-deploy/runtimeclasses/kata-runtimeClasses.yaml - name: Install Rke LocalPath ansible.builtin.command: kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.30/deploy/local-path-storage.yaml 
Enter fullscreen mode Exit fullscreen mode

8. Executando o Ansible

Com tudo configurado, execute o seguinte comando para aplicar o playbook:

ansible-playbook -i hosts.yml playbook.yml 
Enter fullscreen mode Exit fullscreen mode

  • Estrutura de diretórios: Usamos roles para separar as responsabilidades.
  • Playbook: Importa os roles para configurar diferentes partes do cluster.
  • Hosts: Define os grupos de servidores para os nós mestres e trabalhadores.
  • Execução: O comando ansible-playbook aplica todas as tarefas nos servidores.

Rodando tudo junto

  1. Crie um arquivo make file.
.PHONY: all init plan apply destroy ansible-lint terraform-lint ansible-deploy help # Directories TERRAFORM_DIR := terraform ANSIBLE_DIR := ansible # Default target all: init plan apply create-inventory ansible-deploy @echo "Complete deployment finished successfully!" help: @echo "Available targets:" @echo " init - Initialize Terraform" @echo " plan - Create Terraform plan" @echo " apply - Apply Terraform changes" @echo " destroy - Destroy Terraform infrastructure" @echo " create-inventory - Generate Ansible inventory from Terraform outputs" @echo " ansible-lint - Run Ansible linter" @echo " ansible-deploy - Run Ansible playbook" @echo " terraform-lint - Run Terraform formatting and validation" @echo @echo "Example usage:" @echo " make all - Runs init, plan, apply, inventory, and ansible-deploy" # Terraform targets init: cd $(TERRAFORM_DIR) && terraform init plan: cd $(TERRAFORM_DIR) && terraform plan apply: cd $(TERRAFORM_DIR) && terraform apply -auto-approve destroy: cd $(TERRAFORM_DIR) && terraform destroy -auto-approve # Generate Ansible inventory create-inventory: cd $(TERRAFORM_DIR) && ./inventory.sh # Ansible targets ansible-lint: ansible-lint $(ANSIBLE_DIR)/ ansible-deploy: ansible-playbook -i $(ANSIBLE_DIR)/hosts.yml $(ANSIBLE_DIR)/playbook.yml # Terraform linting and validation terraform-lint: cd $(TERRAFORM_DIR) && terraform fmt -check && terraform validate 
Enter fullscreen mode Exit fullscreen mode

Este Makefile automatiza o processo de gerenciamento de infraestrutura com Terraform e Ansible, organizando os comandos em alvos específicos para facilitar o uso e manutenção. Aqui está um resumo das principais funcionalidades:

  1. Alvo Principal (all):

    Executa todo o pipeline, incluindo init, plan, apply, criação do inventário (create-inventory) e a execução do playbook Ansible.

  2. Gerenciamento com Terraform:

    • init: Inicializa o Terraform.
    • plan: Gera o plano de execução.
    • apply: Aplica as alterações na infraestrutura.
    • destroy: Destroi a infraestrutura provisionada.
  3. Inventário Dinâmico:

    • create-inventory: Gera o inventário do Ansible com base na saída do Terraform.
  4. Ansible:

    • ansible-lint: Executa o linter para validar os playbooks.
    • ansible-deploy: Executa o playbook principal (playbook.yml).
  5. Linting e Validação de Terraform:

    • terraform-lint: Valida a formatação e os arquivos de configuração do Terraform.
  6. Ajuda (help):

    Exibe os alvos disponíveis e um exemplo de uso.

    Teste

  7. Crie um manifesto para um nginx

  8. Adicione runtimeClassName: kata-fc em specs.

 apiVersion: v1 kind: Pod metadata: creationTimestamp: null labels: run: nginx1 name: nginx1 spec: runtimeClassName: kata-fc containers: - image: nginx name: nginx1 resources: {} dnsPolicy: ClusterFirst restartPolicy: Always status: {} 
Enter fullscreen mode Exit fullscreen mode
  1. Verifique a versão do kernel do container
 $ k exec -it nginx1 -- bash -c "uname -a" Linux nginx1 6.1.62 #1 SMP Fri Nov 15 11:22:02 UTC 2024 x86_64 GNU/Linux 
Enter fullscreen mode Exit fullscreen mode
  1. E o kernel do host
 root@k8s-master-1:~# uname -a Linux k8s-master-1 6.8.0-49-generic #49-Ubuntu SMP PREEMPT_DYNAMIC Mon Nov 4 02:06:24 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux 
Enter fullscreen mode Exit fullscreen mode

Referências

Repositório do Projeto
Aws Firecracker Kata Containers

Top comments (0)