GitOps for Presentations

Yes, I work for Microsoft. No, I do not like PowerPoint. Here’s my alternative with the source code which I’ll explain here.

For 20+ years I’ve done UNIX/Linux development and have worked at Microsoft for 6 years. And I’ve learned that Microsoft will typically build the all-encompassing Enterprise-ready solution and the OSS ecosystem will build a narrow-focused tool that you can piece together with others.

Each have their own benefits and constraints. There is No Silver Bullet.

A common set of requirements I encounter are:

  • I need to easily present to a public audience
  • I might have to use someone else’s computer
  • I want to share the slides afterwards
  • I need to quickly update the slides
  • I just want to display text and images. (PowerPoint is an absurdly impressive tool with lots of features that I rarely use.)

Internal Microsoft SharePoint policy prevents sharing slides with external visitors. This often results in emailing 10-100MB PPTs or PDF files around. Blah!

Piecing bits of OSS, I present to you “GitOps for Presentations”. It involves:

  • Git + GitHub – Version control of content
  • Markdown – Easy styling of content
  • MARP – Converts CommonMark to HTML, PDF, PPT
  • VSCode – Edit the content (There’s even a MARP extension which allows you to preview in real-time!)
  • GitHub Actions – Build the presentation from Markdown
  • GitHub Pages – Host the presentation



  • MARP’s formatting is basic. Especially if you’re coming from PowerPoint

That’s cool, but why didn’t you …

  • use Remark or Reveal.js?
  • There’s many great presentations frameworks, but I wanted something really simple. KISS
  • You should be able to replace MARP with any of those other frameworks and still get the same results.
  • just present your PPT and email it?
  • That requires work and time. At conferences, I don’t have time/might forget to follow-up with everyone. I create a QR code and put it at the end of the slides. This enables self-service, discovery and also saves me previous keystrokes.
  • use or Google Slides?
  • Microsoft has embraced OSS and purchased GitHub, so I wanted to find a way to explore integrating all of this. I’ve been very happy with the results!

I’m sold! How do I get started?

  • I’ve made it easy for anyone to get started by creating a GitHub template for this project (which is also a presentation)
  • Click “Use this template” and create a new repository
  • Enable GitHub Actions to auto-publish to GitHub Pages
  • In your new Repo, click Settings -> Pages
  • Set Source to GitHub Actions
  • You’re done!


  • I’m quite familiar with GitOps, and while this is outside of running Kubernetes clusters as IaC, there are some similarities with the top-level concept of using Git to set my desired state of my presentation.
  • MARP technically uses CommonMark. It’s close enough for what most people will need

Managing Azure Subscription Quota and Throttling Issues

As Azure customers and partners build bigger and more complex solutions in their subscriptions, you might hit quota and throttling issues. These can be irksome and cause confusion. This article will walkthrough some of the scenarios I’ve seen and how to design with them in mind.

Let’s make sure we’re on the same page regarding terminology used in this article:

Managing Quotas

Because quotas are mostly static, viewing your quotas is pretty simple. Simply to go the Azure Portal and click on “My quotas”.

If you need to increase your quota, you might need to open an Azure Support ticket. For example, if you need to start deploying in a new region, you might need to open a ticket to increase the “Total Regional vCPUs” and “VMSS” quotas in “West Central US”. Once the ticket has been approved, the quota will be available to you.

Managing Throttling

For the most part, you won’t need to worry about throttling, but if you’re doing very large scale deployments with LOTS of constant churning of resources, you might hit throttling limits.

These limits are less about the number of resources, but HOW you use the resources. For example:

  • You can have 5000 AKS cluster in one subscription, each AKS cluster can have a maximum of 100 node pools. If you try creating the max # of AKS clusters with the max # of node pools simultaneously, then you’ll definitely hit the throttling limit.
  • Some OSS projects aggressively call ARM and the RP API’s in a reconciliation loop. Multiple instances of these projects will also hit the throttling limit.

Since throttling is specific to the current time window, it can be trickier. There’s no “hard formula” for when you’ll hit a threshold. But when you do, you’ll probably start seeing 429 HTTP status responses.

Throttling Examples

Thankfully, you can get insights into your current throttling status by looking at response headers for the requests.

  • x-ms-ratelimit-remaining-subscription-reads – # of read operations to this subscription remaining
  • x-ms-ratelimit-remaining-subscription-writes – # of writes operations to this subscription remaining
  • x-ms-ratelimit-remaining-resource – Compute RP specific header, which could show multiple policy statuses. (see “Read Request for GETting a VMSS” below for details)

Let’s dig into this deeper using the Azure CLI.

Example: Create a Resource Group (Write Request)

Because this request creates a RG, it will count against our subscription writes:

? az group create -n $RG –location $LOCATION –verbose –debug –debug 2>&1 | grep 'x-ms'

DEBUG: 'x-ms-client-request-id': '<guid>'
DEBUG: 'x-ms-ratelimit-remaining-subscription-writes': '1199'
DEBUG: 'x-ms-request-id': '<guid>'
DEBUG: 'x-ms-correlation-request-id': '<guid>'
DEBUG: 'x-ms-routing-request-id': 'SOUTHCENTRALUS:20230512T163152Z:<guid>'

NOTE: The key point is how the x-ms-ratelimit-remaining-subscription-writes is now 1199 (instead of the standard 1200 per hour as per the Subscription and Tenant limits)

Example: GET a VMSS (Read Request)

