Daily MaverickWorkers’ trillions should rebuild SA for all, not feed financial speculation for the fewThe Jerusalem PostFormer hostages blast 'NAZA' documentary, accuse filmmakers of erasing their sufferingESPN DeportesBroncos y Chiefs prometen gran espectáculo en el primer "Monday Night Football" de la temporadaInquirerRomualdez now behind barsESPNLamar Jackson looks like old self in Ravens' new offenseRFIUkraine: Volodymyr Zelensky suspend le procureur général accusé de corruptionCBS NewsMitch McConnell returns to Capitol Hill after monthslong absenceABC NewsKennedy Center warns of bankruptcy unless Trump’s name is added to buildingThe Guardian AustraliaAustralia politics live: Labor defends Catherine King’s top-tier Qantas membership gift; Hanson accused of ‘Americanising’ politicsالشرقخلاف بشأن "عثمانوف" يؤجل تجديد عقوبات الاتحاد الأوروبي على روسياABC News (Australia)Fitzroy Crossing calls for stronger FASD supportThe Japan TimesJapanese lawmakers seek out renewed dialogue in Beijing
The Daily Newsstand · Free, Always
Monday, September 14, 2026

Выполняю тестовое задание для DevOps Cloud.ru Camp 2025

Translate

Я тут недавно пытался попасть на бесплатные курсы от Aston, но получил отказ с такой формулировкой.

Денис, добрый день!
После изучения вашей анкеты, к сожалению, мы не готовы пригласить вас к дальнейшему рассмотрению на обучение в компании. У нас очень большой поток кандидатов на курсы. В данный момент взяли в рассмотрение участников с показателями выше. Ваша анкета будет сохранена,  возможно, вернемся к вашей кандидатуре, при наборе на следующий поток.
Благодарим за внимание к нашей компании.

Хотя я даже не понял, как смогли оценить мои показатели и в чем они измерялись, если не давали ни тестового задания, ни собеседования. В анкете у них несколько вопросов и уровень образования. Но да ладно, это тема отдельной статьи.

Сегодня хочу попробовать пройти тестовое задание от клауд.ру. Оно охватывает тот самый стек для devops, linux, docker, git, kubernetes.

На первый взгляд, задание кажется не таким уж и сложным, однако, дьявол кроется в деталях и есть неоднозначное право выбора, которое будет зависеть от ситуации.

Тестовое задание состоит из 4 задач, начнем с первой.

Web приложение на Python/Golang

Цель

Написать простейший echo-server на Python или Go

Задача

Требуется написать простое веб-приложение на Python/Go, которое слушает входящие соединения на порту 8000 и предоставляет страницу с информацией:

    имя хоста
    ip адрес хоста
    имя автора, которое передаётся через переменную окружения $AUTHOR

Для приложения написать Dockerfile и запушить образ в докер хаб* (запушить в приватный регистри - пройти регистрацию в докер хаб, сделать регистри приватным).

В ответе к задаче приложить:

    исходный код
    Dockerfile
    команды, которые использовались для пуша образа в регистри

Полученный артефакты положить в папку /01-application

На первом пункте уже кроется подвох. Python или Go , здесь нужно знать контекст, потому что при грамотной сборке на го, образ получится около 2 мб, вместо около 20 на пайтоне. С другой стороны, на питоне докерфайл писать легче, потому как мультистэйдж сборка здесь не прокатит.

Для чистоты эксперимента, я сделаю 2 варианта, начну с го.

Создаем папку 01-application, в ней 2 файла main.go, go.mod.

// main.go
package main

import (
	"fmt"
	"net"
	"net/http"
	"os"
)

func envOr(key, fallback string) string {
	if v := os.Getenv(key); v != "" {
		return v
	}
	return fallback
}

// IP контейнера в его собственном network namespace.
func getContainerIP() string {
	conn, err := net.Dial("udp", "8.8.8.8:80")
	if err != nil {
		return "unknown"
	}
	defer conn.Close()
	return conn.LocalAddr().(*net.UDPAddr).IP.String()
}

