Некоторые проблемы возникают на ровном месте. И обычные способы решения не подходят. Вчера я увидел следующее сообщение при выполнении workflow на GitHub Actions:
Error response from daemon: OCI runtime create failed: container_linux.go:345: starting container process caused "exec: \"/entrypoint.sh\": permission denied": unknown
Мы запускаем docker-compose, контейнеры начинают собираться и в момент, когда контейнер web решает запустить свой entrypoint (действие, совершаемое при каждом старте контейнера), мы получаем эту ошибку.
Быстрый поиск нам дает понимание, что это проблема с разрешениями, и нужно просто выполнить классический набор команд, чтобы сделать файл исполняемым:
Мы так и делаем, но проблема не уходит. Несколько часов поисков, везде один тот же рецепт. И вы даже с ним согласны, ведь когда вы просматриваете файлы командой:
ls -la
Вы видите, что исполнительный бит «х» добавился, и ну не может такого быть, чтобы он терялся по пути. А оказывается может. И теряется.
В тот момент, когда мы делаем на Windows Git Bash команду git add, мы теряем исполнительный бит. Как до этого догадаться? Я поднял контейнеры без запуска entrypoint.sh и проверил его права, executive bit был потерян. Я начал искать его на виртуальной машине, которую создает GitHub Actions для деплоя и тоже не нашел его там. Тогда я догадался переформулировать поисковый запрос на…
Git add lost executive bit
Только такая формулировка дала мне правильный набор решений. Оказывается, именно под операционной системой Windows система контроля версий git может (но не всегда) терять исполнительный бит при добавлении в репозиторий. Решается это следующим набором команд:
git add --chmod=+x -- entrypoint.sh
git commit
То есть мы добавляем не файлу в операционной системе исполнительный бит, а файлу в момент присоединения его к коммиту.
Решение не помогло? Замените CRLF на LF
Есть еще одна особенность, которая может повредить исполняемые bash-файлы — это окончания строк. В среде Windows принято в конце строки ставить перевод каретки на следующую строку и в самое начало, и называется это дело CRLF.
Естественно, в среде Linux все иначе, там перевод на начало строки не делают, только разрыв оставляют: LF. Проблема в том, что текстовые файлы по умолчанию в Windows используют несовместимые управляющие символы, и если вы хотите сделать ваш entrypoint.sh исполняемым — зайдите в настройки вашего любимого текстового редактора или IDE и установите параметр «Окончание строк» равным LF (Unix). Я лично рекомендую Notepad++ для этого:
Надеюсь это маленькое расследование однажды сэкономит вам несколько часов времени.
Чем дольше знакомишься с темой, тем больше замечательных и простых вариантов находишь для выполнения своих задач. Деплой своего проекта — крайне обширная тема, и очень тяжело избежать ловушки мышления, когда пытаешь сделать на удаленной машине так же, как ты делаешь на своей.
Допустим, как работает docker-compose? Ему необходим файл docker-compose.yaml, в котором описаны все контейнеры и их связи, а также от одного до бесконечности файлов Dockerfile, которые помогут собрать образы. Также всякие конфиги nginx, файлы с переменными окружения .env. И первое желание — перенести все эти файлы на удаленный сервер с помощью ssh или scp и начать разворачивать там руками.
Но есть гораздо более правильный путь (This is the way). Наша задача копнуть чуть глубже и попробовать развернуть наши контейнеры на удаленном сервере без каких либо файлов в принципе. Не верите?
Для начала нам нужно понять, что докер умеет гораздо больше, чем мы знаем. Например, он умеет управлять удаленным докером, установленным на нашем сервере. Для этого нам нужно создать так называемый контекст с настройками нашего удаленного сервера. По умолчанию у нас уже есть один контекст, он описывает подключение докера к нашей собственной машине, посмотрите на него:
docker context ls
Вы увидите этот слой с настройками, его имя default *, где астериск означает, что данный контекст (слой, файл с настройками) в данный момент активен.
Если мы хотим создать новый контекст (опять-таки, для удобства думайте о нем как о файле с настройками), то нам нужно выполнить несколько условий:
У нас должен быть приватный ключ SSH
На сервере должен быть соответствующий ему открытый ключ
Мы должны знать IP адрес сервера. Можно настроить и по домену, но, как показала практика, IP адрес дает более надежную идентификацию хоста
Наш хост должен быть знаком нашему компьютеру, чтобы избежать лишних вопросов про то, доверяем ли мы ему
Пункты в целом несложные, последний вообще можно исполнить без SSH подключения к серверу:
В данной строке есть команда create (создать), имя нашего нового контекста remote и хост, где работает Docker. Дело в том, что докер может управлять не только докером, но и целым swarm или Kubernetes. Это все системы оркестрации, где вы можете (как дирижер оркестра) управлять деплоем на сотни машин, про них позже, сейчас мы просто прокладываем мост между двумя докерами, самое простое решение, поэтому ключ пишем --docker.
Попробуйте обязательно эту команду на своей локальной машине, указав удаленный хост. После этого у вас в списке контекста появится новый пункт — remote! Это значит, что теперь мы можем управлять нашей машиной на расстоянии, для этого нам нужно применить настройки этого контекста командой:
docker context use remote
Теперь * астериск будет стоять напротив пункта remote в списке контекстов. Не забудьте потом переключиться обратно на default, когда закончите.
Ну что, проверим магию? Давай посмотрим список контейнеров на удаленном хосте командой…
docker ps
Именно! Все обычные команды, которые мы выполняли на локальной машине, подходят и теперь, только общаются они с удаленным сервером!
Если у нас крутятся какие-то контейнеры, то давайте их все остановим и удалим, чтобы развернуть наш удаленный docker-compose. Раньше я предлагал ввести следующие команды:
Объясню, что здесь происходит. Мы стягиваем свежий образ нашего проекта с докерхаб, затем очень нежно выключаем наш проект с помощью встроенной в docker-compose функции down. Дальше я запускаю профилактическое удаление тома со статикой, так как она может обновиться в процессе разработки. В вашем случае нужно подставить правильное название тома, узнать его можно командой:
docker volume ls
Теперь самое время запустить docker-compose. Здесь важно понимать, что docker и docker-compose, несмотря на очевидное сходство в названии, по сути своей разные программы для выполнения разных задач. Раньше они вообще мало были связаны, сейчас интеграция становится все лучше и лучше. Совсем недавно docker-compose научился работать с контекстами docker, но пока это происходит не в автоматическом режиме после использования команды use, а с помощью специального флага —context. Перейдите в директорию с файлом docker-compose.yaml и выполните команду:
docker-compose --context remote up -d --force-recreate
Смотрите, мы используем контекст remote, поднимаем наши контейнеры в -d режиме (чтобы не видеть вывод) и на всякий случай силой их пересоздаем, если на сервере остались какие-то контейнеры с подобным названием. Все зависимости у нас под рукой — переменные окружения, yaml файл и прочие. Убедитесь, что все ваши контейнеры создаются на основе образов, а не собираются из файлов.
В идеальном мире вы получите контейнеры, развернутые на вашем сервере. Проверьте, работает ли сайт по нужному адресу и порту.
Как нам применить эти знания к деплою проекта с помощью GitHub Actions?
Алгоритм будет следующий:
Создаем виртуальную машину
Разворачиваем наш репозиторий (можно не ставить зависимости, нам нужны только файлы): actions/checkout@v2
Устанавливаем Python 3.8: actions/setup-python@v2
Устанавливаем наш SSH KEY в виртуальную машину и добавляем наш IP в доверенные хосты. Очень поможет вот этот action.
Устанавливаем последнюю версию докера, чтобы она могла управлять всеми другими версиями докера, в том числе и младшими (используйте sudo pip install docker-compose, в текущей версии по curl есть критический баг с OpenSSL и вы точно не хотите с ним провести пару часов)
Создаем пустой файл .env (почему-то без него docker-compose на сервере не может перейти к этапу просмотра переменных окружения в системе, на локальной машине таких проблем нет). Поможет команда touch .env
Запускаем все наши команды из статьи выше, не забыв заменить секретные данные переменными окружения.
Вот и все. Получилось, что весь деплой уместился в 6 строчек, если не считать объявления переменных окружения и установки зависимостей.
Иногда самые сложные вопросы имеют самые легкие ответы. Когда мы работаем на локальной машине, нам проще всего взять наши переменные окружения (environment variables) из файла .env при работе с docker-compose. Но ведь в таком виде секреты передавать в репозиторий нельзя! Значит, мы в лучшем случае запишем туда .env.template, где перечислим переменные и, возможно, дадим им некоторые значения по умолчанию для примера.
Очевидно, что сами секреты мы заведем в настройках репозитория, в разделе GitHub Secrets:
Но как их дальше передать на сервер? Первое, что хочется сделать — сформировать на сервере .env файл неким скриптом, но есть путь гораздо проще и чище, давайте используем стандартную команду export, которая в командной среде может присваивать переменную окружения и хранить ее прямо в системе, без каких-либо файлов! Добавим GitHub Action следующего вида:
Возможно, здесь много повторяющего кода, и хотелось бы записать это одной строкой с одним экспортом, но так, на мой взгляд, проще и понятнее. А также небольшая формула в экселе позволяет это довольно быстро сгенерировать:
Теперь все переменные будут автоматически подставляться в docker-compose.
env_file:
- ./.env
Которая, как известно, сканирует файл .env в поиске переменных. А все можно прочитать в документации:
When you set the same environment variable in multiple files, here’s the priority used by Compose to choose which value to use:
1) Compose file 2) Shell environment variables 3) Environment file 4) Dockerfile 5) Variable is not defined
Сначала env файл, а затем уже переменные Shell. Это позволяет нам на компьютере для тестирования использовать один набор переменных, а для деплоя — другой.
В рамках Continuous Integration нам нужно уметь разворачивать не только контейнеры командой docker, но и группу контейнеров, например django + postgres + nginx.
На локальной машине это просто, команда docker-compose up -d выполнит все инструкции из файла docker-compose.yaml, но на сервере его нет.
Существует два способа доставить файл docker-compose.yaml на удаленный сервер. Мы не будем рассматривать вариант «доставить руками», ведь у нас непрерывная интеграция, а значит на каждый коммит и пуш в репозиторий на GitHub наш проект после прохождения всех тестов должен развернуться самостоятельно на сервере.
Вариант 1
Используя CI на базе GitHub Actions, мы можем воспользоваться сторонним действием, которое позволяет нам подключиться к удаленному серверу и скопировать произвольный файл из репозитория, который предварительно развернут на сервере GitHub:
Обратите внимание, в разделе GitHub Secrets вам нужно будет указать необходимые переменные. Если вы не используете пароль на ваш SSH KEY — просто не указываете эту строчку.
Повторимся, копирует он с виртуальной машины GitHub, и для того, чтобы на ней были ваши файлы, необходимо перед этим действием выполнить команду сбора файлов:
- name: Check out the repo
uses: actions/checkout@v2
Вариант 2
Подключиться через SSH в GitHub Actions к серверу и достать нужный файл из образа, который мы скачаем с Docker Hub:
Давайте разберем, что происходит. Вы подключаетесь по SSH, скачиваете ваш образ, дальше вы создаете временный контейнер из этого образа и кладете его имя в переменную id.
Следом идут обычные команды копирования из контейнера на хостовую машину. Мы берем файлы для docker-compose, в том числе настройки nginx, а затем удаляем ненужный нам контейнер. Все, заключительным действием мы на всякий случай выключим и удалим все контейнеры (если это не первый наш деплой, то действие поможет избежать конфликтов) и запустим наши контейнеры:
docker kill $(docker ps -q)
docker rm $(docker ps -a -q)
sudo docker-compose up -d --force-recreate
Обратите внимание на флаг -d в последней команде — если его забыть, то логи докера никогда не закончатся и ваш CI никогда не завершится успешно.
В случае, когда за SSL сертификаты для докеризированных приложений отвечает внешний веб-сервер (например, Caddy), нужно очень внимательно отнестись к тому, как вы открываете порты в своем docker-compose.
Но здесь есть серьезная проблема. Ваш nginx теперь доступен не только по адресу localhost или 127.0.0.1, но и всему миру, если у сервера есть публичный адрес.
Мы же выпускаем сертификат с помощью Caddy, значит доступ к сайту будет защищен только если пользователь придет по доменному имени. А что если он забьет ip адрес и добавить в конце порт 8888? Так он получит доступ к сайту без SSL, и может воспользоваться данной возможностью множеством неприятных способов.
Поэтому мы будем создавать контейнеры правильно, мы будем их показывать только приложениям, которые крутятся на нашем сервере:
Обратите внимание, что с появлением локального айпи адреса нам пришлось взять все выражение в кавычки, иначе докер не сможет достойно распарсить данную строку.
Смотрим, что показывает нам nmap:
user@server:~$ nmap 127.0.0.1
Starting Nmap 7.91 ( https://nmap.org ) at 2021-02-24 06:42 UTC
Nmap scan report for localhost (127.0.0.1)
Host is up (0.00011s latency).
Not shown: 993 closed ports
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
443/tcp open https
5000/tcp open upnp
5432/tcp open postgresql
8000/tcp open http-alt
8888/tcp open sun-answerbook
Видим, что внутри порт 888 открыт. Сканируем порты по внешнему доменному имени:
matakov@matacoder:~$ nmap site.ru
Starting Nmap 7.91 ( https://nmap.org ) at 2021-02-24 06:44 UTC
Nmap scan report for site.ru (178.154.254.176)
Host is up (0.0011s latency).
Not shown: 997 filtered ports
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
443/tcp open https
Все, уязвимость ушла, наружу смотрит только SSH, HTTP и HTTPS.
В рамках учебного проекта мне потребовалось развернуть на одном сервере два приложения. Первое из них — классический Django Framework, который крутится с помощью Guncorn, файлы лежат прямо на сервере. Второе же хитрое — полностью докеризированная связка Nginx + Gunicorn + PostgreSQL.
Поднять незащищенное соединение совсем не сложно, у нас наружу светятся два порта, один от Gunicorn, один от Nginx из докера. Но как защитить все это дело? Если для серверного приложения с помощью Certbot это делается легко, то для докера настройки связки Certbot в контейнере и Nginx в контейнере — занятие не для слабонервных. А для того, чтобы сертификат заработал, нам потребовалось бы еще и пробрасывать 443 порт на докер, а сделать это можно двумя способами — либо не расшифровывая трафик через stream директиву Nginx, либо расшифровывая, но придется указать пути до ключей и, что самое сложное, понадобиться дать certbot положить файл в корневую директорию сайта для проверки соединения, а она у нас благодаря гуникорну не так-то просто достижима.
В качестве решения можно использовать сервер Caddy. Идея будет заключаться в том, что за все сертификаты будет отвечать только она, за ней уже пойдет незашифрованный трафик до нужных нам портов.
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee -a /etc/apt/sources.list.d/caddy-stable.list
sudo apt updatesudo apt install caddy
Дальше необходимо создать конфиругацию. У кадди есть особенность, файл конфига может лежать где угодно. Я оставил его в домашней директории, он должен быть назван Caddyfile. Вот содержимое:
Этот конфиг позволяет запустить сайт1, который у нас лежит на сервере в указанной папке. Директива notStatic создает исключения для Кадди, эти файлы она не будет отправлять в Gunicorn и будет сама обслуживать. Обратный прокси — стандартная как для Ngnix, так и для Кадди операция, мы передаем все запросы дальше, на 8000 порт Гуникорна.
Вторая часть конфига не делает каких-либо исключений для статики, так как у нас в контейнерах крутится Nginx и он ответственный за статику.
А теперь немного магии от Кадди. Когда мы в директории, где у нас каддифайл лежит, запустим команду sudo caddy start, она автоматически выпустит SSL сертификаты для обоих доменов и будет их обновлять, проверяя оставшееся время каждые 10 дней. То есть самая сложная часть в случае с приложением, которое лежит на сервере — указать пути до статики, а если поднимается докеризированное приложение — то всего две строки и готово.
Последние недели я занимаюсь саморазвитием — решаю задачи Эйлера по программированию. Дошел уже до 39. И в каком-то разговоре с женой про сыновей и их имена с двумя Р я понял, что могу написать программу, которая может найти все такие имена в базе данных. Файл с именами я нашел тут: https://stackoverflow.com/questions/1803628/raw-list-of-person-names
А код написал сам:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fileencoding=utf-8
'''
looking for names with double R
'''
import os
fi = open('/Users/matakov/names.txt', 'r')
pyramid=[]
for line in fi:
pyramid.append( list(map( str, line.split() )) )
pyramid = sorted(pyramid)
#print ((pyramid))
for line in pyramid:
#print (line)
r=0
for char in line[0]:
if char == 'r' or char == 'R':
r+=1
if r >= 2: print (line[0])
Довольно просто оказалось. Вот так я провожу досуг, когда не пишу в блог.
Вот сам список, как видите, мои сыновья в нем присутствуют:
TWO R NAMES:
ARRON
ARTHUR
ARTURO
AURORA
AURORE
BARABARA
BARBAR
BARBARA
BARBERA
BARBRA
BARRETT
BARRIE
BARRY
BERNARD
BERNARDA
BERNARDINA
BERNARDINE
BERNARDO
BERRY
BERTRAM
BRADFORD
BRODERICK
CARRI
CARRIE
CARROL
CARROLL
CARRY
CARTER
CHERRI
CHERRIE
CHERRY
CHERRYL
CHRISTOPER
CHRISTOPHER
CIERRA
CORRIE
CORRIN
CORRINA
CORRINE
CORRINNE
CRISTOPHER
DARREL
DARRELL
DARREN
DARRICK
DARRIN
DARRON
DARRYL
DEIRDRE
DERRICK
DIERDRE
DORRIS
ERROL
FARRAH
FLORRIE
FORREST
FREDERIC
FREDERICA
FREDERICK
FREDERICKA
FREDRIC
FREDRICK
FREDRICKA
GARRET
GARRETT
GARRY
GERARD
GERARDO
GERRI
GERRY
GERTRUD
GERTRUDE
GERTRUDIS
GREGORIA
GREGORIO
GREGORY
GROVER
HARRIET
HARRIETT
HARRIETTE
HARRIS
HARRISON
HARRY
HERBERT
HERIBERTO
IRMGARD
JARRED
JARRETT
JARROD
JERRELL
JERRI
JERRICA
JERRIE
JERROD
JERROLD
JERRY
KARREN
KARRI
KARRIE
KARRY
KERRI
KERRIE
KERRY
KRISTOFER
KRISTOPHER
LARRAINE
LARRY
LORRAINE
LORRETTA
LORRI
LORRIANE
LORRIE
LORRINE
MARAGARET
MARAGRET
MARGARET
MARGARETA
MARGARETE
MARGARETT
MARGARETTA
MARGARETTE
MARGARITA
MARGARITE
MARGARITO
MARGART
MARGERET
MARGERT
MARGERY
MARGHERITA
MARGORIE
MARGRET
MARGRETT
MARGUERITA
MARGUERITE
MARGURITE
MARICRUZ
MARJORIE
MARJORY
MARQUERITE
MARRY
MARYROSE
MERRI
MERRIE
MERRILEE
MERRILL
MERRY
MONSERRATE
MORRIS
MURRAY
NORBERT
NORBERTO
NORRIS
PARKER
PERRY
PIERRE
PORFIRIO
PORTER
RAMIRO
RAYFORD
RICARDA
RICARDO
RICHARD
RIGOBERTO
ROBERT
ROBERTA
ROBERTO
RODERICK
RODGER
RODRICK
RODRIGO
ROGER
RORY
ROSAMARIA
ROSARIA
ROSARIO
ROSAURA
ROSEMARIE
ROSEMARY
RUPERT
SHARRI
SHARRON
SHERRELL
SHERRI
SHERRIE
SHERRIL
SHERRILL
SHERRON
SHERRY
SHERRYL
SIERRA
SOCORRO
STARR
TARRA
TERRA
TERRANCE
TERRELL
TERRENCE
TERRESA
TERRI
TERRIE
TERRILYN
TERRY
TIERRA
TORRI
TORRIE
TREVOR
WARNER
WARREN
WERNER