<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Bernie Ops]]></title><description><![CDATA[Bernie Ops]]></description><link>https://bernieops.com</link><generator>RSS for Node</generator><lastBuildDate>Fri, 17 Apr 2026 11:46:00 GMT</lastBuildDate><atom:link href="https://bernieops.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How to Rescue Your Kubernetes Cluster with etcd Backups]]></title><description><![CDATA[How to perform a backup of the etcd datastore
To back up the cluster store, or etcd, we can create a snapshot file using the CLI tool etcdctl. This lab assumes a successful installation of the etcdctl tool and that prior knowledge of what etcd is and...]]></description><link>https://bernieops.com/how-to-rescue-your-kubernetes-cluster-with-etcd-backups</link><guid isPermaLink="true">https://bernieops.com/how-to-rescue-your-kubernetes-cluster-with-etcd-backups</guid><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Bernie Camejo]]></dc:creator><pubDate>Fri, 25 Apr 2025 10:12:02 GMT</pubDate><content:encoded><![CDATA[<h3 id="heading-how-to-perform-a-backup-of-the-etcd-datastore"><strong>How to perform a backup of the etcd datastore</strong></h3>
<p>To back up the cluster store, or <code>etcd</code>, we can create a snapshot file using the CLI tool <code>etcdctl</code>. This lab assumes a successful installation of the <code>etcdctl</code> tool and that prior knowledge of what <code>etcd</code> is and its purpose exists.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># First perform the backup with snapshot option </span>
$ etcdctl snapshot save etcd-backup --cacert /etc/kubernetes/pki/etcd/ca.crt --cert /etc/kubernetes/pki/etcd/server.crt --key /etc/kubernetes/pki/etcd/server.crt

<span class="hljs-comment"># ... Output omitted</span>
Snapshot saved at etcd-backup
</code></pre>
<p><code>--cacert</code> verifies certificates using the k8s Certificate Authority (CA) <code>--cert</code> identifies secure client using the <code>etcd</code> server certificate <code>--key</code> identifies secure client using the <code>etcd</code> key file</p>
<h3 id="heading-restoring-the-etcd-backup"><strong>Restoring the etcd backup</strong></h3>
<p>To restore the backup we use again the <code>etcdctl</code> CLI tool and the <code>snapshot</code> command. What's key in this task is that the backup will be <strong>restored to an</strong> <code>etcd</code> directory. That's why we use the <code>--data-dir</code> option with the command.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Second perform the restore operation</span>
<span class="hljs-comment"># The command here will restore the backup to the /var/lib/etcd-backup directory</span>
$ etcdctl snapshot restore etcd-backup --data-dir /var/lib/etcd-backup

<span class="hljs-comment"># ... Output omitted</span>
</code></pre>
<h3 id="heading-change-the-location-of-the-etcd-data"><strong>Change the location of the etcd data</strong></h3>
<p>Once the backup and restore operations are completed, the next step is to change the location where Kubernetes looks for the <code>etcd</code> data.</p>
<p>To do this, we need to change the YAML file for the <code>etcd.yaml</code> manifest which is located in <code>/etc/kubernetes/manifests/</code>. Why in this directory? Because any YAML placed in this directory will be scheduled by the <code>kube-scheduler</code> process.</p>
<p>The part of the file that needs to be changed is at the bottom.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">volumes:</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">hostPath:</span>
    <span class="hljs-attr">path:</span> <span class="hljs-string">/etc/kubernetes/pki/etcd</span>
    <span class="hljs-attr">type:</span> <span class="hljs-string">DirectoryOrCreate</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">etcd-certs</span>
<span class="hljs-bullet">-</span> <span class="hljs-attr">hostPath:</span>
    <span class="hljs-attr">path:</span> <span class="hljs-string">/var/lib/etcd-backup</span> <span class="hljs-comment"># &lt;--- This is the directory where we stored the snapshot</span>
    <span class="hljs-attr">type:</span> <span class="hljs-string">DirectoryOrCreate</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">etcd-data</span>
</code></pre>
<p>With that done, Kubernetes will perform the necessary actions and receive an API response from the server with the new cluster data.</p>
]]></content:encoded></item><item><title><![CDATA[ReplicaSets in Kubernetes: Core Building Blocks for Application Scaling]]></title><description><![CDATA[ReplicaSets are Kubernetes objects, like everything else in k8s. Their primary function is to ensure that a stable number of replica pods are always running the cluster in accordance with the spec in the YAML file. For this reason, they are instrumen...]]></description><link>https://bernieops.com/replicasets-in-kubernetes-core-building-blocks-for-application-scaling</link><guid isPermaLink="true">https://bernieops.com/replicasets-in-kubernetes-core-building-blocks-for-application-scaling</guid><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Bernie Camejo]]></dc:creator><pubDate>Fri, 18 Apr 2025 03:06:57 GMT</pubDate><content:encoded><![CDATA[<p>ReplicaSets are Kubernetes objects, like everything else in k8s. Their primary function is to ensure that a stable number of replica pods are always running the cluster in accordance with the spec in the YAML file. For this reason, they are instrumental in providing high availability for your application. Typically, you don't work directly with ReplicaSets but rather with Deployments which are a higher level method of ensuring the current state matches the desired state of the cluster.</p>
<h2 id="heading-obtaining-information"><strong>Obtaining information</strong></h2>
<p>The most basic method to list the number of ReplicaSets is using <code>kubectl</code>.</p>
<pre><code class="lang-bash">$ kubectl get rs
NAME              DESIRED   CURRENT   READY   AGE
new-replica-set   4         4         0       5m12s
</code></pre>
<p>Here we can see that there are 4 desired pods in the ReplicaSet spec, and there are 4 pods running. Even if we deleted one of the pods with <code>kubectl delete pod &lt;name-of-pod&gt;</code>, the ReplicaSet will ensure another pod is running to match the number of replicas, i.e., 4.</p>
<h2 id="heading-creating-a-replicaset"><strong>Creating a ReplicaSet</strong></h2>
<p>Like most objects in k8s, the easiest way to create a ReplicaSet is using a YAML file.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">apiVersion:</span> <span class="hljs-string">apps/v1</span>
<span class="hljs-attr">kind:</span> <span class="hljs-string">ReplicaSet</span>
<span class="hljs-attr">metadata:</span>
  <span class="hljs-attr">name:</span> <span class="hljs-string">replica-set-1</span>
<span class="hljs-attr">spec:</span>
  <span class="hljs-attr">replicas:</span> <span class="hljs-number">3</span>
  <span class="hljs-attr">selector:</span>
    <span class="hljs-attr">matchLabels:</span>
      <span class="hljs-attr">tier:</span> <span class="hljs-string">frontend</span>
  <span class="hljs-attr">template:</span>
    <span class="hljs-attr">metadata:</span>
      <span class="hljs-attr">labels:</span>
        <span class="hljs-attr">tier:</span> <span class="hljs-string">frontend</span>
    <span class="hljs-attr">spec:</span>
      <span class="hljs-attr">containers:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-attr">name:</span> <span class="hljs-string">nginx</span>
        <span class="hljs-attr">image:</span> <span class="hljs-string">nginx</span>
