#cloudfront #aws #troubleshooting #одинденьизжизни
Однажды меня попросили сделать перенос старого SPA (Single Page Application)-приложения на новый S3 bucket в CloudFront.
Но не просто перенос, а хитрый: часть путей должна идти на новое приложение, а часть - оставаться на старом.
Legacy страницы типа /welcome, /login-start, /login-end пока не переписаны в новом коде, их надо сохранить.
А всё остальное - на свежий
По сути частичная миграция на новое приложение, не ломая старое.
Казалось бы, что может пойти не так?😁
Задача звучит просто:
-
-
-
Открываю Terraform, добавляю новый origin для app-v2 bucket.
Меняю
Добавляю
Terraform plan - всё как задумано.
Apply. Жду. Готово.
Иду проверять.
- https://stage-app.example.com/ - работает, новое приложение грузится. ✅
- https://stage-app.example.com/?appId=xxx - работает, редиректит. ✅
- https://stage-app.example.com/welcome - ...
Чо.🤡
Первая мысль - кэш CloudFront. Делаю invalidation. Жду. Проверяю.
Та же херня.
Вторая мысль - может origin неправильный?
Проверяю напрямую S3 bucket:
Bucket работает. index.html есть. 4755 байт. Всё ок.
Третья мысль - может
В CloudFront есть глобальный обработчик 404 ошибок, который возвращает
Думаю: "А, наверное S3 возвращает 404, а CloudFront берёт index.html с дефолтного origin!"
Убираю
Та же херня. Даже хуже - теперь вместо "Server Error" просто "Not found".
Возвращаю
Ладно, поехали курлить по-взрослому.
Стоп. 200? Не 404? CloudFront отдаёт 200 и HTML?
Смотрю body:
Да, это HTML старого приложения.
Так почему "Server Error" в браузере?
Если HTML грузится, значит проблема в JavaScript.
Приложение падает после загрузки.
Открываю DevTools > Network.
И тут я вижу ЭТО:
JS файлы возвращают HTML вместо JavaScript.
Браузер пытается выполнить HTML как JavaScript.
Приложение падает с "Server Error".
Сука.😁
Проверяю напрямую S3:
S3 отдаёт правильно! 1MB JavaScript!
А через CloudFront:
CloudFront отдаёт HTML. И
Теперь понятно что происходит.
Сажусь рисовать на планшете путь запроса (а я всегда рисую).
Проблема:
1. Браузер >
2. CloudFront матчит
3. S3 не находит файл
4. S3 возвращает
5. Браузер получает HTML старого приложения ✅
6. HTML содержит:
7. Браузер резолвит
8. Браузер запрашивает
9. CloudFront матчит
10. В app-v2 bucket НЕТ файла main.a1b2c3d4.chunk.js!
11. S3 возвращает 404
12. CloudFront custom_error_response > /index.html
13. Браузер получает index.html (HTML) вместо JavaScript
14. JavaScript парсер: "че за херня, это не JS"
15. Приложение: "Server Error"🐒
Однажды меня попросили сделать перенос старого SPA (Single Page Application)-приложения на новый S3 bucket в CloudFront.
Но не просто перенос, а хитрый: часть путей должна идти на новое приложение, а часть - оставаться на старом.
Legacy страницы типа /welcome, /login-start, /login-end пока не переписаны в новом коде, их надо сохранить.
А всё остальное - на свежий
app-v2.По сути частичная миграция на новое приложение, не ломая старое.
Казалось бы, что может пойти не так?
Задача звучит просто:
-
/* > новый bucket (app-v2)-
/welcome*, /login-start*, /login-end*, /help-bot* > старый bucket-
/assets/*, /sdk.js > старый bucketОткрываю Terraform, добавляю новый origin для app-v2 bucket.
Меняю
default_cache_behavior на новый origin.Добавляю
ordered_cache_behavior для legacy путей на старый bucket.Terraform plan - всё как задумано.
Apply. Жду. Готово.
Иду проверять.
- https://stage-app.example.com/ - работает, новое приложение грузится. ✅
- https://stage-app.example.com/?appId=xxx - работает, редиректит. ✅
- https://stage-app.example.com/welcome - ...
"Server Error. An unexpected error happened."
Чо.
Первая мысль - кэш CloudFront. Делаю invalidation. Жду. Проверяю.
Та же херня.
Вторая мысль - может origin неправильный?
Проверяю напрямую S3 bucket:
curl -I "http://stage-app.example.com.s3-website-us-east-1.amazonaws.com/index.html"
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 4755
Bucket работает. index.html есть. 4755 байт. Всё ок.
Третья мысль - может
custom_error_response мешает?В CloudFront есть глобальный обработчик 404 ошибок, который возвращает
/index.html.Думаю: "А, наверное S3 возвращает 404, а CloudFront берёт index.html с дефолтного origin!"
Убираю
custom_error_response. Apply. Проверяю.Та же херня. Даже хуже - теперь вместо "Server Error" просто "Not found".
Возвращаю
custom_error_response. Сижу, чешу репу.Ладно, поехали курлить по-взрослому.
curl -sI "https://stage-app.example.com/welcome"
HTTP/2 200
content-type: text/html
x-cache: Hit from cloudfront
Стоп. 200? Не 404? CloudFront отдаёт 200 и HTML?
Смотрю body:
curl -s "https://stage-app.example.com/welcome" | head -20
Да, это HTML старого приложения.
webpackJsonstatham@app/web. Всё верно.Так почему "Server Error" в браузере?
Если HTML грузится, значит проблема в JavaScript.
Приложение падает после загрузки.
Открываю DevTools > Network.
И тут я вижу ЭТО:
/static/js/main.a1b2c3d4.chunk.js → content-type: text/html ❌
/static/css/main.e5f6g7h8.chunk.css → content-type: text/html ❌
JS файлы возвращают HTML вместо JavaScript.
Браузер пытается выполнить HTML как JavaScript.
Приложение падает с "Server Error".
Сука.
Проверяю напрямую S3:
curl -sI "http://stage-app.example.com.s3-website-us-east-1.amazonaws.com/static/js/main.a1b2c3d4.chunk.js"
HTTP/1.1 200 OK
Content-Type: text/javascript
Content-Length: 1080675
S3 отдаёт правильно! 1MB JavaScript!
А через CloudFront:
curl -sI "https://stage-app.example.com/static/js/main.a1b2c3d4.chunk.js"
HTTP/2 200
content-type: text/html
x-cache: Error from cloudfront
CloudFront отдаёт HTML. И
x-cache: Error from cloudfront. Красота.Теперь понятно что происходит.
Сажусь рисовать на планшете путь запроса (а я всегда рисую).
Проблема:
1. Браузер >
/welcome2. CloudFront матчит
/welcome* > origin: OLD bucket3. S3 не находит файл
/welcome (это SPA route, не файл)4. S3 возвращает
error_document = index.html5. Браузер получает HTML старого приложения ✅
6. HTML содержит:
<script src="./static/js/main.a1b2c3d4.chunk.js">7. Браузер резолвит
./static/... относительно /welcome8. Браузер запрашивает
/static/js/main.a1b2c3d4.chunk.js9. CloudFront матчит
/* (default) > origin: app-v2 bucket ❌10. В app-v2 bucket НЕТ файла main.a1b2c3d4.chunk.js!
11. S3 возвращает 404
12. CloudFront custom_error_response > /index.html
13. Браузер получает index.html (HTML) вместо JavaScript
14. JavaScript парсер: "че за херня, это не JS"
15. Приложение: "Server Error"
Please open Telegram to view this post
VIEW IN TELEGRAM
👍8
#cloudfront #aws #troubleshooting #одинденьизжизни
Вот оно.
Два разных SPA с разными static assets в одном CloudFront distribution.
Legacy paths идут на старый bucket, но их
А там этих файлов нет.
Классика жанра: симптомы в одном месте, причина в другом.
Ошибка "Server Error" от JavaScript, а проблема в CloudFront routing.
Логи чистые, метрики зелёные, status code 200 - всё отлично, только ничего не работает.
Для визуалов:
Ну или вот так, что не работало (красиво только на ПК):
Мораль:
Нельзя без дополнительной логики безопасно смешать два SPA с разными static assets в одном CloudFront distribution, если они используют пересекающиеся пути (/static/*) и простой path-based routing.
Относительные пути (./static/...) превращаются в абсолютные (/static/...) и летят на default origin.
И не всегда виноват девопс, что криво пилит редирект или бихейвиор.😀
Решения:
- отдельные CloudFront distributions для старого и нового приложения
- Cloudflare Workers / Lambda@Edge для умного роутинга
- добавить /static/* behavior на старый bucket (но тогда сломается новое приложение)
- не переключать default, тестировать новое приложение на отдельном домене
Я выбрал вариант 4 (вроде, уже не помню) и пошёл пить кофе.
А RFC переписали, убрав требование "переключить трафик через один CloudFront".
Иногда лучшее решение - не решать задачу в лоб.
Вот оно.
Два разных SPA с разными static assets в одном CloudFront distribution.
Legacy paths идут на старый bucket, но их
./static/* резолвится как /static/* и летит на default origin - новый bucket.А там этих файлов нет.
Классика жанра: симптомы в одном месте, причина в другом.
Ошибка "Server Error" от JavaScript, а проблема в CloudFront routing.
Логи чистые, метрики зелёные, status code 200 - всё отлично, только ничего не работает.
Для визуалов:
ДО (работало):
══════════════════════════════════════════
CloudFront
│
▼
┌─────────┐
│ OLD S3 │ ← всё идёт сюда
│ bucket │
└─────────┘
ПОСЛЕ (сломалось):
══════════════════════════════════════════
CloudFront
│
┌───────────┴───────────┐
▼ ▼
┌─────────┐ ┌─────────┐
│ OLD S3 │ │ NEW S3 │ ← default
│ bucket │ │ bucket │
└─────────┘ └─────────┘
/welcome* /* (всё остальное)
/login-* включая /static/* ❌
Ну или вот так, что не работало (красиво только на ПК):
Browser CloudFront S3
| | |
| GET /welcome | |
|------------- req ---------->| |
| | /welcome* --> OLD bucket |
| |------------- req ---------->| OLD bucket
| | | (app-v1)
| |<------------ res -----------| index.html
|<----------- res ------------| HTML + <script> |
| | |
| GET /static/js/main.js | |
|------------- req ---------->| |
| | /* (default) --> NEW |
| |------------- req ---------->| NEW bucket
| | | (app-v2)
| | |
| | main.js NOT FOUND! |
| |<------------ 404 -----------|
| | |
| | custom_error_response |
| | 404 --> /index.html |
| |------------- req ---------->| NEW bucket
| |<------------ res -----------| index.html
|<----------- res ------------| HTML (not JS!) WRONG! | (app-v2!)
| | |
| "Server Error" | |
| JS parser: WTF?! | |
Мораль:
Нельзя без дополнительной логики безопасно смешать два SPA с разными static assets в одном CloudFront distribution, если они используют пересекающиеся пути (/static/*) и простой path-based routing.
Относительные пути (./static/...) превращаются в абсолютные (/static/...) и летят на default origin.
И не всегда виноват девопс, что криво пилит редирект или бихейвиор.
Решения:
- отдельные CloudFront distributions для старого и нового приложения
- Cloudflare Workers / Lambda@Edge для умного роутинга
- добавить /static/* behavior на старый bucket (но тогда сломается новое приложение)
- не переключать default, тестировать новое приложение на отдельном домене
Я выбрал вариант 4 (вроде, уже не помню) и пошёл пить кофе.
А RFC переписали, убрав требование "переключить трафик через один CloudFront".
Иногда лучшее решение - не решать задачу в лоб.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤9👍4
Make. Build. Break. Reflect.
#пятница #байки #troubleshooting Задолго до работы в айти я был простым советским инженером спутниковой связи. Тарелки, антенны, модемы, кабели, бесконечные командировки. В то время случалось масса странных историй. Мы разрабатывали спутниковое оборудование…
#байки #пятница #tcp #networking
Когда-то давно я работал в компании, которая делала железки для спутниковой связи.
Не те, что вы вешаете на дачу для ТВ, а серьёзные штуки - морские терминалы VSAT, Inmarsat-модемы для судов, станции для нефтянки в тайге.
Короче, интернет там, где провода не дотягиваются.
И вот первое, что узнаёшь в этой индустрии:
всё, что ты знал о сетях - забудь. 🙃
Геостационарный спутник висит на высоте ~36 000 км.
Сигнал от твоего модема до спутника и обратно - это примерно 72 000 км.
Дальше сигнал летит на наземную станцию (HUB), там обрабатывается, и только потом идёт в интернет.
А ответ - всё в обратном порядке.
Суммарное расстояние: ~144 000 км (минимум, на экваторе).
Скорость света в вакууме: 299 792 км/с.
Добавь обработку на спутнике, на HUB'е, в модеме - получаешь 600-800 мс RTT.
И это не баг. Это физика. Скорость света не обманешь.
Классический звонок от клиента:
- Алло, вы нам поставили модем на 10 мегабит, а у нас файлы качаются со скоростью 500 килобит!
- А какой размер файла?
- 100 мегабайт!
- А пинг какой?
- 700 миллисекунд...
- (вздох) Сейчас объясню.
И начинается лекция по TCP, которую никто не хотел слышать.
Но вам я расскажу.
Bandwidth-Delay Product: труба, которую надо заполнить
TCP работает так: отправил данные > ждёшь ACK > отправляешь ещё.
Сколько данных можно "запихнуть" в сеть до получения первого ACK, ограничено TCP Window.
А сколько данных должно лететь одновременно, чтобы канал был загружен?
Это называется BDP (Bandwidth-Delay Product):
Считаем для нашего случая (все каналы 10 Мбит/с):
Видишь разницу?
Чтобы полностью загрузить 10 Мбит спутниковый канал, надо держать почти мегабайт данных "в полёте" одновременно.
А дефолтное TCP окно в старой винде - 64 КБ. 😭
Математика боли
Максимальный throughput TCP (без потерь) приблизительно равен:
Подставляем:
Семь процентов утилизации.
Клиент платит за 10 мегабит, а получает 730 килобит.
И это не наш модем плохой. Это TCP.
Хотя клиент думает обратно.😀
А давайте потюним TCP!
Конечно, никто ж до этого не догадался же, ага.
Ладно, мы ж не дураки. Знаем про sysctl, знаем про window scaling.
На Linux:
Когда-то давно я работал в компании, которая делала железки для спутниковой связи.
Не те, что вы вешаете на дачу для ТВ, а серьёзные штуки - морские терминалы VSAT, Inmarsat-модемы для судов, станции для нефтянки в тайге.
Короче, интернет там, где провода не дотягиваются.
И вот первое, что узнаёшь в этой индустрии:
всё, что ты знал о сетях - забудь. 🙃
⚠️ Важно: всё, что ниже - про геостационарные спутники (GEO).
Это классический VSAT, Inmarsat, морская и промышленная связь.
Starlink, OneWeb и прочие LEO-созвездия - совсем другая история.
Там спутники летают на 550 км вместо 36 000, RTT составляет 20-40 мс, и проблемы с TCP практически отсутствуют.
Но это технология 2020-х, а я работал в эпоху, когда GEO был единственным вариантом.
Геостационарный спутник висит на высоте ~36 000 км.
Сигнал от твоего модема до спутника и обратно - это примерно 72 000 км.
Дальше сигнал летит на наземную станцию (HUB), там обрабатывается, и только потом идёт в интернет.
А ответ - всё в обратном порядке.
🛰 СПУТНИК
(36 000 км)
/ \
/ \
① ↗️ / \ ↘️ ②
/ ~36 000 км \
/ в каждую \
/ сторону \
/ \
┌──────────────┐ ┌──────────────┐
│ ТВОЙ МОДЕМ │ │ HUB │
│ (корабль, │ │ (наземная │
│ платформа) │ │ станция) │
└──────────────┘ └──────┬───────┘
│
│ ③ оптика
▼
┌──────────────┐
│ ИНТЕРНЕТ │
└──────────────┘
Суммарное расстояние: ~144 000 км (минимум, на экваторе).
Скорость света в вакууме: 299 792 км/с.
RTT = 144000 / 299792 ≈ 480 мс (только свет!)
Добавь обработку на спутнике, на HUB'е, в модеме - получаешь 600-800 мс RTT.
И это не баг. Это физика. Скорость света не обманешь.
Классический звонок от клиента:
- Алло, вы нам поставили модем на 10 мегабит, а у нас файлы качаются со скоростью 500 килобит!
- А какой размер файла?
- 100 мегабайт!
- А пинг какой?
- 700 миллисекунд...
- (вздох) Сейчас объясню.
И начинается лекция по TCP, которую никто не хотел слышать.
Но вам я расскажу.
Bandwidth-Delay Product: труба, которую надо заполнить
TCP работает так: отправил данные > ждёшь ACK > отправляешь ещё.
Сколько данных можно "запихнуть" в сеть до получения первого ACK, ограничено TCP Window.
А сколько данных должно лететь одновременно, чтобы канал был загружен?
Это называется BDP (Bandwidth-Delay Product):
BDP = Bandwidth × RTT
Считаем для нашего случая (все каналы 10 Мбит/с):
LAN (RTT 1 мс):
BDP = 10 000 бит ≈ 1.25 КБ
WAN (RTT 50 мс):
BDP = 500 000 бит ≈ 62.5 КБ
Спутник (RTT 700 мс):
BDP = 7 000 000 бит ≈ 875 КБ
Видишь разницу?
Чтобы полностью загрузить 10 Мбит спутниковый канал, надо держать почти мегабайт данных "в полёте" одновременно.
А дефолтное TCP окно в старой винде - 64 КБ. 😭
Математика боли
Максимальный throughput TCP (без потерь) приблизительно равен:
Throughput ≈ Window Size / RTT
Подставляем:
Window = 64 КБ = 524 288 бит
RTT = 0.7 с
Throughput = 524288 / 0.7 ≈ 749 000 бит/с ≈ 730 Кбит/с
730 Кбит/с на 10 Мбит канале.
Семь процентов утилизации.
Клиент платит за 10 мегабит, а получает 730 килобит.
И это не наш модем плохой. Это TCP.
Хотя клиент думает обратно.
А давайте потюним TCP!
Конечно, никто ж до этого не догадался же, ага.
Ладно, мы ж не дураки. Знаем про sysctl, знаем про window scaling.
На Linux:
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.wmem_max=16777216
sysctl -w net.ipv4.tcp_rmem="4096 87380 16777216"
sysctl -w net.ipv4.tcp_wmem="4096 65536 16777216"
sysctl -w net.ipv4.tcp_window_scaling=1
Please open Telegram to view this post
VIEW IN TELEGRAM
👍13🔥4
Make. Build. Break. Reflect.
#пятница #байки #troubleshooting Задолго до работы в айти я был простым советским инженером спутниковой связи. Тарелки, антенны, модемы, кабели, бесконечные командировки. В то время случалось масса странных историй. Мы разрабатывали спутниковое оборудование…
#байки #пятница #tcp #networking
На Windows (реестр, куда ж без него):
Помогает? Да, частично.
Но есть нюансы:
- Обе стороны должны поддерживать большие окна.
А сервер где-нибудь в Китае - фиг знает как настроен.
- Потери убивают всё.
При 1% потерь и 700 мс RTT скорость падает катастрофически.
- Slow Start
TCP начинает медленно и наращивает окно раз в RTT. При 700 мс это оооочень долго.
Короче, тюнинг помогает, но не спасает. Нужен костыль уровня "бог".
TCP Acceleration: красивый обман
И тут на сцену выходит
В народе -
Идея гениальна в своей наглости.
Как это работает:
1. На стороне клиента стоит модем с PEP (наша железка).
2. На стороне HUB'а - ответная часть PEP.
3. Клиент отправляет TCP-пакет.
4. Локальный PEP сразу отвечает ACK, не дожидаясь ответа с сервера.
5. Клиент думает "ура, данные доставлены!" и шлёт ещё.
6. Тем временем PEP буферизирует данные и гонит их по спутнику *своим протоколом* (оптимизированным под high latency).
7. Удалённый PEP получает данные и уже по-честному передаёт серверу.
По сути, мы разрываем TCP-сессию на два локальных сегмента с низким RTT, а между ними гоним трафик специальным протоколом.
Клиент видит: "пинг 700 мс, но скорость 10 мегабит!" 🎉
Магия? Нет, честный обман.😁
Конечно, есть ограничения:
- Память не бесконечна.
Каждая "ускоренная" сессия жрёт буфер. Модем на 100 сессий - это одно, на 10 000 - совсем другие деньги.
- При потере связи - боль.
Если спутник моргнул, а PEP уже насобирал мегабайт данных "в кредит" - это всё надо переслать. А клиент уже уверен, что данные доставлены.
- Шифрование.
HTTPS, VPN, IPsec - PEP не может влезть в сессию, не может подменить ACK. Приходится либо терминировать SSL на PEP (что не всегда возможно), либо PEP работает только на транспортном уровне и не так эффективен.
- Оборудование на обеих сторонах.
Если у тебя PEP только на модеме, а на HUB'е нет - толку мало. Поэтому это работает в контролируемых сетях VSAT.
Вместо морали
Работая со спутниковой связью, начинаешь по-другому смотреть на сети.
Когда твой пинг 700 мс, ты понимаешь, что TCP придумали для LAN.
Твой клиент на корабле посреди Тихого океана жалуется на скорость, ты не можешь сказать "перезагрузите роутер".
Тебе надо найти решение.
Если вы думали, что всё знаете о сетях и TCP просто представьте:
Вы на нефтяной платформе в Северном море.
С ноутбуком 2005 года.
И Windows XP.
И TCP-окном в 64 КБ.
И RTT 800 мс.
А на улице идёт дождь, который дарит помехи.
И надо срочно скачать огромный файл.
Вы знаете о TCP и сетях не всё😀
На Windows (реестр, куда ж без него):
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters
TcpWindowSize = 65535 (или больше с window scaling)
Tcp1323Opts = 3 (timestamps + window scaling)
Помогает? Да, частично.
Но есть нюансы:
- Обе стороны должны поддерживать большие окна.
А сервер где-нибудь в Китае - фиг знает как настроен.
- Потери убивают всё.
При 1% потерь и 700 мс RTT скорость падает катастрофически.
- Slow Start
TCP начинает медленно и наращивает окно раз в RTT. При 700 мс это оооочень долго.
Короче, тюнинг помогает, но не спасает. Нужен костыль уровня "бог".
TCP Acceleration: красивый обман
И тут на сцену выходит
PEP - Performance Enhancing Proxy.В народе -
TCP Accelerator. Маркетологи любят это слово.Идея гениальна в своей наглости.
БЕЗ PEP (честный TCP):
Client Satellite (700ms) Server
│ │
│───── DATA ───────────────────────────────>│
│ 700 ms │
│<─────────────────────────────────── ACK ──│
│ 700 ms │
│───── DATA ───────────────────────────────>│
│ Total: очень медленно │
С PEP (хитрый обман):
Client Local PEP Satellite Remote PEP Server
│ │ 700ms │ │
│── DATA ─>│ │ │
│<── ACK ──│ (мгновенно!) │ │
│── DATA ─>│ │ │
│<── ACK ──│ │ │
│── DATA ─>│ │ │
│<── ACK ──│─────── DATA ───────────>│ │
│ │ (много данных сразу!) │── DATA ──>│
│ │ │<── ACK ───│
│ │ │ │
│ Client думает, На самом деле данные │
│ что всё уже ещё летят по спутнику, │
│ доставлено! но клиент уже шлёт ещё! │
Как это работает:
1. На стороне клиента стоит модем с PEP (наша железка).
2. На стороне HUB'а - ответная часть PEP.
3. Клиент отправляет TCP-пакет.
4. Локальный PEP сразу отвечает ACK, не дожидаясь ответа с сервера.
5. Клиент думает "ура, данные доставлены!" и шлёт ещё.
6. Тем временем PEP буферизирует данные и гонит их по спутнику *своим протоколом* (оптимизированным под high latency).
7. Удалённый PEP получает данные и уже по-честному передаёт серверу.
По сути, мы разрываем TCP-сессию на два локальных сегмента с низким RTT, а между ними гоним трафик специальным протоколом.
Клиент видит: "пинг 700 мс, но скорость 10 мегабит!" 🎉
Магия? Нет, честный обман.
Конечно, есть ограничения:
- Память не бесконечна.
Каждая "ускоренная" сессия жрёт буфер. Модем на 100 сессий - это одно, на 10 000 - совсем другие деньги.
- При потере связи - боль.
Если спутник моргнул, а PEP уже насобирал мегабайт данных "в кредит" - это всё надо переслать. А клиент уже уверен, что данные доставлены.
- Шифрование.
HTTPS, VPN, IPsec - PEP не может влезть в сессию, не может подменить ACK. Приходится либо терминировать SSL на PEP (что не всегда возможно), либо PEP работает только на транспортном уровне и не так эффективен.
- Оборудование на обеих сторонах.
Если у тебя PEP только на модеме, а на HUB'е нет - толку мало. Поэтому это работает в контролируемых сетях VSAT.
Вместо морали
Работая со спутниковой связью, начинаешь по-другому смотреть на сети.
Когда твой пинг 700 мс, ты понимаешь, что TCP придумали для LAN.
Твой клиент на корабле посреди Тихого океана жалуется на скорость, ты не можешь сказать "перезагрузите роутер".
Тебе надо найти решение.
Если вы думали, что всё знаете о сетях и TCP просто представьте:
Вы на нефтяной платформе в Северном море.
С ноутбуком 2005 года.
И Windows XP.
И TCP-окном в 64 КБ.
И RTT 800 мс.
А на улице идёт дождь, который дарит помехи.
И надо срочно скачать огромный файл.
Вы знаете о TCP и сетях не всё
Please open Telegram to view this post
VIEW IN TELEGRAM
👍40🔥5❤4
#aws
Есть у меня личный паблик EKS (да, я плачу ~93-95 долларов ежемесячно).
Решил я обновить версию 1.33 на версию 1.34:
- control plane отлично обновился
- одна нод группа на
- вторая нод группа из одной ноды на😁
При том, что официально 1.35 в AWS ещё нет.
Повторные ручные попытки дернуть апгрейд приводят лишь к этой ошибке.
Хорошо,что это не прод на работе.
Спасибо хоть на этом.
- - -
Ждём 1.35 на днях?
Есть у меня личный паблик EKS (да, я плачу ~93-95 долларов ежемесячно).
Решил я обновить версию 1.33 на версию 1.34:
- control plane отлично обновился
- одна нод группа на
t3.micro обновилась так же на отлично- вторая нод группа из одной ноды на
t3.small почему-то пыталась обновится до 1.35.При том, что официально 1.35 в AWS ещё нет.
Повторные ручные попытки дернуть апгрейд приводят лишь к этой ошибке.
Хорошо,что это не прод на работе.
Спасибо хоть на этом.
- - -
Ждём 1.35 на днях?
Please open Telegram to view this post
VIEW IN TELEGRAM
😁12❤1
#пятница #всратость #javascript
Заходят как-то в бар арифметика с плавающей точкой и неявное приведение типов...
Баланс отпуска (PTO) на JS:
- когда "1.41" - "1" почему-то превращается в "15.92".
Обожаю этот язык, не меняйте ничего!1!!
Заходят как-то в бар арифметика с плавающей точкой и неявное приведение типов...
- когда "1.41" - "1" почему-то превращается в "15.92".
Обожаю этот язык, не меняйте ничего!1!!
🤣11🙈4😁2👍1
#victorialogs #victoriametrics #grafana #troubleshooting
Когда простая задача превращается в тупик:
Самобичевание, часть 1 из 2.
Вот уже много лет я работаю.
За плечами - десятки пройденных боёв с Kubernetes, облаками, мониторингом и коллегами.
Поел не одну ложку коричневой субстанции.
Казалось, что в области observability я видывал всё.
Но одна простая задача недавно поставила меня в тупик настолько основательно, что я до сих пор негодую на себя.
Как всё начиналось
У нас на работе есть собственная система observability - довольно сложная, с десятками компонентов, которые менялись со временем.
Сначала были одни инструменты, потом другие, затем поменялась концепция хранения, добавился мульти-тенантный доступ и так далее.
И я попал на задачу в самый неподходящий момент: когда нужно было переделать дашборды и для клиентов (с их интерфейсом и логами), и для внутренней техподдержки SRE.
Задача звучала тривиально:
- поменять дата-сорс в Grafana с Loki на VictoriaLogs
- параллельно часть метрик из VictoriaMetrics тоже переехала в VictoriaLogs - веб-серверный компонент стало удобнее хранить именно там
- переделать существующие панели, сохранив при этом изоляцию данных: чтобы клиенты не видели чужие логи, а права доступа работали корректно
Казалось, пара дней - и готово.
Когда тупик оказался глубже, чем я думал
Я провозился с этой задачей больше недели.
Почему?
Потому что оказалось, что я не знал многого.
Да нихера я не знал, начиная с базовой архитектуры:
- как работает индексация полей в VictoriaLogs (спойлер: индексируются все поля, не только лейблы как в Loki, плюс есть отдельные _stream_fields для ускорения типичных фильтров)
- как агенты отправляют данные и что такое _stream_fields
- что такое тенанты (AccountID, ProjectID) и как они задаются через HTTP-заголовки
- какие эндпоинты существуют (/select/logsql/query для запросов, /insert/loki/api/v1/push для записи)
Самое сложное - это понимание метрик в логах.
Когда берёшь запрос, не ясно: эта метрика уже распарсена из JSON на этапе ingestion, или её нужно парсить прямо в запросе?
Оказалось, что VictoriaLogs по умолчанию автоматически парсит JSON при записи данных (начиная с версии v1.16.0).
При Loki-совместимом ingestion VictoriaLogs пытается распарсить JSON из поля сообщения (message / _msg) и разложить его ключи в отдельные поля, которые затем индексируются.
Это можно отключить флагом -loki.disableMessageParsing или параметром disable_message_parsing=1 в URL.
А для парсинга в запросах есть свои операторы LogsQL:
- unpack_json - распаковка JSON
- unpack_logfmt - распаковка logfmt
- extract и extract_regexp - извлечение по паттерну
- fields pipe - работа с полями
Но вот сюрприз номер два: оказалось, что в Grafana можно вообще не парсить через LogsQL.
Есть Grafana Transformations - extractFields с format: "json" - и он достаёт поля из JSON прямо в браузере. 🤡
Это работает, но жрёт ресурсы на клиенте и ломает голову: где парсить - в запросе или в трансформации?
Примеры запросов, с которыми пришлось работать:
Тут видно:
- фильтрация по stream полям (stream:"stderr")
- поиск по тексту ("[crit]", "[emerg]")
- stats pipe с count() as total
- переменные Grafana (${tenant_id}, ${region})
И каждый раз для новой панели нужно было решать:
парсить в LogsQL через unpack_json или в Grafana через transformations?
Использовать stats pipe или range запрос?
Какой подход быстрее, а какой уложит VictoriaLogs ошибками 502 и 504?
Спойлер: я раз 30 уложил на лопатки виктория сторадж.
Когда простая задача превращается в тупик:
миграция дашбордов с Loki на VictoriaLogs
Самобичевание, часть 1 из 2.
Вот уже много лет я работаю.
За плечами - десятки пройденных боёв с Kubernetes, облаками, мониторингом и коллегами.
Поел не одну ложку коричневой субстанции.
Казалось, что в области observability я видывал всё.
Но одна простая задача недавно поставила меня в тупик настолько основательно, что я до сих пор негодую на себя.
Как всё начиналось
У нас на работе есть собственная система observability - довольно сложная, с десятками компонентов, которые менялись со временем.
Сначала были одни инструменты, потом другие, затем поменялась концепция хранения, добавился мульти-тенантный доступ и так далее.
И я попал на задачу в самый неподходящий момент: когда нужно было переделать дашборды и для клиентов (с их интерфейсом и логами), и для внутренней техподдержки SRE.
Задача звучала тривиально:
- поменять дата-сорс в Grafana с Loki на VictoriaLogs
- параллельно часть метрик из VictoriaMetrics тоже переехала в VictoriaLogs - веб-серверный компонент стало удобнее хранить именно там
- переделать существующие панели, сохранив при этом изоляцию данных: чтобы клиенты не видели чужие логи, а права доступа работали корректно
Казалось, пара дней - и готово.
Когда тупик оказался глубже, чем я думал
Я провозился с этой задачей больше недели.
Почему?
Потому что оказалось, что я не знал многого.
Да нихера я не знал, начиная с базовой архитектуры:
- как работает индексация полей в VictoriaLogs (спойлер: индексируются все поля, не только лейблы как в Loki, плюс есть отдельные _stream_fields для ускорения типичных фильтров)
- как агенты отправляют данные и что такое _stream_fields
- что такое тенанты (AccountID, ProjectID) и как они задаются через HTTP-заголовки
- какие эндпоинты существуют (/select/logsql/query для запросов, /insert/loki/api/v1/push для записи)
Самое сложное - это понимание метрик в логах.
Когда берёшь запрос, не ясно: эта метрика уже распарсена из JSON на этапе ingestion, или её нужно парсить прямо в запросе?
Оказалось, что VictoriaLogs по умолчанию автоматически парсит JSON при записи данных (начиная с версии v1.16.0).
При Loki-совместимом ingestion VictoriaLogs пытается распарсить JSON из поля сообщения (message / _msg) и разложить его ключи в отдельные поля, которые затем индексируются.
Это можно отключить флагом -loki.disableMessageParsing или параметром disable_message_parsing=1 в URL.
А для парсинга в запросах есть свои операторы LogsQL:
- unpack_json - распаковка JSON
- unpack_logfmt - распаковка logfmt
- extract и extract_regexp - извлечение по паттерну
- fields pipe - работа с полями
Но вот сюрприз номер два: оказалось, что в Grafana можно вообще не парсить через LogsQL.
Есть Grafana Transformations - extractFields с format: "json" - и он достаёт поля из JSON прямо в браузере. 🤡
Это работает, но жрёт ресурсы на клиенте и ломает голову: где парсить - в запросе или в трансформации?
Примеры запросов, с которыми пришлось работать:
{tenant_id="${tenant_id}"} stream:"stderr" "[crit]" | stats count() as total
{tenant_id="${tenant_id}", region=~"${region}"} eventType:kubernetes-eventТут видно:
- фильтрация по stream полям (stream:"stderr")
- поиск по тексту ("[crit]", "[emerg]")
- stats pipe с count() as total
- переменные Grafana (${tenant_id}, ${region})
И каждый раз для новой панели нужно было решать:
парсить в LogsQL через unpack_json или в Grafana через transformations?
Использовать stats pipe или range запрос?
Какой подход быстрее, а какой уложит VictoriaLogs ошибками 502 и 504?
Спойлер: я раз 30 уложил на лопатки виктория сторадж.
👍6
#victorialogs #victoriametrics #grafana #troubleshooting
Самобичевание, часть 2 из 2.
Особенности Grafana, о которых я забыл
Потом всплыли нюансы самой Grafana.
В плагине VictoriaLogs для Grafana (victoriametrics-logs-datasource) есть несколько queryType:
- Raw Logs - для логовой панели (лента логов, tailing)
- Range - для time series и графиков по времени, под капотом ходит в /select/logsql/stats_query_range
- Stats - для агрегатов без временной развёртки (одно число/набор чисел по фильтру)
И вот тут началась жесть с выбором правильного queryType:
- queryType: "range" - для time series графиков
- queryType: "stats" - для агрегаций через | stats count() as total
- queryType: "logs" - для сырых логов
Где-то нужен range-запрос, где-то stats.
Где-то метрика уже доступна как число в отдельном поле, а где-то её нужно извлекать из JSON.
И если ошибёшься с queryType - панель просто пустая, без ошибок.
Сидишь и гадаешь: запрос кривой или тип панели неправильный?😭
Разница между _stream_fields (лейблы потока, быстрая фильтрация) и обычными полями лога оказалась критичной:
- фильтровать по stream field - быстро и дёшево
- по log field - медленнее, особенно если JSON-парсинг выполняется в запросе
Выводы и самоанализ
Когда я осмотрел свои коммиты, увидел десятки итераций:
- сначала делал парсинг в запросах через unpack_json, потом переключился на Grafana transformations, потом понял - лучше хранить поля структурированно при ingestion
- менял queryType с range на stats и обратно, потому что панели то пустые, то не те данные
- возился с мульти-тенантностью: настраивал лейблы типа tenant_id в запросах и флаги изоляции
- оптимизировал запросы со stats count() as total чтобы не гонять по всей базе полнотекстовый поиск
- добавлял фильтры по stream:"stderr" вместо поиска по всем логам
Полная фигня, если честно. 🤡
Это сильно меня опечалило:
- либо я отстал от технологий и мои знания Prometheus/Grafana устарели и надо срочно учить весь VM стек
- либо я переоценил себя при оценке задачи
- либо это действительно сложная задача, которую я недооценил
Скорее всего, правда посередине.
VictoriaLogs - не просто замена Loki, это другой инструмент с другой философией:
- Loki индексирует только лейблы потока, текст сообщений не индексируется - regex по message сканирует содержимое
- VictoriaLogs индексирует все поля логов, так что по ним можно быстро фильтровать и искать. При этом стрим-поля через _stream_fields дополнительно помогают с компрессией и ускоряют типичные фильтры
- LogsQL ≠ LogQL - это разные языки запросов, хоть и похожие
- Высокая кардинальность (trace_id, user_id, ip) в VictoriaLogs гораздо менее болезненна, чем в Loki, если хранить такие вещи как обычные поля, а не stream fields. Loki сильно страдает, если тащить user_id/trace_id в лейблы
А миграция дашбордов в мульти-тенантной среде - это не рутина, а полноценный проект.
Во всяком случае так вышло для меня в этот раз.
И это я ещё пропускаю часть про VMrules и recording rules, которые пришлось добавить для некоторых панелей....😭
Задача - теперь для меня не сложная. Ну я максимум часа 2-3 на подобное потрачу.
Проблема в том, что я переоценил свои силы и знания (которых и не было).
В общем надо учиться и читать документацию, прежде, чем приступать к задаче.
Самобичевание, часть 2 из 2.
Особенности Grafana, о которых я забыл
Потом всплыли нюансы самой Grafana.
В плагине VictoriaLogs для Grafana (victoriametrics-logs-datasource) есть несколько queryType:
- Raw Logs - для логовой панели (лента логов, tailing)
- Range - для time series и графиков по времени, под капотом ходит в /select/logsql/stats_query_range
- Stats - для агрегатов без временной развёртки (одно число/набор чисел по фильтру)
И вот тут началась жесть с выбором правильного queryType:
- queryType: "range" - для time series графиков
- queryType: "stats" - для агрегаций через | stats count() as total
- queryType: "logs" - для сырых логов
Где-то нужен range-запрос, где-то stats.
Где-то метрика уже доступна как число в отдельном поле, а где-то её нужно извлекать из JSON.
И если ошибёшься с queryType - панель просто пустая, без ошибок.
Сидишь и гадаешь: запрос кривой или тип панели неправильный?
Разница между _stream_fields (лейблы потока, быстрая фильтрация) и обычными полями лога оказалась критичной:
- фильтровать по stream field - быстро и дёшево
- по log field - медленнее, особенно если JSON-парсинг выполняется в запросе
Выводы и самоанализ
Когда я осмотрел свои коммиты, увидел десятки итераций:
- сначала делал парсинг в запросах через unpack_json, потом переключился на Grafana transformations, потом понял - лучше хранить поля структурированно при ingestion
- менял queryType с range на stats и обратно, потому что панели то пустые, то не те данные
- возился с мульти-тенантностью: настраивал лейблы типа tenant_id в запросах и флаги изоляции
- оптимизировал запросы со stats count() as total чтобы не гонять по всей базе полнотекстовый поиск
- добавлял фильтры по stream:"stderr" вместо поиска по всем логам
Полная фигня, если честно. 🤡
Это сильно меня опечалило:
- либо я отстал от технологий и мои знания Prometheus/Grafana устарели и надо срочно учить весь VM стек
- либо я переоценил себя при оценке задачи
- либо это действительно сложная задача, которую я недооценил
Скорее всего, правда посередине.
VictoriaLogs - не просто замена Loki, это другой инструмент с другой философией:
- Loki индексирует только лейблы потока, текст сообщений не индексируется - regex по message сканирует содержимое
- VictoriaLogs индексирует все поля логов, так что по ним можно быстро фильтровать и искать. При этом стрим-поля через _stream_fields дополнительно помогают с компрессией и ускоряют типичные фильтры
- LogsQL ≠ LogQL - это разные языки запросов, хоть и похожие
- Высокая кардинальность (trace_id, user_id, ip) в VictoriaLogs гораздо менее болезненна, чем в Loki, если хранить такие вещи как обычные поля, а не stream fields. Loki сильно страдает, если тащить user_id/trace_id в лейблы
А миграция дашбордов в мульти-тенантной среде - это не рутина, а полноценный проект.
Во всяком случае так вышло для меня в этот раз.
И это я ещё пропускаю часть про VMrules и recording rules, которые пришлось добавить для некоторых панелей....
Задача - теперь для меня не сложная. Ну я максимум часа 2-3 на подобное потрачу.
Проблема в том, что я переоценил свои силы и знания (которых и не было).
В общем надо учиться и читать документацию, прежде, чем приступать к задаче.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍14🔥1
#kubernetes #мысли
Сижу, разбираюсь с
- https://docs.crossplane.io/latest/
- https://marketplace.upbound.io/providers (да, мне понравились upbound)
Надо было кое-что найти, а обычный лист выдаёт сотни ответов без грепа.
Глянул сколько у нас всего кастом кайндов, а там..
Ах этот безумный-безумный мир.
513
498
В наших кластерах CRD станет скоро больше, чем базовых сущностей кубернетиса.
Безумие, абсолютное безумие.
В целом можно на собеседовании/в баре с коллегами по девопс-цеху меряться:
- У нас в кластере 498 CRD, а сколько у вас?😁
Скоро это будет показывать насколько зрелый проект, мощная команда и глубокое погружение в экосистему кубера(надеюсь нет).
Очередной раз не завидую всем молодым специалистам, кому надо будет учить этот кубер.
Сижу, разбираюсь с
Crossplane и провайдерами для Azure.- https://docs.crossplane.io/latest/
- https://marketplace.upbound.io/providers (да, мне понравились upbound)
Надо было кое-что найти, а обычный лист выдаёт сотни ответов без грепа.
Глянул сколько у нас всего кастом кайндов, а там..
Ах этот безумный-безумный мир.
kubectl api-resources --verbs=list | grep -v customresourcedefinition | wc -l
513
kubectl get crd -o name | wc -l
498
В наших кластерах CRD станет скоро больше, чем базовых сущностей кубернетиса.
Безумие, абсолютное безумие.
В целом можно на собеседовании/в баре с коллегами по девопс-цеху меряться:
- У нас в кластере 498 CRD, а сколько у вас?
Скоро это будет показывать насколько зрелый проект, мощная команда и глубокое погружение в экосистему кубера
Очередной раз не завидую всем молодым специалистам, кому надо будет учить этот кубер.
Please open Telegram to view this post
VIEW IN TELEGRAM
💯12😱6😁1
1.png
431.8 KB
#devops #git
Никакого рокетсайнса или, прости господи, лайфхаков, просто делюсь наблюдением.
Последние месяцы для работы со схемами/диаграммами вместо привычных ресурсов (draw.io excalidraw.com diagrams.mingrammer.com) перешёл на связку:
- любой AI ассистент, любая модель
- markdown файл https://www.markdownguide.org/
- mermaid фреймворк https://mermaid.js.org/
Поддержка mermaid в MD файлах уже достаточно давно, а основные инструменты - VSCode, GitHub, GitLab, Cursor умеют это отображать для человека и в IDE, и в веб-браузере.
В некоторых случаях надо ставить плагин, но в некоторых уже работает и так.
Как это работает:
- я прошу ассистента нарисовать некую схему. Схема может быть обычная flowchart, может быть state, sequence или что-то другое
- в основном README.md или CLAUDE.md появляется блок кода, который отлично лежит в md файле и при этом
- - схема видна в IDE через preview режим
- - схема видна в браузере на страницах GitHub/GitLab
- - схема понятна любой нейронке
- самое главное, это прозрачно и для меня и нейронка понимает что хочет, чтобы изменение кода было корректным
- ну и всё это в git репозитории
Рандомный пример промпта:
Рандомный пример кода (в файлах формата
Результат на скриншоте (GitHub + Cursor IDE).
Сами промпт/схема просто рандомно-мусорная, лишь для примера.
Можно рисовать что угодно:
- схема инфры
- как работает хэндшейки бэкенда
- логика распределения подов по нод группам в кубере
- как работает самописный оператор
- sequence diagram, как микросервисы общаются при отказе базы
и так далее.
Связка мне нравится, я думаю многие это используют.
Если ещё не пробовали - начните.
Мы итак многие инженерные вещи переусложнили.
Хочется хотя бы с диаграммами и схемами сделать всё проще.
Никакого рокетсайнса или, прости господи, лайфхаков, просто делюсь наблюдением.
Последние месяцы для работы со схемами/диаграммами вместо привычных ресурсов (draw.io excalidraw.com diagrams.mingrammer.com) перешёл на связку:
- любой AI ассистент, любая модель
- markdown файл https://www.markdownguide.org/
- mermaid фреймворк https://mermaid.js.org/
Поддержка mermaid в MD файлах уже достаточно давно, а основные инструменты - VSCode, GitHub, GitLab, Cursor умеют это отображать для человека и в IDE, и в веб-браузере.
В некоторых случаях надо ставить плагин, но в некоторых уже работает и так.
Как это работает:
- я прошу ассистента нарисовать некую схему. Схема может быть обычная flowchart, может быть state, sequence или что-то другое
- в основном README.md или CLAUDE.md появляется блок кода, который отлично лежит в md файле и при этом
- - схема видна в IDE через preview режим
- - схема видна в браузере на страницах GitHub/GitLab
- - схема понятна любой нейронке
- самое главное, это прозрачно и для меня и нейронка понимает что хочет, чтобы изменение кода было корректным
- ну и всё это в git репозитории
Рандомный пример промпта:
Нарисуй flowchart в mermaid, который показывает, как Pod с toleration scheduling'ится на tainted node в Kubernetes
Рандомный пример кода (в файлах формата
*.md)graph TB
subgraph "Node Pool"
N1[Node 1<br/>No Taint]
N2[Node 2<br/>Taint: dedicated=app:NoSchedule]
N3[Node 3<br/>Taint: dedicated=app:NoSchedule]
end
subgraph "Pods"
P1[Pod A<br/>No Toleration]
P2[Pod B<br/>Toleration: dedicated=app]
P3[Pod C<br/>No Toleration]
end
P1 -->|Can Schedule| N1
P1 -.->|Cannot Schedule| N2
P1 -.->|Cannot Schedule| N3
P2 -->|Can Schedule| N1
P2 -->|Can Schedule| N2
P2 -->|Can Schedule| N3
P3 -->|Can Schedule| N1
P3 -.->|Cannot Schedule| N2
P3 -.->|Cannot Schedule| N3
style N1 fill:#E8F5E9,stroke:#4CAF50,stroke-width:2px,color:#1B5E20
style N2 fill:#FCE4EC,stroke:#E91E63,stroke-width:2px,color:#880E4F
style N3 fill:#FCE4EC,stroke:#E91E63,stroke-width:2px,color:#880E4F
style P1 fill:#E3F2FD,stroke:#2196F3,stroke-width:2px,color:#0D47A1
style P2 fill:#E3F2FD,stroke:#2196F3,stroke-width:2px,color:#0D47A1
style P3 fill:#E3F2FD,stroke:#2196F3,stroke-width:2px,color:#0D47A1
Результат на скриншоте (GitHub + Cursor IDE).
Сами промпт/схема просто рандомно-мусорная, лишь для примера.
Можно рисовать что угодно:
- схема инфры
- как работает хэндшейки бэкенда
- логика распределения подов по нод группам в кубере
- как работает самописный оператор
- sequence diagram, как микросервисы общаются при отказе базы
и так далее.
Связка мне нравится, я думаю многие это используют.
Если ещё не пробовали - начните.
Мы итак многие инженерные вещи переусложнили.
Хочется хотя бы с диаграммами и схемами сделать всё проще.
5👍28❤6
#longread #grafana #kubernetes #troubleshooting #одинденьизжизни
А давненько не было лонгридов.
Три дашборда на границе Grafana
Часть 1 из 3.
У нас был дашборд
Пришло время мигрировать на VictoriaLogs.
Чтобы не ломать продакшен, я сделал всё по уму: создал копию дашборда с новым UID
Пару недель работал над ним параллельно с основным.
Переписывал запросы, проверял, что данные сходятся.
Всё работало.
Старый дашборд на Loki, новый на VictoriaLogs - оба живут рядом, никому не мешают.
Когда draft был готов, убрал из названия "Draft", поменял UID на
Ещё через неделю решили, что суффикс
Меняю в JSON:
на
И UID с
Коммит, пуш, ArgoCD синкается.
Зелёненькое. Красота.
Иду пить чай.
А, нет, не иду, коллеги пишут, что многое не работает, ссылки ведут на старую/поломанную борду.
Иду в Grafana проверить.
А там старый дашборд. С "Draft" в названии.
И рядом ещё какие-то СТАРЫЕ версии.
И переменная
Эээ, а я вообще смеержил в main ветку?
Да, смержил, вижу новый код в git.
Первая мысль - может ArgoCD не синканул?
Всё синхронизировано.
Статус: Healthy, Synced.
Странно.
Может ConfigMap в кубернетисе не обновился?
Смотрю - там новый JSON.
Без "Draft". Всё верно.
Ничего старого.
Может в поде Grafana старый файл закэшировался?
Нет, файл тоже новый. UID правильный. Title без "Draft".
Может другие файлы рядом лежат и он их синкает?
Все файлы внутри пода проверил - не, только новые тут.
Проверил во второй реплике POD Grafana -так же только новые дашборды.
Бред.
Чего имеем:
- в гите есть изменения, старых дашбордов нет
- ConfigMap правильный
- файл в поде правильный
а в UI - старое.
Что-то не сходится.
Что я упускаю?
А давненько не было лонгридов.
Три дашборда на границе Grafana
Часть 1 из 3.
У нас был дашборд
service-metrics-overview, который работал с Loki. Пришло время мигрировать на VictoriaLogs.
Чтобы не ломать продакшен, я сделал всё по уму: создал копию дашборда с новым UID
service-metrics-overview-vl-draft и пометкой "[VictoriaLogs Draft]" в названии.Пару недель работал над ним параллельно с основным.
Переписывал запросы, проверял, что данные сходятся.
Всё работало.
Старый дашборд на Loki, новый на VictoriaLogs - оба живут рядом, никому не мешают.
Когда draft был готов, убрал из названия "Draft", поменял UID на
service-metrics-overview-vl. Ещё через неделю решили, что суффикс
-vl не нужен - пусть будет просто service-metrics-overview, как раньше. Старый Loki-дашборд к тому моменту уже удалили.Меняю в JSON:
"title": "Service Metrics Overview [VictoriaLogs Draft]"
на
"title": "Service Metrics Overview [VictoriaLogs]"
И UID с
service-metrics-overview-vl-draft на service-metrics-overview.Коммит, пуш, ArgoCD синкается.
Зелёненькое. Красота.
Иду пить чай.
А, нет, не иду, коллеги пишут, что многое не работает, ссылки ведут на старую/поломанную борду.
Иду в Grafana проверить.
А там старый дашборд. С "Draft" в названии.
И рядом ещё какие-то СТАРЫЕ версии.
И переменная
loki висит, хотя я её точно убирал.Эээ, а я вообще смеержил в main ветку?
Да, смержил, вижу новый код в git.
Первая мысль - может ArgoCD не синканул?
argocd app get dashboards --refresh
Всё синхронизировано.
Статус: Healthy, Synced.
Странно.
Может ConfigMap в кубернетисе не обновился?
kubectl get configmap -n grafana dashboards-myapp-service-metrics-common -o yaml | tail -20
Смотрю - там новый JSON.
Без "Draft". Всё верно.
Ничего старого.
Может в поде Grafana старый файл закэшировался?
kubectl exec -n grafana deployment/prod-region1-grafana -c grafana -- tail -5 /tmp/dashboards/MyApp/service-metrics-overview.json
Нет, файл тоже новый. UID правильный. Title без "Draft".
Может другие файлы рядом лежат и он их синкает?
Все файлы внутри пода проверил - не, только новые тут.
Проверил во второй реплике POD Grafana -так же только новые дашборды.
Бред.
Чего имеем:
- в гите есть изменения, старых дашбордов нет
- ConfigMap правильный
- файл в поде правильный
а в UI - старое.
Что-то не сходится.
Что я упускаю?
👍6🔥1
#longread #grafana #kubernetes #troubleshooting #одинденьизжизни
Три дашборда на границе Grafana
Часть 2 из 3.
Смотрю логи Grafana:
И вот оно:
Grafana provisioning видит файл, пытается его применить, но не может - в базе уже есть дашборд с таким UID.
Кстати в некоторых версиях Grafana логи могут не содержать сам UID.
В таком случае придётся идти в API или базу данных напрямую.
У нас графана с авторизацией по SSO, у меня права, как у девелопера.
Мне лень писать заявки на админский доступ и ковырять UI интерфейс, думаю мне хватит и curl + localhost для анализа.
Ведь я знаю, что рут пароль лежит в секретах кубера.😁
Я сам себе админ😀
Проверяю через Grafana API:
И вижу:
-
-
-
Сука, да откуда вы берётесь.
Три версии одного дашборда.
А файл у меня один (гит, конфигмап, файл внутри пода).
Тут до меня дошло: каждый раз, когда я менял UID, provisioning создавал новую запись в базе.
А старые записи никуда не девались - они просто висели мёртвым грузом.
Теперь, когда я хочу вернуть оригинальный UID
Grafana хранит дашборды в PostgreSQL. Provisioning работает так:
- видит файл с UID
- проверяет - есть ли в БД запись с таким UID?
- если есть и привязана к этому файлу - обновляет
- если есть, но привязана к другому источнику - ошибка
Это не баг, а задуманное поведение: provisioning не перезаписывает дашборды, созданные из других источников.
Лезу в базу:
Смотрю что там:
Три записи от разных этапов миграции.
Файл один.
Вот и причина.
Варианты:
1. Удалить через UI - не выйдет (я ж попробовал), "provisioned dashboard cannot be deleted"
2. Ждать - не рассосётся само
3. Рестарт пода/подов - не вариант, мне по шапке за это дадут, графана как бы на всех.
4. Принудительный reload provisioning через API:
Не помогло - ошибка та же, записи в БД мешают.
5. Удалить записи из базы напрямую🤡
Выбираю последний вариант. Сначала бэкап:
Затем удаление:
Три дашборда на границе Grafana
Часть 2 из 3.
Смотрю логи Grafana:
kubectl logs -n grafana deployment/prod-region1-grafana -c grafana --since=1h | grep -iE "provision|dashboard"
И вот оно:
{"error":"A dashboard with the same uid already exists","file":"/tmp/dashboards/MyApp/service-metrics-overview.json"}Grafana provisioning видит файл, пытается его применить, но не может - в базе уже есть дашборд с таким UID.
Кстати в некоторых версиях Grafana логи могут не содержать сам UID.
В таком случае придётся идти в API или базу данных напрямую.
У нас графана с авторизацией по SSO, у меня права, как у девелопера.
Мне лень писать заявки на админский доступ и ковырять UI интерфейс, думаю мне хватит и curl + localhost для анализа.
Ведь я знаю, что рут пароль лежит в секретах кубера.
Я сам себе админ
Проверяю через Grafana API:
GRAFANA_PASS=$(kubectl get secret -n grafana creds-grafana-admin -o jsonpath='{.data.admin-password}' | base64 -d)
kubectl exec -n grafana deployment/prod-region1-grafana -c grafana -- curl -s -u "admin:${GRAFANA_PASS}" "http://localhost:3000/api/search?query=service%20metrics"И вижу:
-
service-metrics-overview -
service-metrics-overview-vl-
service-metrics-overview-vl-draftСука, да откуда вы берётесь.
Три версии одного дашборда.
А файл у меня один (гит, конфигмап, файл внутри пода).
Тут до меня дошло: каждый раз, когда я менял UID, provisioning создавал новую запись в базе.
А старые записи никуда не девались - они просто висели мёртвым грузом.
Теперь, когда я хочу вернуть оригинальный UID
service-metrics-overview, он уже занят старой записью.Grafana хранит дашборды в PostgreSQL. Provisioning работает так:
- видит файл с UID
- проверяет - есть ли в БД запись с таким UID?
- если есть и привязана к этому файлу - обновляет
- если есть, но привязана к другому источнику - ошибка
Это не баг, а задуманное поведение: provisioning не перезаписывает дашборды, созданные из других источников.
Лезу в базу:
# ищу primary ноду (рид реплика не даст писать)
kubectl exec -n grafana pg-monitoring-1 -c postgres -- psql -U postgres -d grafana -c "SELECT pg_is_in_recovery();"
# true = replica
kubectl exec -n grafana pg-monitoring-2 -c postgres -- psql -U postgres -d grafana -c "SELECT pg_is_in_recovery();"
# false = primary
Смотрю что там:
kubectl exec -n grafana pg-monitoring-2 -c postgres -- psql -U postgres -d grafana -c "SELECT id, uid, title, version FROM dashboard WHERE uid LIKE '%service-metrics%' ORDER BY uid;"
id | uid | title | version
------+-------------------------------------+--------------------------------------------------+---------
3781 | service-metrics-overview | Service Metrics Overview | 1
3830 | service-metrics-overview-vl | Service Metrics Overview [VictoriaLogs] | 1
3825 | service-metrics-overview-vl-draft | Service Metrics Overview [VictoriaLogs Draft] | 1
Три записи от разных этапов миграции.
Файл один.
Вот и причина.
Варианты:
1. Удалить через UI - не выйдет (я ж попробовал), "provisioned dashboard cannot be deleted"
2. Ждать - не рассосётся само
3. Рестарт пода/подов - не вариант, мне по шапке за это дадут, графана как бы на всех.
4. Принудительный reload provisioning через API:
kubectl exec -n grafana deployment/prod-region1-grafana -c grafana -- curl -s -X POST -u "admin:${GRAFANA_PASS}" "http://localhost:3000/api/admin/provisioning/dashboards/reload"Не помогло - ошибка та же, записи в БД мешают.
5. Удалить записи из базы напрямую
Выбираю последний вариант. Сначала бэкап:
kubectl exec -n grafana pg-monitoring-2 -c postgres -- pg_dump -U postgres grafana > grafana_backup_$(date +%Y%m%d_%H%M%S).sql
Затем удаление:
kubectl exec -n grafana pg-monitoring-2 -c postgres -- psql -U postgres -d grafana -c "DELETE FROM dashboard WHERE uid IN ('service-metrics-overview', 'service-metrics-overview-vl', 'service-metrics-overview-vl-draft');"DELETE 3
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥5
#longread #grafana #kubernetes #troubleshooting #одинденьизжизни
Три дашборда на границе Grafana
Часть 3 из 3.
Жду 30 секунд (updateIntervalSeconds в provisioning конфиге).
Проверяю:
Одна запись. Правильный title. Правильный UID.
Иду в UI - работает. Никаких дублей, никаких "Draft", никакого Loki.
Победа! 🎉
Если provisioning не срабатывает автоматически, можно вызвать reload через API (выше был пример курлом) или перезапустить pod Grafana (не мой вариант).
Подробнее про provisioning: https://grafana.com/docs/grafana/latest/administration/provisioning/
На всё ушло часа два.
Могло бы - минут пятнадцать, если бы сразу глянул в логи.
Выводы, а куда без них.
- Grafana provisioning не перезаписывает существующие записи в БД, если они созданы из другого источника. Он кидает ошибку в лог и оставляет старую версию.
- ошибка "A dashboard with the same uid already exists" - это про запись в PostgreSQL, не про файл. Файл может быть идеальным, но если в базе мусор - ничего не заработает.
- логи первым делом. Там обычно всё написано. Но не во всех версиях Grafana логи достаточно информативны.
- удаление из БД работает, но это неофициальный подход. Я не нашёл другого способа. Для provisioned dashboards безопасно - они автоматически пересоздаются из файлов. Главное - делать на primary ноде PostgreSQL и иметь бэкап.
- миграции дашбордов - это не "поменять пару строк". Особенно если UID менялся несколько раз. Особенно, если я баран.
Как избежать этой проблемы (предполагаю):
- не меняйте UID после первого деплоя. Если нужна миграция - создайте новый дашборд с новым UID, а старый удалите полностью (включая запись в БД).
- используйте стабильные UID с префиксами:
- не используйте один UID для дашбордов в разных папках - это создаёт race condition.
- документируйте миграции в changelog или README.
- используйте
Пример конфигурации провижининга
Альтернативный подход, без прямого доступа к БД - когда его нет или страшно (предполагаю, что это правильный путь, но я не проверял):
- временно переименовать ConfigMap (например добавить суффикс
- дождаться, пока Grafana удалит дашборды (если
- вернуть ConfigMap с правильным именем и новым UID
- provisioning создаст дашборды заново
Этот способ медленнее, но безопаснее.
Для зануд:
- - -
- https://github.com/grafana/grafana/issues/12411
- https://github.com/grafana/grafana/issues/41085
- https://github.com/grafana/grafana/issues/73043
Три дашборда на границе Grafana
Часть 3 из 3.
Жду 30 секунд (updateIntervalSeconds в provisioning конфиге).
Проверяю:
kubectl exec -n grafana pg-monitoring-2 -c postgres -- psql -U postgres -d grafana -c "SELECT id, uid, title FROM dashboard WHERE uid LIKE '%service-metrics%';"
id | uid | title
------+-----------------------------+--------------------------------------------
3838 | service-metrics-overview | Service Metrics Overview [VictoriaLogs]
Одна запись. Правильный title. Правильный UID.
Иду в UI - работает. Никаких дублей, никаких "Draft", никакого Loki.
Победа! 🎉
Если provisioning не срабатывает автоматически, можно вызвать reload через API (выше был пример курлом) или перезапустить pod Grafana (не мой вариант).
Подробнее про provisioning: https://grafana.com/docs/grafana/latest/administration/provisioning/
На всё ушло часа два.
Могло бы - минут пятнадцать, если бы сразу глянул в логи.
Выводы, а куда без них.
- Grafana provisioning не перезаписывает существующие записи в БД, если они созданы из другого источника. Он кидает ошибку в лог и оставляет старую версию.
- ошибка "A dashboard with the same uid already exists" - это про запись в PostgreSQL, не про файл. Файл может быть идеальным, но если в базе мусор - ничего не заработает.
- логи первым делом. Там обычно всё написано. Но не во всех версиях Grafana логи достаточно информативны.
- удаление из БД работает, но это неофициальный подход. Я не нашёл другого способа. Для provisioned dashboards безопасно - они автоматически пересоздаются из файлов. Главное - делать на primary ноде PostgreSQL и иметь бэкап.
- миграции дашбордов - это не "поменять пару строк". Особенно если UID менялся несколько раз. Особенно, если я баран.
Как избежать этой проблемы (предполагаю):
- не меняйте UID после первого деплоя. Если нужна миграция - создайте новый дашборд с новым UID, а старый удалите полностью (включая запись в БД).
- используйте стабильные UID с префиксами:
myteam-service-metrics-overview
myteam-app-logs-dashboard
- не используйте один UID для дашбордов в разных папках - это создаёт race condition.
- документируйте миграции в changelog или README.
- используйте
allowUiUpdates: false в provisioning конфиге - это предотвращает дрейф конфигурации.Пример конфигурации провижининга
apiVersion: 1
providers:
- name: 'sidecarProvider'
orgId: 1
type: file
disableDeletion: false
allowUiUpdates: false # запрет редактирования в UI
updateIntervalSeconds: 30 # интервал сканирования файлов
options:
foldersFromFilesStructure: true
path: /tmp/dashboards
Альтернативный подход, без прямого доступа к БД - когда его нет или страшно (предполагаю, что это правильный путь, но я не проверял):
- временно переименовать ConfigMap (например добавить суффикс
-old)- дождаться, пока Grafana удалит дашборды (если
disableDeletion: false)- вернуть ConfigMap с правильным именем и новым UID
- provisioning создаст дашборды заново
Этот способ медленнее, но безопаснее.
Для зануд:
Прямое изменение базы данных Grafana - неофициальная практика!
Это не является моим советом или рекомендацией!
Просто поделился своей историей.
- - -
- https://github.com/grafana/grafana/issues/12411
- https://github.com/grafana/grafana/issues/41085
- https://github.com/grafana/grafana/issues/73043
🔥8❤1
#kubernetes
Пока я спал там взломали все инторнеты и все кубернетисы.
Источник на английском с глубоким разбором.
- https://grahamhelton.com/blog/nodes-proxy-rce
Хорошие ребята, кого не жалко репостнуть:
- https://xn--r1a.website/tech_b0lt_Genona/6101
- https://labs.iximiuz.com/tutorials/nodes-proxy-rce-c9e436a9
Хорошо, что все кубернетисы (кроме моих личных лабораторий) на предыдущих и нынешней работах это приватные сетки (что спасает от внешних негодяев, но не спасает от внутренних, но таких у нас нет).
Как проверить, подвержен ли ваш кластер/SA? А легко!
Копи-паст скрипта в своем кластере:
Да, такие дела. Ну и порт 10250 должен быть доступен.
Пока я спал там взломали все инторнеты и все кубернетисы.
Источник на английском с глубоким разбором.
- https://grahamhelton.com/blog/nodes-proxy-rce
Хорошие ребята, кого не жалко репостнуть:
- https://xn--r1a.website/tech_b0lt_Genona/6101
- https://labs.iximiuz.com/tutorials/nodes-proxy-rce-c9e436a9
Хорошо, что все кубернетисы (кроме моих личных лабораторий) на предыдущих и нынешней работах это приватные сетки (что спасает от внешних негодяев, но не спасает от внутренних, но таких у нас нет).
Как проверить, подвержен ли ваш кластер/SA? А легко!
Копи-паст скрипта в своем кластере:
kubectl get serviceaccounts --all-namespaces -o json | \
jq -r '.items[] | "\(.metadata.namespace)\t\(.metadata.name)"' | \
while IFS=$'\t' read -r ns sa; do
if kubectl auth can-i get nodes/proxy --as="system:serviceaccount:${ns}:${sa}" -n "$ns" >/dev/null 2>&1; then
echo -e "${ns}\t${sa}\tyes"
fi
done
Да, такие дела. Ну и порт 10250 должен быть доступен.
🔥2
#kubernetes #golang #troubleshooting
Снова про операторы.
У ресурса
-
-
Как это работает на самом деле:
Когда ты создаёшь или обновляешь секрет, API-сервер мержит
В etcd хранилке и в ответе API сохраняется только
При чтении секрета из кластера поле
А теперь типичнейшая ошибка, на которую я снова попался.😭
Код оператора с ошибкой:
В реконсайлере проверка "изменился ли секрет?":
Видите проблему?
При создании desired-секрета мы используем
Но в Go-структуре этого объекта поле
Что происходит:
- оператор создаёт секрет с
- при следующем reconcile оператор читает секрет - там
- оператор строит desired-секрет - там
- сравнение видит разницу:
(потому что nil)
- оператор решает обновить секрет и делает
записывая nil в поле данных!
- теперь в кластере секрет с
(0 ключей)
- в следующем reconcile сравнение
оба пустые? Да!
- оператор думает: "секрет актуален", и никогда не восстанавливает данные🤡
Результат: секрет живёт,
Поды падают с:
Фикс очевиден: использовать
Теперь
🎉🎉🎉
Мораль:
-
-
- в client-go структуре
- если используешь
- сравнивай и применяй то, что реально используешь
Потерял на этом баге часа два, пока не дошло. Сука.
Секрет создавался, лейблы были,
Классика - симптомы в одном месте (падающие поды), причина в другом (nil вместо мапы в билдере).
Снова про операторы.
У ресурса
Secret в Kubernetes есть два поля для данных:-
Data map[string][]byte - бинарные данные, в YAML выглядят как base64-
StringData map[string]string - строковые данные, plain text в YAMLКак это работает на самом деле:
StringData - write-only поле для удобства. Когда ты создаёшь или обновляешь секрет, API-сервер мержит
StringData в Data (ключи из StringData перезаписывают Data при совпадении). В etcd хранилке и в ответе API сохраняется только
Data. При чтении секрета из кластера поле
StringData всегда пустое - его там просто нет.Поэтому StringData - для людей и YAML-манифестов, а Data - для кода операторов.
А теперь типичнейшая ошибка, на которую я снова попался.
Код оператора с ошибкой:
func BuildCredentialsSecret(instance *v1alpha1.MyApp) *corev1.Secret {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: instance.Name + "-credentials",
Namespace: instance.Namespace,
},
Type: corev1.SecretTypeOpaque,
StringData: map[string]string{
"API_HOST": instance.Spec.Config.API.Host,
"API_TOKEN": instance.Spec.Config.API.Token,
},
}
}В реконсайлере проверка "изменился ли секрет?":
func secretDataEqual(a, b map[string][]byte) bool {
if len(a) != len(b) {
return false
}
for k, v := range a {
if !bytes.Equal(v, b[k]) {
return false
}
}
return true
}
// в reconcile:
if !secretDataEqual(found.Data, secret.Data) {
// обновляем секрет
found.Data = secret.Data // ВОТ ТУТ ПРОБЛЕМА
return r.Update(ctx, found)
}Видите проблему?
При создании desired-секрета мы используем
StringData. Но в Go-структуре этого объекта поле
Data равно nil, потому что мы его не заполняли!!!Что происходит:
- оператор создаёт секрет с
StringData > API сервер конвертирует в Data, в кластере всё ок- при следующем reconcile оператор читает секрет - там
Data заполнен- оператор строит desired-секрет - там
Data = nil, StringData заполнен- сравнение видит разницу:
len(found.Data) != 0, а len(secret.Data) == 0
(потому что nil)
- оператор решает обновить секрет и делает
found.Data = secret.Data
записывая nil в поле данных!
- теперь в кластере секрет с
data: {}(0 ключей)
- в следующем reconcile сравнение
secretDataEqual(found.Data, nil)
оба пустые? Да!
(len(nil) == 0, len(map{}) == 0)- оператор думает: "секрет актуален", и никогда не восстанавливает данные
Результат: секрет живёт,
ownerReference на месте, лейблы есть, а данных - ноль. Поды падают с:
MountVolume.SetUp failed for volume "credentials":
references non-existent secret key: API_TOKEN
Фикс очевиден: использовать
Data в коде оператора:func BuildCredentialsSecret(instance *v1alpha1.MyApp) *corev1.Secret {
return &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: instance.Name + "-credentials",
Namespace: instance.Namespace,
},
Type: corev1.SecretTypeOpaque,
Data: map[string][]byte{
"API_HOST": []byte(instance.Spec.Config.API.Host),
"API_TOKEN": []byte(instance.Spec.Config.API.Token),
},
}
}Теперь
secret.Data заполнен, сравнение работает корректно, и ты случайно не затираешь данные nil-ом.🎉🎉🎉
Мораль:
-
stringData - для YAML и kubectl, где удобно писать plain text-
data - для кода операторов на Go- в client-go структуре
StringData и Data - разные поля; API сервер не синхронизирует их при чтении- если используешь
StringData в коде, помни: secret.Data == nil в этой структуре- сравнивай и применяй то, что реально используешь
Потерял на этом баге часа два, пока не дошло. Сука.
Секрет создавался, лейблы были,
ownerReference был, а данных - ноль. Классика - симптомы в одном месте (падающие поды), причина в другом (nil вместо мапы в билдере).
Please open Telegram to view this post
VIEW IN TELEGRAM
❤6👍6
#kubernetes #aws #eks #helm
Тихо и незаметно вышла мажорная версия AWS Load Balancer Controller - 3.0.0.
https://github.com/kubernetes-sigs/aws-load-balancer-controller
Вместе с ней и Helm-чарт тоже прыгнул на 3.x.
https://github.com/aws/eks-charts/tree/master/stable/aws-load-balancer-controller
Как я понял по заявлению мейнтейнеров, breaking changes в пользовательском API (Ingress, Service, Gateway) они не предполагают.
Но issue на GitHub уже висят, так что посмотрим.
Что важного и интересного (для меня):
- Gateway API объявлен GA (production ready) - теперь можно смело использовать в бою, а не в тестовых проектах 🔥
- Версия Helm-чарта выровнена с версией контроллера - раньше было 2.x контроллер с чартом 1.x, теперь 3.0.0 = 3.0.0. Наконец-то! ❤️
- Новые фиксы NLB target groups, порядка сабнетов, AZ mismatch и webhook-сертификатов
- CRD ListenerRuleConfiguration добавлен, но пока экспериментальный - не использовать в проде
- Минимальная версия Kubernetes - теперь официально требуется 1.24+ (интересно, кто ещё на таком старье сидит😁 )
- IPv6 для internet-facing ALB/NLB - теперь нативно поддерживается без костылей, раньше были ограничения 🎉
Нюансы с апгрейдом:
- перед helm upgrade до 3.0.0 нужно вручную обновить CRD'ы. Issue #4555 прямо про это - люди натыкаются на проблемы, потому что в release notes это не очень явно прописано.
Но как по мне так это и очевидно, так как мы все знаем, что при хелм апгрейде CRD не обновляется никогда и ни у кого, архитектурная особенность самого хелма.
- для Gateway API нужны не только CRD самого контроллера, но и базовые Gateway API CRD (HTTPRoute, GatewayClass и т.д.), если их ещё нет в кластере
Сам, конечно же, я пока обновляться не буду.
Почитаю issues ещё недельку, потом с командой решим - стоит ли сразу или подождать патч-релиз.
Better safe than sorry, особенно с ингресс-контроллерами, которые держат весь трафик.
Тихо и незаметно вышла мажорная версия AWS Load Balancer Controller - 3.0.0.
https://github.com/kubernetes-sigs/aws-load-balancer-controller
Вместе с ней и Helm-чарт тоже прыгнул на 3.x.
https://github.com/aws/eks-charts/tree/master/stable/aws-load-balancer-controller
Как я понял по заявлению мейнтейнеров, breaking changes в пользовательском API (Ingress, Service, Gateway) они не предполагают.
Но issue на GitHub уже висят, так что посмотрим.
Что важного и интересного (для меня):
- Gateway API объявлен GA (production ready) - теперь можно смело использовать в бою, а не в тестовых проектах 🔥
- Версия Helm-чарта выровнена с версией контроллера - раньше было 2.x контроллер с чартом 1.x, теперь 3.0.0 = 3.0.0. Наконец-то! ❤️
- Новые фиксы NLB target groups, порядка сабнетов, AZ mismatch и webhook-сертификатов
- CRD ListenerRuleConfiguration добавлен, но пока экспериментальный - не использовать в проде
- Минимальная версия Kubernetes - теперь официально требуется 1.24+ (интересно, кто ещё на таком старье сидит
- IPv6 для internet-facing ALB/NLB - теперь нативно поддерживается без костылей, раньше были ограничения 🎉
Нюансы с апгрейдом:
- перед helm upgrade до 3.0.0 нужно вручную обновить CRD'ы. Issue #4555 прямо про это - люди натыкаются на проблемы, потому что в release notes это не очень явно прописано.
Но как по мне так это и очевидно, так как мы все знаем, что при хелм апгрейде CRD не обновляется никогда и ни у кого, архитектурная особенность самого хелма.
- для Gateway API нужны не только CRD самого контроллера, но и базовые Gateway API CRD (HTTPRoute, GatewayClass и т.д.), если их ещё нет в кластере
Сам, конечно же, я пока обновляться не буду.
Почитаю issues ещё недельку, потом с командой решим - стоит ли сразу или подождать патч-релиз.
Better safe than sorry, особенно с ингресс-контроллерами, которые держат весь трафик.
Please open Telegram to view this post
VIEW IN TELEGRAM
💯4🎃1
Please open Telegram to view this post
VIEW IN TELEGRAM
❤7🤣4🤯3💔2👍1
Там
Прям с графиками, картиночками, наглядно.
В целом годно, как и многие его другие ресурсы/статьи. Никакого рокетсайнса и кишочков - просто база-дополнение к официальной документации.
Рекомендую как для общего развития начинающим специалистам, так и в качестве подготовки к собеседованиям на тему кубернетиса.
https://learnkube.com/kubernetes-api-explained
Daniele Polencic обновил статью про k8s API.Прям с графиками, картиночками, наглядно.
В целом годно, как и многие его другие ресурсы/статьи. Никакого рокетсайнса и кишочков - просто база-дополнение к официальной документации.
Рекомендую как для общего развития начинающим специалистам, так и в качестве подготовки к собеседованиям на тему кубернетиса.
https://learnkube.com/kubernetes-api-explained
🔥10👍2
#gitlab #troubleshooting #одинденьизжизни
На новом проекте дали задачу: небольшие изменения в гошном операторе Kubernetes.
Секреты, cluster-scoped объекты, ничего космического.
Покрутил код, разобрался в архитектуре, поднял локальный kind-кластер, погонял тесты.
Makefile на месте, всё зелёное. Красота.
Коммичу, пушу MR.
Тесты падают. 🙃
Ну ладно, думаю. Я серьёзно менял логику, наверное поломал что-то. Бывает.
Возвращаюсь, перепроверяю. Локально гоняю - всё проходит.
Коммичу, пушу - снова падает.
Ок, уже интересно.
Открываю
Повторяю ровно то же самое локально - make test, всё зелёное.
В GitLab - красное.
Смотрю логи джобы внимательнее.
Один из тестов падает на скачивании image из container registry соседнего репозитория. Не хватает авторизации. Ага.
Проверяю:
- мой GitLab токен - валидный✅
- доступ в соседние репозитории (сайдкар-образы для тестов) - есть✅
- образы в registry - на месте✅
- пайплайн до моих изменений - зелёный, тесты проходили✅
Всё на месте, а не работает.🤡
Перепроверяю ещё тысячу раз.
Локально - 100% то же самое - работает.
В GitLab - нет.
Тут приходит мысль: а может дело не в коде, а в правах?
Пайплайн-то запускается от моего имени.
А у тех, кто запускал до меня, может быть что-то другое.
Иду смотреть настройки репозитория.
Settings - нет доступа.
Access Tokens - нет доступа.
Deploy Tokens - нет доступа.
У меня Developer, без права заглянуть хоть куда-то в настройки.
Пишу в личку коллеге, который точно Admin:
- Слушай, можешь по этой ссылке просто нажать Retry на джобе тестов?
Он жмёт.
Тесты проходят. ✅Сука.
Я жму Retry.
Тесты падают. ❌Сука.
Пишу главной команде DevOps (да, оксюморон - девопс пишет девопсам "спасити-памагити").
Объясняю ситуацию: так и так, тесты падают только когда пайплайн запускает мой юзер, предполагаю, что
Ну, после некоторых усилий удалось донести мысль. Ребята берут таймаут.
Через несколько дней/часов:
- Ретрай.
Ретраю.
Зелёное. 🎉
- А что было?
- Мы тоже долго дебажили, скормили все токены в Claude, но нашли.
У тебя в профиле GitLab стояла галочка External User.
External. User. Сука.
Галочка. В профиле. Которую я не ставил. Которую даже не видел (у меня нет доступа к админ-панели). Которую кто-то когда-то поставил при создании моего аккаунта. Или не снял. Или поставил по дефолту в LDAP/SAML/SCIM-маппинге. Или просто потому что.
Что делает External User в GitLab:
- https://docs.gitlab.com/administration/external_users/
- внешний пользователь не имеет доступа к internal-репозиториям
- CI_JOB_TOKEN наследует ограничения профиля
- даже если у тебя есть явный Developer-доступ к проекту, некоторые internal-ресурсы (включая container registry соседних проектов) могут быть недоступны
- в логах пайплайна это выглядит как обычная 401/403 при pull image - ни слова про external user
Всё работает. Везде. Кроме одного места.
И это место - галочка в чужой админ-панели, которую ты даже увидеть не можешь.
Потрачено:
- ~6-7 часов моего времени на дебаг
- N часов времени DevOps-команды
- массу нервных клеток
- массу токенов в Claude
Выводы:
- если пайплайн падает только у вашего юзера, а у коллег - нет, проблема не всегда в коде.
Проблема иногда в правах. Не в правах репозитория, а в правах профиля.
- External User в GitLab - это тихий убийца. Никаких предупреждений в UI, никаких баннеров "вы external". Просто
- вы не увидите эту галочку сами - она в Admin Area, доступ только у администраторов GitLab.
- при онбординге на новый проект спросите: "А мой аккаунт точно не external?" Серьёзно. Одна галочка - полдня жизни.
- локальные тесты != CI тесты. Даже если команды идентичны. Контекст авторизации разный.
- и как обычно: во всём виноваты девопсы. Даже когда девопс - это ты сам.😢
Тихий убийца времени.
На новом проекте дали задачу: небольшие изменения в гошном операторе Kubernetes.
Секреты, cluster-scoped объекты, ничего космического.
Покрутил код, разобрался в архитектуре, поднял локальный kind-кластер, погонял тесты.
Makefile на месте, всё зелёное. Красота.
Коммичу, пушу MR.
Тесты падают. 🙃
Ну ладно, думаю. Я серьёзно менял логику, наверное поломал что-то. Бывает.
Возвращаюсь, перепроверяю. Локально гоняю - всё проходит.
Коммичу, пушу - снова падает.
Ок, уже интересно.
Открываю
.gitlab-ci.yml, читаю джобу тестов строчка за строчкой.Повторяю ровно то же самое локально - make test, всё зелёное.
В GitLab - красное.
Смотрю логи джобы внимательнее.
Один из тестов падает на скачивании image из container registry соседнего репозитория. Не хватает авторизации. Ага.
Проверяю:
- мой GitLab токен - валидный✅
- доступ в соседние репозитории (сайдкар-образы для тестов) - есть✅
- образы в registry - на месте✅
- пайплайн до моих изменений - зелёный, тесты проходили✅
Всё на месте, а не работает.
Перепроверяю ещё тысячу раз.
Локально - 100% то же самое - работает.
В GitLab - нет.
Тут приходит мысль: а может дело не в коде, а в правах?
Пайплайн-то запускается от моего имени.
А у тех, кто запускал до меня, может быть что-то другое.
Иду смотреть настройки репозитория.
Settings - нет доступа.
Access Tokens - нет доступа.
Deploy Tokens - нет доступа.
У меня Developer, без права заглянуть хоть куда-то в настройки.
Пишу в личку коллеге, который точно Admin:
- Слушай, можешь по этой ссылке просто нажать Retry на джобе тестов?
Он жмёт.
Тесты проходят. ✅Сука.
Я жму Retry.
Тесты падают. ❌Сука.
Пишу главной команде DevOps (да, оксюморон - девопс пишет девопсам "спасити-памагити").
Объясняю ситуацию: так и так, тесты падают только когда пайплайн запускает мой юзер, предполагаю, что
CI_JOB_TOKEN моего профиля не имеет нужных прав, он передаётся в кубер как image pull secret, а там авторизация не проходит.Ну, после некоторых усилий удалось донести мысль. Ребята берут таймаут.
Через несколько дней/часов:
- Ретрай.
Ретраю.
Зелёное. 🎉
- А что было?
- Мы тоже долго дебажили, скормили все токены в Claude, но нашли.
У тебя в профиле GitLab стояла галочка External User.
External. User. Сука.
Галочка. В профиле. Которую я не ставил. Которую даже не видел (у меня нет доступа к админ-панели). Которую кто-то когда-то поставил при создании моего аккаунта. Или не снял. Или поставил по дефолту в LDAP/SAML/SCIM-маппинге. Или просто потому что.
Что делает External User в GitLab:
- https://docs.gitlab.com/administration/external_users/
- внешний пользователь не имеет доступа к internal-репозиториям
- CI_JOB_TOKEN наследует ограничения профиля
- даже если у тебя есть явный Developer-доступ к проекту, некоторые internal-ресурсы (включая container registry соседних проектов) могут быть недоступны
- в логах пайплайна это выглядит как обычная 401/403 при pull image - ни слова про external user
Всё работает. Везде. Кроме одного места.
И это место - галочка в чужой админ-панели, которую ты даже увидеть не можешь.
Потрачено:
- ~6-7 часов моего времени на дебаг
- N часов времени DevOps-команды
- массу нервных клеток
- массу токенов в Claude
Выводы:
- если пайплайн падает только у вашего юзера, а у коллег - нет, проблема не всегда в коде.
Проблема иногда в правах. Не в правах репозитория, а в правах профиля.
- External User в GitLab - это тихий убийца. Никаких предупреждений в UI, никаких баннеров "вы external". Просто
CI_JOB_TOKEN молча получает урезанные права.- вы не увидите эту галочку сами - она в Admin Area, доступ только у администраторов GitLab.
- при онбординге на новый проект спросите: "А мой аккаунт точно не external?" Серьёзно. Одна галочка - полдня жизни.
- локальные тесты != CI тесты. Даже если команды идентичны. Контекст авторизации разный.
- и как обычно: во всём виноваты девопсы. Даже когда девопс - это ты сам.
Please open Telegram to view this post
VIEW IN TELEGRAM
👍18🤡2💔1
#longread #troubleshooting #operators #kubectl #kubernetes #одинденьизжизни
Внезапный и незапланированный лонгрид.
Разбирал по работе задачу, узнал новое про фичу
Теперь делюсь историей и знаниями с вами.
Приятного прочтения.
https://teletype.in/@kruchkov_alexandr/wWa-INvQvJH
Внезапный и незапланированный лонгрид.
Разбирал по работе задачу, узнал новое про фичу
kubectl cache discovery.Теперь делюсь историей и знаниями с вами.
Приятного прочтения.
https://teletype.in/@kruchkov_alexandr/wWa-INvQvJH
Teletype
Когда kubectl врёт
Сегодня мне для нашего кубернетис оператора надо было поменять Namespaced scope на Cluster scope у одного CRD.
15👍23🔥1