TGViewer
Channel Public Channel
Сочный DevOps

Сочный DevOps

@andtree_sec

От CI/CD до SIEM, SOC и безопасности — здесь делюсь опытом, мыслями и полезняшками на стыке DevOps и безопасности. Всё сочное — в «Сочном DevOps».
Subscribers
420
Photos
8
Videos
0
Links
52

Showing posts older than #122 · Back to latest

Older Posts 20 shown
Post #121 34
Как по-быстрому развернуть VPN

Пример с wireguard. Само собой понадобится зарубежный VPS с публичным IP адресом на интерфейсе. У меня Ubuntu 22.04.
Ставим нужные пакеты:
apt-get update && apt-get install wireguard wireguard-tools mawk iproute2 qrencode


Включаем ip forwarding в /etc/sysctl.conf:
net.ipv4.ip_forward=1
net.ipv4.conf.all.forwarding=1
net.ipv6.conf.all.forwarding=1


Сохраняем:
sysctl -p


Для простоты настройки, можно скачать скрипт:
wget https://raw.githubusercontent.com/burghardt/easy-wg-quick/master/easy-wg-quick


Даем бит выполнения и запускаем:
chmod +x easy-wg-quick
./easy-wg-quick


После этого в директории запуска будут сгенерированы все нужные конфигурационные файлы. Нужно скопировать основной в /etc/wireguard
cp wghub.conf /etc/wireguard/


И запустить WG:
systemctl start wg-quick@wghub
systemctl enable wg-quick@wghub


Чтобы сделать новый конфиг для клиента:
./easy-wg-quick test
cp wghub.conf /etc/wireguard/
systemctl restart wg-quick@wghub


Конфиг клиента автоматически добавляется в конфиг сервера (wghub), поэтому его нужно заново переместить в нужный каталог и перезапустить wg.
Посмотреть сформированный QR-code можно в консоли:
cat wgclient_<user>.qrcode.txt


Для подключение на ПК, нужно будет импортировать в клиент конфиг, а вот с телефоном все гораздо проще. Достаточно скачать приложение wireguard и отсканировать в нем QR-code.

#vpn
Post #120 35
Защита от DDoS на nginx

В nginx есть параметр limit_req_zone, с его помощью можно ограничивать количество одновременных запросов к сайту с одного ip адреса.
Создадим файл /etc/nginx/conf.d/mapping.conf
geo $limited {
default 1;
<ip_address> 0;
}

map $limited $limit {
1 $binary_remote_addr;
0 "";
}

limit_req_zone $limit zone=defender:10m rate=200r/s;


$binary_remote_addr - зарезервированная переменная в nginx, в ней будет хранится ip клиента.
zone=defender:10m - defender это произвольное имя зоны, 10m - объем памяти в мегабайтах для хранения данных в ней.
rate=200r/s - ограничение равное 200 запросов в секунды с одного ip клиента.

Тут мы используем модуль geo, который нам позволяет задать не просто IP адрес, но и подсеть из адресов. Присваиваем это все переменной $limited и создаем маппинг, где дефолтное значение равно 1, добавленные нами IP адреса или подсети будет иметь значение 0.
Значение 0 в данном случае разрешающее, т.е. то, что имеет значение 0 не будет попадать под ограничение лимитов зоны.
Далее мы создаем маппинг на основе $limited переменной и записываем это в новую переменную $limit. Если на предыдущем шаге, в модуле geo мы получили 0, то в новом маппинге 0 будет соответствовать пустая строка, а 1 будет соответствовать адрес клиента.
Получается, что если мы имеем пустую строку, то у нас нет IP адреса к которому можно было бы применить ограничения зоны, следовательно он пропускается.
Если совсем упрощенно, то <ip_address> 0; - это и есть наш "белый список", остальное будет заполнено автоматически.

Посмотреть белый адрес кстати можно так:
curl ident.me


Потом в нужный на location добавляем строки:
location / {
limit_req zone=defender burst=10 nodelay;
try_files $uri $uri/ /index.php?$args;
}


Тут мы указываем какую зону использовать для данного локейшена. Параметр burst - скачок, возможный сверх лимита, в данном случае на 10 запросов больше.
nodelay - говорит отдавать статус (503 по дефолту) немедленно, когда исчерпан лимит.