func handler(w http.ResponseWriter, r *http.Request) {
	containerHost, _ := os.Hostname()   // имя из UTS namespace = ID контейнера
	containerIP := getContainerIP()     // адрес из network namespace

	// Данные ХОСТА не добываются — они приходят снаружи.
	hostHost := envOr("HOST_HOSTNAME", "(not injected)")
	hostIP := envOr("HOST_IP", "(not injected)")
	author := envOr("AUTHOR", "anonymous")

	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	fmt.Fprintf(w, `<!DOCTYPE html>
<html><head><title>Echo Server</title></head>
<body style="font-family:monospace">
  <h1>Echo Server</h1>
  <ul>
    <li><b>Container hostname:</b> %s</li>
    <li><b>Container IP:</b> %s</li>
    <li><b>Host hostname:</b> %s</li>
    <li><b>Host IP:</b> %s</li>
    <li><b>Author:</b> %s</li>
  </ul>
</body></html>`, containerHost, containerIP, hostHost, hostIP, author)
}

func healthz(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "ok")
}

func main() {
	http.HandleFunc("/", handler)
	http.HandleFunc("/healthz", healthz)
	http.ListenAndServe(":8000", nil)
}
// go.mod
module echo-server

go 1.23

Далее напишем докерфайл, используя лучшие практики, мультистэйдж сборки, выкинем все лишнее, чтобы образ весил минимальное количество мегабайт, а также создадим пользователя, чтобы контейнер не был запущен от рута.

# ---- Этап 1: сборка ----
FROM golang:1.23-alpine AS builder
WORKDIR /src
COPY go.mod ./
COPY main.go ./
# CGO_ENABLED=0 — статическая линковка, чтобы бинарник
# жил в scratch без единой системной библиотеки.
# -ldflags="-s -w" срезает отладочные символы (меньше вес).
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /echo-server .
RUN echo 'appuser:x:10001:10001::/:' > /etc/passwd

# ---- Этап 2: финальный образ ----
FROM scratch
COPY --from=builder /echo-server /echo-server
COPY --from=builder /etc/passwd /etc/passwd
USER appuser
EXPOSE 8000
ENV AUTHOR="alfabuster"
ENTRYPOINT ["/echo-server"]

Далее зарегаемся на докерхабе, если еще нет аккаунта, создаем репозиторий, делаем его приватным и создаем токен доступа.

# 1. Логинимся (введёт логин и Access Token — не пароль, пароли уже немодно)
docker login -u <ваш_логин>

# 2. Собираем с правильным тегом: <логин>/<репозиторий>:<версия>
docker build -t <ваш_логин>/echo-server:1.0 .

# 3. Проверяем локально, что оно вообще дышит
docker run --rm -p 8001:8000 \
  -e AUTHOR="alfabuster" \
  -e HOST_HOSTNAME="$(hostname)" \
  -e HOST_IP="$(ip -4 route get 1.1.1.1 | grep -oP 'src \K\S+')" \
  <ваш_логин>/echo-server:1.0
#    открываем http://localhost:8001 — должны увидеть hostname/IP/author

Если все сделали правильно, то увидим инфу о контейнере и хосте.

# 4. Пушим в приватный регистри
docker push <ваш_логин>/echo-server:1.0

Теперь проделаем, все тоже самое, но для пайтона.

В папке созаем server.py с таким кодом.

#!/usr/bin/env python3
import os
import socket
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer


def env_or(key, fallback):
    return os.getenv(key) or fallback


def get_container_ip():
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        s.connect(("8.8.8.8", 80))
        return s.getsockname()[0]
    except Exception:
        return "unknown"
    finally:
        s.close()


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/healthz":
            self.send_response(200)
            self.send_header("Content-Type", "text/plain")
            self.end_headers()
            self.wfile.write(b"ok\n")
            return

        container_host = socket.gethostname()      # ID контейнера
        container_ip = get_container_ip()
        host_host = env_or("HOST_HOSTNAME", "(not injected)")
        host_ip = env_or("HOST_IP", "(not injected)")
        author = env_or("AUTHOR", "anonymous")

        body = f"""<!DOCTYPE html>
<html><head><title>Echo Server</title></head>
<body style="font-family:monospace">
  <h1>Echo Server (Python)</h1>
  <ul>
    <li><b>Container hostname:</b> {container_host}</li>
    <li><b>Container IP:</b> {container_ip}</li>
    <li><b>Host hostname:</b> {host_host}</li>
    <li><b>Host IP:</b> {host_ip}</li>
    <li><b>Author:</b> {author}</li>
  </ul>
</body></html>"""

        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.end_headers()
        self.wfile.write(body.encode("utf-8"))

    def log_message(self, *args):
        pass