</code></pre>
<p>Once the YAML has been defined, it's very simple to create the ReplicaSet.</p>
<pre><code class="lang-bash">$ kubectl apply -f replica-set.yaml`
replicaset.apps/replicaset-1 created
</code></pre>
<h2 id="heading-scaling-a-replicaset"><strong>Scaling a ReplicaSet</strong></h2>
<p>They can be scaled up or down to fit your operational demands. The ReplicaSet controller will determined which pod(s) to delete, in case of a scale down event, or where to run a new pod in the case of a scale up event.</p>
<pre><code class="lang-bash">$ kubectl scale rs replica-set --replicas=5
</code></pre>
<p>The flag <code>--replicas=5</code> will now scale the ReplicaSet to 5 pods. This could mean you're scaling down or up to 5 pods. The command is the same, which is useful and efficient.</p>
]]></content:encoded></item><item><title><![CDATA[Kubernetes Administration 101: Basic Cluster Tasks Every Admin Should Know]]></title><description><![CDATA[Examining pods
Using kubectl we can examine the pods running in the cluster. The simplest way is to run this command.
# Simple command to examine the pods
$ kubectl get po

# To get information about the pods running in a specific namespace and showi...]]></description><link>https://bernieops.com/kubernetes-administration-101-basic-cluster-tasks-every-admin-should-know</link><guid isPermaLink="true">https://bernieops.com/kubernetes-administration-101-basic-cluster-tasks-every-admin-should-know</guid><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Bernie Camejo]]></dc:creator><pubDate>Wed, 16 Apr 2025 11:30:03 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-examining-pods"><strong>Examining pods</strong></h2>
<p>Using <code>kubectl</code> we can examine the pods running in the cluster. The simplest way is to run this command.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Simple command to examine the pods</span>
$ kubectl get po

<span class="hljs-comment"># To get information about the pods running in a specific namespace and showing their IP addresses</span>
<span class="hljs-comment"># in this example the kube-system namespace</span>
$ kubectl get po -n kube-system -o wide
</code></pre>
<p>By using the <code>-o wide</code> option we can get additional information, including a column specifying the IP addresses.</p>
<h2 id="heading-performing-a-backup-of-etcd"><strong>Performing a backup of etcd</strong></h2>
<p>The datastore etcd, also known as the cluster store, has all the stateful configuration information about our cluster. One common operation is to back up the etcd datastore using a CLI utility called <code>etcdctl</code>.</p>
<h3 id="heading-pre-requisite"><strong>Pre-requisite</strong></h3>
<ul>
<li>Have <code>etcdctl</code> installed</li>
</ul>
<pre><code class="lang-bash">$ sudo apt update; apt install -y etcd-client
<span class="hljs-comment"># Following that we can perform a snapshot backup of the etcd datastore</span>
$ etcdctl snapshot save snapshotdb --cacert /etc/kubernetes/pki/etcd/ca.crt --cert /etc/kubernetes/pki/etcd/server.crt --key /etc/kubernetes/pki/server.key
<span class="hljs-comment"># ... output omitted</span>
Snapshot saved at snapshotdb
</code></pre>
<p>The above command creates a snapshot in the current directory called <code>snapshotdb</code>. To check the status of the snapshot you can use <code>$ etcdctl snapshot status snapshotdb --write-out=table</code>. This will generate a table like below</p>
<pre><code class="lang-bash">+----------+----------+------------+------------+
|   HASH   | REVISION | TOTAL KEYS | TOTAL SIZE |
+----------+----------+------------+------------+
| e4f4157c |   102366 |        817 |     2.2 MB |
+----------+----------+------------+------------+
</code></pre>
<h2 id="heading-restoring-etcd-from-snapshot"><strong>Restoring etcd from snapshot</strong></h2>
<p>If needed, we can use etcdctl to also restore the backed up version of the datastore. In order to perform the restore operation, we need to specify a directory already accessed by etcd. By checking the <code>etcd.yaml</code> file in the <code>/etc/kubernetes/manifests</code> directory, we can change that directory to something like <code>/var/lib/etcd-restore</code>.</p>
<pre><code class="lang-bash">$ etcdctl snapshot restore &lt;snapshot-name&gt; --data-dir /var/lib/etcd-restore
</code></pre>
<h2 id="heading-upgrading-kubernetes-version"><strong>Upgrading Kubernetes version</strong></h2>
<p>The last task we will perform in this lab is upgrading the version of <code>kubeadm</code>. For example, if in the exam there's a question about a company needing to upgrade the Kubernetes controller to version X, then we know that <code>kubeadm</code> is the tool to use. The first thing to do is to view the version of our control plane components.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># This will show a list of control plane components and their current version</span>
<span class="hljs-comment"># as well as the version to which we can upgrade</span>
$ kubeadm upgrade plan

COMPONENT                 NODE                      CURRENT    TARGET
kube-apiserver            acing-cka-control-plane   v1.32.2    v1.32.3
kube-controller-manager   acing-cka-control-plane   v1.32.2    v1.32.3
kube-scheduler            acing-cka-control-plane   v1.32.2    v1.32.3
kube-proxy                                          1.32.2     v1.32.3
CoreDNS                                             v1.11.3    v1.11.3
etcd                      acing-cka-control-plane   3.5.16-0   3.5.16-0
</code></pre>
<p>After running the command above you will get the command needed to upgrade, as output. <code>$ kubeadm upgrade apply v1.32.3</code></p>
]]></content:encoded></item><item><title><![CDATA[How to create Kubernetes YAML files the smart way with kubectl]]></title><description><![CDATA[To save time, and ensure the syntax and formatting are correct, a more efficient way to create a YAML file with the specs for a pod is to use kubectl and the --dry-run flag.
$ kubectl run pod --image nginx --dry-run=client -o yaml > nginx-pod.yaml

T...]]></description><link>https://bernieops.com/how-to-create-kubernetes-yaml-files-the-smart-way-with-kubectl</link><guid isPermaLink="true">https://bernieops.com/how-to-create-kubernetes-yaml-files-the-smart-way-with-kubectl</guid><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Bernie Camejo]]></dc:creator><pubDate>Mon, 14 Apr 2025 10:01:20 GMT</pubDate><content:encoded><![CDATA[<p>To save time, and ensure the syntax and formatting are correct, a more efficient way to create a YAML file with the specs for a pod is to use <code>kubectl</code> and the <code>--dry-run</code> flag.</p>
<pre><code class="lang-bash">$ kubectl run pod --image nginx --dry-run=client -o yaml &gt; nginx-pod.yaml
</code></pre>
<p>This will let <code>kubectl</code> write the YAML file. The main benefits of doing it this way are:</p>
<ol>
<li><p>You can modify the YAML template <em>before</em> having to deploy it to the Kubernetes cluster.</p>
</li>
<li><p>You can modify the template to add volumes, env variables and other configurations.</p>
</li>
<li><p>The formatting of the YAML file will be correct and you reduce the chances of syntax error.</p>
</li>
</ol>
<p>The <code>-o yaml</code> flag tells <code>kubectl</code> to output the definition in YAML format.</p>
]]></content:encoded></item><item><title><![CDATA[Building a Kubernetes Cluster from Scratch: Part 2 - Installing Container Runtime and Kubernetes Components]]></title><description><![CDATA[This is the second part of my Kubernetes installation series using kubeadm. In Part 1, I covered the environment preparation, including hardware requirements and network configuration, as well as required Linux kernel modules.
In this Part 2, I'll co...]]></description><link>https://bernieops.com/building-a-kubernetes-cluster-from-scratch-part-2-installing-container-runtime-and-kubernetes-components</link><guid isPermaLink="true">https://bernieops.com/building-a-kubernetes-cluster-from-scratch-part-2-installing-container-runtime-and-kubernetes-components</guid><category><![CDATA[Kubernetes]]></category><dc:creator><![CDATA[Bernie Camejo]]></dc:creator><pubDate>Thu, 10 Apr 2025 11:07:55 GMT</pubDate><content:encoded><![CDATA[<p>This is the second part of my Kubernetes installation series using <code>kubeadm</code>. In <a target="_blank" href="https://bernieops.com/building-a-kubernetes-cluster-from-scratch-part-1-environment-setup">Part 1,</a> I covered the environment preparation, including hardware requirements and network configuration, as well as required Linux kernel modules.</p>
<p>In this Part 2, I'll continue by installing and configuring <code>containerd</code> as my container runtime and the installation of the essential Kubernetes componets like kubeadm, kubelet, and kubectl.</p>
<h2 id="heading-installing-the-container-runtime">Installing the container runtime</h2>
<p>Kubernetes is a container orchestration system, and as such it needs a <strong>container runtime</strong> responsible for running the containers in the cluster. The container runtime pulls the images, starts/stops the containers and report container status back to Kubernetes.</p>
<p>Before installing the container runtime, we need to download the GPG keys to ensure the packages haven't been tampered with, and <code>apt</code> will need this information later as well.</p>
<pre><code class="lang-bash">root@cp:~<span class="hljs-comment"># curl -fsSL https://download.docker.com/linux/ubuntu/gpg \</span>
| gpg --dearmor -o /etc/apt/keyrings/docker.gpg
root@cp:~<span class="hljs-comment">#</span>
root@cp:~<span class="hljs-comment"># echo \</span>
<span class="hljs-string">"deb [arch=<span class="hljs-subst">$(dpkg --print-architecture)</span> signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu \
<span class="hljs-subst">$(lsb_release -cs)</span> stable"</span> | tee /etc/apt/sources.list.d/docker.list &gt; /dev/null
</code></pre>
<p>Now we install containerd</p>
<pre><code class="lang-bash">root@cp:~<span class="hljs-comment"># apt-get update &amp;&amp; apt-get install containerd.io -y</span>
root@cp:~<span class="hljs-comment"># containerd config default | tee /etc/containerd/config.toml</span>
root@cp:~<span class="hljs-comment"># systemctl restart containerd</span>
</code></pre>
<h2 id="heading-installing-the-kubernetes-software">Installing the Kubernetes software</h2>
<p>First, like with containerd, we need to download the public key for the package repositories.</p>
<pre><code class="lang-bash">root@cp:~<span class="hljs-comment"># curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.31/deb/Release.key \</span>
| sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg

<span class="hljs-comment"># Add the appropriate k8s repository</span>
root@cp:~<span class="hljs-comment"># echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] \</span>
https://pkgs.k8s.io/core:/stable:/v1.31/deb/ /<span class="hljs-string">" \
| tee /etc/apt/sources.list.d/kubernetes.list</span>
</code></pre>
<p>Finally, update the repo and install the Kubernetes software</p>
<pre><code class="lang-bash">root@cp:~<span class="hljs-comment"># apt-get update</span>
root@cp:~<span class="hljs-comment"># apt-get install -y kubeadm=1.31.1-1.1 kubelet=1.31.1-1.1 kubectl=1.31.1-1.1</span>
</code></pre>
<p>In the next part of this series, I will set the local DNS alias for my <code>cp</code> Kubernetes server and assign it with the name <code>k8scp</code>. Then I will create a configuration file for the cluster.</p>
]]></content:encoded></item><item><title><![CDATA[Building a Kubernetes Cluster from Scratch - Part 1: Environment Setup]]></title><description><![CDATA[There are multiple tools to install Kubernetes. A community supported tool is kubeadm and this lab uses it to install and build a Kubernetes cluster.
Connecting to future control plane and worker nodes
I'm using AWS to launch two instances that will ...]]></description><link>https://bernieops.com/building-a-kubernetes-cluster-from-scratch-part-1-environment-setup</link><guid isPermaLink="true">https://bernieops.com/building-a-kubernetes-cluster-from-scratch-part-1-environment-setup</guid><category><![CDATA[Kubernetes]]></category><category><![CDATA[Linux]]></category><dc:creator><![CDATA[Bernie Camejo]]></dc:creator><pubDate>Tue, 08 Apr 2025 11:36:10 GMT</pubDate><content:encoded><![CDATA[<p>There are multiple tools to install Kubernetes. A community supported tool is <code>kubeadm</code> and this lab uses it to install and build a Kubernetes cluster.</p>
<h2 id="heading-connecting-to-future-control-plane-and-worker-nodes">Connecting to future control plane and worker nodes</h2>
<p>I'm using AWS to launch two instances that will be my Kubernetes cluster. Both instances are Ubuntu 24.04 with 2vCPUs and 8GB of RAM. Using my AWS key pair, the next step is to connect to both instances using <code>ssh</code>.</p>
<h3 id="heading-installing-kubernetes">Installing Kubernetes</h3>
<p>The first step is to connect to the first EC2 instance to update and upgrade the control plane or <strong>cp</strong>.</p>
<pre><code class="lang-bash">$ ssh -i /path/to/my-key.pem ubuntu@instance-ip
$ sudo -i
root@cp:~<span class="hljs-comment"># apt update &amp;&amp; apt upgrade -y</span>
</code></pre>
<p>With the system updated, the next step is to install a <strong>container runtime</strong>. A commonly used option, easy to deploy and lightweight is <code>containerd</code>. Also, Kubernetes has deprecated Docker engine as of 2020, and <code>containerd</code> offers simplicity and focus as it doesn't include the additional stuff that Docker includes. This is purely a container runtime which supports the Container Runtime Interface or CRI as expected by Kubernetes.</p>
<p>We need to install some dependencies.</p>
<pre><code class="lang-bash">root@cp:~<span class="hljs-comment"># apt install apt-transport-https software-properties-common ca-certificates socat -y</span>
</code></pre>
<p>Next, two kernel modules need to be loaded due to Kubernetes infrastructure requirements. They enable core container and networking functionality that Kubernetes needs. The <code>overlay</code> module relates to storage management and the <code>br_netfilter</code> module relates to networking and packet routing.</p>
<pre><code class="lang-bash">root@cp:˜<span class="hljs-comment"># modprobe overlay</span>
root@cp:˜<span class="hljs-comment"># modprobe br_netfilter</span>
</code></pre>
<p>Finally, we need to tweak the network routing and policies as Kubernetes relies on <code>iptables</code> rules. Without creating the rules below, network packets may bypass <code>iptables</code> rules and inter-pod communication <strong>would fail</strong>. The below command creates a configuration file <code>kubernetes.conf</code> at <code>/etc/sysctl.d</code> and it's loaded when the system starts. The two lines below, starting with <code>net.bridge…</code> ensure that IPv4 and IPv6 packets follow <code>iptables</code> rules. Finally, <code>ip_forward</code> enables IP forwarding in the Linux kernel.</p>
<pre><code class="lang-bash">root@cp:˜<span class="hljs-comment"># cat &lt;&lt; EOF | tee /etc/sysctl.d/kubernetes.conf</span>
&gt; net.bridge.bridge-nf-call-ip6tables = 1
&gt; net.bridge.bridge-nf-call-iptables = 1
&gt; net.ipv4.ip_forward = 1
EOF
</code></pre>
<p>Once those rules have been added, we ensure the changes are loaded by the current kernel.</p>
<pre><code class="lang-bash">root@cp:~<span class="hljs-comment"># sysctl --system</span>
</code></pre>
<p>That's part 1 on this series. The next part will deal with the installation of the <code>containerd</code> runtime and the Kubernetes software itself.</p>
]]></content:encoded></item><item><title><![CDATA[Beyond Docker: Setting Up and Managing Linux Containers with LXC]]></title><description><![CDATA[Linux Containers, or LXC, is an interface for Linux kernel vritualisation. You can create Linux containers that enable persistence, and system-level functionality. When comparing LXC to Docker, they both serve different use cases. Docker is more suit...]]></description><link>https://bernieops.com/beyond-docker-setting-up-and-managing-linux-containers-with-lxc</link><guid isPermaLink="true">https://bernieops.com/beyond-docker-setting-up-and-managing-linux-containers-with-lxc</guid><category><![CDATA[Linux]]></category><category><![CDATA[Docker]]></category><dc:creator><![CDATA[Bernie Camejo]]></dc:creator><pubDate>Sat, 05 Apr 2025 11:13:33 GMT</pubDate><content:encoded><![CDATA[<p>Linux Containers, or LXC, is an interface for Linux kernel vritualisation. You can create Linux containers that enable persistence, and system-level functionality. When comparing LXC to Docker, they both serve different use cases. Docker is more suitable for <strong>application containerisation</strong> whereas LXC is more relevant for <strong>system-level functionality</strong> and also <strong>persistent environments</strong>.</p>
<h2 id="heading-installing-lxc">Installing LXC</h2>
<pre><code class="lang-bash">$ sudo apt update
$ sudo apt install -y lxc
</code></pre>
<p>Since we need to create an <strong>unprivileged container</strong> (for security reasons), the user that will be attached to this container needs to have permissions to create network devices.</p>
<pre><code class="lang-bash">$ sudo bash -c <span class="hljs-string">'echo &lt;username&gt; veth lxcbr0 10 &gt;&gt; /etc/lxc/lxc-usernet'</span>
$ cat /etc/lxc/lxc-usernet

<span class="hljs-comment"># USERNAME TYPE BRIDGE COUNT</span>
&lt;username&gt; veth lxcbr0 10
</code></pre>
<h2 id="heading-setting-up-the-config-file">Setting up the config file</h2>
<p>The configuration file for <code>lxc</code> may not exist. So create the directory inside the <code>.config</code> directory and copy the <code>default.conf</code> located in <code>/etc/lxc/</code>.</p>
<pre><code class="lang-bash">$ mkdir -p ~/.config/lxc
$ cp /etc/lxc/default.conf ~/.config/lxc/default.conf
$ chmod 664 ~/.config/lxc/default.conf
</code></pre>
<p>The configuration file has to be updated with the UID and GID of the unprivileged user. They can both be extracted from the <code>/etc/subuid</code> and <code>/etc/subgid</code> files.</p>
<pre><code class="lang-bash">$ cat /etc/subuid
ubuntu:100000:65536
&lt;username&gt;:165536:65536

$ cat /etc/subgid
ubuntu:100000:65536
&lt;username&gt;:165536:65536

$ <span class="hljs-built_in">echo</span> lxc.idmap = u 0 165536 65536 &gt;&gt; ~/.config/lxc/default.conf
$ <span class="hljs-built_in">echo</span> lxc.idmap = g 0 165536 65536 &gt;&gt; ~/.config/lxc/default.conf

$ cat ~/.config/lxc/default.conf
lxc.net.0.type = veth
lxc.net.0.link = lxcbr0
lxc.net.0.flags = up
lxc.net.0.hwaddr = 00:16:3e:xx:xx:xx
lxc.idmap = u 0 165536 65536
lxc.idmap = g 0 165536 65536
</code></pre>
<h2 id="heading-setting-access-control-list">Setting access control list</h2>
<p>To prevent possible permission errors, we need to set up an access control list on our <code>.local</code> directory.</p>
<pre><code class="lang-bash">$ sudo apt update
$ sudo apt install -y acl
$ setfacl -R -m u:165536:x ~/.<span class="hljs-built_in">local</span>
</code></pre>
<p>This command sets an ACL (Access Control List) for a specific user (UID 165536), recursively on the ~/.local directory, giving that user execute (x) permission.</p>
<h2 id="heading-creating-an-unprivileged-container">Creating an unprivileged container</h2>
<p>Once the setup is complete, we can create a container using the <code>download</code> template. This gives us all available images designed to work <strong>without privileges</strong>.</p>
<pre><code class="lang-bash">$ lxc-create --template download --name unpriv-cont-user
</code></pre>
<p>Once the image index is downloaded, the CLI tool will display the images and await for the user to provide the distro, release and architecture required. In this case, <strong>ubuntu</strong>, <strong>jammy</strong> and <strong>amd64</strong> will be used.</p>
<pre><code class="lang-bash">---

Distribution:
ubuntu
Release:
jammy
Architecture:
amd64

Downloading the image index
Downloading the rootfs
Downloading the metadata
The image cache is now ready
Unpacking the rootfs

---
You just created an Ubuntu jammy amd64 (20250404_20:34) container.
</code></pre>
<h2 id="heading-starting-the-container">Starting the container</h2>
<p>Now the container has been created, we can start it.</p>
<pre><code class="lang-bash">$ lxc-start -n unpriv-cont-user -d
</code></pre>
<p>With the container running we can interact with its environment.</p>
<pre><code class="lang-bash">$ lxc-attach -n unpriv-cont-user
<span class="hljs-comment"># hostname</span>
unpriv-cont-user
<span class="hljs-comment"># exit</span>
$ lxc-stop -n unpriv-cont-user
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Getting Started with Linux cgroups: A Practical Guide]]></title><description><![CDATA[What are cgroups?
They're a Linux kernel feature that allows the allocation, limiting and prioritisation of system resources across processes. They are a foundation for container technologies like Docker and Kubernetes.
The first thing is to install ...]]></description><link>https://bernieops.com/getting-started-with-linux-cgroups-a-practical-guide</link><guid isPermaLink="true">https://bernieops.com/getting-started-with-linux-cgroups-a-practical-guide</guid><category><![CDATA[Linux]]></category><category><![CDATA[Docker]]></category><category><![CDATA[containers]]></category><dc:creator><![CDATA[Bernie Camejo]]></dc:creator><pubDate>Fri, 21 Mar 2025 10:40:30 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-what-are-cgroups">What are <code>cgroups</code>?</h2>
<p>They're a Linux kernel feature that allows the allocation, limiting and prioritisation of system resources across processes. They are a foundation for container technologies like Docker and Kubernetes.</p>
<p>The first thing is to install <code>cgroup-tools</code>. This package is a collection of command-line utilities for managing and interacting with Linux control groups or <code>cgroups</code>.</p>
<pre><code class="lang-bash">$ sudo apt update
$ sudo apt install -y cgroup-tools
</code></pre>
<p>To test things are working we can run a command like <code>lscgroup</code> which lists all the current <code>cgroups</code> configured on the system. The output is in the format</p>
<pre><code class="lang-bash">controler:path
</code></pre>
<p>Where the cgroup controller, or subsystem, is before the colon, e.g., cpu, memory, and the path in the cgroup hierarchy after the colon. We can also list cgroups by process ID or PID. For example:</p>
<pre><code class="lang-bash">$ sudo cat /proc/1/cgroup
</code></pre>
<p>This lists the cgroups associated with processes with PID 1, or the first process started at boot.</p>
<pre><code class="lang-bash">bernie@ubuntu:~$ sudo cat /proc/1/cgroup
13:memory:/init.scope
12:devices:/init.scope
11:freezer:/
10:cpuset:/
9:rdma:/
8:perf_event:/
7:pids:/init.scope
6:cpu,cpuacct:/init.scope
5:blkio:/init.scope
4:hugetlb:/
3:misc:/
2:net_cls,net_prio:/
1:name=systemd:/init.scope
0::/init.scope
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Secure Cloud Computing: How to Deploy and Connect to a Google Cloud VM]]></title><description><![CDATA[Pre-requisites

Have a Google Cloud (GCP) account

Some knowledge of SSH and how it works


Step 1: Create SSH keys
We will connect to a GCP virtual server using SSH to ensure secure communication between our local environment and the remote virtual ...]]></description><link>https://bernieops.com/secure-cloud-computing-how-to-deploy-and-connect-to-a-google-cloud-vm</link><guid isPermaLink="true">https://bernieops.com/secure-cloud-computing-how-to-deploy-and-connect-to-a-google-cloud-vm</guid><category><![CDATA[Google]]></category><category><![CDATA[Google Cloud Platform]]></category><dc:creator><![CDATA[Bernie Camejo]]></dc:creator><pubDate>Sat, 15 Mar 2025 11:54:43 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-pre-requisites">Pre-requisites</h2>
<ol>
<li><p>Have a Google Cloud (GCP) account</p>
</li>
<li><p>Some knowledge of SSH and how it works</p>
</li>
</ol>
<h2 id="heading-step-1-create-ssh-keys">Step 1: Create SSH keys</h2>
<p>We will connect to a GCP virtual server using SSH to ensure secure communication between our local environment and the remote virtual server.</p>
<pre><code class="lang-bash">$ ssh-keygen -t ed25519 -C <span class="hljs-string">"containers"</span>
</code></pre>
<p>This command will generate an <strong>SSH key pair</strong>. To learn more, visit <a target="_blank" href="https://www.ssh.com/academy/ssh/keygen">this page</a>.</p>
<h2 id="heading-step-2-create-a-new-gcp-project">Step 2: Create a new GCP project</h2>
<p>On the console, create a new project to create the Google Compute Engine (GCE) virtual server, that we will connect to later.</p>
<h2 id="heading-step-3-configure-a-virtual-network">Step 3: Configure a virtual network</h2>
<p>We need to create a Google VPC network. From the console go to the VPC networks page, and click <code>Create VPC network</code>. Name the VPC something like "container-vpc", and select the <strong>Automatic</strong> subnet creation mode, which only supports IPv4. Don't worry about firewall rules (later step), and click <code>Create</code>.</p>
<h2 id="heading-step-4-create-a-firewall">Step 4: Create a firewall</h2>
<p>Once the virtual network has been created, a firewall rule needs to be created and configured to allow inbound SSH traffic.<br />In our case, we can create a firewall rule named <code>allow-inbound-ssh-traffic</code> which allows traffic from anywhere using <code>0.0.0.0/0</code> as the IP range, and <code>ssh</code> as the allowed protocol.</p>
<h2 id="heading-step-5-start-a-new-vm-instance">Step 5: Start a new VM instance</h2>
<p>When creating a VM on Google Cloud, choosing E2 instances is recommended for standard workloads like web servers, small-to-medium databases, development environments, and microservices that don't require specific hardware features.<br />In terms of settings, it's important to attach the network created in step 3 to the new instance. For this lab, we have created a Ubuntu LTS 20.04 server, and configured the SSH key created in step 1. Once the settings are finished, we start the instance.</p>
<h2 id="heading-step-6-test-connectivity">Step 6: Test connectivity</h2>
<p>We could connect to the new instance using Google's <code>SSH-in-browser</code> feature. However, this in-browser temrinal may not offer all the features we would use when remote managing a VM server.</p>
<pre><code class="lang-bash">$ ssh &lt;username&gt;@&lt;public_ip&gt;
The authenticity of host <span class="hljs-string">'xx.xx.xxx.xx'</span> can<span class="hljs-string">'t be
established.
ECDSA key fingerprint is
SHA256: &lt;some text&gt;
Are you sure you want to continue connecting (yes/no/[fingerprint])?
$ yes</span>
</code></pre>
<p>This last step should connect us to our remote cloud-hosted Ubuntu server.</p>
<pre><code class="lang-bash">user@ubuntu:~$
</code></pre>
]]></content:encoded></item><item><title><![CDATA[Mastering CloudFormation: Hands-on Detection and Remediation of Infrastructure Drift]]></title><description><![CDATA[Overview
This is a demo of how to create a stack using AWS CloudFormation, detect drift in the stack, and perform a stack update. The demo task is creating an environment for a dev team, who asked for an Apache server with HTTP access. The stack cons...]]></description><link>https://bernieops.com/mastering-cloudformation-hands-on-detection-and-remediation-of-infrastructure-drift</link><guid isPermaLink="true">https://bernieops.com/mastering-cloudformation-hands-on-detection-and-remediation-of-infrastructure-drift</guid><category><![CDATA[AWS]]></category><category><![CDATA[#IaC]]></category><category><![CDATA[cloudformation]]></category><dc:creator><![CDATA[Bernie Camejo]]></dc:creator><pubDate>Sat, 08 Mar 2025 07:57:29 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-overview">Overview</h2>
<p>This is a demo of how to create a stack using AWS CloudFormation, detect drift in the stack, and perform a stack update. The demo task is creating an environment for a dev team, who asked for an Apache server with HTTP access. The stack consists of the following:</p>
<ul>
<li><p>A dedicated VPC</p>
</li>
<li><p>Single public subnet</p>
</li>
<li><p>Amazon EC2 instance</p>
</li>
</ul>
<h2 id="heading-pre-requisites">Pre-requisites</h2>
<ul>
<li><p>Have AWS CLI installed</p>
</li>
<li><p>Have an AWS account</p>
</li>
</ul>
<pre><code class="lang-dockerfile">$ aws --version
$ aws-cli/<span class="hljs-number">2.17</span>.<span class="hljs-number">18</span> Python/<span class="hljs-number">3.9</span>.<span class="hljs-number">20</span> ...
</code></pre>
<h2 id="heading-details">Details</h2>
<h3 id="heading-step-1-create-the-required-parameters">Step 1: Create the required parameters</h3>
<p>Parameters are reusable inputs that allow flexibility when building IaC templates. They are popular to specify property values of stack resources.</p>
<pre><code class="lang-dockerfile">InstanceType:
  Description: Webserver EC2 instance type
  Type: String
  Default: t2.nano
  AllowedValues:
    - t2.nano
    - t2.micro
    - t2.small
  ConstraintDescription: must be a valid EC2 instance type.
</code></pre>
<p>The <code>ConstraintDescription</code> is interesting because it provides the user with details when a constraint is violated. In this case, if a user tries to create an invalid EC2 instance type, then they get a useful error message.</p>
<h3 id="heading-step-2-adding-resources-to-the-cloudformation-template">Step 2: Adding resources to the CloudFormation template</h3>
<p>Here the AWS resources that CloudFormation will provision are declared. Below is a demonstration of how to specify a route in a route table. The <code>Type</code> of this resource is <code>AWS::EC2::Route</code>.</p>
<pre><code class="lang-dockerfile">Route:
  Type: <span class="hljs-string">'AWS::EC2::Route'</span>
  DependsOn:
    - VPC
    - AttachGateway
  Properties:
    RouteTableId: !Ref RouteTable
    DestinationCidrBlock: <span class="hljs-number">0.0</span>.<span class="hljs-number">0.0</span>/<span class="hljs-number">0</span>
    GatewayId: !Ref InternetGateway
</code></pre>
<p>Note how the <code>Route</code> resource refers to other resources with the <code>!Ref</code> function. In this case, both <code>RouteTable</code>and <code>InternetGateway</code>.</p>
<h3 id="heading-step-3-adding-an-output">Step 3: Adding an output</h3>
<p>Outputs are mainly used to capture important details about the resources in the stack, as they allow a convenient way to store the information in a separate file, or simply make later reference easier using the <code>aws cli</code> utility.</p>
<pre><code class="lang-dockerfile">Outputs:
  AppURL:
    Description: New created application URL
    Value: !Sub <span class="hljs-string">'http://${WebServerInstance.PublicIp}'</span>
</code></pre>
<p>In this case, we are using <code>!Sub</code> to dynamically insert the public IP of the EC2 instance, and get a full HTTP URL when the stack is provisioned. In other words, using the <code>!Sub</code> function will allow us to retrieve the URL to access the web server.</p>
<h3 id="heading-step-4-create-the-stack">Step 4: Create the stack</h3>
<p>Once the <code>YAML</code> file is finished, it's simple to create the stack.</p>
<pre><code class="lang-dockerfile">$ aws cloudformation create-stack --stack-name Demo_Web_Server --parameters ParameterKey=InstanceType,ParameterValue=t2.micro --template-body file://cf_stack.yaml
$ {
    <span class="hljs-string">"StackId"</span>: <span class="hljs-string">"arn:aws:cloudformation:....."</span>
}
</code></pre>
<p>It might be useful to query the status of the stack process with the following command.</p>
<pre><code class="lang-dockerfile">$ aws cloudformation describe-stacks --stack-name Demo_Web_Server --query <span class="hljs-string">"Stacks[0].StackStatus"</span>
$ <span class="hljs-string">"CREATE_COMPLETE"</span> <span class="hljs-comment"># This is the desired output</span>
</code></pre>
<h4 id="heading-stack-creation-complete"><strong>Stack creation complete</strong></h4>
<p><a target="_blank" href="https://github.com/bernie-cm/cloudformation_lab/blob/main/assets/20250308_cloudformation_stack_created.png"><img src="https://github.com/bernie-cm/cloudformation_lab/raw/main/assets/20250308_cloudformation_stack_created.png" alt="Link" /></a></p>
<h4 id="heading-using-outputs-during-stack-creation"><strong>Using outputs during stack creation</strong></h4>
<p><a target="_blank" href="https://github.com/bernie-cm/cloudformation_lab/blob/main/assets/20250308_cloudformation_outputs.png"><img src="https://github.com/bernie-cm/cloudformation_lab/raw/main/assets/20250308_cloudformation_outputs.png" alt="Link" /></a></p>
<h3 id="heading-step-5-testing-drift-detection-in-a-cloudformation-stack">Step 5: Testing drift detection in a CloudFormation stack</h3>
<p>CloudFormation is powerful because it can be used to detect stack changes <strong>not initiated within CloudFormation</strong>. In other words, if someone were to make changes to the stack using the AWS Console, CloudFormation can be used to detect and rectify those changes. First, it's necessary to run the 'Detect Drift' stack action, and once that's run, select 'View drift results'.</p>
<h4 id="heading-using-stack-actions-to-detect-drift">Using Stack actions to detect drift</h4>
<p><a target="_blank" href="https://github.com/bernie-cm/cloudformation_lab/blob/main/assets/20250308_detect_drift.png"><img src="https://github.com/bernie-cm/cloudformation_lab/raw/main/assets/20250308_detect_drift.png" alt="Link" /></a></p>
<h4 id="heading-drift-detection-report">Drift detection report</h4>
<p><a target="_blank" href="https://github.com/bernie-cm/cloudformation_lab/blob/main/assets/20250308_drift_detection_report.png"><img src="https://github.com/bernie-cm/cloudformation_lab/raw/main/assets/20250308_drift_detection_report.png" alt="Link" /></a></p>
<p>You can also detect drift via the CLI.</p>
<pre><code class="lang-dockerfile">$ aws cloudformation describe-stack-resource-drifts ...
</code></pre>
<h3 id="heading-step-6-rectify-the-stack-using-a-change-set">Step 6: Rectify the stack using a change set</h3>
<p>Once drift has been detected, the resource can be modified to the expected value in the CloudFormation template. To do this, we can use a Change Set, and then implement the changes to the environment.</p>
<pre><code class="lang-dockerfile">$ aws cloudformation create-change-set --stack-name Demo_WebServer --change-set-name Demo_Change_set --parameters ParameterKey=InstanceType,ParameterValue=t2.micro, --template-body file://cf_stack-CS.yaml
</code></pre>
<p>Once the Change Set has been created, it will appear under the original stack. The next step would be to <strong>Execute change set</strong> and if the rollback is successful the Change Set is no longer available.</p>
]]></content:encoded></item><item><title><![CDATA[When Ansible Can't See Your EC2 Instances: Resolving AWS Dynamic Inventory Issues]]></title><description><![CDATA[AWS Dynamic Inventory is best practice when using Ansible to configure AWS infrastructure. It allows you to perform automatic discovery of EC2 instances because it'd be difficult to maintain a static inventory of hosts as AWS changes IPs dynamically....]]></description><link>https://bernieops.com/when-ansible-cant-see-your-ec2-instances-resolving-aws-dynamic-inventory-issues</link><guid isPermaLink="true">https://bernieops.com/when-ansible-cant-see-your-ec2-instances-resolving-aws-dynamic-inventory-issues</guid><category><![CDATA[ansible]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[AWS]]></category><dc:creator><![CDATA[Bernie Camejo]]></dc:creator><pubDate>Wed, 26 Feb 2025 19:39:51 GMT</pubDate><content:encoded><![CDATA[<p>AWS Dynamic Inventory is best practice when using Ansible to configure AWS infrastructure. It allows you to perform automatic discovery of EC2 instances because it'd be difficult to maintain a static inventory of <code>hosts</code> as AWS changes IPs dynamically. It's in fact <a target="_blank" href="https://docs.ansible.com/ansible/latest/collections/amazon/aws/docsite/aws_ec2_guide.html">recommended by Ansible</a> when working with cloud providers.</p>
<p>The workflow for working with Dynamic Inventory is you first provision your infrastructure using IaC (e.g., Terraform). Then you configure the AWS Dynamic Inventory plugin <code>aws_ec2</code> within the Ansible <code>ansible.cfg</code> file. I ran into problems when testing connectivity between my control node and my AWS infrastructure using Ansible. Here's how I worked through that problem.</p>
<pre><code class="lang-dockerfile"><span class="hljs-comment"># Listing all hosts Ansible can see</span>
$ ansible-inventory --list

<span class="hljs-comment"># Verifying I have connectivity</span>
$ ansible all -m ping
</code></pre>
<p>When running the list flag for my inventory, I was getting an empty dictionary <code>hostvars: {}</code> so right away I knew there was an issue. And this was confirmed when I ran the <code>ping</code> command, because I received an error that only <code>localhost</code> was available. Using AWS CLI to further investigate the problem, I confirmed that my EC2 instances did exist and had the right tags.</p>
<pre><code class="lang-dockerfile">aws ec2 describe-instances --filters <span class="hljs-string">"Name=tag:Environment,Values=development"</span>
</code></pre>
<p>I then investigated my Terraform <code>main.tf</code> file and found the problem. The tag I was using in my Terraform file was <code>Name = DevOpsInstance</code> but the tag I was using in my Ansible <code>aws_ec2.yml</code> dynamic inventory file was <code>Environment = Development</code>. This is what caused the issue, a single tag that had mismatching names and values.</p>
<p>Once the two files contained the same information, I ran the <code>ansible</code> CLI commands again and was able to list my AWS infrastructure. Attention to detail is crucial when working across different tools, and managing large code bases.</p>
]]></content:encoded></item><item><title><![CDATA[Hands-on Infrastructure as Code: AWS Deployment with Terraform]]></title><description><![CDATA[I've been building a DevOps automation project on my GitHub page to showcase how Terraform, Ansible and Docker can be used together to quickly deploy, and automate the configuration of assets using Infrastructure as Code (IaC).
Set up local environme...]]></description><link>https://bernieops.com/hands-on-infrastructure-as-code-aws-deployment-with-terraform</link><guid isPermaLink="true">https://bernieops.com/hands-on-infrastructure-as-code-aws-deployment-with-terraform</guid><category><![CDATA[Terraform]]></category><category><![CDATA[#IaC]]></category><category><![CDATA[Devops]]></category><category><![CDATA[AWS]]></category><dc:creator><![CDATA[Bernie Camejo]]></dc:creator><pubDate>Mon, 24 Feb 2025 10:55:12 GMT</pubDate><content:encoded><![CDATA[<p>I've been building a <a target="_blank" href="https://github.com/bernie-cm/devops-automation">DevOps automation project on my GitHub page</a> to showcase how Terraform, Ansible and Docker can be used together to quickly deploy, and automate the configuration of assets using Infrastructure as Code (IaC).</p>
<h3 id="heading-set-up-local-environment-variables">Set up local environment variables</h3>
<p>For this project, I'm building on AWS. In order to use Terraform, I need to set up environment variables to store my <strong>AWS Access Key ID</strong>and then my <strong>AWS Secret Access Key</strong>. It's super important not to share these, or store them in a script that later gets pushed to GitHub.</p>
<pre><code class="lang-dockerfile">$ export AWS_ACCESS_KEY_ID=&lt;your_aws_key&gt;
$ export AWS_SECRET_ACCESS_KEY=&lt;your_secret_key&gt;
</code></pre>
<h3 id="heading-main-configuration-file">Main configuration file</h3>
<p>Once the local variables are set, two files will be required to create an EC2 instance on AWS through IaC. First, the <a target="_blank" href="http://main.tf"><code>main.tf</code></a> file.</p>
<pre><code class="lang-dockerfile">terraform {
  required_providers {
    aws = {
      source  = <span class="hljs-string">"hashicorp/aws"</span>
      version = <span class="hljs-string">"~&gt; 4.16"</span>
    }
  }

  required_version = <span class="hljs-string">"&gt;= 1.2.0"</span>
}

provider <span class="hljs-string">"aws"</span> {
  region = var.aws_region
}

resource <span class="hljs-string">"aws_instance"</span> <span class="hljs-string">"app_server"</span> {
  ami           = var.ami_id
  instance_type = var.instance_type

  tags = {
    Name = <span class="hljs-string">"DevOpsDemoInstance"</span>
  }
}
</code></pre>
<p>Next, it's time to define a <a target="_blank" href="http://variables.tf"><code>variables.tf</code></a> file that will store the variables used by <a target="_blank" href="http://main.tf"><code>main.tf</code></a> during execution. For simple deployments, like this one, it would be enough to declare the variables in the <a target="_blank" href="http://main.tf"><code>main.tf</code></a> file. However, for more complex projects, or when better organisation is needed, or the configuration will be reused, it's best practice to use a <a target="_blank" href="http://variables.tf"><code>variables.tf</code></a> file. For that reason, that's the approach used here since this is a learning exercise.</p>
<h3 id="heading-variables-file">Variables file</h3>
<pre><code class="lang-dockerfile">variable <span class="hljs-string">"aws_region"</span> {
  description = <span class="hljs-string">"AWS region"</span>
  type        = string
  default     = <span class="hljs-string">"ap-southeast-2"</span>
}

variable <span class="hljs-string">"instance_type"</span> {
  description = <span class="hljs-string">"EC2 instance type"</span>
  type        = string
  default     = <span class="hljs-string">"t2.micro"</span>
}

variable <span class="hljs-string">"ami_id"</span> {
  description = <span class="hljs-string">"The AMI ID for the EC2 instance"</span>
  type        = string
  default     = <span class="hljs-string">"ami-0b0a3a2350a9877be"</span>
}
</code></pre>
<p>Finally, the next thing is to initialise the directory using <code>terraform init</code>, and then create the infrastructure using <code>terraform apply</code>and reviewing what will be built. Once satisfied, select <code>yes</code> and your AWS infrastructure will be provisioned. Once the proof-of-concept is finished, remember to use <code>terraform destroy</code> to tear down your infrastructure.</p>
]]></content:encoded></item><item><title><![CDATA[Stop Wrestling with UNIX Timestamps: A Clean Pandas Solution]]></title><description><![CDATA[During the initial stages of building my International Space Station (ISS) data engineering project, I quickly realised a format issue with the timestamps broadcast by the ISS Open API. The timestamps used by the API were set in the UNIX format, whic...]]></description><link>https://bernieops.com/stop-wrestling-with-unix-timestamps-a-clean-pandas-solution</link><guid isPermaLink="true">https://bernieops.com/stop-wrestling-with-unix-timestamps-a-clean-pandas-solution</guid><category><![CDATA[Python]]></category><category><![CDATA[Data Science]]></category><dc:creator><![CDATA[Bernie Camejo]]></dc:creator><pubDate>Sun, 23 Feb 2025 10:58:58 GMT</pubDate><content:encoded><![CDATA[<p>During the initial stages of building my <a target="_blank" href="https://github.com/bernie-cm/iss-lambda">International Space Station</a> (ISS) data engineering project, I quickly realised a format issue with the timestamps broadcast by the ISS Open API. The timestamps used by the API were set in the UNIX format, which aren't terribly user friendly for consumers of the data.</p>
<p>My goal was to ensure that my dataframe's timestamps are correctly converted from UNIX to ISO 8601 during ingestion. <a target="_blank" href="https://en.wikipedia.org/wiki/Unix_time">UNIX timestamps</a> represent how many seconds have passed since midnight 1 January 1970.</p>
<p>An example UNIX timestamp looks like this:</p>
<blockquote>
<p>1707568200 → Converts to 2024-02-10T12:30:00 UTC</p>
</blockquote>
<p>While interesting from a historical point of view, I didn't want my ETL application to keep timestamps in this format. So after doing research, the <code>pandas</code> library has a useful function called <code>to_datetime(series)</code>. This function can then be combined with another <code>pandas</code> function called <code>dt.strftime</code> to finally convert the timestamp to the ISO 8601 format.</p>
<pre><code class="lang-dockerfile">def convert_unix_to_iso(series: pd.Series) -&gt; pd.Series:
    <span class="hljs-string">""</span><span class="hljs-string">"
    Convert Unix timestamps to ISO 8601 formatted datetime strings.

    Args:
        series (pd.Series): Series containing Unix timestamps

    Returns:
        pd.Series: Series with timestamps in ISO format (YYYY-MM-DDTHH:MM:SS)
    "</span><span class="hljs-string">""</span>
    return pd.to_datetime(series, unit=<span class="hljs-string">'s'</span>).dt.strftime(<span class="hljs-string">'%Y-%m-%dT%H:%M:%S'</span>)
</code></pre>
<p>The above code takes a series right after my ISS ETL script ingests data, and immediately converts the relevant column of timestamps into ISO 8601 format, which is far more useful for end users.</p>
]]></content:encoded></item><item><title><![CDATA[Dockerising an ISS Location Tracker: Lessons from Local Development]]></title><description><![CDATA[I've been building a data engineering project using Python, Docker, and AWS to ingest data from the International Space Station location API. In a previous post, I wrote about troubleshooting the connection to an Amazon RDS Postgres database over the...]]></description><link>https://bernieops.com/dockerising-an-iss-location-tracker-lessons-from-local-development</link><guid isPermaLink="true">https://bernieops.com/dockerising-an-iss-location-tracker-lessons-from-local-development</guid><category><![CDATA[AWS]]></category><category><![CDATA[Docker]]></category><dc:creator><![CDATA[Bernie Camejo]]></dc:creator><pubDate>Fri, 21 Feb 2025 10:45:57 GMT</pubDate><content:encoded><![CDATA[<p>I've been building a <a target="_blank" href="https://github.com/bernie-cm/iss-lambda">data engineering project</a> using Python, Docker, and AWS to ingest data from the International Space Station location API. In a previous post, I wrote about <a target="_blank" href="https://bernieops.com/troubleshooting-a-connection-to-an-amazon-rds-postgres-database-over-the-internet">troubleshooting the connection</a> to an Amazon RDS Postgres database over the internet.</p>
<p>Once the connection could be established over the public internet, I wanted to first containerise the Python processor, and then test on my local dev system to ensure the data was being written correctly to the Amazon RDS Postgres database.</p>
<h2 id="heading-dockerfile">Dockerfile</h2>
<pre><code class="lang-dockerfile"><span class="hljs-keyword">FROM</span> python:<span class="hljs-number">3.13</span>.<span class="hljs-number">2</span>

<span class="hljs-keyword">RUN</span><span class="bash"> pip install pandas sqlalchemy requests psycopg2</span>

<span class="hljs-keyword">WORKDIR</span><span class="bash"> /app</span>
<span class="hljs-keyword">COPY</span><span class="bash"> main.py main.py</span>

<span class="hljs-keyword">ENTRYPOINT</span><span class="bash"> [<span class="hljs-string">"python3"</span>, <span class="hljs-string">"main.py"</span>]</span>
</code></pre>
<p>Unfortunately, while I was able to build a Docker image using the above Dockerfile, and successfully run the container on my local environment, in the end this didn't work when pushing the container to Amazon ECR. I will get into how I fixed this problem in the next post.</p>
]]></content:encoded></item><item><title><![CDATA[Troubleshooting a connection to an Amazon RDS Postgres database over the internet]]></title><description><![CDATA[I'm building a data engineering project, consisting of an ETL pipeline that serves data to a visualisation application (written in Python). One of the first steps to set up the project is to create the database that will hold the data returned from t...]]></description><link>https://bernieops.com/troubleshooting-a-connection-to-an-amazon-rds-postgres-database-over-the-internet</link><guid isPermaLink="true">https://bernieops.com/troubleshooting-a-connection-to-an-amazon-rds-postgres-database-over-the-internet</guid><category><![CDATA[AWS]]></category><dc:creator><![CDATA[Bernie Camejo]]></dc:creator><pubDate>Wed, 19 Feb 2025 10:50:24 GMT</pubDate><content:encoded><![CDATA[<p>I'm building a data engineering project, consisting of an ETL pipeline that serves data to a visualisation application (written in Python). One of the first steps to set up the project is to create the database that will hold the data returned from the API call. For this project, I decided to use an Amazon RDS database running a Postgres implementation as it's a widely implemented open-source database technology.</p>
<h2 id="heading-bluf">BLUF</h2>
<ul>
<li><p><strong>Understanding security groups</strong>: Configure inbound rules to enable public access to an RDS Postgres database</p>
</li>
<li><p><strong>Setting up database connections</strong>: Use <code>psql</code> CLI for rapid database testing and proof-of-concept</p>
</li>
</ul>
<h2 id="heading-challenges">Challenges</h2>
<p>During the initial setup and testing, I couldn't access my Amazon RDS instance over the public internet. This had to be fixed as my application was accessing a public API to obtain the current location of the International Space Station (ISS). I followed these steps to logically eliminate the potential cause of the problem.</p>
<ul>
<li>Look into the security group attached to my RDS database, and inspect the <strong>inbound rules</strong>. Since the initial connection to the RDS instance was failing, this was the first step to identify the problem. I ensured the inbound rules on the security group were set so that TCP traffic on port 5432 (default port for Postgres) was allowed.</li>
</ul>
<p><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXcAyOoE76P4PbIr06BGVMBIQscOQleJqaFkl1N47bGGSKzdjWag_FsKgaScy0_7msBtNFP1EkqK7DDEVcNt05GrdLy5qIs-eUYADOtbAbQX_u804UiOd5N1658D8Ft9p0wmUMOvPA?key=RyTGcg8X1T3ptgtp402chN98" alt /></p>
<ul>
<li><p>Ensure the RDS is <strong>publicly accessible</strong>. This was another setting I had to review, as the point is to access the RDS instance over the public internet. So, the RDS database <strong><em>must be</em></strong> publicly accessible.</p>
<p>  <img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXcJXOuKOpVDn2HggnNLQpqTwMmU82hvFJ7CFcgr3B-gk4ggbV56a3PaeM4nVN37gfvVbaoYJE5JAikYtkvBzTKOKY48dZIeG186TVdBwiD-IH7CyxgHR57nmzLHm-jlCuUG18UqHg?key=RyTGcg8X1T3ptgtp402chN98" alt /></p>
</li>
<li><p>Finally, I needed to make sure the VPC had an <strong>internet gateway</strong> attached.</p>
</li>
</ul>
<h2 id="heading-success">Success</h2>
<p>Once those three troubleshooting steps were done, I was able to establish a connection to my Amazon RDS Postgres database using <code>psql</code>.</p>
<p><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXcUGdMytgqR7uOt6uQ7X2kINlGNm0_GQNLKDt8LMdBCGxhi0A5ya1st9Vz6wxIqAD5GPzVhTyAsMxhuU4b61_OIW29w_ZB0rsEEDuRd-Ap4VDacH0ahXxcpD4R0lcEpfia5I_Kn5w?key=RyTGcg8X1T3ptgtp402chN98" alt /></p>
]]></content:encoded></item><item><title><![CDATA[Route 53 Routing Policies (2/2)]]></title><description><![CDATA[In part 1, I explored the first four routing policies used by Route 53 when responding to DNS queries. These are the rest of the policies:

Geolocation routing: based on the geographic location of the user, Route 53 will respond accordingly. For exam...]]></description><link>https://bernieops.com/route-53-routing-policies-22</link><guid isPermaLink="true">https://bernieops.com/route-53-routing-policies-22</guid><category><![CDATA[AWS]]></category><dc:creator><![CDATA[Bernie Camejo]]></dc:creator><pubDate>Tue, 18 Feb 2025 02:00:12 GMT</pubDate><content:encoded><![CDATA[<p>In <a target="_blank" href="https://bernieops.com/route-53-routing-policies-1">part 1</a>, I explored the first four routing policies used by Route 53 when responding to DNS queries. These are the rest of the policies:</p>
<ul>
<li><p>Geolocation routing: based on the geographic location of the user, Route 53 will respond accordingly. For example, this is useful for compliance or content distribution restrictions.</p>
</li>
<li><p>Geoproximity routing: not to be confused with geolocation, in this case a bias weight is established to AWS resources allocated in specific regions. In other words, this policy takes into consideration the location of the user <strong>and</strong> the location of the AWS resource being queried for.</p>
</li>
<li><p>IP-based routing: CIDR blocks are allocated to different AWS resources, and depending on the user's IP address, different DNS responses will be provided.</p>
</li>
<li><p>Multi-value routing: based on health checks, Route 53 is able to return multiple records for a single resource. For example, a website with three different IP addresses.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739703783993/7b6ebec9-eb3b-4dea-ab9e-7a6258eee6b9.png" alt class="image--center mx-auto" /></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[How to reduce friction when pushing code to Git]]></title><description><![CDATA[I was getting tired of doing git add ., git commit -m "Message", and git push. So I created a shell function in my .zshrc to do a single command that executes the three steps.
function gpush() {
    local msg="${1:-Auto commit}"
    git add .
    git...]]></description><link>https://bernieops.com/how-to-reduce-friction-when-pushing-code-to-git</link><guid isPermaLink="true">https://bernieops.com/how-to-reduce-friction-when-pushing-code-to-git</guid><category><![CDATA[shell script]]></category><category><![CDATA[Bash]]></category><category><![CDATA[zsh]]></category><dc:creator><![CDATA[Bernie Camejo]]></dc:creator><pubDate>Mon, 17 Feb 2025 02:00:11 GMT</pubDate><content:encoded><![CDATA[<p>I was getting tired of doing <code>git add .</code>, <code>git commit -m "Message"</code>, and <code>git push</code>. So I created a shell function in my <code>.zshrc</code> to do a single command that executes the three steps.</p>
<pre><code class="lang-sh"><span class="hljs-keyword">function</span> <span class="hljs-function"><span class="hljs-title">gpush</span></span>() {
    <span class="hljs-built_in">local</span> msg=<span class="hljs-string">"<span class="hljs-variable">${1:-Auto commit}</span>"</span>
    git add .
    git commit -m <span class="hljs-string">"<span class="hljs-variable">$msg</span>"</span>
    git push
}
</code></pre>
<p>Now, I don't have to repeat the same three steps, but rather call the <code>gpush</code> function.</p>
<pre><code class="lang-sh">gpush <span class="hljs-string">"Commit message"</span>
</code></pre>
<p>And done.</p>
]]></content:encoded></item><item><title><![CDATA[Route 53 Routing Policies (1/2)]]></title><description><![CDATA[Route 53 includes routing policies that determine how it responds to client DNS queries. These policies enable you to configure different ways to route traffic based on conditions or events. There are 8 routing policies that you need to know for the ...]]></description><link>https://bernieops.com/route-53-routing-policies-1</link><guid isPermaLink="true">https://bernieops.com/route-53-routing-policies-1</guid><category><![CDATA[AWS]]></category><dc:creator><![CDATA[Bernie Camejo]]></dc:creator><pubDate>Sat, 15 Feb 2025 22:48:37 GMT</pubDate><content:encoded><![CDATA[<p>Route 53 includes <strong>routing</strong> policies that determine how it responds to client DNS queries. These policies enable you to configure different ways to route traffic based on conditions or events. There are 8 routing policies that you need to know for the Solutions Architect exam, and this post only covers the first four.</p>
<ul>
<li><p>Simple routing: directs traffic to a single resource, like a web browser. If there are multiple IP addresses associated with that resource, Route 53 will return <strong>all</strong> of the IP addresses, but the client will pick one at random.</p>
</li>
<li><p>Weighted routing: different weights are assigned to individual resources, and Route 53 splits traffic based on the assigned proportions. For example, one EC2 instance gets 70% of the traffic, another 20% and the last one 10%.</p>
</li>
<li><p>Latency-based routing: this is similar to a geographic policy, but it's actually based on how Route 53 calculates <strong>latency</strong> between users and AWS resources like an Elastic Load Balancer. Usually, clients will be routed to networks with the lowest latency.</p>
</li>
<li><p>Failover routing: health checks must be created, and if they fail then Route 53 will stop sending traffic to the secondary/backup instances if the primary fails those checks.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1739659650788/d631abc0-e34c-4c29-89f8-016582ee9ac6.png" alt class="image--center mx-auto" /></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Fixing incorrect KMS Key Policy when deploying Lambda with an IAM Role]]></title><description><![CDATA[Often you'll want to create environment variables to pass as CLI arguments to a Lambda function. Lambda as a service does not support passing CLI arguments using things like argparse, but rather you have to configure individual environment variables ...]]></description><link>https://bernieops.com/fixing-incorrect-kms-key-policy-when-deploying-lambda-with-an-iam-role</link><guid isPermaLink="true">https://bernieops.com/fixing-incorrect-kms-key-policy-when-deploying-lambda-with-an-iam-role</guid><category><![CDATA[AWS]]></category><category><![CDATA[lambda]]></category><dc:creator><![CDATA[Bernie Camejo]]></dc:creator><pubDate>Fri, 14 Feb 2025 23:31:55 GMT</pubDate><content:encoded><![CDATA[<p>Often you'll want to create environment variables to <a target="_blank" href="https://bernieops.com/using-environment-variables-when-deploying-lambda-containers">pass as CLI arguments</a> to a Lambda function. Lambda as a service does not support passing CLI arguments using things like <code>argparse</code>, but rather you have to configure individual environment variables within the AWS console.</p>
<pre><code class="lang-plaintext">Lambda was unable to configure access to your environment variables because the KMS key is invalid for CreateGrant. Please check your KMS key settings. KMS Exception: InvalidArnException KMS Message: ARN does not refer to a valid principal
</code></pre>
<p>The above error message is clear in that the IAM Role your Lambda function is using does not have the correct policy, nor existing KMS keys refer to a valid principal. After a lot of troubleshooting, and careful reading of the <a target="_blank" href="https://docs.aws.amazon.com/kms/latest/developerguide/key-policy-overview.html#key-policy-elements">AWS documentation</a>, I identified that the problem could be by adding an additional statement to the relevant KMS Key Policy.</p>
<p>The solution:</p>
<ol>
<li><p>Identify the specific IAM Role that your Lambda function is using to encrypt environment variables using KMS.</p>
</li>
<li><p>Edit the KMS Key Policy to include the IAM Role as an AWS <code>Principal</code>.</p>
</li>
<li><p>Add the following statement to your Key Policy.</p>
</li>
</ol>
<pre><code class="lang-json">      <span class="hljs-string">"Principal"</span>: {
        <span class="hljs-attr">"AWS"</span>: <span class="hljs-string">"arn:aws:iam::471112566722:role/service-role/iss_locations_lambda-role-grcyflva"</span>
      },
      <span class="hljs-string">"Action"</span>: [
        <span class="hljs-string">"kms:Encrypt"</span>,
        <span class="hljs-string">"kms:Decrypt"</span>,
        <span class="hljs-string">"kms:ReEncrypt*"</span>,
        <span class="hljs-string">"kms:GenerateDataKey*"</span>,
        <span class="hljs-string">"kms:DescribeKey"</span>
      ]
</code></pre>
<p>And just like that, my Lambda function was able to use a customer-manager KMS key, customised with the correct key policy, to create and encrypt the required environment variables for the application.</p>
]]></content:encoded></item><item><title><![CDATA[Using environment variables when deploying Lambda containers]]></title><description><![CDATA[While building a simple application that pulls API location data for the International Space Station, I was running the ingestion and transformation script locally on my system, passing the necessary parameters to the CLI. Below is the code snippet o...]]></description><link>https://bernieops.com/using-environment-variables-when-deploying-lambda-containers</link><guid isPermaLink="true">https://bernieops.com/using-environment-variables-when-deploying-lambda-containers</guid><category><![CDATA[AWS]]></category><category><![CDATA[lambda]]></category><category><![CDATA[Docker]]></category><dc:creator><![CDATA[Bernie Camejo]]></dc:creator><pubDate>Fri, 14 Feb 2025 11:32:39 GMT</pubDate><content:encoded><![CDATA[<p>While building a simple application that pulls API location data for the International Space Station, I was running the ingestion and transformation script locally on my system, passing the necessary parameters to the CLI. Below is the code snippet of how I was going about this.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">main</span>(<span class="hljs-params">params</span>):</span>
    user = params.u
    password = params.p
    host = params.host
    ...


<span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">"__main__"</span>:
    <span class="hljs-comment"># Create the CLI parser</span>
    parser = argparse.ArgumentParser(description=<span class="hljs-string">"Call ISS API and store current position in Amazon RDS"</span>)

    <span class="hljs-comment"># Create two CLI arguments to ask for username and password</span>
    parser.add_argument(<span class="hljs-string">"-u"</span>, help=<span class="hljs-string">"username for Postgres"</span>)
    parser.add_argument(<span class="hljs-string">"-p"</span>, help=<span class="hljs-string">"password for Postgres"</span>)
    parser.add_argument(<span class="hljs-string">"--host"</span>, help=<span class="hljs-string">"hostname for RDS Postgres server"</span>)

    args = parser.parse_args()
    main()
</code></pre>
<p>Realising this wasn't probably going to work when deploying the container using a Lambda function, simply because Lambda <strong>does not</strong> support passing CLI arguments directly to the container. My research pointed me to a better implementation using environment variables to pass the <code>-u</code>, <code>-p</code>, and <code>--host</code> parameters to the container.</p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">main</span>():</span>
    <span class="hljs-comment"># Read database credentials from environment variables</span>
    user = os.getenv(<span class="hljs-string">"POSTGRES_USER"</span>)
    password = os.getenv(<span class="hljs-string">"POSTGRES_PASSWORD"</span>)
    host = os.getenv(<span class="hljs-string">"POSTGRES_HOST"</span>)

    ...

    <span class="hljs-keyword">if</span> __name__ == <span class="hljs-string">"__main__"</span>:
    main()
</code></pre>
<p>The result: a more elegant solution that works with a Lambda function calling the container stored in ECR.</p>
]]></content:encoded></item></channel></rss>