docs: add 100 Days of DevOps challenge notes

This commit is contained in:
2026-08-04 23:18:41 +02:00
parent 76d4b4d716
commit a6ee7a2b07
24 changed files with 6901 additions and 0 deletions

View File

@@ -0,0 +1,303 @@
## Task 1
To accommodate the backup agent tool's specifications, the system admin team at xFusionCorp Industries requires the creation of a user with a non-interactive shell. Here's your task:
Create a user named siva with a non-interactive shell on App Server 2.
```bash
sudo useradd -s /sbin/nologin siva
# Verify
grep siva /etc/passwd
```
## Task 2
As part of the temporary assignment to the Nautilus project, a developer named siva requires access for a limited duration. To ensure smooth access management, a temporary user account with an expiry date is needed. Here's what you need to do:
Create a user named siva on App Server 1 in Stratos Datacenter. Set the expiry date to 2027-04-15, ensuring the user is created in lowercase as per standard protocol.
```bash
sudo useradd -e 2027-04-15 siva
# Verify
sudo chage -l siva | grep -i expire
```
## Task 3
Your task is to disable direct SSH root login on all app servers within the Stratos Datacenter.
App servers are these:
Server Name IP Hostname User Password Purpose
Application Server 1 Dynamic stapp01 tony Ir0nM@n Hosts Nautilus Application 1
Application Server 2 Dynamic stapp02 steve Am3ric@ Hosts Nautilus Application 2
Application Server 3 Dynamic stapp03 banner BigGr33n Hosts Nautilus Application 3
```bash
cat > harden_ssh.sh <<'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
CREDS="${1:-creds.txt}"
REMOTE_CMD='
echo "$SUDO_PASS" | sudo -S -p "" sed -i "s/^#*\s*PermitRootLogin.*/PermitRootLogin no/" /etc/ssh/sshd_config &&
echo "$SUDO_PASS" | sudo -S -p "" sshd -t &&
echo "$SUDO_PASS" | sudo -S -p "" systemctl restart sshd &&
echo "--- $(hostname) effective config ---" &&
echo "$SUDO_PASS" | sudo -S -p "" sshd -T | grep -i permitrootlogin
'
while read -r host user pass <&3; do
[[ -z "$host" || "$host" == \#* ]] && continue
echo "==> Hardening $host"
SSHPASS="$pass" sshpass -e ssh -n \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o LogLevel=ERROR \
"$user@$host" \
"SUDO_PASS='$pass' bash -c '$REMOTE_CMD'"
echo
done 3< "$CREDS"
SCRIPT
chmod +x harden_ssh.sh
# --- deps ---
sudo yum install -y sshpass 2>/dev/null || sudo apt install -y sshpass
./harden_ssh.sh
```
## Task 4
Your task is to grant executable permissions to the /tmp/xfusioncorp.sh script on App Server 1. Additionally, ensure that all users have the capability to execute it.
```bash
chmod a+rx /tmp/xfusioncorp.sh
```
## Task 5
Following a security audit, the xFusionCorp Industries security team has opted to enhance application and server security with SELinux. To initiate testing, the following requirements have been established for App server 2 in the Stratos Datacenter:
Install the required SELinux packages.
Permanently disable SELinux for the time being; it will be re-enabled after necessary configuration changes.
No need to reboot the server, as a scheduled maintenance reboot is already planned for tonight.
Disregard the current status of SELinux via the command line; the final status after the reboot should be disabled.
```bash
sudo yum install -y selinux-policy selinux-policy-targeted policycoreutils policycoreutils-python-utils libselinux-utils setools-console mcstrans
sudo sed -i 's/^SELINUX=.*/SELINUX=disabled/' /etc/selinux/config
# verify
grep '^SELINUX=' /etc/selinux/config
```
## Task 6
The Nautilus system admins team has prepared scripts to automate several day-to-day tasks. They want them to be deployed on all app servers in Stratos DC on a set schedule. Before that they need to test similar functionality with a sample cron job. Therefore, perform the steps below:
a. Install cronie package on all Nautilus app servers and start crond service.
b. Add a cron */5 * * * * echo hello > /tmp/cron_text for root user
```bash
# --- creds ---
cat > creds.txt <<'EOF'
stapp01 tony Ir0nM@n
stapp02 steve Am3ric@
stapp03 banner BigGr33n
EOF
chmod 600 creds.txt
# --- deploy script ---
cat > deploy_cron.sh <<'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
CREDS="${1:-creds.txt}"
# Everything runs inside one root shell: authenticate sudo ONCE via -S,
# then stdin is free for the crontab pipe inside.
REMOTE_CMD='
yum install -y cronie
systemctl enable --now crond
echo "*/5 * * * * echo hello > /tmp/cron_text" | crontab -u root -
echo "--- $(hostname) ---"
systemctl is-active crond
crontab -u root -l
'
while read -r host user pass <&3; do
[[ -z "$host" || "$host" == \#* ]] && continue
echo "==> Deploying cron on $host"
SSHPASS="$pass" sshpass -e ssh -n \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o LogLevel=ERROR \
"$user@$host" \
"echo '$pass' | sudo -S -p '' bash -c '$REMOTE_CMD'" \
2> >(grep -v '^\[sudo\]' >&2)
echo
done 3< "$CREDS"
SCRIPT
chmod +x deploy_cron.sh
# --- run ---
./deploy_cron.sh
```
## Task 7
The system admins team of xFusionCorp Industries has set up some scripts on jump host that run on regular intervals and perform operations on all app servers in Stratos Datacenter. To make these scripts work properly we need to make sure the thor user on jump host has password-less SSH access to all app servers through their respective sudo users (i.e tony for app server 1). Based on the requirements, perform the following:
Set up a password-less authentication from user thor on jump host to all app servers through their respective sudo users.
```bash
ssh-keygen -t rsa -b 4096 -N '' -f ~/.ssh/id_rsa
# --- creds ---
cat > creds.txt <<'EOF'
stapp01 tony Ir0nM@n
stapp02 steve Am3ric@
stapp03 banner BigGr33n
EOF
chmod 600 creds.txt
# --- copy keys ---
cat > setup_keys.sh <<'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
CREDS="${1:-creds.txt}"
while read -r host user pass <&3; do
[[ -z "$host" || "$host" == \#* ]] && continue
echo "==> Copying key to $host"
SSHPASS="$pass" sshpass -e ssh-copy-id \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
"$user@$host"
done 3< "$CREDS"
SCRIPT
chmod +x setup_keys.sh
./setup_keys.sh
```
## Task 8
During the weekly meeting, the Nautilus DevOps team discussed about the automation and configuration management solutions that they want to implement. While considering several options, the team has decided to go with Ansible for now due to its simple setup and minimal pre-requisites. The team wanted to start testing using Ansible, so they have decided to use jump host as an Ansible controller to test different kind of tasks on rest of the servers.
Install ansible version 4.10.0 on Jump host using pip3 only. Make sure Ansible binary is available globally on this system, i.e all users on this system are able to run Ansible commands.
```bash
sudo pip3 install ansible==4.10.0
# verify
ansible --version
which ansible # want /usr/local/bin/ansible
```
## Task 9
There is a critical issue going on with the Nautilus application in Stratos DC. The production support team identified that the application is unable to connect to the database. After digging into the issue, the team found that mariadb service is down on the database server.
```bash
systemctl enable mariadb
sudo chown mysql:mysql /run/mariadb
sudo chmod 755 /run/mariadb
sudo systemctl start mariadb
sudo systemctl status mariadb --no-pager
systemctl start mariadb
```
## Task 10
The production support team of xFusionCorp Industries is working on developing some bash scripts to automate different day to day tasks. One is to create a bash script for archiving website content files. They have a static website running on App Server 1 in Stratos Datacenter, and they need to create a bash script named beta_archive.sh which should accomplish the following tasks. (Also remember to place the script under the /scripts directory on App Server 1).
a. Create a zip archive named xfusioncorp_beta.zip of /var/www/html/beta directory.
b. Save the archive in the /archives/ directory on the App Server 1. This is a temporary storage, as archives from this location will be cleaned on a weekly basis. Therefore, the archive should also be copied to the Nautilus Storage Server so it can be retrieved later for validation purposes.
c. Copy the created archive to the Nautilus Storage Server server in the /archives/ location.
d. Please make sure script won't ask for password while copying the archive file. Additionally, the respective server user (for example, tony in case of App Server 1) must be able to run it.
e. Do not use sudo inside the script.
Note:
The zip package must be installed on given App Server before executing the script. This package is essential for creating the zip archive of the website files. Install it manually outside the script.
```bash
# install zip (task explicitly says do this manually)
sudo yum install -y zip
# make dirs, owned by tony so script needs no sudo
sudo mkdir -p /scripts /archives
sudo chown tony:tony /scripts /archives
# --- on stapp01, as tony ---
# generate tony's key if he doesn't have one
[ -f ~/.ssh/id_rsa ] || ssh-keygen -t rsa -b 4096 -N '' -f ~/.ssh/id_rsa
# creds for the storage server (the only target for this task)
cat > /tmp/creds.txt <<'EOF'
ststor01 natasha Bl@kW
EOF
chmod 600 /tmp/creds.txt
# distribute tony's pubkey to storage server
cat > /tmp/setup_keys.sh <<'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
CREDS="${1:-/tmp/creds.txt}"
while read -r host user pass <&3; do
[[ -z "$host" || "$host" == \#* ]] && continue
echo "==> Copying tony's key to $host"
SSHPASS="$pass" sshpass -e ssh-copy-id \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
"$user@$host"
done 3< "$CREDS"
SCRIPT
chmod +x /tmp/setup_keys.sh
/tmp/setup_keys.sh
# verify passwordless works
ssh -o BatchMode=yes -o StrictHostKeyChecking=no natasha@ststor01 hostname
# archive copying script
cat > /scripts/beta_archive.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
SRC="/var/www/html/beta"
ARCHIVE_NAME="xfusioncorp_beta.zip"
LOCAL_DIR="/archives"
REMOTE_USER="natasha"
REMOTE_HOST="ststor01"
REMOTE_DIR="/archives"
# a + b: create zip in local /archives
zip -r "${LOCAL_DIR}/${ARCHIVE_NAME}" "$SRC"
# c: copy to storage server (passwordless via key)
scp "${LOCAL_DIR}/${ARCHIVE_NAME}" "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/"
echo "Archive created and copied: ${ARCHIVE_NAME}"
EOF
chmod +x /scripts/beta_archive.sh
```

View File