if __name__ == "__main__":
    ThreadingHTTPServer(("0.0.0.0", 8003), Handler).serve_forever()

Напишем простой докерфайл. А теперь ключевой момент, ради которого всё затевалось. Помните элегантный финал Go-версии — FROM scratch, образ из чистой пустоты весом в мегабайты? С Python так не выйдет никогда. Python — язык интерпретируемый: чтобы запустить server.py, в образе обязан жить сам интерпретатор со всей его свитой. В пустоту его не положишь, multi-stage тут бесполезен — выкидывать нечего, рантайм нужен целиком.

FROM python:3.12-alpine
WORKDIR /app
RUN adduser -D appuser 
COPY --chown=appuser:appuser app.py .
COPY server.py .
EXPOSE 8003
ENV AUTHOR="alfabuster"
ENTRYPOINT [ "python3" , "server.py"]

Однако писать такой файл проще, всего 8 строк, которые по гайду из докера, напишет даже человек без профильного образования в IT.

Теперь сравним размер полученных из билда образов.

alt text

alt text

Как видно разница примерно в 10 раз. Здесь однозначно выигрывает go.

А что на счет безопасности? Образ на scratch — это крепость без дверей: там нет sh, нет apt, нет curl, нет вообще ничего, кроме бинарника. Злоумышленник, пробравшийся в такой контейнер, оказывается в пустой бетонной коробке — ни закрепиться, ни осмотреться, ни подтянуть инструменты. Однако scratch не является защитой от RCE, при определенных условиях можно использовать загруженные/встроенные бинарники или эксплуатировать уязвимости ядра/процесса.

В python:3.12-alpine sh присутсвует. Также содержит сам Python, поэтому при RCE атакующий может использовать Python как инструмент, даже если каких-то Unix-команд нет.

Справедливости ради — у Python есть и козыри. Dockerfile проще (никакой стадии сборки), правки на лету без перекомпиляции, а внутрь можно зайти и потыкать палкой, когда что-то пошло не так. Go-scratch за свою неприступность расплачивается тем, что отлаживать его — как чинить запечатанные часы: удобной дверцы нет. Это классический размен безопасность против удобства.

Поскольку я учусь на безопасника, для меня выбор очевиден, однако в рамках тестового задания, не знаю, какой вариант правильный, возможно они оба верные. Просто для прода, нужно знать контекст и действовать по ситуации.

Тем временем первая задача выполнена, переходим ко второй.

Ansible playbook или роль

Цель

Автоматизация конфигурации на Ansible
Задача

Требуется поднять Виртуальную Машину (VM) с ОС Ubuntu 22.04. Возможные сценарии: локально через Virtual Box (или любое другое средство виртуализации), либо воспользовавшись Free-tier предложением от cloud.ru - https://cloud.ru/offers/free-tier.

Выполнить следующие задачи, используя Ansible:

    На VM установить пакет docker-ce;
    Запустить на docker контейнеры c подготовленным Web-приложением из первой задачи в 3-х экземплярах. Для развёртывания использовать приватный регистри из первой задачи;
    Настроить балансировщик на nginx, который будет отвечать при обращении к хосту и прокидывать запрос на запущенные контейнеры. Выбрать алгоритм балансировки для приложения и обосновать свой выбор.

В ответе к задаче приложить:

    исходный код Ansible
    текстовое обоснование выбранного алгоритма балансировки

Полученные артефакты положить в папку /02-ansible

Ок, приступим. Поскольку хостовая система у меня Void Linux, вручную качать и устанавливать ubuntu, я не буду, воспользуюсь утилитой vagrant.

Для установки, установлю пакетный менеджер Brew.

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

Он рассчитан на bash, но у меня оболочка fish, поэтому, то что советует установщик в выводе в моем случае не сработает.

Нужно добавить Homebrew в PATH fish

fish_add_path /home/linuxbrew/.linuxbrew/bin
fish_add_path /home/linuxbrew/.linuxbrew/sbin

После этого можно устанавливать сам Vagrant.

brew tap hashicorp/tap
brew install hashicorp/tap/vagrant