This request performs a GET (read) request on an existing VMSS. This is similar to the write request for the RG, but since Compute RP also has a separate set of throttling policies, it also counts against the Compute RP limits.

? az vmss show -n $VMSS_NAME -g $RG --debug 2>&1 | grep x-ms
DEBUG: 'x-ms-client-request-id': '<guid>'
DEBUG: 'x-ms-ratelimit-remaining-resource': 'Microsoft.Compute/GetVMScaleSet3Min;197,Microsoft.Compute/GetVMScaleSet30Min;1297'
DEBUG: 'x-ms-request-id': '<guid>'
DEBUG: 'x-ms-ratelimit-remaining-subscription-reads': '11999'
DEBUG: 'x-ms-correlation-request-id': '<guid>'
DEBUG: 'x-ms-routing-request-id': 'SOUTHCENTRALUS:20230512T162738Z:<guid>'

NOTE: The key point is how x-ms-ratelimit-remaining-resource has two key-value pairs:

  • Microsoft.Compute/GetVMScaleSet3Min;197 – I ran this command before, so I have 197 requests available in the 3 minute window for performing GET requests on the VMSS resource
  • Microsoft.Compute/GetVMScaleSet30Min;1297 – I now have 1297 requests available in the 30 minute window for performing GET requests on VMSS resources

NOTE: x-ms-ratelimit-remaining-subscription-reads doesn’t seem to decrease (11999). Even if I run the same command again. I haven’t figured that out yet.

Designing with quotas and throttling in mind

Most Azure deployments won’t need this type of fine tuning, but just in case, there’s some documented Throttling Best Practices as well as my personal pro-tips:

  • Use the Azure SDK, as many services have the recommended retry guidance built-in
  • Instead of creating and deleting VMSS (which consume multiple VMSS API requests), scale the VMSS to 0 (which only consumes 1 VMSS API request)
  • Any type of Kubernetes cluster auto-scaler will perform a reconciliation loop with Azure Compute RP. This could eat into your throttling limits
  • Use the Azure Quota Service API to programmatically request quota increases

If you’re unable to workaround the throttling limits, then the next step is to look at the Deployment Stamp pattern using multiple subscriptions. You can programmatically create subscriptions using Subscription vending.

Hopefully this article has helped you understand quotas limits and throttling limits in Azure, and how to work around them. Let me know if you have any additional questions and/or feedback and I can follow-up with additional details.

AKS + Private Link Service + Private Endpoint

This walkthrough shows how to setup a Private Link Service with an AKS cluster and create a Private Endpoint in a separate Vnet.

While many tutorials might give you a full ARM template, this is designed as a walkthrough which completely uses the CLI so you can understand what’s happening at every step of the process.

It focuses on an “uninteresting” workload and uses podinfo as the sample app. This is because it’s easy to deploy and customize with a sample Helm chart.

This is inspired and leans heavily on the Azure Docs for creating a Private Link Service.


Private Link Endpoint Service



This walkthrough assumes you let Azure create the Vnet when creating the AKS cluster. If you manually created the Vnet, then the general steps are the same, except you must enter the AKS_MC_VNET, AKS_MC_SUBNET env vars manually.

Setup Steps

First, create a sample AKS cluster and install Podinfo on it.

# Set these values

# Create the AKS cluster
az aks create -n $AKS_NAME -g $AKS_RG

# Get the MC Resource Group
AKS_MC_RG=$(az aks show -n $AKS_NAME -g $AKS_RG | jq -r '.nodeResourceGroup')
echo $AKS_MC_RG

# Get the Vnet Name
AKS_MC_VNET=$(az network vnet list -g $AKS_MC_RG | jq -r '.[0].name')

AKS_MC_SUBNET=$(az network vnet subnet list -g $AKS_MC_RG --vnet-name $AKS_MC_VNET | jq -r '.[0].name')


AKS_MC_LB_INTERNAL_FE_CONFIG=$(az network lb rule list -g $AKS_MC_RG --lb-name=$AKS_MC_LB_INTERNAL | jq -r '.[0]')

# Deploy a sample app using an Internal LB
helm upgrade --install --wait podinfo-internal-lb \
--set-string service.annotations."service\.beta\.kubernetes\.io\/azure-load-balancer-internal"=true \
--set service.type=LoadBalancer \
--set ui.message=podinfo-internal-lb \

Install Steps – Create the Private Link Service

These steps will be done in the MC_ resource group.

# Disable the private link service network policies
az network vnet subnet update \
--name $AKS_MC_SUBNET \
--resource-group $AKS_MC_RG \
--vnet-name $AKS_MC_VNET \
--disable-private-link-service-network-policies true

# Create the PLS
az network private-link-service create \
--resource-group $AKS_MC_RG \
--name $PLS_NAME \
--vnet-name $AKS_MC_VNET \
--subnet $AKS_MC_SUBNET \
--lb-name $AKS_MC_LB_INTERNAL \
--lb-frontend-ip-configs $AKS_MC_LB_INTERNAL_FE_CONFIG

Install Steps – Create the Private Endpoint

These steps will be done in our private-endpoint-rg resource group.

az group create \
--name $PE_RG \
--location $LOCATION


az network vnet create \
--resource-group $PE_RG \
--name $PE_VNET \
--address-prefixes \
--subnet-name $PE_SUBNET \

# Disable the private link service network policies
az network vnet subnet update \
--name $PE_SUBNET \
--resource-group $PE_RG \
--vnet-name $PE_VNET \
--disable-private-endpoint-network-policies true

