<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.7.4">Jekyll</generator><link href="https://omgjlk.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://omgjlk.github.io/" rel="alternate" type="text/html" /><updated>2018-12-25T00:29:27+00:00</updated><id>https://omgjlk.github.io/feed.xml</id><title type="html">Derp Ops – The blog of Jesse Keating</title><subtitle>A computer, bike, and climb nerd living in the Pacific Northwest. I do cloudy things with computers, currently at GitHub.
</subtitle><author><name>Jesse Keating</name></author><entry><title type="html">Kicking the tires of the Brigade service for Kubernetes</title><link href="https://omgjlk.github.io/github/python/computers/docker/kubernetes/2017/11/14/kicking-the-tires-of-brigade.html" rel="alternate" type="text/html" title="Kicking the tires of the Brigade service for Kubernetes" /><published>2017-11-14T21:57:24+00:00</published><updated>2017-11-14T21:57:24+00:00</updated><id>https://omgjlk.github.io/github/python/computers/docker/kubernetes/2017/11/14/kicking-the-tires-of-brigade</id><content type="html" xml:base="https://omgjlk.github.io/github/python/computers/docker/kubernetes/2017/11/14/kicking-the-tires-of-brigade.html">&lt;p&gt;Recently, the Azure team &lt;a href=&quot;https://open.microsoft.com/2017/10/23/announcing-brigade-event-driven-scripting-kubernetes/&quot;&gt;announced&lt;/a&gt;&amp;nbsp;a new open source service called Brigade. The point of this service is to provide a neat way to execute pipelines of job executions via containers within a Kubernetes&amp;nbsp;system, in response to events from the likes of GitHub or Dockerhub. I thought I'd take it for a test drive on the &lt;a href=&quot;https://www.ibm.com/cloud/container-service&quot;&gt;IBM Cloud Containers Service&lt;/a&gt;,&amp;nbsp;which exposes Kubernetes.&lt;/p&gt;
&lt;h2&gt;Background Knowledge&lt;/h2&gt;
&lt;p&gt;This post assumes some level of understanding of &lt;a href=&quot;https://kubernetes.io/&quot;&gt;Kubernetes&lt;/a&gt;, &lt;a href=&quot;https://www.docker.com/&quot;&gt;(Docker) Containers&lt;/a&gt;, and GitHub &lt;a href=&quot;https://help.github.com/articles/about-pull-requests/&quot;&gt;pull requests&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;What's the point?&lt;/h2&gt;
&lt;p&gt;Why would somebody want a Brigade like service? There are many reasons, but for me the reason that stood out was an easy way to set up a Continuous Integration and Continuous Delivery pipeline. That is, I wanted proposed changes to code I have hosted on GitHub to be tested before I merge it, and once merged, to be deployed somewhere. There are many options available to provide CI/CD services in the (growing)&amp;nbsp;&lt;a href=&quot;https://github.com/marketplace/category/continuous-integration&quot;&gt;GitHub Marketplace&lt;/a&gt;, however being in control of your own service and infrastructure, as well as being able to potentially tie into other event streams, is an appealing thought. Full disclosure, I was recently working on a &lt;a href=&quot;https://github.com/BonnyCI&quot;&gt;project&lt;/a&gt; to add another option to the GitHub Marketplace for CI/CD, based on the open source &lt;a href=&quot;https://docs.openstack.org/infra/zuul/feature/zuulv3/index.html&quot;&gt;Zuul&lt;/a&gt; project from OpenStack.&lt;/p&gt;
&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;The installation process for Brigade was very easy, which is to be expected from the same team that created &lt;a href=&quot;https://helm.sh&quot;&gt;Helm&lt;/a&gt;. It should be no surprise that the installation is managed via Helm. Pretty much any Kubernetes system will suffice, and for my experimentation I made use of the IBM Containers Service. I could have just as easily used &lt;a href=&quot;https://kubernetes.io/docs/getting-started-guides/minikube/&quot;&gt;Minikube&lt;/a&gt;, however one aspect of the Brigade service is ingesting &lt;a href=&quot;https://developer.github.com/webhooks/&quot;&gt;GitHub webhook&lt;/a&gt; events. Having a Kubernetes cluster available that supported LoadBalancer services makes things a bit easier.&lt;/p&gt;
&lt;h3&gt;Installing the Brigade Service&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&quot;https://github.com/Azure/brigade/blob/master/docs/topics/install.md&quot;&gt;installation documentation&lt;/a&gt; from Brigade is very straight forward. All I had to do was make sure I had Helm installed on my laptop and my cluster, and that my &lt;code&gt;kubectl&lt;/code&gt;&amp;nbsp;client was properly configured to reach my cluster. There are two Helm charts to deal with. One chart defines a project to interact with, such as a GitHub repository. This chart is expected to be used multiple times, once for each repository to be interacted with. As such, there is an example set of values that need to be copied and defined specifically for the repository. In my case, I populated the `myvalues.js` file with details for a GitHub repository I specifically created for this experiment, `j2sol/demobrigade`. I defined a secret phrase that will be used to validate incoming events as having come from GitHub, and supplied a GitHub&lt;a href=&quot;https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/&quot;&gt; personal access token&lt;/a&gt; so that the Brigade install can act on my behalf when interfacing with the GitHub API. I left the rest of the values alone, and installed the chart:&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:sh decode:true&quot;&gt;&gt; helm install --name demobrigade-project ./chart/brigade-project -f myvalues.yaml
NAME:   demobrigade-project
LAST DEPLOYED: Mon Nov 13 10:13:36 2017
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==&gt; v1/Secret
NAME                                                            TYPE    DATA  AGE
brigade-463662f2bc06a89c2464d52d1b248602a1085ed2ccf2953df6a8fa  Opaque  6     0s


NOTES:
Your new Brigade project has been created.

This is a brigade project for github.com/j2sol/demobrigade

It will clone the public repo

To check it's state, run:

  helm status demobrigade-project&lt;/pre&gt;
