Перейти к содержанию

Настройка ВМ

Установим основные пакеты, для этого сначала обновим локальную базу данных пакетного менеджера 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, так как:

  1. su требует пароль от root, а не от пользователя запускающего команду; довольно часто пользователю root не задают пароль или деактивируют его, что не позволяет использовать su;
  2. sudo поддерживает более тонкую настройку;
  3. 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) и установите следующие расширения. Указанные в скобках идентификаторы расширений можно скопировать в строку поиска:

  1. Remote - SSH (ms-vscode-remote.remote-ssh)
  2. Docker (ms-azuretools.vscode-docker)
  3. Ansible (redhat.ansible)
  4. 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
  1. !/bin/sh — директива, указывающая команду, которой следует запускать исполняемый файл;

  2. # shellcheck shell=sh — директива ShellCheck, указывающая диалект Shell использующийся при проверке;
  3. set -eu — устанавливаем опции Shell
  4. -e — включает мгновенный выход при неуспешном завершении любой команды;
  5. -u — подстановка незаданных переменных приводит к ошибке;
  6. Другие опции Shell смотри в man set;
  7. При отладке следует использовать 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

Домашнее задание

Безопасность ВМ — УЗ

  1. Сгенерируйте новый ssh ключ
  2. Создайте нового персонального пользователя на ВМ
  3. Добавьте к новому пользователю публичную часть ключа
  4. Добавите пользователя в группу wheel, sudo

Безопасность ВМ — Сеть

Проверьте, что нет работающих сетевых сервисов, о которых вы не знаете

Полезные команды

useradd, usermod, gpasswd, vim, nano, ssh-keygen, cat
политика nopassword: user_name ALL=(ALL) NOPASSWD:ALL

Как сдавать

Выполнение домашнего задания займет около 2-х часов
Домашнее задание нужно выполнять на вторых машинах

Дедлайн
Задание нужно выполнить до второй практики

◉ После выполнения задания напишите в чат своего преподавателя по практике «Я выполнил (а) задание, готов (а) к проверке»