az network private-endpoint create \
--connection-name $PE_CONN_NAME \
--name $PE_NAME \
--private-connection-resource-id $PLS_ID \
--resource-group $PE_RG \
--subnet $PE_SUBNET \
--manual-request false \
--vnet-name $PE_VNET

# We need the NIC ID to get the newly created Private IP
PE_NIC_ID=$(az network private-endpoint show -g $PE_RG --name $PE_NAME -o json | jq -r '.networkInterfaces[0].id')
echo $PE_NIC_ID

# Get the Private IP from the NIC
PE_IP=$(az network nic show --ids $PE_NIC_ID -o json | jq -r '.ipConfigurations[0].privateIpAddress')
echo $PE_IP

Validation Steps – Create a VM

Lastly, validate that this works by creating a VM in the Vnet with the Private Endpoint.

az vm create \
--resource-group $PE_RG \
--name ubuntu \
--image UbuntuLTS \
--public-ip-sku Standard \
--vnet-name $PE_VNET \
--subnet $PE_SUBNET \
--admin-username $USER \
--ssh-key-values ~/.ssh/

VM_PIP=$(az vm list-ip-addresses -g $PE_RG -n $VM_NAME | jq -r '.[0][0].ipAddress')
echo $VM_PIP

# SSH into the host
ssh $VM_IP


# The output should look like:
$ curl
"hostname": "podinfo-6ff68cbf88-cxcvv",
"version": "6.0.3",
"revision": "",
"color": "#34577c",
"logo": "",
"message": "podinfo-internal-lb",
"goos": "linux",
"goarch": "amd64",
"runtime": "go1.16.9",
"num_goroutine": "9",
"num_cpu": "2"

Multiple PLS/PE

To test a specific use case, I wanted to create multiple PLS and PE’s. This set of instructions lets you easily loop through and create multiple instances.

# podinfo requires a high numbered port, eg 9000+

helm upgrade --install --wait podinfo-$SUFFIX \
--set-string service.annotations."service\.beta\.kubernetes\.io\/azure-load-balancer-internal"=true \
--set service.type=LoadBalancer \
--set service.httpPort=$SUFFIX \
--set service.externalPort=$SUFFIX \
--set ui.message=podinfo-$SUFFIX \

# This might be easier to hard-code
AKS_MC_LB_INTERNAL_FE_CONFIG=$(az network lb rule list -g $AKS_MC_RG --lb-name=$AKS_MC_LB_INTERNAL -o json | jq -r ".[] | select( .backendPort == $SUFFIX) |")


az network private-link-service create \
--resource-group $AKS_MC_RG \
--name $PLS_NAME \
--vnet-name $AKS_MC_VNET \
--subnet $AKS_MC_SUBNET \
--lb-name $AKS_MC_LB_INTERNAL \
--lb-frontend-ip-configs $AKS_MC_LB_INTERNAL_FE_CONFIG

PLS_ID=$(az network private-link-service show \
--name $PLS_NAME \
--resource-group $AKS_MC_RG \
--query id \
--output tsv)
echo $PLS_ID

az network private-endpoint create \
--connection-name $PE_CONN_NAME \
--name $PE_NAME \
--private-connection-resource-id $PLS_ID \
--resource-group $PE_RG \
--subnet $PE_SUBNET \
--manual-request false \
--vnet-name $PE_VNET

PE_NIC_ID=$(az network private-endpoint show -g $PE_RG --name $PE_NAME -o json | jq -r '.networkInterfaces[0].id')
echo $PE_NIC_ID

PE_IP=$(az network nic show --ids $PE_NIC_ID -o json | jq -r '.ipConfigurations[0].privateIpAddress')
echo $PE_IP

echo "From your Private Endpoint VM run: curl $PE_IP:$SUFFIX"

I created this article to help myself (and hopefully you!) to clearly understand all of the resources and how they interact to create a Private Link Service and Private Endpoint fronting a private service inside an AKS cluster. This has been highly enlightening for me and I hope it has for you too.

When, How and Where to use ClusterAPI (CAPI) and ClusterAPI for Azure (CAPZ)

This article explains why, when, and how to use self-managed Kubernetes clusters in Azure for testing custom scenarios.

Kubernetes has gotten so large and complex that most companies prefer to use the managed service (e.g. AKS, GKE) instead of running it themselves. By using a managed Kubernetes service, this frees up the operations team to focus on their core competency instead of optimizing, backing up and upgrading of Kubernetes.

While this reduces the operational burden, you lose the ability to modify the platform. Sometimes these are acceptable tradeoffs, sometimes you need to manage it yourself.

Historically, AKS-engine was the OSS tool for creating unmanaged Kubernetes clusters on Azure, but it had some limitations. CAPI/CAPZ is the go-forward solution for creating and operating self-managed clusters declaratively.

I highly recommend reading Scott Lowe’s article on An introduction to CAPI. It covers a lot of terminology and concepts used here.

One of the reasons for using CAPI/CAPZ is as a testing and development tool for Kubernetes on Azure. For example, you might need to build and test the following scenarios:

  • A kernel change to the worker nodes
  • A modification to the K8S config on control plane nodes
  • An installation of a different CNI
  • The use of K8S to manage K8S

This diagram represents a high level architecture of a starter CAPI/CAPZ cluster.