На хосте создаю любую папку, в ней создаю файл Vagrantfile.

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/jammy64"
  config.vm.network "private_network",
  ip: "10.0.0.7",
  virtualbox__intnet: "intnet"
  config.vm.provider "virtualbox" do |vb|
    vb.memory = 2048
    vb.cpus   = 2
  end
end

Этим файлом я создал машину во внтуренней сети, которая у меня уже была.

Пингуем сервер, чтобы убедиться, что вагрант все верно настроил, доступ по ssh, настраиваем через ключи.

Теперь можно создавать плейбук через Ansible.

В папке 02-ansible создаем пару файлов ansible.cfg и inventory.ini.

[defaults]
inventory = inventory.ini
remote_user = vagrant
private_key_file = ~/.ssh/id_ed25519
[web]
web-vm ansible_host=10.0.0.7

[web:vars]
ansible_python_interpreter=/usr/bin/python3

В этих файлах указываем, нашу созданную убунту через вагрант, ее ip ssh ключ и переменные.

Для данной задачи хватит и плейбука, можно конечно городить и роль, но для данной задачи хватит и плейбука.

Нам нужно, установить docker, залогиниться в приватный докер репозиторий, запустить 3 контейнера и накрыть балансировщиком на nginx.

Возможно руками, залезь на 1 сервак, там установить докер и запустить 3 контейнера будет быстрее. Но задание требует именно ansible, поэтому файл будет достаточно большим.

Начало плейбука

---
- name: Deploy echo-server behind an nginx load balancer
  hosts: web
  become: true          # выполнять с sudo — иначе apt ругнется на permission

  vars:
    docker_image: "<ваш_логин>/echo-server:1.0"
    author_name: "alfabuster"
    container_count: 3
    base_port: 8000                  # контейнеры сядут на 8001, 8002, 8003
    registry_user: "<ваш_логин>"
    registry_password: "{{ vault_registry_token }}"   # прячем в vault, создадим далее

Далее установим сам докер

  tasks:
    - name: Install prerequisites
      apt:
        name: [ca-certificates, curl, gnupg, python3-pip]
        state: present
        update_cache: true

    - name: Create apt keyrings directory
      file:
        path: /etc/apt/keyrings
        state: directory
        mode: '0755'

    - name: Download Docker's GPG key
      get_url:
        url: https://download.docker.com/linux/ubuntu/gpg
        dest: /etc/apt/keyrings/docker.asc
        mode: '0644'

    - name: Add Docker apt repository
      apt_repository:
        repo: "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable"
        filename: docker
        state: present

    - name: Install docker-ce
      apt:
        name: [docker-ce, docker-ce-cli, containerd.io]
        state: present
        update_cache: true

    - name: Install Docker SDK for Python
      pip:
        name: docker
        state: present

    - name: Ensure docker is running and enabled
      service:
        name: docker
        state: started
        enabled: true

Обратите внимание на ansible_distribution_release — это факт, который Ansible сам собирает о хосте перед стартом (для 22.04 подставит jammy).

Модуль docker_container работает не через CLI, а через Python-библиотеку docker. Поэтому мы ставим pip install docker — без неё модуль выдаст загадочную ошибку.

Приватный реджистри и запуск трех контейнеров

- name: Log in to the private registry
  community.docker.docker_login:
    username: "{{ registry_user }}"
    password: "{{ registry_password }}"

- name: Pull the application image
  community.docker.docker_image:
    name: "{{ docker_image }}"
    source: pull

- name: Run echo-server containers
  community.docker.docker_container:
    name: "echo-{{ item }}"
    image: "{{ docker_image }}"
    state: started
    restart_policy: unless-stopped
    published_ports:
      - "127.0.0.1:{{ item }}:8000"
    env:
      AUTHOR: "{{ author_name }}"
  loop:
    - 8001
    - 8002
    - 8003

published_ports привязан к 127.0.0.1, а не к 0.0.0.0. Контейнеры слушают только локально — снаружи к ним не достучаться, единственная дверь наружу — через nginx.

Nginx и его конфиг через шаблон.

    - name: Install nginx
      apt:
        name: nginx
        state: present

    - name: Deploy the load-balancer config
      template:
        src: nginx-lb.conf.j2
        dest: /etc/nginx/sites-available/echo-lb.conf
      notify: Reload nginx

    - name: Enable the site
      file:
        src: /etc/nginx/sites-available/echo-lb.conf
        dest: /etc/nginx/sites-enabled/echo-lb.conf
        state: link
      notify: Reload nginx

    - name: Remove the default nginx site
      file:
        path: /etc/nginx/sites-enabled/default
        state: absent
      notify: Reload nginx

    - name: Ensure nginx is running and enabled
      service:
        name: nginx
        state: started
        enabled: true

  handlers:
    - name: Reload nginx
      service:
        name: nginx
        state: reloaded

