Writing scripts on a single machine to controlling multiple servers from one central machine. That was the point where Bash started feeling less like a beginner topic and more like real DevOps work.
The setup had one main machine called scriptbox and three target machines: web01, web02, and web03. The first two were CentOS-based and the third one was Ubuntu. The goal was to access them from scriptbox, solve the password problem, and finally automate web server deployment across all of them.
Starting from scriptbox to web01, web02, and web03
The first important idea was that scriptbox acted as the control
machine. Instead of manually logging into every server one by
one, I could execute commands remotely from scriptbox using SSH.
Hostname mapping was added so the machines could be accessed by
names like web01 and web03 rather than
raw IP addresses
ssh vagrant@web01That worked, but there was a problem: it kept asking for a password. For quick testing that is fine, but for automation it becomes a blocker. If a script pauses every time and waits for a password, it is no longer true automation.
The password issue
When the remote command was executed from scriptbox, the command
itself worked, but authentication still needed manual input. The
same pattern appeared when trying to move files with
scp or run commands with ssh remotely.
This exposed a basic truth: remote execution only becomes
scalable when authentication is non-interactive
ssh devops@web01 uptime
Another interesting issue appeared with web03,
which was Ubuntu-based. It was not allowing password login in
the same way as the other machines. The reason turned out to be
the SSH server configuration. In
/etc/ssh/sshd_config, password authentication was
disabled, so it needed to be enabled before password-based login
could even be tested from scriptbox
Creating a proper automation user
Instead of continuing with the default Vagrant user, a separate
user called devops was created on the remote
machines. That user was then added to sudoers with
NOPASSWD so system-level actions such as installing
packages and starting services could be executed without
interactive prompts
This felt like a more realistic setup. In real environments, automation usually should not depend on a default user account. It is better to create a dedicated account with just enough permissions to perform the required actions.
From password-based SSH to key-based SSH
The real improvement came with SSH key-based authentication.
Instead of entering a password again and again, a key pair was
generated with ssh-keygen. Then the public key was
copied to each remote machine with ssh-copy-id.
ssh-keygen ssh-copy-id devops@web01 ssh-copy-id devops@web02
ssh-copy-id devops@web03
After that, remote commands could run without asking for a password. This was the turning point. Once SSH stopped being interactive, Bash loops and remote execution became truly useful.
That also helped me understand the practical difference between a public key and a private key. The public key is placed on the remote server, and the private key stays on the control machine. If they match, authentication succeeds. It is cleaner, faster, and safer than repeatedly typing passwords.
Using a loop to target multiple servers
The next big step was creating a host file that listed all target machines. Then a loop could go through each host one by one and run commands remotely. This made the script feel like a lightweight orchestration system
for host in $(cat remote_hosts) do ssh devops@$host hostname
done
A single loop can target 3 machines, 30 machines, or even hundreds, depending on what is listed in the inventory file.
The CentOS vs Ubuntu difference
When trying package installation remotely, another useful
problem appeared. The same command did not work everywhere.
CentOS-based machines accepted yum, while Ubuntu
required apt. So the script had to detect the
operating system and decide which commands, package names, and
service names to use
That taught me an important DevOps lesson: automation is not just about sending commands remotely. It is also about making scripts smart enough to handle differences between environments.
Pushing and executing the deployment script
The final step was putting it all together. Instead of just
executing one-liner commands, the deployment script itself was
copied to each machine using scp, executed remotely
with ssh, and then cleaned up from the remote
machine afterward
scp multios_websetup.sh devops@$host:/tmp/ ssh devops@$host
"sudo /tmp/multios_websetup.sh" ssh devops@$host "rm -f
/tmp/multios_websetup.sh"
That script handled the web server setup differently depending on the OS. On CentOS, it used the right package and service names for Apache. On Ubuntu, it switched to the Ubuntu equivalents. The result was that web01, web02, and web03 all got web setup completed from a single control point
Why I think this matters
What I liked most about this section is that it shows how tools like Ansible do not come out of nowhere. Behind the nicer syntax and abstraction, the core ideas are still here: inventory, remote execution, authentication, OS-aware logic, file transfer, and command orchestration.
Learning this gave me more confidence. It made me feel that if I can understand and build this flow manually, then I will be in a much better position to learn more advanced DevOps tools the right way.