The rest of this article will explain how to implement the above scenarios utilizing the CAPI quickstart. Because the command arguments will change over time, this article will describe the steps and provide a link to the full details like this:

Link to CAPI Quick Start with details: base command to run

Create the KIND Cluster

Similar to RepRap, CAPI uses a Kubernetes cluster to make more Kubernetes clusters. The easiest way is with Kuberenetes IN Docker (KIND). As the name implies, it’s a Kubernetes cluster which runs as a Docker container. This is our starting point for what we call “Bootstrap Cluster”.

Create Kind Cluster: kind create cluster

Initialize cluster for Azure

We will use this bootstrap cluster to initialize the “Management Cluster” which contains all of the CRDs and runs the CAPI controllers. This is where we will apply all of our changes to meet our scenarios.

Initialize cluster for Azure: clusterctl init --infrastructure azure

Generate cluster configuration

Now that our management cluster is ready, we want to define what our workload cluster will look like. Thankfully, there are different flavors we can pick from. By using the default, we will get an unmanaged K8S cluster using virtual machines.

Generate cluster configuration: clusterctl generate cluster capi-quickstart > capi-quickstart.yaml

We now have a file which contains the CRDs which will define our workload cluster. We will modify capi-quickstart.yaml and edit the CRDs to implement each of our scenarios.

Full documentation is available for CAPI (baseline) CRDs and CAPZ (Azure specific resources) CRDs.

Scenario: Worker node kernel change

If we want to modify the worker nodes, we likely want to add a preKubeadmCommands and postKubeadmCommands directive in the KubeadmConfigTemplate.

preKubeadmCommands allows a list of commands to run on the worker node BEFORE joining the cluster.

postKubeadmCommands allows a list of commands to run on the worker node AFTER joining the cluster.

kind: KubeadmConfigTemplate
  name: capi-quickstart-md-0
  namespace: default
        - wget -P /tmp<path>.deb
        - dpkg -i /tmp/<package name>.deb
        - reboot

After you’ve made these changes, you can proceed to the rest of the steps by applying the resources to your management cluster which will then create your workload cluster and deploy the CNI.

Scenario: Modify Kubernetes components

If we want to modify the control plane, we can make changes to the KubeadmControlPlane. This allows us to leverage the kubeadm API to customize various components.

For example, to enable a Feature Gate on the kube-apiserver:

kind: KubeadmControlPlane
  name: capi-quickstart-control-plane
  namespace: default
          feature-gates: MyFeatureGate=true

The above example omits some fields for brevity. Make sure that you keep any existing args and configurations that you are not modifying in-place.

After you’ve made these changes, you can proceed to the rest of the steps by applying the resources to your management cluster which will then create your workload cluster and deploy the CNI.

Apply the Workload Cluster

Now that we have defined what our cluster should look like, apply the resources to the management cluster. The CAPZ operator will detect the updated resources and talk to Azure Resource Manager.

Apply the workload cluster kubectl apply -f capi-quickstart.yaml

Monitor the Cluster Creation

After you’ve made the changes to the capi-quickstart.yaml resources and applied them, you’re ready to watch the cluster come up.

Watch the cluster creation:

  • kubectl get cluster
  • clusterctl describe cluster capi-quickstart
  • kubectl get kubeadmcontrolplane – Verify the Control Plane is up

Now that the workload cluster is up and running, it’s time to start using it!

Get the Kubeconfig for the Workload Cluster

Now that we’re dealing with two clusters (management cluster in Docker and workload cluster in Azure), we now have two kubeconfig files. For ease, we will save it to the local directory.

Get the Kubeconfig for the workload cluster clusterctl get kubeconfig capi-quickstart > capi-quickstart.kubeconfig

Install the CNI

By default, the workload cluster will not have a CNI and one must be installed.

Deploy the CNI kubectl --kubeconfig=./capi-quickstart.kubeconfig apply -f https://...calico.yaml

Scenario: Install a different CNI

If you want to use flannel as your CNI, then you can apply the resources to your management cluster which will then create your workload cluster.

However, instead of Deploying the CNI, you can follow the steps in the Install Flannel walkthrough.


When you’re done, you can cleanup both the workload and management cluster easily.

Delete the workload cluster kubectl delete cluster capi-quickstart

If you want to create the workload cluster again, you can do so by re-applying capi-quickstart.yaml

Delete the management cluster kind delete cluster

If you want to create the management cluster again, you must start from scratch. If you delete the management cluster without deleting the workload cluster, then the workload cluster and Azure resources will remain.


Similar to how Kubernetes allows you to orchestrate containers using a declarative syntax, CAPI/CAPZ allows you to do the same, but for Kubernetes clusters in Azure.

This article covered example scenarios for when to use CAPI/CAPZ as well as a walkthrough on how to implement them.

I’m especially excited for the future of CAPI/CAPZ and how it can integrate with other Cloud Native methodologies like GitOps to declaratively manage clusters.

P.S. I am extremely grateful to Cecile Robert Michon’s (Twitter & Github) technical guidance for this article. Without her support, I wouldn’t have gotten this far and definitely would have missed a few key scenarios. Thanks Cecile!

How I built a super cool LED Lanyard

I scoffed when I heard wearable technology was an upcoming “big thing” for fashion. However, since basking in the warm glowing, warming glow of my first LED on an Arduino, I was hooked.

When my buddy, Dan Stach, started showcasing his breakthroughs in lanyard technology, I knew he was onto something. And I knew I wanted to turn it to 11.

