Przejdź do treści
Lekcja 104 z 117

Kurs › Moduł 11 › Lekcja 104

Instalacja i konfiguracja ze skryptu

⏱ ok. 11 min czytania· moduł 11: Automatyzacja: Git, Ansible i Bash w pracy admina

Po tej lekcji: kursant pisze skrypt, który instaluje pakiety przez apt i dnf bez żadnych pytań (DEBIAN_FRONTEND=noninteractive, debconf, -y), zmienia konfigurację przez sed tak, że można go uruchomić dowolnie wiele razy, i rozumie, czym jest idempotencja w Bashu.

Wstęp

Piątek, 17:30. Odpalasz skrypt instalujący pakiety na nowym serwerze i idziesz po kurtkę. W poniedziałek wracasz. Skrypt stoi. Od trzech dni. A na ekranie:

Configuring iperf3
------------------

Choose this option if Iperf3 should start automatically as a daemon, now and at
boot time.

Start Iperf3 as a daemon automatically? [yes/no]

Jeden pakiet zadał jedno pytanie i cały weekend czekał na odpowiedź. Jak kurier, który stoi pod drzwiami, bo nikt nie podpisał się na tablecie. Dziś uczymy się instalować tak, żeby nikt o nic nie pytał, i pisać skrypty, które po drugim uruchomieniu niczego nie psują.

apt bez pytań

Hurtownia z kurierem, czyli menedżer pakietów, pyta o trzy rodzaje rzeczy. Trzeba wyłączyć wszystkie trzy.

Pytanie 1: „czy na pewno?”. To załatwia -y.

Pytanie 2: pytania samego pakietu. Na Debianie i Ubuntu zadaje je debconf, czyli formularz konfiguracyjny pakietu. Niebieskie okienko przy instalacji Postfixa albo pytanie o iperf3 z haka. Wyłączasz je zmienną:

sudo DEBIAN_FRONTEND=noninteractive apt-get install -y iperf3

Nieinteraktywnie debconf bierze odpowiedź domyślną. A jeśli chcesz inną niż domyślna? Wypełniasz formularz z wyprzedzeniem. Nazwę pytania znajdziesz w pliku templates pakietu albo przez debconf-get-selections z pakietu debconf-utils na maszynie, gdzie pakiet już jest.

echo 'iperf3 iperf3/start_daemon boolean false' | sudo debconf-set-selections
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -q iperf3
sudo debconf-show iperf3
* iperf3/start_daemon: false

Gwiazdka znaczy: odpowiedź była ustawiona, a nie zgadnięta. Format linijki to: pakiet, nazwa pytania, typ, wartość.

Pytanie 3: plik konfiguracyjny. Zmieniłeś ręcznie plik w /etc, a aktualizacja pakietu przynosi nową wersję tego pliku. dpkg pyta: wziąć twoją czy nową? W skrypcie odpowiadasz z góry:

sudo DEBIAN_FRONTEND=noninteractive apt-get -y \
    -o Dpkg::Options::=--force-confdef \
    -o Dpkg::Options::=--force-confold \
    upgrade

--force-confdef znaczy „zrób to, co jest domyślne”, a gdy domyślnego nie ma, --force-confold mówi „zostaw moją wersję”. Nowa wersja ląduje obok jako .dpkg-dist i możesz ją spokojnie porównać diff-em.

[Ubuntu] Na Ubuntu jest jeszcze needrestart, który po aktualizacji pyta, które usługi zrestartować. W konfiguracji na Ubuntu 24.04 jest napisane, że przy wywołaniu z APT domyślnie restartuje sam, a w trybie nieinteraktywnym nie pyta. Jeśli wolisz tylko listę bez restartów: NEEDRESTART_MODE=l.

I jeszcze jedno: w skryptach apt-get, nie apt. Sam apt mówi to wprost:

WARNING: apt does not have a stable CLI interface. Use with caution in scripts.

apt jest dla ludzi: kolorki, pasek postępu, format może się zmienić. apt-get jest dla skryptów.

dnf bez pytań

[RHEL] Tu jest prościej, bo pakiety RPM z zasady nie zadają pytań przy instalacji. Zostaje „czy na pewno?”:

sudo dnf install -y -q chrony vim-enhanced
sudo dnf upgrade -y

