Author: marilyn_ingram

  • Day Two: Locked Out by My Own Firewall (and Two Other Fixes)

    Day Two: Locked Out by My Own Firewall (and Two Other Fixes)

    Day one was about building the lab. Day two was about keeping it running. Three things went wrong, and each one taught me something different about operating infrastructure, not just building it.

    ProblemReal causeFix
    WordPress couldn’t resize imagesA PHP library was missingAdded it to my Ansible role
    Ansible timed out on both serversMy IP changed, twiceUpdated one Terraform variable
    “Error establishing a database connection”Database server wasn’t respondingRestarted it and checked with Run Command

    1. “The web server cannot generate responsive image sizes”

    When I uploaded screenshots for my posts, WordPress showed this warning. The message suggested converting my images to PNG, but they already were PNGs. The real problem was that my server was missing GD, the PHP library WordPress uses to make smaller copies of images for phones and thumbnails.

    The fix had to be in code, not on the server. If I’d installed it by hand, the next rebuild would lose it. So I added it to my Ansible web role, along with a few other PHP extensions WordPress recommends:

    - name: Install Apache and PHP
      ansible.builtin.apt:
        name:
          - apache2
          - php
          - php-mysql
          - php-gd
          - php-xml
          - php-mbstring
          - php-curl
          - php-zip
          - php-intl
          - libapache2-mod-php
        state: present
      notify: Reload Apache

    The notify line was new too. It tells Ansible to reload Apache, but only when this task actually changes something. When I ran the playbook, the recap showed the new packages installing, followed by RUNNING HANDLER [web : Reload Apache]. Then I saved the change through a pull request, so GitHub’s checks ran on it before it reached my main branch.

    2. Locked out by my own firewall

    Next, Ansible couldn’t connect to either server:

    ssh: connect to host <server IP> port 22: Connection timed out

    Nothing was broken. My firewall only allows SSH from one IP address, mine. I had worked from a different network earlier in the day, so my IP had changed. Then I went home, and it changed back. Both times, the firewall was doing exactly what I told it to: block everyone else.

    I compared my current IP with the one in my Terraform settings, updated that one variable, and applied it:

    Plan: 0 to add, 1 to change, 0 to destroy.

    Terraform updated only the firewall rule, without touching the servers. It finished in about two seconds.

    Lesson: when I switch networks, my IP changes, so I need to update that one rule. The blog stays reachable either way, because the firewall only limits SSH, not website visitors.

    3. “Error establishing a database connection”

    Then my blog showed WordPress’s database error. Azure said both VMs were running, but I couldn’t SSH into the database server.

    I restarted it, then used Run Command, an Azure feature that runs a command inside the VM through Azure’s own agent, without needing SSH:

    az vm run-command invoke -g rg-homelab -n vm-homelab-db \
      --command-id RunShellScript \
      --scripts "uptime; systemctl is-active mysql ssh"

    It reported the server had been up for 6 minutes, with both MySQL and SSH active. After that, my blog loaded normally.

    To be honest, I’m not completely sure what the original problem was. My firewall IP was also wrong at the time, which made both problems look the same from my laptop. When I logged in afterward, memory usage was at 69% on a small VM running MySQL, so the server may have run low on memory. Adding swap space, or giving the database server a slightly bigger VM, is on my list.

    What I learned

    • Fix it in code. A fix that only lives on the server disappears on the next rebuild.
    • Check one thing at a time. Two problems at once (a wrong IP and a struggling server) can look like one.
    • Compare to something that works. When one server answered and the other didn’t, I knew it wasn’t my whole connection.
    • Have a way in that doesn’t depend on SSH. Run Command let me check the server from inside Azure.
  • Setting Up a Cloud Lab on Windows with WSL

    Setting Up a Cloud Lab on Windows with WSL

    I use a Windows laptop, and Ansible only runs on Linux. The solution is WSL (Windows Subsystem for Linux), which runs Ubuntu right inside Windows. Here’s how I set up everything for my cloud lab, including the beginner mistakes I made along the way.

    1. Install Ubuntu with WSL

    In PowerShell (run as administrator):

    wsl --install

    After a restart, Ubuntu asked me to create a username and password.

    Beginner lesson: Linux shows nothing when you type a password. No dots, no stars. I thought my keyboard wasn’t working. It was.

    2. Get files from Windows into Linux

    Windows drives show up inside Ubuntu under /mnt/c. My first copy command failed because I typed the placeholder YOURNAME literally instead of my actual Windows username. This command found the file for me:

    find /mnt/c/Users -maxdepth 3 -name homelab.zip 2>/dev/null

    Beginner lesson: anything in ALL CAPS in a tutorial command is usually a placeholder you need to replace.

    3. Install the tools

    I installed four tools: Ansible, Git, Terraform, and the Azure CLI. Then I checked each one:

    ansible --version
    git --version
    terraform -version
    az version

    My Ubuntu version was so new that some installers didn’t support it yet, so I pointed Terraform’s installer at the previous Ubuntu release, which worked fine.

    4. Sign in to Azure and GitHub

    WSL can’t open a Windows browser on its own, so for both the Azure CLI and the GitHub CLI, I copied the sign-in link or code into my browser manually.

    az login
    gh auth login

    What I’d tell another beginner

    • Read every error message. Most of them say exactly what’s wrong.
    • Replace placeholders before running commands.
    • Check each step worked before moving to the next one.
    • Closing Ubuntu keeps your files, but you’ll need to re-run any export commands next time.
  • What My First CI Pipeline Caught

    What My First CI Pipeline Caught

    After my lab was running, I put the code on GitHub and set up a CI (continuous integration) pipeline with GitHub Actions. My very first pull request failed. Here’s what happened.

    What my pipeline checks

    Every pull request automatically runs:

    • terraform fmt: is the code formatted correctly?
    • terraform validate: is the configuration valid?
    • TFLint: common Terraform mistakes
    • Checkov: a security scan of the infrastructure code
    • ansible-lint: Ansible best practices

    What failed

    All the Terraform checks passed. The last step, ansible-lint, failed with two errors like this:

    fqcn[canonical]: You should use canonical module name
    `ansible.mysql.mysql_db` instead of `community.mysql.mysql_db`.

    The MySQL modules I used have a newer official name. The linter wanted the new one.

    The decision

    The obvious fix was to rename the modules. But first I checked whether my own Ansible had the new names:

    ansible-galaxy collection list ansible.mysql

    It printed nothing. My laptop only had the older names, and that version was the one I had tested and knew worked. Renaming would have passed the linter but broken my actual deployments.

    So instead, I added the rule to my lint config as a documented exception, and kept the working code. I’ll switch to the new names once my local setup supports them.

    A second fix

    I also noticed every check ran twice: once for the push and once for the pull request. I changed the pipeline to run only on pull requests.

    The result

    All checks were successful
    0 cancelled, 0 failing, 1 successful, 1 skipped, and 0 pending checks

    What I learned

    CI didn’t just catch a problem. It made me read the log and make a real trade-off between following a rule and keeping working code.

  • Idempotency: Why My Playbook Changed Nothing the Second Time

    Idempotency: Why My Playbook Changed Nothing the Second Time

    “Idempotent” was a word I memorized for courses without really understanding it. Then I ran my Ansible playbook a second time and saw it for myself.

    What I saw

    The first run installed MySQL, Apache, PHP, and WordPress across my two servers. The second run finished with this:

    PLAY RECAP
    db   : ok=10  changed=0  unreachable=0  failed=0  skipped=0
    web  : ok=12  changed=0  unreachable=0  failed=0  skipped=1

    changed=0 on both servers. Ansible checked every task, found everything already correct, and changed nothing.

    What idempotent means

    Running the same automation twice gives the same result. The second run only changes what’s different.

    Ansible tasks describe the state you want, not steps to blindly repeat. For example, state: present means “make sure this package is installed.” If it already is, Ansible moves on.

    The “skipped” task

    One task was skipped on the second run: downloading WordPress. That task has a creates rule, which tells Ansible to skip it if the WordPress folder already exists:

    - name: Download and unpack WordPress
      ansible.builtin.unarchive:
        src: https://wordpress.org/latest.tar.gz
        dest: /var/www/
        remote_src: true
        creates: /var/www/wordpress

    Why it matters

    • It’s safe to re-run. If something goes wrong halfway through, I can just run it again.
    • It fixes drift. If someone changes a setting by hand, the next run puts it back the way the code says.
    • It scales. The same playbook works on 2 servers or 200.

    Compare that to a basic script that runs useradd client1: the second time, it fails because the user already exists. That’s the difference idempotency makes.

  • Terraform vs. Ansible: What Each One Actually Does

    Terraform vs. Ansible: What Each One Actually Does

    When I started learning infrastructure as code, I kept mixing up Terraform and Ansible. They’re both written as code, and they both deal with servers. Building my own lab is what finally made the difference click.

    The short version

    Terraform builds the house. Ansible furnishes it.

    TerraformAnsible
    Main jobCreates infrastructure (provisioning)Sets up what’s inside it (configuration)
    In my labNetwork, firewall, public IPs, 2 VMsMySQL, Apache, PHP, WordPress
    How you write itHCL: you describe the end resultYAML: a list of tasks
    How it knows what existsA state fileChecks each server every run

    Terraform: building the infrastructure

    Here’s a small piece of my Terraform. It describes the firewall rule that only allows SSH from my home IP address:

    security_rule {
      name                   = "allow-ssh-from-admin"
      priority               = 100
      direction              = "Inbound"
      access                 = "Allow"
      protocol               = "Tcp"
      destination_port_range = "22"
      source_address_prefix  = var.admin_cidr
    }

    The workflow is always the same: terraform init, then terraform plan to preview the changes, then terraform apply to make them. The plan step is the safety check, because it shows exactly what will change before anything happens.

    Ansible: configuring the servers

    Once the servers exist, Ansible connects to them over SSH and sets them up. Here’s the task that installs Apache and PHP on my web server:

    - name: Install Apache and PHP
      ansible.builtin.apt:
        name:
          - apache2
          - php
          - php-mysql
          - libapache2-mod-php
        state: present

    Ansible is agentless, so nothing has to be installed on the servers first. It just needs SSH access.

    How they hand off to each other

    This is my favorite part. When Terraform finishes, it writes Ansible’s inventory file, the list of servers and their IP addresses. So Ansible always knows the real addresses, and I never have to copy them by hand.

    [web]
    web ansible_host=<web server IP>
    
    [db]
    db ansible_host=<database server IP>

    What I learned

    If I rebuilt my original LAMP project today, Terraform would create the server, and Ansible would install Apache, MySQL, and PHP and create each client’s user and permissions. What took me hours by hand would be two commands.

  • 5 Azure Errors I Hit on Day One (and How I Fixed Them)

    5 Azure Errors I Hit on Day One (and How I Fixed Them)

    My first night building this lab, I didn’t get a single clean run. I hit five errors in a row. Each one taught me something about how Azure actually works, so here they are.

    #ErrorWhat it meantFix
    1“You don’t have access to this”My school blocks device-code sign-inUsed regular browser sign-in
    2SubscriptionNotFoundNew subscription, services not registeredRegistered resource providers
    3RequestDisallowedByAzureStudent subscription only allows some regionsMoved to North Central US
    4InvalidResourceGroupLocationLeftover resource group in the old regionDeleted it and re-ran
    5SkuNotAvailableNo capacity for that VM sizeChanged one variable

    1. My school blocked one sign-in method

    I tried signing in to the Azure CLI with az login --use-device-code. The sign-in succeeded, but then I got “You don’t have access to this.” My school’s IT blocks that sign-in method for student accounts, which is a common security policy.

    Fix: I used the regular browser sign-in instead. Because I’m on Linux inside Windows (WSL), it couldn’t open a browser on its own, so I copied the sign-in link into my browser manually. That worked.

    az login

    2. SubscriptionNotFound

    My first attempt to create a storage account failed with SubscriptionNotFound, even though my subscription clearly existed. It turns out brand-new Azure subscriptions need each service “registered” before you can use it.

    az provider register --namespace Microsoft.Storage
    az provider register --namespace Microsoft.Network
    az provider register --namespace Microsoft.Compute

    I checked the status every 30 seconds or so until all three said Registered.

    3. RequestDisallowedByAzure

    Next, Azure refused to create anything in eastus. My Azure for Students subscription has a policy that only allows certain regions. I looked it up:

    az policy assignment list --query "[].parameters" -o json

    The allowed list was North Central US, Canada Central, Sweden Central, Norway East, and France Central. I picked North Central US because it’s closest to Georgia.

    4. InvalidResourceGroupLocation

    When I re-ran my setup in the new region, it failed again: the resource group already existed in eastus from my first attempt, and a resource group can’t move regions. I deleted it and started clean.

    az group delete --name rg-homelab-tfstate --yes

    5. SkuNotAvailable

    Finally, terraform apply built 10 of my 12 resources, then both VMs failed: Azure had no capacity for the Standard_B1s size in my region.

    Fix: I changed one variable to a different small size, Standard_B2ats_v2, and ran terraform plan again. It said “Plan: 2 to add.” Terraform’s state file remembered the 10 resources it had already built, so it only created the two missing VMs. They finished in 49 seconds.

    What I learned

    Every one of these error messages told me exactly what was wrong. The skill isn’t avoiding errors. It’s reading them carefully and fixing one thing at a time.

  • How I Built This Blog with Terraform and Ansible on Azure

    How I Built This Blog with Terraform and Ansible on Azure

    Welcome to Ingram Cloud Lab! I’m Marilyn, an MIS student at Georgia College, and this blog is where I document what I’m learning about cloud infrastructure, starting with the blog itself.

    Why I built it

    In one of my Linux courses, I set up a LAMP server on AWS by hand: installing Apache, MySQL, and PHP, then creating separate users and permissions for three client websites. It worked, but it was slow and easy to get wrong. I kept thinking: what if I needed fifty servers?

    That question led me to infrastructure as code, where servers, networks, and firewalls are described in files instead of set up by clicking. I’d taken courses on Terraform and Ansible, but I wanted to use them on something real. So I built the server this blog runs on.

    What’s running this site

    • Terraform builds everything in Azure: a resource group, a virtual network and subnet, a firewall that only allows SSH from my IP, and two Ubuntu servers. Terraform’s state file is stored remotely in Azure Storage.
    • Ansible configures the servers. One runs MySQL. The other runs Apache, PHP, and WordPress, and connects to the database over the private network.
    • GitHub Actions checks every change I make, with format checks, a linter, and a security scan, before it’s merged.

    How a change gets from my laptop to Azure

    1. I change the code on a new Git branch.
    2. I open a pull request on GitHub, and the CI pipeline checks it automatically.
    3. Once the checks pass, I merge it.
    4. terraform apply builds or updates the infrastructure.
    5. ansible-playbook configures the servers.

    Right now I run the last two steps from my laptop. Connecting them to GitHub so they run automatically after I approve a change is my next project.

    A note on how I wrote the code

    I used AI to help generate the starting code, then I ran every piece myself and worked through every error. Honestly, that’s where most of the learning happened. I hit five different Azure errors on the first day, and I wrote about all of them in my next post.

    What I learned

    Terraform builds the house. Ansible furnishes it. Knowing which tool does which job made infrastructure as code finally make sense to me.

    The code for this whole site is public on GitHub: github.com/Marilyn-Ingram/homelab