handlers — механизм, который отличает Ansible от тупого шелл-скрипта. notify: Reload nginx не перезагружает nginx немедленно. Он ставит галочку «надо будет перечитать конфиг» и делает это один раз в конце, и только если конфиг реально изменился.

Шаблон templates/nginx-lb.conf.j2

upstream echo_backend {
    server 127.0.0.1:8001;
    server 127.0.0.1:8002;
    server 127.0.0.1:8003;
}

server {
    listen 80 default_server;
    server_name _;

    location / {
        proxy_pass http://echo_backend;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    }
}

Обоснование алгоритма

А теперь ради чего всё затевалось. У nginx есть несколько алгоритмов балансировки, и вот полный расклад, кто есть кто:

  • round-robin (по умолчанию) — раздаёт запросы по кругу: первый на 8001, второй на 8002, третий на 8003, четвёртый снова на 8001.

  • least_conn — отдаёт тому бэкенду, у кого меньше активных соединений.

  • ip_hash — привязывает клиента к бэкенду по хешу его IP (липкие сессии).

  • hash / random — экзотика для особых случаев.

Поскольку у нас простое stateless приложение, нет ни куков, ни личного кабинета, ни сессий. Все 3 контейнера равнозначны и идентичны. Поэтому равномерное распределение по кругу будет оптимальным вариантом в данной ситуации.

Least_conn здесь не станет преимуществом, потому как нагрзука в условии не указана. ip_hash в данной топологии противопоказан, так как весь трафик приходит с localhost и был бы прикован к одному контейнеру.

Поэтому выбираем round-robin.

Создаем vault и запускаем плейбук

Создание vault и пароля к нему.

ansible-vault create group_vars/web/vault.yml

Внутри одна строка с токеном из докера

vault_registry_token: "docker_token"

Далее можем запускать наш плейбук.

ansible-playbook playbook.yaml --ask-vault-pass

Тут не все прошло гладко, вылезла ошибка.

fatal: [web-vm]: FAILED! => {"changed": false, "msg": "Error connecting: Error while fetching server API version: Not supported URL scheme http+docker"}

Это произошло потому что, я давно не обновлял коллекции из ansible. Чиним.

ansible-galaxy collection install community.docker --upgrade

Запускаем плейбук еще раз и все заработало.

Проверяем сервак.

Балансировщик работает, второе задание выполнено.

Kubernetes manifest

Цель

Деплой приложения в Kubernetes
Задача

Требуется написать манифест для запуска приложения из первой задачи в Kubernetes в отдельном неймспейсе в виде Deployment с 3 репликами и сервиса с типом ClusterIP.

Выполнить следующие задачи:

    Для приложения организовать проброс значения переменной AUTHOR;
    Реализовать readiness- и liveness- пробы;
    Использовать образ из приватного регистри из задачи 1;
    опционально: поднять ingress-controller и создать правило ingress;
    опционально: использовать helm-chart, а не raw манифест.

В ответе к задаче приложить:

    исходный код всех манифестов

Полученные артефакты положить в папку /03-kubernetes

В задании не сказано, что нужно установливать кубер на машину. Однако для наглядности нужно понимать и тестировать написанные манифесты.

У меня на убунту для тестов как раз стоит кубер и 2 воркер ноды версии v1.31.14. Поэтому я применю написанные манифесты.

Недавно проходил курс Certified Kubernetes Administrator от kodekloud, там подсмотрел полезную тему для генерации манифестов.

Во первых, пропишем алиас, чтобы каждый раз не вводить kubectl.

alias k=kubectl

Во вторых, почти любой объект kubectl create умеет напечатать готовый манифест вместо создания.

--dry-run=client -o yaml

Дальше можно воспользоваться перенаправлением в файл и у нас будет готовый шаблон манифеста, с правильными отступами по канонам yaml.

Создаем namespace

k create ns cloud --dry-run=client -o yaml

В выводе будет лежать готовый манифест.

apiVersion: v1
kind: Namespace
metadata:
  creationTimestamp: null
  name: cloud
spec: {}
status: {}

Конечно врядли, кто пишет манифест для создания namespace, его проще создать командой k create ns name. Однако в задании нужен именно манифест.

Секрет

Задача усложняется тем, что реджистри приватный. Для него необходимо создавать секрет. Иначе получим вечный ImagePullBackOff.

Создаётся секрет императивно, чтобы не хранить токен в git.

kubectl create secret docker-registry regcred \
  --docker-server=https://index.docker.io/v1/ \
  --docker-username=<ваш_логин> \
  --docker-password=<ваш_access_token> \
  --namespace=cloud

Секрет живёт внутри неймспейса cloud — в другом неймспейсе он невидим.

Configmap

Можно было впихнуть AUTHOR прямо в манифест как value: “alfabuster”. Но задание просит «организовать проброс», а по-взрослому переменные конфигурации живут в ConfigMap — отдельно от кода деплоя:

apiVersion: v1
kind: ConfigMap
metadata:
  creationTimestamp: null
  name: cloud-configmap
  namespace: cloud
data:
  AUTHOR: alfabuster

Deployment

Собственно дальше идет деплоймент с 3 репликами и пробами. Генерируем шаблон, а дальше правим руками.

k create deploy cloud-deploy --image=alfabuster/echo-server:1.0 --replicas=3 --port=8000 -n cloud --dry-run=client -o yaml

Итоговый манифест получился такой.

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: cloud-deploy
  name: cloud-deploy
  namespace: cloud
spec:
  replicas: 3
  selector:
    matchLabels:
      app: cloud-deploy
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: cloud-deploy
    spec:
      imagePullSecrets:
        - name: regcred
      containers:
      - image: alfabuster/echo-server:1.0
        name: echo-server
        ports:
        - containerPort: 8000
        env:
          - name: AUTHOR
            valueFrom:
              configMapKeyRef:
                name: cloud-configmap
                key: AUTHOR
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8000
          initialDelaySeconds: 10
          periodSeconds: 5
          timeoutSeconds: 3
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /healthz
            port: 8000
          periodSeconds: 5
  • livenessProbe отвечает на вопрос «пациент вообще жив?». Если проба падает — Kubernetes перезапускает контейнер. Это лекарство от зависших процессов и дедлоков.

  • readinessProbe отвечает на вопрос «пациент готов принимать посетителей?». Если проба падает — под не перезапускается, его просто исключают из эндпоинтов Service, то есть перестают слать трафик, пока он не оклемается.

Service

Service в Kubernetes — это тоже балансировщик. ClusterIP раскидывает входящие соединения по всем готовым подам.

apiVersion: v1
kind: Service
metadata:
  name: cloud-deploy
  namespace: cloud
spec:
  type: ClusterIP
  selector:
    app: cloud-deploy
  ports:
    - port: 80
      targetPort: 8000
      protocol: TCP

Ingress

Создаем уже привычным способом, через генератор.

k create ingress cloud-ingress --rule="cloud.local/*=cloud-deploy:80" -n cloud --dry-run=client -o yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  creationTimestamp: null
  name: cloud-ingress
  namespace: cloud
spec:
  rules:
  - host: cloud.local
    http:
      paths:
      - backend:
          service:
            name: cloud-deploy
            port:
              number: 80
        path: /
        pathType: Prefix
status:
  loadBalancer: {}

Домен cloud.local надо прописать в /etc/hosts, указав на IP кластера (minikube ip или адрес ноды). Без этой записи браузер будет искать cloud.local в интернете.

alt text

alt text

По итогу должно получиться, тоже самое приложение, но уже в кубере на своем домене, с 3 репликами и пробами.

alt text

alt text

Запереть контейнер изнутри*

Цель

Проверить навыки администрирования *nix
Задача

    Создать ВМ с ОС Ubuntu 22.04;
    Установить пакет docker-ce;
    Выполнить команду docker run -it --rm ubuntu -- bash;
    Не покидая контейнер, не меняя флаги запуска и настройки демона, выполнить задачи:
    4.1. сделать apt-get update, показать, что команда выполняется успешно;
    4.2. изнутри контейнера заблокировать ему выход в сеть интернет, команды блокировки с описанием приложить к задаче;
    4.3. показать, что apt-get update больше не работает.
    Доп. задача - найти ещё +1 способ выполнить подобную блокировку изнутри контейнера.

В ответе к задаче приложить:

    текстовое описание применённых конфигураций

Полученные артефакты положить в папку /04-unix

На первый взгляд задача несложная, однако не зря она со звездочкой. Для начала запустим контейнер с убунтой и проверим связь.

alt text

alt text

Вот в чём фокус: Docker по умолчанию запускает контейнеры с урезанным набором Linux capabilities, выбрасывая большинство привилегий, даже когда внутри вы root.

Root внутри контейнера не равняется рут на хосте.

Из очевидного в голову приходит установить iptables и запретить исходящий трафик. Установить можем, однако для правки таблицей маршрутизации рута недостаточно.

root@d79ba688c2c9:/# iptables -A OUTPUT -j DROP
iptables v1.8.11 (nf_tables): Could not fetch rule set generation id: Permission denied (you must be root)

Контейнер работает под root, но демон docker по умолчанию сбрасывает все capabilities, кроме дефолтного набора. А в таком списке отсутсвует привелегия NET_ADMIN, без которой вы не можете ни трогать iptables, ни гасить интерфейсы, ни править таблицу маршрутизации.

Флаги запуска и настройки демона трогать нельзя, иначе можно было бы просто в запуске добавить нужную капу и задача решилась очень быстро.

Нам нужно заблокировать интернет именно для утилиты apt.

Способ №1 - nsswitch.conf

Начну не с самого очевидного варианта файла /etc/nsswitch.conf, я о нем сам недавно узнал, принимая участие в одном CTF.

NSS расшифровывается как Name Service Switch. Это механизм, встроенный в стандартную библиотеку C (glibc). Когда любая программа пытается узнать IP-адрес по доменному имени или проверить, существует ли в системе определенный пользователь, она обращается к glibc. А glibc первым делом читает /etc/nsswitch.conf, чтобы понять, где именно искать ответ.

В структуре этого файла есть строка hosts: files dns

Убрав dns из строки hosts:, вы вообще запрещаете системе обращаться к DNS — резолвиться будет только то, что явно прописано в /etc/hosts.

Кстати NSS управляет не только хостами. В файле прописаны правила для десятка системных баз данных.

Но в данном таске нас интересует dns, поэтому редактируем файл и получаем неработающий apt.

alt text

alt text

Способ №2 - resolv.conf

Второй способ, аналогичен первому, только на этот раз править будем файл /etc/resolv.conf.

Если /etc/nsswitch.conf решает, нужно ли вообще делать DNS-запрос, то /etc/resolv.conf указывает системе, к кому именно с ним обращаться. Это главный конфигурационный файл DNS-резолвера. Он часто генерируется networkmanager’ом, поэтому при следующем подключении вероятнее всего он затрется, но для демонстрации в контейнере, хватит обычной правки через vim или nano.

> /etc/resolv.conf
alt text

alt text

Снова получаем неработающий интернет.

Способ 3 - /etc/hosts

/etc/hosts — это простейший локальный справочник, который жестко связывает IP-адреса с доменными именами.

В него можно прописать локальный ip адрес серверов убунту и обновление не пройдет.

127.0.0.1 archive.ubuntu.com
127.0.0.1 security.ubuntu.com
127.0.0.1 ports.ubuntu.com
alt text

alt text

Способ 4 - apt proxy

У APT есть собственная настройка HTTP/HTTPS-прокси. Можно попробовать для всех HTTP-запросов использовать прокси 127.0.0.1:9.

Порт 9 используется для протокола Discard (RFC 863), который является сетевым эквивалентом черной дыры (/dev/null). Любые данные, отправленные на этот порт по TCP или UDP, просто уничтожаются сервером без какого-либо ответа или подтверждения.

cat > /etc/apt/apt.conf.d/99-block-network <<'EOF'
Acquire::http::Proxy "http://127.0.0.1:9";
Acquire::https::Proxy "http://127.0.0.1:9";
EOF
alt text

alt text

Как итог

Получилось неплохое задание, мне понравилось его выполнять. Много чего в памяти освежил, некоторые вещи изучил. Задание не сказать, чтобы сильно простое, но и не сложное. Некоторые задания потребуют необычного подхода и глубокое знание linux.

View the original on Хабр

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.