Если нужно изменить отдаваемый статус при достижении лимита:
limit_req_status 429;


#nginx
Post #119 39
Переопределение пользователя docker образа в gitlab

Порой бывает, что в базовом образе, в котором выполняются те или иные команды нужно получить root, чтобы доставить пакет или еще чего. Но сам базовый образ запускается от другого пользователя, а пересобирать, публиковать в наш регистри мы не хотим. Обычно это простые кейсы, без сложной логики, поэтому порой даже самой сборки docker в проекте нет.

Переопределить пользователя можно вот так:
image:
name: <image>
docker:
user: root


#gitlab
Post #118 38
Реализация "канарейки" на ingress

Допустим у нас в качестве ingress controller используется nginx. В таком случае можно легко и просто реализовать "канарейку" для проверки нового кода.

Суть реализации - у нас есть на проде стабильно работающее приложение и есть ветка в которой добавляется новый функционал, который требует проверки на проде, но не сразу, а постепенно, например мы хотим 10% трафика пустить на новый код, остальное оставить на старом. Для этого нужно разрешить тем или иным способом, в зависимости от реализации CI/CD деплой новой ветки на прод. Обычно хорошо использовать завязку именования kubernetes объектов по переменной CI_COMMIT_REF_SLUG в gilab-ci, таким образом и если мы используем helm, это создаст новый набор кубовых сущностей с другим именем.
Далее просто выставляем в канареечном ingress нужные аннотации и делаем host равный host основного ingress.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: canary
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
ingressClassName: nginx
rules:
- host: example.ingress.host
http:
paths:
- pathType: Prefix
path: /
backend:
service:
name: canary
port:
number: 80


canary-weight - это процент трафика, который будет идти на данный релиз.
host - должен быть точно такой же, как у основного ingress.

Из практических наблюдений. Бывает такое, что используется набор из нескольких ingress у приложения. Например ingress-int для открытия каких-то внутренних роутов и ingress-ext - внешний, куда проксирует запросы к примеру nginx, обслуживающий внешние домены. Так вот, если оба этих ingress используют один и тот же backend (k8s service), то канарейка работать не будет. В данном случае нужно обязательно пускать каждый ingress через свой backend.

#kubernetes
Post #117 34
Немного про kube-proxy

Это сетевой компонент Kubernetes, который нужен для маршрутизации и балансировки трафика внутри кластера. Kube-proxy создает виртуальные IP адреса для сущностей типа service и распределяет трафик между подами, связанными с этими services. Помимо этого kube-proxy настраивает правила на уровне узла, чтобы трафик, поступающий на виртуальные IP service, направлялся на нужные поды.

Режимы работы:

iptables - строит правила с помощью iptables и распределяет трафик, перебирая цепочки. В больших инсталяциях кластера может тормозить, потому что цепочки правил обрабатываются последовательно.
IPVS (IP Virtual Server) - Использует механизм ядра Linux на уровне подсистемы netfilter для балансировки. Этот режим более производительный и поддерживает разные режимы балансировки, дефолт round-robin, может обрабатывать пакеты параллельно.

Хотя kube-proxy и занимается маршрутизацией и балансировкой, в kubernetes все равно нужен CNI сетевой плагин, который будет отвечать за сеть подов, в то время как kube-proxy нужен именно для сети services.

Чтобы включить IPVS мод, сначала надо проверить, что он доступен на уровне ядра, хотя в современных дистрибутивах вряд ли может быть иначе:
lsmod |grep ip_vs


Открываем на редактирование configMap kube-proxy:
kubectl edit cm kube-proxy -n kube-system


И на самом верхнем уровне указываем mode: ipvs
apiVersion: kubeproxy.config.k8s.io/v1alpha1
kind: KubeProxyConfiguration
mode: "ipvs"


#kubernetes
Post #116 35
Запрещаем использование capabilities в kubernetes с помощью kyverno

Kyverno - это мощный инструмент для обеспечения безопасности в Kubernetes, который умеет валидировать, мутировать и управлять различными сущностями кластера согласно заданным правилам (политикам).

Установить можно следующей командой:
kubectl create -f https://github.com/kyverno/kyverno/releases/download/v1.13.0/install.yaml