We iterated over and over until we streamlined the design to be portable enough to fit behind the badge. After rigorous field-testing (i.e.: lot of tech conferences), the design now fits in a small travel case and can be assembled in minutes.

If you are interested in building your own, you should know a few things about the build and procurement process:


We tried a few different straps and the black one from Strapworks looks the coolest. The white strap looks ok, but the black one makes the design look very 8-bit.

Controller case

This has the most potential for customization and fun. My original case was custom co-designed with the owner of Bricks and Minifigs, Dallas (a used LEGO retailer). I’ve streamlined this to use LEGO + tack to make the box smaller.

For a while I considered selling LED lanyards, and planned to use a 4″x6″ craft case, but was unsatisfied with the results. Every option has felt too bulky or awkward for my taste while LEGO hits that perfect intersection of cool, functional and modular.

LED Controller

The controller is what you use to turn the LED on & off as well as display patterns. The cheapest/easiest is to purchase an RF LED controller because it just works and comes with a remote for ~$11.

I really wanted to go take it to the next level so I used a RaspberryPi Zero W for extra customization. This presented NUMEROUS challenges which is deserving of a separate post. If you are interested in pursuing this, contact me to discuss.

LED strip

There are many LED options, but the ones we wanted were WS2812B, which used RGB LED, could be individually addressed, and would easily talk with our LED controller. We also liked the 144 pixels per meter which gave it a nice density. We also liked IP30 which mean it had no plastic coating on top. This made it more likely to break, but more flexible and would dissipate the heat better.


Come to find out, managing electricity is hard. We lost many hours debugging why the LED would power off when changing patterns. Cheap wires were the culprit as the controller couldn’t pull enough current for some configurations (e.g. all white LED). A credit-card battery worked best because of the bendable power cord which was useful in the tight space of the controller box and minimized our wire problems.


If by the end of this post, you’re still interested in making your own, you’re in luck! I created a parts list and assembly instructions.
Please let me know how it goes (share your pics!) and if there’s anything I can do to improve the experience.

— Tommy wants to be the light of your life. Well, at least your lanyard.

Creating a Dev Env with Azure Functions for Python

Azure Functions (one of the many Serverless Platforms inside Azure) allows you to use Python as your runtime environment.

This is great; however, it requires Python 3.6, which is a problem for my development on a Mac (which uses Python 3.7). Downgrading/dual installation has the potential for many perils, so I wanted an isolated runtime development environment which wouldn’t interfere with my current setup.

Here’s my requirements:

  • Run Azure Functions locally (e.g. “func host start”)
  • Publish Azure Functions locally (e.g. “func azure functionapp publish”)
  • Use my existing Azure CLI session (i.e. don’t have to login each time)
  • Won’t confuse my existing Python 3.7 env.

Docker to the rescue!

I’ll spare you the details of the iterative process of creating the Dockerfile, but after a some iterations, I knew I was on the right track.

You can copy/create my code here:

In short, it creates a Docker image and runs it with:

  • Ubuntu 18.04
  • Python 3.6
  • Azure CLI
  • Azure Function Core Tools
  • Forwards port 7071 from inside the container to my localhost (used for local function testing)
  • Mounts my home dir to /root to maintain my Azure CLI session. (e.g. No login!)

This will definitely save me time each time I want to setup a new Function (or other) environment and I hope it helps save time for you too.

Make my func the p-func!

RoboClippy – Combining 20 years of Internet rage into an adorable package.

The internet’s a weird place. We already knew that, yet it keeps finding new ways to amaze me.

Someone thought it would be a novel idea to incorporate Alexa with a novelty electronic fish (Big Mouth Billy Bass). Now you can ask a fish for the current weather and the fish can tell you if it’s a good day to catch its brethren.

I take that back. The world is a weird place. And I embrace it and want to leave my mark on it.

So, when given an opportunity to build something similar, a robotic version of Clippy was the only natural solution. People have re-fallen in love with Clippy.

However, one thing was missing. A proper, physical manifestation of our favorite sentient paperclip. This is the story of that journey.

RoboClippy Mark I

RoboClippy Mark I was cute, but definitively not a paperclip. I could get the eyebrows to wiggle, but only manually.

RoboClippy Mark II

Upon creating RoboClippy Mark II, I realized that the concept of “Uncanny Valley” applies to both humans and paperclips.

RoboClippy Mark III

In RoboClippy Mark III, I realized that animatronics done wrong is nightmare fuel.

But I made progress! The eyebrows articulate (not realistically), but it doesn’t look natural or have fine motor control.

Since I want to use motors to control LEGO, the LEGO Mindstorms sounded perfect! Alas, I encountered a number of issues interacting with their LEGO power supplies.

  • LEGO Mindstorm motors use 9V; however, I wanted to power everything from USB which only uses 5V
  • LEGO Mindstorm connectors have weird wiring and I wanted more standard cabling

There’s a whole field of hobby electronics and motors, so I went down that path. I soon learned that not all motors are created equal. Servo motors are good for broad movements where you apply voltage to tell the motor “turn left” or “turn right”. Stepper motors are good for fine movements where you apply voltage to tell the motor “Go to position X”.

After some experimentation, stepper motors became the natural fit. The catch is that they use a special protocol to control them called PWM. And if you want to control multiple motors, you want to use a different protocol (I2C). In my case, I wanted to control 3 motors (mouth, left eyebrow, right eyebrow). This was getting more complex than I expected, but I was learning a lot and excited.

