Почти каждая инфраструктура рано или поздно проходит через один и тот же этап взросления.
Сначала:
- серверов мало;
- все делается руками;
- root-пароли живут в заметках;
- SSH-доступ «вроде работает».
Потом серверов становится:
- 20,
- 50,
- 200,
- и внезапно оказывается, что инфраструктура уже напоминает археологический раскоп.
На одном сервере:
- deploy
На другом:
- ansible
На третьем:
- вообще доступ только под root.
А где-то еще живет легендарный:
user: test
password: test123
Именно в этот момент обычно приходит осознание:
пора приводить SSH-доступ и пользователей к единому виду.
Что мы хотим получить
Нормальная современная схема выглядит так:
- единый технический пользователь;
- SSH-ключи вместо паролей;
- sudo;
- единая схема доступа;
- возможность нормально использовать Ansible;
- централизованное управление.
Подход №1 - Shell-скрипт
Это классический bootstrap-подход.
Когда:
- Ansible еще нет;
- доступ только по root;
- нужно быстро подготовить сервер.
Shell все еще остается отличным инструментом для:
- первоначальной инициализации;
- rescue-сценариев;
- cloud-init;
- автоматической подготовки образов;
- bootstrap новых серверов.
Но важно понимать:
shell - это скорее стартовая точка.
Современный shell-скрипт подготовки сервера
Вот вариант скрипта, который:
- создает пользователя;
- генерирует SSH-ключ;
- настраивает authorized_keys;
- выдает sudo;
- отключает SSH-пароли;
- подготавливает сервер к работе с Ansible.
#!/usr/bin/env bash
set -euo pipefail
############################################
# Настройки
############################################
USERNAME="devops"
USER_SHELL="/bin/bash"
SSH_KEY_TYPE="ed25519"
SSH_KEY_COMMENT="Generated by bootstrap script"
DISABLE_PASSWORD_AUTH="true"
DISABLE_ROOT_LOGIN="false"
# Дополнительные публичные ключи
AUTHORIZED_KEYS=(
"ssh-ed25519 AAAAC3NzaC1..."
"ssh-rsa AAAAB3Nza..."
)
############################################
# Проверка root
############################################
if [[ "$(id -u)" -ne 0 ]]; then
echo "[ERROR] Скрипт нужно запускать от root"
exit 1
fi
############################################
# Проверка пользователя
############################################
if id "$USERNAME" >/dev/null 2>&1; then
echo "[INFO] Пользователь уже существует"
else
echo "[INFO] Создаем пользователя"
useradd \
--create-home \
--shell "$USER_SHELL" \
"$USERNAME"
fi
############################################
# Добавление sudo
############################################
echo "[INFO] Настраиваем sudo"
cat <<EOF >/etc/sudoers.d/$USERNAME
$USERNAME ALL=(ALL) NOPASSWD: ALL
EOF
chmod 440 /etc/sudoers.d/$USERNAME
############################################
# Создание .ssh
############################################
SSH_DIR="/home/$USERNAME/.ssh"
mkdir -p "$SSH_DIR"
chmod 700 "$SSH_DIR"
chown -R "$USERNAME:$USERNAME" "$SSH_DIR"
############################################
# Генерация SSH ключа
############################################
KEY_PATH="$SSH_DIR/id_${SSH_KEY_TYPE}"
if [[ ! -f "$KEY_PATH" ]]; then
echo "[INFO] Генерируем SSH ключ"
sudo -u "$USERNAME" ssh-keygen \
-t "$SSH_KEY_TYPE" \
-N "" \
-C "$SSH_KEY_COMMENT" \
-f "$KEY_PATH"
else
echo "[INFO] SSH ключ уже существует"
fi
############################################
# authorized_keys
############################################
AUTHORIZED_KEYS_FILE="$SSH_DIR/authorized_keys"
touch "$AUTHORIZED_KEYS_FILE"
chmod 600 "$AUTHORIZED_KEYS_FILE"
for KEY in "${AUTHORIZED_KEYS[@]}"; do
if ! grep -qxF "$KEY" "$AUTHORIZED_KEYS_FILE"; then
echo "$KEY" >> "$AUTHORIZED_KEYS_FILE"
fi
done
############################################
# Добавляем локально сгенерированный ключ
############################################
GENERATED_PUBLIC_KEY=$(cat "${KEY_PATH}.pub")
if ! grep -qxF "$GENERATED_PUBLIC_KEY" "$AUTHORIZED_KEYS_FILE"; then
echo "$GENERATED_PUBLIC_KEY" >> "$AUTHORIZED_KEYS_FILE"
fi
############################################
# Исправление владельца
############################################
chown -R "$USERNAME:$USERNAME" "$SSH_DIR"
############################################
# SSH hardening
############################################
if [[ "$DISABLE_PASSWORD_AUTH" == "true" ]]; then
echo "[INFO] Отключаем PasswordAuthentication"
sed -i \
's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' \
/etc/ssh/sshd_config
fi
if [[ "$DISABLE_ROOT_LOGIN" == "true" ]]; then
echo "[INFO] Отключаем root login"
sed -i \
's/^#\?PermitRootLogin.*/PermitRootLogin no/' \
/etc/ssh/sshd_config
fi
############################################
# Перезапуск SSH
############################################
systemctl restart ssh || systemctl restart sshd
############################################
# Информация
############################################
SERVER_IP=$(hostname -I | awk '{print $1}')
echo ""
echo "======================================"
echo "Пользователь успешно подготовлен"
echo "======================================"
echo "Сервер: $SERVER_IP"
echo "Пользователь: $USERNAME"
echo "SSH ключ:"
echo "${KEY_PATH}.pub"
echo "======================================"
В скрипте добавил:
set -euo pipefail
Это:
- аварийное завершение при ошибках;
- защита от неинициализированных переменных;
- более безопасное выполнение.
Старые shell-скрипты без этого обычно работают по принципу:
«Ну ошибка и ошибка, едем дальше».
А потом половина сервера настроена, половина нет.
Добавил идемпотентность
Скрипт:
- не пересоздает пользователя;
- не дублирует ключи;
- не ломает существующие настройки.
Используем
ed25519
Сейчас это уже стандарт де-факто:
ssh-keygen -t ed25519
Добавил SSH hardening
Автоматически:
- отключаем парольную авторизацию;
- можем отключить root login.
Но почему shell все равно проигрывает Ansible
Вот тут начинается самое интересное.
Проблемы shell-подхода
1. Плохо масштабируется
На 3 серверах:
- нормально.
На 300:
- начинается боль.
2. Нет нормального состояния системы
Shell не знает:
- что уже применено;
- что изменилось;
- что сломалось.
Ansible это знает.
3. Сложно поддерживать
Через полгода shell-скрипт обычно выглядит как:
if grep ...
then
sed ...
else
awk ...
fi
А рядом комментарий:
# НЕ ТРОГАТЬ
# РАБОТАЕТ
Подход №2 - Ansible role
Вот тут уже начинается взрослая инфраструктура.
Ansible:
- знает состояние;
- умеет идемпотентность;
- централизует управление;
- нормально логирует изменения;
- хорошо масштабируется.
Структура роли
roles/
└── prepare_access/
├── defaults/
│ └── main.yml
├── tasks/
│ └── main.yml
├── handlers/
│ └── main.yml
defaults/main.yml
---
username: devops
user_shell: /bin/bash
generate_ssh_key: true
ssh_key_type: ed25519
ssh_key_comment: "Ansible generated key"
sudo_nopasswd: true
disable_ssh_password_auth: true
disable_root_login: false
additional_authorized_keys: []
tasks/main.yml
---
- name: Создание пользователя
user:
name: "{{ username }}"
shell: "{{ user_shell }}"
create_home: true
state: present
- name: Добавление sudo
user:
name: "{{ username }}"
groups: sudo
append: true
- name: Создание .ssh
file:
path: "/home/{{ username }}/.ssh"
state: directory
owner: "{{ username }}"
group: "{{ username }}"
mode: '0700'
- name: Генерация SSH ключа
openssh_keypair:
path: "/home/{{ username }}/.ssh/id_{{ ssh_key_type }}"
type: "{{ ssh_key_type }}"
owner: "{{ username }}"
group: "{{ username }}"
mode: '0600'
comment: "{{ ssh_key_comment }}"
- name: Чтение публичного ключа
slurp:
src: "/home/{{ username }}/.ssh/id_{{ ssh_key_type }}.pub"
register: generated_pubkey
- name: Добавление generated key
authorized_key:
user: "{{ username }}"
key: "{{ generated_pubkey['content'] | b64decode }}"
- name: Добавление дополнительных ключей
authorized_key:
user: "{{ username }}"
key: "{{ item }}"
loop: "{{ additional_authorized_keys }}"
- name: Настройка sudoers
copy:
dest: "/etc/sudoers.d/{{ username }}"
content: |
{{ username }} ALL=(ALL) NOPASSWD: ALL
mode: '0440'
- name: Отключение SSH паролей
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?PasswordAuthentication'
line: 'PasswordAuthentication no'
notify: restart ssh
- name: Отключение root login
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?PermitRootLogin'
line: 'PermitRootLogin no'
when: disable_root_login
notify: restart ssh
handlers/main.yml
---
- name: restart ssh
service:
name: ssh
state: restarted
Что в итоге лучше
Shell подходит для:
- bootstrap;
- rescue;
- cloud-init;
- первоначальной инициализации.
Ansible подходит для:
- production;
- сопровождения;
- поддержки;
- масштабирования;
- централизованного управления.
Итог
Shell-скрипты - это хороший старт.
Но Ansible - это уже инфраструктурная зрелость.
Когда:
- все серверы одинаковы;
- SSH стандартизирован;
- доступ централизован;
- изменения воспроизводимы -
инфраструктура перестает быть набором «магических серверов» и начинает становиться системой.
А это уже огромная разница между:
"работает - не трогай"
и:
"мы контролируем инфраструктуру"