Ниже я приведу пример кластерной политики для запрета использования capabilities NET_ADMIN. Аналогично можно запретить и другие CAP, да и много чего в целом.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: prevent-net-admin-capability
spec:
background: false
failurePolicy: Fail
validationFailureAction: Enforce
webhookTimeoutSeconds: 30
rules:
- name: prevent-net-admin-capability
match:
any:
- resources:
kinds:
- Pod
exclude:
any:
- resources:
namespaces:
- "kube-system"
preconditions:
all:
- key: "{{ request.operation }}"
operator: In
value:
- CREATE
- UPDATE
validate:
message: "Adding NET_ADMIN capability is not allowed."
deny:
conditions:
any:
- key: "{{ request.object.spec.containers[0].securityContext.capabilities.add[0] }}"
operator: Equals
value: "NET_ADMIN"
- key: "{{ request.object.spec.initContainers[0].securityContext.capabilities.add[0] }}"
operator: Equals
value: "NET_ADMIN"
- key: "{{ request.object.spec.ephemeralContainers[0].securityContext.capabilities.add[0] }}"
operator: Equals
value: "NET_ADMIN"


В этом примере мы запрещаем добавление NET_ADMIN для containers, initContainers, ephemeralContainers, кроме namespace "kube-system" в секции exclude. Проверим работоспособность, попробовав создать тестовый под:

apiVersion: v1
kind: Pod
metadata:
name: test-pod
spec:
containers:
- name: test-container
image: busybox
command: ["sleep", "3600"]
securityContext:
capabilities:
add:
- NET_ADMIN


В выводе должны увидеть:
error from server: error when creating "pod.yaml": admission webhook "validate.kyverno.svc-fail" denied the request:

resource Pod/default/test-pod was blocked due to the following policies

prevent-net-admin-capability:
prevent-net-admin-capability: Adding NET_ADMIN capability is not allowed.


#devsecops
Post #115 33
Реализация DLX (Dead Letter Exchange) паттерна в RabbitMQ

DLX или его называют очередью повторных попыток, позволяет реализовать логику, при которой, если при обработке сообщения из RabbitMQ произошла ошибка на консьюмере, то мы отправляем сообщение в специальную очередь. В этой очереди оно находится определённое время N, после чего уходит назад в основную очередь, и RabbitMQ, который работает по push модели, проталкивает это сообщение снова в консьюмер.

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

Пример из одного проекта на python, реализующий данных подход. Вся декларация происходит на консьюмере:
async def read_messages(
host: str, username: str, password: str, queue_name: str, callback
) -> None:
connection, channel = await rabbitmq_connection(
host=host, username=username, password=password
)
await channel.set_qos(prefetch_count=settings.rabbitmq.prefetch_count)
logger.info("Соединение с rabbitmq установлено.")
dead_letter_exchange = await channel.declare_exchange(
name=settings.rabbitmq.dlx_exchange_name,
type=aio_pika.ExchangeType.DIRECT,
durable=True,
)
dead_letter_queue = await channel.declare_queue(
name=settings.rabbitmq.dlx_queue_name,
durable=True,
arguments={
"x-message-ttl": settings.rabbitmq.dlx_message_ttl,
"x-dead-letter-exchange": "",
"x-dead-letter-routing-key": settings.rabbitmq.queue_name,
},
)
await dead_letter_queue.bind(
dead_letter_exchange, routing_key=settings.rabbitmq.dlx_routing_key
)
queue = await channel.declare_queue(
name=queue_name,
durable=True,
arguments={
"x-dead-letter-exchange": settings.rabbitmq.dlx_exchange_name,
"x-dead-letter-routing-key": settings.rabbitmq.dlx_routing_key,
},
)
await queue.consume(lambda message: callback(message=message, channel=channel))
await asyncio.Future()
await connection.close()


В dead_letter_exchange - декларируем exchange, который будет отправлять сообщения в очередь повторных попыток. Помним, что в RabbitMQ сообщения всегда попадают сначала в exchange, даже если вам кажется, что вы пишете напрямую в очередь.
dead_letter_queue - декларируем очередь повторных попыток.
Параметры:
x-message-ttl - время в миллисекундах, в течение которого сообщение будет находиться в этой очереди перед повторной отправкой в основную очередь. Это обеспечивает задержку между попытками обработки сообщения.
x-dead-letter-exchange - exchange, куда сообщение должно быть отправлено после истечения TTL. В данном случае это пустая строка "", что означает использование default_exchange.
x-dead-letter-routing-key - routing key, по которому понимаем, в какую очередь отправить сообщение при повторной отправке.