-y przy okazji zgadza się na import klucza GPG nowego repozytorium, więc dodawaj tylko repozytoria, którym ufasz. Pliki konfiguracyjne RPM załatwia po swojemu: twoja wersja zostaje, a nowa ląduje obok jako .rpmnew. Albo odwrotnie, stara jako .rpmsave. Po dużej aktualizacji warto poszukać: sudo find /etc -name '*.rpmnew'.

Idempotencja w Bashu

Pamiętasz przycisk w windzie z lekcji o Ansible? Ansible ma idempotencję w modułach. W Bashu musisz ją napisać sam. Zasada jedna: najpierw sprawdź, potem zrób. Porównaj:

# Źle: każde uruchomienie dopisuje kolejną linijkę
echo 'PermitRootLogin no' >> sshd_config

# Dobrze: dopisz tylko, jeśli takiej linijki jeszcze nie ma
grep -qxF 'PermitRootLogin no' sshd_config || echo 'PermitRootLogin no' >> sshd_config

grep -q nic nie wypisuje, tylko daje kciuk w górę albo w dół. -x znaczy „cała linijka”, -F znaczy „to zwykły tekst, nie wzorzec”. Więcej przykładów z tej samej rodziny:

mkdir -p /opt/app                                  # nie krzyczy, gdy już jest
id deploy &>/dev/null || useradd -m deploy          # konto tylko raz
ln -sfn /opt/app/v2 /opt/app/current               # link zawsze w dobre miejsce
install -m 0644 -o root -g root app.conf /etc/app/  # plik z prawami za jednym razem
dpkg-query -W -f='${Status}' htop 2>/dev/null | grep -q 'ok installed'   # [Ubuntu] jest?
rpm -q htop &>/dev/null                             # [RHEL] jest?

systemctl enable --now też jest idempotentne: włączona usługa zostaje włączona.

sed, który nie psuje przy drugim przebiegu

Najczęstsza konfiguracja w Linuksie to „klucz wartość” w pliku tekstowym, często zakomentowana: #PermitRootLogin prohibit-password. Chcesz ją ustawić. Przypadki są trzy: linijka zakomentowana, linijka z inną wartością, linijki nie ma wcale. Do tego czwarty: już jest dobrze i nic nie rób. Jak rubryka w formularzu: wpisujesz tylko, gdy jest pusta albo błędna. Nie dopisujesz drugiej rubryki na marginesie.

ustaw() {
    local plik=$1 klucz=$2 wartosc=$3
    if grep -qE "^${klucz}[[:space:]]+${wartosc}[[:space:]]*$" "$plik"; then
        return 0
    fi
    [[ -e $plik.orig ]] || cp -p -- "$plik" "$plik.orig"
    if grep -qE "^#?[[:space:]]*${klucz}[[:space:]]" "$plik"; then
        sed -i -E "0,/^#?[[:space:]]*${klucz}[[:space:]].*/s//${klucz} ${wartosc}/" "$plik"
    else
        printf '%s %s\n' "$klucz" "$wartosc" >> "$plik"
    fi
    log "SSH: $klucz $wartosc"
    zmieniono_ssh=1
}

Co tu się dzieje:

  • Najpierw sprawdzenie: jest dokładnie tak, jak chcemy? Wychodzimy bez dotykania pliku.
  • Kopia .orig robiona tylko raz. Inaczej drugie uruchomienie nadpisałoby oryginał już zmienioną wersją.
  • sed -i -E zmienia plik w miejscu, a 0,/wzorzec/ to adres z GNU sed: „od początku do pierwszego trafienia”. Podmieniamy tylko pierwsze wystąpienie, zakomentowane albo nie. Puste s//.../ używa ostatniego wzorca, żeby nie pisać go dwa razy.
  • Nie było takiego klucza? Dopisujemy na końcu.

Cały skrypt

#!/usr/bin/env bash
# przygotuj-serwer.sh - idempotentne przygotowanie serwera (Ubuntu/Debian i RHEL)
# Można uruchamiać wiele razy: robi tylko to, czego jeszcze nie ma.
set -euo pipefail

SSHD_KONF=${SSHD_KONF:-/etc/ssh/sshd_config}
ADMIN=${ADMIN:-deploy}
zmieniono_ssh=0

log() { printf '[%(%F %T)T] %s\n' -1 "$*"; }