RoboClippy Mark IV

RoboClippy Mark IV was a technological breakthrough. With the help of my good friends at Bricks and Minifigs, Plano we had a working prototype which looked realistic and could articulate it’s mouth.

Remember Uncanny Valley? Without the eyebrows, it looks … off.

So, how are we doing so far? We’ve got a great structure, the mouth articulates, and the eyebrows articulate! However, it’s lacking a “soul”. We want it to move the mouth when speaking, we want it to simulate Clippy’s voice and we want to use the eyebrows to emote.

Enter Azure Cognitive Services. There are many services it offers, and in this case I’ll be using Azure Speech to Text and Text to Speech so that I can give RoboClippy a voice and to listen to what people are saying.

Now for the next problem: Determining when RoboClippy is speaking. It seems intuitive to have RoboClippy’s mouth move when the audio is playing and stop when it’s complete, right? Uncanny Valley wins again. If you see someone’s mouth moving when they’re not speaking (e.g. at the end of a sentence), it doesn’t look right.

So, the next option is to calculate voltage off of the soundcard/speaker, right? Again, there’s more nuances to be discovered. Sound is a wave, so measuring at any point only gets you a snapshot. Also most microphones measure -2.5V to 2.5V and the Arduino can only detect 0 – 5V, so we’re missing half the data! A Step-up Converter fixed that problem, but added additional complexity.

RoboClippy Mark V

Now we’re at RoboClippy Mark V. I’m using an Arduino to measure the sound and control the servos. All of the communication happens on my laptop. It’s powered and controlled by the USB. Unfortunately, the results were really flakey and since it drew power from the laptop, there was a potential of the motors drawing too much current and frying it. It also took about 5-10 minutes to setup and get right each time. Major progress! But not very practical.

My local Makerspace had some Google AIY Voice Kits which I experimented with. It had a speaker, a microphone, and a cardboard case, all you needed was to supply your own Raspberry Pi. This was exactly the packaging I needed to contain RoboClippy’s brains.

The last step is making RoboClippy “talk”. But some interesting questions arose:

  • How do I know when to start listening? Wait for a user to press a button? Not a great experience.
  • How do I know when to stop listening? Again, not a great experience.
  • What’s the quickest way to respond? Perform S2T & T2S locally? Use a service?
  • How can I best utilize Azure? This is Clippy, so using MS products makes sense.

Thankfully, someone wrote an OSS library to solve many of these problems. I also learned something about Alexa/Cortana/OK Google that I wasn’t aware of. Keyword detection (aka Hotwork detection).

Anyone with an Amazon Dot/Echo is worried that Alexa/Amazon is always listening in on us. Keyword detection is training an AI model to “wake-up” and do something when it hears specific pitches/frequencies. You can even create you own keyword! Enter Snowboy, a service from Kitt.AI for making your own keyword. This allows RoboClippy to wake-up upon that specific pitch/frequency and then start “really” listening on the microphone. Thankfully, the same OSS library supported Snowboy so this was surprisingly easy to incorporate. You can even contribute to the “Hello Clippy” keyword.

RoboClippy Mark VI

Now, witness the power of this fully armed and operational RoboClippy.

  • Our RoboClippy is now MUCH more extensible.
  • Google hardware for microphone and speakers
  • Microsoft Azure for Text2Speech, Speech2Text, Natural Language Processing
  • I2C to PWM for motor controls
  • RaspberryPi for orchestration
  • Power + control (ssh + Python) can be done remotely
  • 5 easy-to-connect wires (4 for I2C, 1 USB for power)
  • Written in Python
  • Available as Open Source

Build your own Robo-Clippy

To build your own, you will need:

In an upcoming blog, I will detail the steps necessary to create your own. If you can’t wait and want to start working on one now, feel free to email me at tommy at this domain.

If you’re interested in seeing my presentation on this story, you can view it here:

Special thanks:

  • Special thanks to Jason and Andrea of Bricks and Minifigs, Plano who helped design LEGO Clippy
  • Greg Miller who helped me understand the properties of sound via oscilloscope
  • Nina Zakharenko who was the catalyst for this blog post
  • Chloe Condon who is an even bigger Clippy fan than myself and helped fuel this social rebirth

Ark + Azure Kubernetes Service

As much as Cloud Providers tout their availability and uptime, disasters happen. It’s inevitable. And it’s usually up to you to be prepared. There are services that can help; however, they’re not always “Kubernetes aware”.

Thankfully, the great folks at Heptio open-sourced Ark, a Disaster Recovery tool which works for all the major cloud providers.

I got hands-on with Ark and followed their Azure steps. It was a good start, but didn’t highlight how an actual failover and recovery would look to the operator. I took their steps and created a step-by-step guide to perform a full migration.

Ark support Azure native resources, namely Managed Disk + Snapshots.
You can review those steps here:

Another option would be to use Restic, which performs backups to a local file system. Later, I’ll detail the steps on how to use Restic with Azure.

If you’re looking for Best Practices on supporting Business Continuity and Disaster Recovery for AKS/K8S clusters in Azure, you’re in luck! I wrote a Microsoft article covering this use case, which can be found here:

The Journey to Kubernetes

I created this article with the intent of explaining the migration journey from deploying a legacy application with manual steps to an automated Kubernetes deployment with proper DevOps practices. Its intent is not to help you understand Kubernetes deeper (there’s an abundance of materials out there already).