После чего делаем bind между dead_letter_exchange и очередью повторных попыток.

А ниже декларируем основную очередь. Проект небольшой, для реализации кое-каких DevOps штук, поэтому использование default_exchange в данном случае вполне оправдано. Это, собственно, тот exchange, куда сообщения отправляются по умолчанию; binding и routing_key RabbitMQ создаст автоматически. Routing key будет равен имени очереди.

И обработка сообщений в callback функции:
async def message_handle(
message: IncomingMessage, channel: AbstractRobustChannel
) -> None:
try:
async with message.process():
data = json.loads(message.body)
<some logic here>
except Exception as err:
await message.reject(requeue=False)


Здесь используется контекстный менеджер async with, если блок кода внутри выполнится без ошибок, значит в RabbitMQ уйдёт ack, что означает, что сообщение обработано, и оно будет удалено из очереди. Если возникнет ошибка, мы вызываем message.reject(requeue=False), что отклоняет сообщение без повторной постановки в ту же очередь, именно этот параметр помечает сообщение как "мертвое".
#python
Post #114 33
Экстренное удаление pod в kubernetes

Когда Pod не хочет завершаться обычными способами, можно воспользоваться следующей командой для принудительного удаления:
kubectl delete pod <pod_name> --force --grace-period=0 -n <ns_name>


Параметры, которые тут используем:
--force - принудительное удаление пода.
--grace-period=0 - устанавливает период ожидание перед завершением в 0 секунд, что мгновенно убивает процесс.

Немного про grace-period:
grace-period — это время, предоставленное Pod-у для корректного завершения перед его остановкой. Обычно оно задается в секундах и позволяет запущенным процессам завершиться правильно, например, закрыть соединения с базами данных, завершить обработку запросов и т.д.

Если grace-period не указан, Kubernetes использует значение по умолчанию, которое указано в конфигурации Pod-а (terminationGracePeriodSeconds: 30).
Команда выше устанавливает период равным 0, принуждая Pod завершиться немедленно. Это удобно, когда Pod "подвис", но может привести к потере данных или незавершенной работе, так как в таком случае процессу в контейнере отправляется сигнал KILL, в то время как в обычных условиях отправляется TERM.

#kubernetes
Post #113 31
LimitRange в kubernetes

Это сущность, которая используется для управления ограничением ресурсов, которые могут быть назначены контейнерам в подах, подам и PVC (PersistentVolumeClaim). Не обязательно назначать их все, можно указывать только те, что нужно конкретно вам, но ниже я приведу полный манифест:

apiVersion: v1
kind: LimitRange
metadata:
name: resource-limit-range
namespace: default
spec:
limits:
- type: Container
max:
cpu: "2"
memory: "2Gi"
ephemeral-storage: "4Gi"
min:
cpu: "200m"
memory: "256Mi"
ephemeral-storage: "500Mi"
default:
cpu: "500m"
memory: "1Gi"
ephemeral-storage: "1Gi"
defaultRequest:
cpu: "250m"
memory: "512Mi"
ephemeral-storage: "500Mi"

- type: Pod
max:
cpu: "4"
memory: "4Gi"
ephemeral-storage: "8Gi"
min:
cpu: "500m"
memory: "512Mi"
ephemeral-storage: "1Gi"

- type: PersistentVolumeClaim
max:
storage: "100Gi"
min:
storage: "1Gi"
default:
storage: "10Gi"
defaultRequest:
storage: "5Gi"


Есть три типа (type):

1. Container - ограничения для контейнеров. В max - мы задаем максимально возможное значение для контейнера по памяти, CPU, временному хранилищу.
В min соответственно минимальные значения.
default - это дефолтное значение лимита, которое будет автоматически применено, если в манифесте ничего не указано.
defaultRequest - тоже что и default, но только для request.

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

3. PersistentVolumeClaim - ограничение для pvc (запроса на сторадж), параметры аналогичные типу container, но вместо памяти и CPU, размер стораджа.

#kubernetes
Post #112 30
Пример сборки FastApi приложений

