Сочный DevOps
420 subscribers
8 photos
52 links
От CI/CD до SIEM, SOC и безопасности — здесь делюсь опытом, мыслями и полезняшками на стыке DevOps и безопасности. Всё сочное — в «Сочном DevOps».
Download Telegram
Интеграция 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
Выселение (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
Реальный пример использования 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
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
Немного про 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
Получаем значение защищенной переменной в gitlab

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

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


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

echo <value> | base64 -d


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

#gitlab
Пример сборки 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
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
Экстренное удаление 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
Реализация 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
Запрещаем использование 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
Немного про 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
Реализация "канарейки" на 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
Переопределение пользователя docker образа в gitlab

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

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


#gitlab
Защита от 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
Как по-быстрому развернуть 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
Про affinity и antiAffinity в k8s

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

Есть два основных типа:
1. Node affinity - управляет тем, на каких узлах следует размещать поды, например по меткам узлов.
2. Pod affinity/AntiAffinity - управляет тем, где поды должны или не должны размещаться, относительно других подов.

У каждого из них есть два основных типа использования:
requiredDuringSchedulingIgnoredDuringExecution - обязательное условие, если по нужным параметрам нода не будет найдена, под "зависнет" в Pending.
preferredDuringSchedulingIgnoredDuringExecution - предпочтительное условие, то есть kubernetes(scheduler) постарается разместить под на подходящих узлах, но не гарантирует этого.

Node Affinity:
Позволяет задавать условия, по которым Pod будет размещен на узле, подходящим по node labels.
Пример размещения пода на узле с меткой disktype=ssd:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: disktype
operator: In
values:
- ssd


У nodeAffinity нет свойства anti, как например у PodAffinity, но похожего поведения можно добиться используя операторы NotIn или DoesNotExist, чтобы исключить узлы с определенными метками.
Пример с исключением запуска подов на узле с лейблом environment=production:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: environment
operator: NotIn
values:
- production


Pod Affinity и Pod AntiAffinity:
Просто affinity (без anti) указывает на то, чтобы поды запускались на узлах, на которых уже есть поды с определенным лейблом.
Пример размещение подов на одном узле с другими подами с лейблом app=frontend:
affinity:
podAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- frontend
topologyKey: "kubernetes.io/hostname"


AntiAffinity - противоположность для просто affinity. То есть говорим НЕ размещать поды на узлах, на которых уже есть поды с определенным лейблом.
Пример с лейблом app=backend:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- backend
topologyKey: "kubernetes.io/hostname"


#kubernetes
Про QoS классы в k8s

QoS классы или классы качества обслуживания определяют, как будет происходить управление ресурсами для каждого пода в завиcимости от request и limits по CPU и памяти.

Существует три класса:
1. Guaranteed - поды получают этот класс, если для всех контейнеров, указанные request и limits совпадают. Такой под будет иметь самые высокие шансы оставаться запущенным, в случае проблем с ресурсами. Тут еще важно помнить про такой параметр у kubelet как --cpu-manager-policy=static. Если установлен этот параметр и под имеет данный класс (Guaranteed), а также контейнерам было выделено целое количество ядер, а не в милликор, то данные ядра как-бы резервируются за этим и только этим подом, то есть никакие другие поды не будут конкурировать за ресурсы с данным подом.

2. Burstable - поды получают данный класс, если хотя бы один из контейнеров имеет request и limit, но эти значения могут не совпадать или могут быть не указаны для некоторых контейнеров. Это позволяет использовать больше ресурсов, если они доступны, но в случае проблем с ними, под будет остановлен.

3. BestEffort - поды получают этот класс, если ни один из контейнеров не имеет ни request, ни limit для CPU или памяти. Такие поды будут иметь самый низкий приоритет и буду первыми подвержены выселению с ноды, при нехватке ресурсов.

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

#kubernetes
Выпадающее меню в логе джобы gitlab

Например есть у нас какая-то отладочная информация в джобе, которая достаточно большая, возможно это values, скрипты, что-то еще. В гитлаб можно сделать выпадающее меню в логе, при нажатии на которое, будет развернут лог c тем, что мы туда положили.
echo -e "\e[0Ksection_start:`date +%s`:templates[collapsed=true]\r\e[0K$Debug info"
env
echo -e "\e[0Ksection_end:`date +%s`:templates\r\e[0K"


В логах появится строка Debug info, если нажать на нее, то внутри будет вывод команды env.

#gitlab
Пример сборки nuxt приложения

FROM <you nodejs image> AS node_base
WORKDIR /application
USER node

FROM node_base AS node_modules_prod
COPY --chown=node:node package.json .yarnrc yarn.lock ./
RUN --mount=type=secret,id=npm_token,target=/run/secrets/npm_token,uid=10064,gid=10064 \
--mount=type=cache,sharing=locked,target=/home/node/.cache/yarn,id=yarn_cache,uid=10064,gid=10064 \
--mount=type=tmpfs,target=/tmp/ \
export NPM_TOKEN=`cat /run/secrets/npm_token` && \
yarn install --frozen-lockfile --non-interactive --no-progress --prod

FROM node_modules_prod AS node_modules_dev
RUN --mount=type=secret,id=npm_token,target=/run/secrets/npm_token,uid=10064,gid=10064 \
--mount=type=cache,sharing=locked,target=/home/node/.cache/yarn,id=yarn_cache,uid=10064,gid=10064 \
--mount=type=tmpfs,target=/tmp/ \
export NPM_TOKEN=`cat /run/secrets/npm_token` && \
yarn install --frozen-lockfile --non-interactive --no-progress

FROM node_modules_dev AS build
COPY --chown=node:node ./ ./
RUN --mount=type=secret,id=npm_token,target=/run/secrets/npm_token,uid=10064,gid=10064 \
--mount=type=cache,sharing=locked,target=/home/node/.cache/yarn,id=yarn_cache,uid=10064,gid=10064 \
--mount=type=tmpfs,target=/tmp/ \
export NPM_TOKEN=`cat /run/secrets/npm_token` && \
yarn build

FROM node_modules_prod AS app
COPY --from=build /application/.output/server /application/.output/server
COPY --from=build /application/.output/public /application/.output/public
COPY --from=build /application/.output/nitro.json /application/.output/nitro.json
ENTRYPOINT ["/usr/bin/tini","--"]
CMD ["/usr/bin/node",".output/server/index.mjs"]


Используем multistage сборку, финальный образ делаем только из prod зависимостей. Обычно мы публикуем node_module_dev шаг в регистри, его потом можно использовать для различных QA job или линтинга, но в финальном образе dev зависимости естественно не нужны.

mount=type=secret - позволяет безопасно монтировать секрет, полученный например в job ранее, обычно это CI_JOB_TOKEN, который является временным и живет в рамках job. Он обладает правами того, кто ее запустил. А также с использованием данного подхода будет автоматически очищен, после сборки.

mount=type=cache - это кэширование с помощью buildkit. Тут создается временный монтируемый каталог в контейнере на этапе сборки. Сам кэш лежит на ноде (если это настроено на раннере), про него более подробно я писал тут.

sharing=locked - гарантирует, что кэш обновляется только одной сборкой в момент времени.

#docker
Тулза для скачивания всех репозиториев в gitlab

Небольшая CLI утилита, которая позволяет скачать все доступные репозитории в gitlab. Если репозиторий уже скачан, то он будет просто обновлен.
Бывает полезно, когда на работе у вас на поддержке целая куча реп.

Использование с ssh:
gitlab_grabber -t <token> -u <domain> -k -d /<dir> -i <path_to_ssh_private_key>


Использование с http. Тут предполагается, что у вас oauth, то есть включена двухфакторка.
gitlab_grabber -t <token> -u <domain> -k -d /<dir> --auth http


Поставить можно через pip
pip install gitlab-grabber


Для работы нужен python3.11 или выше.
Работает асинхронно, но с ограничением в 20 одновременных (ну почти, это же python) скачиваний, это сделано намеренно, так как GitPython либа - синхронная, поэтому пришлось использовать в asyncio to_thread метод, чтобы не блокировать основной поток. Если репозиториев 100+, то 100+ тредов не самое лучшее решение. Поэтому ограничился 20-ю.

Ссылка на мой github с кодом.