As a Cloud Solution Architect for Microsoft, every week I work with our partners to assist them towards containerization and Kubernetes. I’ll use AKS and discuss it’s strengths and weaknesses without holding punches. Disclaimer: Given I work for Microsoft, I am self-aware of my bias. So in this article, I will make an effort to be more critical of Azure to balance that out.

Beginning With the End in Mind, I created the following outline:


Duckiehunt is secure, monitored and deployable with the least amount of manual effort, cost and code-change.


I wrote Duckiehunt in 2007 as a LAMP website. It embodies many of the customer requirements I see:

  • Old code, using legacy tooling
  • Want a reliable, resilient infrastructure
  • Want to automate deployment
  • Don’t want to re-write
  • Migration should involve minimal/no code change
  • Need to update to modern standards (e.g. HTTPS, MySQL encryption, private DB instance with backups)


  • CI/CD (Code Check-in triggers automated tests and pushes to Production)
  • Monitoring cluster + app (visualization + alerts if down)
  • HTTPS enabled for (CA Cert + forced redirection to https)
  • Running on Kubernetes (AKS)
  • Managed MySQL

Milestones: (in reverse order of accomplishment)

  • Production DNS migrated
  • Azure Monitor + Container Monitoring Solution + LogAnalytics
  • Distinct Dev + Prod environments
  • VSTS + Github integration
  • Securely expose UI + API
  • Integrated MySQL instance
  • Installed on AKS
  • Test in Minikube
  • Migrate App to Container

From here on, I’ll explain my journey as steps fulfilling the milestones I created. I’ll list my estimated time, as along with my actual time to compare. The times below are not “Time to get X working”, but “Time to get X working correctly and automate as if I had to support this in production” (which I do). As a result, they’re much higher than a simple success case.

Migrate app to Container

Estimated Time: 4 hours. Actual Time: 10 hours

I wrote this in 2007 using a PHP version that is no longer supported (5.3) and a framework (CodeIgniter) that is not as active. I didn’t want to re-write it yet. Thankfully, 5.6 is mostly backwards compatible and I was able to find a container using that.

I would have been done in ~4 hours; however, I lost an embarrassing amount of hours banging my head against the wall when I automated the docker build. (I would always get 404) I learned this was because Linux’s file system is case-sensitive and OSX’s is not, and the PHP framework I chose in 2007 expects the first character of some files to start with a capital letter. *grumble* *grumble*

Test in Minikube

Estimated time: 12 hours. Actual Time: 10 hours

Now that I got my PHP app running in a container, it was time to get it running inside Kubernetes. To do this, I needed to deploy, integrate and test the following: Pod, Service, Secrets, Configuration, MySQL and environment variables.

This is a pretty iterative approach of “This, this…nope…how about this?…Nope…This?…ah ha!…Ok, now this…Nope.” This is where Draft comes in. It’s a Kubernetes tool specifically designed for this use case, and I think I’ve started to develop romantic feelings for this tool because of how much time and headache it saved me while being dead simple to use.

Install in AKS

Estimated time: 8 hours. Actual time: 2 hours

Creating a new AKS cluster takes about 10 minutes and is instantly ready to use. Because I had done the work on testing it Minikube the hard-word was already done, but I expected some additional hiccups. Again, this is where my love and adoration of Draft started to shine. I was almost done in 30 minutes, but I took some shortcuts with Minikube that came back to bite me.

Integrated MySQL instance

Estimated time: 2 hours. Actual time: 3 hours

Azure now offers MySQL as a Service (aka Azure Database for MySQL) and I chose to use that. I could have run MySQL in a container in the cluster; however, I would have had to manage my own SLA, backups, scaling, etc. Given my intent of this project is to have the least amount of work and cost, and the cost is still within my MSDN budget, I chose to splurge.

I spent an hour experimenting with Open Service Broker for Azure (a way of managing external dependencies, like MySQL, native to K8S). I really like the idea, but I wanted one instance for both Dev + Prod and needed a high control over how my app read in database parameters (since it was written in 2007). If I was doing more deployments than one, OSBA would be the right fit, but not this time.

Steps taken:

  1. Create the Azure Database for MySQL Instance
  2. Created the dev/prod accounts
  3. Migrated the data (mysqldump)
  4. White-listed the source IPs (To MySQL, the cluster traffic looks as if it’s coming from the Ingress IP address)
  5. Injected the connection string to my application (Using K8S Secrets)

Then I was off to the races. OSBA would have automated all of that for me, but I’ll save that for a proverbial rainy day.

Securely expose UI + API

Estimated time: 4 hours. Actual time: 20 hours

This was the most frustrating part of the entire journey. I decided to use Nginx Ingress Controller with Cert-manager (for SSL). There’s lots of old documentation that conflicts with recommended practices, which led to lots of confusion and frustration. I got so frustrated I purposely deleted the entire cluster and started from scratch.