(( EUID == 0 )) || { echo "Uruchom jako root (sudo)." >&2; exit 1; }

# 1. Która rodzina systemu?
# shellcheck source=/dev/null
. /etc/os-release
case " ${ID} ${ID_LIKE:-} " in
    *" debian "*|*" ubuntu "*)
        rodzina=deb
        pakiety=(chrony curl bash-completion vim)
        grupa_admin=sudo
        usluga_czasu=chrony ;;
    *" rhel "*|*" fedora "*|*" centos "*)
        rodzina=rpm
        pakiety=(chrony curl bash-completion vim-enhanced)
        grupa_admin=wheel
        usluga_czasu=chronyd ;;
    *) echo "Nieznany system: $ID" >&2; exit 1 ;;
esac
log "System: $PRETTY_NAME (rodzina $rodzina)"

# 2. Pakiety: instaluj tylko brakujące, bez żadnych pytań
brakujace=()
for p in "${pakiety[@]}"; do
    if [[ $rodzina == deb ]]; then
        dpkg-query -W -f='${Status}' "$p" 2>/dev/null | grep -q 'ok installed' || brakujace+=("$p")
    else
        rpm -q "$p" &>/dev/null || brakujace+=("$p")
    fi
done
if (( ${#brakujace[@]} )); then
    log "Instaluję: ${brakujace[*]}"
    if [[ $rodzina == deb ]]; then
        export DEBIAN_FRONTEND=noninteractive
        apt-get update -q
        apt-get install -y -q \
            -o Dpkg::Options::=--force-confdef \
            -o Dpkg::Options::=--force-confold \
            "${brakujace[@]}"
    else
        dnf install -y -q "${brakujace[@]}"
    fi
else
    log "Pakiety: wszystko już jest"
fi

# 3. Konto admina
if id "$ADMIN" &>/dev/null; then
    log "Konto $ADMIN: już istnieje"
else
    useradd -m -s /bin/bash -G "$grupa_admin" "$ADMIN"
    log "Konto $ADMIN: założone"
fi

# 4. Jedna opcja w konfiguracji SSH: podmień albo dopisz, nigdy dwa razy
ustaw() {
    local plik=$1 klucz=$2 wartosc=$3
    if grep -qE "^${klucz}[[:space:]]+${wartosc}[[:space:]]*$" "$plik"; then
        return 0
    fi
    [[ -e $plik.orig ]] || cp -p -- "$plik" "$plik.orig"
    if grep -qE "^#?[[:space:]]*${klucz}[[:space:]]" "$plik"; then
        sed -i -E "0,/^#?[[:space:]]*${klucz}[[:space:]].*/s//${klucz} ${wartosc}/" "$plik"
    else
        printf '%s %s\n' "$klucz" "$wartosc" >> "$plik"
    fi
    log "SSH: $klucz $wartosc"
    zmieniono_ssh=1
}
ustaw "$SSHD_KONF" PermitRootLogin no
ustaw "$SSHD_KONF" MaxAuthTries 3

if (( zmieniono_ssh )); then
    sshd -t -f "$SSHD_KONF"
    systemctl reload ssh 2>/dev/null || systemctl reload sshd 2>/dev/null \
        || log "UWAGA: nie udało się przeładować SSH"
else
    log "SSH: bez zmian"
fi

# 5. Czas: usługa włączona i uruchomiona
systemctl enable --now "$usluga_czasu" 2>/dev/null \
    || log "UWAGA: nie udało się włączyć $usluga_czasu"
log "Gotowe"

ShellCheck: czysto. Zwróć uwagę na różnice rodzin: vim kontra vim-enhanced, grupa sudo kontra wheel, usługa chrony kontra chronyd, a na RHEL usługa SSH nazywa się sshd, na Ubuntu ssh. Przed przeładowaniem SSH jest sshd -t, czyli sprawdzenie składni. Z set -e zły plik zatrzyma skrypt, zanim zamknie Ci drzwi.

Ostrzeżenie: ten skrypt zmienia konfigurację SSH. Testuj na maszynie wirtualnej, a na prawdziwym serwerze trzymaj otwartą drugą sesję SSH, dopóki nie sprawdzisz, że nowa sesja się loguje.

Pierwsze uruchomienie, na świeżej maszynie wirtualnej z Ubuntu:

[2026-11-03 22:52:23] System: Ubuntu 24.04.5 LTS (rodzina deb)
[2026-11-03 22:52:23] Instaluję: chrony
...
[2026-11-03 22:52:35] Konto deploy: założone
[2026-11-03 22:52:35] SSH: PermitRootLogin no
[2026-11-03 22:52:35] SSH: MaxAuthTries 3
[2026-11-03 22:52:35] Gotowe

I drugie, chwilę później:

[2026-11-03 22:52:37] System: Ubuntu 24.04.5 LTS (rodzina deb)
[2026-11-03 22:52:37] Pakiety: wszystko już jest
[2026-11-03 22:52:37] Konto deploy: już istnieje
[2026-11-03 22:52:37] SSH: bez zmian
[2026-11-03 22:52:37] Gotowe
diff /etc/ssh/sshd_config.orig /etc/ssh/sshd_config
42c42
< #PermitRootLogin prohibit-password
---
> PermitRootLogin no
44c44
< #MaxAuthTries 6
---
> MaxAuthTries 3

Dokładnie dwie zmienione linijki, każda raz. Skrypt jest jak dobry kelner: drugi raz nie przynosi tej samej zupy.

Pułapka: pierwsza wartość wygrywa

Na Ubuntu na samej górze sshd_config stoi Include /etc/ssh/sshd_config.d/*.conf. A SSH dla każdej opcji bierze pierwszą wartość, jaką przeczyta. Sprawdziłem na kopii: w głównym pliku PasswordAuthentication no, w dołączanym 50-cloud-init.conf jest PasswordAuthentication yes:

sudo sshd -T | grep -E '^(passwordauthentication|permitrootlogin)'
permitrootlogin no
passwordauthentication yes

Twój sed zadziałał bez zarzutu, a serwer dalej wpuszcza na hasło. Dlatego nowoczesny sposób to własny plik w sshd_config.d z niskim numerem, na przykład 10-firma.conf, który wczyta się przed innymi. A sshd -T po każdej zmianie pokazuje, co serwer naprawdę myśli. [RHEL] Na RHEL to samo: Include na górze i plik 50-redhat.conf w sshd_config.d.

Jak to działa pod spodem

Każdy pakiet .deb ma w środku skrypty: preinst, postinst, config i plik templates z pytaniami. Zajrzyjmy do iperf3 bez instalowania:

apt-get download iperf3
dpkg-deb -e iperf3_*.deb ctrl
head -4 ctrl/templates
grep db_input ctrl/config
Template: iperf3/start_daemon
Type: boolean
Default: false
Description: Start Iperf3 as a daemon automatically?
db_input high iperf3/start_daemon || true

Skrypt config mówi debconfowi: zadaj pytanie o priorytecie high. debconf patrzy na DEBIAN_FRONTEND. Przy Dialog rysuje niebieskie okienko, przy readline pyta tekstem, a przy noninteractive nie pyta nikogo i bierze to, co ma w swojej bazie w /var/cache/debconf/. debconf-set-selections wpisuje odpowiedź właśnie do tej bazy, zanim pakiet zapyta.

Ściąga flag

KomendaFlagaCo robi
apt-get-yodpowiada „tak” na pytanie o instalację
apt-get-qmniej komunikatów
apt-get-o Dpkg::Options::=--force-confdefprzy pliku konfiguracyjnym weź domyślną decyzję
apt-get-o Dpkg::Options::=--force-confolda jak jej brak, zostaw moją wersję
apt-get-ssymulacja, nic nie instaluje
DEBIAN_FRONTEND=noninteractivedebconf nie zadaje pytań
debconf-set-selections(z wejścia)odpowiedzi na pytania pakietu z wyprzedzeniem
debconf-showPAKIETjakie odpowiedzi ma pakiet
dnf-y / -qbez pytań / ciszej
rpm-q PAKIETczy pakiet jest zainstalowany (kod wyjścia)
dpkg-query-W -f='${Status}'stan pakietu na Ubuntu
grep-qxFcicho, cała linijka, zwykły tekst
sed-i / -i.bakzmień w miejscu / z kopią
sed-E '0,/re/s//X/'podmień tylko pierwsze trafienie (GNU)
sshd-t / -Tsprawdź składnię / pokaż końcowe ustawienia

⚠ Typowy błąd

Skrypt zawisł na pytaniu z haka. Admin naciska Ctrl+C i uruchamia go jeszcze raz. A apt na to:

E: dpkg was interrupted, you must manually run 'dpkg --configure -a' to correct the problem.

Przerwałeś dpkg w połowie konfigurowania pakietu. Pakiet jest rozpakowany, ale nieskonfigurowany. Jak meble wniesione do mieszkania, ale niezmontowane. dpkg --audit pokaże, kto został w połowie:

The following packages are only half configured, probably due to problems
configuring them the first time.
 iperf3               Internet Protocol bandwidth measuring tool

Naprawa: odpowiedz z wyprzedzeniem i dokończ konfigurację bez pytań:

echo 'iperf3 iperf3/start_daemon boolean false' | sudo debconf-set-selections
sudo DEBIAN_FRONTEND=noninteractive dpkg --configure -a
sudo dpkg --audit          # pusto = wszystko skonfigurowane

I nigdy więcej apt-get install w skrypcie bez DEBIAN_FRONTEND=noninteractive i -y. Drugi grzech: brak apt-get update przed instalacją na świeżym serwerze. Lista pakietów jest stara albo pusta i dostajesz E: Unable to locate package. Z kolei update przy każdym uruchomieniu niepotrzebnie męczy repozytoria, dlatego w skrypcie robię go tylko wtedy, gdy czegoś brakuje.

✎ Zadanie

Napisz własny przygotuj-serwer.sh dla swojej maszyny wirtualnej (Ubuntu albo RHEL, a najlepiej obu). Niech instaluje bez pytań trzy pakiety, zakłada konto deploy, ustawia funkcją ustaw dwie opcje w kopii pliku /etc/ssh/sshd_config i sprawdza ją sshd -t -f. Uruchom go trzy razy i pokaż, że za drugim i trzecim razem nic się nie zmienia: diff kopii z .orig ma pokazać każdą zmianę dokładnie raz. Na Ubuntu zainstaluj nieinteraktywnie iperf3 z odpowiedzią true ustawioną przez debconf-set-selections i sprawdź debconf-show iperf3.

Pokaż rozwiązanie

Skrypt przygotuj-serwer.sh z poprzedniej lekcji puszczamy trzy razy na kopii konfiguracji SSH. Pierwszy przebieg zmienia, drugi i trzeci tylko sprawdzają:

cp /etc/ssh/sshd_config ~/sshd_test
for i in 1 2 3; do sudo SSHD_KONF=~/sshd_test ./przygotuj-serwer.sh; done
diff ~/sshd_test.orig ~/sshd_test
sudo sshd -t -f ~/sshd_test && echo "składnia OK"
[2026-11-04 02:38:24] SSH: PermitRootLogin no
[2026-11-04 02:38:24] SSH: MaxAuthTries 3
...
[2026-11-04 02:38:24] SSH: bez zmian
...
[2026-11-04 02:38:25] SSH: bez zmian
42c42
< #PermitRootLogin prohibit-password
---
> PermitRootLogin no
44c44
< #MaxAuthTries 6
---
> MaxAuthTries 3
składnia OK

Każda zmiana dokładnie raz. Teraz iperf3 z odpowiedzią „tak”:

echo 'iperf3 iperf3/start_daemon boolean true' | sudo debconf-set-selections
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y iperf3
sudo debconf-show iperf3
* iperf3/start_daemon: true

A jeśli iperf3 już był zainstalowany? Wtedy apt-get install nic nie zrobi, bo pakiet jest. Nową odpowiedź wczytujesz przez sudo dpkg-reconfigure -f noninteractive iperf3. Sprawdziłem: po true w multi-user.target.wants pojawia się iperf3.service, po false znika. Jedna linijka w bazie debconf i serwer sam wie, czego chcesz. Jak dobrze wyszkolony kelner, który pamięta, że pijesz bez cukru.

Co dalej

Skrypty z ostatnich lekcji biegają jako root, po wielu serwerach, z hasłami do baz. To już nie są zabawki. W następnej lekcji bezpieczne skrypty: gdzie trzymać hasła, jak nie zrobić rm -rf / przez pustą zmienną, czemu eval to zły pomysł i jakie uprawnienia dać skryptom.