@@ -0,0 +1,636 @@
## Task 11
The Nautilus application development team recently finished the beta version of one of their Java-based applications, which they are planning to deploy on one of the app servers in Stratos DC. After an internal team meeting, they have decided to use the tomcat application server. Based on the requirements mentioned below complete the task:
a. Install tomcat server on App Server 3.
b. Configure it to run on port 8088.
c. There is a ROOT.war file on Jump host at location /tmp.
```bash
# from jump-host
scp /tmp/ROOT.war steve@stapp02:/tmp/
sudo yum install -y tomcat tomcat-webapps tomcat-admin-webapps
sudo sed -i 's/port="8080"/port="6300"/' /etc/tomcat/server.xml
# verify
sudo grep -n '6300' /etc/tomcat/server.xml
sudo cp /tmp/ROOT.war /var/lib/tomcat/webapps/
sudo chown tomcat:tomcat /var/lib/tomcat/webapps/ROOT.war
sudo systemctl enable --now tomcat
sudo systemctl restart tomcat # ensure it picks up server.xml change if already running
# give it a few seconds to explode the WAR, then:
curl -I http://localhost:8088/
# possible problem
# Stock ROOT collision. If tomcat-webapps installed its own ROOT/ dir, and your ROOT.war sits beside it, tomcat may not redeploy over an # existing exploded dir. Nuke the stock one first: sudo rm -rf /var/lib/tomcat/webapps/ROOT then drop the war and restart.
```
## Task 12
Our monitoring tool has reported an issue in Stratos Datacenter. One of our app servers has an issue, as its Apache service is not reachable on port 8089 (which is the Apache port). The service itself could be down, the firewall could be at fault, or something else could be causing the issue.
Use tools like telnet, netstat, etc. to find and fix the issue. Also make sure Apache is reachable from the jump host without compromising any security settings.
Once fixed, you can test the same using command curl http://stapp02:8089 command from jump host.
```bash
ss -tulnp
# kill semndmail sitting on the port
# start httpd
# remove reject rule from iptables
iptables -D INPUT 5
```
## Task 13
We have one of our websites up and running on our Nautilus infrastructure in Stratos DC. Our security team has raised a concern that right now Apaches port i.e 8084 is open for all since there is no firewall installed on these hosts. So we have decided to add some security layer for these hosts and after discussions and recommendations we have come up with the following requirements:
1. Install iptables and all its dependencies on each app host.
2. Block incoming port 8084 on all apps for everyone except for LBR host.
3. Make sure the rules remain, even after system reboot.
```bash
# --- resolve LBR host IP (script runs from jump-host) ---
LBR_IP=$(getent hosts stlb01 | awk '{print $1}')
echo "LBR IP resolved to: $LBR_IP"
[ -z "$LBR_IP" ] && { echo "ERROR: could not resolve stlb01"; exit 1; }
# --- creds (app servers only — the three targets) ---
cat > creds.txt <<'EOF'
stapp01 tony Ir0nM@n
stapp02 steve Am3ric@
stapp03 banner BigGr33n
EOF
chmod 600 creds.txt
# --- firewall script ---
cat > fw_8084.sh <<SCRIPT
#!/usr/bin/env bash
set -euo pipefail
CREDS="\${1:-creds.txt}"
LBR_IP="10.244.196.7"
REMOTE_CMD="
yum install -y iptables iptables-services
# iptables -F
iptables -A INPUT -p tcp --dport 8084 -s \$LBR_IP -j ACCEPT
iptables -A INPUT -p tcp --dport 8084 -j DROP
systemctl enable --now iptables
iptables-save > /etc/sysconfig/iptables
echo \"--- \\\$(hostname) 8084 rules ---\"
iptables -nvL INPUT | grep 8084
"
while read -r host user pass <&3; do
[[ -z "\$host" || "\$host" == \\#* ]] && continue
echo "==> Firewalling \$host"
SSHPASS="\$pass" sshpass -e ssh -n \\
-o StrictHostKeyChecking=no \\
-o UserKnownHostsFile=/dev/null \\
-o LogLevel=ERROR \\
"\$user@\$host" \\
"echo '\$pass' | sudo -S -p '' bash -c '\$REMOTE_CMD'" \\
2> >(grep -v '^\[sudo\]' >&2)
echo
done 3< "\$CREDS"
SCRIPT
chmod +x fw_8084.sh
# --- run ---
export LBR_IP=10.244.196.7
./fw_8084.sh
```
## Task 14
The production support team of xFusionCorp Industries has deployed some of the latest monitoring tools to keep an eye on every service, application, etc. running on the systems. One of the monitoring systems reported about Apache service unavailability on one of the app servers in Stratos DC.
Identify the faulty app host and fix the issue. Make sure Apache service is up and running on all app hosts. They might not have hosted any code yet on these servers, so you don't need to worry if Apache isn't serving any pages. Just make sure the service is up and running. Also, make sure Apache is running on port 6400 on all app servers.
```bash
# --- creds ---
cat > creds.txt <<'EOF'
stapp01 tony Ir0nM@n
stapp02 steve Am3ric@
stapp03 banner BigGr33n
EOF
chmod 600 creds.txt
# --- RECON: what's the state of httpd + port 6400 on each box ---
cat > apache_recon.sh <<'SCRIPT'
#!/usr/bin/env bash
set -euo pipefail
CREDS="${1:-creds.txt}"
REMOTE_CMD='
echo "active: $(systemctl is-active httpd 2>/dev/null)"
echo "enabled: $(systemctl is-enabled httpd 2>/dev/null)"
echo "Listen directive: $(grep -iE "^Listen" /etc/httpd/conf/httpd.conf 2>/dev/null)"
echo "bound on 6400: $(ss -tlnp | grep :6400 || echo NO)"
echo "last httpd error:"
journalctl -u httpd --no-pager 2>/dev/null | tail -5
'
while read -r host user pass <&3; do
[[ -z "$host" || "$host" == \#* ]] && continue
echo "===================== $host ====================="
SSHPASS="$pass" sshpass -e ssh -n \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o LogLevel=ERROR \
"$user@$host" \
"echo '$pass' | sudo -S -p '' bash -c '$REMOTE_CMD'" \
2> >(grep -v '^\[sudo\]' >&2)
echo
done 3< "$CREDS"
SCRIPT
chmod +x apache_recon.sh
./apache_recon.sh
# Verify
cat > apache_verify.sh <<'SCRIPT'
#!/usr/bin/env bash
set -uo pipefail # note: no -e, we want to check ALL hosts even if one fails
CREDS="${1:-creds.txt}"
REMOTE_CMD='
fail=0
# 1. service running?
if [ "$(systemctl is-active httpd 2>/dev/null)" = "active" ]; then
echo " [PASS] httpd is active"
else
echo " [FAIL] httpd NOT active (state: $(systemctl is-active httpd 2>/dev/null))"
fail=1
fi
# 2. enabled for boot?
if [ "$(systemctl is-enabled httpd 2>/dev/null)" = "enabled" ]; then
echo " [PASS] httpd enabled at boot"
else
echo " [WARN] httpd NOT enabled at boot (state: $(systemctl is-enabled httpd 2>/dev/null))"
fi
# 3. bound on 6400?
if ss -tlnp 2>/dev/null | grep -q :6400; then
echo " [PASS] listening on port 6400"
else
echo " [FAIL] NOT listening on 6400"
fail=1
fi
# 4. actually responds? (any HTTP code = apache answering; 403/404 fine, task says no content needed)
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 http://localhost:6400/ 2>/dev/null)
if [ -n "$code" ] && [ "$code" != "000" ]; then
echo " [PASS] HTTP response on 6400 (code: $code)"
else
echo " [FAIL] no HTTP response on 6400"
fail=1
fi
echo " RESULT: $([ $fail -eq 0 ] && echo ALL-GOOD || echo NEEDS-ATTENTION)"
'
overall=0
while read -r host user pass <&3; do
[[ -z "$host" || "$host" == \#* ]] && continue
echo "===================== $host ====================="
out=$(SSHPASS="$pass" sshpass -e ssh -n \
-o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o LogLevel=ERROR \
"$user@$host" \
"echo '$pass' | sudo -S -p '' bash -c '$REMOTE_CMD'" \
2> >(grep -v '^\[sudo\]' >&2))
echo "$out"
echo "$out" | grep -q NEEDS-ATTENTION && overall=1
echo
done 3< "$CREDS"
echo "=================================================="
[ $overall -eq 0 ] && echo "✅ ALL HOSTS PASS" || echo "❌ ONE OR MORE HOSTS NEED ATTENTION"
SCRIPT
chmod +x apache_verify.sh
./apache_verify.sh
```
## Task 15
The system admins team of xFusionCorp Industries needs to deploy a new application on App Server 2 in Stratos Datacenter. They have some pre-requites to get ready that server for application deployment. Prepare the server as per requirements shared below:
1. Install and configure nginx on App Server 2.
2. On App Server 2 there is a self signed SSL certificate and key present at location /tmp/nautilus.crt and /tmp/nautilus.key. Move them to some appropriate location and deploy the same in Nginx.
3. Create an index.html file with content Welcome! under Nginx document root.
4. For final testing try to access the App Server 2 link (via hostname) from jump host using curl command. For example: curl -Ik https://<app-server-name>/.
```bash
# ===== nginx + SSL deploy on stapp02 (run as steve) =====
# 1. install nginx
sudo yum install -y nginx
# 2. relocate certs out of /tmp to a proper location + lock down perms
sudo mkdir -p /etc/nginx/ssl
sudo mv /tmp/nautilus.crt /etc/nginx/ssl/nautilus.crt
sudo mv /tmp/nautilus.key /etc/nginx/ssl/nautilus.key
sudo chmod 600 /etc/nginx/ssl/nautilus.key
sudo chmod 644 /etc/nginx/ssl/nautilus.crt
# 3. SSL server block — server_name pulled from the box's own FQDN
FQDN=$(hostname -f)
echo "Configuring nginx for: $FQDN"
sudo tee /etc/nginx/conf.d/nautilus.conf > /dev/null <<EOF
server {
listen 443 ssl;
server_name ${FQDN};
ssl_certificate /etc/nginx/ssl/nautilus.crt;
ssl_certificate_key /etc/nginx/ssl/nautilus.key;
root /usr/share/nginx/html;
index index.html;
location / {
try_files \$uri \$uri/ =404;
}
}
EOF
# 4. index.html with Welcome! under docroot
echo 'Welcome!' | sudo tee /usr/share/nginx/html/index.html > /dev/null
# 5. validate + start + enable
sudo nginx -t
sudo systemctl enable --now nginx
sudo systemctl restart nginx
# 6. local sanity check (before testing from jump-host)
echo "--- local curl test ---"
curl -Ik https://localhost/
echo "--- service state ---"
sudo systemctl is-active nginx
```
## Task 16
Day by day traffic is increasing on one of the websites managed by the Nautilus production support team. Therefore, the team has observed a degradation in website performance. Following discussions about this issue, the team has decided to deploy this application on a high availability stack i.e on Nautilus infra in Stratos DC. They started the migration last month and it is almost done, as only the LBR server configuration is pending. Configure LBR server as per the information given below:
a. Install nginx on the LBR (load balancer) server if it is not already installed.
b. Configure load-balancing with the http context making use of all App Servers. Ensure that you update only the main Nginx configuration file located at /etc/nginx/nginx.conf.
c. Make sure you do not update the apache port that is already defined in the apache configuration on all app servers, also make sure apache service is up and running on all the app servers.
d. Once done, you can access the website by running curl http://stlb01:80 in the terminal.
```bash
# ===== LBR config — run from jump-host =====
# --- creds (3 app servers for recon + LBR for deploy) ---
# ===== LBR config — run from jump-host =====
# --- creds (3 app servers for recon + LBR for deploy) ---
cat > creds.txt <<'EOF'
stapp01 tony Ir0nM@n
stapp02 steve Am3ric@
stapp03 banner BigGr33n
stlb01 loki Mischi3f
EOF
chmod 600 creds.txt
# helper: pull pass for a given host from creds
getpass() { awk -v h="$1" '$1==h{print $3}' creds.txt; }
getuser() { awk -v h="$1" '$1==h{print $2}' creds.txt; }
# --- STEP 1: recon — confirm apache up on all app servers + grab the port ---
echo "===== Apache recon on app servers ====="
for host in stapp01 stapp02 stapp03; do
u=$(getuser "$host"); p=$(getpass "$host")
echo "=== $host ==="
SSHPASS="$p" sshpass -e ssh -n \
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR \
"$u@$host" \
"echo '$p' | sudo -S -p '' bash -c 'systemctl is-active httpd; grep -i \"^Listen\" /etc/httpd/conf/httpd.conf'" \
2> >(grep -v '^\[sudo\]' >&2)
done
# --- STEP 2: auto-detect the apache port from stapp01 ---
u=$(getuser stapp01); p=$(getpass stapp01)
APACHE_PORT=$(SSHPASS="$p" sshpass -e ssh -n \
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR \
"$u@stapp01" \
"grep -iE '^Listen' /etc/httpd/conf/httpd.conf | awk '{print \$2}' | tr -d '\r'" \
2>/dev/null)
echo ">>> Detected Apache port: $APACHE_PORT"
[ -z "$APACHE_PORT" ] && { echo "ERROR: could not detect apache port"; exit 1; }
# --- STEP 3: build nginx.conf LOCALLY (quoted heredoc keeps nginx $vars literal) ---
cat > /tmp/lbr_nginx.conf <<'EOF'
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 1024;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 4096;
include /etc/nginx/mime.types;
default_type application/octet-stream;
upstream app_servers {
server stapp01:__PORT__;
server stapp02:__PORT__;
server stapp03:__PORT__;
}
server {
listen 80;
server_name stlb01;
location / {
proxy_pass http://app_servers;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
EOF
# substitute the real apache port
sed -i "s/__PORT__/${APACHE_PORT}/g" /tmp/lbr_nginx.conf
echo ">>> nginx.conf built with upstream port ${APACHE_PORT}:"
grep -A4 'upstream app_servers' /tmp/lbr_nginx.conf
# --- STEP 4: ship config to stlb01 + deploy ---
u=$(getuser stlb01); p=$(getpass stlb01)
# scp the config to a temp spot on stlb01 (passwordless via sshpass)
SSHPASS="$p" sshpass -e scp \
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR \
/tmp/lbr_nginx.conf "$u@stlb01:/tmp/lbr_nginx.conf"
# install nginx, back up original, move new config in, validate, start
SSHPASS="$p" sshpass -e ssh -n \
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR \
"$u@stlb01" \
"echo '$p' | sudo -S -p '' bash -c '
yum install -y nginx
cp -n /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak 2>/dev/null || true
cp /tmp/lbr_nginx.conf /etc/nginx/nginx.conf
nginx -t
systemctl enable --now nginx
systemctl restart nginx
echo \"--- nginx state ---\"
systemctl is-active nginx
'" \
2> >(grep -v '^\[sudo\]' >&2)
# --- STEP 5: final test ---
echo "===== TEST: curl http://stlb01:80 ====="
u=$(getuser stlb01); p=$(getpass stlb01)
SSHPASS="$p" sshpass -e ssh -n \
-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR \
"$u@stlb01" "curl -s http://stlb01:80 | head -20" \
2>/dev/null
echo
echo "You can also test directly from jump-host:"
echo " curl http://stlb01:80"
```
## Task 17
The Nautilus application development team has shared that they are planning to deploy one newly developed application on Nautilus infra in Stratos DC. The application uses PostgreSQL database, so as a pre-requisite we need to set up PostgreSQL database server as per requirements shared below:
PostgreSQL database server is already installed on the Nautilus database server.
a. Create a database user kodekloud_aim and set its password to 8FmzjvFU6S.
b. Create a database kodekloud_db8 and grant full permissions to user kodekloud_aim on this database.
```bash
# ===== PostgreSQL setup on stdb01 (run as peter) =====
# sudo into postgres user first
sudo -i -u postgres
psql -c "SELECT version();"
psql -c "CREATE USER kodekloud_aim WITH PASSWORD '8FmzjvFU6S';"
psql -c "CREATE DATABASE kodekloud_db8;"
psql -c "GRANT ALL PRIVILEGES ON DATABASE kodekloud_db8 TO kodekloud_aim;"
psql -c "\l" | grep kodekloud_db8
psql -c "\du" | grep kodekloud_aim
psql -c "\l kodekloud_db8"
```
## Task 18
We need to setup a database server on Nautilus DB Server in Stratos Datacenter. Please perform the below given steps on DB Server:
a. Install/Configure MariaDB server.
b. Create a database named kodekloud_db3.
c. Create a user called kodekloud_gem and set its password to YchZHRcLkL.
d. Grant full permissions to user kodekloud_gem on database kodekloud_db3.
```bash
# ===== MariaDB setup on stdb01 (run as peter) =====
# a. install + start MariaDB
sudo yum install -y mariadb-server
sudo systemctl enable --now mariadb
# b/c/d. create db, user, grant privileges (root via unix_socket auth)
sudo mysql -e "CREATE DATABASE kodekloud_db3;"
sudo mysql -e "CREATE USER 'kodekloud_gem'@'localhost' IDENTIFIED BY 'YchZHRcLkL';"
sudo mysql -e "GRANT ALL PRIVILEGES ON kodekloud_db3.* TO 'kodekloud_gem'@'localhost';"
sudo mysql -e "FLUSH PRIVILEGES;"
# --- verify ---
echo "--- database exists? ---"
sudo mysql -e "SHOW DATABASES;" | grep kodekloud_db3
echo "--- user exists? ---"
sudo mysql -e "SELECT User,Host FROM mysql.user WHERE User='kodekloud_gem';"
echo "--- grants ---"
sudo mysql -e "SHOW GRANTS FOR 'kodekloud_gem'@'localhost';"
```
## Task 18
xFusionCorp Industries is planning to host two static websites on their infra in Stratos Datacenter. The development of these websites is still in-progress, but we want to get the servers ready. Please perform the following steps to accomplish the task:
a. Install httpd package and dependencies on app server 2.
b. Apache should serve on port 5000.
c. There are two website's backups /home/thor/media and /home/thor/demo on jump_host. Set them up on Apache in a way that media should work on the link http://localhost:5000/media/ and demo should work on link http://localhost:5000/demo/ on the mentioned app server.
d. Once configured you should be able to access the website using curl command on the respective app server, i.e curl http://localhost:5000/media/ and curl http://localhost:5000/demo/
```bash
# ===== Apache two-site setup: jump-host → stapp02 (via sshpass) =====
# --- creds ---
cat > creds.txt <<'EOF'
stapp02 steve Am3ric@
EOF
chmod 600 creds.txt
# pull steve's creds
USER=$(awk '$1=="stapp02"{print $2}' creds.txt)
PASS=$(awk '$1=="stapp02"{print $3}' creds.txt)
SSHOPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR"
# --- Step 1: ship both backups from jump-host to stapp02 ---
echo "==> Transferring media + demo to stapp02"
SSHPASS="$PASS" sshpass -e scp $SSHOPTS -r /home/thor/media "$USER@stapp02:/tmp/"
SSHPASS="$PASS" sshpass -e scp $SSHOPTS -r /home/thor/demo "$USER@stapp02:/tmp/"
# --- Step 2: install + configure on stapp02 (single sudo shell) ---
echo "==> Configuring httpd on stapp02"
REMOTE_CMD='
yum install -y httpd
sed -i "s/^Listen 80$/Listen 5000/" /etc/httpd/conf/httpd.conf
mkdir -p /var/www/html/media /var/www/html/demo
cp -r /tmp/media/* /var/www/html/media/
cp -r /tmp/demo/* /var/www/html/demo/
apachectl configtest
systemctl enable --now httpd
systemctl restart httpd
echo "--- Listen directive ---"
grep -i "^Listen" /etc/httpd/conf/httpd.conf
echo "--- httpd state ---"
systemctl is-active httpd
'
SSHPASS="$PASS" sshpass -e ssh -n $SSHOPTS "$USER@stapp02" \
"echo '$PASS' | sudo -S -p '' bash -c '$REMOTE_CMD'" \
2> >(grep -v '^\[sudo\]' >&2)
# --- Step 3: test on stapp02 (curl runs locally on the app server) ---
echo "===== TEST: /media/ ====="
SSHPASS="$PASS" sshpass -e ssh -n $SSHOPTS "$USER@stapp02" \
'curl -s http://localhost:5000/media/ | head'
```
## Task 20
The Nautilus application development team is planning to launch a new PHP-based application, which they want to deploy on Nautilus infra in Stratos DC. The development team had a meeting with the production support team and they have shared some requirements regarding the infrastructure. Below are the requirements they shared:
a. Install nginx on app server 2 , configure it to use port 8098 and its document root should be /var/www/html.
b. Install php-fpm version 8.1 on app server 2, it must use the unix socket /var/run/php-fpm/default.sock (create the parent directories if don't exist).
c. Configure php-fpm and nginx to work together.
d. Once configured correctly, you can test the website using curl http://stapp02:8098/index.php command from jump host.
NOTE: We have copied two files, index.php and info.php, under /var/www/html as part of the PHP-based application setup. Please do not modify these files.
```bash
# ===== nginx + php-fpm 8.2 setup: jump-host → stapp02 (via sshpass) =====
# --- creds ---
cat > creds.txt <<'EOF'
stapp02 steve Am3ric@
EOF
chmod 600 creds.txt
USER=$(awk '$1=="stapp02"{print $2}' creds.txt)
PASS=$(awk '$1=="stapp02"{print $3}' creds.txt)
SSHOPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR"
# --- build nginx server block LOCALLY (quoted heredoc = nginx $vars stay literal) ---
cat > /tmp/php_site.conf <<'EOF'
server {
listen 8092;
server_name stapp02.stratos.xfusioncorp.com;
root /var/www/html;
index index.php index.html;
location / {
try_files $uri $uri/ =404;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php-fpm/default.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
EOF
# --- ship the nginx config to stapp02 ---
echo "==> Shipping nginx config"
SSHPASS="$PASS" sshpass -e scp $SSHOPTS /tmp/php_site.conf "$USER@stapp02:/tmp/php_site.conf"
# --- remote: install + configure everything in one sudo shell ---
echo "==> Configuring stapp02"
REMOTE_CMD='
set -e
# a. nginx
yum install -y nginx
# b. php-fpm 8.2 — enable the module stream then install
yum module reset php -y 2>/dev/null || true
yum module enable php:8.2 -y 2>/dev/null || true
yum install -y php-fpm
echo "--- php-fpm version ---"
php-fpm --version | head -1
# install nginx server block (replace stock default.conf)
cp /tmp/php_site.conf /etc/nginx/conf.d/default.conf
# php-fpm pool -> unix socket + nginx ownership
sed -i "s#^listen = .*#listen = /var/run/php-fpm/default.sock#" /etc/php-fpm.d/www.conf
sed -i "s/^;*listen.owner = .*/listen.owner = nginx/" /etc/php-fpm.d/www.conf
sed -i "s/^;*listen.group = .*/listen.group = nginx/" /etc/php-fpm.d/www.conf
sed -i "s/^;*listen.mode = .*/listen.mode = 0660/" /etc/php-fpm.d/www.conf
sed -i "s/^user = .*/user = nginx/" /etc/php-fpm.d/www.conf
sed -i "s/^group = .*/group = nginx/" /etc/php-fpm.d/www.conf
# create socket parent dir (task requirement)
mkdir -p /var/run/php-fpm
chown nginx:nginx /var/run/php-fpm
# validate nginx config
nginx -t
# start + enable both
systemctl enable --now php-fpm nginx
systemctl restart php-fpm nginx
echo "--- states ---"
systemctl is-active php-fpm
systemctl is-active nginx
echo "--- socket ---"
ls -l /var/run/php-fpm/default.sock
'
SSHPASS="$PASS" sshpass -e ssh -n $SSHOPTS "$USER@stapp02" \
"echo '$PASS' | sudo -S -p '' bash -c '$REMOTE_CMD'" \
2> >(grep -v '^\[sudo\]' >&2)
# --- test from jump-host ---
echo "===== TEST: curl http://stapp02:8092/index.php ====="
SSHPASS="$PASS" sshpass -e ssh -n $SSHOPTS "$USER@stapp02" \
'curl -s http://localhost:8092/index.php | head -20' 2>/dev/null
echo
echo "Direct from jump-host: curl http://stapp02:8092/index.php"
```

View File

@@ -0,0 +1,165 @@
## Task 21
The Nautilus development team has provided requirements to the DevOps team for a new application development project, specifically requesting the establishment of a Git repository. Follow the instructions below to create the Git repository on the Storage server in the Stratos DC:
Utilize yum to install the git package on the Storage Server.
Create a bare repository named /opt/beta.git (ensure exact name usage).
```bash
ssh natasha@ststor01
# install git
sudo yum install -y git
# create the bare repo (exact path/name)
sudo git init --bare /opt/beta.git
# verify
ls -la /opt/beta.git
```
## Task 22
The DevOps team established a new Git repository last week, which remains unused at present. However, the Nautilus application development team now requires a copy of this repository on the Storage Server in the Stratos DC. Follow the provided details to clone the repository:
The repository to be cloned is located at /opt/cluster.git
Clone this Git repository to the /usr/src/kodekloudrepos directory. Perform this task using the natasha user, and ensure that no modifications are made to the repository or existing directories, such as changing permissions or making unauthorized alterations.
```bash
ssh natasha@ststor01
git clone /opt/news.git /usr/src/kodekloudrepos
ls -la /usr/src/kodekloudrepos
```
## Task 23
There is a Git server utilized by the Nautilus project teams. Recently, a new developer named Jon joined the team and needs to begin working on a project. To begin, he must fork an existing Git repository. Follow the steps below:
Click on the Gitea UI button located on the top bar to access the Gitea page.
Login to Gitea server using username jon and password Jon_pass123.
Once logged in, locate the Git repository named sarah/story-blog and fork it under the jon user.
Note: For tasks requiring web UI changes, screenshots are necessary for review purposes. Additionally, consider utilizing screen recording software such as loom.com to record and share your task completion process.
Solution:
Clicky-clack in gitea UI
## Task 24
Nautilus developers are actively working on one of the project repositories, /usr/src/kodekloudrepos/media. Recently, they decided to implement some new features in the application, and they want to maintain those new changes in a separate branch. Below are the requirements that have been shared with the DevOps team:
On Storage server in Stratos DC create a new branch xfusioncorp_media from master branch in /usr/src/kodekloudrepos/media git repo.
Please do not try to make any changes in the code.
```bash
ssh natasha@ststor01
cd /usr/src/kodekloudrepos/media
git checkout master
git checkout -b xfusioncorp_media
```
## Task 25
The Nautilus application development team has been working on a project repository /opt/media.git. This repo is cloned at /usr/src/kodekloudrepos on storage server in Stratos DC. They recently shared the following requirements with DevOps team:
Create a new branch datacenter in /usr/src/kodekloudrepos/media repo from master and copy the /tmp/index.html file (present on storage server itself) into the repo. Further, add/commit this file in the new branch and merge back that branch into master branch. Finally, push the changes to the origin for both of the branches.
```bash
ssh natasha@ststor01
# do some pretty uninteresant git operations and file copies
```
## Task 26
The xFusionCorp development team added updates to the project that is maintained under /opt/demo.git repo and cloned under /usr/src/kodekloudrepos/demo. Recently some changes were made on Git server that is hosted on Storage server in Stratos DC. The DevOps team added some new Git remotes, so we need to update remote on /usr/src/kodekloudrepos/demo repository as per details mentioned below:
a. In /usr/src/kodekloudrepos/demo repo add a new remote dev_demo and point it to /opt/xfusioncorp_demo.git repository.
b. There is a file /tmp/index.html on same server; copy this file to the repo and add/commit to master branch.
c. Finally push master branch to this new remote origin.
```bash
ssh natasha@ststor01
sudo su -
cd /usr/src/kodekloudrepos/demo
git remote add dev_demo /opt/xfusioncorp_demo.git
# b. copy file in, stage, commit on master
git checkout master # make sure we're on master
cp /tmp/index.html .
git add index.html
git commit -m "Add index.html"
# c. push master to the NEW remote (dev_demo), not origin
git push dev_demo master
```
## Task 27
The Nautilus application development team was working on a git repository /usr/src/kodekloudrepos/games present on Storage server in Stratos DC. However, they reported an issue with the recent commits being pushed to this repo. They have asked the DevOps team to revert repo HEAD to last commit. Below are more details about the task:
In /usr/src/kodekloudrepos/games git repository, revert the latest commit ( HEAD ) to the previous commit (JFYI the previous commit hash should be with initial commit message ).
Use revert games message (please use all small letters for commit message) for the new revert commit.
```bash
ssh natasha@ststor01
sudo su -
cd /usr/src/kodekloudrepos/games
sudo git log --oneline
git revert HEAD --no-commit
git commit -m "revert games"
```
## Task 28
The Nautilus application development team has been working on a project repository /opt/news.git. This repo is cloned at /usr/src/kodekloudrepos on storage server in Stratos DC. They recently shared the following requirements with the DevOps team:
There are two branches in this repository, master and feature. One of the developers is working on the feature branch and their work is still in progress, however they want to merge one of the commits from the feature branch to the master branch, the message for the commit that needs to be merged into master is Update info.txt. Accomplish this task for them, also remember to push your changes eventually.
```bash
ssh natasha@ststor01
sudo su -
cd /usr/src/kodekloudrepos/news
git log feature --oneline
git checkout master
git cherry-pick 9472ff2
git push origin master
```
## Task 29
Clicky clack in gitea UI
## Task 30
The Nautilus application development team was working on a git repository /usr/src/kodekloudrepos/beta present on Storage server in Stratos DC. This was just a test repository and one of the developers just pushed a couple of changes for testing, but now they want to clean this repository along with the commit history/work tree, so they want to point back the HEAD and the branch itself to a commit with message add data.txt file. Find below more details:
In /usr/src/kodekloudrepos/beta git repository, reset the git commit history so that there are only two commits in the commit history i.e initial commit and add data.txt file.
Also make sure to push your changes.
```bash
ssh natasha@ststor01
sudo su -
cd /usr/src/kodekloudrepos/beta
git log --oneline
git reset --hard e23457b
git push -f
```

View File

@@ -0,0 +1,221 @@
## Task 31
The Nautilus application development team was working on a git repository /usr/src/kodekloudrepos/media present on Storage server in Stratos DC. One of the developers stashed some in-progress changes in this repository, but now they want to restore some of the stashed changes. Find below more details to accomplish this task:
Look for the stashed changes under /usr/src/kodekloudrepos/media git repository, and restore the stash with stash@{1} identifier. Further, commit and push your changes to the origin.
```bash
ssh natasha@ststor01
sudo su -
cd /usr/src/kodekloudrepos/media
git stash list
# git stash apply stash@{1}
git stash pop stash@{1}
sudo git add -A
sudo git commit -m "Restore stashed changes from stash@{1}"
sudo git push origin master
```
## Task 32
The Nautilus application development team has been working on a project repository /opt/demo.git. This repo is cloned at /usr/src/kodekloudrepos on storage server in Stratos DC. They recently shared the following requirements with DevOps team:
One of the developers is working on feature branch and their work is still in progress, however there are some changes which have been pushed into the master branch, the developer now wants to rebase the feature branch with the master branch without loosing any data from the feature branch, also they don't want to add any merge commit by simply merging the master branch into the feature branch. Accomplish this task as per requirements mentioned.
Also remember to push your changes once done.
```bash
ssh natasha@ststor01
sudo su -
cd /usr/src/kodekloudrepos/demo/
git checkout feature
git rebase master # replay feature on top of master, linear
git push origin feature --force # force-push rewritten feature
git log --oneline --graph --all
```
## Task 33
Sarah and Max were working on writting some stories which they have pushed to the repository. Max has recently added some new changes and is trying to push them to the repository but he is facing some issues. Below you can find more details:
SSH into storage server using user max and password Max_pass123. Under /home/max you will find the story-blog repository. Try to push the changes to the origin repo and fix the issues. The story-index.txt must have titles for all 4 stories. Additionally, there is a typo in The Lion and the Mooose line where Mooose should be Mouse.
Click on the Gitea UI button on the top bar. You should be able to access the Gitea page. You can login to Gitea server from UI using username sarah and password Sarah_pass123 or username max and password Max_pass123.
Note: For these kind of scenarios requiring changes to be done in a web UI, please take screenshots so that you can share it with us for review in case your task is marked incomplete. You may also consider using a screen recording software such as loom.com to record and share your work.
```bash
ssh max@ststor01
# password: Max_pass123
cd /home/max/story-blog
git status
git log --oneline -5
git push origin master
```
## Task 34
The Nautilus application development team was working on a git repository /opt/cluster.git which is cloned under /usr/src/kodekloudrepos directory present on Storage server in Stratos DC. The team want to setup a hook on this repository, please find below more details:
Merge the feature branch into the master branch, but before pushing your changes complete below point.
Create a post-update hook in this git repository so that whenever any changes are pushed to the master branch, it creates a release tag with name release-2023-06-15, where 2023-06-15 is supposed to be the current date. For example if today is 20th June, 2023 then the release tag must be release-2023-06-20. Make sure you test the hook at least once and create a release tag for today's release.
Finally remember to push your changes.
Note: Perform this task using the natasha user, and ensure the repository or existing directory permissions are not altered.
```bash
ssh natasha@ststor01
sudo su -
cd /usr/src/kodekloudrepos/cluster
git checkout master
git merge feature
sudo tee /opt/cluster.git/hooks/post-update > /dev/null <<'EOF'
#!/bin/bash
# post-update hook: tag a dated release when master is pushed
release_date=$(date +%F) # YYYY-MM-DD
tag="release-${release_date}"
git tag -f "$tag" master
EOF
sudo chmod +x /opt/cluster.git/hooks/post-update
# now post-update hook should be alive
git push origin master
# Verify
# tag should now exist in the bare repo
sudo git --git-dir=/opt/cluster.git tag -l
# or from the working clone after fetching
sudo git fetch origin --tags
sudo git tag -l
```
## Task 35
The Nautilus DevOps team aims to containerize various applications following a recent meeting with the application development team. They intend to conduct testing with the following steps:
Install docker-ce and docker compose packages on App Server 3.
Initiate the docker service.
```bash
ssh banner@stapp03
# add prerequisites + Docker's official repo
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
# install docker-ce, CLI, containerd, and compose
sudo yum install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# start + enable the service
sudo systemctl enable --now docker
```
## Task 36
The Nautilus DevOps team is conducting application deployment tests on selected application servers. They require a nginx container deployment on Application Server 1. Complete the task with the following instructions:
On Application Server 1 create a container named nginx_1 using the nginx image with the alpine tag. Ensure container is in a running state.
```bash
ssh tony@stapp01
sudo docker run -d --name nginx_1 nginx:alpine
# verify
sudo docker ps
## Task 37
The Nautilus DevOps team possesses confidential data on App Server 1 in the Stratos Datacenter. A container named ubuntu_latest is running on the same server.
Copy an encrypted file /tmp/nautilus.txt.gpg from the docker host to the ubuntu_latest container located at /opt/. Ensure the file is not modified during this operation.
```bash
ssh tony@stapp01
docker cp /tmp/nautilus.txt.gpg ubuntu_latest:/opt/
# confirm it's in the container
sudo docker exec ubuntu_latest ls -l /opt/nautilus.txt.gpg
# integrity check — compare checksums host vs container
md5sum /tmp/nautilus.txt.gpg
sudo docker exec ubuntu_latest md5sum /opt/nautilus.txt.gpg
```
## Task 38
Nautilus project developers are planning to start testing on a new project. As per their meeting with the DevOps team, they want to test containerized environment application features. As per details shared with DevOps team, we need to accomplish the following task:
a. Pull busybox:musl image on App Server 1 in Stratos DC and re-tag (create new tag) this image as busybox:blog.
```bash
ssh tony@stapp01
# a. pull the source image
docker pull busybox:musl
# re-tag it as busybox:blog
docker tag busybox:musl busybox:blog
```
## Task 39
One of the Nautilus developer was working to test new changes on a container. He wants to keep a backup of his changes to the container. A new request has been raised for the DevOps team to create a new image from this container. Below are more details about it:
a. Create an image news:xfusion on Application Server 1 from a container ubuntu_latest that is running on same server.
```bash
ssh tony@stapp01
sudo docker commit ubuntu_latest news:xfusion
sudo docker images news # news:xfusion present
```
## Task 40
One of the Nautilus DevOps team members was working to configure services on a kkloud container that is running on App Server 3 in Stratos Datacenter. Due to some personal work he is on PTO for the rest of the week, but we need to finish his pending work ASAP. Please complete the remaining work as per details given below:
a. Install apache2 in kkloud container using apt that is running on App Server 3 in Stratos Datacenter.
b. Configure Apache to listen on port 6300 instead of default http port. Do not bind it to listen on specific IP or hostname only, i.e it should listen on localhost, 127.0.0.1, container ip, etc.
c. Make sure Apache service is up and running inside the container. Keep the container in running state at the end.
```bash
ssh banner@stapp03
# BigGr33n
docker exec -it kkloud bash
# in running container
apt update
apt install -y apache2
sed -i 's/^Listen 80$/Listen 6300/' /etc/apache2/ports.conf
sed -i 's/<VirtualHost \*:80>/<VirtualHost *:6300>/' /etc/apache2/sites-enabled/000-default.conf
service apache2 start
# verify inside container
# is it listening on 6300 on all interfaces?
apt install -y net-tools 2>/dev/null
netstat -tlnp | grep 6300
# or
ss -tlnp | grep 6300
# functional check
curl http://localhost:6300

View File

@@ -0,0 +1,367 @@
## Task 41
As per recent requirements shared by the Nautilus application development team, they need custom images created for one of their projects. Several of the initial testing requirements are already been shared with DevOps team. Therefore, create a docker file /opt/docker/Dockerfile (please keep D capital of Dockerfile) on App server 1 in Stratos DC and configure to build an image with the following requirements:
a. Use ubuntu:24.04 as the base image.
b. Install apache2 and configure it to work on 3000 port. (do not update any other Apache configuration settings like document root etc).
```bash
ssh tony@stapp01
sudo mkdir -p /opt/docker
sudo tee /opt/docker/Dockerfile > /dev/null <<'EOF'
FROM ubuntu:24.04
# install apache2 non-interactively
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y apache2 && \
apt-get clean
# configure apache to listen on port 3000
RUN sed -i 's/^Listen 80$/Listen 3000/' /etc/apache2/ports.conf && \
sed -i 's/<VirtualHost \*:80>/<VirtualHost *:3000>/' /etc/apache2/sites-enabled/000-default.conf
EXPOSE 3000
CMD ["apache2ctl", "-D", "FOREGROUND"]
EOF
```
## Task 42
The Nautilus DevOps team needs to set up several docker environments for different applications. One of the team members has been assigned a ticket where he has been asked to create some docker networks to be used later. Complete the task based on the following ticket description:
a. Create a docker network named as news on App Server 3 in Stratos DC.
b. Configure it to use macvlan drivers.
c. Set it to use subnet 192.168.30.0/24 and iprange 192.168.30.0/24.
```
```bash
ssh banner@stapp03
# BigGr33n
docker network create -d macvlan \
--subnet 192.168.30.0/24 \
--ip-range 192.168.30.0/24 \
news
docker network inspect news
```
## Task 43
The Nautilus DevOps team is planning to host an application on a nginx-based container. There are number of tickets already been created for similar tasks. One of the tickets has been assigned to set up a nginx container on Application Server 3 in Stratos Datacenter. Please perform the task as per details mentioned below:
a. Pull nginx:alpine docker image on Application Server 3.
b. Create a container named demo using the image you pulled.
c. Map host port 3000 to container port 80. Please keep the container in running state.
```bash
ssh banner@stapp03
# BigGr33n
# a. pull the image
docker pull nginx:alpine
# b + c. run named container, map host 3000 → container 80, detached
docker run -d --name demo -p 3000:80 nginx:alpine
```
## Task 44
The Nautilus application development team shared static website content that needs to be hosted on the httpd web server using a containerised platform. The team has shared details with the DevOps team, and we need to set up an environment according to those guidelines. Below are the details:
a. On App Server 3 in Stratos DC create a container named httpd using a docker compose file /opt/docker/docker-compose.yml (please use the exact name for file).
b. Use httpd (preferably latest tag) image for container and make sure container is named as httpd; you can use any name for service.
c. Map 80 number port of container with port 3003 of docker host.
d. Map container's /usr/local/apache2/htdocs volume with /opt/itadmin volume of docker host which is already there. (please do not modify any data within these locations).
```bash
ssh banner@stapp03
# BigGr33n
sudo mkdir -p /opt/docker
sudo tee /opt/docker/docker-compose.yml > /dev/null <<'EOF'
version: "3"
services:
webserver:
image: httpd:latest
container_name: httpd
ports:
- "3003:80"
volumes:
- /opt/itadmin:/usr/local/apache2/htdocs
EOF
cd /opt/docker
sudo docker compose up -d
```
## Task 45
The Nautilus DevOps team is working to create new images per requirements shared by the development team. One of the team members is working to create a Dockerfile on App Server 2 in Stratos DC. While working on it she ran into issues in which the docker build is failing and displaying errors. Look into the issue and fix it to build an image as per details mentioned below:
a. The Dockerfile is placed on App Server 2 under /opt/docker directory.
b. Fix the issues with this file and make sure it is able to build the image.
c. Do not change base image, any other valid configuration within Dockerfile, or any of the data been used — for example, index.html.
Note: Please note that once you click on FINISH button all the existing containers will be destroyed and new image will be built from your Dockerfile.
```bash
ssh steve@stapp02
# Am3ric@
sudo tee /opt/docker/Dockerfile > /dev/null <<'EOF'
FROM httpd:2.4.43
WORKDIR /usr/local/apache2
RUN sed -i "s/Listen 80/Listen 8080/g" /usr/local/apache2/conf/httpd.conf
RUN sed -i '/LoadModule\ ssl_module modules\/mod_ssl.so/s/^#//g' conf/httpd.conf
RUN sed -i '/LoadModule\ socache_shmcb_module modules\/mod_socache_shmcb.so/s/^#//g' conf/httpd.conf
RUN sed -i '/Include\ conf\/extra\/httpd-ssl.conf/s/^#//g' conf/httpd.conf
COPY certs/server.crt /usr/local/apache2/conf/server.crt
COPY certs/server.key /usr/local/apache2/conf/server.key
COPY html/index.html /usr/local/apache2/htdocs/
EOF
```
## Task 46
The Nautilus Application development team recently finished development of one of the apps that they want to deploy on a containerized platform. The Nautilus Application development and DevOps teams met to discuss some of the basic pre-requisites and requirements to complete the deployment. The team wants to test the deployment on one of the app servers before going live and set up a complete containerized stack using a docker compose fie. Below are the details of the task:
On App Server 1 in Stratos Datacenter create a docker compose file /opt/finance/docker-compose.yml (should be named exactly).
The compose should deploy two services (web and DB), and each service should deploy a container as per details below:
For web service:
a. Container name must be php_blog.
b. Use image php with any apache tag. Check here for more details.
c. Map php_blog container's port 80 with host port 8089
d. Map php_blog container's /var/www/html volume with host volume /var/www/html.
For DB service:
a. Container name must be mysql_blog.
b. Use image mariadb with any tag (preferably latest). Check here for more details.
c. Map mysql_blog container's port 3306 with host port 3306
d. Map mysql_blog container's /var/lib/mysql volume with host volume /var/lib/mysql.
e. Set MYSQL_DATABASE=database_blog and use any custom user ( except root ) with some complex password for DB connections.
After running docker-compose up you can access the app with curl command curl <server-ip or hostname>:8089/
For more details check here.
Note: Once you click on FINISH button, all currently running/stopped containers will be destroyed and stack will be deployed again using your compose file.
```bash
ssh tony@stapp01
# Ir0nM@n
sudo su -
sudo mkdir -p /opt/finance
sudo tee /opt/finance/docker-compose.yml > /dev/null <<'EOF'
version: "3.8"
services:
web:
image: php:apache
container_name: php_blog
ports:
- "8089:80"
volumes:
- /var/www/html:/var/www/html
db:
image: mariadb:latest
container_name: mysql_blog
ports:
- "3306:3306"
volumes:
- /var/lib/mysql:/var/lib/mysql
environment:
MYSQL_DATABASE: database_blog
MYSQL_USER: blog_user
MYSQL_PASSWORD: Bl0g_P@ss2024
MYSQL_ROOT_PASSWORD: R00t_P@ss2024
EOF
cd /opt/finance
sudo docker compose up -d
sudo docker ps # php_blog + mysql_blog, both Up
curl http://localhost:8089/
```
## Task 47
A python app needed to be Dockerized, and then it needs to be deployed on App Server 3. We have already copied a requirements.txt file (having the app dependencies) under /python_app/src/ directory on App Server 3. Further complete this task as per details mentioned below:
Create a Dockerfile under /python_app directory:
Use any python image as the base image.
Install the dependencies using requirements.txt file.
Expose the port 3004.
Run the server.py script using CMD.
Build an image named nautilus/python-app using this Dockerfile.
Once image is built, create a container named pythonapp_nautilus:
Map port 3004 of the container to the host port 8096.
Once deployed, you can test the app using curl command on App Server 3.
```bash
ssh banner@stapp03
# BigGr33n
sudo su -
ls -la /python_app/src/ # confirm server.py + requirements.txt here
tee /python_app/Dockerfile > /dev/null <<'EOF'
FROM python:3.12
WORKDIR /python_app/src
COPY src/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY src/ .
EXPOSE 3004
CMD ["python", "server.py"]
EOF
cd /python_app
docker build -t nautilus/python-app .
docker run -d --name pythonapp_nautilus -p 8096:3004 nautilus/python-app
docker ps
curl http://localhost:8096/
```
### Task 48
The Nautilus DevOps team is diving into Kubernetes for application management. One team member has a task to create a pod according to the details below:
Create a pod named pod-httpd using the httpd image with the latest tag. Ensure to specify the tag as httpd:latest.
Set the app label to httpd_app, and name the container as httpd-container.
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
### Solution
# Create pod-httpd
## Apply
```bash
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: pod-httpd
labels:
app: httpd_app
spec:
containers:
- name: httpd-container
image: httpd:latest
EOF
```
## Verify
```bash
kubectl get pods
kubectl get pod pod-httpd --show-labels
kubectl describe pod pod-httpd
```
Want: `pod-httpd` in `Running` state, label `app=httpd_app`, container named `httpd-container` running `httpd:latest`.
## Task 49
The Nautilus DevOps team is delving into Kubernetes for app management. One team member needs to create a deployment following these details:
Create a deployment named nginx to deploy the application nginx using the image nginx:latest (ensure to specify the tag)
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
### Solution
# Create nginx deployment via heredoc
```bash
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
app: nginx
spec:
replicas: 1
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest
EOF
```
## Verify
```bash
kubectl get deployments
kubectl get pods
kubectl describe deployment nginx | grep -i image
```
## Task 50
The Nautilus DevOps team has noticed performance issues in some Kubernetes-hosted applications due to resource constraints. To address this, they plan to set limits on resource utilization. Here are the details:
Create a pod named httpd-pod with a container named httpd-container. Use the httpd image with the latest tag (specify as httpd:latest). Configure the following container-level resource requests and limits for the container:
Requests: Memory: 15Mi, CPU: 100m
Limits: Memory: 20Mi, CPU: 100m
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
### Solution
# Create httpd-pod with resource limits
```bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: httpd-pod
labels:
app: httpd_app
spec:
containers:
- name: httpd-container
image: httpd:latest
resources:
requests:
memory: "15Mi"
cpu: "100m"
limits:
memory: "20Mi"
cpu: "100m"
EOF
```
## Verify
```bash
kubectl get pod httpd-pod
kubectl describe pod httpd-pod | grep -A6 -i limits
```
Want: `httpd-pod` Running, and the describe output showing requests (15Mi/100m) + limits (20Mi/100m).

View File

@@ -0,0 +1,575 @@
## Task 51
An application currently running on the Kubernetes cluster employs the nginx web server. The Nautilus application development team has introduced some recent changes that need deployment. They've crafted an image nginx:1.18 with the latest updates.
Execute a rolling update for this application, integrating the nginx:1.18 image. The deployment is named nginx-deployment.
Ensure all pods are operational post-update.
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
### Solution
# Rolling update nginx-deployment to nginx:1.18
## Check current state first
```bash
kubectl get deployment nginx-deployment
kubectl describe deployment nginx-deployment | grep -i image
```
Note the container name from the describe output (needed for the set image command).
## Perform the rolling update
```bash
kubectl set image deployment/nginx-deployment nginx-container=nginx:1.18
```
Replace `nginx` (left of `=`) with the actual container name if it differs — check the describe output above.
## Watch the rollout
```bash
kubectl rollout status deployment/nginx-deployment
```
## Verify
```bash
kubectl rollout status deployment/nginx-deployment
kubectl get pods
kubectl describe deployment nginx-deployment | grep -i image
```
Want: rollout `successfully rolled out`, all pods `Running`, image now `nginx:1.18`.
## Task 52
Earlier today, the Nautilus DevOps team deployed a new release for an application. However, a customer has reported a bug related to this recent release. Consequently, the team aims to revert to the previous version.
There exists a deployment named nginx-deployment; initiate a rollback to the previous revision.
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
### Solution
# Rollback nginx-deployment to previous revision
## Check history first (optional but good)
```bash
kubectl rollout history deployment/nginx-deployment
```
Shows the revisions — confirms there's a previous one to roll back to.
## Perform the rollback
```bash
kubectl rollout undo deployment/nginx-deployment
```
## Watch + verify
```bash
kubectl rollout status deployment/nginx-deployment
kubectl get pods
kubectl describe deployment nginx-deployment | grep -i image
```
Want: rollout `successfully rolled out`, all pods `Running`, image reverted to the previous version.
## Task 53
We encountered an issue with our Nginx and PHP-FPM setup on the Kubernetes cluster this morning, which halted its functionality. Investigate and rectify the issue:
The pod name is nginx-phpfpm and configmap name is nginx-config. Identify and fix the problem.
Once resolved, copy /home/thor/index.php file from the jump host to the nginx-container within the nginx document root. After this, you should be able to access the website using Website button on the top bar.
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
### Solution
# Fix nginx-phpfpm pod
## Step 1 — recon: inspect the pod and configmap
```bash
# pod state + how it's configured
kubectl describe pod nginx-phpfpm
kubectl get pod nginx-phpfpm -o yaml
# the nginx config
kubectl describe configmap nginx-config
kubectl get configmap nginx-config -o yaml
```
## Step 2 — identify the mismatch
Compare two things:
- The **document root** in the nginx config (from the configmap) — look for `root /some/path;`
- The **shared volume mountPath** in the pod spec — where the shared `emptyDir` volume is mounted in *both* the nginx and php-fpm containers
The bug: these two paths **don't match**. nginx serves from one path, but the shared volume (where files land) is mounted at a different path → nginx can't find the files.
## Step 3 — fix the mismatch
The fix is to make them consistent. Usually the **configmap's `root` directive** is edited to match the volume mountPath (or vice versa). Edit the configmap:
```bash
kubectl edit configmap nginx-config
```
Change the `root` line so it matches the shared volume's mountPath in the pod spec. Common correct value: `/var/www/html`.
## Step 4 — recreate the pod (configmap changes need a pod restart)
```bash
kubectl get pod nginx-phpfpm -o yaml > /tmp/nginx-phpfpm.yaml
kubectl delete pod nginx-phpfpm
kubectl apply -f /tmp/nginx-phpfpm.yaml
```
## Step 5 — copy index.php into the nginx container's docroot
```bash
kubectl cp /home/thor/index.php nginx-phpfpm:/var/www/html/index.php -c nginx-container
```
(Use the actual docroot path confirmed in step 2/3, and `-c nginx-container` to target the right container.)
## Step 6 — verify
```bash
kubectl get pod nginx-phpfpm
kubectl exec nginx-phpfpm -c nginx-container -- ls -l /var/www/html/
```
Then hit the **Website** button.
## Task 54
We are working on an application that will be deployed on multiple containers within a pod on Kubernetes cluster. There is a requirement to share a volume among the containers to save some temporary data. The Nautilus DevOps team is developing a similar template to replicate the scenario. Below you can find more details about it.
Create a pod named volume-share-nautilus.
For the first container, use image debian with latest tag only and remember to mention the tag i.e debian:latest, container should be named as volume-container-nautilus-1, and run a sleep command for it so that it remains in running state. Volume volume-share should be mounted at path /tmp/blog.
For the second container, use image debian with the latest tag only and remember to mention the tag i.e debian:latest, container should be named as volume-container-nautilus-2, and again run a sleep command for it so that it remains in running state. Volume volume-share should be mounted at path /tmp/games.
Volume name should be volume-share of type emptyDir.
After creating the pod, exec into the first container i.e volume-container-nautilus-1, and just for testing create a file blog.txt with the content Welcome to xFusionCorp Industries under the mounted path of first container i.e /tmp/blog.
The file blog.txt should be present under the mounted path /tmp/games on the second container volume-container-nautilus-2 as well, since they are using a shared volume.
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
### Solution
# Create volume-share-nautilus pod
## Create the pod
```bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: volume-share-nautilus
spec:
containers:
- name: volume-container-nautilus-1
image: debian:latest
command: ["sleep", "infinity"]
volumeMounts:
- name: volume-share
mountPath: /tmp/blog
- name: volume-container-nautilus-2
image: debian:latest
command: ["sleep", "infinity"]
volumeMounts:
- name: volume-share
mountPath: /tmp/games
volumes:
- name: volume-share
emptyDir: {}
EOF
```
## Wait for it to be Running
```bash
kubectl get pod volume-share-nautilus -w
```
## Write the file in container 1
```bash
kubectl exec volume-share-nautilus -c volume-container-nautilus-1 -- \
bash -c 'echo "Welcome to xFusionCorp Industries" > /tmp/blog/blog.txt'
```
## Verify it appears in container 2 (shared volume)
```bash
kubectl exec volume-share-nautilus -c volume-container-nautilus-2 -- \
cat /tmp/games/blog.txt
```
Want: `Welcome to xFusionCorp Industries` — proving the emptyDir is shared across both containers at their respective mount paths.
## Task 55
We have a web server container running the nginx image. The access and error logs generated by the web server are not critical enough to be placed on a persistent volume. However, Nautilus developers need access to the last 24 hours of logs so that they can trace issues and bugs. Therefore, we need to ship the access and error logs for the web server to a log-aggregation service. Following the separation of concerns principle, we implement the Sidecar pattern by deploying a second container that ships the error and access logs from nginx. Nginx does one thing, and it does it well - serving web pages. The second container also specializes in its task - shipping logs. Since containers are running on the same Pod, we can use a shared emptyDir volume to read and write logs.
Create a pod named webserver.
Create an emptyDir volume named shared-logs.
Create a regular container in the webserver pod from the nginx:latest image named nginx-container, and an init container from the ubuntu:latest image named sidecar-container.
Add the following command to the sidecar-container "sh","-c","while true; do cat /var/log/nginx/access.log /var/log/nginx/error.log; sleep 30; done"
Mount the shared-logs volume in both containers at /var/log/nginx. Ensure all containers are in a running state.
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
### Solution
# Create webserver sidecar pod
```bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: webserver
spec:
volumes:
- name: shared-logs
emptyDir: {}
initContainers:
- name: sidecar-container
image: ubuntu:latest
restartPolicy: Always
command: ["sh", "-c", "while true; do cat /var/log/nginx/access.log /var/log/nginx/error.log; sleep 30; done"]
volumeMounts:
- name: shared-logs
mountPath: /var/log/nginx
containers:
- name: nginx-container
image: nginx:latest
volumeMounts:
- name: shared-logs
mountPath: /var/log/nginx
EOF
```
## Verify
```bash
kubectl get pod webserver
kubectl get pod webserver -o jsonpath='{.status.phase}{"\n"}'
kubectl describe pod webserver | grep -A2 -i "state"
```
Want: pod `Running`, both `nginx-container` and `sidecar-container` up (Ready/Running).
## Task 56
Some of the Nautilus team developers are developing a static website and they want to deploy it on Kubernetes cluster. They want it to be highly available and scalable. Therefore, based on the requirements, the DevOps team has decided to create a deployment for it with multiple replicas. Below you can find more details about it:
Create a deployment using nginx image with latest tag only and remember to mention the tag i.e nginx:latest. Name it as nginx-deployment. The container should be named as nginx-container, also make sure replica counts are 3.
Create a NodePort type service named nginx-service. The nodePort should be 30011.
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
### Solution
# nginx deployment + NodePort service
## Deployment
```bash
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx-container
image: nginx:latest
ports:
- containerPort: 80
EOF
```
## NodePort Service
```bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
type: NodePort
selector:
app: nginx
ports:
- port: 80
targetPort: 80
nodePort: 30011
EOF
```
## Verify
```bash
kubectl get deployment nginx-deployment
kubectl get pods -l app=nginx
kubectl get service nginx-service
```
Want: deployment `3/3` ready, three pods Running, service `nginx-service` type NodePort exposing `30011`.
## Task 57
The Nautilus DevOps team is working on to setup some pre-requisites for an application that will send the greetings to different users. There is a sample deployment, that needs to be tested. Below is a scenario which needs to be configured on Kubernetes cluster. Please find below more details about it.
Create a pod named print-envars-greeting.
Configure spec as, the container name should be print-env-container and use bash image.
Create three environment variables:
a. GREETING and its value should be Welcome to
b. COMPANY and its value should be Stratos
c. GROUP and its value should be Industries
Use command ["/bin/sh", "-c", 'echo "$(GREETING) $(COMPANY) $(GROUP)"'] (please use this exact command), also set its restartPolicy policy to Never to avoid crash loop back.
You can check the output using kubectl logs -f print-envars-greeting command.
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
### Solution
# Create print-envars-greeting pod
```bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: print-envars-greeting
spec:
restartPolicy: Never
containers:
- name: print-env-container
image: bash
command: ["/bin/sh", "-c", 'echo "$(GREETING) $(COMPANY) $(GROUP)"']
env:
- name: GREETING
value: "Welcome to"
- name: COMPANY
value: "Stratos"
- name: GROUP
value: "Industries"
EOF
```
## Check the output
```bash
kubectl logs -f print-envars-greeting
```
Want: `Welcome to Stratos Industries` in the logs, pod in `Completed` state.
## Task 58
The Nautilus DevOps teams is planning to set up a Grafana tool to collect and analyze analytics from some applications. They are planning to deploy it on Kubernetes cluster. Below you can find more details.
1.) Create a deployment named grafana-deployment-xfusion using any grafana image for Grafana app. Set other parameters as per your choice.
2.) Create NodePort type service with nodePort 32000 to expose the app.
You do not need to make any configuration changes inside the Grafana app once deployed; just make sure you can access the Grafana login page.
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
### Solution
# Grafana deployment + NodePort service
```bash
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: grafana-deployment-xfusion
labels:
app: grafana-deployment-xfusion
spec:
replicas: 1
selector:
matchLabels:
app: grafana-deployment-xfusion
template:
metadata:
labels:
app: grafana-deployment-xfusion
spec:
containers:
- name: grafana
image: grafana/grafana:latest
ports:
- containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
name: grafana-service
spec:
type: NodePort
selector:
app: grafana-deployment-xfusion
ports:
- port: 32000
targetPort: 3000
nodePort: 32000
EOF
```
## Verify
```bash
kubectl get deployment grafana-deployment-xfusion
kubectl get pods -l app=grafana
kubectl get service grafana-service
kubectl rollout status deployment/grafana-deployment-xfusion
```
Want: deployment `1/1` ready, pod Running, service exposing `32000`. Then hit the app — Grafana login page should load.
## Task 59
Last week, the Nautilus DevOps team deployed a redis app on Kubernetes cluster, which was working fine so far. This morning one of the team members was making some changes in this existing setup, but he made some mistakes and the app went down. We need to fix this as soon as possible. Please take a look.
The deployment name is redis-deployment. The pods are not in running state right now, so please look into the issue and fix the same.
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
### Solution
- incorrect config map name
- incorrect image on redis
## Task 60
The Nautilus DevOps team is working on a Kubernetes template to deploy a web application on the cluster. There are some requirements to create/use persistent volumes to store the application code, and the template needs to be designed accordingly. Please find more details below:
Create a PersistentVolume named as pv-devops. Configure the spec as storage class should be manual, set capacity to 4Gi, set access mode to ReadWriteOnce, volume type should be hostPath and set path to /mnt/devops (this directory is already created, you might not be able to access it directly, so you need not to worry about it).
Create a PersistentVolumeClaim named as pvc-devops. Configure the spec as storage class should be manual, request 1Gi of the storage, set access mode to ReadWriteOnce.
Create a pod named as pod-devops, mount the persistent volume you created with claim name pvc-devops at document root of the web server, the container within the pod should be named as container-devops using image nginx with latest tag only (remember to mention the tag i.e nginx:latest).
Create a node port type service named web-devops using node port 30008 to expose the web server running within the pod.
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
### Solution
# PV + PVC + Pod + Service (devops)
```bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-devops
spec:
storageClassName: manual
capacity:
storage: 4Gi
accessModes:
- ReadWriteOnce
hostPath:
path: /mnt/devops
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-devops
spec:
storageClassName: manual
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Pod
metadata:
name: pod-devops
labels:
app: web-devops
spec:
containers:
- name: container-devops
image: nginx:latest
ports:
- containerPort: 80
volumeMounts:
- name: web-storage
mountPath: /usr/share/nginx/html
volumes:
- name: web-storage
persistentVolumeClaim:
claimName: pvc-devops
---
apiVersion: v1
kind: Service
metadata:
name: web-devops
spec:
type: NodePort
selector:
app: web-devops
ports:
- port: 80
targetPort: 80
nodePort: 30008
EOF
```
## Verify
```bash
kubectl get pv pv-devops
kubectl get pvc pvc-devops
kubectl get pod pod-devops
kubectl get svc web-devops
```
Want: PV `Bound`, PVC `Bound`, pod `Running`, service exposing `30008`.

View File

@@ -0,0 +1,994 @@
## Task 61
There are some applications that need to be deployed on Kubernetes cluster and these apps have some pre-requisites where some configurations need to be changed before deploying the app container. Some of these changes cannot be made inside the images so the DevOps team has come up with a solution to use init containers to perform these tasks during deployment. Below is a sample scenario that the team is going to test first.
Create a Deployment named as ic-deploy-xfusion.
Configure spec as replicas should be 1, labels app should be ic-xfusion, template's metadata lables app should be the same ic-xfusion.
The initContainers should be named as ic-msg-xfusion, use image fedora with latest tag and use command '/bin/bash', '-c' and 'echo Init Done - Welcome to xFusionCorp Industries > /ic/blog'. The volume mount should be named as ic-volume-xfusion and mount path should be /ic.
Main container should be named as ic-main-xfusion, use image fedora with latest tag and use command '/bin/bash', '-c' and 'while true; do cat /ic/blog; sleep 5; done'. The volume mount should be named as ic-volume-xfusion and mount path should be /ic.
Volume to be named as ic-volume-xfusion and it should be an emptyDir type.
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
### Solution
# Create ic-deploy-xfusion deployment
```bash
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
name: ic-deploy-xfusion
labels:
app: ic-xfusion
spec:
replicas: 1
selector:
matchLabels:
app: ic-xfusion
template:
metadata:
labels:
app: ic-xfusion
spec:
initContainers:
- name: ic-msg-xfusion
image: fedora:latest
command: ["/bin/bash", "-c", "echo Init Done - Welcome to xFusionCorp Industries > /ic/blog"]
volumeMounts:
- name: ic-volume-xfusion
mountPath: /ic
containers:
- name: ic-main-xfusion
image: fedora:latest
command: ["/bin/bash", "-c", "while true; do cat /ic/blog; sleep 5; done"]
volumeMounts:
- name: ic-volume-xfusion
mountPath: /ic
volumes:
- name: ic-volume-xfusion
emptyDir: {}
EOF
```
## Verify
```bash
kubectl get deployment ic-deploy-xfusion
kubectl get pods -l app=ic-xfusion
kubectl logs -l app=ic-xfusion -c ic-main-xfusion
```
Want: deployment `1/1` ready, pod Running, and the main container's logs printing `Init Done - Welcome to xFusionCorp Industries` every 5s.
## Task 62
The Nautilus DevOps team is working to deploy some tools in Kubernetes cluster. Some of the tools are licence based so that licence information needs to be stored securely within Kubernetes cluster. Therefore, the team wants to utilize Kubernetes secrets to store those secrets. Below you can find more details about the requirements:
We already have a secret key file ecommerce.txt under the /opt/ directory. Create a generic secret named ecommerce, it should contain the password/license-number present in ecommerce.txt file.
Also create a pod named secret-nautilus.
Configure pod's spec as container name should be secret-container-nautilus, image should be debian with latest tag (remember to mention the tag with image). Use sleep command for container so that it remains in running state. Consume the created secret and mount it under /opt/games within the container.
To verify you can exec into the container secret-container-nautilus, to check the secret key under the mounted path /opt/games. Before hitting the Check button please make sure pod/pods are in running state, also validation can take some time to complete so keep patience.
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
### Solution
# Create secret + pod that mounts it
## Step 1 — create the generic secret from the file
```bash
kubectl create secret generic ecommerce --from-file=/opt/ecommerce.txt
```
## Step 2 — create the pod mounting the secret at /opt/games
```bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Pod
metadata:
name: secret-nautilus
spec:
containers:
- name: secret-container-nautilus
image: debian:latest
command: ["sleep", "infinity"]
volumeMounts:
- name: secret-volume
mountPath: /opt/games
volumes:
- name: secret-volume
secret:
secretName: ecommerce
EOF
```
## Verify
```bash
kubectl get secret ecommerce
kubectl get pod secret-nautilus
kubectl exec secret-nautilus -- ls -l /opt/games
kubectl exec secret-nautilus -- cat /opt/games/ecommerce.txt
```
Want: secret `ecommerce` exists, pod `Running`, and `/opt/games/ecommerce.txt` present with the license content inside the container.
## Task 63
There is an iron gallery app that the Nautilus DevOps team was developing. They have recently customized the app and are going to deploy the same on the Kubernetes cluster. Below you can find more details:
Create a namespace iron-namespace-nautilus
Create a deployment iron-gallery-deployment-nautilus for iron gallery under the same namespace you created.
:- Labels run should be iron-gallery.
:- Replicas count should be 1.
:- Selector's matchLabels run should be iron-gallery.
:- Template labels run should be iron-gallery under metadata.
:- The container should be named as iron-gallery-container-nautilus, use kodekloud/irongallery:2.0 image ( use exact image name / tag ).
:- Resources limits for memory should be 100Mi and for CPU should be 50m.
:- First volumeMount name should be config, its mountPath should be /usr/share/nginx/html/data.
:- Second volumeMount name should be images, its mountPath should be /usr/share/nginx/html/uploads.
:- First volume name should be config and give it emptyDir and second volume name should be images, also give it emptyDir.
Create a deployment iron-db-deployment-nautilus for iron db under the same namespace.
:- Labels db should be mariadb.
:- Replicas count should be 1.
:- Selector's matchLabels db should be mariadb.
:- Template labels db should be mariadb under metadata.
:- The container name should be iron-db-container-nautilus, use kodekloud/irondb:2.0 image ( use exact image name / tag ).
:- Define environment, set MYSQL_DATABASE its value should be database_apache, set MYSQL_ROOT_PASSWORD and MYSQL_PASSWORD value should be with some complex passwords for DB connections, and MYSQL_USER value should be any custom user ( except root ).
:- Volume mount name should be db and its mountPath should be /var/lib/mysql. Volume name should be db and give it an emptyDir.
Create a service for iron db which should be named iron-db-service-nautilus under the same namespace. Configure spec as selector's db should be mariadb. Protocol should be TCP, port and targetPort should be 3306 and its type should be ClusterIP.
Create a service for iron gallery which should be named iron-gallery-service-nautilus under the same namespace. Configure spec as selector's run should be iron-gallery. Protocol should be TCP, port and targetPort should be 80, nodePort should be 32678 and its type should be NodePort.
Note:
We don't need to make connection b/w database and front-end now, if the installation page is coming up it should be enough for now.
The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
### Solution
# Iron Gallery app — namespace, deployments, services
```bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Namespace
metadata:
name: iron-namespace-nautilus
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: iron-gallery-deployment-nautilus
namespace: iron-namespace-nautilus
labels:
run: iron-gallery
spec:
replicas: 1
selector:
matchLabels:
run: iron-gallery
template:
metadata:
labels:
run: iron-gallery
spec:
containers:
- name: iron-gallery-container-nautilus
image: kodekloud/irongallery:2.0
resources:
limits:
memory: "100Mi"
cpu: "50m"
volumeMounts:
- name: config
mountPath: /usr/share/nginx/html/data
- name: images
mountPath: /usr/share/nginx/html/uploads
volumes:
- name: config
emptyDir: {}
- name: images
emptyDir: {}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: iron-db-deployment-nautilus
namespace: iron-namespace-nautilus
labels:
db: mariadb
spec:
replicas: 1
selector:
matchLabels:
db: mariadb
template:
metadata:
labels:
db: mariadb
spec:
containers:
- name: iron-db-container-nautilus
image: kodekloud/irondb:2.0
env:
- name: MYSQL_DATABASE
value: "database_apache"
- name: MYSQL_ROOT_PASSWORD
value: "R00t_C0mpl3x_P@ss"
- name: MYSQL_PASSWORD
value: "Us3r_C0mpl3x_P@ss"
- name: MYSQL_USER
value: "iron_user"
volumeMounts:
- name: db
mountPath: /var/lib/mysql
volumes:
- name: db
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: iron-db-service-nautilus
namespace: iron-namespace-nautilus
spec:
type: ClusterIP
selector:
db: mariadb
ports:
- protocol: TCP
port: 3306
targetPort: 3306
---
apiVersion: v1
kind: Service
metadata:
name: iron-gallery-service-nautilus
namespace: iron-namespace-nautilus
spec:
type: NodePort
selector:
run: iron-gallery
ports:
- protocol: TCP
port: 80
targetPort: 80
nodePort: 32678
EOF
```
## Verify
```bash
kubectl get all -n iron-namespace-nautilus
kubectl get pods -n iron-namespace-nautilus
kubectl get svc -n iron-namespace-nautilus
```
Want: both deployments `1/1`, both pods Running, db service ClusterIP on 3306, gallery service NodePort on 32678. Then the iron gallery install page should load via the nodePort.
## Task 64
One of the DevOps engineers was trying to deploy a python app on Kubernetes cluster. Unfortunately, due to some mis-configuration, the application is not coming up. Please take a look into it and fix the issues. Application should be accessible on the specified nodePort.
The deployment name is python-deployment-devops, its using poroko/flask-demo-app image. The deployment and service of this app is already deployed.
nodePort should be 32345 and targetPort should be python flask app's default port.
Note: The kubectl utility on the jump-host has been configured to work with the Kubernetes cluster.
### Solution
```bash
kubectl set image deployment/python-deployment-devops python-container-devops=poroko/flask-demo-app
kubectl patch svc python-service-devops --type=json -p='[
{"op":"replace","path":"/spec/ports/0/targetPort","value":5000},
{"op":"replace","path":"/spec/ports/0/nodePort","value":32345}
]'
```
## Task 65
The Nautilus application development team observed some performance issues with one of the application that is deployed in Kubernetes cluster. After looking into number of factors, the team has suggested to use some in-memory caching utility for DB service. After number of discussions, they have decided to use Redis. Initially they would like to deploy Redis on kubernetes cluster for testing and later they will move it to production. Please find below more details about the task:
Create a redis deployment with following parameters:
Create a config map called my-redis-config having maxmemory 2mb in redis-config.
Name of the deployment should be redis-deployment, it should use
redis:alpine image and container name should be redis-container. Also make sure it has only 1 replica.
The container should request for 1 CPU.
Mount 2 volumes:
a. An Empty directory volume called data at path /redis-master-data.
b. A configmap volume called redis-config at path /redis-master.
c. The container should expose the port 6379.
Finally, redis-deployment should be up and running.
### Solution
# Redis deployment with configmap
```bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: ConfigMap
metadata:
name: my-redis-config
data:
redis-config: |
maxmemory 2mb
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-deployment
labels:
app: redis
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis-container
image: redis:alpine
resources:
requests:
cpu: "1"
ports:
- containerPort: 6379
volumeMounts:
- name: data
mountPath: /redis-master-data
- name: redis-config
mountPath: /redis-master
volumes:
- name: data
emptyDir: {}
- name: redis-config
configMap:
name: my-redis-config
EOF
```
## Verify
```bash
kubectl get configmap my-redis-config
kubectl get deployment redis-deployment
kubectl get pods -l app=redis
kubectl describe pod -l app=redis | grep -A3 -i "requests\|mounts"
```
Want: configmap exists, deployment `1/1` ready, pod Running, CPU request 1, both volumes mounted.
## Task 66
A new MySQL server needs to be deployed on Kubernetes cluster. The Nautilus DevOps team was working on to gather the requirements. Recently they were able to finalize the requirements and shared them with the team members to start working on it. Below you can find the details:
1.) Create a PersistentVolume mysql-pv, its capacity should be 250Mi, set other parameters as per your preference.
2.) Create a PersistentVolumeClaim to request this PersistentVolume storage. Name it as mysql-pv-claim and request a 250Mi of storage. Set other parameters as per your preference.
3.) Create a deployment named mysql-deployment, use any mysql image as per your preference. Mount the PersistentVolume at mount path /var/lib/mysql.
4.) Create a NodePort type service named mysql and set nodePort to 30007.
5.) Create a secret named mysql-root-pass having a key pair value, where key is password and its value is YUIidhb667, create another secret named mysql-user-pass having some key pair values, where first key is username and its value is kodekloud_pop, second key is password and value is BruCStnMT5, create one more secret named mysql-db-url, key name is database and value is kodekloud_db10
6.) Define some environment variables within the container:
a.) name: MYSQL_ROOT_PASSWORD, should pick value from secretKeyRef name: mysql-root-pass and key: password
b.) name: MYSQL_DATABASE, should pick value from secretKeyRef name: mysql-db-url and key: database
c.) name: MYSQL_USER, should pick value from secretKeyRef name: mysql-user-pass key key: username
d.) name: MYSQL_PASSWORD, should pick value from secretKeyRef name: mysql-user-pass and key: password
### Solution
# MySQL deployment with secrets, PV/PVC, service
```bash
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Secret
metadata:
name: mysql-root-pass
type: Opaque
stringData:
password: YUIidhb667
---
apiVersion: v1
kind: Secret
metadata:
name: mysql-user-pass
type: Opaque
stringData:
username: kodekloud_pop
password: BruCStnMT5
---
apiVersion: v1
kind: Secret
metadata:
name: mysql-db-url
type: Opaque
stringData:
database: kodekloud_db10
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: mysql-pv
spec:
storageClassName: manual
capacity:
storage: 250Mi
accessModes:
- ReadWriteOnce
hostPath:
path: /mnt/mysql-data
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pv-claim
spec:
storageClassName: manual
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 250Mi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql-deployment
labels:
app: mysql
spec:
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-root-pass
key: password
- name: MYSQL_DATABASE
valueFrom:
secretKeyRef:
name: mysql-db-url
key: database
- name: MYSQL_USER
valueFrom:
secretKeyRef:
name: mysql-user-pass
key: username
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-user-pass
key: password
volumeMounts:
- name: mysql-storage
mountPath: /var/lib/mysql
volumes:
- name: mysql-storage
persistentVolumeClaim:
claimName: mysql-pv-claim
---
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
type: NodePort
selector:
app: mysql
ports:
- port: 3306
targetPort: 3306
nodePort: 30007
EOF
```
## Verify
```bash
kubectl get secrets
kubectl get pv mysql-pv
kubectl get pvc mysql-pv-claim
kubectl get deployment mysql-deployment
kubectl get pods -l app=mysql
kubectl get svc mysql
```
Want: 3 secrets, PV+PVC `Bound`, deployment `1/1`, pod Running, service NodePort 30007.
## Task 67
The Nautilus Application development team has finished development of one of the applications and it is ready for deployment. It is a guestbook application that will be used to manage entries for guests/visitors. As per discussion with the DevOps team, they have finalized the infrastructure that will be deployed on Kubernetes cluster. Below you can find more details about it.
BACK-END TIER
Create a deployment named redis-master for Redis master.
a.) Replicas count should be 1.
b.) Container name should be master-redis-devops and it should use image redis.
c.) Request resources as CPU should be 100m and Memory should be 100Mi.
d.) Container port should be redis default port i.e 6379.
Create a service named redis-master for Redis master. Port and targetPort should be Redis default port i.e 6379.
Create another deployment named redis-slave for Redis slave.
a.) Replicas count should be 2.
b.) Container name should be slave-redis-devops and it should use gcr.io/google_samples/gb-redisslave:v3 image.
c.) Requests resources as CPU should be 100m and Memory should be 100Mi.
d.) Define an environment variable named GET_HOSTS_FROM and its value should be dns.
e.) Container port should be Redis default port i.e 6379.
Create another service named redis-slave. It should use Redis default port i.e 6379.
Create another service named redis-follower. Port and targetPort should be Redis default port i.e 6379. Its selector app should be redis-slave.
FRONT END TIER
Create a deployment named frontend.
a.) Replicas count should be 3.
b.) Container name should be php-redis-devops and it should use gcr.io/google-samples/gb-frontend@sha256:a908df8486ff66f2c4daa0d3d8a2fa09846a1fc8efd65649c0109695c7c5cbff image.
c.) Request resources as CPU should be 100m and Memory should be 100Mi.
d.) Define an environment variable named as GET_HOSTS_FROM and its value should be dns.
e.) Container port should be 80.
Create a service named frontend. Its type should be NodePort, port should be 80 and its nodePort should be 30009.
Finally, you can check the guestbook app by clicking on App button.
You can use any labels as per your choice.
### Solution
# Guestbook app — full stack
```bash
kubectl apply -f - <<'EOF'
# ===== BACK-END: redis-master =====
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-master
labels:
app: redis-master
spec:
replicas: 1
selector:
matchLabels:
app: redis-master
template:
metadata:
labels:
app: redis-master
spec:
containers:
- name: master-redis-devops
image: redis
resources:
requests:
cpu: "100m"
memory: "100Mi"
ports:
- containerPort: 6379
---
apiVersion: v1
kind: Service
metadata:
name: redis-master
spec:
selector:
app: redis-master
ports:
- port: 6379
targetPort: 6379
---
# ===== BACK-END: redis-slave =====
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-slave
labels:
app: redis-slave
spec:
replicas: 2
selector:
matchLabels:
app: redis-slave
template:
metadata:
labels:
app: redis-slave
spec:
containers:
- name: slave-redis-devops
image: gcr.io/google_samples/gb-redisslave:v3
resources:
requests:
cpu: "100m"
memory: "100Mi"
env:
- name: GET_HOSTS_FROM
value: dns
ports:
- containerPort: 6379
---
apiVersion: v1
kind: Service
metadata:
name: redis-slave
spec:
selector:
app: redis-slave
ports:
- port: 6379
targetPort: 6379
---
apiVersion: v1
kind: Service
metadata:
name: redis-follower
spec:
selector:
app: redis-slave
ports:
- port: 6379
targetPort: 6379
---
# ===== FRONT-END =====
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
labels:
app: frontend
spec:
replicas: 3
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: php-redis-devops
image: gcr.io/google-samples/gb-frontend@sha256:a908df8486ff66f2c4daa0d3d8a2fa09846a1fc8efd65649c0109695c7c5cbff
resources:
requests:
cpu: "100m"
memory: "100Mi"
env:
- name: GET_HOSTS_FROM
value: dns
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: frontend
spec:
type: NodePort
selector:
app: frontend
ports:
- port: 80
targetPort: 80
nodePort: 30009
EOF
```
## Verify
```bash
kubectl get deployments
kubectl get pods
kubectl get svc
kubectl get pods -o wide | grep -E "redis|frontend"
```
Want: redis-master `1/1`, redis-slave `2/2`, frontend `3/3`, all four services present, frontend NodePort 30009. Then the App button loads the guestbook.
## Task 68
The DevOps team at xFusionCorp Industries is initiating the setup of CI/CD pipelines and has decided to utilize Jenkins as their server. Execute the task according to the provided requirements:
1. Install Jenkins on the jenkins server using the apt utility only, and start it using the service command.
If you face a timeout issue while starting the Jenkins service, first check the service status with service jenkins status
Then review the logs in /var/log/jenkins/jenkins.log to identify the cause.
2. Jenkin's admin user name should be theadmin, password should be Adm!n321, full name should be Mariyam and email should be mariyam@jenkins.stratos.xfusioncorp.com.
Note:
1. To access the jenkins server, connect from the jump host using the root user with the password S3curePass.
2. After Jenkins server installation, click the Jenkins button on the top bar to access the Jenkins UI and follow on-screen instructions to create an admin user.
### Solution
# Install Jenkins on jenkins server + create admin user
## Access the server
From the jump host, SSH in as root:
```bash
ssh root@jenkins
# password: S3curePass
```
---
## Part 1 — Install Jenkins (CLI)
### Step 1: Install Java (Jenkins prerequisite)
```bash
apt update
apt install -y fontconfig openjdk-17-jre
java -version
apt update
apt install -y openjdk-21-jre
# make Java 21 the default (in case 17 is still selected)
update-alternatives --config java
```
### Step 2: Add the Jenkins apt repository + signing key
```bash
wget -O /usr/share/keyrings/jenkins-keyring.asc \
https://pkg.jenkins.io/debian-stable/jenkins.io-2023.key
echo "deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc]" \
"https://pkg.jenkins.io/debian-stable binary/" | \
tee /etc/apt/sources.list.d/jenkins.list > /dev/null
```
### Step 3: Install Jenkins
```bash
apt update
apt install -y jenkins
```
### Step 4: Start Jenkins using the service command
```bash
service jenkins start
service jenkins status
```
### If you hit a startup timeout
```bash
service jenkins status
cat /var/log/jenkins/jenkins.log
```
The log identifies the cause — most commonly a missing/wrong Java version. Ensure `openjdk-17-jre` is installed and set as default (`update-alternatives --config java`), then restart:
```bash
service jenkins restart
```
Jenkins can take 6090s on first start; a first-attempt timeout doesn't always mean failure — re-check status after waiting.
---
## Part 2 — Create the admin user (Web UI)
### Step 1: Get the initial unlock password
```bash
cat /var/lib/jenkins/secrets/initialAdminPassword
```
Copy the output.
### Step 2: Open the Jenkins UI
Click the **Jenkins** button on the top bar.
### Step 3: Unlock Jenkins
Paste the initial admin password from Step 1 → **Continue**.
### Step 4: Install plugins
Choose **"Install suggested plugins"** and wait for completion.
### Step 5: Create the First Admin User
Fill in exactly:
| Field | Value |
|-------|-------|
| Username | `theadmin` |
| Password | `Adm!n321` |
| Full name | `Mariyam` |
| Email | `mariyam@jenkins.stratos.xfusioncorp.com` |
Click **Save and Continue****Save and Finish****Start using Jenkins**.
---
## Verify
- `service jenkins status` shows Jenkins active/running.
- You can log into the Jenkins UI as `theadmin` / `Adm!n321`.
- The dashboard loads with `theadmin` as the logged-in user.
---
## Key points
- **Java first** — Jenkins is a Java app; without a JRE it won't start (the exact timeout the task warns about). Java 17 is the safe LTS-compatible choice.
- **Repo + keyring required** — Jenkins isn't in Ubuntu base repos; add `pkg.jenkins.io` with its signing key before `apt install jenkins`.
- **`debian-stable`** = Jenkins LTS line (more stable than weekly).
- **`service jenkins start`** per the task (not `systemctl`).
- **`initialAdminPassword`** unlocks the first UI screen before you can create `theadmin`.
- **Admin user is created via the web wizard**, not CLI — the four field values must be exact for the grader.
- Take a screenshot of the finished admin-user setup / dashboard for review.
## Task 69
The Nautilus DevOps team has recently setup a Jenkins server, which they want to use for some CI/CD jobs. Before that they want to install some plugins which will be used in most of the jobs. Please find below more details about the task
1. Click on the Jenkins button on the top bar to access the Jenkins UI. Login using username admin and password Adm!n321.
2. Once logged in, install the Git and GitLab plugins. You may need to restart Jenkins to complete the plugin installation; if required, opt to Restart Jenkins when installation is complete and no jobs are running on the plugin installation/update page (Update Centre).
Note:
1. After restarting Jenkins, wait for the login page to reappear before proceeding.
2. For tasks involving web UI changes, capture screenshots to share for review or consider using screen recording software like loom.com for documentation and sharing.
### Solution
# Install Git + GitLab plugins in Jenkins
## Step 1 — Log in
Click the **Jenkins** button on the top bar. Log in with:
- Username: `admin`
- Password: `Adm!n321`
## Step 2 — Navigate to Plugin Manager
**Manage Jenkins****Plugins** (under System Configuration; older versions: **Manage Plugins**).
## Step 3 — Find and select the plugins
Go to the **Available plugins** tab.
In the search box, search and check each:
- Search `Git` → check the **Git** plugin
- Search `GitLab` → check the **GitLab** plugin
## Step 4 — Install
Click **Install** (or **Download now and install after restart**).
## Step 5 — Restart Jenkins
On the installation progress page, check the box:
**"Restart Jenkins when installation is complete and no jobs are running"**
Jenkins restarts. **Wait for the login page to reappear** before doing anything else.
## Step 6 — Verify
After restart, log back in and check:
**Manage Jenkins****Plugins****Installed plugins** tab → search `Git` and `GitLab` — both should be listed.
## Task 70
The Nautilus team is integrating Jenkins into their CI/CD pipelines. After setting up a new Jenkins server, they're now configuring user access for the development team, Follow these steps:
1. Click on the Jenkins button on the top bar to access the Jenkins UI. Login with username admin and password Adm!n321.
2. Create a jenkins user named rose with the password TmPcZjtRQx. Their full name should match Rose.
3. Utilize the Project-based Matrix Authorization Strategy to assign overall read permission to the rose user.
4. Remove all permissions for Anonymous users (if any) ensuring that the admin user retains overall Administer permissions.
5. For the existing job, grant rose user only read permissions, disregarding other permissions such as Agent, SCM etc.
Note:
1. You may need to install plugins and restart Jenkins service. After plugins installation, select Restart Jenkins when installation is complete and no jobs are running on plugin installation/update page.
2. After restarting the Jenkins service, wait for the Jenkins login page to reappear before proceeding. Avoid clicking Finish immediately after restarting the service.
3. Capture screenshots of your configuration for review purposes. Consider using screen recording software like loom.com for documentation and sharing.
### Solution:
# Configure Jenkins user access for rose
## Step 1 — Log in
Click **Jenkins** button → login `admin` / `Adm!n321`.
## Step 2 — Install Matrix Authorization Strategy plugin (if not present)
**Manage Jenkins → Plugins → Available plugins** → search **"Matrix Authorization Strategy"** → check it → **Download now and install after restart**.
Wait for Jenkins to fully restart, wait for the **login page** to reappear, log back in.
## Step 3 — Create user rose
**Manage Jenkins → Users → Create User**:
- Username: `rose`
- Password: `TmPcZjtRQx`
- Full name: `Rose`
- (email can be left blank or any value)
Click **Create User**.
## Step 4 — Switch to Project-based Matrix Authorization Strategy
**Manage Jenkins → Security** (or **Configure Global Security**):
- Under **Authorization**, select **"Project-based Matrix Authorization Strategy"**
- A permission matrix appears.
## Step 5 — Set global permissions
In the matrix:
- **Add user** `admin` → grant **Overall / Administer** (full row, or at minimum Administer). **Do this FIRST — critical.**
- **Add user** `rose` → grant **Overall / Read** only.
- **Anonymous** row → **uncheck ALL permissions** (remove everything).
Click **Save**.
## Step 6 — Grant rose read-only on the existing job
Go to the existing job → **Configure** → enable **"Enable project-based security"** (checkbox in the job config):
- **Add user** `rose`
- Grant **only Read** (under Job → Read). Leave Agent, SCM, Build, etc. **unchecked**.
Click **Save**.
## Verify
- `admin` retains Administer (you can still access everything).
- `rose` can log in, sees the job (read), can't configure/build.
- Anonymous has no permissions.

View File

@@ -0,0 +1,927 @@
## Task 71
Some new requirements have come up to install and configure some packages on the Nautilus infrastructure under Stratos Datacenter. The Nautilus DevOps team installed and configured a new Jenkins server so they wanted to create a Jenkins job to automate this task. Find below more details and complete the task accordingly:
1. Access the Jenkins UI by clicking on the Jenkins button in the top bar. Log in using the credentials: username admin and password Adm!n321.
2. Create a new Jenkins job named install-packages and configure it with the following specifications:
Add a string parameter named PACKAGE.
Configure the job to install a package specified in the $PACKAGE parameter on the storage server (Stratos Datacenter).
Build the job at least once (e.g. with parameter PACKAGE=vim-enhanced) so the package is installed on the Storage server and can be verified.
### Solution
# Jenkins job: install-packages
## Step 1 — Log in
Click **Jenkins** → login `admin` / `Adm!n321`.
## Step 2 — Install the "SSH" plugin (Publish Over SSH)
**Manage Jenkins → Plugins → Available plugins** → search **"SSH"** (the "Publish Over SSH" plugin) → check → **Download now and install after restart**.
Wait for restart, wait for login page, log back in.
## Step 3 — Configure the SSH remote host (storage server)
**Manage Jenkins → System** (Configure System) → scroll to **Publish over SSH** section → **SSH Servers → Add**:
- **Name**: `ststor01` (a label)
- **Hostname**: `ststor01` (or its IP)
- **Username**: `natasha`
- Click **Advanced** → check **Use password authentication****Passphrase/Password**: `Bl@kW`
Click **Test Configuration** — should say **Success**. Save.
## Step 4 — Create the job
**New Item** → name `install-packages`**Freestyle project** → OK.
## Step 5 — Add the string parameter
In job config → check **"This project is parameterized"** → **Add Parameter → String Parameter**:
- **Name**: `PACKAGE`
- (default value optional, e.g. `vim-enhanced`)
## Step 6 — Add the build step (install on storage server)
Under **Build Steps****Add build step → Send files or execute commands over SSH**:
- **SSH Server**: select `ststor01`
- **Exec command**:
```bash
sudo yum install -y $PACKAGE
```
Save.
## Step 7 — Build with a parameter
Click **Build with Parameters** → set `PACKAGE` = `vim-enhanced`**Build**.
Check the build's **Console Output** — should show the yum install succeeding.
## Verify
On ststor01:
```bash
rpm -q vim-enhanced # should show it's installed
```
## Task 72
A new DevOps Engineer has joined the team and he will be assigned some Jenkins related tasks. Before that, the team wanted to test a simple parameterized job to understand basic functionality of parameterized builds. He is given a simple parameterized job to build in Jenkins. Please find more details below:
Click on the Jenkins button on the top bar to access the Jenkins UI. Login using username admin and password Adm!n321.
1. Create a parameterized job which should be named as parameterized-job
2. Add a string parameter named Stage; its default value should be Build.
3. Add a choice parameter named env; its choices should be Development, Staging and Production.
4. Configure job to execute a shell command, which should echo both parameter values (you are passing in the job).
5. Build the Jenkins job at least once with choice parameter value Development to make sure it passes.
### solution
# Jenkins job: parameterized-job
## Step 1 — Log in
Click **Jenkins** → login `admin` / `Adm!n321`.
## Step 2 — Create the job
**New Item** → name `parameterized-job`**Freestyle project** → OK.
## Step 3 — Enable parameters
In job config → check **"This project is parameterized"**.
### String parameter
**Add Parameter → String Parameter**:
- **Name**: `Stage`
- **Default Value**: `Build`
### Choice parameter
**Add Parameter → Choice Parameter**:
- **Name**: `env`
- **Choices** (one per line):
```
Development
Staging
Production
```
## Step 4 — Add the shell build step
Under **Build Steps****Add build step → Execute shell**:
```bash
echo "Stage: $Stage"
echo "env: $env"
```
Save.
## Step 5 — Build with parameters
Click **Build with Parameters**:
- `Stage` = `Build` (default)
- `env` = `Development`
Click **Build**.
## Verify
Open the build → **Console Output** → should show:
```
Stage: Build
env: Development
```
Build result: **SUCCESS**.
## Task 73
The devops team of xFusionCorp Industries is working on to setup centralised logging management system to maintain and analyse server logs easily. Since it will take some time to implement, they wanted to gather some server logs on a regular basis. At least one of the app servers is having issues with the Apache server. The team needs Apache logs so that they can identify and troubleshoot the issues easily if they arise. So they decided to create a Jenkins job to collect logs from the server. Please create/configure a Jenkins job as per details mentioned below:
Click on the Jenkins button on the top bar to access the Jenkins UI. Login using username admin and password Adm!n321
1. Create a Jenkins jobs named copy-logs.
2. Configure it to periodically build every 5 minutes to copy the Apache logs (both access_log and error_log) from App Server 2 (stapp02) from the default logs location to location /usr/src/itadmin on the Storage Server.
3. Build the job at least once so that the logs are copied and can be verified.
### Solution
# Jenkins job: copy-logs
## Step 1 — Log in
Click **Jenkins** → login `admin` / `Adm!n321`.
## Step 2 — Ensure "Publish over SSH" plugin is installed
**Manage Jenkins → Plugins → Available** → search **"SSH"** (Publish Over SSH) → install if not present → restart, wait for login.
## Step 3 — Configure SSH server for App Server 2
**Manage Jenkins → System → Publish over SSH → SSH Servers → Add**:
- **Name**: `stapp02`
- **Hostname**: `stapp02`
- **Username**: `steve`
- **Advanced** → **Use password authentication** → Password: `Am3ric@`
- **Test Configuration** → should succeed. Save.
## Step 4 — Set up passwordless SSH from stapp02 → ststor01
The job will run *on stapp02* and scp to ststor01, so stapp02's steve needs passwordless access to ststor01's natasha. SSH into stapp02 and set it up:
```bash
ssh steve@stapp02
ssh-keygen -t rsa -N '' -f ~/.ssh/id_rsa # if not present
ssh-copy-id natasha@ststor01 # password: Bl@kW
# test:
ssh natasha@ststor01 hostname # should return ststor01, no prompt
```
## Step 5 — Create the job
**New Item** → name `copy-logs`**Freestyle project** → OK.
## Step 6 — Configure the schedule (every 5 min)
Check **Build Triggers → Build periodically** → Schedule:
```
*/5 * * * *
```
## Step 7 — Add the build step
**Build Steps → Send files or execute commands over SSH**:
- **SSH Server**: `stapp02`
- **Exec command**:
```bash
scp /var/log/httpd/access_log natasha@ststor01:/usr/src/itadmin/
scp /var/log/httpd/error_log natasha@ststor01:/usr/src/itadmin/
```
Save.
## Step 8 — Build once
Click **Build Now**. Check **Console Output** for success.
## Verify
On ststor01:
```bash
ls -l /usr/src/itadmin/
# should show access_log and error_log
```
## Task 74
There is a requirement to create a Jenkins job to automate the database backup. Below you can find more details to accomplish this task:
Click on the Jenkins button on the top bar to access the Jenkins UI. Login using username admin and password Adm!n321.
Create a Jenkins job named database-backup.
Configure it to take a database dump of the kodekloud_db01 database present on the App server (stapp01) in Stratos Datacenter, the database user is kodekloud_roy and password is asdfgdsd.
The dump should be named in db_$(date +%F).sql format, where date +%F is the current date.
Copy the db_$(date +%F).sql dump to the Storage server (ststor01) under location /home/natasha/db_backups.
Further, schedule this job to run periodically at */10 * * * * (please use this exact schedule format).
### Schedule
# Jenkins job: database-backup
## Step 1 — Log in
Click **Jenkins** → login `admin` / `Adm!n321`.
## Step 2 — Ensure "Publish over SSH" plugin installed
**Manage Jenkins → Plugins → Available** → search **"SSH"** (Publish Over SSH) → install if needed → restart, wait for login.
## Step 3 — Configure SSH server for App Server 1
**Manage Jenkins → System → Publish over SSH → SSH Servers → Add**:
- **Name**: `stapp01`
- **Hostname**: `stapp01`
- **Username**: `tony`
- **Advanced** → **Use password authentication** → Password: `Ir0nM@n`
- **Test Configuration** → success. Save.
## Step 4 — Passwordless SSH from stapp01 → ststor01
The dump runs on stapp01 and scp's to ststor01, so tony@stapp01 needs passwordless access to natasha@ststor01:
```bash
ssh tony@stapp01
ssh-keygen -t rsa -N '' -f ~/.ssh/id_rsa # if not present
ssh-copy-id natasha@ststor01 # password: Bl@kW
ssh natasha@ststor01 hostname # test — should return ststor01, no prompt
```
## Step 5 — Create the job
**New Item** → name `database-backup`**Freestyle project** → OK.
## Step 6 — Schedule
**Build Triggers → Build periodically** → Schedule:
```
*/10 * * * *
```
## Step 7 — Build step
**Build Steps → Send files or execute commands over SSH**:
- **SSH Server**: `stapp01`
- **Exec command**:
```bash
mysqldump -u kodekloud_roy -pasdfgdsd kodekloud_db01 > /tmp/db_$(date +%F).sql
scp /tmp/db_$(date +%F).sql natasha@ststor01:/home/natasha/db_backups/
```
Save.
## Step 8 — Build once
Click **Build Now** → check **Console Output** for success.
## Verify
On ststor01:
```bash
ls -l /home/natasha/db_backups/
# should show db_YYYY-MM-DD.sql
```
## Task 75
The Nautilus DevOps team has installed and configured new Jenkins server in Stratos DC which they will use for CI/CD and for some automation tasks. There is a requirement to add all app servers as slave nodes in Jenkins so that they can perform tasks on these servers using Jenkins. Find below more details and accomplish the task accordingly.
Click on the Jenkins button on the top bar to access the Jenkins UI. Login using username admin and password Adm!n321.
1. Add all app servers as SSH build agent/slave nodes in Jenkins. Slave node name for app server 1, app server 2 and app server 3 must be App_server_1, App_server_2, App_server_3 respectively.
2. Add labels as below:
App_server_1 : stapp01
App_server_2 : stapp02
App_server_3 : stapp03
3. Remote root directory for App_server_1 must be /home/tony/jenkins, for App_server_2 must be /home/steve/jenkins and for App_server_3 must be /home/banner/jenkins.
### Solution
# Add app servers as Jenkins SSH build agents
## Step 1 — Log in
Click **Jenkins** → login `admin` / `Adm!n321`.
## Step 2 — Ensure "SSH Build Agents" plugin is installed
**Manage Jenkins → Plugins → Available** → search **"SSH Build Agents"** → install if needed → restart, wait for login.
## Step 3 — Prerequisite: Java + remote dirs on each app server
Agents run a Java process, and the remote root dir must exist. On each app server:
```bash
# App Server 1 (tony)
ssh tony@stapp01
# Ir0nM@n
sudo yum install -y java-17-openjdk # or java-17; match Jenkins controller
mkdir -p /home/tony/jenkins
exit
# App Server 2 (steve)
ssh steve@stapp02
# Am3ric@
sudo yum install -y java-17-openjdk
mkdir -p /home/steve/jenkins
exit
# App Server 3 (banner)
ssh banner@stapp03
# BigGr33n
sudo yum install -y java-21-openjdk
mkdir -p /home/banner/jenkins
exit
```
## Step 4 — Add credentials for each app server
**Manage Jenkins → Credentials → System → Global credentials → Add Credentials**:
For each app server, add a **Username with password** credential:
| Server | Username | Password |
|--------|----------|----------|
| stapp01 | tony | Ir0nM@n |
| stapp02 | steve | Am3ric@ |
| stapp03 | banner | BigGr33n |
(Kind: Username with password. Set a recognizable ID like `stapp01-cred`.)
## Step 5 — Create the three nodes
For **App_server_1**:
**Manage Jenkins → Nodes → New Node**:
- **Node name**: `App_server_1`
- Type: **Permanent Agent** → Create
- **Remote root directory**: `/home/tony/jenkins`
- **Labels**: `stapp01`
- **Usage**: Use this node as much as possible
- **Launch method**: **Launch agents via SSH**
- **Host**: `stapp01`
- **Credentials**: select the tony credential
- **Host Key Verification Strategy**: **Non verifying Verification Strategy** (or Manually trusted)
- Save.
Repeat for **App_server_2**:
- Node name: `App_server_2`
- Remote root directory: `/home/steve/jenkins`
- Labels: `stapp02`
- SSH Host: `stapp02`, credentials: steve
Repeat for **App_server_3**:
- Node name: `App_server_3`
- Remote root directory: `/home/banner/jenkins`
- Labels: `stapp03`
- SSH Host: `stapp03`, credentials: banner
## Step 6 — Verify
**Manage Jenkins → Nodes** — all three (App_server_1/2/3) should show **online** (green, no red X). Click each; the agent log should show a successful connection.
## Task 76
The xFusionCorp Industries has recruited some new developers. There are already some existing jobs on Jenkins and two of these new developers need permissions to access those jobs. The development team has already shared those requirements with the DevOps team, so as per details mentioned below grant required permissions to the developers.
Click on the Jenkins button on the top bar to access the Jenkins UI. Login using username admin and password Adm!n321.
There is an existing Jenkins job named Packages, there are also two existing Jenkins users named sam with password sam@pass12345 and rohan with password rohan@pass12345.
Grant permissions to these users to access Packages job as per details mentioned below:
a.) Make sure to select Inherit permissions from parent ACL under inheritance strategy for granting permissions to these users.
b.) Grant mentioned permissions to sam user : build, configure and read.
c.) Grant mentioned permissions to rohan user : build, cancel, configure, read, update and tag.
Note:
Please do not modify/alter any other existing job configuration.
You might need to install some plugins and restart Jenkins service. So, we recommend clicking on Restart Jenkins when installation is complete and no jobs are running on plugin installation/update page i.e update centre. Also Jenkins UI sometimes gets stuck when Jenkins service restarts in the back end. In this case, please make sure to refresh the UI page.
For these kind of scenarios requiring changes to be done in a web UI, please take screenshots so that you can share it with us for review in case your task is marked incomplete. You may also consider using a screen recording software such as loom.com to record and share your work.
### Solution
# Grant sam + rohan permissions on Packages job
## Step 1 — Log in
Click **Jenkins** → login `admin` / `Adm!n321`.
## Step 2 — Confirm global authorization supports project-based matrix
2. (If Matrix Auth plugin missing) install it → "Restart when complete & no jobs running" → refresh UI if stuck → wait for login page.
3. Ensure global Authorization = Project-based Matrix Auth (don't change if already set; ensure admin keeps Administer).
**Manage Jenkins → Security** → Authorization should be **"Project-based Matrix Authorization Strategy"**. (If it's already set from a prior task, leave it — just ensure admin has Overall/Administer so you don't lock out.)
## Step 3 — Open the Packages job's security config
Go to the **Packages** job → **Configure** → find **"Enable project-based security"** and check it.
Set **Inheritance Strategy****"Inherit permissions from parent ACL"**.
## Step 4 — Add sam and grant permissions
Click **Add user** → enter `sam` → check these boxes for sam:
- **Job → Build**
- **Job → Configure**
- **Job → Read**
## Step 5 — Add rohan and grant permissions
Click **Add user** → enter `rohan` → check these for rohan:
- **Job → Build**
- **Job → Cancel** (under Run category, "Cancel")
- **Job → Configure**
- **Job → Read**
- **Job → Update** (SCM/Update — the "Update" permission)
- **Job → Tag** (SCM → Tag)
## Step 6 — Save
Click **Save**.
## Verify
- Log in as `sam` → can see/build/configure Packages, nothing more.
- Log in as `rohan` → can see/build/cancel/configure/update/tag Packages.
## Task 77
The development team of xFusionCorp Industries is working on to develop a new static website and they are planning to deploy the same on Nautilus App Server using Jenkins pipeline. They have shared their requirements with the DevOps team and accordingly we need to create a Jenkins pipeline job. Please find below more details about the task:
Click on the Jenkins button on the top bar to access the Jenkins UI. Login using username admin and password Adm!n321.
Similarly, click on the Gitea button on the top bar to access the Gitea UI. Login using username sarah and password Sarah_pass123. There under user sarah you will find a repository named web_app that is already cloned on App Server 1 under /var/www/html. sarah is a developer who is working on this repository.
Add a slave node named App Server 1. It should be labeled as stapp01 and its remote root directory should be /home/sarah/jenkins_agent (the repository is cloned under /var/www/html; the agent uses a separate directory so it does not pollute the repo).
We have already cloned repository on App Server 1 under /var/www/html.
Apache is already installed on the app server and is running on port 8080.
Create a Jenkins pipeline job named xfusion-webapp-job (it must not be a Multibranch pipeline) and configure it to:
Deploy the code from web_app repository under /var/www/html on App Server 1, as this is the document root of the app server. The pipeline should have a single stage named Deploy ( which is case sensitive ) to accomplish the deployment.
LB server is already configured. You should be able to see the latest changes you made by clicking on the App button. Please make sure the required content is loading on the main URL https://<LBR-URL> i.e there should not be a sub-directory like https://<LBR-URL>/web_app etc.
Note:
You might need to install some plugins and restart Jenkins service. So, we recommend clicking on Restart Jenkins when installation is complete and no jobs are running on plugin installation/update page i.e update centre. Also, Jenkins UI sometimes gets stuck when Jenkins service restarts in the back end. In this case, please make sure to refresh the UI page.
For these kind of scenarios requiring changes to be done in a web UI, please take screenshots so that you can share it with us for review in case your task is marked incomplete. You may also consider using a screen recording software such as loom.com to record and share your work.
### Solution
#### Prerequisities
```bash
ssh sarah@stapp01 # or via the appropriate user
# Sarah_pass123
# Java for the agent (match controller — likely 17)
sudo yum install -y java-17-openjdk
sudo alternatives --set java java-17-openjdk.x86_64 # make it default
java -version
# agent working dir
mkdir -p /home/sarah/jenkins_agent
# confirm the repo is there + git works in docroot
cd /var/www/html
sudo git status # confirm it's a git clone of web_app
```
# Jenkins pipeline: xfusion-webapp-job
## Step 1 — Log in
Jenkins UI → `admin` / `Adm!n321`.
## Step 2 — Install required plugins
Likely needed: **SSH Build Agents**, **Pipeline**, **Git**.
**Manage Jenkins → Plugins → Available** → install any missing → **"Restart Jenkins when installation is complete and no jobs are running"** → refresh UI if it hangs → wait for login page.
## Step 3 — Add credentials for sarah@stapp01
**Manage Jenkins → Credentials → Global → Add Credentials**:
- Kind: Username with password
- Username: `sarah`
- Password: (sarah's app-server password — check the creds table)
- ID: `stapp01-sarah`
## Step 4 — Add the agent node
**Manage Jenkins → Nodes → New Node**:
- **Name**: `App Server 1` (exact — with spaces)
- Type: Permanent Agent
- **Remote root directory**: `/home/sarah/jenkins_agent`
- **Labels**: `stapp01`
- **Launch method**: Launch agents via SSH
- Host: `stapp01`
- Credentials: `stapp01-sarah`
- Host Key Verification Strategy: **Non verifying**
- Save → confirm it comes **online**.
## Step 5 — Create the pipeline job
**New Item** → name `xfusion-webapp-job`**Pipeline** (NOT Multibranch) → OK.
In the job config → **Pipeline** section → Definition: **Pipeline script** → paste:
```groovy
pipeline {
agent { label 'stapp01' }
stages {
stage('Deploy') {
steps {
sh '''
cd /var/www/html
git pull origin master
'''
}
}
}
}
```
Save.
## Step 6 — Build
**Build Now** → check Console Output for success.
## Step 7 — Verify
- App button / `https://<LBR-URL>` shows the web_app content at the **root** (no /web_app subdir).
- On stapp01: `ls /var/www/html` shows the latest repo files.
## Task 78
The development team of xFusionCorp Industries is working on to develop a new static website and they are planning to deploy the same on Nautilus App Server using Jenkins pipeline. They have shared their requirements with the DevOps team and accordingly we need to create a Jenkins pipeline job. Please find below more details about the task:
Click on the Jenkins button on the top bar to access the Jenkins UI. Login using username admin and password Adm!n321.
Similarly, click on the Gitea button on the top bar to access the Gitea UI. Login using username sarah and password Sarah_pass123. There under user sarah you will find a repository named web_app that is already cloned on App Server 1 under /var/www/html. sarah is a developer who is working on this repository.
Add a slave node named App Server 1. It should be labeled as stapp01 and its remote root directory should be /home/sarah/jenkins_agent (the repository is cloned under /var/www/html).
We have already cloned repository on App Server 1 under /var/www/html.
Apache is already installed on the app server and is running on port 8080.
Create a Jenkins pipeline job named nautilus-webapp-job (it must not be a Multibranch pipeline) and configure it to:
Add a string parameter named BRANCH.
It should conditionally deploy the code from web_app repository under /var/www/html on App Server 1, as this is the document root of the app server. The pipeline should have a single stage named Deploy ( which is case sensitive ) to accomplish the deployment.
The pipeline should be conditional, if the value master is passed to the BRANCH parameter then it must deploy the master branch, on the other hand if the value feature is passed to the BRANCH parameter then it must deploy the feature branch.
LB server is already configured. You should be able to see the latest changes you made by clicking on the App button. Please make sure the required content is loading on the main URL https://<LBR-URL> i.e there should not be a sub-directory like https://<LBR-URL>/web_app etc.
Note:
You might need to install some plugins and restart Jenkins service. So, we recommend clicking on Restart Jenkins when installation is complete and no jobs are running on plugin installation/update page i.e update centre. Also, Jenkins UI sometimes gets stuck when Jenkins service restarts in the back end. In this case, please make sure to refresh the UI page.
For these kind of scenarios requiring changes to be done in a web UI, please take screenshots so that you can share it with us for review in case your task is marked incomplete. You may also consider using a screen recording software such as loom.com to record and share your work.
### Solution
#### Prerequisities
```bash
ssh sarah@stapp01 # or via the appropriate user
# Sarah_pass123
# Java for the agent (match controller — likely 17)
sudo yum install -y java-17-openjdk
sudo alternatives --set java java-17-openjdk.x86_64 # make it default
java -version
# agent working dir
mkdir -p /home/sarah/jenkins_agent
# confirm the repo is there + git works in docroot
cd /var/www/html
sudo git status # confirm it's a git clone of web_app
```
## Step 2 — Install required plugins
Likely needed: **SSH Build Agents**, **Pipeline**, **Git**.
**Manage Jenkins → Plugins → Available** → install any missing → **"Restart Jenkins when installation is complete and no jobs are running"** → refresh UI if it hangs → wait for login page.
## Step 3 — Add credentials for sarah@stapp01
**Manage Jenkins → Credentials → Global → Add Credentials**:
- Kind: Username with password
- Username: `sarah`
- Password: (sarah's app-server password — check the creds table)
- ID: `stapp01-sarah`
## Step 4 — Add the agent node
**Manage Jenkins → Nodes → New Node**:
- **Name**: `App Server 1` (exact — with spaces)
- Type: Permanent Agent
- **Remote root directory**: `/home/sarah/jenkins_agent`
- **Labels**: `stapp01`
- **Launch method**: Launch agents via SSH
- Host: `stapp01`
- Credentials: `stapp01-sarah`
- Host Key Verification Strategy: **Non verifying**
- Save → confirm it comes **online**.
# Jenkins pipeline: nautilus-webapp-job (conditional)
## Create the pipeline job
**New Item** → name `nautilus-webapp-job`**Pipeline** (NOT Multibranch) → OK.
## Add the string parameter
Check **"This project is parameterized"** → **Add Parameter → String Parameter**:
- **Name**: `BRANCH`
## Pipeline script
**Pipeline** section → Definition: **Pipeline script** → paste:
```groovy
pipeline {
agent { label 'stapp01' }
stages {
stage('Deploy') {
steps {
script {
if (params.BRANCH == 'master') {
sh '''
cd /var/www/html
git checkout master
git pull origin master
'''
} else if (params.BRANCH == 'feature') {
sh '''
cd /var/www/html
git checkout feature
git pull origin feature
'''
}
}
}
}
}
}
```
Save.
## Build
**Build with Parameters** → set `BRANCH` = `master` (then test again with `feature`) → Build.
## Verify
- App button / `https://<LBR-URL>` shows the deployed branch's content at root.
- On stapp01: `cd /var/www/html && git branch` shows the checked-out branch.
## Task 79
The Nautilus development team had a meeting with the DevOps team where they discussed automating the deployment of one of their apps using Jenkins (the one in Stratos Datacenter). They want to auto deploy the new changes in case any developer pushes to the repository. As per the requirements mentioned below configure the required Jenkins job.
Click on the Jenkins button on the top bar to access the Jenkins UI. Login using username admin and Adm!n321 password.
Similarly, you can access the Gitea UI using Gitea button. Username and password for Git are sarah and Sarah_pass123. Under user sarah you will find a repository named web that is already cloned on App Server 1 under sarah's home (/home/sarah/web). sarah is a developer who is working on this repository.
1. httpd is already installed and configured on the app server (listening on port 8080). Ensure the httpd service is running on App Server 1 (e.g. start it manually if needed). You can make starting/restarting httpd part of your Jenkins job if you prefer.
2. Create a Jenkins job named datacenter-app-deployment and configure it so that if anyone pushes any new change to the origin repository in master branch, the job should auto build and deploy the latest code on App Server 1 under /var/www/html directory.
Before deployment, ensure that the ownership of the /var/www/html directory is set to user sarah, so that Jenkins can successfully deploy files to that directory.
3. SSH into App Server 1 using sarah user credentials mentioned above. Under sarah user's home (/home/sarah/web) you will find a cloned Git repository named web. Under this repository there is an index.html file, update its content to Welcome to the xFusionCorp Industries, then push the changes to the origin into master branch. This push must trigger your Jenkins job and the latest changes must be deployed on the server, also make sure it deploys the entire repository content not only index.html file.
Click on the App button on the top bar to access the app. Please make sure the required content is loading on the main URL (e.g. http://stlb01:8091) i.e there should not be any sub-directory like http://stlb01:8091/web etc.
Note:
1. You might need to install some plugins and restart Jenkins service. So, we recommend clicking on Restart Jenkins when installation is complete and no jobs are running on plugin installation/update page i.e update centre. Also some times Jenkins UI gets stuck when Jenkins service restarts in the back end so in such case please make sure to refresh the UI page.
2. Make sure Jenkins job passes even on repetitive runs as validation may try to build the job multiple times.
3. Deployment related tasks should be done by sudo user on the destination server to avoid any permission issues so make sure to configure your Jenkins job accordingly.
4. For these kind of scenarios requiring changes to be done in a web UI, please take screenshots so that you can share it with us for review in case your task is marked incomplete. You may also consider using a screen recording software such as loom.com to record and share your work.
### Solution
#### Prerequisities
```bash
ssh sarah@stapp01 # or via the appropriate user
# Sarah_pass123
# Java for the agent (match controller — likely 17)
sudo yum install -y java-17-openjdk
sudo alternatives --set java java-17-openjdk.x86_64 # make it default
java -version
# agent working dir
mkdir -p /home/sarah/jenkins_agent
# confirm the repo clone
cd /home/sarah/web && git remote -v && git branch
```
# Jenkins job: datacenter-app-deployment (webhook auto-deploy)
## Step 1 — Plugins
Ensure installed: **Git**, **Gitea** (or Generic Webhook Trigger), **Publish Over SSH** or the agent approach.
Install missing → restart → refresh UI if stuck.
## Step 2 — Node or SSH config for stapp01
Either use an SSH agent node (label stapp01) OR Publish-Over-SSH to stapp01.
(Given note #3 "use sudo on destination," the job runs commands on stapp01 as sarah with sudo.)
## Step 3 — Create the job
**New Item** → `datacenter-app-deployment`**Freestyle project** → OK.
## Step 4 — Source Code Management
**Git**:
- Repository URL: the Gitea `web` repo URL (e.g. `http://<gitea>/sarah/web.git`)
- Credentials: sarah / Sarah_pass123
- Branch: `*/master`
## Step 5 — Build Trigger
Check **"Poll SCM"** as a fallback AND configure the Gitea webhook trigger:
- If Gitea plugin: check **"Build when a change is pushed to Gitea"**
- (Fallback: Poll SCM with `* * * * *` or a webhook)
## Step 6 — Build step
**Execute shell** (if job runs on stapp01 agent) — deploy the repo contents to docroot:
```bash
sudo cp -r $WORKSPACE/* /var/www/html/
sudo systemctl restart httpd
```
Or if pulling directly on the server, sync the /home/sarah/web content.
## Step 7 — Configure Gitea webhook
In Gitea → repo `web`**Settings → Webhooks → Add Webhook → Gitea**:
- Target URL: `http://<jenkins-url>/gitea-webhook/post` (or the Jenkins job's webhook endpoint)
- Trigger: Push events
- Save → Test Delivery.
## Step 8 — Make the change + push (proves the trigger)
On stapp01 as sarah:
```bash
cd /home/sarah/web
echo "Welcome to the xFusionCorp Industries" > index.html
git add index.html
git commit -m "Update index.html"
git push origin master
```
This push should auto-trigger the Jenkins job.
## Verify
- Jenkins job auto-built after the push.
- `http://stlb01:8091` shows "Welcome to the xFusionCorp Industries" at root.
- `/var/www/html` contains the full repo content, not just index.html.
## Tak 80
The DevOps team was looking for a solution where they want to restart Apache service on all app servers if the deployment goes fine on these servers in Stratos Datacenter. After having a discussion, they came up with a solution to use Jenkins chained builds so that they can use a downstream job for services which should only be triggered by the deployment job. So as per the requirements mentioned below configure the required Jenkins jobs.
Click on the Jenkins button on the top bar to access the Jenkins UI. Login using username admin and Adm!n321 password.
Similarly you can access Gitea UI on port 3000 (or click the Gitea button) and username and password for Git is sarah and Sarah_pass123 respectively. Under user sarah you will find a repository named web.
Apache is already installed and configured on the app server. The doc root /var/www/html on App Server 1 is a local git repository tracking the origin web repository.
1. Create a Jenkins job named devops-app-deployment and configure it to pull changes from the master branch of the web repository on App Server 1 under /var/www/html directory.
2. Create another Jenkins job named manage-services and make it a downstream job for devops-app-deployment. Things to take care about this job are:
a. This job should restart httpd service on the app server (App Server 1).
b. Trigger this job only if the upstream job i.e devops-app-deployment is stable.
The LB server is already configured. Click on the App button on the top bar to access the app. Please make sure the required content is loading on the main URL (e.g. http://stlb01:8091) i.e there should not be a sub-directory like http://stlb01:8091/web etc.
Note:
1. You might need to install some plugins and restart Jenkins service. So, we recommend clicking on Restart Jenkins when installation is complete and no jobs are running on plugin installation/update page i.e update centre. Also some times Jenkins UI gets stuck when Jenkins service restarts in the back end so in such case please make sure to refresh the UI page.
2. Make sure Jenkins job passes even on repetitive runs as validation may try to build the job multiple times.
3. Deployment related tasks should be done by sudo user on the destination server to avoid any permission issues so make sure to configure your Jenkins job accordingly.
4. For these kind of scenarios requiring changes to be done in a web UI, please take screenshots so that you can share it with us for review in case your task is marked incomplete. You may also consider using a screen recording software such as loom.com to record and share your work.
### Solution
# Chained Jenkins Builds: devops-app-deployment → manage-services
### Complete setup from an empty Jenkins
---
## PART 0 — Prerequisites on App Server 1 (CLI)
SSH into stapp01 and set up NOPASSWD sudo for sarah (deploy commands need passwordless sudo).
```bash
ssh sarah@stapp01
# password: Sarah_pass123
# NOPASSWD sudo drop-in
echo 'sarah ALL=(ALL) NOPASSWD: ALL' | sudo tee /etc/sudoers.d/sarah
sudo chmod 0440 /etc/sudoers.d/sarah
sudo visudo -c # must say "parsed OK"
sudo -n true && echo "NOPASSWD works" # must print NOPASSWD works
# confirm docroot is a git repo tracking origin web
cd /var/www/html
sudo git remote -v # should show the 'web' origin
sudo git branch # note the branch (master)
# ensure httpd is running
sudo systemctl start httpd
sudo systemctl status httpd --no-pager
exit
```
---
## PART 1 — Install required plugins
Jenkins UI → login `admin` / `Adm!n321`.
**Manage Jenkins → Plugins → Available plugins**, search and install:
- **Publish Over SSH**
Check **"Restart Jenkins when installation is complete and no jobs are running"**.
Wait for the restart; if the UI hangs, **refresh the page**. Wait for the login page, then log back in.
---
## PART 2 — Configure the SSH server (stapp01)
**Manage Jenkins → System** (Configure System) → scroll to **Publish over SSH****SSH Servers → Add**:
| Field | Value |
|-------|-------|
| Name | `stapp01` |
| Hostname | `stapp01` |
| Username | `sarah` |
Click **Advanced** → check **Use password authentication, or use a different key**:
| Field | Value |
|-------|-------|
| Passphrase / Password | `Sarah_pass123` |
Click **Test Configuration** → must return **Success**. **Save**.
---
## PART 3 — Job 1: devops-app-deployment (the deploy)
**New Item** → name `devops-app-deployment`**Freestyle project** → OK.
### Build step
**Build Steps → Add build step → Send files or execute commands over SSH**:
- **SSH Server**: `stapp01`
- **Exec command**:
```bash
cd /var/www/html
sudo git checkout master
sudo git pull origin master
```
**Save.**
---
## PART 4 — Job 2: manage-services (downstream service restart)
**New Item** → name `manage-services`**Freestyle project** → OK.
### Build step
**Build Steps → Add build step → Send files or execute commands over SSH**:
- **SSH Server**: `stapp01`
- **Exec command**:
```bash
sudo systemctl restart httpd
```
**Save.**
---
## PART 5 — Chain the jobs (upstream → downstream, only if stable)
Open **devops-app-deployment → Configure**:
**Post-build Actions → Add post-build action → Build other projects**:
- **Projects to build**: `manage-services`
- Select **"Trigger only if build is stable"**
**Save.**
---
## PART 6 — Run and verify
### Build the upstream job
Open **devops-app-deployment → Build Now**.
### Check the chain
- `devops-app-deployment` completes **stable** (blue/green ball, SUCCESS).
- It automatically triggers **manage-services** (visible in the build's "Downstream" section / console).
- `manage-services` runs and restarts httpd.
### Verify on the servers
```bash
# on stapp01
ssh sarah@stapp01 'cd /var/www/html && git log --oneline -3' # latest master pulled
ssh sarah@stapp01 'systemctl is-active httpd' # active
```
### Verify the app
Click the **App** button → `http://stlb01:8091` must show the site content at the **root** (no `/web` subdirectory).
---
## Key points
- **Chaining is set on the UPSTREAM job** — "Build other projects" post-build action in `devops-app-deployment`, pointing at `manage-services`.
- **"Trigger only if build is stable"** — `manage-services` runs ONLY when the deploy job is SUCCESS. Failed/unstable deploy → no httpd restart. This is the core requirement (restart Apache only if deployment goes fine).
- **Docroot is the git repo** — `/var/www/html` tracks origin `web`, so the deploy is an in-place `sudo git pull origin master`. Files sit at docroot root → served at `/` → no `/web` subdirectory.
- **Both jobs reach stapp01 via Publish Over SSH** — commands in the "Exec command" boxes run ON stapp01 as sarah. NOPASSWD sudo makes `sudo git` / `sudo systemctl` non-interactive.
- **Idempotent (repeatable)** — `git checkout master` (no-op if already there), `git pull` ("already up to date"), `systemctl restart` (repeatable). Repeated builds of either job pass — important since validation builds multiple times.
- **Separate jobs by design** — httpd restart lives in its own downstream job, not baked into the deploy. That separation IS the "chained builds" architecture the task asks for.
---
## Screenshots to capture (for review)
- `devops-app-deployment` config showing the SSH exec step + the post-build "Build other projects → manage-services (only if stable)".
- `manage-services` config showing the httpd restart step.
- A successful build of `devops-app-deployment` showing it triggered `manage-services`.
- The app loading at the root URL.

View File

@@ -0,0 +1,198 @@
## Task 81
The development team of xFusionCorp Industries is working on to develop a new static website and they are planning to deploy the same on Nautilus App Server using Jenkins pipeline. They have shared their requirements with the DevOps team and accordingly we need to create a Jenkins pipeline job. Please find below more details about the task:
Click on the Jenkins button on the top bar to access the Jenkins UI. Login using username admin and password Adm!n321.
Similarly, click on the Gitea button on the top bar to access the Gitea UI. Login using username sarah and password Sarah_pass123.
There is a repository named sarah/web in Gitea that is already cloned on App Server 1 under /var/www/html directory.
Update the content of the file index.html under the same repository to Welcome to xFusionCorp Industries and push the changes to the origin into the master branch.
Apache is already installed on the app server and is running on port 8080.
Add App Server 1 as a Jenkins agent (slave) node: name App Server 1, label stapp01, remote root directory /home/sarah/jenkins_agent, launch via SSH with host stapp01 and credentials for user sarah. Install java-17-openjdk on App Server 1 if needed.
Create a Jenkins pipeline job named deploy-job (it must not be a Multibranch pipeline job) and pipeline should have two stages Deploy and Test ( names are case sensitive ). Configure these stages as per details mentioned below.
a. The Deploy stage should deploy the code from web repository under /var/www/html on App Server 1, as this is the document root of the app server.
b. The pipeline should run on the App Server 1 node (e.g. use label stapp01).
c. The Test stage should just test if the app is working fine and website is accessible. Its up to you how you design this stage to test it out, you can simply add a curl command as well to run a curl against the LBR URL (http://stlb01:8091) to see if the website is working or not. Make sure this stage fails in case the website/app is not working or if the Deploy stage fails.
Click on the App button on the top bar to see the latest changes you deployed. Please make sure the required content is loading on the main URL http://stlb01:8091 i.e there should not be a sub-directory like http://stlb01:8091/web etc.
Note:
You might need to install some plugins and restart Jenkins service. So, we recommend clicking on Restart Jenkins when installation is complete and no jobs are running on plugin installation/update page i.e update centre. Also, Jenkins UI sometimes gets stuck when Jenkins service restarts in the back end. In this case, please make sure to refresh the UI page.
For these kind of scenarios requiring changes to be done in a web UI, please take screenshots so that you can share it with us for review in case your task is marked incomplete. You may also consider using a screen recording software such as loom.com to record and share your work.
### Solution
# Jenkins Pipeline: deploy-job (Deploy + Test stages)
### Complete setup
---
## PART 0 — Prerequisites on App Server 1 (CLI)
```bash
ssh sarah@stapp01
# password: Sarah_pass123
# Java 17 for the Jenkins agent
sudo yum install -y java-17-openjdk
sudo alternatives --set java java-17-openjdk.x86_64
java -version # must show 17
# agent working directory
mkdir -p /home/sarah/jenkins_agent
# NOPASSWD sudo (deploy writes to /var/www/html which may be root-owned)
echo 'sarah ALL=(ALL) NOPASSWD: ALL' | sudo tee /etc/sudoers.d/sarah
sudo chmod 0440 /etc/sudoers.d/sarah
sudo visudo -c
sudo -n true && echo "NOPASSWD works"
# confirm the repo clone + branch
cd /var/www/html
sudo git remote -v
sudo git branch
```
---
## PART 1 — Update index.html and push (as sarah on stapp01)
```bash
cd /var/www/html
# update the file content
echo "Welcome to xFusionCorp Industries" | sudo tee index.html
# configure git identity if needed
sudo git config user.email "sarah@stratos.xfusioncorp.com"
sudo git config user.name "sarah"
# commit and push to master
sudo git add index.html
sudo git commit -m "Update index.html content"
sudo git push origin master
```
If the push asks for credentials, use `sarah` / `Sarah_pass123`.
---
## PART 2 — Install plugins (Jenkins UI)
Jenkins UI → login `admin` / `Adm!n321`.
**Manage Jenkins → Plugins → Available plugins**, install if missing:
- **SSH Build Agents**
- **Pipeline** (usually present)
- **Git** (usually present)
Check **"Restart Jenkins when installation is complete and no jobs are running"** → refresh UI if it hangs → wait for login page.
---
## PART 3 — Add credentials for sarah
**Manage Jenkins → Credentials → System → Global credentials → Add Credentials**:
- Kind: **Username with password**
- Username: `sarah`
- Password: `Sarah_pass123`
- ID: `stapp01-sarah`
- Create.
---
## PART 4 — Add App Server 1 as an agent node
**Manage Jenkins → Nodes → New Node**:
- **Node name**: `App Server 1`
- Type: **Permanent Agent** → Create
Configure:
| Field | Value |
|-------|-------|
| Remote root directory | `/home/sarah/jenkins_agent` |
| Labels | `stapp01` |
| Usage | Use this node as much as possible |
| Launch method | **Launch agents via SSH** |
| Host | `stapp01` |
| Credentials | `stapp01-sarah` (sarah) |
| Host Key Verification Strategy | **Non verifying Verification Strategy** |
**Save** → confirm the node comes **online** (green, no red X). If offline, check the agent log (Java version, credentials, host key).
---
## PART 5 — Create the pipeline job
**New Item** → name `deploy-job`**Pipeline** (NOT Multibranch) → OK.
In the job config → **Pipeline** section → Definition: **Pipeline script** → paste:
```groovy
pipeline {
agent { label 'stapp01' }
stages {
stage('Deploy') {
steps {
sh '''
cd /var/www/html
sudo git checkout master
sudo git pull origin master
'''
}
}
stage('Test') {
steps {
sh '''
curl -f http://stlb01:8091 -o /dev/null
'''
}
}
}
}
```
**Save.**
---
## PART 6 — Build and verify
**Build Now** → check **Console Output**:
- Deploy stage: pulls latest master into `/var/www/html`.
- Test stage: curls the LBR URL and greps for the content.
- Both stages green = SUCCESS.
Verify the app:
- Click **App** button / `http://stlb01:8091` → shows **Welcome to xFusionCorp Industries** at the **root** (no `/web` subdirectory).
---
## Key points
- **Pipeline pinned to `stapp01` node** — `agent { label 'stapp01' }` runs the whole pipeline on App Server 1, so the git pull happens where the docroot is, and the curl runs from a host that can reach the LBR.
- **Two stages, exact case-sensitive names** — `Deploy` and `Test`. Capital D, capital T. The grader checks these literally.
- **Deploy = in-place git pull** — `/var/www/html` is already the repo clone, so `sudo git pull origin master` updates it in place. Files at docroot root → served at `/` → no `/web` subdir.
- **Test stage must FAIL if the site is broken (requirement c)** — this is the critical design point:
- `curl -f` — the `-f` flag makes curl **return a non-zero exit code on HTTP errors** (4xx/5xx). A failed HTTP response fails the shell step → fails the stage.
- `| grep "Welcome to xFusionCorp Industries"` — grep returns non-zero if the expected content isn't found, also failing the stage.
- Combined: if the site is down (curl fails) OR the content is wrong (grep fails), the Test stage fails. Exactly what the task wants.
- If the Deploy stage fails, the pipeline stops before Test anyway (declarative pipelines halt on first stage failure).
- **`sudo` in the sh steps** — `/var/www/html` may be root-owned; NOPASSWD sudo lets `sudo git` run non-interactively on the agent.
- **Plain Pipeline, not Multibranch** — single inline script, not branch-discovery.
- **Idempotent** — `git checkout master` (no-op if already there) + `git pull` ("up to date") + curl test are all repeatable.
---
## Screenshots to capture
- The `deploy-job` pipeline config (the script with Deploy + Test stages).
- The `App Server 1` node showing online.
- A successful build with both stages green (stage view).
- The app loading at `http://stlb01:8091` with the correct content.

View File

@@ -0,0 +1,123 @@
# Assignment
The Nautilus DevOps team is testing Ansible playbooks on various servers within their stack. They've placed some playbooks under /home/thor/playbook/ directory on the jump host and now intend to test them on app server 3 in Stratos DC. However, an inventory file needs creation for Ansible to connect to the respective app. Here are the requirements:
a. Create an ini type Ansible inventory file /home/thor/playbook/inventory on jump host.
b. Include App Server 3 in this inventory along with necessary variables for proper functionality.
c. Ensure the inventory hostname corresponds to the server name as per the wiki, for example stapp01 for app server 1 in Stratos DC.
Note: Validation will execute the playbook using the command ansible-playbook -i inventory playbook.yml. Ensure the playbook functions properly without any extra arguments.
# Solution
# Ansible Inventory — App Server 3 (`stapp03`)
Create an INI-format inventory at `/home/thor/playbook/inventory` so
`ansible-playbook -i inventory playbook.yml` connects to App Server 3 with **no extra arguments**.
> Note: this is an Ansible task, not Kubernetes — there's no manifest to pipe into `kubectl`. The
> heredoc below writes the inventory file instead.
## Create the inventory (heredoc → file)
```bash
cat > /home/thor/playbook/inventory <<'EOF'
[app_servers]
stapp03 ansible_user=banner ansible_ssh_pass=BigGr33n ansible_connection=ssh
EOF
```
## How it works
### Why the hostname must be `stapp03`
Requirement (c) pins the inventory hostname to the **server name from the wiki**`stapp01`,
`stapp02`, `stapp03` for App Servers 13 in Stratos DC. This matters twice over:
1. The playbook's `hosts:` directive refers to hosts (or groups) by these names. Naming the entry
`appserver3` or a raw IP would mean `hosts: stapp03` never matches, and Ansible skips with
"no hosts matched."
2. **`stapp03` is itself resolvable** from the jump host (via `/etc/hosts` / DNS in the lab
network), so the inventory name doubles as the connection target. That's why **no `ansible_host`
is needed** — Ansible resolves `stapp03` directly. Hardcoding an IP would add an assumption that
could be wrong and break a setup that otherwise works.
Confirm resolution before running anything:
```bash
getent hosts stapp03 # or: grep stapp /etc/hosts
```
If that returns an address, you're set. `ansible_host` is only warranted in the rare case where the
name **doesn't** resolve — and then you'd take the IP from that lookup or the wiki.
### The connection variables
Each `key=value` after the hostname is a **host variable** telling Ansible how to authenticate:
| Variable | Purpose |
|----------|---------|
| `ansible_user` | SSH username on App Server 3. |
| `ansible_ssh_pass` | SSH password. Needed because these servers use password auth, not key-based. |
| `ansible_connection` | Transport plugin; `ssh` is the default for remote hosts, stated explicitly for clarity. |
> **Verify the credentials against your lab's wiki.** The values shown (`banner` / `BigGr33n`)
> follow the common Stratos DC pattern, but treat them as placeholders to confirm rather than
> facts — substitute whatever the wiki lists for App Server 3.
### Why the variables live *in* the inventory
The validation runs exactly `ansible-playbook -i inventory playbook.yml` — no `-u`, no `-k`, no
`--private-key`. So every piece of connection information must come from the inventory file itself.
Putting user and password in as host vars is what makes the bare command work; omitting them would
force Ansible to fall back to the current user and key-based auth, and the connection would fail.
### Password auth needs `sshpass`
`ansible_ssh_pass` requires the `sshpass` utility on the control node (the jump host). It's normally
pre-installed in these labs; if Ansible errors with "to use the 'ssh' connection type with
passwords, you must install the sshpass program," install it (`sudo yum install -y sshpass`).
### Host key checking
A first-time SSH connection can fail on host-key verification. The safest fix for the validation is
an `ansible.cfg` beside the playbook, since it applies with no extra command-line arguments:
```bash
cat > /home/thor/playbook/ansible.cfg <<'EOF'
[defaults]
host_key_checking = False
EOF
```
(The equivalent `export ANSIBLE_HOST_KEY_CHECKING=False` works too, but relies on the environment
being set when validation runs.)
## Verify
```bash
cd /home/thor/playbook
# Inventory parses and lists stapp03
ansible-inventory -i inventory --list
# Connectivity check — this settles hostname resolution AND credentials at once
ansible -i inventory stapp03 -m ping
# The actual validation command
ansible-playbook -i inventory playbook.yml
```
Expected — `ansible-inventory` showing `stapp03` with its vars, the ping returning
`"ping": "pong"` with `SUCCESS`, and the playbook running to completion with no failed tasks.
> `UNREACHABLE` on the ping means either the hostname didn't resolve (check `getent hosts stapp03`)
> or the user/password don't match the wiki. "No hosts matched" from the playbook means its `hosts:`
> value doesn't align with `stapp03` or the group name — open `playbook.yml` and match the inventory
> group to it.

View File

@@ -0,0 +1,147 @@
# Assignment
An Ansible playbook needs completion on the jump host, where a team member left off. Below are the details:
The inventory file /home/thor/ansible/inventory requires adjustments. The playbook must run on App Server 3 in Stratos DC. Update the inventory accordingly.
Create a playbook /home/thor/ansible/playbook.yml. Include a task to create an empty file /tmp/file.txt on App Server 3.
Note: Validation will run the playbook using the command ansible-playbook -i inventory playbook.yml. Ensure the playbook works without any additional arguments.
# Solution
# Ansible Inventory + Playbook — create `/tmp/file.txt` on App Server 3
Complete the setup on the jump host so `ansible-playbook -i inventory playbook.yml` runs against
`stapp03` and creates an empty file — with **no extra arguments**.
> Note: this is an Ansible task, not Kubernetes — no manifests to pipe into `kubectl`. The heredocs
> below write the two files.
## Step 0 — See what's already there
The inventory "requires adjustments," so inspect it before overwriting:
```bash
cat /home/thor/ansible/inventory
```
Note any existing hostnames/groups — if the playbook or validation expects a particular group name,
keep it. Otherwise the version below replaces it cleanly.
## Step 1 — Inventory
```bash
cat > /home/thor/ansible/inventory <<'EOF'
[app_servers]
stapp03 ansible_user=banner ansible_ssh_pass=BigGr33n ansible_connection=ssh
EOF
```
## Step 2 — Playbook
```bash
cat > /home/thor/ansible/playbook.yml <<'EOF'
---
- name: Create an empty file on App Server 3
hosts: all
tasks:
- name: Create /tmp/file.txt
ansible.builtin.file:
path: /tmp/file.txt
state: touch
mode: '0644'
EOF
```
## How it works
### The inventory
- **Hostname `stapp03`** — App Server 3's server name per the wiki. It's resolvable from the jump
host, so the inventory name doubles as the connection target; **no `ansible_host` is needed**.
Confirm with `getent hosts stapp03` if you want to be sure.
- **`ansible_user` / `ansible_ssh_pass`** — the SSH credentials. These servers use password auth, so
both are required. **Verify the values against your lab's wiki** — the ones shown follow the
common Stratos DC pattern but should be confirmed, not assumed.
- **`ansible_connection=ssh`** — the transport plugin; the default for remote hosts, stated
explicitly.
Because validation runs the bare command (no `-u`, `-k`, or `--private-key`), **every** connection
detail must live in the inventory file itself.
### The playbook
- **`hosts: all`** — deliberately chosen over `hosts: stapp03`. Since the inventory contains only
App Server 3, `all` targets exactly that host, and it stays correct regardless of what the group is
named. Using a specific name risks a "no hosts matched" skip if the inventory group and the
playbook's `hosts:` value ever drift apart.
- **`ansible.builtin.file` with `state: touch`** — creates the file if absent, leaving it empty.
This is the right module for "create an empty file"; `copy` with empty `content` would also work
but `file`/`touch` expresses the intent directly.
- **`mode: '0644'`** — standard read/write-owner, read-others permissions. Quoted so YAML treats it
as a string, not an octal-looking integer (a classic gotcha: unquoted `0644` can be misparsed).
- **No `become`** — `/tmp` is world-writable, so the SSH user can create the file without
privilege escalation. Adding `become: yes` would invite a sudo-password prompt and could break the
no-extra-arguments requirement.
### A note on idempotency
`state: touch` updates the file's timestamps on every run, so Ansible reports **changed** each
time rather than **ok**. That's fine for this task. If you want true idempotency:
```yaml
- name: Create /tmp/file.txt
ansible.builtin.file:
path: /tmp/file.txt
state: touch
mode: '0644'
modification_time: preserve
access_time: preserve
```
This leaves timestamps alone when the file already exists, so re-runs report `ok`.
### Host key checking
A first-time SSH connection can fail on host-key verification. The safest fix — because it needs no
extra command-line arguments during validation — is an `ansible.cfg` beside the playbook:
```bash
cat > /home/thor/ansible/ansible.cfg <<'EOF'
[defaults]
host_key_checking = False
EOF
```
`sshpass` must also be present on the jump host for `ansible_ssh_pass` to work. It's usually
pre-installed; if not, Ansible's error names it explicitly.
## Verify
```bash
cd /home/thor/ansible
# Inventory parses
ansible-inventory -i inventory --list
# Connectivity + credentials in one shot
ansible -i inventory all -m ping
# The actual validation command
ansible-playbook -i inventory playbook.yml
# Confirm the file exists on the target
ansible -i inventory all -m command -a "ls -l /tmp/file.txt"
```
Expected — ping returns `"ping": "pong"` with `SUCCESS`, the playbook completes with
`ok=2 changed=1` and no failures, and the final check lists `/tmp/file.txt` on stapp03.
> `UNREACHABLE` means the hostname didn't resolve or the credentials don't match the wiki. A
> `PLAY RECAP` showing `skipped` or "no hosts matched" means the playbook's `hosts:` value doesn't
> align with the inventory — `hosts: all` avoids that class of failure.

View File

@@ -0,0 +1,161 @@
# Assignment
The Nautilus DevOps team needs to copy data from the jump host to all application servers in Stratos DC using Ansible. Execute the task with the following details:
a. Create an inventory file /home/thor/ansible/inventory on jump_host and add all application servers as managed nodes.
b. Create a playbook /home/thor/ansible/playbook.yml on the jump host to copy the /usr/src/dba/index.html file to all application servers, placing it at /opt/dba.
Note: Validation will run the playbook using the command ansible-playbook -i inventory playbook.yml. Ensure the playbook functions properly without any extra arguments.
# Solution
# Ansible Inventory + Playbook — copy `index.html` to all App Servers
Set up the jump host so `ansible-playbook -i inventory playbook.yml` copies
`/usr/src/dba/index.html` to `/opt/dba` on **all three** Stratos DC application servers — with **no
extra arguments**.
> Note: this is an Ansible task, not Kubernetes — no manifests to pipe into `kubectl`. The heredocs
> below write the two files.
## Step 1 — Inventory (all three app servers)
```bash
cat > /home/thor/ansible/inventory <<'EOF'
[app_servers]
stapp01 ansible_user=tony ansible_ssh_pass=Ir0nM@n ansible_become_pass=Ir0nM@n
stapp02 ansible_user=steve ansible_ssh_pass=Am3ric@ ansible_become_pass=Am3ric@
stapp03 ansible_user=banner ansible_ssh_pass=BigGr33n ansible_become_pass=BigGr33n
[app_servers:vars]
ansible_connection=ssh
EOF
```
## Step 2 — Playbook
```bash
cat > /home/thor/ansible/playbook.yml <<'EOF'
---
- name: Copy index.html to all application servers
hosts: all
become: yes
tasks:
- name: Ensure /opt/dba exists
ansible.builtin.file:
path: /opt/dba
state: directory
mode: '0755'
- name: Copy index.html to /opt/dba
ansible.builtin.copy:
src: /usr/src/dba/index.html
dest: /opt/dba/index.html
mode: '0644'
EOF
```
## How it works
### The inventory — all three managed nodes
Each app server in Stratos DC gets its own line under the `[app_servers]` group, keyed by the
**server name from the wiki** (`stapp01`, `stapp02`, `stapp03`). Those names resolve from the jump
host, so the inventory name doubles as the connection target — **no `ansible_host` needed**. Confirm
with `getent hosts stapp01` if you want certainty.
| Variable | Purpose |
|----------|---------|
| `ansible_user` | SSH username, different per server. |
| `ansible_ssh_pass` | SSH password (these servers use password auth, not keys). |
| `ansible_become_pass` | The **sudo** password, needed because the playbook uses `become: yes`. |
| `ansible_connection` | Transport plugin — set once for the whole group via `[app_servers:vars]`. |
> **Verify every credential against your lab's wiki.** The user/password pairs shown follow the
> common Stratos DC pattern, but treat them as values to confirm rather than facts.
The `[app_servers:vars]` block sets `ansible_connection` once for all members instead of repeating
it on every line — the same result, less duplication.
Because validation runs the bare command (no `-u`, `-k`, `-K`, or `--private-key`), **all**
connection *and* escalation details must live in the inventory.
### Why `become: yes` is required here
`/opt/dba` is root-owned. The SSH users (`tony`, `steve`, `banner`) can't write there directly, so
the play escalates to root with `become: yes`. That in turn means sudo may demand a password —
which is why `ansible_become_pass` is set per host in the inventory. Without it, the run would hang
or fail with "Missing sudo password," and you can't pass `-K` because no extra arguments are
allowed.
> If your lab's app-server users have **passwordless** sudo, `ansible_become_pass` is harmless and
> simply unused. Including it covers both cases.
### The two tasks
1. **`ansible.builtin.file` with `state: directory`** — guarantees `/opt/dba` exists before the
copy. The `copy` module won't create missing parent directories, so if `/opt/dba` weren't already
present the copy would fail with "Destination directory does not exist." This task makes the
playbook robust either way, and it's idempotent (reports `ok` when the directory already exists).
2. **`ansible.builtin.copy`** — transfers the file:
- **`src: /usr/src/dba/index.html`** — a path on the **control node** (the jump host). The `copy`
module reads from the controller by default; that's exactly what "copy data from the jump host"
means here. (Copying between two paths on the *remote* machine would instead need
`remote_src: yes` — not the case here.)
- **`dest: /opt/dba/index.html`** — the explicit target path. Writing the filename out is clearer
than relying on `dest: /opt/dba/` directory-expansion behavior.
- **`mode: '0644'`** — quoted so YAML treats it as a string; unquoted octal like `0644` is a
classic misparse.
### Why `hosts: all`
The inventory contains only the three app servers, so `all` targets exactly them — and it can't
break if the group name and the playbook's `hosts:` value ever drift apart. Using
`hosts: app_servers` also works given this inventory; `all` is simply the more failure-proof
choice.
### Host key checking
First-time SSH connections can fail on host-key verification. The safest fix — no extra
command-line arguments needed during validation — is an `ansible.cfg` beside the playbook:
```bash
cat > /home/thor/ansible/ansible.cfg <<'EOF'
[defaults]
host_key_checking = False
EOF
```
`sshpass` must also be installed on the jump host for password auth; it usually is, and Ansible's
error names it explicitly if not.
## Verify
```bash
cd /home/thor/ansible
# All three hosts listed
ansible-inventory -i inventory --list
# Connectivity + credentials across every server at once
ansible -i inventory all -m ping
# The actual validation command
ansible-playbook -i inventory playbook.yml
# Confirm the file landed on all three
ansible -i inventory all -b -m command -a "ls -l /opt/dba/index.html"
```
Expected — `ping` returning `SUCCESS`/`pong` for stapp01, stapp02, and stapp03; the playbook
finishing with `failed=0` for all three hosts; and the final check listing `/opt/dba/index.html` on
each server.
> `UNREACHABLE` means a hostname didn't resolve or credentials don't match the wiki. "Missing sudo
> password" means `ansible_become_pass` is absent or wrong for that host. Permission denied on the
> copy means `become: yes` didn't take effect.

View File

@@ -0,0 +1,179 @@
# Assignment
The Nautilus DevOps team is testing various Ansible modules on servers in Stratos DC. They're currently focusing on file creation on remote hosts using Ansible. Here are the details:
a. Create an inventory file ~/playbook/inventory on jump host and include all app servers.
b. Create a playbook ~/playbook/playbook.yml to create a blank file /opt/app.txt on all app servers.
c. Set the permissions of the /opt/app.txt file to 0755.
d. Ensure the user/group owner of the /opt/app.txt file is tony on app server 1, steve on app server 2 and banner on app server 3.
Note: Validation will execute the playbook using the command ansible-playbook -i inventory playbook.yml, so ensure the playbook functions correctly without any additional arguments.
# Solution
# Ansible Inventory + Playbook — create `/opt/app.txt` with per-host ownership
Set up the jump host so `ansible-playbook -i inventory playbook.yml` creates a blank
`/opt/app.txt` on all three Stratos DC app servers, mode `0755`, owned by a **different user on
each host** — with **no extra arguments**.
> Note: this is an Ansible task, not Kubernetes — no manifests to pipe into `kubectl`. The heredocs
> below write the two files.
## Step 1 — Inventory (all three app servers)
```bash
cat > ~/playbook/inventory <<'EOF'
[app_servers]
stapp01 ansible_user=tony ansible_ssh_pass=Ir0nM@n ansible_become_pass=Ir0nM@n
stapp02 ansible_user=steve ansible_ssh_pass=Am3ric@ ansible_become_pass=Am3ric@
stapp03 ansible_user=banner ansible_ssh_pass=BigGr33n ansible_become_pass=BigGr33n
[app_servers:vars]
ansible_connection=ssh
EOF
```
## Step 2 — Playbook
```bash
cat > ~/playbook/playbook.yml <<'EOF'
---
- name: Create /opt/app.txt on all app servers
hosts: all
become: yes
tasks:
- name: Create blank file with correct mode and ownership
ansible.builtin.file:
path: /opt/app.txt
state: touch
mode: '0755'
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
EOF
```
## How it works
### The neat trick: `{{ ansible_user }}` for per-host ownership
Requirement (d) wants a **different owner per server**`tony` on stapp01, `steve` on stapp02,
`banner` on stapp03. Notice those are exactly the SSH users already defined per host in the
inventory. So instead of writing three separate tasks with `when:` conditionals, the playbook
references the variable:
```yaml
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
```
Ansible evaluates `ansible_user` **per host** during the play, so it resolves to `tony` on stapp01,
`steve` on stapp02, and `banner` on stapp03 automatically. One task, three correct outcomes — and it
stays correct if a host is added or a username changes, since the inventory is the single source of
truth.
> `group` uses the same value because these systems create a matching primary group for each user
> (user `tony` → group `tony`), which is the Linux default. If your lab's groups differ, set `group`
> explicitly per host instead.
The alternative — hardcoding with conditionals — would look like this and is strictly worse:
```yaml
# NOT recommended, shown for contrast
- name: Set owner on stapp01
ansible.builtin.file:
path: /opt/app.txt
owner: tony
when: inventory_hostname == "stapp01"
# ...repeated for each host
```
### The inventory
Each app server is keyed by its **wiki server name** (`stapp01`, `stapp02`, `stapp03`), which
resolves from the jump host — so the inventory name doubles as the connection target and **no
`ansible_host` is needed**. Confirm with `getent hosts stapp01` if you want certainty.
| Variable | Purpose |
|----------|---------|
| `ansible_user` | SSH username — **and** the file owner, via the templating above. |
| `ansible_ssh_pass` | SSH password (password auth, not keys). |
| `ansible_become_pass` | Sudo password, needed because the play uses `become: yes`. |
| `ansible_connection` | Transport plugin, set once for the group via `[app_servers:vars]`. |
> **Verify every credential against your lab's wiki.** The pairs shown follow the common Stratos DC
> pattern but should be confirmed rather than assumed.
Since validation runs the bare command (no `-u`, `-k`, `-K`), **all** connection and escalation
details must live in the inventory.
### Why `become: yes` is required
`/opt` is root-owned, so the SSH users can't create a file there directly — the play escalates with
`become: yes`. Escalation is also what makes **`owner:`/`group:` work at all**: changing a file's
ownership requires root (`chown` is privileged). Without `become`, you'd get permission errors on
both the create and the chown.
Because sudo may prompt for a password and you can't pass `-K`, `ansible_become_pass` is set per
host in the inventory. (If the lab has passwordless sudo, the variable is simply unused — harmless
either way.)
### The `file` module options
- **`state: touch`** — creates the file if absent, leaving it blank. Exactly "create a blank file."
- **`mode: '0755'`** — quoted so YAML reads it as a string; unquoted octal like `0755` is a classic
misparse that silently produces the wrong permissions.
- **`owner` / `group`** — applied by the module after creation, which is why root privileges are
needed.
Note `state: touch` bumps timestamps on every run, so re-runs report **changed** rather than **ok**.
That's fine here; add `modification_time: preserve` and `access_time: preserve` if you want strict
idempotency.
### Host key checking
First-time SSH connections can fail on host-key verification. The safest fix — needing no extra
command-line arguments — is an `ansible.cfg` beside the playbook:
```bash
cat > ~/playbook/ansible.cfg <<'EOF'
[defaults]
host_key_checking = False
EOF
```
`sshpass` must also be installed on the jump host for password auth.
## Verify
```bash
cd ~/playbook
# All three hosts listed
ansible-inventory -i inventory --list
# Connectivity + credentials across every server
ansible -i inventory all -m ping
# The actual validation command
ansible-playbook -i inventory playbook.yml
# Confirm mode and per-host ownership
ansible -i inventory all -b -m command -a "ls -l /opt/app.txt"
```
Expected — `ping` returning `SUCCESS` for all three; the playbook finishing with `failed=0`; and the
final listing showing `-rwxr-xr-x` with owner/group `tony tony` on stapp01, `steve steve` on
stapp02, and `banner banner` on stapp03.
> "Missing sudo password" ⇒ `ansible_become_pass` absent or wrong. A chown failure ⇒ `become: yes`
> didn't take. Wrong owner ⇒ check that `ansible_user` in the inventory matches the required owner
> for that host.

View File

@@ -0,0 +1,158 @@
# Assignment
The Nautilus DevOps team is planning to test several Ansible playbooks on different app servers in Stratos DC. Before that, some pre-requisites must be met. Essentially, the team needs to set up a password-less SSH connection between Ansible controller and Ansible managed nodes. One of the tickets is assigned to you; please complete the task as per details mentioned below:
a. Jump host is our Ansible controller, and we are going to run Ansible playbooks through thor user from jump host.
b. There is an inventory file /home/thor/ansible/inventory on jump host. Using that inventory file test Ansible ping from jump host to App Server 2, make sure ping works.
# Solution
# Passwordless SSH — jump host (`thor`) → App Server 2 (`stapp02`)
Set up key-based SSH from the Ansible controller to App Server 2, then confirm with an Ansible ping.
> Note: this is an Ansible/SSH task, not Kubernetes — no manifests to pipe into `kubectl`. Heredocs
> are used for the files; the key-copy step is interactive by design.
## Step 1 — Generate an SSH key pair for `thor` (if none exists)
```bash
# Check first — don't overwrite an existing key
ls -l ~/.ssh/id_rsa.pub 2>/dev/null || ssh-keygen -t rsa -b 4096 -N "" -f ~/.ssh/id_rsa
```
- `-N ""` — empty passphrase, so SSH never prompts (that's the whole point of *password-less*).
- `-f ~/.ssh/id_rsa` — the default location Ansible/SSH look in, so no extra config is needed.
## Step 2 — Copy the public key to App Server 2
```bash
ssh-copy-id steve@stapp02
```
This prompts for `steve`'s password **once** — that's expected and unavoidable; it's the one-time
bootstrap that establishes trust. Accept the host-key fingerprint if asked (`yes`).
> Verify the username for App Server 2 against your lab's wiki. `steve` follows the common Stratos
> DC pattern, but confirm rather than assume.
Confirm it worked — this should log in with **no password prompt**:
```bash
ssh steve@stapp02 hostname
exit
```
## Step 3 — Inventory
The inventory already exists and may contain password variables. With key-based auth in place,
those are no longer needed:
```bash
# Inspect what's there first
cat /home/thor/ansible/inventory
```
```bash
cat > /home/thor/ansible/inventory <<'EOF'
[app_servers]
stapp02 ansible_user=steve ansible_connection=ssh
EOF
```
## Step 4 — Test the Ansible ping
```bash
cd /home/thor/ansible
ansible -i inventory stapp02 -m ping
```
Expected:
```
stapp02 | SUCCESS => {
"ansible_facts": {...},
"changed": false,
"ping": "pong"
}
```
## How it works
### What "password-less" actually means
SSH supports several auth methods. Password auth requires a secret typed (or supplied via
`sshpass`/`ansible_ssh_pass`) on **every** connection. **Public-key auth** instead proves identity
cryptographically:
1. `ssh-keygen` creates a key pair on the controller — a **private** key (`~/.ssh/id_rsa`, stays on
the jump host, never shared) and a **public** key (`~/.ssh/id_rsa.pub`).
2. `ssh-copy-id` appends that public key to `~/.ssh/authorized_keys` on the managed node.
3. On each later connection, the server challenges the client to prove it holds the matching private
key. No password crosses the wire, and nothing needs to be typed.
That one-time password prompt in Step 2 exists because you must authenticate *somehow* to install
the key. After that, it's never needed again.
### Why the inventory drops `ansible_ssh_pass`
With the key installed, Ansible connects over SSH using `thor`'s private key automatically — it's at
the default path, so no `ansible_ssh_private_key_file` is required either. Leaving a stale
`ansible_ssh_pass` in the inventory isn't fatal, but removing it is the point of the exercise:
authentication is now key-based, and the inventory should reflect that.
What remains:
| Variable | Purpose |
|----------|---------|
| `ansible_user=steve` | **Still required** — it tells Ansible which remote account to log in as. Key auth proves *who you are*, not *whom you connect as*. |
| `ansible_connection=ssh` | Transport plugin; the default for remote hosts, stated explicitly. |
`stapp02` resolves from the jump host, so the inventory name doubles as the connection target — no
`ansible_host` needed. Confirm with `getent hosts stapp02` if unsure.
### Why the `ping` module is the right test
`ansible -m ping` isn't ICMP — it opens a real SSH connection, runs a trivial Python module on the
target, and returns `pong`. So a `SUCCESS` result proves the **entire chain** works: hostname
resolution, SSH key authentication, the remote user, and Python on the managed node. That's exactly
what requirement (b) asks you to demonstrate.
### Host key checking
The first connection prompts to accept the host's fingerprint. Doing it manually in Step 2 (via
`ssh-copy-id`/`ssh`) gets it into `~/.ssh/known_hosts` before Ansible runs, which is the cleanest
approach. If Ansible still trips on it, add an `ansible.cfg` beside the inventory:
```bash
cat > /home/thor/ansible/ansible.cfg <<'EOF'
[defaults]
host_key_checking = False
EOF
```
## Verify
```bash
# Key exists on the controller
ls -l ~/.ssh/id_rsa ~/.ssh/id_rsa.pub
# Key installed on the target (should list thor's key)
ssh steve@stapp02 "cat ~/.ssh/authorized_keys"
# Passwordless login works
ssh steve@stapp02 hostname
# The required check
cd /home/thor/ansible && ansible -i inventory stapp02 -m ping
```
Expected — both key files present, `thor@jump_host` visible in the target's `authorized_keys`, SSH
logging in without a prompt, and the ping returning `SUCCESS` with `"ping": "pong"`.
> Still prompted for a password? Permissions are the usual culprit: on the **target**,
> `~/.ssh` must be `700` and `~/.ssh/authorized_keys` `600` — SSH silently refuses keys on
> loosely-permissioned files. On the **controller**, `~/.ssh/id_rsa` must be `600`.

View File

@@ -0,0 +1,163 @@
# Assignment
The Nautilus Application development team wanted to test some applications on app servers in Stratos Datacenter. They shared some pre-requisites with the DevOps team, and packages need to be installed on app servers. Since we are already using Ansible for automating such tasks, please perform this task using Ansible as per details mentioned below:
Create an inventory file /home/thor/playbook/inventory on jump host and add all app servers in it.
Create an Ansible playbook /home/thor/playbook/playbook.yml to install samba package on all app servers using Ansible yum module.
Make sure user thor should be able to run the playbook on jump host.
Note: Validation will try to run playbook using command ansible-playbook -i inventory playbook.yml so please make sure playbook works this way, without passing any extra arguments.ß
# Solution
# Ansible Inventory + Playbook — install `samba` on all App Servers
Set up the jump host so `ansible-playbook -i inventory playbook.yml` installs the `samba` package on
all three Stratos DC app servers — with **no extra arguments**.
> Note: this is an Ansible task, not Kubernetes — no manifests to pipe into `kubectl`. The heredocs
> below write the two files.
## Step 1 — Inventory (all three app servers)
```bash
cat > /home/thor/playbook/inventory <<'EOF'
[app_servers]
stapp01 ansible_user=tony ansible_ssh_pass=Ir0nM@n ansible_become_pass=Ir0nM@n
stapp02 ansible_user=steve ansible_ssh_pass=Am3ric@ ansible_become_pass=Am3ric@
stapp03 ansible_user=banner ansible_ssh_pass=BigGr33n ansible_become_pass=BigGr33n
[app_servers:vars]
ansible_connection=ssh
EOF
```
## Step 2 — Playbook
```bash
cat > /home/thor/playbook/playbook.yml <<'EOF'
---
- name: Install samba on all app servers
hosts: all
become: yes
tasks:
- name: Install samba package
ansible.builtin.yum:
name: samba
state: present
EOF
```
## Step 3 — Ensure `thor` can run it
```bash
# thor owns the playbook directory and its contents
sudo chown -R thor:thor /home/thor/playbook
chmod 644 /home/thor/playbook/inventory /home/thor/playbook/playbook.yml
```
If you created both files as `thor` (as the heredocs above do), ownership is already correct and
this step is a no-op safety check.
## How it works
### The inventory
Each app server is keyed by its **wiki server name** (`stapp01`, `stapp02`, `stapp03`), which
resolves from the jump host — so the inventory name doubles as the connection target and **no
`ansible_host` is needed**. Verify with `getent hosts stapp01` if unsure.
| Variable | Purpose |
|----------|---------|
| `ansible_user` | SSH username, different per server. |
| `ansible_ssh_pass` | SSH password (these servers use password auth). |
| `ansible_become_pass` | **Sudo** password — required because installing packages needs root. |
| `ansible_connection` | Transport plugin, set once for the group via `[app_servers:vars]`. |
> **Verify every credential against your lab's wiki.** The pairs shown follow the common Stratos DC
> pattern but should be confirmed rather than assumed.
Since validation runs the bare command (no `-u`, `-k`, `-K`), **all** connection and escalation
details must live in the inventory file.
### Why `become: yes` is mandatory here
Installing a package writes to system directories and the RPM database — strictly root-only
operations. The SSH users (`tony`, `steve`, `banner`) are unprivileged, so the play escalates with
`become: yes`. Without it, the task fails with a permissions error from yum.
Because sudo may prompt for a password and you **can't** pass `-K` (no extra arguments allowed),
`ansible_become_pass` is set per host in the inventory. That's the single most common failure point
on this task — "Missing sudo password" with no way to supply it at runtime.
> If the lab's app-server users have passwordless sudo, the variable is simply unused — harmless
> either way.
### The `yum` module
- **`name: samba`** — the package to install.
- **`state: present`** — ensures the package is installed, and does nothing if it already is. This
makes the task **idempotent**: the first run reports `changed`, subsequent runs report `ok`.
(`state: latest` would instead upgrade on every run, which isn't what "install" asks for.)
- **`ansible.builtin.yum`** — the task explicitly requires the yum module. These app servers are
RHEL/CentOS-family, so yum is correct. On modern Fedora/RHEL 8+ the `dnf` module is the successor,
and Ansible's `yum` module transparently delegates to dnf where appropriate — so `yum` works here
regardless.
### Why `hosts: all`
The inventory contains only the three app servers, so `all` targets exactly them — and it can't
break if the group name and the playbook's `hosts:` value drift apart. `hosts: app_servers` also
works given this inventory; `all` is just the more failure-proof choice.
### Requirement 3 — "thor should be able to run the playbook"
The validation runs as `thor`, so `thor` must be able to **read** both files. Creating them with the
heredocs above (as `thor`) satisfies this automatically. The `chown`/`chmod` in Step 3 is a
belt-and-braces check in case the directory was pre-created by another user — a root-owned
`playbook.yml` that `thor` can't read would fail validation before Ansible even starts.
### Host key checking
First-time SSH connections can fail on host-key verification. The safest fix — needing no extra
command-line arguments — is an `ansible.cfg` beside the playbook:
```bash
cat > /home/thor/playbook/ansible.cfg <<'EOF'
[defaults]
host_key_checking = False
EOF
```
`sshpass` must also be installed on the jump host for password auth.
## Verify
```bash
cd /home/thor/playbook
# All three hosts listed
ansible-inventory -i inventory --list
# Connectivity + credentials across every server
ansible -i inventory all -m ping
# The actual validation command
ansible-playbook -i inventory playbook.yml
# Confirm samba is installed on all three
ansible -i inventory all -b -m command -a "rpm -q samba"
```
Expected — `ping` returning `SUCCESS` for all three; the playbook finishing with `failed=0`; and
`rpm -q samba` printing an installed version (e.g. `samba-4.x.x-...`) on each server rather than
"package samba is not installed."
> "Missing sudo password" ⇒ `ansible_become_pass` absent or wrong. A yum permissions error ⇒
> `become: yes` didn't take effect. `UNREACHABLE` ⇒ hostname resolution or credentials.

View File

@@ -0,0 +1,172 @@
# Assignment
The Nautilus DevOps team wants to install and set up a simple httpd web server on all app servers in Stratos DC. Additionally, they want to deploy a sample web page for now using Ansible only. Therefore, write the required playbook to complete this task. Find more details about the task below.
We already have an inventory file under /home/thor/ansible directory on jump host. Create a playbook.yml under /home/thor/ansible directory on jump host itself.
Using the playbook, install httpd web server on all app servers. Additionally, make sure its service should up and running.
Using blockinfile Ansible module add some content in /var/www/html/index.html file. Below is the content:
Welcome to XfusionCorp!
This is Nautilus sample file, created using Ansible!
Please do not modify this file manually!
The /var/www/html/index.html file's user and group owner should be apache on all app servers.
The /var/www/html/index.html file's permissions should be 0777 on all app servers.
Note:
i. Validation will try to run the playbook using command ansible-playbook -i inventory playbook.yml so please make sure the playbook works this way without passing any extra arguments.
ii. Do not use any custom or empty marker for blockinfile module.
# Solution
# Ansible Playbook — install httpd + deploy sample page on all App Servers
Create `/home/thor/ansible/playbook.yml` so `ansible-playbook -i inventory playbook.yml` installs
httpd, starts it, and writes a sample `index.html` with specific ownership and permissions — with
**no extra arguments**.
> Note: this is an Ansible task, not Kubernetes — no manifests to pipe into `kubectl`. The heredoc
> below writes the playbook.
## Step 0 — The inventory already exists; leave it alone
The task states an inventory is already present. **Do not recreate or overwrite it** — just confirm
what's in it:
```bash
cat /home/thor/ansible/inventory
```
Check that it lists the app servers with their connection variables. If it lacks
`ansible_become_pass` and the app-server users require a sudo password, add that per host — the
playbook needs privilege escalation and you can't pass `-K` at runtime.
## Step 1 — Playbook
```bash
cat > /home/thor/ansible/playbook.yml <<'EOF'
---
- name: Install httpd and deploy sample web page
hosts: all
become: yes
tasks:
- name: Install httpd package
ansible.builtin.yum:
name: httpd
state: present
- name: Start and enable httpd service
ansible.builtin.service:
name: httpd
state: started
enabled: yes
- name: Add sample content to index.html
ansible.builtin.blockinfile:
path: /var/www/html/index.html
create: yes
block: |
Welcome to XfusionCorp!
This is Nautilus sample file, created using Ansible!
Please do not modify this file manually!
owner: apache
group: apache
mode: '0777'
EOF
```
## How it works
### Why `become: yes`
Every task here is privileged: installing a package writes to system directories and the RPM
database, managing a systemd service requires root, and `/var/www/html/` is root-owned. The SSH
users are unprivileged, so the whole play escalates with `become: yes` at the play level rather than
repeating it per task.
Since validation runs the bare command with no `-K`, the sudo password must come from
`ansible_become_pass` in the **existing** inventory. If the play fails with "Missing sudo password,"
that variable is what's missing.
### Task 1 — install httpd
`ansible.builtin.yum` with **`state: present`** installs the package if absent and does nothing
otherwise, making it idempotent (`changed` on first run, `ok` after). `state: latest` would upgrade
on every run, which isn't what's asked.
### Task 2 — service up and running
`ansible.builtin.service` handles both halves of the requirement:
- **`state: started`** — ensures httpd is running **right now**.
- **`enabled: yes`** — ensures it starts automatically **on boot**.
These are independent settings; "up and running" is satisfied by `started`, and `enabled` makes it
durable across reboots. Including both is the standard, complete answer.
### Task 3 — `blockinfile` with default markers
`blockinfile` inserts (or updates) a **block** of text delimited by marker comments, so the block can
be managed idempotently on re-runs — Ansible finds its own markers and replaces the content between
them rather than appending duplicates.
- **No `marker:` parameter is specified**, per note (ii). Omitting it uses the default,
`# {mark} ANSIBLE MANAGED BLOCK`, which produces `# BEGIN ANSIBLE MANAGED BLOCK` and
`# END ANSIBLE MANAGED BLOCK` around your text. Supplying a custom or empty marker would violate
the requirement — and an empty marker would break idempotency, since Ansible could no longer locate
its block.
- **`create: yes`** — creates `/var/www/html/index.html` if it doesn't exist. A fresh httpd install
typically has no `index.html`, and without this flag `blockinfile` would fail with "Destination
does not exist."
- **`block: |`** — the literal block scalar preserves the text **exactly**, including line breaks.
Note the double space in `This is Nautilus sample file` — that's reproduced verbatim from the
requirement, since validation may compare the content character for character.
- **`owner: apache` / `group: apache`** — the required ownership. `apache` is the user httpd runs as
on RHEL/CentOS-family systems, and the account exists only **after** the httpd package is
installed — which is why the install task must come first. Reordering would fail with "chown
failed: failed to look up user apache."
- **`mode: '0777'`** — quoted so YAML parses it as a string. Unquoted octal (`0777`) is a classic
misparse that silently yields the wrong permissions.
### Ordering matters
The three tasks form a dependency chain: **install** creates the `apache` user and
`/var/www/html/`; **service** brings httpd up; **blockinfile** then writes into a directory that
exists and assigns ownership to a user that exists. Ansible runs tasks top to bottom, so this order
is the correctness requirement, not just style.
### `hosts: all`
The inventory holds the app servers, so `all` targets exactly them and can't break if the group name
and the playbook's `hosts:` value drift apart.
## Verify
```bash
cd /home/thor/ansible
# Connectivity first
ansible -i inventory all -m ping
# The actual validation command
ansible-playbook -i inventory playbook.yml
# httpd installed and running
ansible -i inventory all -b -m command -a "systemctl is-active httpd"
# File content, ownership, and permissions
ansible -i inventory all -b -m command -a "ls -l /var/www/html/index.html"
ansible -i inventory all -b -m command -a "cat /var/www/html/index.html"
```
Expected — the playbook finishing with `failed=0`; `systemctl is-active httpd` returning `active`
on each server; `ls -l` showing `-rwxrwxrwx` with `apache apache`; and `cat` showing the three lines
wrapped in `# BEGIN ANSIBLE MANAGED BLOCK` / `# END ANSIBLE MANAGED BLOCK`.
> Those BEGIN/END marker lines are **expected and correct** — they're the default markers required by
> note (ii). A "failed to look up user apache" error means the blockinfile task ran before httpd was
> installed. "Missing sudo password" means the inventory needs `ansible_become_pass`.

View File

@@ -0,0 +1,162 @@
# Assignment
Developers are looking for dependencies to be installed and run on Nautilus app servers in Stratos DC. They have shared some requirements with the DevOps team. Because we are now managing packages installation and services management using Ansible, some playbooks need to be created and tested. As per details mentioned below please complete the task:
a. On jump host create an Ansible playbook /home/thor/ansible/playbook.yml and configure it to install httpd on all app servers.
b. After installation make sure to start and enable httpd service on all app servers.
c. The inventory /home/thor/ansible/inventory is already there on jump host.
d. Make sure user thor should be able to run the playbook on jump host.
Note: Validation will try to run playbook using command ansible-playbook -i inventory playbook.yml so please make sure playbook works this way, without passing any extra arguments.
# Solution
# Ansible Playbook — install and enable httpd on all App Servers
Create `/home/thor/ansible/playbook.yml` so `ansible-playbook -i inventory playbook.yml` installs
httpd and brings the service up on all app servers — with **no extra arguments**.
> Note: this is an Ansible task, not Kubernetes — no manifests to pipe into `kubectl`. The heredoc
> below writes the playbook.
## Step 0 — The inventory already exists; leave it alone
Requirement (c) confirms the inventory is already in place. **Don't recreate or overwrite it**
just verify its contents:
```bash
cat /home/thor/ansible/inventory
```
Confirm it lists the app servers with their connection variables. If it lacks
`ansible_become_pass` and the app-server users need a sudo password, add that per host — the
playbook requires privilege escalation and you can't pass `-K` at runtime.
## Step 1 — Playbook
```bash
cat > /home/thor/ansible/playbook.yml <<'EOF'
---
- name: Install and enable httpd on all app servers
hosts: all
become: yes
tasks:
- name: Install httpd package
ansible.builtin.yum:
name: httpd
state: present
- name: Start and enable httpd service
ansible.builtin.service:
name: httpd
state: started
enabled: yes
EOF
```
## Step 2 — Ensure `thor` can run it (requirement d)
```bash
ls -l /home/thor/ansible/
```
Creating the playbook with the heredoc above (as `thor`) already gives correct ownership. If the
directory was pre-created by another user and `thor` can't read the files, fix it:
```bash
sudo chown -R thor:thor /home/thor/ansible
chmod 644 /home/thor/ansible/playbook.yml
```
## How it works
### Why `become: yes`
Both tasks are privileged: installing a package writes to system directories and the RPM database,
and managing a systemd service requires root. The SSH users on the app servers are unprivileged, so
the play escalates once at the **play level** rather than repeating `become` on each task.
Because validation runs the bare command with no `-K`, the sudo password must come from
`ansible_become_pass` in the existing inventory. "Missing sudo password" at runtime means that
variable is absent or wrong — and it's the most common failure on this task, since there's no way to
supply it on the command line.
### Task 1 — install httpd
`ansible.builtin.yum` with **`state: present`** installs the package if it's missing and does nothing
if it's already there. That makes the task **idempotent**: the first run reports `changed`,
subsequent runs report `ok`. `state: latest` would instead upgrade on every run, which isn't what
"install" asks for.
These app servers are RHEL/CentOS-family, so `yum` is the correct package module. (On modern
RHEL 8+ the `dnf` module is its successor, and Ansible's `yum` module delegates to dnf where
appropriate — so `yum` works regardless.)
### Task 2 — start *and* enable
Requirement (b) has two distinct halves, and `ansible.builtin.service` covers both:
| Option | Effect |
|--------|--------|
| `state: started` | httpd is running **right now**. |
| `enabled: yes` | httpd starts automatically **on boot**. |
These are independent — a service can be running but not enabled (dies on reboot), or enabled but
not currently started. "Start and enable" requires both, so both are set.
### Ordering matters
The install task must precede the service task: you can't start a service whose unit file doesn't
exist yet. Ansible executes tasks top to bottom, so this ordering is a correctness requirement, not
just style. Reversing them would fail with "Could not find the requested service httpd."
### `hosts: all`
The inventory contains the app servers, so `all` targets exactly them — and it can't break if the
inventory's group name and the playbook's `hosts:` value ever drift apart.
### Requirement (d) — "thor should be able to run the playbook"
Validation runs as `thor`, so `thor` must be able to **read** the playbook and inventory. Writing the
file via heredoc as `thor` satisfies this automatically; Step 2 is a safety check for the case where
the directory was pre-created with root ownership, which would fail validation before Ansible even
starts.
### Host key checking
If a first-time SSH connection trips on host-key verification, add an `ansible.cfg` beside the
playbook — safest because it needs no extra command-line arguments:
```bash
cat > /home/thor/ansible/ansible.cfg <<'EOF'
[defaults]
host_key_checking = False
EOF
```
## Verify
```bash
cd /home/thor/ansible
# Connectivity first
ansible -i inventory all -m ping
# The actual validation command
ansible-playbook -i inventory playbook.yml
# httpd installed
ansible -i inventory all -b -m command -a "rpm -q httpd"
# Running AND enabled
ansible -i inventory all -b -m command -a "systemctl is-active httpd"
ansible -i inventory all -b -m command -a "systemctl is-enabled httpd"
```
Expected — `ping` returning `SUCCESS` on every server; the playbook finishing with `failed=0`;
`rpm -q httpd` printing an installed version; `is-active` returning `active`; and `is-enabled`
returning `enabled`.
> Check **both** `is-active` and `is-enabled` — passing only one means half the requirement was met.
> "Could not find the requested service httpd" ⇒ task order is wrong. "Missing sudo password" ⇒ the
> inventory needs `ansible_become_pass`.

View File

@@ -0,0 +1,209 @@
# Assignment
There are some files that need to be created on all app servers in Stratos DC. The Nautilus DevOps team want these files to be owned by user root only however, they also want that the app specific user to have a set of permissions on these files. All tasks must be done using Ansible only, so they need to create a playbook. Below you can find more information about the task.
Create a playbook named playbook.yml under /home/thor/ansible directory on jump host, an inventory file is already present under /home/thor/ansible directory on Jump Server itself.
Create an empty file blog.txt under /opt/data/ directory on app server 1. Set some acl properties for this file. Using acl provide read '(r)' permissions to group tony (i.e entity is tony and etype is group).
Create an empty file story.txt under /opt/data/ directory on app server 2. Set some acl properties for this file. Using acl provide read + write '(rw)' permissions to user steve (i.e entity is steve and etype is user).
Create an empty file media.txt under /opt/data/ on app server 3. Set some acl properties for this file. Using acl provide read + write '(rw)' permissions to group banner (i.e entity is banner and etype is group).
Note: Validation will try to run the playbook using command ansible-playbook -i inventory playbook.yml so please make sure the playbook works this way, without passing any extra arguments.
# Solution
# Ansible Playbook — root-owned files with per-user ACLs
Create `/home/thor/ansible/playbook.yml` so `ansible-playbook -i inventory playbook.yml` creates a
root-owned file on each app server and grants an app-specific user or group extra access via ACLs —
with **no extra arguments**.
> Note: this is an Ansible task, not Kubernetes — no manifests to pipe into `kubectl`. The heredoc
> below writes the playbook.
## Step 0 — Check the existing inventory
The inventory is already present. **Don't overwrite it** — but confirm the hostnames, since the
playbook targets each server by name:
```bash
cat /home/thor/ansible/inventory
```
You need `stapp01`, `stapp02`, `stapp03` (match the playbook's `hosts:` values to whatever names it
uses). Also confirm `ansible_become_pass` is present, since the play requires root and you can't
pass `-K`.
## Step 1 — Playbook
```bash
cat > /home/thor/ansible/playbook.yml <<'EOF'
---
- name: Configure blog.txt on App Server 1
hosts: stapp01
become: yes
tasks:
- name: Create /opt/data/blog.txt owned by root
ansible.builtin.file:
path: /opt/data/blog.txt
state: touch
owner: root
group: root
- name: Grant read permission to group tony
acl:
path: /opt/data/blog.txt
entity: tony
etype: group
permissions: r
state: present
- name: Configure story.txt on App Server 2
hosts: stapp02
become: yes
tasks:
- name: Create /opt/data/story.txt owned by root
ansible.builtin.file:
path: /opt/data/story.txt
state: touch
owner: root
group: root
- name: Grant read+write permission to user steve
acl:
path: /opt/data/story.txt
entity: steve
etype: user
permissions: rw
state: present
- name: Configure media.txt on App Server 3
hosts: stapp03
become: yes
tasks:
- name: Create /opt/data/media.txt owned by root
ansible.builtin.file:
path: /opt/data/media.txt
state: touch
owner: root
group: root
- name: Grant read+write permission to group banner
acl:
path: /opt/data/media.txt
entity: banner
etype: group
permissions: rw
state: present
EOF
```
## How it works
### The core idea: root ownership + ACLs for everyone else
This is exactly the problem ACLs solve. Traditional Unix permissions give a file **one** owner and
**one** group — so if `root` must own the file, there's no way to also grant `tony` or `steve`
specific access through `chmod` alone without opening it to "other" (everyone).
**ACLs** add supplementary entries on top of the standard permission bits, letting you name
additional users or groups and give each its own rights. So:
- `owner: root` / `group: root` satisfies "owned by user root only"
- the `acl` entry satisfies "app specific user should have a set of permissions"
Both requirements hold simultaneously, which plain permissions couldn't achieve.
### Why three separate plays
Each server needs a different **filename**, **entity**, **etype**, and **permission set** — four
things varying at once with no shared pattern. Three plays scoped with `hosts: stapp01` /
`stapp02` / `stapp03` express that directly. A single play with
`when: inventory_hostname == "stapp01"` conditionals would work too, but needs six conditional tasks
and buries the intent.
One playbook file can hold multiple plays; they execute top to bottom, each against its own host set.
### The `file` task
- **`state: touch`** — creates the file if absent, leaving it empty ("create an empty file").
- **`owner: root` / `group: root`** — the explicit ownership requirement. Setting ownership requires
root privileges (`chown` is privileged), which the play already has via `become: yes`.
Note `state: touch` bumps timestamps each run, so re-runs report `changed` rather than `ok`. Fine
here.
### The `acl` module
The module maps directly onto `setfacl`:
| Parameter | Meaning | Values across the three servers |
|-----------|---------|----------------------------------|
| `path` | File to modify | `/opt/data/blog.txt`, `story.txt`, `media.txt` |
| `entity` | **Who** the rule applies to | `tony`, `steve`, `banner` |
| `etype` | Entity kind: `user`, `group`, `other`, `mask` | **group**, **user**, **group** |
| `permissions` | Rights as `rwx` letters | **r**, **rw**, **rw** |
| `state: present` | Ensure the entry exists | — |
Watch the pairing — servers 1 and 3 both use `etype: group` but with **different** permissions
(`r` vs `rw`), while server 2 uses `etype: user`. Transposing `user` and `group` is the most common
way to fail this task.
The shell equivalent for the first play is `setfacl -m g:tony:r /opt/data/blog.txt`; the module makes
it declarative and idempotent.
### Module naming
The short name `acl` is used for compatibility. On modern Ansible the module lives in the
`ansible.posix` collection and the short name routes there automatically when that collection is
installed (standard in a full Ansible install). If you hit "couldn't resolve module/action 'acl'",
switch to the fully-qualified name:
```yaml
ansible.posix.acl:
```
installing it if needed with `ansible-galaxy collection install ansible.posix`.
### Why `become: yes`
Three things here need root: creating files under the root-owned `/opt/data`, setting `owner`/`group`
to root (`chown`), and applying ACLs with `setfacl`. The play escalates once at play level, and the
sudo password comes from `ansible_become_pass` in the inventory since `-K` can't be passed.
### Prerequisite
ACL support needs the `acl` package (`setfacl`/`getfacl`) on the **managed nodes** and a filesystem
mounted with ACL support. Both are standard on RHEL/CentOS-family systems. If a play fails with
"setfacl not found," installing the `acl` package on the target resolves it.
## Verify
```bash
cd /home/thor/ansible
# Connectivity
ansible -i inventory all -m ping
# The actual validation command
ansible-playbook -i inventory playbook.yml
# Ownership and ACLs per server
ansible -i inventory stapp01 -b -m command -a "getfacl /opt/data/blog.txt"
ansible -i inventory stapp02 -b -m command -a "getfacl /opt/data/story.txt"
ansible -i inventory stapp03 -b -m command -a "getfacl /opt/data/media.txt"
```
Expected — the playbook finishing with `failed=0` across all three plays, and `getfacl` output
showing `# owner: root` / `# group: root` at the top plus:
- stapp01 `/opt/data/blog.txt``group:tony:r--`
- stapp02 `/opt/data/story.txt``user:steve:rw-`
- stapp03 `/opt/data/media.txt``group:banner:rw-`
> `getfacl` conveniently shows **both** requirements at once — the owner/group header lines and the
> supplementary ACL entries. Check the `user:` vs `group:` prefix carefully. "Missing sudo password"
> ⇒ inventory needs `ansible_become_pass`. "No hosts matched" ⇒ inventory hostnames don't match the
> `hosts:` values.

View File

@@ -0,0 +1,200 @@
# Assignment
The Nautilus DevOps team want to install and set up a simple httpd web server on all app servers in Stratos DC. They also want to deploy a sample web page using Ansible. Therefore, write the required playbook to complete this task as per details mentioned below.
We already have an inventory file under /home/thor/ansible directory on jump host. Write a playbook playbook.yml under /home/thor/ansible directory on jump host itself. Using the playbook perform below given tasks:
Install httpd web server on all app servers, and make sure its service is up and running.
Create a file /var/www/html/index.html with content:
This is a Nautilus sample file, created using Ansible!
Using lineinfile Ansible module add some more content in /var/www/html/index.html file. Below is the content:
Welcome to Nautilus Group!
Also make sure this new line is added at the top of the file.
The /var/www/html/index.html file's user and group owner should be apache on all app servers.
The /var/www/html/index.html file's permissions should be 0655 on all app servers.
Note: Validation will try to run the playbook using command ansible-playbook -i inventory playbook.yml so please make sure the playbook works this way without passing any extra arguments.
# Solution
# Ansible Playbook — httpd + sample page with `lineinfile`
Create `/home/thor/ansible/playbook.yml` so `ansible-playbook -i inventory playbook.yml` installs
httpd, starts it, and builds an `index.html` with a line prepended at the top — with **no extra
arguments**.
> Note: this is an Ansible task, not Kubernetes — no manifests to pipe into `kubectl`. The heredoc
> below writes the playbook.
## Step 0 — The inventory already exists; leave it alone
```bash
cat /home/thor/ansible/inventory
```
Confirm it lists the app servers with their connection variables, and that
`ansible_become_pass` is present — the play needs root and you can't pass `-K`.
## Step 1 — Playbook
```bash
cat > /home/thor/ansible/playbook.yml <<'EOF'
---
- name: Install httpd and deploy sample web page
hosts: all
become: yes
tasks:
- name: Install httpd package
ansible.builtin.yum:
name: httpd
state: present
- name: Start and enable httpd service
ansible.builtin.service:
name: httpd
state: started
enabled: yes
- name: Create index.html with sample content
ansible.builtin.copy:
dest: /var/www/html/index.html
content: |
This is a Nautilus sample file, created using Ansible!
owner: apache
group: apache
mode: '0655'
- name: Add welcome line at the top of index.html
ansible.builtin.lineinfile:
path: /var/www/html/index.html
line: 'Welcome to Nautilus Group!'
insertbefore: BOF
owner: apache
group: apache
mode: '0655'
EOF
```
Resulting file content:
```
Welcome to Nautilus Group!
This is a Nautilus sample file, created using Ansible!
```
## How it works
### Task order is a hard dependency
The four tasks must run in this sequence:
1. **Install httpd** — this creates the `apache` user *and* the `/var/www/html/` directory. Both are
needed by later tasks.
2. **Start/enable the service** — httpd must exist before it can be managed.
3. **Create the file** — the directory now exists and `apache` is a valid owner.
4. **Prepend the line** — the file must exist before `lineinfile` can modify it.
Reordering breaks things concretely: a `copy` before the install fails with "Destination directory
does not exist" or "failed to look up user apache"; a `lineinfile` before the `copy` has nothing to
insert into.
### Task 1 & 2 — install and run
- **`yum` with `state: present`** — installs if missing, no-ops otherwise (idempotent: `changed`
first run, `ok` after). `state: latest` would upgrade on every run, which isn't what's asked.
- **`service` with `state: started` *and* `enabled: yes`** — two independent settings.
`started` = running now; `enabled` = starts on boot. "Up and running" needs `started`; `enabled`
makes it survive reboots. Both are standard for this requirement.
### Task 3 — `copy` with inline `content`
Using `copy` with the **`content:`** parameter writes a literal string to `dest` — no source file on
the controller needed. The `|` block scalar preserves the text exactly and appends a trailing
newline, so the next task's line lands cleanly above it.
(`copy` also accepts `src:` for pushing an existing file; `content:` is the right choice when the
text is defined inline, as here.)
### Task 4 — `lineinfile` with `insertbefore: BOF`
This is the crux of requirement 3. `lineinfile` ensures a **single line** is present in a file:
- **`line: 'Welcome to Nautilus Group!'`** — the exact text to guarantee.
- **`insertbefore: BOF`** — `BOF` is a special value meaning **Beginning Of File**. It places the
line as the very first line, which is precisely "added at the top." Without it, `lineinfile`
appends to the end by default — the file would still contain both lines, but in the wrong order,
and validation would fail.
(The mirror value is `insertafter: EOF` for the end of the file.)
`lineinfile` is also **idempotent**: on re-runs it finds the line already present and reports `ok`
rather than inserting a duplicate.
### `copy` vs `lineinfile` — why both
They do different jobs. `copy` establishes the file's whole content from scratch (requirement 2);
`lineinfile` surgically inserts one line into an existing file (requirement 3). The task explicitly
asks for `lineinfile` for the second piece, so this two-step approach matches the requirements
directly rather than just writing both lines in one `copy`.
### Ownership and permissions
- **`owner: apache` / `group: apache`** — the `apache` account exists only *after* the httpd package
is installed, which is why task 1 must come first.
- **`mode: '0655'`** — **quoted**. Unquoted octal like `0655` is a classic YAML misparse that
silently produces the wrong permissions. Note this is `0655`, not the more familiar `0644` or
`0755` — copy it exactly.
Both are set on the `copy` **and** the `lineinfile` task. Setting them on the final task is what
guarantees the end state, since `lineinfile` rewrites the file; specifying them on both makes the
result deterministic regardless of which task last touched the file.
### `become: yes`
Installing packages, managing systemd services, writing under root-owned `/var/www/html/`, and
running `chown` all require root. The play escalates once at play level, with the sudo password
coming from `ansible_become_pass` in the inventory.
## Verify
```bash
cd /home/thor/ansible
# Connectivity
ansible -i inventory all -m ping
# The actual validation command
ansible-playbook -i inventory playbook.yml
# Service running and enabled
ansible -i inventory all -b -m command -a "systemctl is-active httpd"
ansible -i inventory all -b -m command -a "systemctl is-enabled httpd"
# Content order, ownership, permissions
ansible -i inventory all -b -m command -a "cat /var/www/html/index.html"
ansible -i inventory all -b -m command -a "ls -l /var/www/html/index.html"
```
Expected — playbook `failed=0`; `is-active``active` and `is-enabled``enabled`; `cat` showing
**`Welcome to Nautilus Group!` on the first line** followed by the sample-file line; and `ls -l`
showing `-rw-r-xr-x` (0655) with `apache apache`.
> The line **order** is the thing to check most carefully — if the welcome line appears at the
> bottom, `insertbefore: BOF` is missing or misspelled. "failed to look up user apache" ⇒ a file task
> ran before the httpd install.

View File

@@ -0,0 +1,189 @@
# Assignment
One of the Nautilus DevOps team members is working on to develop a role for httpd installation and configuration. Work is almost completed, however there is a requirement to add a jinja2 template for index.html file. Additionally, the relevant task needs to be added inside the role. The inventory file ~/ansible/inventory is already present on jump host that can be used. Complete the task as per details mentioned below:
a. Update ~/ansible/playbook.yml playbook to run the httpd role on App Server 2.
b. Create a jinja2 template index.html.j2 under /home/thor/ansible/role/httpd/templates/ directory and add a line This file was created using Ansible on <respective server> (for example This file was created using Ansible on stapp01 in case of App Server 1). Also please make sure not to hard code the server name inside the template. Instead, use inventory_hostname variable to fetch the correct value.
c. Add a task inside /home/thor/ansible/role/httpd/tasks/main.yml to copy this template on App Server 2 under /var/www/html/index.html. Also make sure that /var/www/html/index.html file's permissions are 0755.
d. The user/group owner of /var/www/html/index.html file must be respective sudo user of the server (for example tony in case of stapp01).
Note: Validation will try to run the playbook using command ansible-playbook -i inventory playbook.yml so please make sure the playbook works this way without passing any extra arguments.
# Solution
# Ansible Role — jinja2 template for `index.html`
Add a jinja2 template to the existing `httpd` role and wire it up so
`ansible-playbook -i inventory playbook.yml` deploys it to App Server 2 — with **no extra
arguments**.
> Note: this is an Ansible task, not Kubernetes — no manifests to pipe into `kubectl`. The heredocs
> below write/append the files.
## Step 0 — Inspect what already exists
The role is partially built. Look before you touch anything:
```bash
cat /home/thor/ansible/inventory
cat /home/thor/ansible/playbook.yml
cat /home/thor/ansible/role/httpd/tasks/main.yml
ls -l /home/thor/ansible/role/httpd/
```
Note the existing tasks in `main.yml`**they must be preserved**. Also confirm the inventory has
`stapp02` and an `ansible_become_pass`, since the role needs root and `-K` can't be passed.
## Step 1 — Update the playbook (a)
```bash
cat > /home/thor/ansible/playbook.yml <<'EOF'
---
- name: Install and configure httpd on App Server 2
hosts: stapp02
become: yes
roles:
- role/httpd
EOF
```
## Step 2 — Create the jinja2 template (b)
```bash
mkdir -p /home/thor/ansible/role/httpd/templates
cat > /home/thor/ansible/role/httpd/templates/index.html.j2 <<'EOF'
This file was created using Ansible on {{ inventory_hostname }}
EOF
```
**The quoted `<<'EOF'` is essential here** — it stops the shell from touching `{{ inventory_hostname }}`
so the literal jinja2 expression reaches the file intact.
## Step 3 — Append the task to the role (c, d)
```bash
cat >> /home/thor/ansible/role/httpd/tasks/main.yml <<'EOF'
- name: Deploy index.html from jinja2 template
ansible.builtin.template:
src: index.html.j2
dest: /var/www/html/index.html
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
mode: '0755'
EOF
```
Note **`>>`** (append), not `>` (overwrite) — this preserves the role's existing install/service
tasks. Verify afterwards:
```bash
cat /home/thor/ansible/role/httpd/tasks/main.yml
```
## How it works
### The playbook and the role path
- **`hosts: stapp02`** — requirement (a) scopes the run to App Server 2 only.
- **`roles: - role/httpd`** — the role lives at `/home/thor/ansible/role/httpd`. Ansible's default
search path is `./roles/`, but this directory is named `role` (singular), so the relative path
`role/httpd` is given explicitly. Ansible resolves it relative to the **playbook's directory**,
which is exactly where it sits. Writing just `httpd` would fail with "the role 'httpd' was not
found."
- **`become: yes`** — the role installs packages, manages a service, and writes under root-owned
`/var/www/html/`. All privileged.
### The jinja2 template — `inventory_hostname`
Requirement (b) forbids hardcoding the server name. **`{{ inventory_hostname }}`** is an Ansible
*magic variable* holding the name of the host **as written in the inventory** — so it evaluates to
`stapp02` when the play runs against App Server 2, `stapp01` on App Server 1, and so on.
That's why the file is a **template** (`.j2`) rather than a static file: `template` renders jinja2
expressions at deploy time, substituting the correct value per host. A `copy` task would transfer
the literal text `{{ inventory_hostname }}` unrendered — the exact mistake the requirement is
guarding against.
The rendered result on App Server 2:
```
This file was created using Ansible on stapp02
```
> Related variables worth distinguishing: `inventory_hostname` is the inventory's name for the host
> (what's needed here), while `ansible_hostname` is the machine's actual short hostname discovered by
> fact-gathering. They often match, but the requirement names `inventory_hostname` explicitly.
### The template task
- **`ansible.builtin.template`** — renders jinja2 then copies. The counterpart to `copy` for dynamic
content.
- **`src: index.html.j2`** — a **bare filename**, no path. Inside a role, the `template` module
automatically searches the role's `templates/` directory, so `templates/index.html.j2` is found
without qualification. That's a role convention worth knowing — it's why the directory name matters.
- **`dest: /var/www/html/index.html`** — the deployed path.
- **`mode: '0755'`** — quoted so YAML parses it as a string; unquoted octal is a classic silent
misparse.
### Requirement (d) — owner via `{{ ansible_user }}`
The owner must be "the respective sudo user of the server" — `tony` on stapp01, `steve` on stapp02,
`banner` on stapp03. Those are precisely the `ansible_user` values already defined per host in the
inventory, so:
```yaml
owner: "{{ ansible_user }}"
group: "{{ ansible_user }}"
```
resolves per host automatically — `steve` here, since the play targets stapp02. No conditionals
needed, and it stays correct if the role is later run against other servers. (This is the same
pattern as the earlier per-host ownership task.)
`group` uses the same value because Linux creates a matching primary group per user by default
(`steve` → group `steve`).
### Why appending matters
`tasks/main.yml` already contains the role's install and service tasks — the work "almost completed"
by the team member. Overwriting it with `>` would delete those and the role would no longer install
httpd, so the template task would land in a directory that doesn't exist. `>>` adds the new task to
the end, which is also the correct **order**: httpd must be installed (creating `/var/www/html/`)
before the template is written into it.
## Verify
```bash
cd /home/thor/ansible
# Existing role tasks still present, new task appended at the end
cat role/httpd/tasks/main.yml
# Connectivity
ansible -i inventory stapp02 -m ping
# The actual validation command
ansible-playbook -i inventory playbook.yml
# Rendered content, ownership, permissions
ansible -i inventory stapp02 -b -m command -a "cat /var/www/html/index.html"
ansible -i inventory stapp02 -b -m command -a "ls -l /var/www/html/index.html"
```
Expected — playbook `failed=0`; `cat` printing
**`This file was created using Ansible on stapp02`** (rendered, *not* the literal `{{ ... }}`); and
`ls -l` showing `-rwxr-xr-x` with owner/group `steve steve`.
> If the file contains a literal `{{ inventory_hostname }}`, the task used `copy` instead of
> `template`, or the shell expanded the heredoc — recheck that Step 2 used the **quoted** `<<'EOF'`.
> "The role 'httpd' was not found" ⇒ the `role/httpd` relative path is wrong.

View File

@@ -0,0 +1,182 @@
# Assignment
The Nautilus DevOps team had a discussion about, how they can train different team members to use Ansible for different automation tasks. There are numerous ways to perform a particular task using Ansible, but we want to utilize each aspect that Ansible offers. The team wants to utilise Ansible's conditionals to perform the following task:
An inventory file is already placed under /home/thor/ansible directory on jump host, with all the Stratos DC app servers included.
Create a playbook /home/thor/ansible/playbook.yml and make sure to use Ansible's when conditionals statements to perform the below given tasks.
Copy blog.txt file present under /usr/src/data directory on jump host to App Server 1 under /opt/data directory. Its user and group owner must be user tony and its permissions must be 0644 .
Copy story.txt file present under /usr/src/data directory on jump host to App Server 2 under /opt/data directory. Its user and group owner must be user steve and its permissions must be 0644 .
Copy media.txt file present under /usr/src/data directory on jump host to App Server 3 under /opt/data directory. Its user and group owner must be user banner and its permissions must be 0644.
NOTE: You can use ansible_nodename variable from gathered facts with when condition. Additionally, please make sure you are running the play for all hosts i.e use - hosts: all.
Note: Validation will try to run the playbook using command ansible-playbook -i inventory playbook.yml, so please make sure the playbook works this way without passing any extra arguments.
# Solution
# Ansible Playbook — per-server copies using `when` conditionals
Create `/home/thor/ansible/playbook.yml` so `ansible-playbook -i inventory playbook.yml` copies a
different file to each app server, selected with **`when` conditionals** on a single
`hosts: all` play — with **no extra arguments**.
> Note: this is an Ansible task, not Kubernetes — no manifests to pipe into `kubectl`. The heredoc
> below writes the playbook.
## Step 0 — Confirm the inventory hostnames
The conditionals branch on the hostnames as written in your inventory, so read them first:
```bash
cat /home/thor/ansible/inventory
```
Use exactly the names it lists (`stapp01`, `stapp02`, `stapp03` in the standard setup) in the
`when:` lines below. Also confirm `ansible_become_pass` is present, since the play needs root and
`-K` can't be passed.
## Step 1 — Playbook
```bash
cat > /home/thor/ansible/playbook.yml <<'EOF'
---
- name: Copy files to app servers using conditionals
hosts: all
become: yes
tasks:
- name: Copy blog.txt to App Server 1
ansible.builtin.copy:
src: /usr/src/data/blog.txt
dest: /opt/data/blog.txt
owner: tony
group: tony
mode: '0644'
when: inventory_hostname == "stapp01"
- name: Copy story.txt to App Server 2
ansible.builtin.copy:
src: /usr/src/data/story.txt
dest: /opt/data/story.txt
owner: steve
group: steve
mode: '0644'
when: inventory_hostname == "stapp02"
- name: Copy media.txt to App Server 3
ansible.builtin.copy:
src: /usr/src/data/media.txt
dest: /opt/data/media.txt
owner: banner
group: banner
mode: '0644'
when: inventory_hostname == "stapp03"
EOF
```
## How it works
### The conditional pattern
With **`hosts: all`**, every task is evaluated against **every** host. The `when:` clause is what
narrows each one:
- On stapp01, task 1's condition is true → it **runs**; tasks 2 and 3 are false → **skipped**.
- On stapp02, only task 2 runs. On stapp03, only task 3.
So the play visits all three servers, and each executes exactly the one task meant for it. The
`PLAY RECAP` will show roughly `ok=2 changed=1 skipped=2` per host — **skipped tasks are expected
and correct here**, not a failure.
This is the opposite structural choice from writing three separate plays (`hosts: stapp01`, etc.).
Both produce the same end state; the task explicitly asks for the conditional form, which is what
you'd reach for when the differences are small and you want one play covering the fleet.
### Why `inventory_hostname`
`inventory_hostname` is a **magic variable** holding the host's name **exactly as written in the
inventory file**. That makes it the most reliable thing to branch on:
- You can read its value directly (`cat inventory`) — no guessing, no remote lookup.
- It doesn't depend on fact gathering.
- It can't drift from what the inventory says, because it *is* what the inventory says.
**The alternative — `ansible_nodename`:** the task's note mentions it, and it also works, but it's a
**gathered fact** reporting whatever name the operating system is configured with. That's often a
fully-qualified domain name rather than the short one, and the exact value depends on how the lab's
hosts are configured. Comparing it against the wrong string matches nothing, every task silently
skips, and the playbook reports **success with no work done** — a failure mode that looks like a
pass until you inspect the files.
If you prefer that route, get the real values first rather than assuming them:
```bash
ansible -i inventory all -m setup -a "filter=ansible_nodename"
```
then paste the exact output into the conditions:
```yaml
when: ansible_nodename == "<exact value printed for stapp01>"
```
Either variable satisfies the requirement — the task says you *can* use `ansible_nodename`, not that
you must, and validation checks the resulting files rather than which variable you branched on.
`inventory_hostname` is simply the version with nothing left to verify.
### The `copy` task
- **`src: /usr/src/data/blog.txt`** — a path on the **control node** (the jump host). `copy` reads
from the controller by default, which is exactly "copy from jump host to app server." (Copying
between two paths on the *remote* machine would need `remote_src: yes` — not the case here.)
- **`dest: /opt/data/blog.txt`** — the explicit target path, filename included. Clearer than relying
on `dest: /opt/data/` directory-expansion.
- **`owner` / `group`** — the required per-server user. Hardcoded here because each task is already
pinned to one host by its `when:`, so the literal value is unambiguous and matches the requirement
text directly.
- **`mode: '0644'`** — **quoted**. Unquoted octal like `0644` is a classic YAML misparse that
silently yields wrong permissions.
### Why `become: yes`
`/opt/data` is root-owned, so writing there requires escalation — and setting `owner`/`group` runs
`chown`, which is privileged regardless. The play escalates once at play level, with the sudo
password supplied by `ansible_become_pass` from the inventory.
> If `/opt/data` doesn't already exist on the targets, `copy` fails with "Destination directory does
> not exist" — it won't create missing parents. Add a `file`/`state: directory` task before the
> copies if you hit that.
## Verify
```bash
cd /home/thor/ansible
# Connectivity
ansible -i inventory all -m ping
# The actual validation command
ansible-playbook -i inventory playbook.yml
# Each file, ownership and permissions
ansible -i inventory stapp01 -b -m command -a "ls -l /opt/data/blog.txt"
ansible -i inventory stapp02 -b -m command -a "ls -l /opt/data/story.txt"
ansible -i inventory stapp03 -b -m command -a "ls -l /opt/data/media.txt"
```
Expected — playbook `failed=0` with `skipped=2` on each host (normal for this pattern), and `ls -l`
showing `-rw-r--r--` with `tony tony`, `steve steve`, and `banner banner` respectively.
> **If every task shows `skipped` and no files appear**, the `when:` strings don't match the actual
> hostnames — recheck them against `cat inventory`. That's the classic failure mode for conditionals:
> a run that reports success while doing nothing.

View File

@@ -0,0 +1,105 @@
# Assignment
The Nautilus DevOps team is strategizing the migration of a portion of their infrastructure to the AWS cloud. Recognizing the scale of this undertaking, they have opted to approach the migration in incremental steps rather than as a single massive transition. To achieve this, they have segmented large tasks into smaller, more manageable units. This granular approach enables the team to execute the migration in gradual phases, ensuring smoother implementation and minimizing disruption to ongoing operations. By breaking down the migration into smaller tasks, the Nautilus DevOps team can systematically progress through each stage, allowing for better control, risk mitigation, and optimization of resources throughout the migration process.
Create a VPC named datacenter-vpc in region us-east-1 with any IPv4 CIDR block through terraform.
The Terraform working directory is /home/bob/terraform. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# Terraform VPC — `datacenter-vpc`
Create a VPC in `us-east-1` with any IPv4 CIDR block.
## Create `main.tf` (heredoc → file)
```bash
cd /home/bob/terraform
cat > main.tf <<'EOF'
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_vpc" "datacenter_vpc" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "datacenter-vpc"
}
}
EOF
```
## How to run
```bash
cd /home/bob/terraform
terraform init
terraform apply -auto-approve
```
## How it works
### The heredoc
`cat > main.tf <<'EOF'` writes the file in one shot. The **quoted** `'EOF'` disables shell expansion,
so any `$` or backticks in HCL reach the file literally rather than being interpreted by bash — the
right default whenever writing config files this way.
Note the task says **`main.tf` only** — don't split the provider block into a separate
`provider.tf` or `versions.tf`.
### The provider block
- **`region = "us-east-1"`** — pins the deployment region as required. A VPC is a regional resource,
so this determines where it's created.
- **`required_providers`** with a version constraint keeps `terraform init` from pulling an
unexpected major version.
### The `aws_vpc` resource
One resource does the whole job:
- **`cidr_block = "10.0.0.0/16"`** — the task allows *any* IPv4 block, so this uses a standard
RFC 1918 private range. A `/16` gives 65,536 addresses, plenty of room to carve subnets from
later. Any valid private CIDR (`172.16.0.0/16`, `192.168.0.0/24`, …) would satisfy the requirement
equally; `10.0.0.0/16` is the conventional default.
- **`tags = { Name = "datacenter-vpc" }`** — this is what "named" means for a VPC. AWS VPCs have **no
native name field**; the console and any grader read the `Name` tag. Omit the tag and the VPC still
exists but shows as unnamed — the most common way to fail this task.
The Terraform resource label (`datacenter_vpc`) is just the internal identifier used for references
within the config; it's unrelated to the AWS-visible name, which comes from the tag.
## Verify
```bash
aws ec2 describe-vpcs \
--filters Name=tag:Name,Values=datacenter-vpc \
--query 'Vpcs[0].{Id:VpcId,Cidr:CidrBlock,Name:Tags[?Key==`Name`]|[0].Value}'
```
Expected — the VPC ID, `CidrBlock: 10.0.0.0/16`, and `Name: datacenter-vpc`.
You can also confirm from Terraform's own state:
```bash
terraform state show aws_vpc.datacenter_vpc
```
> If `describe-vpcs` returns nothing, the `Name` tag is missing or misspelled — the filter matches on
> that tag, not on the resource label in the config.

View File

@@ -0,0 +1,178 @@
# Assignment
The Nautilus DevOps team is strategizing the migration of a portion of their infrastructure to the AWS cloud. Recognizing the scale of this undertaking, they have opted to approach the migration in incremental steps rather than as a single massive transition. To achieve this, they have segmented large tasks into smaller, more manageable units. This granular approach enables the team to execute the migration in gradual phases, ensuring smoother implementation and minimizing disruption to ongoing operations. By breaking down the migration into smaller tasks, the Nautilus DevOps team can systematically progress through each stage, allowing for better control, risk mitigation, and optimization of resources throughout the migration process.
Use Terraform to create a security group under the default VPC with the following requirements:
1) The name of the security group must be nautilus-sg.
2) The description must be Security group for Nautilus App Servers.
3) Add an inbound rule of type HTTP, with a port range of 80, and source CIDR range 0.0.0.0/0.
4) Add another inbound rule of type SSH, with a port range of 22, and source CIDR range 0.0.0.0/0.
Ensure that the security group is created in the us-east-1 region using Terraform. The Terraform working directory is /home/bob/terraform. Create the main.tf file (do not create a different .tf file) to accomplish this task.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# Terraform Security Group — `nautilus-sg`
Create a security group in the **default VPC** in `us-east-1` with HTTP and SSH inbound rules open
to the world.
## Create `main.tf` (heredoc → file)
```bash
cd /home/bob/terraform
cat > main.tf <<'EOF'
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
# Look up the existing default VPC
data "aws_vpc" "default" {
default = true
}
resource "aws_security_group" "nautilus_sg" {
name = "nautilus-sg"
description = "Security group for Nautilus App Servers"
vpc_id = data.aws_vpc.default.id
ingress {
description = "HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "nautilus-sg"
}
}
EOF
```
## How to run
```bash
cd /home/bob/terraform
terraform init
terraform apply -auto-approve
```
## How it works
### The heredoc
`cat > main.tf <<'EOF'` writes the file in one shot. The **quoted** `'EOF'` disables shell expansion,
so `$` and backticks in the HCL reach the file literally. The task requires everything in
**`main.tf`** — don't split the provider block into a separate file.
### Finding the default VPC
```hcl
data "aws_vpc" "default" {
default = true
}
```
A **data source** *reads* an existing resource rather than creating one. Setting `default = true`
selects the account's default VPC for the region, and `data.aws_vpc.default.id` then feeds the
security group's `vpc_id`.
This is deliberately a data source, not a resource. The `aws_default_vpc` **resource** would adopt
the default VPC into Terraform state and let Terraform modify (or on destroy, orphan) it — far more
intrusive than needed. Reading it keeps Terraform's ownership limited to the security group itself.
### The security group
- **`name = "nautilus-sg"`** — the group name, exactly as required.
- **`description`** — required by AWS on every security group (Terraform defaults it to "Managed by
Terraform" if omitted). Important: the description is **immutable** — AWS won't let you change it
after creation. If you apply with the wrong text, you must destroy and recreate the group, so get
it right the first time.
- **`vpc_id`** — pins the group to the default VPC per the requirement.
### The ingress rules
Each `ingress` block is one inbound rule:
| Requirement | `protocol` | `from_port` / `to_port` | `cidr_blocks` |
|-------------|-----------|--------------------------|---------------|
| HTTP | `tcp` | 80 / 80 | `0.0.0.0/0` |
| SSH | `tcp` | 22 / 22 | `0.0.0.0/0` |
The "type" the task refers to (HTTP, SSH) is a **console-level label**, not an API field. AWS derives
it from the protocol/port combination — `tcp` + port 80 *is* HTTP, `tcp` + port 22 *is* SSH. That's
why there's no `type` argument in the HCL; setting the right protocol and ports is what makes the
console display those names. The `description` field here is just a human-readable label and doesn't
affect matching.
`from_port` and `to_port` define a **range**; setting both to the same value expresses a single
port. `0.0.0.0/0` means any source IPv4 address.
> Security note: opening SSH (22) to `0.0.0.0/0` is fine for a lab but poor practice in production,
> where you'd restrict it to a bastion host or known CIDR.
### The egress rule
```hcl
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
```
`protocol = "-1"` means **all protocols**, and with ports `0`/`0` this is the standard allow-all
outbound rule. It's included because when Terraform manages a security group with **no** `egress`
block, it strips AWS's default allow-all outbound rule, leaving the group unable to initiate any
outbound traffic. The task doesn't ask about egress, so preserving the normal default is the sane
choice.
## Verify
```bash
aws ec2 describe-security-groups \
--filters Name=group-name,Values=nautilus-sg \
--query 'SecurityGroups[0].{Name:GroupName,Desc:Description,Vpc:VpcId,Ingress:IpPermissions[*].{Proto:IpProtocol,From:FromPort,To:ToPort,Cidr:IpRanges[0].CidrIp}}'
```
Expected — `GroupName: nautilus-sg`, the exact description string, the default VPC's ID, and two
ingress entries: `tcp 80→80 0.0.0.0/0` and `tcp 22→22 0.0.0.0/0`.
> If the description is wrong, `terraform apply` **cannot** fix it in place — run
> `terraform destroy -target=aws_security_group.nautilus_sg` and re-apply with the corrected text.

View File

@@ -0,0 +1,187 @@
# Assignment
The Nautilus DevOps team is strategizing the migration of a portion of their infrastructure to the AWS cloud. Recognizing the scale of this undertaking, they have opted to approach the migration in incremental steps rather than as a single massive transition. To achieve this, they have segmented large tasks into smaller, more manageable units.
For this task, create an EC2 instance using Terraform with the following requirements:
The EC2 instance must use the value datacenter-ec2 as its Name tag, which defines the instance name in AWS.
Use the Amazon Linux ami-0c101f26f147fa7fd to launch this instance.
The Instance type must be t2.micro.
Create a new RSA key named datacenter-kp.
Attach the default (available by default) security group.
The Terraform working directory is /home/bob/terraform. Create the main.tf file (do not create a different .tf file) to provision the instance.
Note: Right-click under the EXPLORER section in VS Code and select Open in Integrated Terminal to launch the terminal.
# Solution
# Terraform EC2 Instance — `datacenter-ec2`
Launch a t2.micro instance with a newly-created RSA key pair and the default security group.
## Create `main.tf` (heredoc → file)
```bash
cd /home/bob/terraform
cat > main.tf <<'EOF'
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
tls = {
source = "hashicorp/tls"
version = "~> 4.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
# --- New RSA key pair ---
resource "tls_private_key" "datacenter_kp" {
algorithm = "RSA"
rsa_bits = 4096
}
resource "aws_key_pair" "datacenter_kp" {
key_name = "datacenter-kp"
public_key = tls_private_key.datacenter_kp.public_key_openssh
}
# --- Default VPC and its default security group ---
data "aws_vpc" "default" {
default = true
}
data "aws_security_group" "default" {
vpc_id = data.aws_vpc.default.id
name = "default"
}
# --- EC2 instance ---
resource "aws_instance" "datacenter_ec2" {
ami = "ami-0c101f26f147fa7fd"
instance_type = "t2.micro"
key_name = aws_key_pair.datacenter_kp.key_name
vpc_security_group_ids = [data.aws_security_group.default.id]
credit_specification {
cpu_credits = "standard"
}
tags = {
Name = "datacenter-ec2"
}
}
EOF
```
## How to run
```bash
cd /home/bob/terraform
terraform init
terraform apply -auto-approve
```
## How it works
### The heredoc
`cat > main.tf <<'EOF'` writes the file in one shot; the **quoted** `'EOF'` stops the shell from
expanding anything in the HCL. Everything goes in **`main.tf`** as the task requires — no separate
provider or key files.
### The RSA key pair (two resources)
AWS never hands back private key material, so creating a usable key pair takes two resources across
two providers:
1. **`tls_private_key`** — generates the key pair **locally**. `algorithm = "RSA"` is what makes the
resulting AWS key pair type `rsa`; `rsa_bits = 4096` sets its strength.
2. **`aws_key_pair`** — uploads only the **public** half
(`tls_private_key.datacenter_kp.public_key_openssh`) under the name `datacenter-kp`. AWS stores
just the public key; because it's RSA, the key pair registers with `KeyType: rsa`.
Referencing the TLS resource's attribute creates an **implicit dependency**, so Terraform generates
the key before trying to import it.
> The task doesn't ask for the private key to be saved to disk, so no `local_file` resource is
> included. It lives in Terraform state only. If you later need to SSH in, add a
> `local_sensitive_file` writing `tls_private_key.datacenter_kp.private_key_pem` with `0400`
> permissions.
### The default security group
```hcl
data "aws_vpc" "default" { default = true }
data "aws_security_group" "default" {
vpc_id = data.aws_vpc.default.id
name = "default"
}
```
Two **data sources** read existing infrastructure rather than creating it: the account's default VPC,
then the security group named `default` **within that VPC**. Scoping by `vpc_id` matters — every VPC
has its own group named "default", so the name alone is ambiguous.
The group is then attached via **`vpc_security_group_ids`**, which takes a list of security group
**IDs**. (The older `security_groups` argument takes names and is for EC2-Classic; on modern VPC
instances `vpc_security_group_ids` is correct.)
Using data sources keeps Terraform from taking ownership of the default SG — it only references it.
### The instance
- **`ami = "ami-0c101f26f147fa7fd"`** — hardcoded exactly as given, so no AMI lookup data source is
needed.
- **`instance_type = "t2.micro"`** — as required.
- **`key_name = aws_key_pair.datacenter_kp.key_name`** — attaches the key pair by reference,
producing an implicit dependency so the key exists before the instance launches.
- **`tags = { Name = "datacenter-ec2" }`** — an EC2 instance's displayed name comes from the `Name`
**tag**, not a native field. Omit it and the instance runs but appears unnamed — the usual way to
fail this requirement. The Terraform resource label (`datacenter_ec2`) is unrelated; it's only an
internal reference.
### Why `credit_specification` is pinned
```hcl
credit_specification {
cpu_credits = "standard"
}
```
Burstable T-family instances run in either **standard** or **unlimited** CPU-credit mode. In
`unlimited`, an instance can burn credits beyond its baseline and incur surcharge billing — which
constrained sandbox environments actively police, sometimes by resetting the instance or suspending
the session. `t2.micro` defaults to `standard`, so this is belt-and-braces, but pinning it removes
any chance of the mode drifting to `unlimited`.
## Verify
```bash
aws ec2 describe-instances \
--filters Name=tag:Name,Values=datacenter-ec2 Name=instance-state-name,Values=pending,running \
--query 'Reservations[0].Instances[0].{Id:InstanceId,Type:InstanceType,Key:KeyName,SG:SecurityGroups[0].GroupName,State:State.Name}'
aws ec2 describe-key-pairs --key-names datacenter-kp \
--query 'KeyPairs[0].{Name:KeyName,Type:KeyType}'
```
Expected — the instance showing `InstanceType: t2.micro`, `KeyName: datacenter-kp`, security group
`default`, and state `pending` then `running` (it takes a minute or two); and the key pair reporting
`KeyType: rsa`.
> If `describe-instances` returns nothing, check the `Name` tag — the filter matches on that tag, not
> the Terraform resource label.