Lessons’ learned:

  1. nginx-ingress is pretty straight-forward and stable. Cert-manager is complicated and I had to restart it a lot. I really miss kube-lego (same functionality, but deprecated. Kube-lego was simple and reliable)
  2. Put your nginx-ingress + cert-manager in kube-system, not in the same namespace as your app
  3. You might have to restart cert manager pods when you modify services. I had issues where cert-manager was not registering my changes.
  4. cert-manager might take ~30 minutes to re-calibrate itself and successfully pull the cert it’s been failing on for the last 6 hours
  5. cert-manager creates secrets when it tries to negotiate, so be mindful of extra resources left around, even if you delete the helm chart
  6. cert-manager injects its own ingress into your service for verifying you own the domain. If you don’t have your service/ingress working properly, cert-manager will not work
  7. If you’re doing DNS changes, cert-manager will take a long time to “uncache” the result. Rebooting kibe-dns doesn’t help.
  8. There’s no documentation for best-practices for setting up 2 different domains with cert-manager (e.g.;
  9. AKS’s HTTP application routing is a neat idea, but you cannot use custom domains. So you’re forced to use its * domain for your services. Great idea, but not useful in real-world scenarios

To summarize, I was finally able to get development and production running in two different namespaces with one ingress controller and one cert-manager. Should have been simple, but death-by-1000-papercuts ensued with managing certs for each of them. Now I’m wiser, but the journey was long and frustrating. That might involve a blog post of its own.

VSTS + Github integration

Estimated time: 4 hours. Actual time: 2 hours

VSTS makes CI/CD easy. Real easy. Almost too easy.

I lost some time (and ~8 failed builds) because the VSTS UX isn’t intuitive to me and documentation is sparse. But now that it’s working, I have a fully automated Github commit -> Production release pipeline which completes within 5 minutes. This will save me a tremendous amount of time in the future. This is what I’m most excited about.

Azure Monitor + Container Monitoring Solution + LogAnalytics

Estimated time: 3 hour. Actual time: None.

This was the surprising part. All of this work was already done for me by setting up the AKS cluster and integrated into the portal. I was impressed that this was glued together without any additional effort needed.

That said, here’s some “gotchas”:

  • The LogAnalytics SLA is ~6 hours. My testing showed that new logs showed up within 5 minutes, but after a cluster is newly created, initial logs would take ~30 minutes to appear.
  • The LogAnalytics UX isn’t intuitive, but the query language is extremely powerful and each of the pods logs were available by clicking through the dashboard.
  • Monitoring and Logging are two pillars of the solution; however, Alerting is missing from the documentation. That integration is forthcoming, and will likely involve another blog entry.
  • The “Health” tile is useful for getting an overview of your cluster; however, the “Metrics” tile seems pretty limited. Both are still in Preview, and I expect to see additional improvements coming soon.

Production DNS migrated

Estimated time: 1 hour. Actual time: 1 hour

Since I did the heavy lifting in the “Securely expose UI + API” section, this was as easy as flipping a light switch and updating the DNS record in my registrar ( No real magic here.


This has been a wonderful learning experience for me, because I was not just trying to showcase AKS/K8S and its potential, but also using it as it is intended to be used, thus getting my hands dirtier than normal. Most of the underestimated time was spent on a few issues that “rat-holed” me due to technical misunderstandings and gaps in my knowledge. I’ve filled in many of those gaps now and hope that it saves you some time too.

If this has been valuable for you, please let me know by commenting below. And if you’re interesting in getting a DuckieHunt duck, let me know as I’d love to see more take flight!

P.S. The source code for this project is also available here.

How to SSH into an AKS agent node

WARNING: SSH’ing into an agent node is an anti-pattern and should be avoided. However, we don’t live in an ideal world, and sometimes we have to do the needful.


This walkthrough creates an SSH Server running as a Pod in your Kubernetes cluster and uses it as a jumpbox to the agent nodes. It is designed for users managing a Kubernetes cluster who cannot readily SSH to into their agent nodes (e.g. AKS) does not publicly expose the agent nodes for security considerations).

This is one of the steps in the Kubernetes Workshop I have built when working with our partners.


It has been tested in AKS cluster; however, it should also work in other cloud providers.

You can follow the steps on the SSH to AKS Cluster Nodes walkthrough; however, that requires you to upload your Private SSH key which I would rather avoid.


* The SSH Public key has been installed for your user on the Agent host
* You have jq installed Not vital, but makes the last step easier to understand.

Install an SSH Server

If you’re paranoid, you can generate your own SSH server container; however, [this one by Corbin Uselton]( has some pretty good security defaults and is available on Docker Hub.

kubectl run ssh-server --image=corbinu/ssh-server --port=22 --restart=Never

Setup port forward

Instead of exposing a service with an IP+Port, we’ll take the easy way and use kubectl to port-forward to your localhost.

NOTE: Run this in a separate window since it will need to be running for as long as you want the SSH connection

kubectl port-forward ssh-server 2222:22

Inject your Public SSH key

Since we’re using the ssh-server as a jumphost, we need to inject our SSH key into the SSH Server. Using root for simplicity’s sake, but I recommend a more secure approach going forward. (TODO: Change this to use a non-privileged user.)

cat ~/.ssh/ | kubectl exec -i ssh-server -- /bin/bash -c "cat >> /root/.ssh/authorized_keys"

SSH to the proxied port

Using the SSH Server as a jumphost (via port-forward proxy), ssh into the IP address of the desired host.

# Get the list of Host + IP's
kubectl get nodes -o json | jq '.items[].status.addresses[].address'
# $USER = Username on the agent host
# $IP = IP of the agent host
ssh -J root@ $USER@$IP

NOTE: If you get “WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!” You might need to add `-o StrictHostKeyChecking=no` to the SSH command if you bounce across clusters. This is because SSH believes that the identity of the host has changed and you need to either remove that entry from your `~/.ssh/known_hosts` or tell it to ignore the host identity.


  • kubectl delete pod ssh-server
  • Kill the kubectl port-forward command