Cleaned up.
This commit is contained in:
@ -1,161 +0,0 @@
|
||||
---
|
||||
title: Разделение докера на среды.
|
||||
description: Как работать с несколькими docker-compose.
|
||||
position: 2
|
||||
category: DevOps
|
||||
---
|
||||
|
||||
|
||||
# Разделение докера на среды
|
||||
|
||||
Должен ли ты разделять среды докера в несколько `docker-compose` файлов?
|
||||
Определенно! В некоторых случаях невозможно разобраться что разработчики хотели сделать или почему ничего не работает. Настройка раздельных сред может стать настояшей мешаниной. В этой статье я покажу как настроить `docker-compose` и избежать миллиона проблем.
|
||||
|
||||
Как мы вообще можем разделить среды для локальной разработки и продовые?
|
||||
Отет прост: Требуется декомпозировать проект и создать отдельные файлы под каждую из сред или даже сервисов.
|
||||
|
||||
Это нормально, если у тебя будет больше 2-х `docker-compose` файлов.
|
||||
|
||||
Например:
|
||||
```
|
||||
deploy
|
||||
├── docker-compose.autotests.yml
|
||||
├── docker-compose.db.yml
|
||||
├── docker-compose.dev.yml
|
||||
├── docker-compose.yml
|
||||
├── dockerfiles
|
||||
│ ├── api.Dockerfile
|
||||
│ └── front.Dockerfile
|
||||
└── scripts
|
||||
├── start-autotests.sh
|
||||
├── start-backend.sh
|
||||
└── start-migrations.sh
|
||||
```
|
||||
|
||||
### Как это работает?
|
||||
Докер умеет работать с множеством `docker-compose` файлов одновременно. И мы можем использовать это для разделения сред.
|
||||
|
||||
Запуск таких конфигураций выглядит следующим образом:
|
||||
```bash
|
||||
$ docker-compose \
|
||||
-f "deploy/docker-compose.yml" \
|
||||
-f "deploy/docker-compose.db.yml" \
|
||||
-f "deploy/docker-compose.dev.yml" \
|
||||
up
|
||||
```
|
||||
|
||||
В каждом из этих файлов определен какой-то кусок конфигурации, который не пересекается. Например в `docker-compose.yml` определено приложение и некоторые необходимые сервисы, а в остальных файлах добавляются сервисы или меняются значения предыдущих.
|
||||
|
||||
Наверное, тут проще показать на примере.
|
||||
|
||||
Допустим у нас есть проект, у которого поднимается бекенд с параметрами, которые отличаются на проде и локально.
|
||||
|
||||
Для простоты примера создадим простецкий проект со следующей структурой:
|
||||
```
|
||||
proj
|
||||
├── deploy
|
||||
│ ├── docker-compose.yml
|
||||
│ └── dockerfiles
|
||||
│ └── script.Dockerfile
|
||||
└── script.py
|
||||
```
|
||||
|
||||
Для начала напишем скрипт, который будет в центре всего.
|
||||
```python{}[script.pt]
|
||||
from sys import argv # это аргуметы переданные в скрипт
|
||||
|
||||
def main():
|
||||
print("; ".join(argv[1:])) # выводит на экран все аргументы программы
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
|
||||
После того, как скрипт готов и отлажен давайте завернем его в докер,
|
||||
создав `Dockerfile`.
|
||||
|
||||
```dockerfile{}[deploy/dockerfiles/script.Dockerfile]
|
||||
from python:3.8 # asd
|
||||
|
||||
WORKDIR /app
|
||||
COPY script.py /app/script.py
|
||||
CMD python /app/script.py
|
||||
```
|
||||
|
||||
Как вы видите, Dockerfile очень прост. Теперь добавим главный `docker-compose.yml`.
|
||||
|
||||
```yaml{}[deploy/docker-compose.yml]
|
||||
---
|
||||
version: '3.7'
|
||||
|
||||
services:
|
||||
script:
|
||||
build: # Собираем приложение используя наш dockerfile.
|
||||
dockerfile: ./deploy/dockerfiles/script.Dockerfile
|
||||
context: .
|
||||
# Запускаем его с командой для продового запуска.
|
||||
command: python script.py this is prod
|
||||
```
|
||||
|
||||
Теперь запустим всё это чудо.
|
||||
|
||||
```bash
|
||||
$ docker-compose \
|
||||
-f "./deploy/docker-compose.yml" \
|
||||
--project-directory "." \
|
||||
run --rm script
|
||||
```
|
||||
|
||||
Вот что будет выведено на экран.
|
||||
```log
|
||||
Creating d-test_script_run ... done
|
||||
this; is; prod
|
||||
```
|
||||
|
||||
Как мы видим, на экран вывелось сообщение, которое мы указали в нашем `docker-compose.yml`
|
||||
|
||||
А теперь для локальной разработки мы не будем ничего менять в нашем файле композиции, а создадим новый рядом.
|
||||
|
||||
```yaml{}[deploy/docker-compose.dev.yml]
|
||||
---
|
||||
version: '3.7'
|
||||
|
||||
services:
|
||||
script:
|
||||
command: python script.py this is dev
|
||||
```
|
||||
|
||||
Теперь добавим ещё один файл в нашу команду запуска.
|
||||
|
||||
```bash
|
||||
$ docker-compose \
|
||||
-f "deploy/docker-compose.yml" \
|
||||
-f "deploy/docker-compose.dev.yml" \
|
||||
--project-directory "." \
|
||||
run --rm script
|
||||
```
|
||||
|
||||
Вот что будет выведено на экран:
|
||||
```log
|
||||
Creating d-test\_script\_run ... done
|
||||
this; is; dev
|
||||
```
|
||||
|
||||
Как можно заметить, конфигурация запуска перезаписалась в порядке вызова.
|
||||
|
||||
То есть, каждый последующий файл композиции может добавлять сервисы и **частично** изменять конфигурацию предыдущих.
|
||||
|
||||
Итоговая структура проекта:
|
||||
```
|
||||
proj
|
||||
├── deploy
|
||||
│ ├── docker-compose.dev.yml
|
||||
│ ├── docker-compose.yml
|
||||
│ └── dockerfiles
|
||||
│ └── script.Dockerfile
|
||||
└── script.py
|
||||
```
|
||||
|
||||
### Где это применимо?
|
||||
Ну, в любом проекте, сложнее того, который мы рассмотрели. Потому что в реальной жизни не всё так радужно и локальная версия приложения может отличаться не только параметрами запуска, но и целыми сервисами, которые требуются для локальной копии приложения.
|
@ -1,139 +0,0 @@
|
||||
---
|
||||
title: Makefiles для чайников.
|
||||
description: Автоматизируем по старинке.
|
||||
position: 3
|
||||
category: 'DevOps'
|
||||
---
|
||||
|
||||
# Makefiles для чайников
|
||||
Что такое мейкфайлы? Вы наверняка их встречали,
|
||||
если собирали проекты из ихсодных файлов.
|
||||
Если описывать в двух словах, то это просто описание команд для упрощения работы
|
||||
с проектом. Изначально делались для того, чтобы удобно компилировать всякие проекты на любых языках.
|
||||
|
||||
## Как они работают?
|
||||
Достаточно просто.
|
||||
|
||||
Вот пример `Makefile`:
|
||||
|
||||
```makefile{}[Makefile]
|
||||
# Сгенерируем простой файл
|
||||
test.gen:
|
||||
@echo 'echo "Hi!"' > "test.gen"
|
||||
|
||||
# Команда для запуска файла test.gen
|
||||
.PHONY: run
|
||||
run: test.gen
|
||||
sh "test.gen"
|
||||
```
|
||||
|
||||
До двоеточий обазначены команды (tagets). Например тут это: `test.gen` и `run`.
|
||||
Данные таргеты можно запускать через `make ${target}`.
|
||||
|
||||
Например, если ввести `make run` в папке с `Makefile`, то мы получим следующее:
|
||||
```console
|
||||
$ sh "test.gen"
|
||||
Hi!
|
||||
```
|
||||
|
||||
Как видно из выхлопа данной команды, у нас успешно запустился файл `test.gen`, хотя мы не запускали команду `make test.gen`. Что произошло? Давайте разбираться.
|
||||
|
||||
## Зависимости таргетов
|
||||
|
||||
На строчке объявления таргета `run` видно, что объявлен `test.gen`. Это зависимость данного таргета и она будет вызвана до того, как выполнится скрипт описываемого таргета. Таких зависимостей может быть много, перечисляются они через пробел.
|
||||
|
||||
Например:
|
||||
```makefile{}[Makefile]
|
||||
.PHONY: target1
|
||||
target1:
|
||||
echo "1"
|
||||
|
||||
.PHONY: target2
|
||||
target2: target1
|
||||
echo "2"
|
||||
|
||||
.PHONY: target3
|
||||
target3: target1 target2
|
||||
echo "memes"
|
||||
```
|
||||
|
||||
При вызове `make target3` будет выведено:
|
||||
```console
|
||||
$ make target3
|
||||
echo "1"
|
||||
1
|
||||
echo "2"
|
||||
2
|
||||
echo "memes"
|
||||
memes
|
||||
```
|
||||
|
||||
Как можно видеть, он построил граф зависимостей и не выполнил `target1` дважды.
|
||||
|
||||
## Сокрытие вывода команд
|
||||
|
||||
В предыдущем примере можно заметить, что он написал все команды в терминал. Для того, чтобы этого избежать следует добавить "@" в начало команды и она не будет напечатана.
|
||||
|
||||
```makefile{}[Makefile]
|
||||
.PHONY: target1
|
||||
target1:
|
||||
@echo "1"
|
||||
|
||||
.PHONY: target2
|
||||
target2: target1
|
||||
@echo "2"
|
||||
|
||||
.PHONY: target3
|
||||
target3: target1 target2
|
||||
@echo "memes"
|
||||
```
|
||||
|
||||
Теперь при вызое `make target3` будет показано следующее:
|
||||
```cosole
|
||||
$ make target3
|
||||
1
|
||||
2
|
||||
memes
|
||||
```
|
||||
|
||||
|
||||
## Валидация сгенерированных файлов
|
||||
Зачастую `Makefile` используют для компиляции С и зачастую требуется
|
||||
собрать какую-либо часть проекта в файл и пропустить сборку этого файла, если он уже собран.
|
||||
Раскрою секрет, в Makefile это базовый функционал.
|
||||
|
||||
Давайте немного поменяем первый Makefile и запустим дважды.
|
||||
```makefile{}[Makefile]
|
||||
# Сгенерируем простой файл
|
||||
test.gen:
|
||||
echo 'echo "Hi!"' > "test.gen"
|
||||
|
||||
# Команда для запуска файла test.gen
|
||||
.PHONY: run
|
||||
run: test.gen
|
||||
sh "test.gen"
|
||||
```
|
||||
|
||||
Теперь вызываемая команда таргета `test.gen` выводится на экран.
|
||||
|
||||
Позапускаем.
|
||||
|
||||
```console
|
||||
$ make run
|
||||
echo 'echo "Hi!"' > "test.gen" # Наша командабыла вызвана.
|
||||
sh "test.gen"
|
||||
Hi!
|
||||
$ make run
|
||||
sh "test.gen" # Наша команда не вызвана.
|
||||
Hi!
|
||||
```
|
||||
|
||||
Дело в том, что названия таргетов - названия файлов, которые должны сгенерировать эти самые таргеты.
|
||||
То есть, в данном случае таргет `test.gen` должен сгенерировать файл `test.gen` по окончании выполнения. Если этот файл уже присутствует, то команда выполнена не будет. Именно поэтому у нас она не запустилась второй раз, так как в первый запуск был создан треубемый файл и его никто не удалял между запусками.
|
||||
|
||||
А что если я вот не хочу чтобы команда создавала и проверяла файлы?
|
||||
Для этого пишут `.PHONY: ${target}`. Например у нас так объявлен таргет `run` и, даже если файл с названием `run` будет присутствовать в директории цель не будет выполняться.
|
||||
|
||||
<br>
|
||||
|
||||
До новых встреч.
|
@ -1,586 +0,0 @@
|
||||
---
|
||||
title: Как стартовать проект
|
||||
description: Лучший вариант начала проекта на питоне.
|
||||
category: Python
|
||||
position: 1
|
||||
---
|
||||
|
||||
# Как стартовать проект
|
||||
|
||||
В целом, вариантов несколько. Например, можно просто хранить несколько скриптов на github и быть довольным, можно дописать к ним `setup.py` и выложиться на pypi. Но лучший способ - структурированный проект с `poetry`.
|
||||
|
||||
Почему?
|
||||
|
||||
Особенностей у poetry много. Сейчас расскажу про то, как использовать и что делать.
|
||||
|
||||
Для начала создадим проект.
|
||||
|
||||
```console
|
||||
$ poetry new new_proj
|
||||
Created package new_proj in new_proj
|
||||
$ cd new_proj
|
||||
```
|
||||
|
||||
Данная команда сгенерирует следующую структуру:
|
||||
```
|
||||
new_proj
|
||||
├── new_proj
|
||||
│ └── __init__.py
|
||||
├── pyproject.toml
|
||||
├── README.rst
|
||||
└── tests
|
||||
├── __init__.py
|
||||
└── test_new_proj.py
|
||||
```
|
||||
|
||||
Это наш новый проект с небольшими заготовками. В целом, тут нет ничего необычного. Весь код библиотеки/проекта будет в папке с названием проекта. `README.rst` - дефолтный README. `pyproject.toml` - мета-данные о проекте, такие как: зависимости, описание, дополнительные установочные опции и многое другое. Ты можешь прочитать побольше о `pyproject.toml` [тут](https://python-poetry.org/docs/pyproject/).
|
||||
|
||||
## Как работать с poetry
|
||||
|
||||
Вся информация есть в [официальной документации](https://python-poetry.org/docs) poetry. Тут я расскажу про основные моменты.
|
||||
|
||||
### Управление зависимостями
|
||||
|
||||
Чтобы добавить зависимость проекта достаточно выполнить
|
||||
```console
|
||||
$ poetry add ${dependency}
|
||||
```
|
||||
|
||||
Данная команда найдет последнюю нужную версию и запишет её в `pyproject.toml` и в `poetry.lock`.
|
||||
|
||||
Для того, чтобы установить зависимость для разработки (линтеры например), достаточно добавить флаг `--dev`.
|
||||
|
||||
```console
|
||||
$ poetry add ${dependency} --dev
|
||||
```
|
||||
|
||||
Чтобы удалить зависимость достаточно просто `add` заменить на `remove`.
|
||||
|
||||
Примеры работы:
|
||||
```console
|
||||
$ poetry add loguru
|
||||
Using version ^0.5.3 for loguru
|
||||
|
||||
Updating dependencies
|
||||
Resolving dependencies... (0.1s)
|
||||
|
||||
Writing lock file
|
||||
|
||||
Package operations: 1 install, 0 updates, 0 removals
|
||||
• Installing loguru (0.5.3)
|
||||
$ poetry remove loguru
|
||||
Updating dependencies
|
||||
Resolving dependencies... (0.1s)
|
||||
|
||||
Writing lock file
|
||||
|
||||
Package operations: 0 installs, 0 updates, 1 removal
|
||||
• Removing loguru (0.5.3)
|
||||
```
|
||||
### Выполнение команд
|
||||
|
||||
При использовании virtualenv для входа в оболочку всегда надо было вводить
|
||||
```bash
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
Однако с poetry это излишне. Он сам создает и менеджит виртуальные среды.
|
||||
Чтобы выполнить одиночную команду можно вызвать `run`.
|
||||
|
||||
Например:
|
||||
```console
|
||||
$ poetry install black --dev
|
||||
Using version ^20.8b1 for black
|
||||
• Installing appdirs (1.4.4)
|
||||
• Installing click (7.1.2)
|
||||
• Installing mypy-extensions (0.4.3)
|
||||
• Installing pathspec (0.8.1)
|
||||
• Installing regex (2020.11.13)
|
||||
• Installing toml (0.10.2)
|
||||
• Installing typed-ast (1.4.2)
|
||||
• Installing typing-extensions (3.7.4.3)
|
||||
• Installing black (20.8b1)
|
||||
$ poetry run black .
|
||||
reformatted new_proj/new_proj/__init__.py
|
||||
reformatted new_proj/tests/test_new_proj.py
|
||||
All done! ✨ 🍰 ✨
|
||||
2 files reformatted, 1 file left unchanged.
|
||||
```
|
||||
|
||||
Для выполнения нескольких комманд подряд, можно войти в shell. Это аналог `source venv/bin/activate`.
|
||||
|
||||
```console
|
||||
$ poetry shell
|
||||
Spawning shell within /home/s3rius/.cache/pypoetry/virtualenvs/new-proj-eutP4v0O-py3.9
|
||||
$ black .
|
||||
reformatted new_proj/new_proj/__init__.py
|
||||
reformatted new_proj/tests/test_new_proj.py
|
||||
All done! ✨ 🍰 ✨
|
||||
2 files reformatted, 1 file left unchanged.
|
||||
```
|
||||
|
||||
### Версионирование
|
||||
|
||||
Менять версии пакета вручную больше не нужно.
|
||||
|
||||
У poetry есть кое что для тебя.
|
||||
```console
|
||||
$ poetry version patch
|
||||
Bumping version from 0.1.0 to 0.1.1
|
||||
$ poetry version preminor
|
||||
Bumping version from 0.1.1 to 0.2.0-alpha.0
|
||||
$ poetry version minor
|
||||
Bumping version from 0.2.0-alpha.0 to 0.2.0
|
||||
$ poetry version premajor
|
||||
Bumping version from 0.2.0 to 1.0.0-alpha.0
|
||||
$ poetry version major
|
||||
Bumping version from 1.0.0-alpha.0 to 1.0.0
|
||||
```
|
||||
|
||||
## pyproject.toml
|
||||
|
||||
Как было сказано ранее, данный файл содержит в себе мета-информацию пакета.
|
||||
|
||||
Пример `pyproject.toml`
|
||||
```toml{}[pyproject.toml]
|
||||
[tool.poetry]
|
||||
name = "new_proj"
|
||||
version = "0.1.0"
|
||||
description = "Test library for example"
|
||||
readme = "README.rst"
|
||||
homepage = "https://test_url.com/"
|
||||
repository = "https://github.meme/"
|
||||
authors = ["Pavel Kirilin <win10@list.ru>"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pytest = "^6.1"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
```
|
||||
|
||||
Для того чтобы устновить все зависимости требуется просто ввести
|
||||
```console
|
||||
$ poetry install
|
||||
```
|
||||
|
||||
Данная команда создаст виртуальную среду сама и установит **все** зависимости. Включая dev-зависимости. Чтобы установить зависимости только для приложения можно добавить `--no-dev` ключ.
|
||||
|
||||
## Как паковать и публиковать на pypi
|
||||
Всё очень просто.
|
||||
Давайте добавим какую-нибудь функцию в проект.
|
||||
|
||||
```python{}[new_proj/main.py]
|
||||
def ab_problem(a: int, b: int) -> int:
|
||||
return a + b
|
||||
```
|
||||
|
||||
```python{}[new_proj/__init.py]
|
||||
from new_proj.main import ab_problem
|
||||
|
||||
__all__ = [
|
||||
'ab_problem'
|
||||
]
|
||||
```
|
||||
|
||||
Теперь соберем проект.
|
||||
|
||||
```console
|
||||
$ poetry build
|
||||
Building new_proj (0.1.0)
|
||||
- Building sdist
|
||||
- Built new_proj-0.1.0.tar.gz
|
||||
- Building wheel
|
||||
- Built new_proj-0.1.0-py3-none-any.whl
|
||||
```
|
||||
|
||||
Та-да. Это готовый к публикации на pypi пакет. Лежит он в папке dist.
|
||||
|
||||
Давайте проверим, что всё работает корректно.
|
||||
|
||||
```console
|
||||
$ pip install "./dist/new_proj-0.1.0-py3-none-any.whl"
|
||||
Processing ./dist/new_proj-0.1.0-py3-none-any.whl
|
||||
Installing collected packages: new-proj
|
||||
Successfully installed new-proj-0.1.0
|
||||
|
||||
$ python
|
||||
Python 3.9.1 (default, Feb 1 2021, 04:02:33)
|
||||
[GCC 10.2.0] on linux
|
||||
Type "help", "copyright", "credits" or "license" for more information.
|
||||
$ from new_proj import ab_problem
|
||||
$ ab_problem(1,33)
|
||||
34
|
||||
```
|
||||
|
||||
Как можно видеть всё работает корректно и теперь мы можем использовать наш пакет.
|
||||
|
||||
Для публикации следует использовать:
|
||||
```console
|
||||
$ poetry publish -u "user" -p "password"
|
||||
```
|
||||
|
||||
Подробнее можно почитать [тут](https://python-poetry.org/docs/cli/#publish).
|
||||
|
||||
# Конфигурация проекта
|
||||
Конечно, такого рода конфигурация проекта всё равно никуда не годиться.
|
||||
Давайте настроим автоматический линтинг.
|
||||
|
||||
```console
|
||||
$ poetry add \
|
||||
flake8 \
|
||||
black \
|
||||
isort \
|
||||
mypy \
|
||||
pre-commit \
|
||||
yesqa \
|
||||
autoflake \
|
||||
wemake-python-styleguide --dev
|
||||
```
|
||||
|
||||
Теперь добавим конфигурационных файлов в корень проекта.
|
||||
Это мои конфигурации, которые я настроил под себя, можешь менять их как хочешь.
|
||||
|
||||
|
||||
Для конфигурации сортировки импортов и проверки типов добавим следющее
|
||||
в наш основной файл проекта.
|
||||
|
||||
Обычно я добавляю эти секции сразу после секции `[tool.poetry.dev-dependencies]`.
|
||||
|
||||
```toml{}[pyproject.toml]
|
||||
...
|
||||
|
||||
[tool.mypy]
|
||||
strict = true
|
||||
ignore_missing_imports = true
|
||||
allow_subclassing_any = true
|
||||
allow_untyped_calls = true
|
||||
pretty = true
|
||||
show_error_codes = true
|
||||
implicit_reexport = true
|
||||
allow_untyped_decorators = true
|
||||
warn_return_any = false
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
multi_line_output = 3
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
`.flake8` - конфигурация линтинга. Тут довольно много. Это игнорирование ненужных кодов ошибок, которые не особо-то и ошибки.
|
||||
```ini{}[.flake8]
|
||||
[flake8]
|
||||
max-complexity = 6
|
||||
inline-quotes = double
|
||||
max-line-length = 88
|
||||
extend-ignore = E203
|
||||
docstring_style=sphinx
|
||||
|
||||
ignore =
|
||||
; Found `f` string
|
||||
WPS305,
|
||||
; Missing docstring in public module
|
||||
D100,
|
||||
; Missing docstring in magic method
|
||||
D105,
|
||||
; Missing docstring in __init__
|
||||
D107,
|
||||
; Found class without a base class
|
||||
WPS306,
|
||||
; Missing docstring in public nested class
|
||||
D106,
|
||||
; First line should be in imperative mood
|
||||
D401,
|
||||
; Found `__init__.py` module with logic
|
||||
WPS412,
|
||||
; Found implicit string concatenation
|
||||
WPS326,
|
||||
; Found string constant over-use
|
||||
WPS226,
|
||||
; Found upper-case constant in a class
|
||||
WPS115,
|
||||
; Found nested function
|
||||
WPS430,
|
||||
; Found using `@staticmethod`
|
||||
WPS602,
|
||||
; Found method without arguments
|
||||
WPS605,
|
||||
; Found overused expression
|
||||
WPS204,
|
||||
; Found too many module members
|
||||
WPS202,
|
||||
; Found too high module cognitive complexity
|
||||
WPS232,
|
||||
; line break before binary operator
|
||||
W503,
|
||||
; Found module with too many imports
|
||||
WPS201,
|
||||
; Found vague import that may cause confusion: X
|
||||
WPS347,
|
||||
; Inline strong start-string without end-string.
|
||||
RST210,
|
||||
; subprocess call with shell=True seems safe, but may be changed in the future.
|
||||
S602,
|
||||
; Starting a process with a partial executable path.
|
||||
S607,
|
||||
; Consider possible security implications associated with subprocess module.
|
||||
S404,
|
||||
; Found nested class
|
||||
WPS431,
|
||||
; Found wrong module name
|
||||
WPS100,
|
||||
; Found too many methods
|
||||
WPS214,
|
||||
; Found too long ``try`` body
|
||||
WPS229,
|
||||
; Found function with too much cognitive complexity
|
||||
WPS231,
|
||||
|
||||
; all init files
|
||||
__init__.py:
|
||||
; ignore not used imports
|
||||
F401,
|
||||
; ignore import with wildcard
|
||||
F403,
|
||||
; Found wrong metadata variable
|
||||
WPS410,
|
||||
|
||||
per-file-ignores =
|
||||
; all tests
|
||||
test_*.py,tests.py,tests_*.py,*/tests/*:
|
||||
; Use of assert detected
|
||||
S101,
|
||||
|
||||
exclude =
|
||||
./.git,
|
||||
./venv,
|
||||
./cached_venv,
|
||||
./var,
|
||||
```
|
||||
|
||||
`.pre-commit-config.yaml` - конфигураци хуков для запуска всех линтеров перед коммитом.
|
||||
```yaml{}[.pre-commit-config.yaml]
|
||||
# See https://pre-commit.com for more information
|
||||
# See https://pre-commit.com/hooks.html for more hooks
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v2.4.0
|
||||
hooks:
|
||||
- id: check-ast
|
||||
- id: trailing-whitespace
|
||||
- id: check-toml
|
||||
- id: end-of-file-fixer
|
||||
|
||||
- repo: https://github.com/asottile/add-trailing-comma
|
||||
rev: v2.1.0
|
||||
hooks:
|
||||
- id: add-trailing-comma
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: black
|
||||
name: Format with Black
|
||||
entry: black
|
||||
language: system
|
||||
types: [python]
|
||||
|
||||
- id: autoflake
|
||||
name: autoflake
|
||||
entry: autoflake
|
||||
language: system
|
||||
types: [ python ]
|
||||
args: [ --in-place, --remove-all-unused-imports, --remove-duplicate-keys ]
|
||||
|
||||
- id: isort
|
||||
name: isort
|
||||
entry: isort
|
||||
language: system
|
||||
types: [ python ]
|
||||
|
||||
- id: flake8
|
||||
name: Check with Flake8
|
||||
entry: flake8
|
||||
language: system
|
||||
pass_filenames: false
|
||||
types: [ python ]
|
||||
args: [--count, .]
|
||||
|
||||
- id: mypy
|
||||
name: Validate types with MyPy
|
||||
entry: mypy
|
||||
language: system
|
||||
types: [ python ]
|
||||
|
||||
- id: yesqa
|
||||
name: Remove usless noqa
|
||||
entry: yesqa
|
||||
language: system
|
||||
types: [ python ]
|
||||
```
|
||||
|
||||
И не забываем про `.gitignore`. Его можно найти [тут](https://github.com/github/gitignore/blob/master/Python.gitignore).
|
||||
|
||||
ОН НЕОБХОДИМ ЧТОБЫ pre-commit РАБОТЛ МАКСИМАЛЬНО КОРРЕКТНО.
|
||||
|
||||
Теперь установим хуки в репозиторий.
|
||||
```console
|
||||
$ git init
|
||||
Initialized empty Git repository in .git/
|
||||
$ poetry shell
|
||||
$ pre-commit install
|
||||
pre-commit installed at .git/hooks/pre-commit
|
||||
$ git commit
|
||||
... # Упадет с кучей ошибок
|
||||
```
|
||||
|
||||
Теперь поправим все возникшие проблемы.
|
||||
|
||||
Во первых исправим тесты.
|
||||
|
||||
В `tests/test_new_proj.py` напишем следующее:
|
||||
```python{}[tests/test_new_proj.py]
|
||||
from new_proj import ab_problem
|
||||
|
||||
|
||||
def test_ab() -> None:
|
||||
"""AB problecm success case."""
|
||||
assert ab_problem(1, 2) == 3
|
||||
```
|
||||
|
||||
Добавим описания в `__init__` файлы.
|
||||
|
||||
```python{}[tests/__init__.py]
|
||||
"""Tests for new_proj."""
|
||||
```
|
||||
|
||||
```python{}[new_proj/__init__.py]
|
||||
"""Project for solving ab problem."""
|
||||
from new_proj.main import ab_problem
|
||||
|
||||
__all__ = [
|
||||
"ab_problem",
|
||||
]
|
||||
```
|
||||
|
||||
Пофиксим основной файл проекта.
|
||||
|
||||
```python{}[new_proj/main.py]
|
||||
def ab_problem(first: int, second: int) -> int:
|
||||
"""
|
||||
Solve AB problem.
|
||||
|
||||
The function sums two integers.
|
||||
|
||||
:param first: a argument.
|
||||
:param second: b argument.
|
||||
:returns: sum.
|
||||
"""
|
||||
return first + second
|
||||
```
|
||||
|
||||
Теперь вы можете сделать свой первый коммит.
|
||||
|
||||
```console
|
||||
$ git commit
|
||||
Check python ast................Passed
|
||||
Trim Trailing Whitespace........Passed
|
||||
Check Toml......................Passed
|
||||
Fix End of Files................Passed
|
||||
Add trailing commas.............Passed
|
||||
Format with Black...............Passed
|
||||
autoflake.......................Passed
|
||||
isort...........................Passed
|
||||
Check with Flake8...............Passed
|
||||
Validate types with MyPy........Passed
|
||||
Remove usless noqa..............Passed
|
||||
pytest..........................Passed
|
||||
```
|
||||
|
||||
Теперь ты знаешь как создавать шедевры.
|
||||
# Создание CLI-приложения
|
||||
А что если я хочу cli-приложение?
|
||||
Ты не представляешь насколько это просто.
|
||||
|
||||
Пойдем модифицируем наш основной файл.
|
||||
|
||||
```python{}[new_proj/main.py]
|
||||
import argparse
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
"""
|
||||
Parse CLI arguments.
|
||||
|
||||
:returns: parsed namespace.
|
||||
"""
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument("a", type=int)
|
||||
parser.add_argument("b", type=int)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def ab_problem(first: int, second: int) -> int:
|
||||
"""
|
||||
Solve AB problem.
|
||||
|
||||
The function sums two integers.
|
||||
|
||||
:param first: a argument.
|
||||
:param second: b argument.
|
||||
:returns: sum.
|
||||
"""
|
||||
return first + second
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Main function."""
|
||||
args = parse_args()
|
||||
print(ab_problem(args.a, args.b)) # noqa: WPS421
|
||||
|
||||
```
|
||||
|
||||
Теперь поправим pyproject.toml таким образом чтобы он создал cli для нашей функции.
|
||||
|
||||
Добавим следующую секцию куда-нибудь в `pyproject.toml`:
|
||||
```toml
|
||||
[tool.poetry.scripts]
|
||||
ab_solver = "new_proj.main:main"
|
||||
```
|
||||
|
||||
Теперь нам доступна программа `ab_solver` внутри shell.
|
||||
|
||||
```console
|
||||
$ poetry install
|
||||
$ poetry shell
|
||||
$ ab_solver 1 2
|
||||
3
|
||||
```
|
||||
|
||||
Хочешь установить? Пожалуйста.
|
||||
```console
|
||||
$ poetry build
|
||||
Building new_proj (0.1.0)
|
||||
- Building sdist
|
||||
- Built new_proj-0.1.0.tar.gz
|
||||
- Building wheel
|
||||
- Built new_proj-0.1.0-py3-none-any.whl
|
||||
$ pip install "./dist/new_proj-0.1.0-py3-none-any.whl"
|
||||
Processing ./dist/new_proj-0.1.0-py3-none-any.whl
|
||||
Installing collected packages: new-proj
|
||||
Successfully installed new-proj-0.1.0
|
||||
$ ab_solver 1 2
|
||||
3
|
||||
```
|
||||
|
||||
Если запаблишить проект, то у пользователя тоже установится ваша cli-программа.
|
||||
|
||||
А на этом всё.
|
||||
|
||||
До новых встреч.
|
@ -1,479 +0,0 @@
|
||||
---
|
||||
title: Ускоряем Python используя Rust.
|
||||
description: Как встроить Rust в проект на Python.
|
||||
position: 2
|
||||
category: 'Python'
|
||||
---
|
||||
|
||||
# Описание проблемы
|
||||
|
||||
Каким бы Python удобным не был, скоростью он похвастаться никак не может.
|
||||
И для некоторых задач это достаточно критично.
|
||||
|
||||
Например, не так давно у меня была задача достать много информации
|
||||
из файлов логов. Проблема в том, что один лог-файл занимает от 3-4 ГБ. А файлов
|
||||
таких много и информацию достать требуется быстро. Изначальный вариант на Python
|
||||
был написан за минут 15-20, но скорость его работы занимала 30-40 минут на один файл. После переписывания одной функции на Rust, скрипт отработал за 1 минуту.
|
||||
|
||||
Давайте напишем свой проект со встроенной функцией на Rust.
|
||||
|
||||
<br>
|
||||
|
||||
Задача проекта будет следующей:
|
||||
|
||||
Дан файл лога запросов на некоторый сервер. Требуется
|
||||
найти количество запросов к определённому файлу и сумму переданных байт.
|
||||
|
||||
Для демонстрации мы напишем 2 функции, одна будет использовать наивное решение задачи на питоне,
|
||||
вторая будет выполнять парсинг на расте.
|
||||
|
||||
Сам файл лог имеет следующую структуру:
|
||||
```
|
||||
"$line_num" "-" "$method $url" "$current_time" "$bytes_sent"
|
||||
```
|
||||
|
||||
где,
|
||||
* $line_num - номер строчки;
|
||||
* $method - HTTP метод;
|
||||
* $url - URL до файла, содержащий один из сгенерированных идентификаторов;
|
||||
* $current_time - текущее время (время выполнения запроса);
|
||||
* $bytes_sent - сколько байт было отправлено.
|
||||
|
||||
Данный формат логов отчасти подражает формату [G-Core labs](https://gcorelabs.com/support/articles/115000511685/).
|
||||
|
||||
# Проект на Python
|
||||
|
||||
Сначала мы напишем весь функционал на Python.
|
||||
Для этого создадим проект `python-rust` по [гайду из блога](/project-start).
|
||||
|
||||
Для создания CLI я буду использовать [typer](https://pypi.org/project/typer/).
|
||||
|
||||
Весь код проекта доступен в [репозитории](https://github.com/s3rius/blog_examples/tree/master/python-rust).
|
||||
|
||||
```bash
|
||||
$ poetry add typer
|
||||
```
|
||||
|
||||
## Генератор логов
|
||||
|
||||
Для тестов напишем генератор лог-файла.
|
||||
|
||||
Данный генератор должен работать следующим образом.
|
||||
Я указываю сколько строк лог-файла я хочу увидеть,
|
||||
сколько уникальных id файлов использовать и название файла, куда писать.
|
||||
|
||||
Функция же генерирует лог в заданном формате.
|
||||
|
||||
```python{}[python_rust/main.py]
|
||||
import secrets
|
||||
import time
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import typer
|
||||
|
||||
tpr = typer.Typer()
|
||||
|
||||
|
||||
def quote(somthing: Any) -> str:
|
||||
"""
|
||||
Quote string.
|
||||
|
||||
:param somthing: any string.
|
||||
:return: quoted string.
|
||||
"""
|
||||
return f'"{somthing}"'
|
||||
|
||||
|
||||
@tpr.command()
|
||||
def generator( # noqa: WPS210
|
||||
output: Path,
|
||||
lines: int = 2_000_000, # noqa: WPS303
|
||||
ids: int = 1000,
|
||||
) -> None:
|
||||
"""
|
||||
Test log generator.
|
||||
|
||||
:param ids: how many file id's to generate.
|
||||
:param output: output file path.
|
||||
:param lines: how many lines to write, defaults to 2_000_000
|
||||
"""
|
||||
ids_pool = [uuid.uuid4().hex for _ in range(ids)]
|
||||
|
||||
with open(output, "w") as out_file:
|
||||
for line_num in range(lines):
|
||||
item_id = secrets.choice(ids_pool)
|
||||
prefix = secrets.token_hex(60)
|
||||
url = f"GET /{prefix}/{item_id}.jpg"
|
||||
current_time = int(time.time())
|
||||
bytes_sent = secrets.randbelow(800) # noqa: WPS432
|
||||
line = [
|
||||
quote(line_num),
|
||||
quote("-"),
|
||||
quote(url),
|
||||
quote(current_time),
|
||||
quote(bytes_sent),
|
||||
]
|
||||
out_file.write(" ".join(line))
|
||||
out_file.write("\n")
|
||||
typer.secho("Log successfully generated.", fg=typer.colors.GREEN)
|
||||
|
||||
|
||||
@tpr.command()
|
||||
def parser(input_file: Path, rust: bool = False) -> None:
|
||||
"""
|
||||
Parse given log file.
|
||||
|
||||
:param input_file: path of input file.
|
||||
:param rust: use rust parser implementation.
|
||||
"""
|
||||
typer.secho("Not implemented", err=True, fg=typer.colors.RED)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Main program entrypoint."""
|
||||
tpr()
|
||||
|
||||
```
|
||||
|
||||
Для удобства работы добавим в pyproject.toml информацию о командах.
|
||||
|
||||
```toml
|
||||
[tool.poetry.scripts]
|
||||
pyrust = "python_rust.main:main"
|
||||
```
|
||||
|
||||
Теперь мы можем вызывать нашу программу.
|
||||
|
||||
```
|
||||
$ pyrust --help
|
||||
Usage: pyrust [OPTIONS] COMMAND [ARGS]...
|
||||
|
||||
Options:
|
||||
--install-completion [bash|zsh|fish|powershell|pwsh]
|
||||
Install completion for the specified shell.
|
||||
--show-completion [bash|zsh|fish|powershell|pwsh]
|
||||
Show completion for the specified shell, to
|
||||
copy it or customize the installation.
|
||||
--help Show this message and exit.
|
||||
|
||||
Commands:
|
||||
generator Test log generator.
|
||||
parser Parse given log file.
|
||||
```
|
||||
|
||||
Работает отлично.
|
||||
|
||||
С помощью данного генератора сгенерируем файл на 8 милионов строк.
|
||||
|
||||
```bash
|
||||
$ pyrust generator test.log --lines 8000000
|
||||
Log successfully generated.
|
||||
```
|
||||
|
||||
У нас получился файл на 1.5G. Для начального теста этого будет достаточно.
|
||||
|
||||
## Реализация парсера на Python
|
||||
|
||||
Представленный формат разделяет ифнормацию кавычками, поэтому
|
||||
для сплита строк мы будем использовать встроенный в python модуль [shlex](https://docs.python.org/3/library/shlex.html).
|
||||
|
||||
Функция парсинга логов будет выглядить следующим образом:
|
||||
|
||||
|
||||
```python{}[python_rust/py_parser.py]
|
||||
import shlex
|
||||
from pathlib import Path
|
||||
from typing import Dict, Tuple
|
||||
|
||||
|
||||
def parse_python( # noqa: WPS210
|
||||
filename: Path,
|
||||
) -> Dict[str, Tuple[int, int]]:
|
||||
"""
|
||||
Parse log file with python.
|
||||
|
||||
:param filename: log file.
|
||||
:return: parsed data.
|
||||
"""
|
||||
parsed_data = {}
|
||||
with open(filename, "r") as input_file:
|
||||
for line in input_file:
|
||||
spl = shlex.split(line)
|
||||
# Splitting method and actual url.
|
||||
url = spl[2].split()[1]
|
||||
# Splitting url by /
|
||||
# This split will turn this "/test/aaa.png"
|
||||
# into "aaa".
|
||||
file_id = url.split("/")[-1].split(".")[0]
|
||||
file_info = parsed_data.get(file_id)
|
||||
# If information about file isn't found.
|
||||
if file_info is None:
|
||||
downloads, bytes_sent = 0, 0
|
||||
else:
|
||||
downloads, bytes_sent = file_info
|
||||
# Incrementing counters.
|
||||
downloads += 1
|
||||
bytes_sent += int(spl[4])
|
||||
# Saving back.
|
||||
parsed_data[file_id] = (downloads, bytes_sent)
|
||||
return parsed_data
|
||||
|
||||
```
|
||||
|
||||
|
||||
Давайте импортируем её, модифицируем команду парсинга и замерим скорость работы.
|
||||
|
||||
|
||||
```python{}[python_rust/main.py]
|
||||
@tpr.command()
|
||||
def parser(input_file: Path, rust: bool = False) -> None:
|
||||
"""
|
||||
Parse given log file.
|
||||
|
||||
:param input_file: path of input file.
|
||||
:param rust: use rust parser implementation.
|
||||
"""
|
||||
if rust:
|
||||
typer.secho("Not implemented", input_file, color=typer.colors.RED)
|
||||
return
|
||||
else:
|
||||
parsed_data = parse_python(input_file)
|
||||
|
||||
typer.secho(
|
||||
f"Found {len(parsed_data)} files", # noqa: WPS237
|
||||
color=typer.colors.CYAN,
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
Как можно видеть из кода, мы просто подсчитываем итоговое количество найденных файлов.
|
||||
|
||||
Посмотрим сколько времени займёт парсинг нашего сгенерированного файла.
|
||||
|
||||
```bash
|
||||
$ time pyrust parser "test.log"
|
||||
Found 1000 files
|
||||
pyrust parser test.log 2443.42s user 2.10s system 99% cpu 40:59.30 total
|
||||
```
|
||||
|
||||
Обработка данного файла заняла 40 минут. Это довольно печальный результат.
|
||||
Самое время попробовать использовать Rust.
|
||||
|
||||
# Интеграция Rust
|
||||
|
||||
Для интеграции Rust в python код я буду использовать [PyO3](https://github.com/PyO3/pyo3) для биндингов и [maturin](https://github.com/PyO3/maturin) для сборки проекта.
|
||||
|
||||
Начнём с инициализации проекта и установки сборщика. В корневой папке проекта
|
||||
создайте папку проекта с растом.
|
||||
|
||||
<b-message type="is-info" has-icon>
|
||||
|
||||
В теории можно использовать maturin как основной инструмент сборки проекта,
|
||||
но это лишает вас всех прелестей poetry. Поэтому удобнее использовать Rust в
|
||||
подпроекте и указать его как зависимость в списке зависимостей своего проекта.
|
||||
|
||||
</b-message>
|
||||
|
||||
```bash
|
||||
$ cargo new --lib rusty_log_parser
|
||||
```
|
||||
|
||||
Теперь создадим `pyproject.toml` и напишем туда описание нашего python пакета.
|
||||
|
||||
```toml{}[pyproject.toml]
|
||||
[tool.poetry]
|
||||
name = "rusty_log_parser"
|
||||
version = "0.1.0"
|
||||
description = "Log file parser with Rust core"
|
||||
authors = ["Pavel Kirilin <win10@list.ru>"]
|
||||
|
||||
[build-system]
|
||||
requires = ["maturin>=0.12,<0.13"]
|
||||
build-backend = "maturin"
|
||||
|
||||
```
|
||||
|
||||
Также поправим `Cargo.toml`.
|
||||
|
||||
```toml{}[Cargo.toml]
|
||||
[lib]
|
||||
# Название модуля
|
||||
name = "rusty_log_parser"
|
||||
# Обязательный тип крейта, чтобы можно было использовать его
|
||||
# в питоне.
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
# Библиотека биндингов для питона.
|
||||
pyo3 = { version = "0.15.1", features = ["extension-module"] }
|
||||
# Библиотека для сплита. Повторяет функционал shlex.
|
||||
shell-words = "1.0.0"
|
||||
```
|
||||
После этих нехитрых изменений попробуем собрать проект.
|
||||
|
||||
|
||||
```shell
|
||||
$ cd rusty_log_parser
|
||||
$ cargo build
|
||||
```
|
||||
|
||||
Теперь напишем сам парсер логов в Rust.
|
||||
|
||||
```rust{}[rusty_log_parser/src/lib.rs]
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::BufRead;
|
||||
|
||||
use pyo3::prelude::*;
|
||||
|
||||
/// Parse log file in rust.
|
||||
/// This function parses log file and returns HashMap with Strings as keys and
|
||||
/// Tuple of two u128 as values.
|
||||
#[pyfunction]
|
||||
fn parse_rust(filename: &str) -> PyResult<HashMap<String, (u128, u128)>> {
|
||||
let mut result_map = HashMap::new();
|
||||
// Iterating over file.
|
||||
for log in io::BufReader::new(File::open(filename)?).lines().flatten() {
|
||||
// Splitting log string.
|
||||
if let Ok(mut spl) = shell_words::split(&log) {
|
||||
// Getting file id.
|
||||
let file_id_opt = spl.get_mut(2).and_then(|http_req| {
|
||||
// Splitting method and URL.
|
||||
http_req.split(' ').into_iter().nth(1).and_then(|url| {
|
||||
// Splitting by / and getting the last split.
|
||||
url.split('/')
|
||||
.into_iter()
|
||||
.last()
|
||||
// Split file id and extension.
|
||||
.and_then(|item_url| item_url.split('.').into_iter().next())
|
||||
// Turning &str into String.
|
||||
.map(String::from)
|
||||
})
|
||||
});
|
||||
// Getting number of bytes sent.
|
||||
let bytes_sent_opt =
|
||||
spl.get_mut(4)
|
||||
// Parsing string to u128
|
||||
.and_then(|bytes_str| match bytes_str.parse::<u128>() {
|
||||
Ok(bytes_sent) => Some(bytes_sent),
|
||||
Err(_) => None,
|
||||
});
|
||||
if file_id_opt.is_none() || bytes_sent_opt.is_none() {
|
||||
continue;
|
||||
}
|
||||
let file_id = file_id_opt.unwrap();
|
||||
let bytes_sent = bytes_sent_opt.unwrap();
|
||||
|
||||
match result_map.get(&file_id) {
|
||||
Some(&(downloads, total_bytes_sent)) => {
|
||||
result_map.insert(file_id, (downloads + 1, total_bytes_sent + bytes_sent));
|
||||
}
|
||||
None => {
|
||||
result_map.insert(file_id, (1, bytes_sent));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result_map)
|
||||
}
|
||||
|
||||
/// A Python module implemented in Rust. The name of this function must match
|
||||
/// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to
|
||||
/// import the module.
|
||||
#[pymodule]
|
||||
fn rusty_log_parser(_py: Python, m: &PyModule) -> PyResult<()> {
|
||||
// Adding function to the module.
|
||||
m.add_function(wrap_pyfunction!(parse_rust, m)?)?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Это и будет наша функция, которую мы будем вызывать.
|
||||
Теперь добавим `python` обёртку для нашего модуля.
|
||||
|
||||
Для этого в проекте `rusty_log_parser` надо создать
|
||||
папку с тем же названием что и проект. В данном случае
|
||||
это будет `rusty_log_parser`.
|
||||
|
||||
Внутри этой папки мы создадим `__init__.py`, в котором
|
||||
опишем и задокументируем доступные функции модуля.
|
||||
|
||||
```python{}[rusty_log_parser/rusty_log_parser/__init__.py]
|
||||
from pathlib import Path
|
||||
from typing import Dict, Tuple
|
||||
|
||||
from .rusty_log_parser import parse_rust as _parse_rust
|
||||
|
||||
|
||||
def parse_rust(input_file: Path) -> Dict[str, Tuple[int, int]]:
|
||||
"""
|
||||
Parse log file using Rust as a backend.
|
||||
|
||||
:param input_file: log file to parse.
|
||||
:return: Parsed
|
||||
"""
|
||||
return _parse_rust(str(input_file.expanduser()))
|
||||
|
||||
```
|
||||
|
||||
Осталось только добавить свеженаписанный пакет как зависимости нашего проекта.
|
||||
|
||||
Для этого в корневой `pyproject.toml` надо добавить следующую зависимость.
|
||||
|
||||
|
||||
```toml{}[pyproject.toml]
|
||||
[tool.poetry.dependencies]
|
||||
...
|
||||
rusty_log_parser = { path = "./rusty_log_parser" }
|
||||
```
|
||||
|
||||
Теперь мы можем использовать нашу функцию.
|
||||
|
||||
Перепишем нашу функцию парсера таким образом,
|
||||
чтобы она использовала rust, когда был передан
|
||||
соответствующий параметр.
|
||||
|
||||
```python{}[python_rust/main.py]
|
||||
from rusty_log_parser import parse_rust
|
||||
|
||||
...
|
||||
|
||||
@tpr.command()
|
||||
def parser(input_file: Path, rust: bool = False) -> None:
|
||||
"""
|
||||
Parse given log file.
|
||||
|
||||
:param input_file: path of input file.
|
||||
:param rust: use rust parser implementation.
|
||||
"""
|
||||
if rust:
|
||||
parsed_data = parse_rust(input_file)
|
||||
else:
|
||||
parsed_data = parse_python(input_file)
|
||||
|
||||
typer.secho(
|
||||
f"Found {len(parsed_data)} files", # noqa: WPS237
|
||||
color=typer.colors.CYAN,
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
Теперь проведём итоговый замер.
|
||||
|
||||
```bash
|
||||
$ time pyrust parser test.log --rust
|
||||
Found 1000 files
|
||||
pyrust parser test.log --rust 20.44s user 0.35s system 99% cpu 20.867 total
|
||||
```
|
||||
|
||||
Итого виден небольшой выйигрыш во времени в 2423 секунды, из чего следует,
|
||||
что реализация на Rust быстрее всего в 122 раза.
|
||||
|
||||
Данной статьёй я не подталкиваю всех переписывать всё на Rust,
|
||||
а просто ознакамливаю с возможностями. Но если всё же хочется,
|
||||
то можно и попробовать.
|
||||
|
||||
До новых встреч.
|
@ -1,755 +0,0 @@
|
||||
---
|
||||
title: Начало работы с kubernetes
|
||||
description: Как там это всё разворачивать в двух словах.
|
||||
category: DevOps
|
||||
position: 4
|
||||
---
|
||||
|
||||
# Проблема в изучении кубернетес
|
||||
|
||||
Многие люди, кто задавались вопросом "Как начать работать с
|
||||
кубернетес?", сталкивались с тем, что документация крайне
|
||||
большая сложная и нет нормального описания как
|
||||
завернуть маленький проект из одного контейнра в свой кластер или
|
||||
как развернуть сам кластер без боли.
|
||||
|
||||
А всё потому что вся документация нацелена на большие
|
||||
production-ready системы с большим rps и тому подобным.
|
||||
<br>
|
||||
В данной статье я попробую исправить это вселенское
|
||||
недопонимание используя k3s, свой комплюктер и немного знаний по кодингу.
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
# Что такое кубернетес и почему это лучше докера
|
||||
|
||||
Многие ребята, кто хорошо знаком с докером и его
|
||||
возможностями, могут задаваться таким вопросом.
|
||||
Для тех кто в танке, напомню, что докер
|
||||
имеет вариант запуска в режиме кластера.
|
||||
Этот функционал называется [docker swarm](https://docs.docker.com/engine/swarm/).
|
||||
В целом, swarm отдалённо напоминает kubernetes,
|
||||
так как в этом режиме докер худо-бедно умеет
|
||||
автоскейлится и запускаться в кластере,
|
||||
но это всё равно немного не то.
|
||||
|
||||
<b-message type="is-info" has-icon>
|
||||
Также замечу, что кубер активно развивается и поддерживается.
|
||||
огромным количество компаний. А вот docker swarm уже по-немногу
|
||||
умирает и документация по нему не то чтобы супер хороша.
|
||||
</b-message>
|
||||
|
||||
По большей части кубернетес это система,
|
||||
которая будет управлять вашими приложениями,
|
||||
следить за их состоянием и помогать вам в их
|
||||
конфигурации. Кубер умеет очень много, поэтому
|
||||
все интересные способности я в этой статье не смогу осветить,
|
||||
но самую базу попробую рассказать.
|
||||
|
||||
# Из чего состоит кубернетес
|
||||
|
||||
Так как я в этой статье хотел затронуть
|
||||
совсем базовые и практические вещи, то рассматривать
|
||||
мы будем только самые часто используемые компоненты.
|
||||
|
||||
- Container
|
||||
- Pod
|
||||
- Deployment
|
||||
- Service
|
||||
- Ingress
|
||||
- Namespace
|
||||
- Secret
|
||||
- ConfigMap
|
||||
|
||||
А теперь рассмотрим каждый ресурс немного поподробнее.
|
||||
|
||||
## Container
|
||||
|
||||
Контейнеры не являются чем-то специфичным для кубернетес.
|
||||
С контейнерами вы можете быть знакомы из кучи других систем.
|
||||
В контексте кубера они не обладают никакими дополнительными
|
||||
свойствами. Это ровно то же, что и контейнеры `containerd`
|
||||
или те, с которыми вы возились с докером. Ничего нового.
|
||||
|
||||
Собираются контейнеры для кубернетеса ровно тем же образом,
|
||||
что и для докера.
|
||||
|
||||
<b-message type="is-warning" has-icon>
|
||||
|
||||
Важная ремарка. Кубер, начиная с 2021 года, не поддерживает
|
||||
докер как бэкенд. Теперь он будет общаться с
|
||||
containerd напрямую. Это значит, что перед использованием
|
||||
контейнеров собранных на локальной машине надо будет импортировать их
|
||||
в `containerd` используя `ctr image import`.
|
||||
|
||||
Как импортировать образы почитать можно в [этой статье](https://cwienczek.com/2020/06/import-images-to-k3s-without-docker-registry/).
|
||||
|
||||
</b-message>
|
||||
|
||||
## Pod
|
||||
|
||||
<div align="center">
|
||||
<img alt="Pods" style="width: 100%" src="/images/k3s_start/pods.svg">
|
||||
</div>
|
||||
|
||||
Поды - это логически связанные группы контейнеров.
|
||||
Это самая базовая еденица кубернетеса.
|
||||
|
||||
В поде находится от одного до множества контейнеров.
|
||||
Интересная особенность пода в том, что все контейнеры
|
||||
делят один сетевой адрес. Другими словами,
|
||||
если у вас один из контейнеров открыл порт `3000`,
|
||||
то другие контейнеры из пода этот порт использовать не смогут.
|
||||
|
||||
То есть, если вы хотите логически связанные приложения
|
||||
поместить в под, то они могут ходить друг к другу через лупбек
|
||||
адреса. Такие как `localhost` или `127.0.0.1`.
|
||||
|
||||
## Deployment
|
||||
|
||||
Поды это круто, но есть одно но. Данные объекты неизменяемые
|
||||
и сам под скейлить вручную занятие сомнительное.
|
||||
|
||||
Конечно, никто не мешает создать вам вручную [ReplicaSet](https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/)
|
||||
и самому крутить там нужные значения. Но в среднем
|
||||
вам с этим возиться не очень то хочется.
|
||||
|
||||
<br>
|
||||
|
||||
Deployment нужен именно для описания подов и создания
|
||||
некоторых ресурсов, нужных для скейлинга.
|
||||
Также с помощью деплойментов можно делать откаты приложения через
|
||||
механиз роллбеков. Я это рассматривать не буду. В этой статье только база.
|
||||
|
||||
## Service
|
||||
|
||||
Сервис - это ресурс, с помощью которого поды могут общаться
|
||||
между собой. По факту сервис описывает, какие порты пода
|
||||
открыты и перенаправляет трафик на них.
|
||||
|
||||
Представьте себе под, который просто проксирует весь трафик с одного
|
||||
порта на какой-нибудь порт пода. Это и есть Service.
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Выглядит сервис примерно так как показано на кртинке выше.
|
||||
Он направляет весь входной трафик с указанного порта на
|
||||
порты подов.
|
||||
|
||||
Также сервис выступает как балансировщик.
|
||||
|
||||
<b-message type="is-info" has-icon>
|
||||
Если вы хотите сделать запрос от одного пода до дргого, внутри кластера, то
|
||||
вам придётся использовать сервис.
|
||||
<br>
|
||||
Если вы попробуете сделать запрос напрямую по IP пода, то у вас, конечно же,
|
||||
получится, но это довольно странная идея из-за того, что при пересоздании
|
||||
пода у него может обновится IP внутри кластера. Поэтому лучше использовать сервисы. Также они потребуются для ингресса.
|
||||
</b-message>
|
||||
|
||||
## Ingress
|
||||
|
||||
Ингресс - это сервис описывающий куда пускать трафик,
|
||||
который поступает снаружи кластера.
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Принцип работы ингресса следующий:
|
||||
|
||||
Вы указываете хост ингресса и различные пути.
|
||||
В зависимости от входящего запроса ингресс выбирает в какой сервис
|
||||
направить запрос и в какой порт.
|
||||
|
||||
Настройка ингресса достаточно гибкая и я не думаю,
|
||||
что вы можете столкнуться с какими либо проблемами.
|
||||
|
||||
## Namespace
|
||||
|
||||
Неймспейсы это логические разделители уровня доступа.
|
||||
|
||||
Я предпочитаю использовать разные неймспейсы под различные логические группы приложений.
|
||||
|
||||
Например, в моём кластере есть отдельные неймспейсы для каждого приложения.
|
||||
|
||||
Скажем, у меня есть проект, в котором есть база данных и веб сервер.
|
||||
Я держу их в одном неймспейсе, который называется также, как и приложени.
|
||||
|
||||
А вот телеграм боты -- приложения достаточно лёгкие.
|
||||
Поэтому у меня есть неймспейс со всеми телеграм ботами.
|
||||
|
||||
Эта штука позволяет просто лучше организовать свой кубернетес кластер.
|
||||
|
||||
Также неймспейсы очень полезны для ограничивания возможностей
|
||||
конкретного пользователя.
|
||||
Например, вы можете создать правило, которое будет разрешать пользователю
|
||||
смотреть на поды в каком-то неймспейсе, но при этом ему нельзя будет
|
||||
что либо изменять.
|
||||
|
||||
## Secret и ConfigMap
|
||||
|
||||
Данные ресурсы нужны только чтобы хранить
|
||||
внутри кластера какую-нибудь информацию.
|
||||
|
||||
Например вы можете сохранить в ConfigMap
|
||||
переменные среды и потом использовать их в подах в одном неймспейсе.
|
||||
|
||||
В секреты обычно кидают сертификаты или какие-нибудь ключи.
|
||||
Но так как секреты не особо секретные принято использовать [Vault](https://www.vaultproject.io/docs/platform/k8s). Но так как эта статья затрагивает только
|
||||
основы рассматривать развертку и настройку Vault мы не будем, ну и также HashiCorp
|
||||
всё довольно подробно расписали сами.
|
||||
|
||||
# Как развернуть k8s у себя
|
||||
|
||||
Для локального кубера есть пара вариантов.
|
||||
|
||||
- k3s (Недоступен под Windows)
|
||||
- minikube
|
||||
|
||||
На первый взгляд minikube может показаться лучшим вариантом.
|
||||
И он действительно хорош тем, что его легко развернуть и почистить после
|
||||
своих экспериментов. Однако, там есть проблемы с ингрессами.
|
||||
По факту они не работают и там надо окольными путями получать
|
||||
адреса.
|
||||
|
||||
<br>
|
||||
|
||||
k3s - это легковесная production-ready реализация k8s. Ingress у него работают
|
||||
отлично, поэтому я буду использовать его.
|
||||
|
||||
<br>
|
||||
|
||||
Я не буду зацикливаться на установке `minikube`, так как
|
||||
он прост в установке и первоначальной настройке. Почитать подробнее можно в
|
||||
[официальном гайде от minikube](https://minikube.sigs.k8s.io/docs/start/).
|
||||
|
||||
С `k3s` всё немного посложнее, но тоже достаточно просто, если немного разобраться. Первоначальная установка описана в [официальной доке k3s](https://rancher.com/docs/k3s/latest/en/installation/install-options/).
|
||||
|
||||
## Подключение кластера
|
||||
|
||||
После установки `kubectl` в домашней дериктории должен был быть
|
||||
сгенерирован файл `.kube/config`. Этот файл содержит данные для
|
||||
подключения к различным кластерам. `minikube` Сам добавляет
|
||||
ключи для подключения к .kube/config. `K3S` никак не изменяет `.kube/config`, поэтому надо будет это сделать вручную.
|
||||
|
||||
Для того, чтобы это сделать сначала разберёмся как выглядит конфиг.
|
||||
|
||||
```yaml{}[.kube/config]
|
||||
apiVersion: v1
|
||||
kind: Config
|
||||
preferences: {}
|
||||
# Массив кластеров.
|
||||
# Каждый элемент -- данные для подключения
|
||||
# Тут также есть названия для каждого кластера.
|
||||
clusters:
|
||||
- name: hyper
|
||||
cluster:
|
||||
certificate-authority-data: DATA+OMITTED
|
||||
server: https://192.168.1.55:6443
|
||||
- name: k3s-local
|
||||
cluster:
|
||||
certificate-authority-data: DATA+OMITTED
|
||||
server: https://127.0.0.1:6443
|
||||
# Массив данных пользователя.
|
||||
# Тут указаны пользователи с
|
||||
# различными сертификатами.
|
||||
# Обычно для разных серверов у вас будут
|
||||
# Различные данные для входа.
|
||||
users:
|
||||
- name: hyper-s3rius
|
||||
user:
|
||||
client-certificate-data: REDACTED
|
||||
client-key-data: REDACTED
|
||||
- name: k3s-user
|
||||
user:
|
||||
client-certificate-data: REDACTED
|
||||
client-key-data: REDACTED
|
||||
# Массив контекстов.
|
||||
# Контекст - это связующее звено
|
||||
# между кластероами и пользователями.
|
||||
contexts:
|
||||
- context:
|
||||
cluster: hyper
|
||||
user: hyper-s3rius
|
||||
name: hyper
|
||||
- context:
|
||||
cluster: k3s-local
|
||||
user: k3s-user
|
||||
name: k3s
|
||||
# Текущий контекст указывает какой
|
||||
# контекст использовать по умолчанию.
|
||||
current-context: "k3s"
|
||||
```
|
||||
|
||||
Для работы с кубером из командной строки
|
||||
можно использовать `kubectl`. Чтобы сменить контекст в любой команде вы
|
||||
можете передать параметр `--context $CONTEXT_NAME`, где `$CONTEXT_NAME` это название контекста.
|
||||
|
||||
Чтобы достать данные для подключения к `k3s` надо посмотреть его конфиг
|
||||
и скопировать данные. Либо выставить переменную среды `KUBECONFIG`, которая будет
|
||||
указывать конфиг k3s. Конфиг подключения `k3s` лежит в файле `/etc/rancher/k3s/k3s.yaml`.
|
||||
|
||||
Можете выполнить команду, которая будет просить kubectl использовать указанный конфиг:
|
||||
`export KUBECONFIG=/etc/rancher/k3s/k3s.yaml`
|
||||
|
||||
<br>
|
||||
|
||||
Либо скопируйте нужные данные для подключения себе в `.kube/config`.
|
||||
|
||||
После настройки выполните команду и проверьте что вам вернулось
|
||||
что-то подобное.
|
||||
|
||||
```bash
|
||||
$ kubectl --context "my-context" get pods
|
||||
No resources found in default namespace.
|
||||
```
|
||||
|
||||
Это значит, что никаких ресурсов пока в
|
||||
кластере нет. Это мы исправим чуть позже. Пока что можно сказать,
|
||||
что подключение прошло успешно.
|
||||
Если же у вас выпадает ошибка, например такая:
|
||||
|
||||
```
|
||||
The connection to the server localhost:8080 was refused - did you specify the right host or port?
|
||||
```
|
||||
|
||||
То либо запустите кластер
|
||||
|
||||
```
|
||||
sudo systemctl start k3s.service
|
||||
```
|
||||
|
||||
Либо у вас неверные данные для входа.
|
||||
Перепроверьте свой `.kube/config`.
|
||||
|
||||
### Мониторинг кластера
|
||||
|
||||
Для просмотра ресурсов и управления кластером используется `kubectl`.
|
||||
Но для того, чтобы с ней работать нужно получше понять, что вообще в кластере есть.
|
||||
|
||||
Для этого я советую использовать [Lens](https://k8slens.dev/). Это крайне
|
||||
удобный интерфейс для управления своим кластером.
|
||||
Также там есть очень клёвая настройка, которая сама включит мониторинг
|
||||
потребления памяти и процессора для всех подов и кластера в общем.
|
||||
|
||||
На локальной машине это не имеет смысла,
|
||||
а вот в проде было бы очень полезно.
|
||||
|
||||
Выглядит Lens примерно так:
|
||||
|
||||
<div align="center" style="margin-top:15px;">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Для изучения **крайне настоятельно рекомендую** настроить Lens.
|
||||
|
||||
# Ваше первое приложение в кластере.
|
||||
|
||||
Вы можете следовать статье, а можете подсмотреть весь код в [репозитории](https://github.com/s3rius/blog_examples/tree/master/req_counter).
|
||||
|
||||
### Сервер
|
||||
|
||||
Давайте создадим своё первое приложение.
|
||||
|
||||
Для этого я буду использовать [express.js](https://expressjs.com/), так как он крайне популярен и прост.
|
||||
|
||||
Для этого сначала напишем сам сервер. Я буду использовать yarn, но можете и npm,
|
||||
сути не поменяет.
|
||||
|
||||
Создайте какую-нибудь папку, где вы будете эксперементировать, откройте в ней ваш любимый тектовый редактор и просто копируйте файлы ниже.
|
||||
|
||||
```json{}[package.json]
|
||||
{
|
||||
"name": "req_counter",
|
||||
"version": "1.0.0",
|
||||
// Указан модуль,
|
||||
// чтобы использовать нормальны импорты,
|
||||
// а не require.
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
// Скрипт старта сервера.
|
||||
"server": "node index.js"
|
||||
},
|
||||
// Зависимости проекта.
|
||||
"dependencies": {
|
||||
"express": "^4.17.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
И само приложение
|
||||
|
||||
```js{}[index.js]
|
||||
import express from "express";
|
||||
import { hostname } from "os";
|
||||
import { argv, exit } from "process";
|
||||
|
||||
// Серверное приложение.
|
||||
const app = express();
|
||||
|
||||
// Глобальный счётчик входящих запросов.
|
||||
let requests = 0;
|
||||
|
||||
// Обработка входящего запроса.
|
||||
app.get("*", (req, res) => {
|
||||
// Увеличиваем глобальный счётчик запросов.
|
||||
requests += 1;
|
||||
// Логгируем входящий запрос.
|
||||
console.log(`${req.method} ${req.url}`);
|
||||
// Возвращаем информацию о текущем хосте и количестве запросов.
|
||||
res.json({
|
||||
requests: requests,
|
||||
hostname: hostname(),
|
||||
});
|
||||
});
|
||||
|
||||
// Аргументы командной строки.
|
||||
// Напрмиер yarn run server 127.0.0.1 8000
|
||||
const args = argv.slice(2);
|
||||
|
||||
// Если передано неверное количество аргументов.
|
||||
if (args.length != 2) {
|
||||
console.error("Usage: yarn run server {host} {port}");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
// Простейший "парсинг" аргументов командной строки.
|
||||
const host = args[0];
|
||||
const port = args[1];
|
||||
|
||||
// Старт сервера.
|
||||
app.listen(port, host, () => {
|
||||
console.log(`Server listening at http://${host}:${port}`);
|
||||
});
|
||||
```
|
||||
|
||||
Это всё. Сервер готов.
|
||||
|
||||
<hr>
|
||||
Протестируем запуск сервера, выполнив команду ниже и открыв в
|
||||
своём любимом браузере http://localhost:8080.
|
||||
|
||||
```
|
||||
yarn run server 0.0.0.0 8080
|
||||
```
|
||||
|
||||
У меня всё работает и успешно отдаётся нужная информация.
|
||||
|
||||
```json
|
||||
{
|
||||
"requests": 1,
|
||||
"hostname": "s3rius-pc"
|
||||
}
|
||||
```
|
||||
|
||||
### Docker образ
|
||||
|
||||
Теперь создадим докер образ приложения.
|
||||
|
||||
Добавим `.dockerignore`, чтобы игнорировать ненужные файлы во время сборки образа.
|
||||
|
||||
```gitignore{}[.dockerignore]
|
||||
node_modules/
|
||||
```
|
||||
|
||||
И добавим в проект `Dockerfile` для описания самого процесса сборки.
|
||||
|
||||
```dockerfile{}[Dockerfile]
|
||||
FROM node:17-alpine
|
||||
|
||||
WORKDIR /app
|
||||
COPY . /app/
|
||||
|
||||
RUN yarn install
|
||||
|
||||
CMD [ "yarn", "run", "server", "0.0.0.0", "8000"]
|
||||
```
|
||||
|
||||
Давайте соберём и запустим проект в контейнере.
|
||||
|
||||
```bash
|
||||
docker build --tag="s3rius/req-counter-express:latest" .
|
||||
|
||||
docker run --rm -it -p 3400:8000 "s3rius/req-counter-express:latest"
|
||||
```
|
||||
|
||||
Можете проверить, что приложение работает успешно, открыв в браузере http://localhost:3400.
|
||||
|
||||
У меня в ответ пришло то же сообщение. Только, как можно заметить,
|
||||
`hostname` поменялся. На самом деле контейнеры используют свои hostname
|
||||
отличные от `hostname` локальной машины.
|
||||
|
||||
```json
|
||||
{
|
||||
"requests": 10,
|
||||
"hostname": "8f23adadc640"
|
||||
}
|
||||
```
|
||||
|
||||
Вариантов как положить это приложение в K8S несколько.
|
||||
|
||||
- Вы можете запушить собранное приложение в [Docker HUB](https://hub.docker.com/) и использовать его.
|
||||
- Можете использовать мой образ `s3rius/req-counter-express:latest`
|
||||
- Сохранить собранный образ как tar файл и импортировать его в containerd напрямую.
|
||||
Как это сделать почитать можно в [этой статье](https://cwienczek.com/2020/06/import-images-to-k3s-without-docker-registry/).
|
||||
|
||||
### Деплой в k8s
|
||||
|
||||
Создайте папку `kube` в папке проекта и теперь мы будем работать в ней.
|
||||
Все ресурсы будут описаны yaml-файлами.
|
||||
|
||||
```yaml{}[kube/deployment.yml]
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
# метаданные самого деплоймента.
|
||||
metadata:
|
||||
name: req-counter-deployment
|
||||
spec:
|
||||
# Количество реплик пода.
|
||||
replicas: 1
|
||||
# Селектор, который выбирает
|
||||
# какие поды принадлежат этому деплойменту.
|
||||
selector:
|
||||
matchLabels:
|
||||
app: req-counter
|
||||
# Шаблон пода,
|
||||
# который будет использоваться при
|
||||
# маштабировании.
|
||||
template:
|
||||
# Метаданные пода.
|
||||
# тут обычно помещаются лейблы,
|
||||
# с помощью которых деплоймент идентифицирует
|
||||
# свои поды.
|
||||
metadata:
|
||||
labels:
|
||||
app: req-counter
|
||||
spec:
|
||||
# Масссив контейнеров
|
||||
containers:
|
||||
# Название контейнера внутри пода.
|
||||
- name: req-counter-app
|
||||
# Образ приложения.
|
||||
image: s3rius/req-counter-express:latest
|
||||
# Ресурсы требуемые для работы приложения.
|
||||
resources:
|
||||
# Минимальное количество ресурсов,
|
||||
# которое кластер гарантированно предоставит приложению.
|
||||
# Также данные значения используются для того,
|
||||
# чтобы выяснить на какой ноде запускать приложение.
|
||||
requests:
|
||||
memory: "50Mi"
|
||||
cpu: "30m"
|
||||
# Максимально возможные значения приложения.
|
||||
# Если приложение выйдет за лимиты,
|
||||
# то кубер убьёт приложение.
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "100m"
|
||||
# Порты на которых открыты приложения.
|
||||
ports:
|
||||
- containerPort: 8000
|
||||
protocol: TCP
|
||||
```
|
||||
|
||||
Теперь опишем сервис для управления трафиком.
|
||||
|
||||
```yaml{}[kube/service.yml]
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
# Метадата сервиса.
|
||||
metadata:
|
||||
name: req-counter-service
|
||||
spec:
|
||||
# Селектор подов,
|
||||
# которым будет пускаться трафик.
|
||||
# Трафик может идти в любой под,
|
||||
# который матчит данному селектору.
|
||||
selector:
|
||||
app: req-counter
|
||||
# Порты для проксирования соединений.
|
||||
ports:
|
||||
# Порт сервиса
|
||||
- port: 80
|
||||
# Порт пода, куда будет идти трафик дальше.
|
||||
targetPort: 8000
|
||||
```
|
||||
|
||||
И в последнюю очередь опишем наш ингрес.
|
||||
|
||||
```yaml{}[kube/ingress.yml]
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
# Метаданные ингресса.
|
||||
metadata:
|
||||
name: req-counter-ingress
|
||||
labels:
|
||||
name: req-counter-ingress
|
||||
spec:
|
||||
# Правила роутинга.
|
||||
rules:
|
||||
# Требуемый хост.
|
||||
- host: req-counter.local
|
||||
http:
|
||||
paths:
|
||||
# Тип пути Prefix значит,
|
||||
# что все запросы, которые начинаются c
|
||||
# ${path} будут матчится тут.
|
||||
- pathType: Prefix
|
||||
# Сам путь.
|
||||
path: "/"
|
||||
backend:
|
||||
service:
|
||||
# Название нашего сервиса.
|
||||
name: req-counter-service
|
||||
# Порт сервиса, куда перенаправлять входящий трафик.
|
||||
port:
|
||||
number: 80
|
||||
```
|
||||
|
||||
Перед тем, как создать все описанные ресурсы создадим неймспейс.
|
||||
|
||||
```bash
|
||||
$ kubectl --context k3s create namespace req-couter-ns
|
||||
namespace/req-couter-ns created
|
||||
```
|
||||
|
||||
После того, как неймспейс создан самое время сделать последний штрих.
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
```bash
|
||||
$ kubectl --context k3s --namespace req-couter-ns apply -f ./kube/
|
||||
deployment.apps/req-counter-deployment created
|
||||
ingress.networking.k8s.io/req-counter-ingress created
|
||||
service/req-counter-service created
|
||||
```
|
||||
|
||||
Готово. Теперь вы можете зайти в lens, выбрать свой кластер из списка
|
||||
и посмотреть как там поживает ваше приложение.
|
||||
|
||||
Также не забудьте указать неймспейс, в который вы деплоили приложение.
|
||||
|
||||
Выглядит это чудо примерно так:
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Также можно использовать команду и вывести всё себе в терминал.
|
||||
|
||||
```
|
||||
$ kubectl --context k3s --namespace req-couter-ns get all
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
pod/req-counter-deployment-764476db97-dt2tc 1/1 Running 0 8m11s
|
||||
|
||||
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
|
||||
service/req-counter-service ClusterIP 10.43.50.23 <none> 80/TCP 8m11s
|
||||
|
||||
NAME READY UP-TO-DATE AVAILABLE AGE
|
||||
deployment.apps/req-counter-deployment 1/1 1 1 8m11s
|
||||
|
||||
NAME DESIRED CURRENT READY AGE
|
||||
replicaset.apps/req-counter-deployment-764476db97 1 1 1 8m11s
|
||||
```
|
||||
|
||||
### Маштабирование
|
||||
|
||||
В рамках демонстрации давате поменяем значение
|
||||
`replicas` в нашем файле деплоймента до `3`.
|
||||
|
||||
```yaml{}[kube/deployment.yml]
|
||||
...
|
||||
spec:
|
||||
# Количество реплик пода.
|
||||
replicas: 3
|
||||
...
|
||||
```
|
||||
|
||||
После изменения просто ещё раз вызовите команду apply.
|
||||
|
||||
```bash
|
||||
$ kubectl --context k3s --namespace req-couter-ns apply -f ./kube/
|
||||
deployment.apps/req-counter-deployment configured
|
||||
ingress.networking.k8s.io/req-counter-ingress unchanged
|
||||
service/req-counter-service unchanged
|
||||
```
|
||||
|
||||
Как можно видеть, изменился только наш `Deployment`.
|
||||
Остальные ресурсы остались нетронутыми.
|
||||
|
||||
Давайте посмотрим на поды в нашем неймспейсе.
|
||||
|
||||
```
|
||||
$ kubectl --context k3s --namespace req-couter-ns get pods
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
req-counter-deployment-764476db97-dt2tc 1/1 Running 0 13m
|
||||
req-counter-deployment-764476db97-tdjrb 1/1 Running 0 69s
|
||||
req-counter-deployment-764476db97-x28fr 1/1 Running 0 69s
|
||||
```
|
||||
|
||||
Как видно, всё правильно. Теперь у нас 3 пода
|
||||
нашего приложения.
|
||||
|
||||
|
||||
Теперь можно выполнить кучу запросов по адресу http://req-counter.local/
|
||||
и получить балансировку между подами из коробки, без дополнительных
|
||||
конфигураций.
|
||||
|
||||
Если у вас не получается найти адрес. Добавьте данный хост себе в
|
||||
`/etc/hosts` на линуксе или в `C:\Windows\System32\drivers\etc\hosts` на windows,
|
||||
дописав в конец файла следующее:
|
||||
|
||||
```[/etc/hosts]
|
||||
127.0.0.1 req-couter.local
|
||||
```
|
||||
|
||||
И теперь вы можете открыть в браузере своё приложение и увидеть,
|
||||
как кубернетес заботливо направляет трафик туда, куда вы хотите.
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
|
||||
### Как мне очистить мой кластер?
|
||||
|
||||
Всё очень просто.
|
||||
|
||||
Так как у нас имеются описния ресурсов, то мы
|
||||
можем удалить всё сразу используя команду
|
||||
|
||||
```bash
|
||||
$ kubectl --context k3s --namespace req-couter-ns delete -f ./kube/
|
||||
deployment.apps "req-counter-deployment" deleted
|
||||
ingress.networking.k8s.io "req-counter-ingress" deleted
|
||||
service "req-counter-service" deleted
|
||||
```
|
||||
|
||||
Таким образом k8s удалит все описанные ресурсы из вашего кластера.
|
||||
|
||||
До новых встреч.
|
@ -1,471 +0,0 @@
|
||||
---
|
||||
title: Traefik - роутинг просто.
|
||||
description: Изучите как использовать traefik.
|
||||
position: 2
|
||||
category: DevOps
|
||||
---
|
||||
|
||||
# Traefik - роутинг просто
|
||||
|
||||
Сегодня я бы хотел рассказать о такой клёвой штуке, как [Traefik](https://traefik.io/traefik/). Не так давно я перевёл все сервисы своего сервера на traefik и это буквально сделало жизнь проще. Сегодня я расскажу вам зачем он нужен, как его настроить, и покажу на примере, как именно он помогает. По факту, это nginx на стероидах, в плане конфигурации, потому что в traefik за тебя сделано гораздо больше.
|
||||
|
||||
# Что такое traefik
|
||||
|
||||
Traefik - это система, которая позволяет настроить маппинг между доменными именами и конкретными приложениями. Допустим, у вас есть контейнер frontend-приложения, которое вы хотели бы разместить на домене `myapp.com`, и также у вас есть контейнер backend-приложения, которое вы бы хотели разместить на домене `api.mayapp.com`. [Traefik](https://traefik.io/traefik/) поможет вам это сделать без лишних файлов конфигурации.
|
||||
|
||||
## Сравнение с другими инструментами
|
||||
|
||||
Если бы вы решили сделать это через [Nginx](https://www.nginx.com/), то вы бы создали новые файлы конфигураций под каждый домен, в папочке с конфигурациями для всех доменов, положили куда-нибудь сертификат от домена `*myapp.com` и подключали бы его вручную в каждом файле доменов. А если бы вам надо было увеличить количество контейнеров отдельного приложения, вы бы указывали сервера в директиве `upstream` и перечисляли там адреса инстансов приложения вручную.
|
||||
|
||||
Любители [Apache HTTP Server](https://httpd.apache.org/) на этом моменте обычно берут дробовик и разносят себе голову хорошим зарядом дроби.
|
||||
|
||||
А господа, которые используют [Traefik](https://traefik.io/traefik/), просто дописывают лейблы контейнеру в `docker-compose.yml` и идут дальше дегустировать вино.
|
||||
|
||||
<b-message type="is-info" has-icon>
|
||||
Если же ваше приложение построено на вебсокетах, то тут уже фанаты Nginx тянутся за дробовиком. Ну а для ценителей traefik ничего не меняется, ведь в нём встроена поддержка HTTP/2.0.
|
||||
</b-message>
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
## Архитектура
|
||||
|
||||
В официальной документации зарисована следующая схема работы traefik:
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
Как можно видеть по данному изображению, есть 5 основных составляющих traefik, а именно:
|
||||
|
||||
- entrypoints
|
||||
- routers
|
||||
- rules (часть routers)
|
||||
- middlewares (чать routers)
|
||||
- services
|
||||
|
||||
### Entrypoint
|
||||
|
||||
Являются основными слушателями входящих соединений. Через них проходит весь трафик. Если попробовать объяснить в двух словах: "Порт, на который должно прийти входящее соединение", - вполне себе неплохое объяснение. В данном туториале я создам 2 `entrypoint`, которые будут слушать на http и https.
|
||||
|
||||
### Routers, Rules и Middlewares
|
||||
|
||||
Роутер - связующее звено между `entrypoint` и сервисом, куда нужно направить трафик. Роутер хранит в себе информацию, куда направить входящий трафик, и
|
||||
правила, по которым можно определить, пускать ли трафик дальше.
|
||||
|
||||
Rules - это и есть те самые правила, которые определяют, через какой роутер пустить трафик.
|
||||
Допустим, у вас в конфиге есть следующие правила:
|
||||
|
||||
```
|
||||
Host(`myapp.com`) && (PathPrefix(`/api`) || PathPrefix(`/memes`))
|
||||
```
|
||||
|
||||
Это можно читать как "Этот роутер пустит в моё приложение, если запрос пришёл на хост `myapp.com` и путь запроса начинается с `/api` или `/memes`"
|
||||
|
||||
Довольно просто, не так ли?
|
||||
|
||||
Ну а middlewares - это некоторая логика, что нужно выполнить с запросом до того, как он попадёт в ваше приложение. В этой статье я не буду рассказывать про существующие `middlewares`, тут потребуется самостоятельное ознакомление с [документацией](https://doc.traefik.io/traefik/middlewares/overview/).
|
||||
|
||||
# Конфигурация
|
||||
|
||||
<b-message type="is-info" has-icon>
|
||||
В данной статье я буду использовать Docker для конфигурации traefik.
|
||||
Вы, конечно же, можете использовать локально установленную версию, всё будет работать, как часы.
|
||||
</b-message>
|
||||
|
||||
Для нашего основного traefik на сервере мы создадим небольшой docker-compose.yml, файл конфига и папочку с сертификатами.
|
||||
Структура будет следующая:
|
||||
|
||||
```
|
||||
.
|
||||
├── certs
|
||||
│ ├── local.key
|
||||
│ └── local.pem
|
||||
├── config.toml
|
||||
└── docker-compose.yml
|
||||
```
|
||||
|
||||
Теперь разберем каждый файл по порядку.
|
||||
Вот основной docker-compose нашего traefik.
|
||||
|
||||
```yaml{}[docker-compose.yml]
|
||||
---
|
||||
version: '3.7'
|
||||
|
||||
services:
|
||||
traefik-proxy:
|
||||
# The official v2.0 Traefik docker image
|
||||
image: traefik:v2.4.8
|
||||
container_name: traefik_main
|
||||
command:
|
||||
# Включает прослушивание докера на новые события и следит за лейблами контейнеров.
|
||||
- --providers.docker=true
|
||||
# Отключает автоматическое создание роутеров ко всем контейнерам на сервере.
|
||||
- --providers.docker.exposedbydefault=false
|
||||
# Сеть докера, по которой будет выполнятся подключение к контейнерам.
|
||||
- --providers.docker.network=traefik-shared
|
||||
# Папка с конфигурационным файлом.
|
||||
- --providers.file.directory=/etc/traefik/dynamic
|
||||
# Включает отслеживание изменений файла конфигурации.
|
||||
- --providers.file.watch=true
|
||||
# Создаёт entrypoint с названием http и слушает 80 порт.
|
||||
- --entrypoints.http.address=:80
|
||||
# Создаёт entrypoint с названием https и слушает 443 порт.
|
||||
- --entrypoints.https.address=:443
|
||||
ports:
|
||||
# The HTTP port
|
||||
- "80:80"
|
||||
# The HTTPS port
|
||||
- "443:443"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- traefik-shared
|
||||
environment:
|
||||
MAIN_HOST: 192.168.1.89
|
||||
volumes:
|
||||
# Обязательный вольюм. Так траефик может слушать
|
||||
# события происходящие в демоне докера.
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
# Вольюм с файлом конфигурации.
|
||||
- ./config.toml:/etc/traefik/dynamic/traefik.toml
|
||||
# Папка сертификатов.
|
||||
- ./certs:/etc/certs/
|
||||
|
||||
networks:
|
||||
# Сетка, внутри которой будут находится приложения.
|
||||
traefik-shared:
|
||||
name: traefik-shared
|
||||
|
||||
```
|
||||
|
||||
Вот какие параметры я передаю в `traefik-cli`.
|
||||
|
||||
Конечно, вы всегда можете глянуть `traefik --help` и подобрать себе желаемые параметры.
|
||||
|
||||
Также из docker-compose файла видно, что я создал докер сеть `traefik-shared`, которую в дальнейшем буду использовать на всех контейнерах, которым требуется свой домен.
|
||||
|
||||
Далее следует папка с сертификатами. Для реальных доменов я использую cloudflare и скачиваю сертификаты и ключи с панели администратора. Также не забываю выставлять мод шифрования на strict.
|
||||
|
||||

|
||||
|
||||
Для генерации локальных сертификатов я использую тулу [mkcert](https://github.com/FiloSottile/mkcert).
|
||||
|
||||
Для любого локального домена я делаю что-то типа:
|
||||
|
||||
```bash
|
||||
mkcert "*.local"
|
||||
mv _wildcard.local-key.pem local.key
|
||||
mv _wildcard.local.pem local.pem
|
||||
```
|
||||
|
||||
И помещаю это в папочку `certs` рядом с `docker-compose.yml`.
|
||||
|
||||
После того как я создал все нужные сертефикаты для всех доменов, их надо указать в файле `config.toml` в папочке traefik.
|
||||
|
||||
Вот пример:
|
||||
|
||||
```toml{}[config.toml]
|
||||
[tls.options]
|
||||
[tls.options.default]
|
||||
# Эта опция вообще для strict cloudflare tls encryption,
|
||||
# Но я включаю её и на локальных доменах.
|
||||
sniStrict = true
|
||||
|
||||
[[tls.certificates]]
|
||||
# Я тут указываю /etc/certs, потому что в docker-compose
|
||||
# у нас volume на эту папку.
|
||||
certFile = "/etc/certs/local.pem"
|
||||
keyFile = "/etc/certs/local.key"
|
||||
```
|
||||
|
||||
<b-message type="is-info" has-icon>
|
||||
Для добавления новых сертификатов в данный файл достаточно добавить:
|
||||
</b-message>
|
||||
|
||||
```toml
|
||||
[[tls.certificates]]
|
||||
certFile = "/etc/certs/<certFile>"
|
||||
keyFile = "/etc/certs/<keyFile>"
|
||||
```
|
||||
|
||||
После этого вы можете запускать traefik и наслаждаться доменами для ваших контейнеров.
|
||||
|
||||
# Запуск приложений
|
||||
|
||||
Теперь сконфигурируем приложение таким образом, чтобы к нему можно было обращаться через доменное имя.
|
||||
|
||||
Для примера возьмем мелкое приложение на nodejs со следующей структурой проекта:
|
||||
|
||||
```
|
||||
.
|
||||
├── docker-compose.yml
|
||||
├── Dockerfile
|
||||
├── .dockerignore
|
||||
├── index.js
|
||||
├── package.json
|
||||
└── yarn.lock
|
||||
```
|
||||
|
||||
```js{}[index.js]
|
||||
const express = require('express')
|
||||
|
||||
let req_count = 0;
|
||||
const hostname = process.env.HOSTNAME;
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
app = express();
|
||||
|
||||
app.get("/", (req, res) => {
|
||||
console.log(`GET / ${hostname}`)
|
||||
res.send({request_num: req_count++, host: hostname})
|
||||
})
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server is listening on port ${PORT}`);
|
||||
});
|
||||
```
|
||||
|
||||
```json{}[package.json]
|
||||
{
|
||||
"name": "express-test",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"author": "s3rius",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"runserver": "node index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.17.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```dockerfile{}[Dockerfile]
|
||||
FROM node:16-alpine3.11
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json yarn.lock /app/
|
||||
|
||||
RUN yarn install
|
||||
|
||||
COPY . /app/
|
||||
|
||||
ENTRYPOINT ["yarn", "run"]
|
||||
```
|
||||
|
||||
```yaml{}[docker-compose.yml]
|
||||
---
|
||||
version: '3.7'
|
||||
|
||||
|
||||
services:
|
||||
server:
|
||||
build: .
|
||||
labels:
|
||||
# Включить поддержку роутинга через traefik.
|
||||
- traefik.enable=true
|
||||
# Поставить правило роутинга, если Host запроса равен test_node.local.
|
||||
- traefik.http.routers.test_node.rule=Host(`test_node.local`)
|
||||
# Слушать на entrypoint http (80 порт, это было объявлено в параметрах запуска traefik).
|
||||
- traefik.http.routers.test_node.entrypoints=http
|
||||
# Сервис, связанный с роутером test_node.
|
||||
- traefik.http.routers.test_node.service=node_test
|
||||
# Порт, куда направлять запросы в сервис node_test.
|
||||
- traefik.http.services.node_test.loadbalancer.server.port=3000
|
||||
command: runserver
|
||||
networks:
|
||||
- traefik-shared
|
||||
|
||||
|
||||
networks:
|
||||
# Докер сеть, в которой находится traefik.
|
||||
traefik-shared:
|
||||
name: traefik-shared
|
||||
external: true
|
||||
```
|
||||
|
||||
<b-message type="is-info" has-icon>
|
||||
Файл yarn.lock генерируется командой `yarn install`. Если у вас не установлен `yarn`, то ничего страшного. Просто это будет занимать чуть больше времени на сборку образа.
|
||||
</b-message>
|
||||
|
||||
Как вы видите, приложение слушает на порт `3000` и отвечает свим hostname и количеством обработанных запросов. А в docker-compose.yml, в отличие от обычного проекта, появились labels.
|
||||
|
||||
<hr/>
|
||||
<b-message type="is-danger" has-icon>
|
||||
|
||||
Роутеры, сервисы и хосты явно создавать нигде не нужно.
|
||||
Они сами создаются, когда вы их указываете.
|
||||
|
||||
Название сервиса и роутера могут совпадать.
|
||||
|
||||
Обратите внимание на косые кавычки при указании хоста! Это обязательно.
|
||||
|
||||
В объявлении labels могут быть использованы переменные среды. Например:
|
||||
`` traefik.http.routers.test_node.rule=Host(`${APP_HOST}`) ``
|
||||
|
||||
</b-message>
|
||||
|
||||
Также можно видеть, что я подключил контейнер к сети, которую мы указывали в контейнере traefik. Здесь она помечена как external.
|
||||
|
||||
Теперь мы можем спокойно запустить наш сервис. Для этого воспользуемся следующей командой:
|
||||
|
||||
```bash
|
||||
docker-compose up --build --scale server=3
|
||||
```
|
||||
|
||||
В данной команде мы указали, что хотим поднять 3 инстанса нашего сервиса. Остальным пусть занимается traefik.
|
||||
|
||||
Теперь попробуем некоторое количество раз выполнить запрос на наш сервис.
|
||||
|
||||
```console
|
||||
$ curl -H "Host: test_node.local" "http://localhost"
|
||||
{"request_num":0,"host":"7417ac8fda92"}
|
||||
```
|
||||
|
||||
Результат должен быть примерно таким:
|
||||
|
||||
[](/images/traefik_imgs/curls.gif)
|
||||
|
||||
Как вы видите, traefik балансирует между контейнерами за нас. И я считаю, что это - победа.
|
||||
|
||||
## Подключение TLS и сертификатов
|
||||
|
||||
Тут всё не намного сложнее. Давайте немного поменяем лейблы нашего контейнера.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
server:
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.test_node.rule=Host(`test_node.local`)
|
||||
- traefik.http.routers.test_node.entrypoints=https
|
||||
- traefik.http.routers.test_node.tls=true
|
||||
- traefik.http.routers.test_node.service=node_test
|
||||
- traefik.http.services.node_test.loadbalancer.server.port=3000
|
||||
```
|
||||
|
||||
Как вы видите, я поменял entrypoints на `https`. Как вы можете помнить, это entrypoint, который слушает на 443 порт. И также я включил поддержку tls лейблом `traefik.http.routers.<router>.tls=true`
|
||||
|
||||
На данном этапе вам потребуется добавить свой хост в `/etc/hosts`, если вы используете нормальную систему. Но если вы всё же на windows, то вам потребуется добавить правило в `C:\Windows\System32\drivers\etc\hosts`.
|
||||
|
||||
И добавляем в конец файла запись:
|
||||
|
||||
```
|
||||
127.0.0.1 test_node.local
|
||||
```
|
||||
|
||||
И также вам потребуется cертификат на этот домен.
|
||||
Для этого:
|
||||
|
||||
1. Создадим сертификат через `mkcert`, как упоминалось ранее;
|
||||
2. Поместим ключ и сертификат в папку certs;
|
||||
3. Добавим ключ и сертификат для вашего домена в config.toml (Формат указан выше).
|
||||
|
||||
Теперь вы можете обращаться к вашему приложению напрямую через локальное доменное имя.
|
||||
|
||||
```console
|
||||
$ curl --insecure https://test_node.local
|
||||
{"request_num":0,"host":"7417ac8fda92"}
|
||||
```
|
||||
|
||||
## Добавление локальных сервисов не из докера
|
||||
|
||||
Все те флаги, которые мы указываем в labels, вы также можете указать в файле конфигурации рядом с docker-compose.yml, указав конкретный ip адрес.
|
||||
|
||||
Например:
|
||||
|
||||
```toml
|
||||
[http.routers]
|
||||
# Define a connection between requests and services
|
||||
[http.routers.<router_name>]
|
||||
rule = "Host(`my_app.local`)"
|
||||
entrypoints = "https"
|
||||
service = "<service_name>"
|
||||
[http.routers.<router_name>.tls]
|
||||
|
||||
[http.services]
|
||||
# Define how to reach an existing service on our infrastructure
|
||||
[http.services.<service_name>.loadBalancer]
|
||||
[[http.services.<service_name>.loadBalancer.servers]]
|
||||
url = "http://192.168.1.89:8100"
|
||||
```
|
||||
|
||||
# Создание локального DNS
|
||||
|
||||
В данном пункте я бы хотел рассказать, как настроить свой DNS-сервер, чтобы ваши домены были доступны всем устройствам в локальной сети. Для этого я буду использовать dnsmasq. Пользователям винды он недоступен, поэтому советую развернуть маленький домашний сервер на линуксе.
|
||||
|
||||
Для этого установите `dnsmasq` и найдите и раскомментируйте, либо добавьте следующие строчки в файл `/etc/dnsmasq.conf`:
|
||||
|
||||
```conf{}[/etc/dnsmasq.conf]
|
||||
# Never forward plain names (without a dot or domain part)
|
||||
domain-needed
|
||||
# Never forward addresses in the non-routed address spaces.
|
||||
bogus-priv
|
||||
address=/.local/192.168.1.89
|
||||
address=/.<other_domain>/<your_local_ip>
|
||||
```
|
||||
|
||||
У меня traefik развернут на хосте `192.168.1.89`. У вас ip может отличаться.
|
||||
Чтобы это узнать, посмотрите свой ip через роутер или выполните `ip addr`.
|
||||
|
||||
Вообще, `dnsmasq` парсит файл `/etc/hosts` и вы можете туда добавлять записи, типа:
|
||||
|
||||
```
|
||||
192.168.1.1 mydomain.local
|
||||
```
|
||||
|
||||
Но так как я указал `address`, то это необязательно. `dnsmasq` и без явного указания поддоменов должен будет работать отлично.
|
||||
|
||||
Запустите `dnsmasq` в режиме сревиса:
|
||||
|
||||
```bash
|
||||
sudo systemctl enable dnsmasq.service
|
||||
sudo systemctl start dnsmasq.service
|
||||
```
|
||||
|
||||
Теперь пойдите в настройки вашего роутера и найдите DNS-сервера. Добавьте туда ваш ip. Также не забудьте сделать локальный ip вашего устройства статичным.
|
||||
|
||||
Для примера, в роутерах keenetic это можно сделать, зарегистрировав устройство в меню 'Список устройств' и нажав на галочку 'Постоянный IP-адрес'.
|
||||
|
||||
И добавьте свой DNS-сервер в список DNS серверов вашего роутера.
|
||||

|
||||
|
||||
Готово. Вы можете попробовать зайти на свой домен с другого устройства в локальной сети, и это должно работать.
|
||||
|
||||
# Мониторинг работы traefik
|
||||
|
||||
Вообще, traefik имеет WEB интерфейс для отслеживания его работы.
|
||||
Для того чтобы подключить его, давайте поменяем `docker-compose.yml`.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
traefik-proxy:
|
||||
# The official v2.0 Traefik docker image
|
||||
image: traefik:v2.4.8
|
||||
container_name: traefik_main
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.traefik_router.rule=Host(`traefik.local`)
|
||||
- traefik.http.routers.traefik_router.service=api@internal
|
||||
- traefik.http.routers.traefik_router.entrypoints=https
|
||||
- traefik.http.routers.traefik_router.tls=true
|
||||
|
||||
command:
|
||||
- --api.dashboard=true
|
||||
...
|
||||
```
|
||||
|
||||
Я добавил ключ `--api.dashboard=true` и лейблы для роута до `traefik`.
|
||||
Замечу, service определён заранее - это `api@internal`. Укажите его в `traefik.http.routers.<router_name>.service`.
|
||||
|
||||
Теперь вы можете зайти на `traefik.local` через свой браузер и увидеть конфигурацию traefik.
|
||||
|
||||
[](/images/traefik_imgs/traefik_web.png)
|
||||
|
||||
До новых встреч.
|
Reference in New Issue
Block a user