Instalacja i konfiguracja ze skryptu
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 iperf3Nieinteraktywnie 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: falseGwiazdka 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_configgrep -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
.origrobiona tylko raz. Inaczej drugie uruchomienie nadpisałoby oryginał już zmienioną wersją. sed -i -Ezmienia plik w miejscu, a0,/wzorzec/to adres z GNU sed: „od początku do pierwszego trafienia”. Podmieniamy tylko pierwsze wystąpienie, zakomentowane albo nie. Pustes//.../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] GotoweI 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] Gotowediff /etc/ssh/sshd_config.orig /etc/ssh/sshd_config42c42
< #PermitRootLogin prohibit-password
---
> PermitRootLogin no
44c44
< #MaxAuthTries 6
---
> MaxAuthTries 3Dokł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 yesTwó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/configTemplate: iperf3/start_daemon
Type: boolean
Default: false
Description: Start Iperf3 as a daemon automatically?
db_input high iperf3/start_daemon || trueSkrypt 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
| Komenda | Flaga | Co robi |
|---|---|---|
apt-get | -y | odpowiada „tak” na pytanie o instalację |
apt-get | -q | mniej komunikatów |
apt-get | -o Dpkg::Options::=--force-confdef | przy pliku konfiguracyjnym weź domyślną decyzję |
apt-get | -o Dpkg::Options::=--force-confold | a jak jej brak, zostaw moją wersję |
apt-get | -s | symulacja, nic nie instaluje |
DEBIAN_FRONTEND | =noninteractive | debconf nie zadaje pytań |
debconf-set-selections | (z wejścia) | odpowiedzi na pytania pakietu z wyprzedzeniem |
debconf-show | PAKIET | jakie odpowiedzi ma pakiet |
dnf | -y / -q | bez pytań / ciszej |
rpm | -q PAKIET | czy pakiet jest zainstalowany (kod wyjścia) |
dpkg-query | -W -f='${Status}' | stan pakietu na Ubuntu |
grep | -qxF | cicho, cała linijka, zwykły tekst |
sed | -i / -i.bak | zmień w miejscu / z kopią |
sed | -E '0,/re/s//X/' | podmień tylko pierwsze trafienie (GNU) |
sshd | -t / -T | sprawdź 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 toolNaprawa: 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 skonfigurowaneI 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 OKKaż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: trueA 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.