&lt;p&gt;The other Helm chart provided in the Brigade project repository is the chart for the Brigade services themselves. This is expected to be installed only once. (They &lt;a href=&quot;https://github.com/Azure/brigade/blob/master/docs/topics/security.md#securing-brigade&quot;&gt;advocate&lt;/a&gt; one Brigade install per tenant, where a tenant is one or more people responsible for one or more associated repositories. I'd take that a bit further and advocate for independent Kubernetes clusters per Tenant.) For my installation, I needed to tweak one feature, which is the &lt;code&gt;rbac&lt;/code&gt;&amp;nbsp;feature. IBM Cloud Container Service's Kubernetes supports Role Base Access Control, and thus my Brigade install needed to account for that. I did not need to edit any files to account for this, I just used an extra option during the helm install:&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:sh decode:true&quot;&gt;&gt; helm install --name brigade ./chart/brigade --set rbac.enabled=true
NAME:   brigade
LAST DEPLOYED: Mon Nov 13 10:14:09 2017
NAMESPACE: default
STATUS: DEPLOYED

RESOURCES:
==&gt; v1beta1/RoleBinding
NAME                  AGE
brigade-brigade-api   2s
brigade-brigade-ctrl  2s
brigade-brigade-gw    2s
brigade-brigade-wrk   2s

==&gt; v1/Service
NAME                 TYPE          CLUSTER-IP      EXTERNAL-IP     PORT(S)         AGE
brigade-brigade-api  ClusterIP     172.21.13.101   &lt;none&gt;          7745/TCP        2s
brigade-brigade-gw   LoadBalancer  172.21.219.206  169.48.217.138  7744:32586/TCP  2s

==&gt; v1beta1/Deployment
NAME                  DESIRED  CURRENT  UP-TO-DATE  AVAILABLE  AGE
brigade-brigade-api   1        1        1           0          2s
brigade-brigade-ctrl  1        1        1           0          2s
brigade-brigade-gw    1        1        1           0          2s

==&gt; v1/Pod(related)
NAME                                   READY  STATUS             RESTARTS  AGE
brigade-brigade-api-57d56ff7f8-rppkh   0/1    ContainerCreating  0         2s
brigade-brigade-ctrl-69ff6bd48f-h49mc  0/1    ContainerCreating  0         2s
brigade-brigade-gw-5bc49bd866-jgd79    0/1    ContainerCreating  0         2s

==&gt; v1/ServiceAccount
NAME                  SECRETS  AGE
brigade-brigade-api   1        2s
brigade-brigade-ctrl  1        2s
brigade-brigade-gw    1        2s
brigade-worker        1        2s

==&gt; v1beta1/Role
NAME                  AGE
brigade-brigade-api   2s
brigade-brigade-ctrl  2s
brigade-brigade-gw    2s
brigade-brigade-wrk   2s


NOTES:
Brigade is now installed!

To find out about your newly configured system, run:

  $ helm status brigade&lt;/pre&gt;
&lt;p&gt;At this point, all the Brigade pods had been created and the services had also been created. For the next step I needed information about the &lt;a href=&quot;https://github.com/Azure/brigade/blob/master/docs/topics/design.md#terminology&quot;&gt;Brigade Gateway&lt;/a&gt; service, easily obtainable from the above output, or by using &lt;code&gt;kubectl&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:sh mark:4 decode:true&quot;&gt;&gt; kubectl get services
NAME                  CLUSTER-IP       EXTERNAL-IP      PORT(S)          AGE
brigade-brigade-api   172.21.13.101    &lt;none&gt;           7745/TCP         1h
brigade-brigade-gw    172.21.219.206   169.48.217.138   7744:32586/TCP   1h
kubernetes            172.21.0.1       &lt;none&gt;           443/TCP          2d&lt;/pre&gt;
&lt;p&gt;The information we need is the &lt;code&gt;EXTERNAL-IP&lt;/code&gt;&amp;nbsp;and the external port, which in this case is `169.48.217.138&amp;nbsp;32586`. With this information I can moved on to the GitHub repository setup.&lt;/p&gt;
&lt;h3&gt;Configuring GitHub&lt;/h3&gt;
&lt;p&gt;My running Brigade service is rather useless without events to act upon. The events I wanted to react to come from GitHub, in the form of webhooks. As certain things happen to a repository on GitHub, it can send data about those events to a set URL. Setting this up requires administrative rights to the repository within GitHub. To set this up, I followed the &lt;a href=&quot;https://github.com/Azure/brigade/blob/master/docs/topics/github.md#configuring&quot;&gt;provided instructions&lt;/a&gt;, replacing the &lt;code&gt;Payload URL&lt;/code&gt;&amp;nbsp;with data discovered in the above section, and the &lt;code&gt;Secret&lt;/code&gt;&amp;nbsp;with the string I added to &lt;code&gt;myvalues.yaml&lt;/code&gt;&amp;nbsp;earlier. Since my demo repository is public, I did not go through the steps to set up a private repository.&lt;/p&gt;
&lt;p&gt;Upon completing the webhook configuration, GitHub will send an event to the webhook, a &lt;code&gt;ping&lt;/code&gt;&amp;nbsp;event. This is visible in the &lt;code&gt;Recent Deliveries&lt;/code&gt;&amp;nbsp;section of the webhook configuration page. It should have a little green checkmark next to the UUID of the delivery. This lets me know that my GitHub project is able to communicate with my Brigade service as expected.&lt;/p&gt;
&lt;h3&gt;Create a brigade.js file&lt;/h3&gt;
&lt;p&gt;With my Brigade install reacting to GitHub events, it's time to direct it to&amp;nbsp;&lt;strong&gt;do&lt;/strong&gt; something when those events occur. The Brigade architecture relies on a special file within the git repository in question to inform the system what to do. This file is expected to be JavaScript. To start with, a simple statement to log a can be used:&lt;/p&gt;
&lt;pre class=&quot;lang:js decode:true&quot;&gt;console.log(&quot;hello from Brigade&quot;)&lt;/pre&gt;
&lt;p&gt;I created this file and pushed it to a &lt;a href=&quot;https://github.com/j2sol/demobrigade/blob/simple-brigade/brigade.js&quot;&gt;branch&lt;/a&gt;. Because I pushed this branch to the repository that is generating webhooks, Brigade got events for the branch creation and push of a commit to the branch. I was able to see this by checking the logs of the gateway pod's container:&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:sh decode:true &quot;&gt;&gt; kubectl get pods
NAME                                    READY     STATUS    RESTARTS   AGE
brigade-brigade-api-57d56ff7f8-rppkh    1/1       Running   0          4h
brigade-brigade-ctrl-69ff6bd48f-h49mc   1/1       Running   0          4h
brigade-brigade-gw-5bc49bd866-jgd79     1/1       Running   0          4h

&gt; kubectl logs brigade-brigade-gw-5bc49bd866-jgd79
&lt;skipping&gt;
[GIN] 2017/11/13 - 21:55:45 | 400 |       77.33&amp;micro;s |  10.184.120.244 |  POST     /events/github
Expected event push, got create
[GIN] 2017/11/13 - 21:58:43 | 400 |     451.036&amp;micro;s |  10.184.120.244 |  POST     /events/github
&lt;skipping&gt;&lt;/pre&gt;
&lt;p&gt;I can also look at the logs of the Brigade controller to get some more information:&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:sh decode:true &quot;&gt;&gt; kubectl logs brigade-brigade-ctrl-69ff6bd48f-h49mc
&lt;skipping&gt;
handler.go:16: EventHandler: type=push provider=github commit=c967a7e629167d7672f31b2061e12add0ff6cc9f
handler.go:43: Started brigade-worker-01byvp8jqfn9vggv0exbe24wbx-c967a7e6 for &quot;push&quot; [c967a7e629167d7672f31b2061e12add0ff6cc9f] at -62135596800&lt;/pre&gt;
&lt;p&gt;If I follow the clues, I can get the logs from the worker pod that was started:&lt;/p&gt;
&lt;pre class=&quot;ranges:false nums:false lang:sh mark:6 decode:true&quot;&gt;&gt; kubectl logs brigade-worker-01byvp8jqfn9vggv0exbe24wbx-c967a7e6
yarn run v1.3.2
$ node prestart.js
prestart: src/brigade.js written
$ node --no-deprecation ./dist/src/index.js
hello from Brigade
Creating PVC named brigade-worker-01byvp8jqfn9vggv0exbe24wbx-c967a7e6
after: default event fired
beforeExit(2): destroying storage
Destroying PVC named brigade-worker-01byvp8jqfn9vggv0exbe24wbx-c967a7e6
Done in 0.87s.&lt;/pre&gt;
&lt;p&gt;With this output I can see the string I asked to be logged to the console, &lt;code&gt;hello from Brigade&lt;/code&gt;. All the basics are set up and functioning as expected!&lt;/p&gt;
&lt;h2&gt;Beyond the basics&lt;/h2&gt;
&lt;p&gt;Simply logging words to the console of a container isn't very useful. One of the basic capabilities of Brigade is to launch containers to do actions upon certain events. For instance, I'd like to validate that my demoapp code passes &lt;a href=&quot;https://pycodestyle.readthedocs.io/en/latest/&quot;&gt;pycodestyle&lt;/a&gt;. To achieve this, I can follow the &lt;a href=&quot;https://github.com/Azure/brigade/blob/master/docs/topics/scripting.md&quot;&gt;scripting guide&lt;/a&gt; in order to update my &lt;code&gt;brigade.js&lt;/code&gt;&amp;nbsp;file to run &lt;code&gt;pycodestyle&lt;/code&gt;&amp;nbsp;within a container using the &lt;code&gt;python:alpine&lt;/code&gt;&amp;nbsp;image on &lt;code&gt;pull_request&lt;/code&gt;&amp;nbsp;events. I've created such a script and opened a &lt;a href=&quot;https://github.com/j2sol/demobrigade/pull/11/files&quot;&gt;pull request&lt;/a&gt;. The content of &lt;code&gt;brigade.js&lt;/code&gt;&amp;nbsp;is:&lt;/p&gt;
&lt;pre class=&quot;lang:js decode:true &quot;&gt;const { events, Job } = require(&quot;brigadier&quot;)

events.on(&quot;pull_request&quot;, () =&gt; {
    var job = new Job(&quot;pycodestyle&quot;, &quot;python:alpine&quot;)
    job.tasks = [
      &quot;pip install pycodestyle&quot;,
      &quot;cd /src&quot;,
      &quot;pycodestyle demoapp&quot;
    ]
    job.run()
})&lt;/pre&gt;
&lt;p&gt;Don't worry if you don't know JavaScript, I don't either. I'm just using details from the scripting guide to create my file content. I'm making use of the &lt;code&gt;/src/&lt;/code&gt;&amp;nbsp;directory, as that's where Brigade has made the code from my repository available. The version of code that is checked out is the tip of the pull request. Once again, I can log the controller pod to discover the worker pod that was created in reaction to the pull request event:&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:sh decode:true &quot;&gt;&gt; kubectl logs brigade-brigade-ctrl-69ff6bd48f-h49mc
&lt;skipping&gt;
handler.go:16: EventHandler: type=pull_request provider=github commit=4023f592ff7a4afb24d667c720a4b21b4c78b6a4
handler.go:43: Started brigade-worker-01byvsvswfc6j0g9bmebn29963-4023f592 for &quot;pull_request&quot; [4023f592ff7a4afb24d667c720a4b21b4c78b6a4] at -62135596800&lt;/pre&gt;
&lt;p&gt;The logs for the worker pod show something interesting:&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:sh mark:18,19 decode:true &quot;&gt;&gt; kubectl logs brigade-worker-01byvsvswfc6j0g9bmebn29963-4023f592
yarn run v1.3.2
$ node prestart.js
prestart: src/brigade.js written
$ node --no-deprecation ./dist/src/index.js
Creating PVC named brigade-worker-01byvsvswfc6j0g9bmebn29963-4023f592
looking up default/github-com-j2sol-demobrigade-pycodestyle
Creating secret pycodestyle-1510614103251-4023f592
Creating Job Cache PVC github-com-j2sol-demobrigade-pycodestyle
undefined
Creating pod pycodestyle-1510614103251-4023f592
Timeout set at 900000
default/pycodestyle-1510614103251-4023f592 phase Pending
default/pycodestyle-1510614103251-4023f592 phase Pending
default/pycodestyle-1510614103251-4023f592 phase Pending
default/pycodestyle-1510614103251-4023f592 phase Pending
default/pycodestyle-1510614103251-4023f592 phase Running
default/pycodestyle-1510614103251-4023f592 phase Failed
FATAL: job pycodestyle(pycodestyle-1510614103251-4023f592): Pod pycodestyle-1510614103251-4023f592 failed to run to completion (rejection)
error handler is cleaning up
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.&lt;/pre&gt;
&lt;p&gt;It appears that the pod created for my job has failed. I'll look at the logs of the failed pod:&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:sh decode:true &quot;&gt;&gt; kubectl logs pycodestyle-1510614103251-4023f592
Collecting pycodestyle
  Downloading pycodestyle-2.3.1-py2.py3-none-any.whl (45kB)
Installing collected packages: pycodestyle
Successfully installed pycodestyle-2.3.1
demoapp/__init__.py:4:1: E302 expected 2 blank lines, found 1
demoapp/__init__.py:9:17: E225 missing whitespace around operator
demoapp/__init__.py:12:1: E302 expected 2 blank lines, found 1
demoapp/__init__.py:17:1: E305 expected 2 blank lines after class or function definition, found 1&lt;/pre&gt;
&lt;p&gt;Looks like my code does not currently pass &lt;code&gt;pycodestyle&lt;/code&gt;! Normally I'd fix those as part of the pull request to add the testing, but I want to leave this pull request open for the sake of the blog. I'll open a &lt;a href=&quot;https://github.com/j2sol/demobrigade/pull/12&quot;&gt;new one&lt;/a&gt; that has the fixed contents on a new branch. This time the worker pod logs show a successful run:&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:sh mark:15 decode:true &quot;&gt;&gt; kubectl logs brigade-worker-01byxzk5nm3vfxvw763a1743xc-c2347081
yarn run v1.3.2
$ node prestart.js
prestart: src/brigade.js written
$ node --no-deprecation ./dist/src/index.js
Creating PVC named brigade-worker-01byxzk5nm3vfxvw763a1743xc-c2347081
looking up default/github-com-j2sol-demobrigade-pycodestyle
Creating secret pycodestyle-1510687220035-c2347081
Creating Job Cache PVC github-com-j2sol-demobrigade-pycodestyle
undefined
Creating pod pycodestyle-1510687220035-c2347081
Timeout set at 900000
default/pycodestyle-1510687220035-c2347081 phase Pending
default/pycodestyle-1510687220035-c2347081 phase Running
default/pycodestyle-1510687220035-c2347081 phase Succeeded
after: default event fired
beforeExit(2): destroying storage
Destroying PVC named brigade-worker-01byxzk5nm3vfxvw763a1743xc-c2347081
Done in 6.98s.&lt;/pre&gt;
&lt;h2&gt;Pipelines&lt;/h2&gt;
&lt;p&gt;Reacting to events to run tests is a pretty common thing. There's nothing special about it. What really makes Brigade interesting is the ability to run multiple jobs in multiple containers, and to create &lt;a href=&quot;https://github.com/Azure/brigade/blob/master/docs/topics/scripting.md#groups&quot;&gt;groups&lt;/a&gt; of those jobs which can either run in parallel or serially. There is also the ability to use &lt;a href=&quot;https://github.com/Azure/brigade/blob/master/docs/topics/scripting.md#storing-data-with-caches-and-shared-space&quot;&gt;content&lt;/a&gt; and &lt;a href=&quot;https://github.com/Azure/brigade/blob/master/docs/topics/scripting.md#jobs-and-return-values&quot;&gt;output&lt;/a&gt; generated as part of a job in a later job.&lt;/p&gt;
&lt;p&gt;For my simple demo repository, I want to set up a functional test of my application, and alter the &lt;code&gt;brigade.js&lt;/code&gt;&amp;nbsp;file to run the functional test and the style test in parallel via two containers. Another &lt;a href=&quot;https://github.com/j2sol/demobrigade/pull/14&quot;&gt;pull request&lt;/a&gt;&amp;nbsp;shows this change. The content of the &lt;code&gt;brigade.js&lt;/code&gt;&amp;nbsp;file is now:&lt;/p&gt;
&lt;pre class=&quot;lang:js decode:true &quot;&gt;const { events, Job, Group } = require(&quot;brigadier&quot;)

events.on(&quot;pull_request&quot;, () =&gt; {
    var style = new Job(&quot;pycodestyle&quot;, &quot;python:alpine&quot;)
    style.tasks = [
      &quot;pip install pycodestyle&quot;,
      &quot;cd /src&quot;,
      &quot;pycodestyle demoapp&quot;
    ]

    var functional = new Job(&quot;functional&quot;, &quot;python:alpine&quot;)
    functional.tasks = [
      &quot;apk update &amp;amp;&amp;amp; apk add curl&quot;,
      &quot;pip install /src/&quot;,
      &quot;/usr/local/bin/demoapp &amp;amp;&quot;,
      &quot;sleep 2&quot;,
      &quot;curl http://127.0.0.1:8000&quot;
    ]

    Group.runAll([style, functional])
})&lt;/pre&gt;
&lt;p&gt;Viewing the logs from the worker pod that was created for the &lt;code&gt;pull_request&lt;/code&gt;&amp;nbsp;event shows the new job names and the pods used. The output also shows how they're running in parallel:&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:sh decode:true &quot;&gt;&gt; kubectl logs brigade-worker-01byy57yadgr1khq86q5z6k07g-96842797
yarn run v1.3.2
$ node prestart.js
prestart: src/brigade.js written
$ node --no-deprecation ./dist/src/index.js
Creating PVC named brigade-worker-01byy57yadgr1khq86q5z6k07g-96842797
looking up default/github-com-j2sol-demobrigade-pycodestyle
looking up default/github-com-j2sol-demobrigade-functional
Creating secret pycodestyle-1510693143841-96842797
Creating secret functional-1510693143844-96842797
Creating Job Cache PVC github-com-j2sol-demobrigade-pycodestyle
undefined
undefined
Creating Job Cache PVC github-com-j2sol-demobrigade-functional
Creating pod pycodestyle-1510693143841-96842797
Creating pod functional-1510693143844-96842797
Timeout set at 900000
Timeout set at 900000
default/pycodestyle-1510693143841-96842797 phase Pending
default/functional-1510693143844-96842797 phase Pending
default/functional-1510693143844-96842797 phase Running
default/pycodestyle-1510693143841-96842797 phase Running
default/pycodestyle-1510693143841-96842797 phase Succeeded
default/functional-1510693143844-96842797 phase Running
default/functional-1510693143844-96842797 phase Running
default/functional-1510693143844-96842797 phase Running
default/functional-1510693143844-96842797 phase Succeeded
after: default event fired
beforeExit(2): destroying storage
Destroying PVC named brigade-worker-01byy57yadgr1khq86q5z6k07g-96842797
Done in 13.03s.&lt;/pre&gt;
&lt;p&gt;I can imagine creating complex sets of jobs within groups to run some tests in parallel, some in serial, more in parallel, while sharing content and output between the sets.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Brigade is an interesting project. It has the capability to create complex workflows in reaction to specific events, and the set up is pretty easy. It may be a better solution than creating your own webhook handler and launching native &lt;a href=&quot;https://kubernetes.io/docs/concepts/workloads/controllers/jobs-run-to-completion/&quot;&gt;Kubernetes Jobs&lt;/a&gt;. As a project, I think it's in the early stages of development. I ran &lt;a href=&quot;https://github.com/Azure/brigade/issues?utf8=✓&amp;amp;q=is%3Aissue%20author%3Aj2sol%20&quot;&gt;into a few issues&lt;/a&gt; as I was playing around. The project developers have been quite responsive and eager for input. I wouldn't recommend taking Brigade into production just yet, but I would encourage exploring the capability and thinking about potential use cases. I'm sure the developers would love more input on how people would like to use Brigade. The project is open source, so if you want to get your hands dirty I'm sure they'd welcome the help too!&lt;/p&gt;</content><author><name>Jesse Keating</name></author><category term="k8s" /><category term="kubernetes" /><category term="python" /><category term="GitHub" /><summary type="html">Recently, the Azure team announced&amp;nbsp;a new open source service called Brigade. The point of this service is to provide a neat way to execute pipelines of job executions via containers within a Kubernetes&amp;nbsp;system, in response to events from the likes of GitHub or Dockerhub. I thought I'd take it for a test drive on the IBM Cloud Containers Service,&amp;nbsp;which exposes Kubernetes. Background Knowledge This post assumes some level of understanding of Kubernetes, (Docker) Containers, and GitHub pull requests. What's the point? Why would somebody want a Brigade like service? There are many reasons, but for me the reason that stood out was an easy way to set up a Continuous Integration and Continuous Delivery pipeline. That is, I wanted proposed changes to code I have hosted on GitHub to be tested before I merge it, and once merged, to be deployed somewhere. There are many options available to provide CI/CD services in the (growing)&amp;nbsp;GitHub Marketplace, however being in control of your own service and infrastructure, as well as being able to potentially tie into other event streams, is an appealing thought. Full disclosure, I was recently working on a project to add another option to the GitHub Marketplace for CI/CD, based on the open source Zuul project from OpenStack. Getting Started The installation process for Brigade was very easy, which is to be expected from the same team that created Helm. It should be no surprise that the installation is managed via Helm. Pretty much any Kubernetes system will suffice, and for my experimentation I made use of the IBM Containers Service. I could have just as easily used Minikube, however one aspect of the Brigade service is ingesting GitHub webhook events. Having a Kubernetes cluster available that supported LoadBalancer services makes things a bit easier. Installing the Brigade Service The installation documentation from Brigade is very straight forward. All I had to do was make sure I had Helm installed on my laptop and my cluster, and that my kubectl&amp;nbsp;client was properly configured to reach my cluster. There are two Helm charts to deal with. One chart defines a project to interact with, such as a GitHub repository. This chart is expected to be used multiple times, once for each repository to be interacted with. As such, there is an example set of values that need to be copied and defined specifically for the repository. In my case, I populated the `myvalues.js` file with details for a GitHub repository I specifically created for this experiment, `j2sol/demobrigade`. I defined a secret phrase that will be used to validate incoming events as having come from GitHub, and supplied a GitHub personal access token so that the Brigade install can act on my behalf when interfacing with the GitHub API. I left the rest of the values alone, and installed the chart: &gt; helm install --name demobrigade-project ./chart/brigade-project -f myvalues.yaml NAME: demobrigade-project LAST DEPLOYED: Mon Nov 13 10:13:36 2017 NAMESPACE: default STATUS: DEPLOYED RESOURCES: ==&gt; v1/Secret NAME TYPE DATA AGE brigade-463662f2bc06a89c2464d52d1b248602a1085ed2ccf2953df6a8fa Opaque 6 0s NOTES: Your new Brigade project has been created. This is a brigade project for github.com/j2sol/demobrigade It will clone the public repo To check it's state, run: helm status demobrigade-project The other Helm chart provided in the Brigade project repository is the chart for the Brigade services themselves. This is expected to be installed only once. (They advocate one Brigade install per tenant, where a tenant is one or more people responsible for one or more associated repositories. I'd take that a bit further and advocate for independent Kubernetes clusters per Tenant.) For my installation, I needed to tweak one feature, which is the rbac&amp;nbsp;feature. IBM Cloud Container Service's Kubernetes supports Role Base Access Control, and thus my Brigade install needed to account for that. I did not need to edit any files to account for this, I just used an extra option during the helm install: &gt; helm install --name brigade ./chart/brigade --set rbac.enabled=true NAME: brigade LAST DEPLOYED: Mon Nov 13 10:14:09 2017 NAMESPACE: default STATUS: DEPLOYED RESOURCES: ==&gt; v1beta1/RoleBinding NAME AGE brigade-brigade-api 2s brigade-brigade-ctrl 2s brigade-brigade-gw 2s brigade-brigade-wrk 2s ==&gt; v1/Service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE brigade-brigade-api ClusterIP 172.21.13.101 7745/TCP 2s brigade-brigade-gw LoadBalancer 172.21.219.206 169.48.217.138 7744:32586/TCP 2s ==&gt; v1beta1/Deployment NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE brigade-brigade-api 1 1 1 0 2s brigade-brigade-ctrl 1 1 1 0 2s brigade-brigade-gw 1 1 1 0 2s ==&gt; v1/Pod(related) NAME READY STATUS RESTARTS AGE brigade-brigade-api-57d56ff7f8-rppkh 0/1 ContainerCreating 0 2s brigade-brigade-ctrl-69ff6bd48f-h49mc 0/1 ContainerCreating 0 2s brigade-brigade-gw-5bc49bd866-jgd79 0/1 ContainerCreating 0 2s ==&gt; v1/ServiceAccount NAME SECRETS AGE brigade-brigade-api 1 2s brigade-brigade-ctrl 1 2s brigade-brigade-gw 1 2s brigade-worker 1 2s ==&gt; v1beta1/Role NAME AGE brigade-brigade-api 2s brigade-brigade-ctrl 2s brigade-brigade-gw 2s brigade-brigade-wrk 2s NOTES: Brigade is now installed! To find out about your newly configured system, run: $ helm status brigade At this point, all the Brigade pods had been created and the services had also been created. For the next step I needed information about the Brigade Gateway service, easily obtainable from the above output, or by using kubectl: &gt; kubectl get services NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE brigade-brigade-api 172.21.13.101 7745/TCP 1h brigade-brigade-gw 172.21.219.206 169.48.217.138 7744:32586/TCP 1h kubernetes 172.21.0.1 443/TCP 2d The information we need is the EXTERNAL-IP&amp;nbsp;and the external port, which in this case is `169.48.217.138&amp;nbsp;32586`. With this information I can moved on to the GitHub repository setup. Configuring GitHub My running Brigade service is rather useless without events to act upon. The events I wanted to react to come from GitHub, in the form of webhooks. As certain things happen to a repository on GitHub, it can send data about those events to a set URL. Setting this up requires administrative rights to the repository within GitHub. To set this up, I followed the provided instructions, replacing the Payload URL&amp;nbsp;with data discovered in the above section, and the Secret&amp;nbsp;with the string I added to myvalues.yaml&amp;nbsp;earlier. Since my demo repository is public, I did not go through the steps to set up a private repository. Upon completing the webhook configuration, GitHub will send an event to the webhook, a ping&amp;nbsp;event. This is visible in the Recent Deliveries&amp;nbsp;section of the webhook configuration page. It should have a little green checkmark next to the UUID of the delivery. This lets me know that my GitHub project is able to communicate with my Brigade service as expected. Create a brigade.js file With my Brigade install reacting to GitHub events, it's time to direct it to&amp;nbsp;do something when those events occur. The Brigade architecture relies on a special file within the git repository in question to inform the system what to do. This file is expected to be JavaScript. To start with, a simple statement to log a can be used: console.log(&quot;hello from Brigade&quot;) I created this file and pushed it to a branch. Because I pushed this branch to the repository that is generating webhooks, Brigade got events for the branch creation and push of a commit to the branch. I was able to see this by checking the logs of the gateway pod's container: &gt; kubectl get pods NAME READY STATUS RESTARTS AGE brigade-brigade-api-57d56ff7f8-rppkh 1/1 Running 0 4h brigade-brigade-ctrl-69ff6bd48f-h49mc 1/1 Running 0 4h brigade-brigade-gw-5bc49bd866-jgd79 1/1 Running 0 4h &gt; kubectl logs brigade-brigade-gw-5bc49bd866-jgd79 [GIN] 2017/11/13 - 21:55:45 | 400 | 77.33&amp;micro;s | 10.184.120.244 | POST /events/github Expected event push, got create [GIN] 2017/11/13 - 21:58:43 | 400 | 451.036&amp;micro;s | 10.184.120.244 | POST /events/github I can also look at the logs of the Brigade controller to get some more information: &gt; kubectl logs brigade-brigade-ctrl-69ff6bd48f-h49mc handler.go:16: EventHandler: type=push provider=github commit=c967a7e629167d7672f31b2061e12add0ff6cc9f handler.go:43: Started brigade-worker-01byvp8jqfn9vggv0exbe24wbx-c967a7e6 for &quot;push&quot; [c967a7e629167d7672f31b2061e12add0ff6cc9f] at -62135596800 If I follow the clues, I can get the logs from the worker pod that was started: &gt; kubectl logs brigade-worker-01byvp8jqfn9vggv0exbe24wbx-c967a7e6 yarn run v1.3.2 $ node prestart.js prestart: src/brigade.js written $ node --no-deprecation ./dist/src/index.js hello from Brigade Creating PVC named brigade-worker-01byvp8jqfn9vggv0exbe24wbx-c967a7e6 after: default event fired beforeExit(2): destroying storage Destroying PVC named brigade-worker-01byvp8jqfn9vggv0exbe24wbx-c967a7e6 Done in 0.87s. With this output I can see the string I asked to be logged to the console, hello from Brigade. All the basics are set up and functioning as expected! Beyond the basics Simply logging words to the console of a container isn't very useful. One of the basic capabilities of Brigade is to launch containers to do actions upon certain events. For instance, I'd like to validate that my demoapp code passes pycodestyle. To achieve this, I can follow the scripting guide in order to update my brigade.js&amp;nbsp;file to run pycodestyle&amp;nbsp;within a container using the python:alpine&amp;nbsp;image on pull_request&amp;nbsp;events. I've created such a script and opened a pull request. The content of brigade.js&amp;nbsp;is: const { events, Job } = require(&quot;brigadier&quot;) events.on(&quot;pull_request&quot;, () =&gt; { var job = new Job(&quot;pycodestyle&quot;, &quot;python:alpine&quot;) job.tasks = [ &quot;pip install pycodestyle&quot;, &quot;cd /src&quot;, &quot;pycodestyle demoapp&quot; ] job.run() }) Don't worry if you don't know JavaScript, I don't either. I'm just using details from the scripting guide to create my file content. I'm making use of the /src/&amp;nbsp;directory, as that's where Brigade has made the code from my repository available. The version of code that is checked out is the tip of the pull request. Once again, I can log the controller pod to discover the worker pod that was created in reaction to the pull request event: &gt; kubectl logs brigade-brigade-ctrl-69ff6bd48f-h49mc handler.go:16: EventHandler: type=pull_request provider=github commit=4023f592ff7a4afb24d667c720a4b21b4c78b6a4 handler.go:43: Started brigade-worker-01byvsvswfc6j0g9bmebn29963-4023f592 for &quot;pull_request&quot; [4023f592ff7a4afb24d667c720a4b21b4c78b6a4] at -62135596800 The logs for the worker pod show something interesting: &gt; kubectl logs brigade-worker-01byvsvswfc6j0g9bmebn29963-4023f592 yarn run v1.3.2 $ node prestart.js prestart: src/brigade.js written $ node --no-deprecation ./dist/src/index.js Creating PVC named brigade-worker-01byvsvswfc6j0g9bmebn29963-4023f592 looking up default/github-com-j2sol-demobrigade-pycodestyle Creating secret pycodestyle-1510614103251-4023f592 Creating Job Cache PVC github-com-j2sol-demobrigade-pycodestyle undefined Creating pod pycodestyle-1510614103251-4023f592 Timeout set at 900000 default/pycodestyle-1510614103251-4023f592 phase Pending default/pycodestyle-1510614103251-4023f592 phase Pending default/pycodestyle-1510614103251-4023f592 phase Pending default/pycodestyle-1510614103251-4023f592 phase Pending default/pycodestyle-1510614103251-4023f592 phase Running default/pycodestyle-1510614103251-4023f592 phase Failed FATAL: job pycodestyle(pycodestyle-1510614103251-4023f592): Pod pycodestyle-1510614103251-4023f592 failed to run to completion (rejection) error handler is cleaning up error Command failed with exit code 1. info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command. It appears that the pod created for my job has failed. I'll look at the logs of the failed pod: &gt; kubectl logs pycodestyle-1510614103251-4023f592 Collecting pycodestyle Downloading pycodestyle-2.3.1-py2.py3-none-any.whl (45kB) Installing collected packages: pycodestyle Successfully installed pycodestyle-2.3.1 demoapp/__init__.py:4:1: E302 expected 2 blank lines, found 1 demoapp/__init__.py:9:17: E225 missing whitespace around operator demoapp/__init__.py:12:1: E302 expected 2 blank lines, found 1 demoapp/__init__.py:17:1: E305 expected 2 blank lines after class or function definition, found 1 Looks like my code does not currently pass pycodestyle! Normally I'd fix those as part of the pull request to add the testing, but I want to leave this pull request open for the sake of the blog. I'll open a new one that has the fixed contents on a new branch. This time the worker pod logs show a successful run: &gt; kubectl logs brigade-worker-01byxzk5nm3vfxvw763a1743xc-c2347081 yarn run v1.3.2 $ node prestart.js prestart: src/brigade.js written $ node --no-deprecation ./dist/src/index.js Creating PVC named brigade-worker-01byxzk5nm3vfxvw763a1743xc-c2347081 looking up default/github-com-j2sol-demobrigade-pycodestyle Creating secret pycodestyle-1510687220035-c2347081 Creating Job Cache PVC github-com-j2sol-demobrigade-pycodestyle undefined Creating pod pycodestyle-1510687220035-c2347081 Timeout set at 900000 default/pycodestyle-1510687220035-c2347081 phase Pending default/pycodestyle-1510687220035-c2347081 phase Running default/pycodestyle-1510687220035-c2347081 phase Succeeded after: default event fired beforeExit(2): destroying storage Destroying PVC named brigade-worker-01byxzk5nm3vfxvw763a1743xc-c2347081 Done in 6.98s. Pipelines Reacting to events to run tests is a pretty common thing. There's nothing special about it. What really makes Brigade interesting is the ability to run multiple jobs in multiple containers, and to create groups of those jobs which can either run in parallel or serially. There is also the ability to use content and output generated as part of a job in a later job. For my simple demo repository, I want to set up a functional test of my application, and alter the brigade.js&amp;nbsp;file to run the functional test and the style test in parallel via two containers. Another pull request&amp;nbsp;shows this change. The content of the brigade.js&amp;nbsp;file is now: const { events, Job, Group } = require(&quot;brigadier&quot;) events.on(&quot;pull_request&quot;, () =&gt; { var style = new Job(&quot;pycodestyle&quot;, &quot;python:alpine&quot;) style.tasks = [ &quot;pip install pycodestyle&quot;, &quot;cd /src&quot;, &quot;pycodestyle demoapp&quot; ] var functional = new Job(&quot;functional&quot;, &quot;python:alpine&quot;) functional.tasks = [ &quot;apk update &amp;amp;&amp;amp; apk add curl&quot;, &quot;pip install /src/&quot;, &quot;/usr/local/bin/demoapp &amp;amp;&quot;, &quot;sleep 2&quot;, &quot;curl http://127.0.0.1:8000&quot; ] Group.runAll([style, functional]) }) Viewing the logs from the worker pod that was created for the pull_request&amp;nbsp;event shows the new job names and the pods used. The output also shows how they're running in parallel: &gt; kubectl logs brigade-worker-01byy57yadgr1khq86q5z6k07g-96842797 yarn run v1.3.2 $ node prestart.js prestart: src/brigade.js written $ node --no-deprecation ./dist/src/index.js Creating PVC named brigade-worker-01byy57yadgr1khq86q5z6k07g-96842797 looking up default/github-com-j2sol-demobrigade-pycodestyle looking up default/github-com-j2sol-demobrigade-functional Creating secret pycodestyle-1510693143841-96842797 Creating secret functional-1510693143844-96842797 Creating Job Cache PVC github-com-j2sol-demobrigade-pycodestyle undefined undefined Creating Job Cache PVC github-com-j2sol-demobrigade-functional Creating pod pycodestyle-1510693143841-96842797 Creating pod functional-1510693143844-96842797 Timeout set at 900000 Timeout set at 900000 default/pycodestyle-1510693143841-96842797 phase Pending default/functional-1510693143844-96842797 phase Pending default/functional-1510693143844-96842797 phase Running default/pycodestyle-1510693143841-96842797 phase Running default/pycodestyle-1510693143841-96842797 phase Succeeded default/functional-1510693143844-96842797 phase Running default/functional-1510693143844-96842797 phase Running default/functional-1510693143844-96842797 phase Running default/functional-1510693143844-96842797 phase Succeeded after: default event fired beforeExit(2): destroying storage Destroying PVC named brigade-worker-01byy57yadgr1khq86q5z6k07g-96842797 Done in 13.03s. I can imagine creating complex sets of jobs within groups to run some tests in parallel, some in serial, more in parallel, while sharing content and output between the sets. Conclusion Brigade is an interesting project. It has the capability to create complex workflows in reaction to specific events, and the set up is pretty easy. It may be a better solution than creating your own webhook handler and launching native Kubernetes Jobs. As a project, I think it's in the early stages of development. I ran into a few issues as I was playing around. The project developers have been quite responsive and eager for input. I wouldn't recommend taking Brigade into production just yet, but I would encourage exploring the capability and thinking about potential use cases. I'm sure the developers would love more input on how people would like to use Brigade. The project is open source, so if you want to get your hands dirty I'm sure they'd welcome the help too!</summary></entry><entry><title type="html">Interactive debugging python code in (mini) Kubernetes</title><link href="https://omgjlk.github.io/python/computers/kubernetes/2017/10/26/interactive-debugging-python-kubernetes.html" rel="alternate" type="text/html" title="Interactive debugging python code in (mini) Kubernetes" /><published>2017-10-26T23:20:10+00:00</published><updated>2017-10-26T23:20:10+00:00</updated><id>https://omgjlk.github.io/python/computers/kubernetes/2017/10/26/interactive-debugging-python-kubernetes</id><content type="html" xml:base="https://omgjlk.github.io/python/computers/kubernetes/2017/10/26/interactive-debugging-python-kubernetes.html">&lt;p&gt;Lately I've been playing around with &lt;a href=&quot;https://kubernetes.io&quot;&gt;Kubernetes&lt;/a&gt;. If you don't know what Kubernetes (k8s) is, then the rest of this post is going to be very confusing to you.&lt;/p&gt;
&lt;h4&gt;Needed concepts&lt;/h4&gt;
&lt;p&gt;I'm going to talk about a few things. Here are some links to places to get up to speed should any of this not make any sense:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io&quot;&gt;Kubernetes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.docker.com&quot;&gt;Docker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.docker.com/compose/&quot;&gt;Docker Compose&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/3.7/installing/index.html&quot;&gt;Python pip&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/getting-started-guides/minikube/&quot;&gt;Minikube&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://kubernetes.io/docs/user-guide/kubectl-overview/&quot;&gt;kubectl&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.python.org/3.7/library/pdb.html&quot;&gt;Python Debugging&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Background&lt;/h3&gt;
&lt;p&gt;I'm somewhat late to the k8s game, and I'm still trying to get my bearings. One task I set out to figure out is how I can replicate my development workflow I had built up with&lt;a href=&quot;https://docs.docker.com/compose/&quot;&gt; Docker Compose&lt;/a&gt;&amp;nbsp;to launch containers of my application locally for testing. The&amp;nbsp;&lt;a href=&quot;https://docs.openstack.org/infra/zuul/feature/zuulv3/index.html&quot;&gt;application&lt;/a&gt;&amp;nbsp;I've been developing on consists of a Zookeeper service and three Python services. I have the Python services&amp;nbsp;broken out into three separate containers based on the same image, but with different launch commands. When testing things locally, I often want to introduce code that I haven't yet committed to a repository, and instead of building new images every time, I make use of a &lt;a href=&quot;https://docs.docker.com/compose/compose-file/#volumes&quot;&gt;volume mount&lt;/a&gt; to bring my code into the container at runtime. This works well with Python thanks to a feature in pip, the tool for installing python packages. I can tell pip to perform an&lt;a href=&quot;https://pip.pypa.io/en/stable/reference/pip_install/#editable-installs&quot;&gt; editable install&lt;/a&gt;. This type of install makes use of a symlink in the installation target path which links to the source directory of the install. In my &lt;a href=&quot;https://github.com/j2sol/z8s/blob/master/Dockerfile-zuul-fedora#L13-L16&quot;&gt;Dockerfile&lt;/a&gt;&amp;nbsp;I clone the source code to &lt;code&gt;/zuul&lt;/code&gt;, and then perform the pip install from there. What this means is that after the install, I could&amp;nbsp;simply alter the files in the original checkout in &lt;code&gt;/zuul&lt;/code&gt;&amp;nbsp;and restart the process and the changes will take effect. To expand on that further, this gives me the ability to &lt;a href=&quot;https://github.com/j2sol/z8s/blob/master/devel.yaml#L5&quot;&gt;attach a volume mount&lt;/a&gt; from my laptop's zuul checkout directory (where I have edited files) to the &lt;code&gt;/zuul&lt;/code&gt;&amp;nbsp;path within the container at start time, so that I can make use of edited files without rebuilding the image.&lt;/p&gt;
&lt;h3&gt;On to Kubernetes!&lt;/h3&gt;
&lt;p&gt;My workflow&amp;nbsp;worked great in docker-compose, but now I want to do this with k8s. To demonstrate how this works, I've created a simple Python web server in a demo app. I've put the &lt;a href=&quot;https://github.com/j2sol/demoapp&quot;&gt;source for this on GitHub&lt;/a&gt; for convenience.&lt;/p&gt;
&lt;p&gt;First, I need to install and run &lt;a href=&quot;https://kubernetes.io/docs/getting-started-guides/minikube/&quot;&gt;minikube&lt;/a&gt;, a tool to run a Kubernetes cluster locally on my laptop. Minikube will download some data and launch a virtual machine in which to run the k8s services, including its own Docker daemon.&lt;/p&gt;
&lt;h5&gt;Build&amp;nbsp;Docker image&lt;/h5&gt;
&lt;p&gt;With minikube running, I next need to configure my Docker client to make use of the Docker Engine within minikube. A simple command `eval $(minikube docker-env)` will set up my client. Now I can build my Docker image so that it'll be available for use within k8s. I've cloned my repository into a &lt;code&gt;src/derpops/demoapp&lt;/code&gt;&amp;nbsp;directory relative to my homer. From there I just need a simple Docker build command to build my image. &lt;strong&gt;NOTE!&lt;/strong&gt; I'm using a tag other than &lt;code&gt;latest&lt;/code&gt;&amp;nbsp;so that Kubernetes will not try to pull the latest version of my image. My image will only exist locally, so a pull would fail.&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:sh decode:true&quot;&gt;$ docker build -t demoapp:demo .
Sending build context to Docker daemon  129.5kB
Step 1 : FROM python:alpine
 ---&gt; 83da41380580
Step 2 : RUN apk --no-cache add --update     git
 ---&gt; Using cache
 ---&gt; bf6e5c3fd23c
Step 3 : RUN git clone https://github.com/j2sol/demoapp.git /demoapp
 ---&gt; Using cache
 ---&gt; eacba0083289
Step 4 : RUN pip install -e /demoapp
 ---&gt; Using cache
 ---&gt; 3f881178557d
Step 5 : RUN pip install rpdb
 ---&gt; Using cache
 ---&gt; 81845e2506d2
Step 6 : RUN apk del git
 ---&gt; Using cache
 ---&gt; 19d2d2bb91d1
Step 7 : CMD demoapp
 ---&gt; Using cache
 ---&gt; 9eddb5c0209a
Successfully built 9eddb5c0209a&lt;/pre&gt;
&lt;h5&gt;Creating a Deployment&lt;/h5&gt;
&lt;p&gt;With the image in place, I can now create a k8s &lt;a href=&quot;https://kubernetes.io/docs/concepts/workloads/controllers/deployment/&quot;&gt;Deployment&lt;/a&gt;. The Deployment lets me define a container to launch, with the image I built above, and a command to run within the container. I can do this with a simple &lt;a href=&quot;https://kubernetes.io/docs/user-guide/kubectl-overview/&quot;&gt;kubectl&lt;/a&gt; command:&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:sh decode:true&quot;&gt;$ kubectl create deployment demoapp --image demoapp:demo
deployment &quot;demoapp&quot; created

$ kubectl get pods
NAME                       READY     STATUS    RESTARTS   AGE
demoapp-3399253556-ngkv0   1/1       Running   0          1s

&lt;/pre&gt;
&lt;p&gt;If my deployment is created successfully, a new pod will show up, which will be running my code.&lt;/p&gt;
&lt;h5&gt;Creating a Service&lt;/h5&gt;
&lt;p&gt;To see if my code is working properly, I need to be able to reach the web server. A k8s &lt;a href=&quot;https://kubernetes.io/docs/concepts/services-networking/service/&quot;&gt;Service&lt;/a&gt; is necessary to set up the networks appropriately. Just like with the Deployment, a simple kubectl command will suffice to create the service:&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:sh decode:true&quot;&gt;$ kubectl create service nodeport demoapp --tcp=8000
service &quot;demoapp&quot; created

$ kubectl get services
NAME         CLUSTER-IP   EXTERNAL-IP   PORT(S)          AGE
demoapp      10.0.0.232   &lt;nodes&gt;       8000:32369/TCP   1m
kubernetes   10.0.0.1     &lt;none&gt;        443/TCP          23h&lt;/pre&gt;
&lt;p&gt;This command created a service that will allow me to reach port 8000 of the container running the application. My &lt;a href=&quot;https://github.com/j2sol/demoapp/blob/master/demoapp/__init__.py#L13&quot;&gt;code specifically tells&lt;/a&gt; the python library to use port 8000 and to listen on all addresses. This type of service is a &lt;a href=&quot;https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport&quot;&gt;nodeport&lt;/a&gt;, which makes this port reachable from every node. However, since this is minikube, I only have one node, and I can ask minikube to tell me what the IP address of the node is with the &lt;code&gt;service&lt;/code&gt;&amp;nbsp;command. I'll ask it to just display the URL, instead of opening the URL in my browser, and then use &lt;code&gt;curl&lt;/code&gt;&amp;nbsp;to access the URL:&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:sh decode:true&quot;&gt;$ minikube service demoapp --url
http://192.168.64.3:32369

$ curl http://192.168.64.3:32369
Hi there!%&lt;/pre&gt;
&lt;h5&gt;Injecting new code&lt;/h5&gt;
&lt;p&gt;My application works, but now I want to alter the code. To get new code into my container, I need to add a volume mount to my deployment.&lt;/p&gt;
&lt;p&gt;VolumeMounts are used to expose content into a container. A VolumeMount definition combines the name of a volume and a path to mount it within the container. The name matches a defined &lt;a href=&quot;https://kubernetes.io/docs/concepts/storage/volumes/&quot;&gt;Volume&lt;/a&gt;, of which many types are supported. The type we're interested in, the&amp;nbsp;&lt;a href=&quot;https://kubernetes.io/docs/concepts/storage/volumes/#hostpath&quot;&gt;&lt;code&gt;hostPath&lt;/code&gt;&lt;/a&gt;&amp;nbsp;type,&amp;nbsp;exposes a file or a directory from the node (the machine a container is running on). The use of minikube &lt;a href=&quot;https://kubernetes.io/docs/getting-started-guides/minikube/#mounted-host-folders&quot;&gt;automatically exposes a folder&lt;/a&gt; from the host machine minikube runs on into the VM where k8s is running, which is the node. In my case, the &lt;code&gt;/Users&lt;/code&gt;&amp;nbsp;directory on my laptop is exposed as &lt;code&gt;/Users&lt;/code&gt;&amp;nbsp;on the node, and thus I can make use of this in a &lt;code&gt;hostPath&lt;/code&gt;&amp;nbsp;volume.&lt;/p&gt;
&lt;p&gt;To add a volume to my Deployment, I'll need to create a &lt;code&gt;yaml&lt;/code&gt;&amp;nbsp;file describing my Deployment spec. I can do this quickly by repeating the earlier command but adding arguments to print out YAML:&lt;/p&gt;
&lt;pre class=&quot;lang:yaml decode:true&quot;&gt;$ kubectl create deployment demoapp --image demoapp:demo --dry-run -o yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: demoapp
  name: demoapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: demoapp
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: demoapp
    spec:
      containers:
      - image: demoapp:demo
        name: demoapp
        resources: {}
status: {}&lt;/pre&gt;
&lt;p&gt;I saved this output to a new file, &amp;nbsp;&lt;code&gt;demoapp-deployment.yaml&lt;/code&gt;, where I can make adjustments as needed. I need to alter the &lt;code&gt;containers&lt;/code&gt;&amp;nbsp;spec for the &lt;code&gt;demoapp&lt;/code&gt;&amp;nbsp;container to define a &lt;code&gt;volumeMount&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:yaml mark:2-4 decode:true&quot;&gt;        resources: {}
        volumeMounts:
          - name: demosource
            mountPath: /demoapp/demoapp
status: {}&lt;/pre&gt;
&lt;p&gt;This references a volume by the name of &lt;code&gt;demosource&lt;/code&gt;&amp;nbsp;which I also need to define in the &lt;code&gt;spec&lt;/code&gt;&amp;nbsp;as a new key:&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:yaml mark:2-5 decode:true&quot;&gt;            mountPath: /demoapp/demoapp
      volumes:
        - name: demosource
          hostPath:
            path: /Users/jkeating/src/derpops/demoapp/demoapp
status: {}&lt;/pre&gt;
&lt;p&gt;These additions will cause the directory &lt;code&gt;/Users/jkeating/src/derpops/demoapp/demoapp&lt;/code&gt;&amp;nbsp;to be mounted to the path &lt;code&gt;/demoapp/demoapp&lt;/code&gt;&amp;nbsp;within the container when it launches. This will overlay the version of code on my laptop on top of the code that already exists in the container.&lt;/p&gt;
&lt;p&gt;To get the new definition of my Deployment in use, I'll delete the existing Deployment and create a new one from the YAML file:&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:sh decode:true&quot;&gt;$ kubectl delete deployment demoapp
deployment &quot;demoapp&quot; deleted

$ kubectl create -f demoapp-deployment.yaml
deployment &quot;demoapp&quot; created

$ kubectl get pods
NAME                       READY     STATUS        RESTARTS   AGE
demoapp-2344086337-x6697   1/1       Running       0          1s
demoapp-3399253556-ngkv0   1/1       Terminating   0          30m&lt;/pre&gt;
&lt;p&gt;The existing pod is being terminated while a new pod is running. If all went well, I should still be able to use &lt;code&gt;curl&lt;/code&gt;&amp;nbsp;to reach the web server:&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:sh decode:true&quot;&gt;$ curl http://192.168.64.3:32369
Hi there!%&lt;/pre&gt;
&lt;p&gt;The output is the same as earlier, as I haven't changed any code. But what if I change the value of the variable &lt;a href=&quot;https://github.com/j2sol/demoapp/blob/master/demoapp/__init__.py#L9&quot;&gt;RESPONSE&lt;/a&gt;&amp;nbsp;to something new by editing the file &lt;code&gt;demoapp/__init__.py&lt;/code&gt;&amp;nbsp;on my laptop.&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:python mark:2 decode:true&quot;&gt;        self.end_headers()
        RESPONSE='''Edited code'''
        self.wfile.write(bytes(RESPONSE, 'UTF-8'))&lt;/pre&gt;
&lt;p&gt;To get my new code in use, I can simply delete the Pod my Deployment created. A Deployment uses a ReplicaSet to control how many Pods are active. Deleting the pod will trigger the creation of a new one, which should pick up my new code. To determine which pod to delete, I'll&amp;nbsp;use the &lt;code&gt;get pods&lt;/code&gt;&amp;nbsp;command via&amp;nbsp;&lt;code&gt;kubectl&lt;/code&gt;. This will list all the running pods. Then I'll use&amp;nbsp;the &lt;code&gt;delete pod&lt;/code&gt;&amp;nbsp;command to delete the `demoapp` pod, and then make sure a new one is created:&lt;code&gt;&lt;/code&gt;&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:sh decode:true&quot;&gt;$ kubectl get pods
NAME                       READY     STATUS    RESTARTS   AGE
demoapp-2344086337-x6697   1/1       Running   0          11m

$ kubectl delete pod demoapp-2344086337-x6697
pod &quot;demoapp-2344086337-x6697&quot; deleted

$ kubectl get pods
NAME                       READY     STATUS        RESTARTS   AGE
demoapp-2344086337-c6qth   1/1       Running       0          1s
demoapp-2344086337-x6697   1/1       Terminating   0          11m&lt;/pre&gt;
&lt;p&gt;The new pod should have my new code, which I'll verify with &lt;code&gt;curl&lt;/code&gt;&amp;nbsp;once more:&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:sh decode:true&quot;&gt;$ curl http://192.168.64.3:32369
Edited code%&lt;/pre&gt;
&lt;h5&gt;Interactive debugging&lt;/h5&gt;
&lt;p&gt;Getting new code used in the container is fun, but what is even more useful is being able to interactively debug this new code. Python developers should be familiar with the use of &lt;a href=&quot;https://docs.python.org/3.7/library/pdb.html&quot;&gt;pdb&lt;/a&gt;, the python debugger. This utility can be used to insert a break point into the source code in order to interactively debug the code at that point in the execution. Pdb is fantastic when you are able to execute the code directly, but requires being attached to the tty that started the python process. That's a difficult feat inside of a system like k8s. Thankfully there is a wrapper around pdb specifically for connecting to remote python processes, called &lt;a href=&quot;https://tamentis.com/projects/rpdb/&quot;&gt;rpdb&lt;/a&gt;. When using rpdb, the wrapper will redirect stdout/stdin to a socket handler, which can be accessed over TCP. This will allow me to define a breakpoint in the code and then connect to the socket remotely in order to attach to the debugger to interact with the process. (One downside of rpdb is that it is not part of the standard library, and thus the library will need to be explicitly installed into the python environment. I've done this in my &lt;a href=&quot;https://github.com/j2sol/demoapp/blob/master/Dockerfile#L8&quot;&gt;Dockerfile&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;To debug my code, I first have to add the breakpoint to one of my source files. At the appropriate line, I need to insert &lt;code&gt;import rpdb; rpdb.set_trace(&quot;0.0.0.0&quot;)&lt;/code&gt;. I specifically need to tell rpdb to listen on &lt;code&gt;0.0.0.0&lt;/code&gt;&amp;nbsp;instead of the default &lt;code&gt;127.0.0.0&lt;/code&gt;. This will make &lt;code&gt;rpdb&lt;/code&gt; listen on all addresses rather than just localhost. This is less secure, but required in order to reach it through k8s. Once again, I'll edit the &lt;code&gt;demoapp/__init__.py&lt;/code&gt;&amp;nbsp;file on my laptop:&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:python mark:3 decode:true crayon-selected&quot;&gt;        self.end_headers()
        RESPONSE='''Edited code'''
        import rpdb; rpdb.set_trace('0.0.0.0')
        self.wfile.write(bytes(RESPONSE, 'UTF-8'))&lt;/pre&gt;
&lt;p&gt;I also need to&amp;nbsp;update my k8s Service&amp;nbsp;to expose the new port. Rpdb will listen on port &lt;code&gt;4444&lt;/code&gt;&amp;nbsp;by default, so I'll&amp;nbsp;use that in my&amp;nbsp;service. First I'll delete the existing &lt;code&gt;demoapp&lt;/code&gt;&amp;nbsp;service and then re-create it adding a second port:&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:sh decode:true&quot;&gt;$ kubectl delete service demoapp
service &quot;demoapp&quot; deleted

$ kubectl create service nodeport demoapp --tcp=8000 --tcp=4444
service &quot;demoapp&quot; created

$ kubectl get services
NAME         CLUSTER-IP   EXTERNAL-IP   PORT(S)                         AGE
demoapp      10.0.0.145   &lt;nodes&gt;       8000:31661/TCP,4444:30611/TCP   0s
kubernetes   10.0.0.1     &lt;none&gt;        443/TCP                         1d&lt;/pre&gt;
&lt;p&gt;I can test that the new service works by using &lt;code&gt;curl&lt;/code&gt;&amp;nbsp;to the new port:&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:sh decode:true&quot;&gt;$ curl http://192.168.64.3:31661
Edited code%&lt;/pre&gt;
&lt;p&gt;Now that the code is edited and the service is created to forward ports through, I&amp;nbsp;can restart the&amp;nbsp;container.&amp;nbsp;Once again I'll delete the running pod to trigger the creation of a replacement.&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:sh decode:true&quot;&gt;$ kubectl get pods
NAME                       READY     STATUS    RESTARTS   AGE
demoapp-2344086337-c6qth   1/1       Running   0          17m

$ kubectl delete pod demoapp-2344086337-c6qth
pod &quot;demoapp-2344086337-c6qth&quot; deleted

$ kubectl get pods
NAME                       READY     STATUS        RESTARTS   AGE
demoapp-2344086337-c6qth   1/1       Terminating   0          18m
demoapp-2344086337-mvh0g   1/1       Running       0          0s&lt;/pre&gt;
&lt;p&gt;Since my breakpoint is inside the &lt;code&gt;do_GET&lt;/code&gt;&amp;nbsp;function, I'll need to use &lt;code&gt;curl&lt;/code&gt;&amp;nbsp;to initiate a &lt;code&gt;GET&lt;/code&gt; request. This will seem to hang in the terminal, as the break point has been reached and execution is waiting. At this point, I should be able to connect to the waiting debugger,&amp;nbsp;&amp;nbsp;using the Service&amp;nbsp;information to determine which port to connect to. In another terminal I can use &lt;code&gt;nc&lt;/code&gt;&amp;nbsp;to connect&amp;nbsp;to the debugger! From this point on, it's debugging as usual.&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:sh decode:true&quot;&gt;$ curl http://192.168.64.3:31661
_
---------------------------------------

$ nc 192.168.64.3 30611
&gt; /demoapp/demoapp/__init__.py(11)do_GET()
-&gt; self.wfile.write(bytes(RESPONSE, 'UTF-8'))
(Pdb) l
  6  	        self.send_response(200)
  7  	        self.send_header('Content-type', 'text/html')
  8  	        self.end_headers()
  9  	        RESPONSE='''Edited code'''
 10  	        import rpdb; rpdb.set_trace('0.0.0.0')
 11  -&gt;	        self.wfile.write(bytes(RESPONSE, 'UTF-8'))
 12
 13  	def run(server_class=http.server.HTTPServer, handler_class=myHandlers):
 14  	    server_address = ('', 8000)
 15  	    myserver = server_class(server_address, handler_class)
 16  	    myserver.serve_forever()
(Pdb)&lt;/pre&gt;
&lt;p&gt;From here I can change the value of &lt;code&gt;RESPONSE&lt;/code&gt;&amp;nbsp;once more, and then continue execution, which should cause my &lt;code&gt;curl&lt;/code&gt;&amp;nbsp;command to return with my new message:&lt;/p&gt;
&lt;pre class=&quot;nums:false nums-toggle:false lang:sh decode:true&quot;&gt;$ curl http://192.168.64.3:31661
Live debugging sure is fun!%

----------------------------------

 16  	    myserver.serve_forever()
(Pdb) RESPONSE='''Live debugging sure is fun!'''
(Pdb) c
_&lt;/pre&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Kuberetes is a pretty huge leap forward in container orchestration. With that advancement comes some complexity, and a whole lot of new concepts to learn. However, the basic building blocks are there to continue using workflows that have been useful in the past. This workflow to debug code live is just a small example of what is possible with k8s, minikube, and containers in general.&lt;/p&gt;
&lt;p&gt;I've added a complete &lt;a href=&quot;https://github.com/j2sol/demoapp/blob/master/demoapp-deployment.yaml&quot;&gt;&lt;code&gt;demoapp-deployment.yaml&lt;/code&gt;&lt;/a&gt;&amp;nbsp;file to the git repository, including a Service definition. Hopefully this example will be useful! As always, comment here or on &lt;a href=&quot;https://twitter.com/iamjkeating&quot;&gt;Twitter&lt;/a&gt; should you have any thoughts to share.&lt;/p&gt;
&lt;p&gt;Happy kubing!&lt;/p&gt;</content><author><name>Jesse Keating</name></author><category term="k8s" /><category term="kubernetes" /><category term="minikube" /><category term="python" /><category term="debugging" /><summary type="html">Lately I've been playing around with Kubernetes. If you don't know what Kubernetes (k8s) is, then the rest of this post is going to be very confusing to you. Needed concepts I'm going to talk about a few things. Here are some links to places to get up to speed should any of this not make any sense: Kubernetes Docker Docker Compose Python pip Minikube kubectl Python Debugging Background I'm somewhat late to the k8s game, and I'm still trying to get my bearings. One task I set out to figure out is how I can replicate my development workflow I had built up with Docker Compose&amp;nbsp;to launch containers of my application locally for testing. The&amp;nbsp;application&amp;nbsp;I've been developing on consists of a Zookeeper service and three Python services. I have the Python services&amp;nbsp;broken out into three separate containers based on the same image, but with different launch commands. When testing things locally, I often want to introduce code that I haven't yet committed to a repository, and instead of building new images every time, I make use of a volume mount to bring my code into the container at runtime. This works well with Python thanks to a feature in pip, the tool for installing python packages. I can tell pip to perform an editable install. This type of install makes use of a symlink in the installation target path which links to the source directory of the install. In my Dockerfile&amp;nbsp;I clone the source code to /zuul, and then perform the pip install from there. What this means is that after the install, I could&amp;nbsp;simply alter the files in the original checkout in /zuul&amp;nbsp;and restart the process and the changes will take effect. To expand on that further, this gives me the ability to attach a volume mount from my laptop's zuul checkout directory (where I have edited files) to the /zuul&amp;nbsp;path within the container at start time, so that I can make use of edited files without rebuilding the image. On to Kubernetes! My workflow&amp;nbsp;worked great in docker-compose, but now I want to do this with k8s. To demonstrate how this works, I've created a simple Python web server in a demo app. I've put the source for this on GitHub for convenience. First, I need to install and run minikube, a tool to run a Kubernetes cluster locally on my laptop. Minikube will download some data and launch a virtual machine in which to run the k8s services, including its own Docker daemon. Build&amp;nbsp;Docker image With minikube running, I next need to configure my Docker client to make use of the Docker Engine within minikube. A simple command `eval $(minikube docker-env)` will set up my client. Now I can build my Docker image so that it'll be available for use within k8s. I've cloned my repository into a src/derpops/demoapp&amp;nbsp;directory relative to my homer. From there I just need a simple Docker build command to build my image. NOTE! I'm using a tag other than latest&amp;nbsp;so that Kubernetes will not try to pull the latest version of my image. My image will only exist locally, so a pull would fail. $ docker build -t demoapp:demo . Sending build context to Docker daemon 129.5kB Step 1 : FROM python:alpine ---&gt; 83da41380580 Step 2 : RUN apk --no-cache add --update git ---&gt; Using cache ---&gt; bf6e5c3fd23c Step 3 : RUN git clone https://github.com/j2sol/demoapp.git /demoapp ---&gt; Using cache ---&gt; eacba0083289 Step 4 : RUN pip install -e /demoapp ---&gt; Using cache ---&gt; 3f881178557d Step 5 : RUN pip install rpdb ---&gt; Using cache ---&gt; 81845e2506d2 Step 6 : RUN apk del git ---&gt; Using cache ---&gt; 19d2d2bb91d1 Step 7 : CMD demoapp ---&gt; Using cache ---&gt; 9eddb5c0209a Successfully built 9eddb5c0209a Creating a Deployment With the image in place, I can now create a k8s Deployment. The Deployment lets me define a container to launch, with the image I built above, and a command to run within the container. I can do this with a simple kubectl command: $ kubectl create deployment demoapp --image demoapp:demo deployment &quot;demoapp&quot; created $ kubectl get pods NAME READY STATUS RESTARTS AGE demoapp-3399253556-ngkv0 1/1 Running 0 1s If my deployment is created successfully, a new pod will show up, which will be running my code. Creating a Service To see if my code is working properly, I need to be able to reach the web server. A k8s Service is necessary to set up the networks appropriately. Just like with the Deployment, a simple kubectl command will suffice to create the service: $ kubectl create service nodeport demoapp --tcp=8000 service &quot;demoapp&quot; created $ kubectl get services NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE demoapp 10.0.0.232 8000:32369/TCP 1m kubernetes 10.0.0.1 443/TCP 23h This command created a service that will allow me to reach port 8000 of the container running the application. My code specifically tells the python library to use port 8000 and to listen on all addresses. This type of service is a nodeport, which makes this port reachable from every node. However, since this is minikube, I only have one node, and I can ask minikube to tell me what the IP address of the node is with the service&amp;nbsp;command. I'll ask it to just display the URL, instead of opening the URL in my browser, and then use curl&amp;nbsp;to access the URL: $ minikube service demoapp --url http://192.168.64.3:32369 $ curl http://192.168.64.3:32369 Hi there!% Injecting new code My application works, but now I want to alter the code. To get new code into my container, I need to add a volume mount to my deployment. VolumeMounts are used to expose content into a container. A VolumeMount definition combines the name of a volume and a path to mount it within the container. The name matches a defined Volume, of which many types are supported. The type we're interested in, the&amp;nbsp;hostPath&amp;nbsp;type,&amp;nbsp;exposes a file or a directory from the node (the machine a container is running on). The use of minikube automatically exposes a folder from the host machine minikube runs on into the VM where k8s is running, which is the node. In my case, the /Users&amp;nbsp;directory on my laptop is exposed as /Users&amp;nbsp;on the node, and thus I can make use of this in a hostPath&amp;nbsp;volume. To add a volume to my Deployment, I'll need to create a yaml&amp;nbsp;file describing my Deployment spec. I can do this quickly by repeating the earlier command but adding arguments to print out YAML: $ kubectl create deployment demoapp --image demoapp:demo --dry-run -o yaml apiVersion: extensions/v1beta1 kind: Deployment metadata: creationTimestamp: null labels: app: demoapp name: demoapp spec: replicas: 1 selector: matchLabels: app: demoapp strategy: {} template: metadata: creationTimestamp: null labels: app: demoapp spec: containers: - image: demoapp:demo name: demoapp resources: {} status: {} I saved this output to a new file, &amp;nbsp;demoapp-deployment.yaml, where I can make adjustments as needed. I need to alter the containers&amp;nbsp;spec for the demoapp&amp;nbsp;container to define a volumeMount: resources: {} volumeMounts: - name: demosource mountPath: /demoapp/demoapp status: {} This references a volume by the name of demosource&amp;nbsp;which I also need to define in the spec&amp;nbsp;as a new key: mountPath: /demoapp/demoapp volumes: - name: demosource hostPath: path: /Users/jkeating/src/derpops/demoapp/demoapp status: {} These additions will cause the directory /Users/jkeating/src/derpops/demoapp/demoapp&amp;nbsp;to be mounted to the path /demoapp/demoapp&amp;nbsp;within the container when it launches. This will overlay the version of code on my laptop on top of the code that already exists in the container. To get the new definition of my Deployment in use, I'll delete the existing Deployment and create a new one from the YAML file: $ kubectl delete deployment demoapp deployment &quot;demoapp&quot; deleted $ kubectl create -f demoapp-deployment.yaml deployment &quot;demoapp&quot; created $ kubectl get pods NAME READY STATUS RESTARTS AGE demoapp-2344086337-x6697 1/1 Running 0 1s demoapp-3399253556-ngkv0 1/1 Terminating 0 30m The existing pod is being terminated while a new pod is running. If all went well, I should still be able to use curl&amp;nbsp;to reach the web server: $ curl http://192.168.64.3:32369 Hi there!% The output is the same as earlier, as I haven't changed any code. But what if I change the value of the variable RESPONSE&amp;nbsp;to something new by editing the file demoapp/__init__.py&amp;nbsp;on my laptop. self.end_headers() RESPONSE='''Edited code''' self.wfile.write(bytes(RESPONSE, 'UTF-8')) To get my new code in use, I can simply delete the Pod my Deployment created. A Deployment uses a ReplicaSet to control how many Pods are active. Deleting the pod will trigger the creation of a new one, which should pick up my new code. To determine which pod to delete, I'll&amp;nbsp;use the get pods&amp;nbsp;command via&amp;nbsp;kubectl. This will list all the running pods. Then I'll use&amp;nbsp;the delete pod&amp;nbsp;command to delete the `demoapp` pod, and then make sure a new one is created: $ kubectl get pods NAME READY STATUS RESTARTS AGE demoapp-2344086337-x6697 1/1 Running 0 11m $ kubectl delete pod demoapp-2344086337-x6697 pod &quot;demoapp-2344086337-x6697&quot; deleted $ kubectl get pods NAME READY STATUS RESTARTS AGE demoapp-2344086337-c6qth 1/1 Running 0 1s demoapp-2344086337-x6697 1/1 Terminating 0 11m The new pod should have my new code, which I'll verify with curl&amp;nbsp;once more: $ curl http://192.168.64.3:32369 Edited code% Interactive debugging Getting new code used in the container is fun, but what is even more useful is being able to interactively debug this new code. Python developers should be familiar with the use of pdb, the python debugger. This utility can be used to insert a break point into the source code in order to interactively debug the code at that point in the execution. Pdb is fantastic when you are able to execute the code directly, but requires being attached to the tty that started the python process. That's a difficult feat inside of a system like k8s. Thankfully there is a wrapper around pdb specifically for connecting to remote python processes, called rpdb. When using rpdb, the wrapper will redirect stdout/stdin to a socket handler, which can be accessed over TCP. This will allow me to define a breakpoint in the code and then connect to the socket remotely in order to attach to the debugger to interact with the process. (One downside of rpdb is that it is not part of the standard library, and thus the library will need to be explicitly installed into the python environment. I've done this in my Dockerfile.) To debug my code, I first have to add the breakpoint to one of my source files. At the appropriate line, I need to insert import rpdb; rpdb.set_trace(&quot;0.0.0.0&quot;). I specifically need to tell rpdb to listen on 0.0.0.0&amp;nbsp;instead of the default 127.0.0.0. This will make rpdb listen on all addresses rather than just localhost. This is less secure, but required in order to reach it through k8s. Once again, I'll edit the demoapp/__init__.py&amp;nbsp;file on my laptop: self.end_headers() RESPONSE='''Edited code''' import rpdb; rpdb.set_trace('0.0.0.0') self.wfile.write(bytes(RESPONSE, 'UTF-8')) I also need to&amp;nbsp;update my k8s Service&amp;nbsp;to expose the new port. Rpdb will listen on port 4444&amp;nbsp;by default, so I'll&amp;nbsp;use that in my&amp;nbsp;service. First I'll delete the existing demoapp&amp;nbsp;service and then re-create it adding a second port: $ kubectl delete service demoapp service &quot;demoapp&quot; deleted $ kubectl create service nodeport demoapp --tcp=8000 --tcp=4444 service &quot;demoapp&quot; created $ kubectl get services NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE demoapp 10.0.0.145 8000:31661/TCP,4444:30611/TCP 0s kubernetes 10.0.0.1 443/TCP 1d I can test that the new service works by using curl&amp;nbsp;to the new port: $ curl http://192.168.64.3:31661 Edited code% Now that the code is edited and the service is created to forward ports through, I&amp;nbsp;can restart the&amp;nbsp;container.&amp;nbsp;Once again I'll delete the running pod to trigger the creation of a replacement. $ kubectl get pods NAME READY STATUS RESTARTS AGE demoapp-2344086337-c6qth 1/1 Running 0 17m $ kubectl delete pod demoapp-2344086337-c6qth pod &quot;demoapp-2344086337-c6qth&quot; deleted $ kubectl get pods NAME READY STATUS RESTARTS AGE demoapp-2344086337-c6qth 1/1 Terminating 0 18m demoapp-2344086337-mvh0g 1/1 Running 0 0s Since my breakpoint is inside the do_GET&amp;nbsp;function, I'll need to use curl&amp;nbsp;to initiate a GET request. This will seem to hang in the terminal, as the break point has been reached and execution is waiting. At this point, I should be able to connect to the waiting debugger,&amp;nbsp;&amp;nbsp;using the Service&amp;nbsp;information to determine which port to connect to. In another terminal I can use nc&amp;nbsp;to connect&amp;nbsp;to the debugger! From this point on, it's debugging as usual. $ curl http://192.168.64.3:31661 _ --------------------------------------- $ nc 192.168.64.3 30611 &gt; /demoapp/demoapp/__init__.py(11)do_GET() -&gt; self.wfile.write(bytes(RESPONSE, 'UTF-8')) (Pdb) l 6 self.send_response(200) 7 self.send_header('Content-type', 'text/html') 8 self.end_headers() 9 RESPONSE='''Edited code''' 10 import rpdb; rpdb.set_trace('0.0.0.0') 11 -&gt; self.wfile.write(bytes(RESPONSE, 'UTF-8')) 12 13 def run(server_class=http.server.HTTPServer, handler_class=myHandlers): 14 server_address = ('', 8000) 15 myserver = server_class(server_address, handler_class) 16 myserver.serve_forever() (Pdb) From here I can change the value of RESPONSE&amp;nbsp;once more, and then continue execution, which should cause my curl&amp;nbsp;command to return with my new message: $ curl http://192.168.64.3:31661 Live debugging sure is fun!% ---------------------------------- 16 myserver.serve_forever() (Pdb) RESPONSE='''Live debugging sure is fun!''' (Pdb) c _ Conclusion Kuberetes is a pretty huge leap forward in container orchestration. With that advancement comes some complexity, and a whole lot of new concepts to learn. However, the basic building blocks are there to continue using workflows that have been useful in the past. This workflow to debug code live is just a small example of what is possible with k8s, minikube, and containers in general. I've added a complete demoapp-deployment.yaml&amp;nbsp;file to the git repository, including a Service definition. Hopefully this example will be useful! As always, comment here or on Twitter should you have any thoughts to share. Happy kubing!</summary></entry><entry><title type="html">OpenStack Nova and Hypervisor disk consumption</title><link href="https://omgjlk.github.io/computers/openstack/2016/10/07/openstack-nova-and-hypervisor-disk-consumption.html" rel="alternate" type="text/html" title="OpenStack Nova and Hypervisor disk consumption" /><published>2016-10-07T23:32:50+00:00</published><updated>2016-10-07T23:32:50+00:00</updated><id>https://omgjlk.github.io/computers/openstack/2016/10/07/openstack-nova-and-hypervisor-disk-consumption</id><content type="html" xml:base="https://omgjlk.github.io/computers/openstack/2016/10/07/openstack-nova-and-hypervisor-disk-consumption.html">&lt;p&gt;Recently I found myself in a situation at $DAYJOB where I needed to account for the local disk consumption of a nova-compute node, a hypervisor. I had a heck of a time gathering all the information I needed to figure out why the space was being consumed the way it was, and since I couldn't find a single source for all this information I felt it was best to write up a post about it (so that I can find it next time I'm in the same scenario).&lt;/p&gt;
&lt;p&gt;This post&amp;nbsp;explores the various ways Nova consumes hypervisor disk space with regard to instance images and booting.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;A Nova setup with libvirt/kvm as the hypervisor, ephemeral disk space being provided by a filesystem directory is assumed, and instances booting with ephemeral disk as opposed to Block Storage volumes.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;There are two main ways Nova consumes the underlying hypervisor disk space: cached images downloaded from the image service (&lt;strong&gt;Glance&lt;/strong&gt;) and instance ephemeral disk files. All of this content is stored in Nova's&amp;nbsp;&lt;em&gt;state path&lt;/em&gt;, which by default is configured for `/var/lib/nova/`. In our setup, we mount a filesystem to Nova's sate path so that we can contain the disk usage to the images and instances that are booted on the system, without risk of filling up `/` or other critical filesystems.&lt;/p&gt;
&lt;h2 id=&quot;magicdomid3642&quot; class=&quot;ace-line&quot; data-node-type=&quot;line&quot;&gt;&lt;span class=&quot;author-251255997 font-color-000000 font-size-medium&quot;&gt;Image cache&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Nova make use of an image cache on each hypervisor. This is a place where each image that's used to boot an instance is downloaded to and preserved for a period of time. Each time a new instance is created on a hypervisor, the disk cache is checked to see if the image requested for the instance is already in the cache. If it is, that image is used then as the basis for the instance. If not, the image is downloaded from the image store and placed in the cache. This cache is typically on the same filesystem as where Nova stores the instance data (the state path), and thus the images in the cache will account for some amount of overall disk usage and availability for instances. Images in the cache are held for a period of time determined by configuration. Whether to clean unused cache images is a configuration toggle, along with a minimum age for the image before discarding the image.&lt;/p&gt;
&lt;p&gt;The amount of space consumed by an image in the cache depends on details from the image itself. While the listed size of the image in the image store may be small, either due to compression or just overall content, the &lt;em&gt;virtual size&lt;/em&gt; of the image may be much larger. This virtual size is used as a value to resize the downloaded image to, as Nova will resize each downloaded image to the match&amp;nbsp;the virtual size.&lt;/p&gt;
&lt;p&gt;The virtual size of the image depends on how the image was created, the source of the image, and the options used while creating the image. When using the `qemu-img` tool to create an image, a size can be specified. This will become the virtual size of the image. If creating an image of an existing instance, either by way of an image creation or a backup (which uses the same method), the size of the image will be matched to the size of disk for the flavor the instance uses. If the instance's flavor states a 200G disk, then the image virtual size from that instance will be 200G, regardless of how little space is actually consumed within the instance.&lt;/p&gt;
&lt;p&gt;During an instance creation, Nova downloads an image from glance, checks the virtual size of the image, and resizes the file to the virtual size of the image. This file is saved in an `instances/_base/` subdirectory within Nova's state path. The resize creates a &lt;em&gt;sparse file&lt;/em&gt;, where the apparent size matches the virtual size, when&amp;nbsp;the actual consumed size may be much lower. Use of the `du` utility can show the difference: `du -h --apparent-size &lt;file&gt;` vs `du -h &lt;file&gt;`.&lt;/p&gt;
&lt;h2&gt;Instance ephemeral disk&lt;/h2&gt;
&lt;p&gt;Each server instance that Nova manages will have its own directory to store data. Part of that data is the &lt;em&gt;ephemeral disk&lt;/em&gt; data, the data within the instance itself. The amount of space consumed by the ephemeral disk depends on configuration details of Nova, and on the flavor of the instance.&lt;/p&gt;
&lt;h4&gt;Copy on write&lt;/h4&gt;
&lt;p&gt;During an instance creation attempt, Nova will&amp;nbsp;download and resize the base image, if the base image doesn't already exist. Then Nova may either create a&lt;em&gt; copy on write&lt;/em&gt; file for the instance, linked to the base image, or copy the entire base image for the instance with no linkage. This decision is based on a configuration entry, `use_cow_images`, which defaults to `True`.&lt;/p&gt;
&lt;p&gt;A copy on write file is an overlay file that will &lt;em&gt;overlay&lt;/em&gt; on top of the base image file and keep track of any changes to the filesystem within the image.&lt;/p&gt;
&lt;p&gt;If copy on write is desired, overlays are created from the base image to the `instances/&lt;uuid&gt;/disk` file within the state path. Otherwise a direct copy will be made to the same path.&lt;/p&gt;
&lt;h4&gt;Preallocation&lt;/h4&gt;
&lt;p&gt;In either case, Nova may make a call to pre-allocate enough blocks on that file to be able to fill the size of the flavor's disk. This is determined by a configuration&amp;nbsp;entry `preallocate_image`.&lt;/p&gt;
&lt;p&gt;If copy on write is used then the file will appear one of two ways. Without image preallocation, the file will only be as large as the amount of change that has occurred in the file since boot, thus it can be quite small to start with, but may expand to the full size of the flavor's disk size. With image preallocation set, both the apparent and actual size will be the full size of the flavor's disk size.&lt;/p&gt;
&lt;p&gt;If a direct copy of the image file is used, then the file will appear in one of two ways. Without image preallocation, then the file will appear exactly as it does in the image cache. The apparent size will can be quite large, but the actual size will be relatively smaller. With image preallocation, both the apparent and the actual size will be the full size of the flavor's disk size.&lt;/p&gt;
&lt;h4&gt;Launching&lt;/h4&gt;
&lt;p&gt;Qemu will be launched referencing the disk file in `instances/&lt;uuid&gt;/`, which may or may not be linked to the cached image file. This linkage is what determines whether or not an image file in the cache is still &quot;in use&quot;, and will prevent Nova from removing the file when it ages out.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;The amount of disk space consumed on a hypervisor depends on numerous factors, such as source image virtual size, the number of active unique images used to boot instances on the hypervisor, and configuration settings regarding disk preallocation and copy on write files. The vast majority of overall consumption of space by Nova will be the sum of all the cached images and all the ephemeral disks for all the instances booted on a given hypervisor.&lt;/p&gt;
&lt;h3&gt;Configuration items that drive decisions&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;`&lt;strong&gt;preallocate_disk&lt;/strong&gt;`: Can set to `none` or `space`. If `space`, an `fallocate` call is made on the instance (overlay) disk to allocate enough blocks to cover the flavor disk size. Without preallocating, the underlying hypervisor filesystem can become overcommitted, and if an instance causes enough data change&amp;nbsp;to occur to it's disk file, the host filesystem may become exhausted. An operator could prevent exhaustion by relying on the `DiskFilter` scheduling filter to avoid scheduling instances to where disk has been fully committed, but there are defects and drawbacks to this filter (a subject for a future post). The default value is `none`&lt;/li&gt;
&lt;li&gt;`&lt;strong&gt;use_cow_images&lt;/strong&gt;`: Can be set to `True` or `False`. If `True`, the instance's disk file is a copy on write file, attached to the base image in nova's image cache (`instances/_base/`). When this happens, the base image for any booted instance is always held open, and cannot be cleaned. This can drive up the storage overhead on a hypervisor. The default value is `True`.&lt;/li&gt;
&lt;li&gt;`&lt;strong&gt;remove_unused_base_images&lt;/strong&gt;`: Can be set to `True` or `False`. If `True`, when a cached image is no longer used by an instance on the hypervisor, and has reached a minimum age, the image will be removed from the cache. This can prevent unbound growth of the image cache on a hypervisor. The default is `True`.&lt;/li&gt;
&lt;li&gt;`&lt;strong&gt;remove_unused_original_minimum_age_seconds&lt;/strong&gt;`: An integer of seconds to indicate how old an image file must be before it is a candidate for removal if unused. The default value is `86400`.&lt;/li&gt;
&lt;/ul&gt;</content><author><name>Jesse Keating</name></author><summary type="html">Recently I found myself in a situation at $DAYJOB where I needed to account for the local disk consumption of a nova-compute node, a hypervisor. I had a heck of a time gathering all the information I needed to figure out why the space was being consumed the way it was, and since I couldn't find a single source for all this information I felt it was best to write up a post about it (so that I can find it next time I'm in the same scenario). This post&amp;nbsp;explores the various ways Nova consumes hypervisor disk space with regard to instance images and booting. A Nova setup with libvirt/kvm as the hypervisor, ephemeral disk space being provided by a filesystem directory is assumed, and instances booting with ephemeral disk as opposed to Block Storage volumes. There are two main ways Nova consumes the underlying hypervisor disk space: cached images downloaded from the image service (Glance) and instance ephemeral disk files. All of this content is stored in Nova's&amp;nbsp;state path, which by default is configured for `/var/lib/nova/`. In our setup, we mount a filesystem to Nova's sate path so that we can contain the disk usage to the images and instances that are booted on the system, without risk of filling up `/` or other critical filesystems. Image cache Nova make use of an image cache on each hypervisor. This is a place where each image that's used to boot an instance is downloaded to and preserved for a period of time. Each time a new instance is created on a hypervisor, the disk cache is checked to see if the image requested for the instance is already in the cache. If it is, that image is used then as the basis for the instance. If not, the image is downloaded from the image store and placed in the cache. This cache is typically on the same filesystem as where Nova stores the instance data (the state path), and thus the images in the cache will account for some amount of overall disk usage and availability for instances. Images in the cache are held for a period of time determined by configuration. Whether to clean unused cache images is a configuration toggle, along with a minimum age for the image before discarding the image. The amount of space consumed by an image in the cache depends on details from the image itself. While the listed size of the image in the image store may be small, either due to compression or just overall content, the virtual size of the image may be much larger. This virtual size is used as a value to resize the downloaded image to, as Nova will resize each downloaded image to the match&amp;nbsp;the virtual size. The virtual size of the image depends on how the image was created, the source of the image, and the options used while creating the image. When using the `qemu-img` tool to create an image, a size can be specified. This will become the virtual size of the image. If creating an image of an existing instance, either by way of an image creation or a backup (which uses the same method), the size of the image will be matched to the size of disk for the flavor the instance uses. If the instance's flavor states a 200G disk, then the image virtual size from that instance will be 200G, regardless of how little space is actually consumed within the instance. During an instance creation, Nova downloads an image from glance, checks the virtual size of the image, and resizes the file to the virtual size of the image. This file is saved in an `instances/_base/` subdirectory within Nova's state path. The resize creates a sparse file, where the apparent size matches the virtual size, when&amp;nbsp;the actual consumed size may be much lower. Use of the `du` utility can show the difference: `du -h --apparent-size ` vs `du -h `. Instance ephemeral disk Each server instance that Nova manages will have its own directory to store data. Part of that data is the ephemeral disk data, the data within the instance itself. The amount of space consumed by the ephemeral disk depends on configuration details of Nova, and on the flavor of the instance. Copy on write During an instance creation attempt, Nova will&amp;nbsp;download and resize the base image, if the base image doesn't already exist. Then Nova may either create a copy on write file for the instance, linked to the base image, or copy the entire base image for the instance with no linkage. This decision is based on a configuration entry, `use_cow_images`, which defaults to `True`. A copy on write file is an overlay file that will overlay on top of the base image file and keep track of any changes to the filesystem within the image. If copy on write is desired, overlays are created from the base image to the `instances//disk` file within the state path. Otherwise a direct copy will be made to the same path. Preallocation In either case, Nova may make a call to pre-allocate enough blocks on that file to be able to fill the size of the flavor's disk. This is determined by a configuration&amp;nbsp;entry `preallocate_image`. If copy on write is used then the file will appear one of two ways. Without image preallocation, the file will only be as large as the amount of change that has occurred in the file since boot, thus it can be quite small to start with, but may expand to the full size of the flavor's disk size. With image preallocation set, both the apparent and actual size will be the full size of the flavor's disk size. If a direct copy of the image file is used, then the file will appear in one of two ways. Without image preallocation, then the file will appear exactly as it does in the image cache. The apparent size will can be quite large, but the actual size will be relatively smaller. With image preallocation, both the apparent and the actual size will be the full size of the flavor's disk size. Launching Qemu will be launched referencing the disk file in `instances//`, which may or may not be linked to the cached image file. This linkage is what determines whether or not an image file in the cache is still &quot;in use&quot;, and will prevent Nova from removing the file when it ages out. Conclusion The amount of disk space consumed on a hypervisor depends on numerous factors, such as source image virtual size, the number of active unique images used to boot instances on the hypervisor, and configuration settings regarding disk preallocation and copy on write files. The vast majority of overall consumption of space by Nova will be the sum of all the cached images and all the ephemeral disks for all the instances booted on a given hypervisor. Configuration items that drive decisions `preallocate_disk`: Can set to `none` or `space`. If `space`, an `fallocate` call is made on the instance (overlay) disk to allocate enough blocks to cover the flavor disk size. Without preallocating, the underlying hypervisor filesystem can become overcommitted, and if an instance causes enough data change&amp;nbsp;to occur to it's disk file, the host filesystem may become exhausted. An operator could prevent exhaustion by relying on the `DiskFilter` scheduling filter to avoid scheduling instances to where disk has been fully committed, but there are defects and drawbacks to this filter (a subject for a future post). The default value is `none` `use_cow_images`: Can be set to `True` or `False`. If `True`, the instance's disk file is a copy on write file, attached to the base image in nova's image cache (`instances/_base/`). When this happens, the base image for any booted instance is always held open, and cannot be cleaned. This can drive up the storage overhead on a hypervisor. The default value is `True`. `remove_unused_base_images`: Can be set to `True` or `False`. If `True`, when a cached image is no longer used by an instance on the hypervisor, and has reached a minimum age, the image will be removed from the cache. This can prevent unbound growth of the image cache on a hypervisor. The default is `True`. `remove_unused_original_minimum_age_seconds`: An integer of seconds to indicate how old an image file must be before it is a candidate for removal if unused. The default value is `86400`.</summary></entry><entry><title type="html">Why I love open source!</title><link href="https://omgjlk.github.io/github/computers/docker/2015/09/05/why-i-love-open-source.html" rel="alternate" type="text/html" title="Why I love open source!" /><published>2015-09-05T16:42:31+00:00</published><updated>2015-09-05T16:42:31+00:00</updated><id>https://omgjlk.github.io/github/computers/docker/2015/09/05/why-i-love-open-source</id><content type="html" xml:base="https://omgjlk.github.io/github/computers/docker/2015/09/05/why-i-love-open-source.html">&lt;p&gt;The other day I decided it was time I got familiar with &lt;a href=&quot;https://www.docker.com/&quot; target=&quot;_blank&quot;&gt;Docker&lt;/a&gt;. Yes, I know, I'm a bit late to the party, but better late than never. I understood some of the concepts around Docker, just not necessarily the mechanics, so it was time to dive in.&lt;/p&gt;
&lt;p&gt;Docker these days has a handy utility named &lt;a href=&quot;https://docs.docker.com/machine/&quot; target=&quot;_blank&quot;&gt;docker-machine&lt;/a&gt;. This tool is used to create a target system to create docker containers on. This is really useful if you're on a Mac and don't have a kernel that supports containers natively. By using docker-machine I was able to provision a VM via &lt;a href=&quot;https://www.virtualbox.org/&quot; target=&quot;_blank&quot;&gt;VirtualBox&lt;/a&gt; that was all set up to run containers. From that point in, docker commands ran as expected, and containers showed up inside the VirtualBox VM.&lt;/p&gt;
&lt;p&gt;This is all well and good, but being a curious nerd I wanted to see what other drivers there were for docker-machine. Unsurprisingly, there are &lt;a href=&quot;https://docs.docker.com/machine/drivers/&quot; target=&quot;_blank&quot;&gt;numerous drivers&lt;/a&gt;, many of them cloud based. There is an &lt;a href=&quot;https://docs.docker.com/machine/drivers/openstack/&quot; target=&quot;_blank&quot;&gt;OpenStack&lt;/a&gt; driver as well, which is great! My &lt;a href=&quot;https://www.blueboxcloud.com/&quot; target=&quot;_blank&quot;&gt;day job&lt;/a&gt; is all about OpenStack and I have numerous clouds at my disposal. The idea of utilizing my cloud to run docker containers just seems natural to me, so that's what I tried to do.&lt;/p&gt;
&lt;p&gt;Unfortunately I ran into a problem. Our clouds work in a way that requires the allocation of a &quot;floating IP&quot; address to an instance in order for that instance to be accessible by the outside world. The docker-machine OpenStack driver supports this, by passing in the correct arguments to tell the driver where to allocate the floating IP from, a pool. It turns out that my account on the cloud I was targeting has admin level rights (a scenario many of my customers will be in), and thus was able to see more available floating IP addresses in the pool than a normal user would, many of which had been allocated to a different project (projects, or tenants in OpenStack are a way to segregate groups of users and resources within a cloud). The docker-machine driver simply attempted to use the first address it thought was available for the instance it just created. In my case, this address had already been allocated to a different tenant and the OpenStack API returned an error when the assignment to my instance was attempted.&lt;/p&gt;
&lt;p&gt;I understood the problem, and I had a general idea of how to fix it. The driver should filter the floating IP addresses by my project ID when searching for an available floating IP to use. I essentially had three choices at this point:&lt;br /&gt;
&lt;strong&gt;Door #1&lt;/strong&gt;: If the project didn't have a public bug tracker I could give up on the tool or write a negative review somewhere or a snarky tweet about it and find something else to play with.&lt;br /&gt;
&lt;strong&gt;Door #2&lt;/strong&gt;: If the project had a public bug tracker I could file a bug in the tracker and explain the scenario that led to the error. I would just have to wait and hope somebody at the project cared enough to fix my bug.&lt;br /&gt;
&lt;strong&gt;Door #3&lt;/strong&gt;:Because docker-machine is an &lt;a href=&quot;https://github.com/docker/machine&quot; target=&quot;_blank&quot;&gt;open source project&lt;/a&gt;, I had a third choice. I could pull down the code and try my hand at fixing the problem myself.&lt;/p&gt;
&lt;p&gt;Obviously I went with door #3. I've never looked at the source code behind Docker before (written in &lt;a href=&quot;https://golang.org/&quot; target=&quot;_blank&quot;&gt;Go&lt;/a&gt;), but I figured I could fudge my way through a small change. Thankfully the Docker project has spent a fair amount of time thinking about how to make &lt;a href=&quot;https://docs.docker.com/project/who-written-for/&quot; target=&quot;_blank&quot;&gt;contributing&lt;/a&gt; to the project an easy process. Numerous documents exist to help guide a first time contributor through &lt;a href=&quot;https://docs.docker.com/project/software-required/&quot; target=&quot;_blank&quot;&gt;setting up a development environment&lt;/a&gt;, understanding the &lt;a href=&quot;https://docs.docker.com/project/test-and-docs/&quot; target=&quot;_blank&quot;&gt;code testing tools&lt;/a&gt;, and walking through the &lt;a href=&quot;https://docs.docker.com/opensource/code/&quot; target=&quot;_blank&quot;&gt;submission&lt;/a&gt; and &lt;a href=&quot;http://docs.docker.com/project/review-pr/&quot; target=&quot;_blank&quot;&gt;review&lt;/a&gt; process. Following these guides I was able to start making modifications to the docker-machine code and testing them out on my laptop. What I thought was going to be a simple change turned out to be a more involved code addition, which led me to reading the code and developer documents for a &lt;a href=&quot;http://gophercloud.io/&quot; target=&quot;_blank&quot;&gt;supporting library&lt;/a&gt; that docker-machine uses.&lt;/p&gt;
&lt;p&gt;Through much iteration and testing, I was finally able to create a change that resolved my issue in a satisfactory way. Being a good open source citizen, I then &lt;a href=&quot;https://github.com/docker/machine/pull/1809&quot; target=&quot;_blank&quot;&gt;submitted this change&lt;/a&gt; back up to the project in hopes of inclusion in a future release. I myself am not currently blocked in using this tool, but I'd like my customers to be able to use this tool as well, in a way that doesn't require me to distribute a modified binary to them.&lt;/p&gt;
&lt;p&gt;This is the real joy of Open Source to me. I found a tool I want to make use of, I discovered a way in which the tool doesn't quite work right for me, I have access to the code to debug the problem, I have access to the documentation and supporting code to develop a solution, and I have the opportunity to contribute a change back to the tool. This process feels so natural to me now that any other way just seems broken. Open Source has enabled me to make my life better, as well as potentially making the life of other users of the software better too, and that gets me right in the feels.&lt;/p&gt;</content><author><name>Jesse Keating</name></author><summary type="html">The other day I decided it was time I got familiar with Docker. Yes, I know, I'm a bit late to the party, but better late than never. I understood some of the concepts around Docker, just not necessarily the mechanics, so it was time to dive in. Docker these days has a handy utility named docker-machine. This tool is used to create a target system to create docker containers on. This is really useful if you're on a Mac and don't have a kernel that supports containers natively. By using docker-machine I was able to provision a VM via VirtualBox that was all set up to run containers. From that point in, docker commands ran as expected, and containers showed up inside the VirtualBox VM. This is all well and good, but being a curious nerd I wanted to see what other drivers there were for docker-machine. Unsurprisingly, there are numerous drivers, many of them cloud based. There is an OpenStack driver as well, which is great! My day job is all about OpenStack and I have numerous clouds at my disposal. The idea of utilizing my cloud to run docker containers just seems natural to me, so that's what I tried to do. Unfortunately I ran into a problem. Our clouds work in a way that requires the allocation of a &quot;floating IP&quot; address to an instance in order for that instance to be accessible by the outside world. The docker-machine OpenStack driver supports this, by passing in the correct arguments to tell the driver where to allocate the floating IP from, a pool. It turns out that my account on the cloud I was targeting has admin level rights (a scenario many of my customers will be in), and thus was able to see more available floating IP addresses in the pool than a normal user would, many of which had been allocated to a different project (projects, or tenants in OpenStack are a way to segregate groups of users and resources within a cloud). The docker-machine driver simply attempted to use the first address it thought was available for the instance it just created. In my case, this address had already been allocated to a different tenant and the OpenStack API returned an error when the assignment to my instance was attempted. I understood the problem, and I had a general idea of how to fix it. The driver should filter the floating IP addresses by my project ID when searching for an available floating IP to use. I essentially had three choices at this point: Door #1: If the project didn't have a public bug tracker I could give up on the tool or write a negative review somewhere or a snarky tweet about it and find something else to play with. Door #2: If the project had a public bug tracker I could file a bug in the tracker and explain the scenario that led to the error. I would just have to wait and hope somebody at the project cared enough to fix my bug. Door #3:Because docker-machine is an open source project, I had a third choice. I could pull down the code and try my hand at fixing the problem myself. Obviously I went with door #3. I've never looked at the source code behind Docker before (written in Go), but I figured I could fudge my way through a small change. Thankfully the Docker project has spent a fair amount of time thinking about how to make contributing to the project an easy process. Numerous documents exist to help guide a first time contributor through setting up a development environment, understanding the code testing tools, and walking through the submission and review process. Following these guides I was able to start making modifications to the docker-machine code and testing them out on my laptop. What I thought was going to be a simple change turned out to be a more involved code addition, which led me to reading the code and developer documents for a supporting library that docker-machine uses. Through much iteration and testing, I was finally able to create a change that resolved my issue in a satisfactory way. Being a good open source citizen, I then submitted this change back up to the project in hopes of inclusion in a future release. I myself am not currently blocked in using this tool, but I'd like my customers to be able to use this tool as well, in a way that doesn't require me to distribute a modified binary to them. This is the real joy of Open Source to me. I found a tool I want to make use of, I discovered a way in which the tool doesn't quite work right for me, I have access to the code to debug the problem, I have access to the documentation and supporting code to develop a solution, and I have the opportunity to contribute a change back to the tool. This process feels so natural to me now that any other way just seems broken. Open Source has enabled me to make my life better, as well as potentially making the life of other users of the software better too, and that gets me right in the feels.</summary></entry><entry><title type="html">To Do Journaling</title><link href="https://omgjlk.github.io/navel-gazing/2015/08/05/to-do-journaling.html" rel="alternate" type="text/html" title="To Do Journaling" /><published>2015-08-05T17:37:01+00:00</published><updated>2015-08-05T17:37:01+00:00</updated><id>https://omgjlk.github.io/navel-gazing/2015/08/05/to-do-journaling</id><content type="html" xml:base="https://omgjlk.github.io/navel-gazing/2015/08/05/to-do-journaling.html">&lt;p&gt;&lt;a href=&quot;https://derpops.bike/wp-content/uploads/2015/08/IMG_3600-e1438795506437.jpg&quot;&gt;&lt;img class=&quot;alignnone size-medium wp-image-93&quot; src=&quot;https://derpops.bike/wp-content/uploads/2015/08/IMG_3600-e1438795488877-225x300.jpg&quot; alt=&quot;IMG_3600&quot; width=&quot;225&quot; height=&quot;300&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A co-worker noticed me taking some notes in my notebook the other day, and asked about my To Do journaling (yes I'm making that word up). There was genuine interest from a few other people, so I decided to make a blog post about it.&lt;/p&gt;
&lt;p&gt;To Do journaling, to me, is writing down a list of things to accomplish for the day. This is a pretty common practice, and there is nothing really revelatory about the process, simply write the things you want to do down. I started doing this when I couldn't keep track of the things I needed to do in my head any more, and I was letting things fall through the crack. There are many ways in which one could enumerate a to do list; there is a whole industry around tools to help one accomplish this. However, given how much of my life is ruled by electronic means and apps and tools, I have eschewed the &quot;smart&quot; tools, and instead have adopted a more traditional model. I find it helps keep me grounded.&lt;/p&gt;
&lt;p&gt;For my journaling, I prefer to use pen and paper. A physical notebook I carry with me in which I can document my to dos for the day, and any sort of notes I need to write down. Writing things down by hand seems to trigger some bit of memory goo in my brain and helps me to remember it. If I type it out, it seems to slip through the synapses and fall out the other side, but writing it down gives it a bit of &quot;stickiness&quot;. Because I will spend a fair amount of time touching and feeling and writing in my notebook, I splurge on a high quality notebook. One that's the right size, the right paper feel, and a nice cover. My current favorite is the &lt;a title=&quot;Amazon link&quot; href=&quot;http://www.amazon.com/Rhodia-118609C-Webnotebook-Black/dp/B002TEADOS&quot; target=&quot;_blank&quot;&gt;Rhodia Webnotebook&lt;/a&gt;, in black. This notebook is hardcover, which helps me write on a variety of surfaces, but it covered in a very comfortable leatherette. The paper is very nice as well. A good notebook deserves a good pen too. I've gone through a multitude of pens in the past, with various favorites along the way. My current favorite is the &lt;a title=&quot;Amazon link&quot; href=&quot;http://www.amazon.com/Sharpie-Point-3-Pack-Black-1758052/dp/B002ONCF9K&quot; target=&quot;_blank&quot;&gt;Sharpie Grip Pen&lt;/a&gt; with a fine black point. These aren't super fancy, but they write nicely (particularly on the Rhodia paper) and feel good in the hand. Additionally they're so cheap that I don't fret if I lose one. To keep my pen and my note pad together, I got a &lt;a title=&quot;Amazon link&quot; href=&quot;http://www.amazon.com/gp/product/B00DMPTRXQ&quot; target=&quot;_blank&quot;&gt;Quiver pen holder&lt;/a&gt;. This actually cost more than the notebook and pens combined, but unlike the notebook and pen which are consumables, the Quiver will transfer from note book to note book. This keeps my pen always with my notebook, and adds a nice touch of style to the setup.&lt;/p&gt;
&lt;p&gt;Enough about the gear, lets talk about the process. Every day I create a new to do list. Every day is a fresh list, so that I don't have to keep paging back through history to find the list of things to do. I make a To Do XX/XX header to indicate the date, and then start listing things prefixed with a dash.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://derpops.bike/wp-content/uploads/2015/08/IMG_3601-e1438795601605.jpg&quot;&gt;&lt;img class=&quot;alignnone size-medium wp-image-94&quot; src=&quot;https://derpops.bike/wp-content/uploads/2015/08/IMG_3601-e1438795588850-225x300.jpg&quot; alt=&quot;IMG_3601&quot; width=&quot;225&quot; height=&quot;300&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is just a simple, unordered list. I typically leave room to the right of the list so that later, after taking various notes below it, if I need to add another item I have room. Having the items unordered also means I don't stress if I have to add an item later, it can go wherever and not upset any ordering. As I start working on something, I'll make a tilde mark next to the list item.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://derpops.bike/wp-content/uploads/2015/08/IMG_3602-e1438795842179.jpg&quot;&gt;&lt;img class=&quot;alignnone size-medium wp-image-95&quot; src=&quot;https://derpops.bike/wp-content/uploads/2015/08/IMG_3602-e1438795828874-225x300.jpg&quot; alt=&quot;IMG_3602&quot; width=&quot;225&quot; height=&quot;300&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Later, as I finish an item, I'll check it off with a check mark. I do not cross the item out, because I want to be able to go back through my history to remember the things I've done. Crossing an item out makes it that much harder to read (my handwriting is challenging enough).&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://derpops.bike/wp-content/uploads/2015/08/IMG_3603-e1438795863549.jpg&quot;&gt;&lt;img class=&quot;alignnone size-medium wp-image-96&quot; src=&quot;https://derpops.bike/wp-content/uploads/2015/08/IMG_3603-e1438795863549-225x300.jpg&quot; alt=&quot;IMG_3603&quot; width=&quot;225&quot; height=&quot;300&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If at the end of the day I haven't accomplished everything on my list, I draw a square around the incomplete items. This square is a visual cue that lets me know I need to carry the item over into the next day's list. In this way I don't lost track of the things I want to accomplish, but I don't have to flip back to previous days. I just carry over the unfinished items to the next day.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://derpops.bike/wp-content/uploads/2015/08/IMG_3604-e1438795887999.jpg&quot;&gt;&lt;img class=&quot;alignnone size-medium wp-image-97&quot; src=&quot;https://derpops.bike/wp-content/uploads/2015/08/IMG_3604-e1438795887999-225x300.jpg&quot; alt=&quot;IMG_3604&quot; width=&quot;225&quot; height=&quot;300&quot; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This very simple process has kept me on top of the things I need to accomplish for the past few years. I've filled a number of notebooks this way, and that feels like a great accomplishment. Every time I feel like nothing is happening and my wheels are spinning I can page through them and realize that yes, I am actually getting things done. This process also helps come review time, looking back at past accomplishments. I haven't done this yet, but I could star the items that would be good to highlight in any future self review scenario.&lt;/p&gt;
&lt;p&gt;Thanks for reading, and feel free to share your own to do process in the comments, or on twitter.&lt;/p&gt;</content><author><name>Jesse Keating</name></author><summary type="html">A co-worker noticed me taking some notes in my notebook the other day, and asked about my To Do journaling (yes I'm making that word up). There was genuine interest from a few other people, so I decided to make a blog post about it. To Do journaling, to me, is writing down a list of things to accomplish for the day. This is a pretty common practice, and there is nothing really revelatory about the process, simply write the things you want to do down. I started doing this when I couldn't keep track of the things I needed to do in my head any more, and I was letting things fall through the crack. There are many ways in which one could enumerate a to do list; there is a whole industry around tools to help one accomplish this. However, given how much of my life is ruled by electronic means and apps and tools, I have eschewed the &quot;smart&quot; tools, and instead have adopted a more traditional model. I find it helps keep me grounded. For my journaling, I prefer to use pen and paper. A physical notebook I carry with me in which I can document my to dos for the day, and any sort of notes I need to write down. Writing things down by hand seems to trigger some bit of memory goo in my brain and helps me to remember it. If I type it out, it seems to slip through the synapses and fall out the other side, but writing it down gives it a bit of &quot;stickiness&quot;. Because I will spend a fair amount of time touching and feeling and writing in my notebook, I splurge on a high quality notebook. One that's the right size, the right paper feel, and a nice cover. My current favorite is the Rhodia Webnotebook, in black. This notebook is hardcover, which helps me write on a variety of surfaces, but it covered in a very comfortable leatherette. The paper is very nice as well. A good notebook deserves a good pen too. I've gone through a multitude of pens in the past, with various favorites along the way. My current favorite is the Sharpie Grip Pen with a fine black point. These aren't super fancy, but they write nicely (particularly on the Rhodia paper) and feel good in the hand. Additionally they're so cheap that I don't fret if I lose one. To keep my pen and my note pad together, I got a Quiver pen holder. This actually cost more than the notebook and pens combined, but unlike the notebook and pen which are consumables, the Quiver will transfer from note book to note book. This keeps my pen always with my notebook, and adds a nice touch of style to the setup. Enough about the gear, lets talk about the process. Every day I create a new to do list. Every day is a fresh list, so that I don't have to keep paging back through history to find the list of things to do. I make a To Do XX/XX header to indicate the date, and then start listing things prefixed with a dash. This is just a simple, unordered list. I typically leave room to the right of the list so that later, after taking various notes below it, if I need to add another item I have room. Having the items unordered also means I don't stress if I have to add an item later, it can go wherever and not upset any ordering. As I start working on something, I'll make a tilde mark next to the list item. Later, as I finish an item, I'll check it off with a check mark. I do not cross the item out, because I want to be able to go back through my history to remember the things I've done. Crossing an item out makes it that much harder to read (my handwriting is challenging enough). If at the end of the day I haven't accomplished everything on my list, I draw a square around the incomplete items. This square is a visual cue that lets me know I need to carry the item over into the next day's list. In this way I don't lost track of the things I want to accomplish, but I don't have to flip back to previous days. I just carry over the unfinished items to the next day. This very simple process has kept me on top of the things I need to accomplish for the past few years. I've filled a number of notebooks this way, and that feels like a great accomplishment. Every time I feel like nothing is happening and my wheels are spinning I can page through them and realize that yes, I am actually getting things done. This process also helps come review time, looking back at past accomplishments. I haven't done this yet, but I could star the items that would be good to highlight in any future self review scenario. Thanks for reading, and feel free to share your own to do process in the comments, or on twitter.</summary></entry><entry><title type="html">Ansible copying content from one remote system to another</title><link href="https://omgjlk.github.io/computers/ansible/2014/12/25/ansible-copying-content-from-one-remote-system-to-another.html" rel="alternate" type="text/html" title="Ansible copying content from one remote system to another" /><published>2014-12-25T00:10:00+00:00</published><updated>2014-12-25T00:10:00+00:00</updated><id>https://omgjlk.github.io/computers/ansible/2014/12/25/ansible-copying-content-from-one-remote-system-to-another</id><content type="html" xml:base="https://omgjlk.github.io/computers/ansible/2014/12/25/ansible-copying-content-from-one-remote-system-to-another.html">&lt;p&gt;Just a quick tip of you're trying to do the same thing I was trying to do.&lt;/p&gt;
&lt;h3&gt;The Problem&lt;/h3&gt;
&lt;p&gt;I am generating some content on Server A. I want to replicate this content onto Servers B and C.&lt;/p&gt;
&lt;h3&gt;The Solution&lt;/h3&gt;
&lt;p&gt;TL;DR: Read files content from Server A. Write files on Servers B and C from those contents.&lt;/p&gt;
&lt;p&gt;Ansible provides a couple modules that make this possible. The first one we'll look at is the &lt;a href=&quot;http://docs.ansible.com/slurp_module.html&quot; target=&quot;_blank&quot;&gt;slurp module&lt;/a&gt;. This module allows you to read in contents of a file from a remote system. Here is an task to read the content, utilizing the &lt;a href=&quot;http://docs.ansible.com/playbooks_delegation.html#run-once&quot; target=&quot;_blank&quot;&gt;run_once&lt;/a&gt; mechanism:&lt;/p&gt;
&lt;pre class=&quot;theme:github toolbar-overlay:false lang:yaml decode:true&quot;&gt;- name: slurp keystone certs
  slurp: src=
  with_items:
    - /etc/keystone/ssl/certs/ca.pem
    - /etc/keystone/ssl/certs/signing_cert.pem
    - /etc/keystone/ssl/private/cakey.pem
    - /etc/keystone/ssl/private/signing_key.pem
  register: pki_certs
  run_once: true&lt;/pre&gt;
&lt;p&gt;This will read each of those files in and save the results in the &lt;strong&gt;pki_certs&lt;/strong&gt; variable. Ansible will only do this once, presumably on the host where this content was generated with a previous run_once task. However the variable data will be assigned to every host to make it easily accessible.&lt;/p&gt;
&lt;p&gt;Next we need to write out the content on our other systems. There are a couple things to consider. First, because the files were read via a with_items loop, the registered content is in a list, specifically in &lt;strong&gt;pki_certs.results&lt;/strong&gt;. This is easy enough to deal with because the results list is a list of dictionaries, and the name of the file is part of that dictionary. The filename resides in the &lt;strong&gt;item&lt;/strong&gt; key, while the content resides in the &lt;strong&gt;content&lt;/strong&gt; key. This allows us to template out both the path to be written as well as the content to be written at that path.&lt;/p&gt;
&lt;p&gt;The next thing we need to consider is that the slurp module stores content in base 64 encoding. That means when we write it back out, we need to &lt;em&gt;decode&lt;/em&gt; it from base 64, otherwise Ansible will happily write out some long strings that look nothing like your file. To decode from base 64, simply use the &lt;a href=&quot;http://docs.ansible.com/playbooks_variables.html#other-useful-filters&quot; target=&quot;_blank&quot;&gt;b64decode filter&lt;/a&gt; on the content variable.&lt;/p&gt;
&lt;p&gt;The last thing to consider has to do with yaml and whitespace and ansible. This may not come into play with every file, but these files have multiple lines. A somewhat recent change in Ansible means that if the &quot;short form&quot; of task description is used (with key=value parameters) your written out file will have &lt;a href=&quot;https://github.com/ansible/ansible/issues/9172&quot; target=&quot;_blank&quot;&gt;double linefeeds&lt;/a&gt;. The simple solution is to use &quot;long form&quot; task syntax as you'll see below:&lt;/p&gt;
&lt;pre class=&quot;theme:github toolbar-overlay:false lang:yaml decode:true&quot;&gt;- name: write out keystone certs
  copy:
    dest: &quot;&quot;
    content: &quot;&quot;
    owner: keystone
    group: keystone
    mode: 0700
  with_items: pki_certs.results&lt;/pre&gt;
&lt;p&gt;Because every host has access to the &lt;strong&gt;pki_certs&lt;/strong&gt; variable this task can run across all of them. You &lt;em&gt;might&lt;/em&gt; see a change registered for the first host in the loop, even though it was the source of the content, due to permissions or ownership changes, however subsequent runs will be nice and clean.&lt;/p&gt;
&lt;p&gt;Hopefully this helps you out and saves you from spending an afternoon poking around at it like I just did!&lt;/p&gt;</content><author><name>Jesse Keating</name></author><summary type="html">Just a quick tip of you're trying to do the same thing I was trying to do. The Problem I am generating some content on Server A. I want to replicate this content onto Servers B and C. The Solution TL;DR: Read files content from Server A. Write files on Servers B and C from those contents. Ansible provides a couple modules that make this possible. The first one we'll look at is the slurp module. This module allows you to read in contents of a file from a remote system. Here is an task to read the content, utilizing the run_once mechanism: - name: slurp keystone certs slurp: src= with_items: - /etc/keystone/ssl/certs/ca.pem - /etc/keystone/ssl/certs/signing_cert.pem - /etc/keystone/ssl/private/cakey.pem - /etc/keystone/ssl/private/signing_key.pem register: pki_certs run_once: true This will read each of those files in and save the results in the pki_certs variable. Ansible will only do this once, presumably on the host where this content was generated with a previous run_once task. However the variable data will be assigned to every host to make it easily accessible. Next we need to write out the content on our other systems. There are a couple things to consider. First, because the files were read via a with_items loop, the registered content is in a list, specifically in pki_certs.results. This is easy enough to deal with because the results list is a list of dictionaries, and the name of the file is part of that dictionary. The filename resides in the item key, while the content resides in the content key. This allows us to template out both the path to be written as well as the content to be written at that path. The next thing we need to consider is that the slurp module stores content in base 64 encoding. That means when we write it back out, we need to decode it from base 64, otherwise Ansible will happily write out some long strings that look nothing like your file. To decode from base 64, simply use the b64decode filter on the content variable. The last thing to consider has to do with yaml and whitespace and ansible. This may not come into play with every file, but these files have multiple lines. A somewhat recent change in Ansible means that if the &quot;short form&quot; of task description is used (with key=value parameters) your written out file will have double linefeeds. The simple solution is to use &quot;long form&quot; task syntax as you'll see below: - name: write out keystone certs copy: dest: &quot;&quot; content: &quot;&quot; owner: keystone group: keystone mode: 0700 with_items: pki_certs.results Because every host has access to the pki_certs variable this task can run across all of them. You might see a change registered for the first host in the loop, even though it was the source of the content, due to permissions or ownership changes, however subsequent runs will be nice and clean. Hopefully this helps you out and saves you from spending an afternoon poking around at it like I just did!</summary></entry><entry><title type="html">On next chapters</title><link href="https://omgjlk.github.io/navel-gazing/2014/10/27/on-next-chapters.html" rel="alternate" type="text/html" title="On next chapters" /><published>2014-10-27T17:53:18+00:00</published><updated>2014-10-27T17:53:18+00:00</updated><id>https://omgjlk.github.io/navel-gazing/2014/10/27/on-next-chapters</id><content type="html" xml:base="https://omgjlk.github.io/navel-gazing/2014/10/27/on-next-chapters.html">&lt;p&gt;I haven't gotten a lot of sleep lately, I've had quite a lot on my mind.&lt;/p&gt;
&lt;p&gt;I've been a &quot;Racker&quot; for almost two years now. I've helped build an amazing thing. The way we expand, deploy, upgrade, operate, and otherwise manipulate OpenStack and&amp;nbsp; the other infrastructure behind the Rackspace Public Cloud is awesome, and getting better all the time. As much as I would love to stay on and continue to intensify the awesomeness, it was time to say good bye. While I've done a lot of the work, I wasn't the only one, and I know that there are a lot of good people still plugged into the mission and it will go on.&lt;/p&gt;
&lt;p&gt;What's next? Well, I've been a working remotely for over 6 years now. It has been fantastic to be able to be home and around my family as we've grown. I've tried hard to strike a balance between work travels and home life, but sometimes that balance falls out of whack, particularly when the social side of me that craves in person interaction overrules the empathy side of me that keeps me in check when I'm putting too much on the shoulders of those I leave behind. With our boys growing older, spending quality time with them is more important than ever, which is making it harder and harder to make frequent visits to my Rackspace offices. At the same time, my boys are both now in full time school, which opens up my day time hours, which means it's a great time to think about re-joining the &quot;work from office&quot; folks. I've also been working for large publicly traded companies for nearly 10 years, and I really felt like it was time to get back into the small startup game.&lt;/p&gt;
&lt;p&gt;I'm very excited to be joining the &lt;a href=&quot;http://bluebox.net&quot; target=&quot;_blank&quot;&gt;Blue Box&lt;/a&gt; team as an OpenStack Engineer. I get to keep working on OpenStack stuff, and Ansible stuff, and other fun open source things. They're in Seattle, which gives me an excuse to get on my bike a few days a week and multi-modal commute in, and still strike a good balance with my home life. I also get to plug more firmly into the Seattle tech scene, which exploded with awesomeness since I was last a part of it.&lt;/p&gt;
&lt;p&gt;Parting ways is never easy, but the relationships I've built feel strong enough to survive. We'll still see each other at various conferences and meet ups, and we'll all keep working to make OpenStack even better.&lt;/p&gt;</content><author><name>Jesse Keating</name></author><summary type="html">I haven't gotten a lot of sleep lately, I've had quite a lot on my mind. I've been a &quot;Racker&quot; for almost two years now. I've helped build an amazing thing. The way we expand, deploy, upgrade, operate, and otherwise manipulate OpenStack and&amp;nbsp; the other infrastructure behind the Rackspace Public Cloud is awesome, and getting better all the time. As much as I would love to stay on and continue to intensify the awesomeness, it was time to say good bye. While I've done a lot of the work, I wasn't the only one, and I know that there are a lot of good people still plugged into the mission and it will go on. What's next? Well, I've been a working remotely for over 6 years now. It has been fantastic to be able to be home and around my family as we've grown. I've tried hard to strike a balance between work travels and home life, but sometimes that balance falls out of whack, particularly when the social side of me that craves in person interaction overrules the empathy side of me that keeps me in check when I'm putting too much on the shoulders of those I leave behind. With our boys growing older, spending quality time with them is more important than ever, which is making it harder and harder to make frequent visits to my Rackspace offices. At the same time, my boys are both now in full time school, which opens up my day time hours, which means it's a great time to think about re-joining the &quot;work from office&quot; folks. I've also been working for large publicly traded companies for nearly 10 years, and I really felt like it was time to get back into the small startup game. I'm very excited to be joining the Blue Box team as an OpenStack Engineer. I get to keep working on OpenStack stuff, and Ansible stuff, and other fun open source things. They're in Seattle, which gives me an excuse to get on my bike a few days a week and multi-modal commute in, and still strike a good balance with my home life. I also get to plug more firmly into the Seattle tech scene, which exploded with awesomeness since I was last a part of it. Parting ways is never easy, but the relationships I've built feel strong enough to survive. We'll still see each other at various conferences and meet ups, and we'll all keep working to make OpenStack even better.</summary></entry><entry><title type="html">Persistent SSH connections with context!</title><link href="https://omgjlk.github.io/computers/2014/09/05/persistent-ssh-connections-with-context.html" rel="alternate" type="text/html" title="Persistent SSH connections with context!" /><published>2014-09-05T20:00:04+00:00</published><updated>2014-09-05T20:00:04+00:00</updated><id>https://omgjlk.github.io/computers/2014/09/05/persistent-ssh-connections-with-context</id><content type="html" xml:base="https://omgjlk.github.io/computers/2014/09/05/persistent-ssh-connections-with-context.html">&lt;p&gt;SSH, the &lt;a title=&quot;OpenSSH&quot; href=&quot;http://www.openssh.com/&quot;&gt;Secure Shell&lt;/a&gt;, is an awesome tool. Rather indispensable for somebody like me who has to operate on remote systems. I use it constantly to either run code from a privileged host or log into systems to diagnose problems. My entire cloud of servers is just a terminal session away.&lt;/p&gt;
&lt;p&gt;I'm also a huge fan of laptops. I really like being portable with my computer. Partly because I work from home, which means I often work from a coffee shop, or various parts of my home. I don't have a &quot;workstation&quot; that I'm tied to, and I haven't for years. I fell in love with the ease in just closing up my laptop and walking outside, or riding my bike to the cafe and opening it back up to continue work right where I left off.&lt;/p&gt;
&lt;p&gt;Unfortunately, over time, the ease of transport has lessened, and for good reasons. First up is the VPN, or Virtual Private Network. VPNs allow me as a remote person to securely log into my employer's network in order to access resources, or SSH into systems. VPNs are ubiquitous now for remote workers. In the good days, my VPN was automatic. If I closed my laptop and relocated within my house, upon opening my laptop the VPN would re-establish itself without my interaction. SSH, with it's built in ability to re-establish communication would often come back fine, and whatever I was working on, i.e. my context, would be saved. But as time went on, automatic VPNs began to be viewed as insecure. They required stored credentials on my laptop, and it mean that whomever had my laptop had access to these credentials. To combat this, VPNs started using &quot;&lt;a href=&quot;http://en.wikipedia.org/wiki/One-time_password&quot;&gt;One Time Passwords&lt;/a&gt;&quot;, or OTPs. OTPs come in many flavors, but essentially they combine a Thing You Have (like a number generating physical device) with a Thing You Know (a passphrase only you know) into a unique string of characters. The numbers from the device plus your passphrase. This combo could be used only once to authenticate and after that it was invalid. More secure, but this ended the days of automatically established VPNs, and it often meant that the time it took me to re-establish my VPN went beyond SSH's ability to recover a connection. Because of this I'd often find myself walking around my house with my laptop open rather than closed, to keep my connections running. Not nearly as cool and convenient of just closing it and walking around.&lt;/p&gt;
&lt;p&gt;Of course, this doesn't consider transitions from my home to a coffee shop. Two problems there, length of time to get to my destination exceeds SSH recovery time, and the local network details will have changed, preventing SSH recovery completely. This means whenever I go somewhere not my home, I have to re-establish my SSH session(s) and recover my context.&lt;/p&gt;
&lt;p&gt;Keeping context is a solved problem. There are tools out there that help with this. &lt;a href=&quot;http://www.gnu.org/software/screen/&quot;&gt;GNU Screen&lt;/a&gt; and &lt;a href=&quot;http://tmux.sourceforge.net/&quot;&gt;Tmux&lt;/a&gt; are very popular options. These utilities essentially create a terminal session that is insulated from disconnections. When you reconnect to wherever a screen or tmux session is running, you can re-attach to the session and all your context is back. These tools have been around for a while and work really well, when you remember to do your work inside one of them. However getting to them is still a manual process. I have to wait for my SSH session to finally realize it can't re-establish my connection, then I have to re-issue the SSH connection command on my local laptop, and once connected I have to re-attach to whatever session I was working on. Not a lot of work, but certainly an annoyance.&lt;/p&gt;
&lt;p&gt;What I want is something that will keep my SSH connections persistent. Persistent across network outages or even network relocations. Not only do I want the connection itself persistent, but I want the context within that connection to be persistent as well. I don't just want my ssh connection to re-establish itself should it timeout, I want to be re-attached to whatever session I was working in.&lt;/p&gt;
&lt;p&gt;Thankfully there are a few tools out there that help with this! &lt;a href=&quot;https://mosh.mit.edu/&quot;&gt;Mosh&lt;/a&gt; and &lt;a href=&quot;http://www.harding.motd.ca/autossh/&quot;&gt;autossh&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Mosh is kind of the new kid on the block, and is rather interesting. It does a few more things than just keep a persistent connection with context. It also does some things which really help with performance (perceived and actual) over slow connections. When you start a mosh session, it uses ssh to connect to the target and starts some software there, software that your local mosh client will use to communicate with. When the network dies or changes, mosh will quickly re-establish communication with the remote software and your terminal acts as if nothing has changed.&lt;/p&gt;
&lt;p&gt;I played around a bit with mosh when it first came out and discovered some things I didn't like about the setup. First, mosh requires new software be installed on&amp;nbsp; your connection target. This can either be extremely easy, or a nightmare depending on the target, corporate policy, etc... The other thing I really didn't like about mosh is what it does to your local terminal window. I currently use OSX as my operating system, and within it I use &lt;a href=&quot;http://iterm2.com/&quot;&gt;iTerm2&lt;/a&gt; as my terminal emulator. Often I use the built in search function of iTerm2 to find things in scrollback, or I just simply use the touchpad to scroll back my iTerm2 window to read things that have &quot;scrolled off&quot; my screen. These things are quick and natural and useful. Unfortunately the way mosh works, neither of those things are possible. Scrolling back will only show you the things on your terminal from BEFORE you started your mosh session. All that has happened within your mosh session and has scrolled off your screen is lost. Mosh says to use screen or tmux to capture that, and use the scrollback capability of screen or tmux to review or search it. Because of these reasons, I don't use mosh, although I will say it is really neat, and does feel extremely fast. If I worked more on very laggy connections I may feel different about it.&lt;/p&gt;
&lt;p&gt;The other option I mentioned is AutoSSH. AutoSSH is similar to mosh, in that it attempts to re-establish a broken connection, but it is different in a few key ways. First, it's a pure ssh implementation. It does not require additional software to be installed on the remote host, and it does not attempt any communication over anything other than ssh. It does not however attempt to keep context. All it will do by itself is re-establish an ssh connection to a given remote host. In order to retain context, screen or tmux are needed. Thankfully it is trivial to use screen or tmux in a way that automatically (re)connects to a session. In my case, I use screen. Screen has one important feature over Tmux for me, and that feature is the way it does scrollback. When using screen in a iTerm2 window, anything that scrolls off the screen is still in the &quot;history&quot; of iTerm2, which means I can scroll up with the touch pad, or use iTerm2's search feature to find things. This does not work when using Tmux, so I have gone with screen.&lt;/p&gt;
&lt;p&gt;Screen has the ability to with one action either create a new session, or if the session named already exists, disconnect that session from wherever it may be connected and reconnect it to where you are now. That is accomplished via &lt;span class=&quot;lang:sh decode:true  crayon-inline &quot;&gt;$ screen -D -R session_name&lt;/span&gt;&amp;nbsp; . This can be added to an execution of autossh, so that when autossh initially establishes your connection, or ever re-establishes your connection, the execution will run:&lt;/p&gt;
&lt;pre class=&quot;lang:sh decode:true&quot;&gt;$ autossh -M 20000 -t host.name 'screen -D -R session_name'&lt;/pre&gt;
&lt;p&gt;This is nearly perfect, but it doesn't seem to react as fast as mosh does to network disconnects and reconnects. This is due to some defaults in autossh, namely how frequently it polls the monitoring port for activity. The default poll time is 600 seconds, which can be quite a long time. I've found that a poll time of 5 seconds seems to keep things feeling fast. To adjust this, it's as simple as adding an environment variable when launching autossh. Also due to a &lt;a href=&quot;https://bugs.launchpad.net/ubuntu/+source/autossh/+bug/992110&quot;&gt;bug&lt;/a&gt; one needs to also adjust the time autossh will wait to first start polling a connection.&lt;/p&gt;
&lt;pre class=&quot;lang:sh decode:true&quot;&gt;$ AUTOSSH_FIRST_POLL=5 AUTOSSH_POLL=5 autossh -M 20000 -t host.name 'screen -D -R session_name'&lt;/pre&gt;
&lt;p&gt;Now autossh will start monitoring my connection after 5 seconds, and monitor it every 5 seconds for changes. When it reconnects, it will automatically reattach my screen session for context. Any scrollback is still in my terminal window so my local native terminal actions still work, which means I can roam at will without losing my work! Granted, this does require that screen is installed on the remote host, but screen is nearly ubiquitous these days, and hardly ever contentious to get installed if it isn't already on your remote host.&lt;/p&gt;
&lt;p&gt;This setup has made my life more awesome, and I hope it will make your life more awesome too, dear reader. If you have anything to add, or other tricks for this style of work life you'd like to share, please use the comments boxes. They require my approval but I'll get to them quite quickly!&lt;/p&gt;</content><author><name>Jesse Keating</name></author><category term="devops" /><category term="ssh" /><summary type="html">SSH, the Secure Shell, is an awesome tool. Rather indispensable for somebody like me who has to operate on remote systems. I use it constantly to either run code from a privileged host or log into systems to diagnose problems. My entire cloud of servers is just a terminal session away. I'm also a huge fan of laptops. I really like being portable with my computer. Partly because I work from home, which means I often work from a coffee shop, or various parts of my home. I don't have a &quot;workstation&quot; that I'm tied to, and I haven't for years. I fell in love with the ease in just closing up my laptop and walking outside, or riding my bike to the cafe and opening it back up to continue work right where I left off. Unfortunately, over time, the ease of transport has lessened, and for good reasons. First up is the VPN, or Virtual Private Network. VPNs allow me as a remote person to securely log into my employer's network in order to access resources, or SSH into systems. VPNs are ubiquitous now for remote workers. In the good days, my VPN was automatic. If I closed my laptop and relocated within my house, upon opening my laptop the VPN would re-establish itself without my interaction. SSH, with it's built in ability to re-establish communication would often come back fine, and whatever I was working on, i.e. my context, would be saved. But as time went on, automatic VPNs began to be viewed as insecure. They required stored credentials on my laptop, and it mean that whomever had my laptop had access to these credentials. To combat this, VPNs started using &quot;One Time Passwords&quot;, or OTPs. OTPs come in many flavors, but essentially they combine a Thing You Have (like a number generating physical device) with a Thing You Know (a passphrase only you know) into a unique string of characters. The numbers from the device plus your passphrase. This combo could be used only once to authenticate and after that it was invalid. More secure, but this ended the days of automatically established VPNs, and it often meant that the time it took me to re-establish my VPN went beyond SSH's ability to recover a connection. Because of this I'd often find myself walking around my house with my laptop open rather than closed, to keep my connections running. Not nearly as cool and convenient of just closing it and walking around. Of course, this doesn't consider transitions from my home to a coffee shop. Two problems there, length of time to get to my destination exceeds SSH recovery time, and the local network details will have changed, preventing SSH recovery completely. This means whenever I go somewhere not my home, I have to re-establish my SSH session(s) and recover my context. Keeping context is a solved problem. There are tools out there that help with this. GNU Screen and Tmux are very popular options. These utilities essentially create a terminal session that is insulated from disconnections. When you reconnect to wherever a screen or tmux session is running, you can re-attach to the session and all your context is back. These tools have been around for a while and work really well, when you remember to do your work inside one of them. However getting to them is still a manual process. I have to wait for my SSH session to finally realize it can't re-establish my connection, then I have to re-issue the SSH connection command on my local laptop, and once connected I have to re-attach to whatever session I was working on. Not a lot of work, but certainly an annoyance. What I want is something that will keep my SSH connections persistent. Persistent across network outages or even network relocations. Not only do I want the connection itself persistent, but I want the context within that connection to be persistent as well. I don't just want my ssh connection to re-establish itself should it timeout, I want to be re-attached to whatever session I was working in. Thankfully there are a few tools out there that help with this! Mosh and autossh. Mosh is kind of the new kid on the block, and is rather interesting. It does a few more things than just keep a persistent connection with context. It also does some things which really help with performance (perceived and actual) over slow connections. When you start a mosh session, it uses ssh to connect to the target and starts some software there, software that your local mosh client will use to communicate with. When the network dies or changes, mosh will quickly re-establish communication with the remote software and your terminal acts as if nothing has changed. I played around a bit with mosh when it first came out and discovered some things I didn't like about the setup. First, mosh requires new software be installed on&amp;nbsp; your connection target. This can either be extremely easy, or a nightmare depending on the target, corporate policy, etc... The other thing I really didn't like about mosh is what it does to your local terminal window. I currently use OSX as my operating system, and within it I use iTerm2 as my terminal emulator. Often I use the built in search function of iTerm2 to find things in scrollback, or I just simply use the touchpad to scroll back my iTerm2 window to read things that have &quot;scrolled off&quot; my screen. These things are quick and natural and useful. Unfortunately the way mosh works, neither of those things are possible. Scrolling back will only show you the things on your terminal from BEFORE you started your mosh session. All that has happened within your mosh session and has scrolled off your screen is lost. Mosh says to use screen or tmux to capture that, and use the scrollback capability of screen or tmux to review or search it. Because of these reasons, I don't use mosh, although I will say it is really neat, and does feel extremely fast. If I worked more on very laggy connections I may feel different about it. The other option I mentioned is AutoSSH. AutoSSH is similar to mosh, in that it attempts to re-establish a broken connection, but it is different in a few key ways. First, it's a pure ssh implementation. It does not require additional software to be installed on the remote host, and it does not attempt any communication over anything other than ssh. It does not however attempt to keep context. All it will do by itself is re-establish an ssh connection to a given remote host. In order to retain context, screen or tmux are needed. Thankfully it is trivial to use screen or tmux in a way that automatically (re)connects to a session. In my case, I use screen. Screen has one important feature over Tmux for me, and that feature is the way it does scrollback. When using screen in a iTerm2 window, anything that scrolls off the screen is still in the &quot;history&quot; of iTerm2, which means I can scroll up with the touch pad, or use iTerm2's search feature to find things. This does not work when using Tmux, so I have gone with screen. Screen has the ability to with one action either create a new session, or if the session named already exists, disconnect that session from wherever it may be connected and reconnect it to where you are now. That is accomplished via $ screen -D -R session_name&amp;nbsp; . This can be added to an execution of autossh, so that when autossh initially establishes your connection, or ever re-establishes your connection, the execution will run: $ autossh -M 20000 -t host.name 'screen -D -R session_name' This is nearly perfect, but it doesn't seem to react as fast as mosh does to network disconnects and reconnects. This is due to some defaults in autossh, namely how frequently it polls the monitoring port for activity. The default poll time is 600 seconds, which can be quite a long time. I've found that a poll time of 5 seconds seems to keep things feeling fast. To adjust this, it's as simple as adding an environment variable when launching autossh. Also due to a bug one needs to also adjust the time autossh will wait to first start polling a connection. $ AUTOSSH_FIRST_POLL=5 AUTOSSH_POLL=5 autossh -M 20000 -t host.name 'screen -D -R session_name' Now autossh will start monitoring my connection after 5 seconds, and monitor it every 5 seconds for changes. When it reconnects, it will automatically reattach my screen session for context. Any scrollback is still in my terminal window so my local native terminal actions still work, which means I can roam at will without losing my work! Granted, this does require that screen is installed on the remote host, but screen is nearly ubiquitous these days, and hardly ever contentious to get installed if it isn't already on your remote host. This setup has made my life more awesome, and I hope it will make your life more awesome too, dear reader. If you have anything to add, or other tricks for this style of work life you'd like to share, please use the comments boxes. They require my approval but I'll get to them quite quickly!</summary></entry><entry><title type="html">SSH Key Rotation with Ansible</title><link href="https://omgjlk.github.io/ansible/2014/06/07/ssh-key-rotation-with-ansible.html" rel="alternate" type="text/html" title="SSH Key Rotation with Ansible" /><published>2014-06-07T12:14:08+00:00</published><updated>2014-06-07T12:14:08+00:00</updated><id>https://omgjlk.github.io/ansible/2014/06/07/ssh-key-rotation-with-ansible</id><content type="html" xml:base="https://omgjlk.github.io/ansible/2014/06/07/ssh-key-rotation-with-ansible.html">&lt;h2&gt;Introduction to SSH Keys&lt;/h2&gt;
&lt;p&gt;SSH keys are fantastic things. They provide a 2-part blob of data, a private part and a public part, that can be used to authenticate ssh connections. You keep the private part private, often with a passphrase to &quot;unlock&quot; it, while you can hand out the public part to things like &lt;a title=&quot;GitHub SSH Help&quot; href=&quot;https://help.github.com/articles/generating-ssh-keys&quot; target=&quot;_blank&quot;&gt;GitHub&lt;/a&gt;, &lt;a title=&quot;Keypairs for Rackspace Cloud Servers&quot; href=&quot;http://www.rackspace.com/knowledge_center/article/manage-ssh-key-pairs-for-cloud-servers-with-python-novaclient&quot; target=&quot;_blank&quot;&gt;compute clouds&lt;/a&gt;,&amp;nbsp; &lt;a title=&quot;Fedora Account System SSH&quot; href=&quot;https://fedoraproject.org/wiki/Account_System#What_is_an_SSH_key.2C_and_why_do_I_need_one.3F&quot; target=&quot;_blank&quot;&gt;other systems&lt;/a&gt; that you might wish to connect to via SSH, and remote servers you will ssh to. The public part of your SSH key pair gets stored in a special file that SSH servers on remote systems read, the &lt;a title=&quot;authorized_keys format&quot; href=&quot;http://en.wikibooks.org/wiki/OpenSSH/Client_Configuration_Files#.7E.2F.ssh.2Fauthorized_keys&quot; target=&quot;_blank&quot;&gt;authorized_keys&lt;/a&gt; file. When you connect, your ssh client will provide details about your private key that the remote end can validate against your public key to authenticate you. This is a great convenience over having to provide a password every single time.&lt;/p&gt;
&lt;p&gt;This convenience for users is also a necessity for infrastructure administration. SSH is ubiquitous in the Linux world, and the vast majority of administration is accomplished over SSH. Without the ability to use SSH Keys (or similar auth mechanisms) one would not be able to automate actions across many systems easily.&lt;/p&gt;
&lt;p&gt;With convenience comes responsibility though. Having a key that an automated process can use to manipulate your fleet of systems is great, but it's also a pretty juicy attack vector. For that reason it is good practice to rotate your keys often. Rotating keys is the act of replacing the keys you're currently using with new keys, and removing the ability for old keys to be used to log into your systems.&lt;/p&gt;
&lt;p&gt;Rotating keys requires a new key. Creating a new key is &lt;a title=&quot;SSH Keygen&quot; href=&quot;http://www.rackspace.com/knowledge_center/article/connecting-to-a-server-using-ssh-on-linux-or-mac-os#SSH_keygen&quot; target=&quot;_blank&quot;&gt;fairly simple&lt;/a&gt;. Getting the public part of this key out into your fleet, and removing existing public keys is a bit harder. Thankfully we have orchestration and automation tools such as &lt;a title=&quot;Ansible&quot; href=&quot;http://www.ansible.com&quot; target=&quot;_blank&quot;&gt;Ansible&lt;/a&gt;. The rest of this blog post will discuss how to use Ansible to automate rotating your ssh credentials across your fleet.&lt;/p&gt;
&lt;h2&gt;Orchestrating SSH Key Rotation&lt;/h2&gt;
&lt;p&gt;Lets consider the steps necessary to rotate a key:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a new key&lt;/li&gt;
&lt;li&gt;Add new key to authorized_keys files on your fleet&lt;/li&gt;
&lt;li&gt;Test new key&lt;/li&gt;
&lt;li&gt;Remove previous keys from authorized_keys files&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;As stated before, step 1 is simple, and for the sake of this post we'll assume that this has been completed, and there is a new key-pair, located at ~/.ssh/id_rsa_new and ~/.ssh/id_rsa_new.pub. The private key part is id_rsa_new, the public is id_rsa_new.pub. It's the pub we need to distribute. For now, we'll also assume that this key has not yet replaced the existing key, and we can still use the existing key to reach our fleet.&lt;/p&gt;
&lt;p&gt;Step 2 is adding the new key to the authorized_keys file. This is where our &lt;a href=&quot;http://docs.ansible.com/playbooks.html&quot; target=&quot;_blank&quot;&gt;Ansible playbook&lt;/a&gt; will begin. First we need a play header and a couple variables defined to reference the public and private parts of our new key-pair.&lt;/p&gt;
&lt;pre class=&quot;theme:neon nums:false lang:yaml decode:true&quot;&gt;- name: Rotate SSH Keys
  hosts: all
  vars:
    - new_pub_key: ~/.ssh/id_rsa_new.pub
    - new_priv_key: ~/.ssh/id_rsa_new

  tasks:
&lt;/pre&gt;
&lt;p&gt;Next we'll need a task to copy the public part of our new key-pair to the remote hosts. For this we will use the &lt;a title=&quot;authorized_keys module documentation&quot; href=&quot;http://docs.ansible.com/authorized_key_module.html&quot; target=&quot;_blank&quot;&gt;authorized_key&lt;/a&gt; module. This module allows us to provide a key to add, which we will do.&lt;/p&gt;
&lt;pre class=&quot;theme:neon nums:false lang:yaml decode:true&quot; title=&quot;key add task&quot;&gt;    - name: Add new key to authorized_keys
      authorized_key: key=&quot;&quot;
                      user=root state=present
&lt;/pre&gt;
&lt;p&gt;Now for step 3, we will want to test this new key, to make sure that our new key addition is working. To do this, we will need to direct Ansible to use our new private key when connecting to our servers. We can use a &lt;a href=&quot;http://docs.ansible.com/set_fact_module.html&quot; target=&quot;_blank&quot;&gt;set_fact&lt;/a&gt; task to set ansible_ssh_private_key variable to our new private key.&lt;/p&gt;
&lt;pre class=&quot;theme:neon nums:false lang:yaml decode:true&quot; title=&quot;set_fact task&quot;&gt;    - name: Make use of new private key when connecting
      set_fact: ansible_private_ssh_key=&lt;/pre&gt;
&lt;p&gt;Our next task will make use of this new key when creating the connection (provided &lt;a title=&quot;SSH Multiplexing&quot; href=&quot;http://en.wikibooks.org/wiki/OpenSSH/Cookbook/Multiplexing&quot; target=&quot;_blank&quot;&gt;ControlPersist&lt;/a&gt; is not at play).&lt;/p&gt;
&lt;p&gt;The next task is step 4, removing previous keys. Because of our previous task, this step will make use of the new key, and accomplish step 3 along the way.&lt;/p&gt;
&lt;p&gt;Currently, the authorized_key Ansible module does not have a method to remove all but the specified ssh key. However I have sent a &lt;a title=&quot;exclusive pull request&quot; href=&quot;https://github.com/ansible/ansible/pull/7680&quot; target=&quot;_blank&quot;&gt;pull request&lt;/a&gt; to accomplish this, by way of the &lt;strong&gt;exclusive&lt;/strong&gt; keyword. The task here will assume that this pull request has merged.&lt;/p&gt;
&lt;pre class=&quot;theme:neon nums:false lang:yaml decode:true&quot; title=&quot;set new key exclusively&quot;&gt;    - name: Make our new key exclusive
      authorized_key: key=&quot;&quot;
                      user=root state=present exclusive=yes&lt;/pre&gt;
&lt;p&gt;This task looks just like the first task, but with the addition of exclusive=yes. If you don't want to use the modified authorized_key module, you could make use of the &lt;a title=&quot;Copy module documentation&quot; href=&quot;http://docs.ansible.com/copy_module.html&quot; target=&quot;_blank&quot;&gt;copy&lt;/a&gt; module which could get content from the new_pub_key file similar to how authorized_key gets content from the file.&lt;/p&gt;
&lt;p&gt;If all has gone well, all that should be left in the authorized_keys file is the public part of our new key-pair. Our new key has been successfully rotated in and the old key is no longer allowed to log in.&lt;/p&gt;
&lt;h2&gt;Next Steps&lt;/h2&gt;
&lt;p&gt;There are more things we could do with our playbook. We could automate the creation of the key itself, which would look something like this:&lt;/p&gt;
&lt;pre class=&quot;theme:neon nums:false lang:yaml decode:true&quot; title=&quot;generate a new key&quot;&gt;    - name: Create new ssh key-pair
      command: ssh-keygen -t rsa -N &quot;&quot; -q
               -f 
      when: inventory_hostname == play_hosts[0]
      delegate_to: localhost&lt;/pre&gt;
&lt;p&gt;The when conditional here makes sure that only one key is generated, by only running on the first&amp;nbsp; host. Delegation is also used to make the action happen on the system calling ansible, rather than a remote host.&lt;/p&gt;
&lt;p&gt;We could also move the private key file into a location that our local ssh config is prepared to use by default:&lt;/p&gt;
&lt;pre class=&quot;theme:neon nums:false lang:yaml decode:true&quot; title=&quot;move key into position&quot;&gt;    - name: Move new key into default location
      command: mv  ~/.ssh/id_rsa
      when: inventory_hostname == play_hosts[0]
      delegate_to: localhost&lt;/pre&gt;
&lt;p&gt;Any number of other tasks could be added around these, or specific options to the existing tasks. This blog post is just enough to get you started.&lt;/p&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;SSH keys are awesome. Anybody using ssh should be using keys. Keys are powerful, and thus need care. Rotate keys frequently and make sure to invalidate old keys. Automation can make this process a lot easier and more reliable.&lt;/p&gt;
&lt;p&gt;For convenience, here is a complete playbook code block:&lt;/p&gt;
&lt;pre class=&quot;theme:neon nums:false lang:yaml decode:true&quot; title=&quot;Complete rotation playbook&quot;&gt;---
- name: Rotate SSH Keys
  hosts: all
  vars:
    - new_pub_key: ~/.ssh/id_rsa_new.pub
    - new_priv_key: ~/.ssh/id_rsa_new

  tasks:
    - name: Create new ssh key-pair
      command: ssh-keygen -t rsa -N &quot;&quot; -q
               -f 
      when: inventory_hostname == play_hosts[0]
      delegate_to: localhost

    - name: Add new key to authorized_keys
      authorized_key: key=&quot;&quot;
                      user=root state=present

    - name: Make use of new private key when connecting
      set_fact: ansible_private_ssh_key=

    - name: Make our new key exclusive
      authorized_key: key=&quot;&quot;
                      user=root state=present exclusive=yes

    - name: Move new key into default location
      command: mv  ~/.ssh/id_rsa
      when: inventory_hostname == play_hosts[0]
      delegate_to: localhost&lt;/pre&gt;
&lt;p&gt;And lastly here is a horn-less unicorn pooping a rainbow I found on photobucket, because this post has been far too serious.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://smg.photobucket.com/user/Master_Becca/media/unicorn.png.html&quot;&gt;&lt;img src=&quot;http://img.photobucket.com/albums/v629/Master_Becca/unicorn.png&quot; alt=&quot;Wheeeeee!&quot; width=&quot;1024&quot; height=&quot;768&quot; /&gt;&lt;/a&gt;&lt;/p&gt;</content><author><name>Jesse Keating</name></author><category term="ssh" /><category term="ansible" /><summary type="html">Introduction to SSH Keys SSH keys are fantastic things. They provide a 2-part blob of data, a private part and a public part, that can be used to authenticate ssh connections. You keep the private part private, often with a passphrase to &quot;unlock&quot; it, while you can hand out the public part to things like GitHub, compute clouds,&amp;nbsp; other systems that you might wish to connect to via SSH, and remote servers you will ssh to. The public part of your SSH key pair gets stored in a special file that SSH servers on remote systems read, the authorized_keys file. When you connect, your ssh client will provide details about your private key that the remote end can validate against your public key to authenticate you. This is a great convenience over having to provide a password every single time. This convenience for users is also a necessity for infrastructure administration. SSH is ubiquitous in the Linux world, and the vast majority of administration is accomplished over SSH. Without the ability to use SSH Keys (or similar auth mechanisms) one would not be able to automate actions across many systems easily. With convenience comes responsibility though. Having a key that an automated process can use to manipulate your fleet of systems is great, but it's also a pretty juicy attack vector. For that reason it is good practice to rotate your keys often. Rotating keys is the act of replacing the keys you're currently using with new keys, and removing the ability for old keys to be used to log into your systems. Rotating keys requires a new key. Creating a new key is fairly simple. Getting the public part of this key out into your fleet, and removing existing public keys is a bit harder. Thankfully we have orchestration and automation tools such as Ansible. The rest of this blog post will discuss how to use Ansible to automate rotating your ssh credentials across your fleet. Orchestrating SSH Key Rotation Lets consider the steps necessary to rotate a key: Create a new key Add new key to authorized_keys files on your fleet Test new key Remove previous keys from authorized_keys files As stated before, step 1 is simple, and for the sake of this post we'll assume that this has been completed, and there is a new key-pair, located at ~/.ssh/id_rsa_new and ~/.ssh/id_rsa_new.pub. The private key part is id_rsa_new, the public is id_rsa_new.pub. It's the pub we need to distribute. For now, we'll also assume that this key has not yet replaced the existing key, and we can still use the existing key to reach our fleet. Step 2 is adding the new key to the authorized_keys file. This is where our Ansible playbook will begin. First we need a play header and a couple variables defined to reference the public and private parts of our new key-pair. - name: Rotate SSH Keys hosts: all vars: - new_pub_key: ~/.ssh/id_rsa_new.pub - new_priv_key: ~/.ssh/id_rsa_new</summary></entry><entry><title type="html">Breaking the cycle - It’s okay to say “I don’t know”.</title><link href="https://omgjlk.github.io/navel-gazing/2014/06/03/breaking-the-cycle-its-okay-to-say-i-dont-know.html" rel="alternate" type="text/html" title="Breaking the cycle - It's okay to say &quot;I don't know&quot;." /><published>2014-06-03T17:18:55+00:00</published><updated>2014-06-03T17:18:55+00:00</updated><id>https://omgjlk.github.io/navel-gazing/2014/06/03/breaking-the-cycle-its-okay-to-say-i-dont-know</id><content type="html" xml:base="https://omgjlk.github.io/navel-gazing/2014/06/03/breaking-the-cycle-its-okay-to-say-i-dont-know.html">&lt;p&gt;While listening to a recent &lt;a title=&quot;Three hardest words&quot; href=&quot;http://freakonomics.com/2014/05/15/the-three-hardest-words-in-the-english-language-a-new-freakonomics-radio-podcast/&quot; target=&quot;_blank&quot;&gt;Freakonomics podcast&lt;/a&gt;, I learned that from childhood on there is programming to teach us that it is better to have an answer, even if it is wrong, than to not have an answer at all. Why this happens is not fully understood, but the consequences are becoming more apparent. Maybe my upbringing was different, but I've never seemed to have a problem with stating that &quot;I don't know&quot;. But I have ran into people who always seem to have the answer, even if it is wrong.&lt;/p&gt;
&lt;p&gt;People in the business world are no exception. There often can be an expectation set, particularly for people in more senior positions, to be experts in our fields, which to some means always having an answer.&lt;/p&gt;
&lt;p&gt;Far too often though, we don't have an answer. Our skill isn't in knowing all the answers, our skill is in being able to &lt;em&gt;discover&lt;/em&gt; the answer. It's knowing when to ask for help, where to get that help, and how to sort through data to find a solution.&lt;/p&gt;
&lt;p&gt;Stating that &quot;I don't know&quot; is not an admittance of failure. Instead it is an invitation for collaboration and discovery. It is the very nature of science and the &lt;a href=&quot;http://www.sciencebuddies.org/science-fair-projects/project_scientific_method.shtml&quot; target=&quot;_blank&quot;&gt;Scientific Method&lt;/a&gt;; starting with an unknown, research to form a hypothesis, test that hypothesis by experimentation, analyze the data and form a conclusion, then communicate the results. Science works when we start with the unknown and journey to the known. The door is open for others to provide ideas and research to the table. Differing opinions are welcome and debated.&lt;/p&gt;
&lt;p&gt;When we skip passed the &quot;I don't know&quot; stage and jump straight to an answer, particularly when practicing &quot;fake it until you make it&quot;, any differing opinion is viewed as adversarial. This creates conflict rather than collaboration. More effort will be spent to entrench on the wrong answer just to hold up the illusion that one has all the answers. Any evidence that does not support the already chosen answer will be ignored or discounted.&lt;/p&gt;
&lt;p&gt;In these situations, a wrong answer asserted with authority, is far more expensive than the answer of &quot;I don't know&quot; and the subsequent period of discovery. Good money and time will be thrown after bad, just to make the wrong answer work, or to find ways to blame other things from preventing the wrong answer from working. This creates a very hostile work environment and does not foster collaboration.&lt;/p&gt;
&lt;p&gt;Speaking of discovery, the only method to to learn new things is through feedback. Feedback often comes from the process of trail and error, or experimentation. When it is okay to be wrong, it is also okay to make mistakes and errors. This leads to much clearer data and better understanding of the problem at hand. The ability and encouragement to try new things, even if (and especially if) they fail is a fantastic tool for learning, and ultimately for discovering the best answers to a given question.&lt;/p&gt;
&lt;p&gt;In many ways, the ability to admit not knowing the answer to something is a valuable trait, one that should be celebrated, encouraged, and cultivated. An environment should be created and maintained that fosters this behavior. Encourage trail and error. Celebrate failures as data about what doesn't work and use it as data to measure what does work against. The rewards will be many.&lt;/p&gt;
&lt;p&gt;I won't claim that this is the best way, I just don't know. What I do know is that organizations I've been a part of that have embraced the culture of discovery have been more successful and more pleasant to be a part of than those which haven't. I encourage those I work with to not be afraid to state &quot;I don't know&quot;. It's impossible to know everything about everything. Together we work to find answers to the unknown and the journey is just as important as the solution.&lt;/p&gt;</content><author><name>Jesse Keating</name></author><summary type="html">While listening to a recent Freakonomics podcast, I learned that from childhood on there is programming to teach us that it is better to have an answer, even if it is wrong, than to not have an answer at all. Why this happens is not fully understood, but the consequences are becoming more apparent. Maybe my upbringing was different, but I've never seemed to have a problem with stating that &quot;I don't know&quot;. But I have ran into people who always seem to have the answer, even if it is wrong. People in the business world are no exception. There often can be an expectation set, particularly for people in more senior positions, to be experts in our fields, which to some means always having an answer. Far too often though, we don't have an answer. Our skill isn't in knowing all the answers, our skill is in being able to discover the answer. It's knowing when to ask for help, where to get that help, and how to sort through data to find a solution. Stating that &quot;I don't know&quot; is not an admittance of failure. Instead it is an invitation for collaboration and discovery. It is the very nature of science and the Scientific Method; starting with an unknown, research to form a hypothesis, test that hypothesis by experimentation, analyze the data and form a conclusion, then communicate the results. Science works when we start with the unknown and journey to the known. The door is open for others to provide ideas and research to the table. Differing opinions are welcome and debated. When we skip passed the &quot;I don't know&quot; stage and jump straight to an answer, particularly when practicing &quot;fake it until you make it&quot;, any differing opinion is viewed as adversarial. This creates conflict rather than collaboration. More effort will be spent to entrench on the wrong answer just to hold up the illusion that one has all the answers. Any evidence that does not support the already chosen answer will be ignored or discounted. In these situations, a wrong answer asserted with authority, is far more expensive than the answer of &quot;I don't know&quot; and the subsequent period of discovery. Good money and time will be thrown after bad, just to make the wrong answer work, or to find ways to blame other things from preventing the wrong answer from working. This creates a very hostile work environment and does not foster collaboration. Speaking of discovery, the only method to to learn new things is through feedback. Feedback often comes from the process of trail and error, or experimentation. When it is okay to be wrong, it is also okay to make mistakes and errors. This leads to much clearer data and better understanding of the problem at hand. The ability and encouragement to try new things, even if (and especially if) they fail is a fantastic tool for learning, and ultimately for discovering the best answers to a given question. In many ways, the ability to admit not knowing the answer to something is a valuable trait, one that should be celebrated, encouraged, and cultivated. An environment should be created and maintained that fosters this behavior. Encourage trail and error. Celebrate failures as data about what doesn't work and use it as data to measure what does work against. The rewards will be many. I won't claim that this is the best way, I just don't know. What I do know is that organizations I've been a part of that have embraced the culture of discovery have been more successful and more pleasant to be a part of than those which haven't. I encourage those I work with to not be afraid to state &quot;I don't know&quot;. It's impossible to know everything about everything. Together we work to find answers to the unknown and the journey is just as important as the solution.</summary></entry></feed>