FROM python:3.10-slim AS app
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
RUN addgroup fastapi_user --gid 1001 && useradd fastapi_user -u 1001 -g 1001 -s /bin/bash && \
pip install --upgrade pip && \
pip install poetry
COPY pyproject.toml poetry.lock /app/
RUN poetry config virtualenvs.create false && \
poetry install --no-dev --no-interaction --no-ansi
COPY . /app
USER fastapi_user
ENTRYPOINT ["gunicorn", "src.main:app"]
CMD ["--workers", "4", "--worker-class", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8085"]


Из интересного тут это:
PYTHONDONTWRITEBYTECODE - отключает создание файлов байткода (.pyc), которые по дефолту хранятся в каталоге "__pycache__".

PYTHONUNBUFFERED - отключает буферизацию вывода python, заставляя его сразу отправлять данные на stdout и stderr. Это нужно, чтобы нормально видеть логи в контейнере в реальном времени, без задержек на буферизацию.

ENTRYPOINT как правило оставляем такой и берем из Dockerfile, а вот CMD частенько переопределяем в кубовом манифесте с помощью args на актуальные значения.

Не забываем про .dockerignore:

venv/
.env/
.venv/

*.pyc
*.pyo
*.pyd
*.cache

.DS_Store
Thumbs.db

.idea/
.vscode/
*.sublime-project
*.sublime-workspace

.git/
.gitignore
.gitattributes
.gitlab-ci.yml
README.md


tests/
coverage/
*.cover
*.coverage
*.pytest_cache
.tox/
.nox/

*.log

Dockerfile
docker-compose.yml

build/
dist/
job_token


Тут нет "__pycache__" потому что он само собой добавлен в .gitignore.

Сборка и запуск:
docker build -t fastapi-app .
docker run -d -p 8085:8085 fastapi-app


#docker
Post #111 28
Получаем значение защищенной переменной в gitlab

Довольно старый "хак". Пример кейса - есть защищенная групповая переменная, доступа к значению которой через UI нет, а получить его надо. Если сделать "echo $var", то увидим "[Masked]" в ответ. Чтобы получить значение, его нужно перевести в base64. Например:

hack:
stage: .pre
script:
- echo $var | base64


В выводе получим закодированное в base64 значение, которое тем же способом можно декодировать локально:

echo <value> | base64 -d


Стейдж .pre это кстати один из дефолтных в gitlab, который выполняется перед любыми другими.

#gitlab
Post #110 34
Немного про Gunicorn и Uvicorn

Иногда по долгу службы приходится программировать всякое на python, иногда даже какой-то бэкенд. Я обычно использую FastAPI для этого. Кстати, мой сайт devopsoffer написан целиком и полностью на этом фреймворке.

Gunicorn — это синхронный сервер приложений WSGI на Python. Напомню, что WSGI — это стандарт интерфейса между веб-серверами и Python веб-приложениями, созданный для синхронных приложений.

Uvicorn, в свою очередь, — это асинхронный сервер ASGI, который поддерживает приложения на FastAPI и Starlette и позволяет использовать асинхронное выполнение запросов. Как правило, их используют вместе для запуска асинхронных приложений, например, так:

gunicorn src.main:app --workers 5 --worker-class uvicorn.workers.UvicornWorker --bind 127.0.0.1:8085


Эта связка позволяет взять лучшее из обоих решений. Gunicorn создаёт количество воркеров, заданное в параметре --workers, и каждый воркер — это отдельный процесс. Таким образом, каждый воркер получает свой GIL (Global Interpreter Lock), что позволяет обойти его ограничения в плане параллельности. Внутри каждого воркера запускается Uvicorn, который обрабатывает HTTP-запросы асинхронно.

Внутри Uvicorn также можно использовать многопоточность, но это чаще применяется для асинхронной обработки, когда происходит освобождение GIL при I/O-bound операциях, таких как работа с сетью и базой данных.

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

Для расчёта оптимального количества воркеров Gunicorn часто используют формулу: workers=(кол-во ядер)×2+1.

Эта формула может служить хорошей отправной точкой, но реальные значения зависят от нагрузки и характера приложения, и оптимальные параметры часто выявляются опытным путём.

#python
Post #109 30
root-app в argocd

В argocd удобно создавать так называемый root application, который разворачивает все остальные app. Манифест:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root-app
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
destination:
namespace: argocd
server: https://kubernetes.default.svc
project: dev
source:
path: "app/"
repoURL: git@gitlab.dev.lan:root/argocd.git
targetRevision: HEAD
syncPolicy:
automated:
prune: true
selfHeal: true


Я использую репозиторий argocd, где в директории app лежат манифесты самих app. Пример с keycloak:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: keycloak
namespace: argocd
spec:
destination:
namespace: keycloak
server: https://kubernetes.default.svc
project: dev
source:
helm:
valueFiles:
- values.yaml
path: keycloak
repoURL: git@gitlab.dev.lan:root/helm-charts.git
targetRevision: main
syncPolicy:
automated: {}


При развертывании любым удобным способом root-app, он автоматически развернет все приложения из директории app/.
В свою очередь эти приложения могут смотреть например на другие репозитории, как keycloak.

#argocd
Post #108 36
Реальный пример использования buildx с образом nginx с одного из проектов

Первый файл, это buildx-common.hcl, определят общие настройки, задает группы.
# Base variables
variable "CI_REGISTRY_IMAGE" { default = "registry/nginx:master-latest" }
variable "CI_COMMIT_REF_SLUG" { default = "dev-local" }
variable "CI_COMMIT_SHORT_SHA" { default = "00000000" }
variable "CI_PIPELINE_IID" { default = "00000" }
variable "IMG_SEMVER" { default = "v0.0.1" }
variable "CI_DEFAULT_BRANCH" { default = "main" }

# Secrets
variable "NPM_REPO_TOKEN" { default = "job_token" }

# Docker images variables
IMAGE_TAG = "${CI_COMMIT_REF_SLUG}-${CI_PIPELINE_IID}"
IMAGE_TAG_LATEST = "${CI_COMMIT_REF_SLUG}-latest"

IMG_TAGS = (
"${CI_COMMIT_REF_SLUG}" == "${CI_DEFAULT_BRANCH}"
? [
"${CI_REGISTRY_IMAGE}/nginx:${IMAGE_TAG}",
"${CI_REGISTRY_IMAGE}/nginx:${IMAGE_TAG_LATEST}",
"${CI_REGISTRY_IMAGE}/nginx:${IMG_SEMVER}"
]
: [
"${CI_REGISTRY_IMAGE}/nginx:${IMAGE_TAG}",
"${CI_REGISTRY_IMAGE}/nginx:${IMAGE_TAG_LATEST}",
"${CI_REGISTRY_IMAGE}/nginx:${IMG_SEMVER}-${CI_COMMIT_REF_SLUG}"
]
)


group "default" {
targets = ["nginx"]
}


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

Основной файл buildx-nginx.hcl:
target "default" {
context="."
dockerfile=".ops/docker/nginx.Dockerfile"
pull = true
secret = [
"id=npm_repo_token,src=${NPM_REPO_TOKEN}"
]
args = {
BUILDKIT_INLINE_CACHE = "1"
}
output = ["type=registry"]
}

target "nginx" {
inherits = ["default"]
target = "nginx"
tags = "${IMG_TAGS}"
cache-from = [
"${CI_REGISTRY_IMAGE}/nginx:${IMAGE_TAG_LATEST}",
"${CI_REGISTRY_IMAGE}/nginx:master-latest"
]
}

target nginx внутри этого файла соответствует targets из group default основного файла. Таргетов и групп может быть разное кол-во, это позволяет удобно управлять сборкой.
cache-from - получает кэш из указанных образов.
secret - используется для "монитрования" сикретов в Dockerfile. Подробнее об этом в официальной документации.

Сама сборка с помощью bake:
docker buildx bake --file $BUILDX_COMMON_FILE --file $BUILDX_FILE $target


В gitlab-ci $targets можно передать список значений через пробел:
build:nginx:
stage: build
variables:
BUILDX_COMMON_FILE: "buildx-common.hcl"
BUILDX_FILE: "buildx-nginx.hcl"
TARGETS: default


#gitlab
#ci
Post #107 26
Выселение (eviction) подов с недоступной ноды

В kubernetes компонент kube-controller-manager отвечает за управление различными контроллерами, один из таких контроллеров это node controller, он следит за состоянием узлов и отвечает за эвакуацию подов с недоступных нод.

У него есть параметр --pod-eviction-timeout, который определяет время ожидания перед началом эвакуации подов с узла, который считается недоступным. То есть данный параметр задает период, в течении которого контроллер ожидает восстановления связи с нодой, перед тем как начать процесс эвакуации подов, которые размещены на этом узле.

Как это работает?

Node controller периодически проверяет состояние каждого узла в кластере. Если узел не отвечает на проверки, в течении периода, определенного параметром --node-monitor-grace-period, данный узел помечается как недоступный (NodeNotReady).
После этого контроллер начинает отсчет таймера эвакуации, определенного в значении --pod-eviction-timeout, о котором было выше.
Когда таймер заканчивается и если узел не восстановился за это время, то начинается процесс эвакуации подов, kube-controller-manager уведомляет другие контроллеры, например replicaSet о необходимости создать новые реплики подов на других, доступных узлах.

По шагам, картина выглядит примерно так:
1. Узел перестает отвечать.
2. Node controller ожидает восстановления узла согласно времени, заданному в --node-monitor-grace-period.
3. Если узел по прежнему недоступен по истечению этого периода, то запускается таймер эвакуации --pod-eviction-timeout.
4. Когда он заканчивается, контроллер начинает выселять поды с недоступного узла.

#kubernetes
Post #106 31
Интеграция ArgoCD и keycloak

С точки зрения настроек клиента в keycloak все примерно тоже самое, как и для настроек под kubernetes. Отличаться будут только URLs, приведу тут свои:
Root URL: https://argocd.dev.lan
Valid Redirect URIs: https://argocd.dev.lan/auth/callback
Admin URL: https://argocd.dev.lan
Web Origins: https://argocd.dev.lan

Настройки в ArgoCD:
Сначала в сикрете argocd-secret нужно добавить поле oidc.keycloak.clientSecret со значением сикрета клиента из keycloak.
oidc.keycloak.clientSecret: c2VjcmV0X2Zyb21fa2V5Y2xvYWtfYXJnb2NkX2NsaWVudA==


Затем настроить дополнительные опции в configMap argocd-cm:
oidc.config: |
name: Keycloak
issuer: https://keycloak.dev.lan/auth/realms/kubernetes
clientID: argocd
clientSecret: $oidc.keycloak.clientSecret
requestedScopes: ["openid", "profile", "email", "groups"]
rootCA: |
-----BEGIN CERTIFICATE-----
<certificate data here>
-----END CERTIFICATE-----
url: https://argocd.dev.lan


rootCA - это CA от keycloak. Иначе будет ошибка х509, если используются само подписанные сертификаты.

Также нужно добавить "маппинг" групп из keycloak к ролям argocd в configMap argocd-rbac-cm:
data:
policy.csv: |
g, admins, role:admin


role:admin это внутренняя роль внутри argocd.
admins - группа из keycloak.

Теперь при открытии страницы логина будет доступна кнопка "Log in via keycloak". Она перебросит на авторизацию в keycloak и после ее прохождения автоматически вернет в argocd.

#keycloak
#argocd
Post #105 26
Ошибка авторизации при использовании vault-webhook от bank-vault с типом inject

Возможно кто-то как и я использует в своих кластерах vault secret webhook от bank-vault для создания сущностей вроде configMaps или Secrets из волта. Есть еще один режим работы - Inject, при котором вебхук мутирует манифест сущности pod и "спавнит" процесс оборачивая запуск в свой бинарь, например если вы запускаете условный "yarn start", то после мутации это будет выглядеть как "/vaut/vault-env yarn start". Переменные будут не видны в env, только если внутри контейнера посмотреть их через этот бинарь /vault/vault-env.
И порой, если используется например несколько контейнеров в поде и все они из внутренних приватных registry, причем из разных, а следовательно и для пуллинга будет использоваться разные imagePullSecrets можно встретить ошибку:

Error creating: Internal error occurred: failed calling webhook "pods.vault-secrets-webhook.admission.banzaicloud.com": failed to call webhook: an error on the server ("{\"kind\":\"AdmissionReview\",\"apiVersion\":\"admission.k8s.io/v1beta1\",\"response\":{\"uid\":\"8f38f2a7-90dd-4d43-a7b5-dd93b5065cf3\",\"allowed\":false,\"status\":{\"metadata\":{},\"status\":\"Failure\",\"message\":\"could not mutate object: <object> UNAUTHORIZED: HTTP Basic: Access denied.

Это происходит из-за того, что "хук" не умеет читать массив, заданный в imagePullSecret секции, он просто берет первый элемент и пытается с ним идти в regsitry, а идет он туда только в том случае, если в манифесте явно не указан command. Вебхуку нужно как-то понимать какой процесс (команду) спавнить, следовательно, если этого нет в манифесте, то он берет imagePullSecret (первый элемент в массиве) и идет в registry, чтобы получить манифет образа и достать command (entrypoint) оттуда. Тут то и происходит ошибка, если у нас в поде два контейнера, креды к которым надо брать из разных секретов, описанных в imagePullSecert.

Решение достаточно простое, для подобных кейсов указывать в манифесте явно команду запуска контейнера, тогда веб-хуку не нужно будет идти в registy и данной ошибки не будет, да и скорость развертывания будет выше, так как мы убирает лишний HTTP вызов.

#kubernetes
Post #104 27
Как точечно перезапустить контейнер с каким-либо control-plane компонентом через containerd?

Сначала нужно найти ID контейнера на нужной мастер ноде:
ctr -n k8s.io container ls |grep <container_name>


Затем получаем его PID:
ctr -n k8s.io container info <container id> |grep proc | grep path

Пример вывода:
procPath: /proc/12345/exe


Находим связанный таск по PID:
ctr -n k8s.io task ls |grep <pid>


Убиваем ее:
ctr -n k8s.io task kill <task-id>


Если контейнер висит в состоянии остановки, удаляем его.
ctr -n k8s.io container rm <container-id>


После этого контейнер будет перезапущен.

#kubernetes
Post #103 27
finalizers в kubernetes

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

Как это работает?

Когда мы инициализируем удаление объекта kubernetes, он (kubernetes) устанавливает в metadata поле deletionTimestamp, но не удаляет объект до выполнения финализаторов. Затем контроллеры, связанные с объектом, обнаруживают, что объект помечен на удаление и имеет финализаторы. Контроллеры выполняют нужные действия, например удаляют связанные ресурсы. После успешного выполнения всех действий, контроллеры удаляют свои финализаторы из списка и когда данный список пуст, kubernetes окончательно удаляет ресурс из кластера.

Что делать если связанных ресурсов уже нет, но процесс удаления "завис"?

Такое не редко можно встретить, если удаляем например целиком namespace через:
kubectl delete ns <ns_name>


Коретка не возвращается, сам namespace висит в статусе terminating, хотя ресурсов внутри уже нет. В таком случае нужно провести ряд ручных действий по удалению NS. Как правило это нужно только для него, другие объекты, например PVC могут быть удалены просто с ручным удалением finalizers из манифеста. Например через kubectl edit pvc <name>.

Сначала сохраним спецификацию ns в виде json файла.
kubectl get ns <ns_name> -o json > namespace.json


Открываем созданный файл на редактирование, у нас должна быть строка:
"spec": {
"finalizers": [
"kubernetes"
]
}

Нам нужно удалить kubernetes finalizers, оставив пустой массив. То есть приводим к виду:
"spec": {
"finalizers": []
}


Затем применяем изменения назад в кластер командой:
kubectl replace --raw "/api/v1/namespaces/<имя-namespace>/finalize" -f namespace.json


После этого повисший ns в состоянии terminating будет удален.

#kubernetes
Post #102 29
Error: UPGRADE FAILED: another operation (install/upgrade/rollback) is in progress

Такую ошибку можно порой встретить при установке helm релиза в kubernetes. Причина появления - например скипнули в пайплайне джоб с деплоем, когда он еще был в процессе установки или параллельно начали выкатывать еще один релиз. Чтобы починить, достаточно найти сикрет, который автоматически создает helm с каждым релизом, с нужной нам версией, он будет иметь вид sh.helm.release.v1.<release_name>.<revision_version> и иметь не конечный статус, то есть статус в котором есть ключевое слово pending.

Достаточно удалить этот сикрет и запустить установку релиза заново.

#helm
#kubernetes
Older posts →
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →