Настройка ВМ
Установим основные пакеты, для этого сначала обновим локальную базу данных пакетного менеджера apt:
sudo apt update
Мы запустили apt update
при помощи программы sudo
, она позволяет выполнять команды как суперпользователь: пользователь с максимальными привилегиями в ОС.
О повышении привилегий
В Linux есть три типа пользователей — суперпользователь (root), системные и стандартные. Различие между стандартными и системными пользователями больше организационное, дистрибутивы Linux резервируют диапазоны UID для служб и конкретные ID для некоторых сервисов, например, Debian.
Суперпользователь имеет больше привилегий: он может редактировать системные файлы, запускать сервера на защищенных портах, трассировать системные вызовы других процессов и т. д. Поэтому для настройки наших ВМ мы часто будем использовать sudo с различными аргументами.
sudo
sudo — программа для повышения привилегий, которая используется во всех современных дистрибутивах Linux. Поддерживает гибкую настройку: указание пользователей и групп, которым доступно повышение привилегий, ограничение запускаемых команд и т. д.
Часто используемые аргументы:
sudo -- COMMAND
— запустить одну команду как суперпользователь;
sudo -u USER -- COMMAND
— запустить одну команду как пользователь USER;
sudo -i
— запустить терминал как суперпользователь, выполнив скрипты инициализации, которые выполняются при обычном входе пользователя в систему.
Другие опции sudo, как и множества других системных утилит, можно посмотреть командой man sudo. Также существует утилита su, которая аналогично выполнять команды как суперпользователь или другой пользователь, но мы не будем пользоваться su, так как:
- su требует пароль от root, а не от пользователя запускающего команду; довольно часто пользователю root не задают пароль или деактивируют его, что не позволяет использовать su;
- sudo поддерживает более тонкую настройку;
- sudo логирует выполненные команды, что упрощает отладку.
Для настройки sudo используйте команду visudo: она открывает конфиг sudo в редакторе и валидирует его при закрытии. Это важно, т.к. при некорректном конфиге sudo вы не сможете повысить свои привилегии чтобы исправить конфиг. Вы можете изменить редактор, используемый visudo указав переменную EDITOR:
sudo EDITOR=nano visudo
Установка утилит
Установим систему контейнеризации Docker:
sudo apt install docker.io docker-compose
Добавим своего пользователя в группу docker, чтобы использовать docker без sudo:
sudo gpasswd -a $USER docker
Так не рекомендуется делать на промышленных серверах, т.к. любой пользователь в группе docker может изменить системные файлы и получить права суперпользователя, мы сделали это здесь для простоты.
Также включим docker при старте системы:
sudo systemctl enable --now docker.service
systemctl — это утилита для управления службами — программами запускаемыми при старте системы без участия пользователя. Например так запущен сервер SSH, при помощи которого мы подключены к ВМ. Мы не будем останавливаться на systemd и systemctl подробно, т.к. docker сам умеет запускать контейнеры при старте системы. Вышеуказанная команда включает запуск службы при старте системы и запускает ее прямо сейчас.
Установим Ansible:
sudo apt-add-repository --update ppa:ansible/ansible
sudo apt install ansible
Первой командой мы добавили приватный репозиторий (PPA) ansible.
Установим остальные утилиты:
sudo apt install git curl jq
systemd
Приведем небольшую справку по командам systemd
systemctl status SERVICE — просмотреть статус службы;
systemctl start SERVICE — запустить службу;
systemctl enable --now SERVICE — включить и запустить службу;
systemctl list-units — вывести список всех служб;
systemctl list-units --failed — вывести список упавших служб;
journalctl -b0 — вывести системные логи с момента включения системы;
journalctl -eu SERVICE — вывести логи конкретного сервиса, перейдя в конец логов;
journalctl -feu SERVICE — отслеживать логи сервиса;
Аналогично полную справку по командам можно посмотреть командами man systemctl и man jounrnalctl.
Настройка локальной IDE
На следующих занятиях мы будем писать довольно много кода, поэтому сразу настроим локальную IDE. Мы предлагаем настроить VSCode с расширением Remote - SSH, чтобы работать из VSCode сразу на ВМ.
На Windows/MacOS установите VSCode с официального сайта.
На Windows используйте системный инсталлятор, если у вас есть права администратора.
На Linux используйте свой пакетный менеджер или Flatpak, или Snap.
На Windows установите OpenSSH клиент командой в терминале PowerShell:
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0
Расширения
Откройте меню расширений (CTRL+SHIFT+X) и установите следующие расширения. Указанные в скобках идентификаторы расширений можно скопировать в строку поиска:
- Remote - SSH (ms-vscode-remote.remote-ssh)
- Docker (ms-azuretools.vscode-docker)
- Ansible (redhat.ansible)
- YAML (redhat.vscode-yaml)
Затем откройте панель команд (F1, CTRL+SHIFT+P или COMMAND+SHIFT+P на MacOS), найдите в ней Remote - SSH: Open SSH Configuration File…:
В открывшемся конфиге введите параметры хоста:
Host teta-devops-1
HostName VM1_PUBLIC_ADDRESS
User student
IdentityFile /path/to/student.pem
Host teta-devops-2
HostName VM2_PRIVATE_ADDRESS
User student
IdentityFile /path/to/student.pem
ProxyJump student@teta-devops-1
Вместо VM1_PUBLIC_ADDRESS укажите присланный вам публичный IP первой виртуальной машины, также вы можете указать настройки подключения ко второй ВМ, указав в качестве адреса ее приватный адрес и задав директиву ProxyJump, чтобы подключение происходило через первую ВМ.
Затем откройте панель команд, найдите Remote-SSH: Connect to Host…
В выпадающем меню выберите первый хост.
Shell
В дальнейшем мы часто будем использовать Shell чтобы писать Dockerfile и скрипты в пайплайнах GitLab CI, поэтому дадим краткую вводную. Более подробную информацию можно найти здесь:
Справочник по Bash;
Справочник по POSIX Shell;
Справочник по стадартным утилитам Shell.
Мы не будем рассматривать расширения Bash и Zsh, т.к. из-за распространения Docker-образов на базе Alpine Linux, вы часто будете встречаться с оболочкой Ash, которая не поддерживает большинство расширений.
Команды
Shell воспринимает на вход строки, состоящие из слов, разделенных пробелами, в такой строке первое слово — это команда (исполняемый файл или встроенная в Shell команда), остальные слова передаются этой команде как аргументы, в виде последовательности слов.
Введите в терминал на своей ВМ следующую команду:
echo Hello World
echo — встроенная команда, которая выводит переданные ей аргументы, разделяя их пробелом. Теперь вставьте несколько пробелов между Hello и World:
echo Hello World
Вы заметите, что вывод не изменился, т.к. Shell распознала команду как последовательность echo, Hello, World и передала в echo аргументы Hello и World. Все наши пробелы были отброшены при чтении команды.
Чтобы писать в аргументах зарезервированные Shell символы используют кавычки и экранирование:
echo Hello\ \ World
Указав перед пробелом символ \ мы экранировали пробел — указали Shell, что этот пробел нужно воспринимать как пробел, а не как разделитель аргументов. Аналогичный результат можно получить при помощи одинарных ' и двойных " кавычек:
echo 'Hello World'
echo "Hello World"
В отличие от, например, Python кавычки не взаимозаменяемы. В строках, заключенных в двойные кавычки можно использовать переменные и подстановку команд. Сравните вывод:
echo "${HOME}"
echo '${HOME}'
Таким образом можно экранировать в том числе символ переноса строки, чтобы писать многострочные команды:
echo Hello\ \
World
Переменные
Shell, как и большинство языков программирования поддерживает переменные. Чтобы создать переменную с именем MYVARнапишите следующее:
MYVAR=value
Обратите внимание на отсутствие пробелов, если написать пробелы около =, то Shell воспримет MYVAR как команду и попытается ее выполнить. В значениях переменных аналогично можно использовать кавычки и экранирование:
MYVAR="value a"
MYVAR='value b'
MYVAR=value\ c
Чтобы воспользоваться значением переменной воспользуемся оператором подстановки $:
MY_MESSAGE='Hello World'
echo $MY_MESSAGE
echo ${MY_MESSAGE}
echo "$MY_MESSAGE"
echo "${MY_MESSAGE}"
Отличие синтаксиса $VAR от ${VAR} в том, что мы можем написать символ, который мог входить в имя переменной, сразу же после подстановки:
MY_MESSAGE='Hello World'
echo "${MY_MESSAGE}X"
echo $MY_MESSAGEX
Обратите внимание, что вторая команда echo не привела к ошибке — по умолчанию в Shell подстановка значения несуществующей переменной возвращает пустую строку.
В Shell все переменные имеют строковый тип, для выполнения числовых операций или для работы с последовательностями некоторые команды интерпретируют свои аргументы как числа или последовательности, возвращая ошибку если это невозможно.
Например программа expr может вычислять арифметические выражения:
expr 1 + 2
expr ( 1 + 2 ) '*' 3
Если передать в качестве одного из аргументов строку, Shell не вернет ошибку, вместо этого ошибку вернет expr:
VAR=sss
expr $VAR + 2
readonly NAME=VALUE запрещает повторные присваивания к переменной NAME
Переменные окружения
Разработчикам знаком концепт переменных окружения, это строковые переменные, передаваемые ОС при старте процесса.
Чтобы сделать обычную переменную Shell переменной окружения используйте команду export:
VAR_1=value_1
VAR_2=value_2
export VAR_1 VAR_2
или
export VAR_1=value_1 VAR_2=value_2
Переменные окружения передаются всем программам, запускаемым в оболочке, удостоверимся, что переменные заданы при помощи env:
env | grep --extended-regexp '^(VAR_1|VAR_2)=' -
Также переменную окружения можно задать для конкретной команды:
X=Y sh -c 'echo $X'
Аналогично, можем воспользоваться программой env:
env X=Y sh -c 'echo $X'
При помощи env --ignore-environment (env -i) можно запустить команду в чистом окружении:
env --ignore-environment X=Y
Подстановка ~
Если аргумент начинается с ~, то ~ заменяется на абсолютный путь к домашней директории текущего пользователя:
echo ~/.ssh
echo 1~
echo "~/.ssh"
Подстановочные шаблоны
Внимательный глаз заметит, что в предыдущем разделе мы закавычили символ *. Мы сделали это т.к. * — символ, входящий в подстановочные шаблоны Shell.
Подстановочный шаблон — это аргумент Shell, который при чтении команды заменяется на последовательность путей файловой системы по определенным правилам. Строка является шаблоном, если содержит символ *, ? или [.
Чаще всего используются *, ?:
* — соответствует любой, в том числе пустой, строке;
? — соответствует одному символу;
Другие шаблоны см. в man 7 glob.
Например:
echo ~/*.txt
Выведет имена всех нескрытых текстовых файлов в домашней директории.
echo ?
Выведет имена всех нескрытых файлов или папок, состоящих из одного символа.
Потоки ввода—вывода
Все программы имеют три стандартных потока ввода-вывода, предоставляемых ОС:
stdin (0) — стандартный поток ввода;
stdout (1) — стандартный поток вывода;
stderr (2) — стандартный поток ошибок;
Программы могут использовать эти потоки чтобы общаться с другими программами и получать ввод от пользователя. В повседневной разработке мы не используем эти потоки активно, т.к. часто наши программы общаются по сети или через файлы, получают ввод из событий клавиатуры/мыши/сенсорных экранов, но в Shell эти потоки — основное средство коммуникации между командами.
Большинство консольных утилит работают так: читают данные из stdin, обрабатывают в соответствие с аргументами, выводят в stdout, сообщения о возникающих ошибках выводят в stderr.
Мы можем перенаправить все стандартные потоки, т. е. подменить откуда команда получает свой ввод или вывод. Например для поиска подстроки ERROR в файле service.log при помощи grep можно написать следующее:
grep --fixed-strings ERROR <service.log
Здесь \< — оператор перенаправления ввода. Вышеуказанная команда говорит использовать как stdin файл service.log вместо терминала.
Чтобы потом сохранить найденные строки в файл search.log напишем следующее:
grep --fixed-strings ERROR <service.log >search.log
\< — оператор перенаправления вывода. Он говорит использовать как stdout файл search.log вместо терминала. При этом файл будет создан, если не существует. Если же такой файл существует, то команда не запустится и вернет ошибку. Используйте >| чтобы перезаписать файл. Чтобы не перетирать файл следует использовать >> — этот оператор запишет вывод в конец файла, если файл существует.
По умолчанию операторы перенаправления вывода используют stdout, чтобы перенаправить другой поток, например stderr, укажите его номер перед оператором:
grep --fixed-strings ERROR <service.log 2>>errors.log
Также можно явно указать, что весь вывод должен направляться в один поток:
stdout: 2>&1
stderr: 1>&2
Чтобы заглушить вывод команды можно воспользоваться специальным файлом /dev/null:
grep --fixed-strings ERROR <service.log &>/dev/null
Результат выполнения команд
Все программы после выхода передают в родительский процесс целое число — код выхода. 0 означает успех, остальные коды отведены под различные виды ошибок. Значение ненулевых кодов зависит от конкретной программы.
В Shell этот код выхода последней команды сохраняется в специальную переменную $?:
echo Hello World
echo $?
Код выполнения команды можно инвертировать при помощи !:
! false
echo $?
false — встроенная команда, которая всегда возвращает 1
Группировка команд
Как несложно догадаться, чтобы запустить несколько команд в одном скрипте, достаточно написать их на разных строках:
echo 1
echo 2
Вместо перевода строки можно использовать символ ;
echo 1 ; echo 2
Оба этих варианта запустят указанные команды последовательно. Shell поддерживает два оператора группировки команд:
{ echo 1 ; echo 2 ; }
запускает команды в текущем окружении оболочки, все присвоенные переменные и настройки оболочки сохранятся вне группы;
( echo 1 ; echo 2 )
запускает команды в отдельной под-оболочке, переменные и настройки не сохранятся.
Например, мы можем перенаправить вывод обеих команд в файл:
{ echo 1 ; echo 2 ; } >log.txt
Или включить режим трассировки (вывода выполняемых команд) для отдельной команды:
(set -x ; grep --fixed-strings ERROR <server.log | wc --lines)
Если мы воспользуемся { set -x; … }, то этот режим останется включенным после выхода из группы.
Также в Shell заданы условные операторы группировки && (логическое И) и || (логическое ИЛИ):
command1 && command2 — вторая команда запустится, если первая завершится успешно;
command1 || command2 — вторая команда запустится, если первая завершится с ошибкой;
Этими операторами можно симулировать условия, не стоит так делать, т.к. это существенно ухудшает читаемость.
Цепочки команд
Для сложной обработки данных в Shell часто применяют несколько команд последовательно к одному вводу, например чтобы посчитать число строк с ошибками в логе:
grep --fixed-strings ERROR <service.log >result_1
wc --lines <result_1
Такую громоздкую запись можно заменить на:
grep --fixed-strings ERROR <service.log | wc --lines
Здесь | — оператор сцепления команд, он перенаправляет stdout левой в stdin правой.
Код выхода цепочки — это код выхода последней команды в цепочке. Этот код также можно инвертировать при помощи !.
Каждая команда в цепочке выполняется в отдельной под-оболочке, присвоенные переменные и настройки не сохранятся.
Подстановки
Подстановка переменных
Рассмотрим расширенный синтаксис подстановки переменных:
${parameter:-word} — подстановка с значением по умолчанию. Если parameter не задан или пуст, то подставляется word;
${parameter:+word} — подстановка с альтернативным значением. Если parameter не задан или пуст, то подставляется пустая строка, иначе подставляется word.
Остальные подстановки см. в документации.
Подстановка команд
Аналогично значениям переменных мы можем подставлять stdout команд при помощи $():
echo $(echo 1)
X=$(echo 2)
Подставляемые команды запускаются в под-оболочке, аналогично группе ().
Арифметическая подстановка
Выражения внутри $(()) воспринимаются как целочисленные арифметические выражения:
echo $(( 1 + 3 ))
Асинхронные команды
Shell поддерживает запуск команд в фоне при помощи оператора &:
command &
ID процесса (PID) последней фоновой команды сохраняется в специальную переменную $!, мы можем запомнить значение этой переменной, чтобы дождаться результата выполнения команды:
command &
command_pid=$!
# do something...
wait $command_pid
При вызове wait без аргументов команда дожидается выполнения всех асинхронно запущенных команд.
Условия
if-then-else-fi
if command1
then
command2
elif command3
then
command4
else
command5
fi
Если command1 завершается успешно, то выполняется command2 и т. д.
Для проверки простых условий используется встроенная команда [ (test):
VERBOSE=1
grep --quiet --fixed-strings ERROR <server.log
if [ $VERBOSE -eq 1 ] && [ $? -eq 0 ]
then
echo 'found errors in server.log'
fi
или
VERBOSE=1
if [ $VERBOSE -eq 1 ] && grep --quiet --fixed-strings ERROR <server.log
then
echo 'found errors in server.log'
Аргументы команды [ см. в man test.
case
В Shell есть оператор множественного выбора case (аналог switch-case или match):
case word in
pattern1)
command1
;;
pattern2)
command2
;;
pattern3)
command3
esac
Здесь word — это строка, чаше всего результат подстановки. Синтаксис шаблонов совпадает с подстановочными шаблонами.
Пример:
case $BASE-$VARIANT in
*-pypy)
SUFFIX=pypy-$DEBIAN_VERSION
;;
debian-python)
SUFFIX=$DEBIAN_VERSION
;;
alpine-python)
SUFFIX=alpine
;;
*)
echo 'unknown variant'
exit 1
esac
export SUFFIX
Циклы
for
for name in word1 word2 word3
do
command
done
word должно раскрываться в последовательность строк. Каждый элемент присваивается переменной name, затем выполняются команды в блоке do … done.
Пример:
for i in 1 2 3 4 5
do
echo "i = $i"
done
for i in "1 2 3 4 5"
do
echo "i = $i"
done
list=$(printf '1\n2\n3\n4\n5')
for i in $listte
do
echo "i = $i"
done
while
while command1
do
command2
done
Операторы раннего выхода
Shell поддерживает операторы раннего выхода из цикла break и continue:
break — завершает цикл
continue — переходит к следующей итерации
Пример:
for i in ~/*.txt
do
size=$(stat -c %s "$i")
if [ "$size" -ne "10000" ]; then
continue
fi
done
Функции
Shell поддерживает группировку кода в функции:
function_name() {
}
Аргументы функции передаются при помощи специальных переменных:
$@ — аргументы в виде списка;
$* — аргументы в виде строки;
$# — число аргументов;
$n — один аргумент (n >= 1);
Также для работы с аргументами используют оператор shift, который сдвигает все аргументы на один влево, поглощая первый аргумент.
Внутри функций также доступна команда return EXIT_CODE, которая завершает функцию с указанным кодом выхода. Если код не указан, то он равен нулю.
Пример:
join_by() {
if [ "$#" -eq 0 ]; then
return
fi
delimiter="$1"
shift
while [ "$#" -gt 1 ]; do
printf '%s%b' "$1" "$delimiter"
shift
done
printf '%s' "$1"
}
Инструменты
При написании скриптов следует использовать следующие инструменты:
shellcheck — статический анализатор Shell скриптов;
shfmt — форматировщик скриптов;
VSCode
Для VSCode следует использовать следующие плагины:
shell-format (foxundermoon.shell-format)
ShellCheck (timonwong.shellcheck)
Скрипты
Мы будем начинать все скрипты так:
#!/bin/sh
# shellcheck shell=sh
set -eu
-
!/bin/sh — директива, указывающая команду, которой следует запускать исполняемый файл;
- # shellcheck shell=sh — директива ShellCheck, указывающая диалект Shell использующийся при проверке;
- set -eu — устанавливаем опции Shell
- -e — включает мгновенный выход при неуспешном завершении любой команды;
- -u — подстановка незаданных переменных приводит к ошибке;
- Другие опции Shell смотри в man set;
- При отладке следует использовать set -x, эта опция включает трассировку команд.
Примеры
Считаем размер всех текстовых файлов в домашней директории:
#!/bin/sh
# shellcheck shell=sh
set -eu
total_size() {
total=0
while read -r p; do
size=$(stat -c %s "$p")
total=$((total + ${size:-0}))
done
echo $total
}
find ~ -type f -iname '*.txt' | total_size
Выводим число запущенных программ:
#!/bin/sh
# shellcheck shell=sh
set -eu
format_executable() {
tr '\0' '\n' | sed -e 's:[\n\t ]:\n:g' | head -n 1 | tr '/' '\n' | tail -n 1
}
list_executables() {
for p in /proc/*/cmdline; do
if [ "$p" = "/proc/self/cmdline" ] || [ "$p" = "/proc/thread-self/cmdline" ]; then
continue
fi
executable=$(format_executable <"$p")
if [ -n "$executable" ]; then
echo "$executable"
fi
done
}
list_executables | sort | uniq -c | sort -n -r -k 1
Домашнее задание
Безопасность ВМ — УЗ
- Сгенерируйте новый ssh ключ
- Создайте нового персонального пользователя на ВМ
- Добавьте к новому пользователю публичную часть ключа
- Добавите пользователя в группу wheel, sudo
Безопасность ВМ — Сеть
Проверьте, что нет работающих сетевых сервисов, о которых вы не знаете
Полезные команды
useradd, usermod, gpasswd, vim, nano, ssh-keygen, cat
политика nopassword: user_name ALL=(ALL) NOPASSWD:ALL
Как сдавать
Выполнение домашнего задания займет около 2-х часов
Домашнее задание нужно выполнять на вторых машинах
Дедлайн
Задание нужно выполнить до второй практики
◉ После выполнения задания напишите в чат своего преподавателя по практике «Я выполнил (а) задание, готов (а) к проверке»