<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[KyWa Thoughts]]></title><description><![CDATA[Just a collection of thoughts]]></description><link>https://blog.kywa.io/</link><image><url>https://blog.kywa.io/favicon.png</url><title>KyWa Thoughts</title><link>https://blog.kywa.io/</link></image><generator>Ghost 5.2</generator><lastBuildDate>Sun, 19 Apr 2026 08:06:56 GMT</lastBuildDate><atom:link href="https://blog.kywa.io/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Container Development - Getting Started]]></title><description><![CDATA[<!--kg-card-begin: markdown--><h1 id="container-developmentgetting-started">Container Development - Getting Started</h1>
<p>Application development is something that can be done so many different ways its hard to keep track of all the tools that are available. Some just use whatever standard toolset is available while others try to find &quot;3rd party&quot; tools for their chosen</p>]]></description><link>https://blog.kywa.io/container-development-getting-started/</link><guid isPermaLink="false">625d699ac696efca17ca6dbc</guid><dc:creator><![CDATA[Kyle Walker]]></dc:creator><pubDate>Wed, 07 Sep 2022 18:56:18 GMT</pubDate><content:encoded><![CDATA[<!--kg-card-begin: markdown--><h1 id="container-developmentgetting-started">Container Development - Getting Started</h1>
<p>Application development is something that can be done so many different ways its hard to keep track of all the tools that are available. Some just use whatever standard toolset is available while others try to find &quot;3rd party&quot; tools for their chosen language to help compile/build their application. And then there is application development which is going to be built and then run within a Container. The tools for the Container landscape are massive and grows more every day. Some tools are there to make life easy and others are there because we needed a new standard for something (relevant <a href="https://xkcd.com/927/">xkcd</a>).</p>
<p>I am here to bring some light on a tool that isn&apos;t necesssarily meant to be a new standard, but a tool that can help application developers looking to turn their application into something that can be built easily and run in a Container.</p>
<p><strong>NOTE</strong> Please note there are some starting knowledge required to make use of this post. If you are unfamiliar with building Container Images, I&apos;d recommend taking a look at the 4th blog post in the MineOps series here for a better understanding: <a href="https://blog.kywa.io/mineops-part-4">https://blog.kywa.io/mineops-part-4</a></p>
<h2 id="building-a-container-image">Building a Container Image</h2>
<p>There is really only one way to build a Container Image and that is with the use of a <code>Dockerfile</code>. This file contains instructions on how to build a Container Image. It typically starts with a <code>FROM</code> line which indicates the &quot;base&quot; Container Image to use and then other instructions like <code>COPY</code> or <code>RUN</code> to copy files into the Container Image or run some commands (such as installing packages) in the Container Image. These instructions that get run during build time are what the resulting Container Image will have &quot;in it&quot;. The other important part is what does the resulting Container Image do when it starts? This is typically handled by an <code>ENTRYPOINT</code> or <code>CMD</code> (or both) instruction in the <code>Dockerfile</code> (or inherited from the base image).</p>
<p>Let&apos;s look at a basic example of a <code>Dockerfile</code> to build a simple application that runs a Python Flask application:</p>
<pre><code class="language-docker"># We are going to start with the official Python image from Docker Hub
FROM docker.io/python:3.8

# This sets our working directory (where the Container will &quot;start&quot; from)
WORKDIR /python-container

# Getting our applicatoin code into the container
COPY . .

# Install our dependencies
RUN python3 -m pip install -r requirements.txt

# Tell the Container what to do when it starts
CMD [&quot;python3&quot;, &quot;-m&quot; , &quot;flask&quot;, &quot;run&quot;, &quot;--host=0.0.0.0&quot;]
</code></pre>
<p>The above <code>Dockerfile</code> will result in a Container Image that will start a Flask application:</p>
<pre><code class="language-sh">user@workstation-$ docker build -t pyflask .
[+] Building 3.3s (9/9) FINISHED
 =&gt; [internal] load build definition from Dockerfile                                                               0.0s
 =&gt; =&gt; transferring dockerfile: 214B                                                                               0.0s
 =&gt; [internal] load .dockerignore                                                                                  0.0s
 =&gt; =&gt; transferring context: 2B                                                                                    0.0s
 =&gt; [internal] load metadata for docker.io/library/python:3.8                                                      0.0s
 =&gt; [1/4] FROM docker.io/library/python:3.8                                                                        0.1s
 =&gt; [internal] load build context                                                                                  0.0s
 =&gt; =&gt; transferring context: 180.35kB                                                                              0.0s
 =&gt; [2/4] WORKDIR /python-container                                                                                0.0s
 =&gt; [3/4] COPY . .                                                                                                 0.0s
 =&gt; [4/4] RUN python3 -m pip install -r requirements.txt                                                           2.9s
 =&gt; exporting to image                                                                                             0.1s
 =&gt; =&gt; exporting layers                                                                                            0.1s
 =&gt; =&gt; writing image sha256:7994aab4a965fc6b739456f0c0f738ca3943ccb77f9ff9ad6dbb67aaf82c2514                       0.0s
 =&gt; =&gt; naming to docker.io/library/pyflask                                                                         0.0s

Use &apos;docker scan&apos; to run Snyk tests against images to find vulnerabilities and learn how to fix them

user@workstation-$ docker run -d -p 5000 pyflask
8151bd52f0ea875750f7c832de8ab7f37bdd37fef1d657b7b69e6e184070aae9

user@workstation-$ docker ps
CONTAINER ID   IMAGE     COMMAND                  CREATED         STATUS         PORTS                     NAMES
8151bd52f0ea   pyflask   &quot;python3 -m flask ru&#x2026;&quot;   3 seconds ago   Up 3 seconds   0.0.0.0:62069-&gt;5000/tcp   cool_lamarr

user@workstation-$ curl http://localhost:62069
&lt;!doctype html&gt;
&lt;html&gt;
  &lt;head&gt;
    &lt;title&gt;flaskdev&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
      &lt;h1&gt;Container Flask&lt;/h1&gt;
  &lt;/body&gt;
&lt;/html

user@workstation-$ docker logs 8151bd52f0ea
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on all addresses (0.0.0.0)
   WARNING: This is a development server. Do not use it in a production deployment.
 * Running on http://127.0.0.1:5000
 * Running on http://172.17.0.2:5000 (Press CTRL+C to quit)
172.17.0.1 - - [27/Jul/2022 12:22:33] &quot;GET / HTTP/1.1&quot; 200 -
</code></pre>
<p>This is fine and works well for most people who know how to create a <code>Dockerfile</code> and put the specific instructions in there. What about those who maybe don&apos;t know or are unsure of what they need to do to run their application in a Container?</p>
<p>Enter Source-to-Image, or <code>s2i</code></p>
<h2 id="what-is-s2i">What is <code>s2i</code></h2>
<p>I am not known for my wordsmithing skills, so I will let the <code>s2i</code> repository speak for itself:</p>
<hr>
Source-to-Image (S2I) is a toolkit and workflow for building reproducible container images from source code. S2I produces ready-to-run images by injecting source code into a container image and letting the container prepare that source code for execution. By creating self-assembling builder images, you can version and control your build environments exactly like you use container images to version your runtime environments.
<hr>
<p>The big <strong>tl;dr</strong> of <code>s2i</code> is you can build your application with no need to worry about how to actually build it. With <code>s2i</code> you get a few components to handle things for you. The first is an <code>assemble</code> script which handles the building of the application. The next is a <code>run</code> script that handles the running of the application. The <code>run</code> script has logic to essentially see what packages exist and then determine which path to start the application (primarily a Python situation). The logic in both scripts is fairly straightforward and works for most situations, but can also be expanded upon by creating your own scripts which allows you to add extra logic into them. We will look at how to customize this towards the end of this post.</p>
<h3 id="using-the-s2i-binary">Using the <code>s2i</code> binary</h3>
<p>Since we talked about not needing to know how to actually build your application, lets do this without a Dockerfile first. The <code>s2i</code> binary allows you to do a few things. First of which and the most important to us, is a build. It will make use of <code>docker</code> or <code>podman</code>, whichever it finds in your <code>PATH</code> and then run a build with what is specified. The other common alternative, and this is useful for implementing this into a CI/CD system of some kind, is a <code>generate</code> function. This will &quot;do&quot; the <code>s2i</code> process, but only up to generating a <code>Dockerfile</code> which can then be used or customized further.</p>
<h4 id="s2i-build"><code>s2i build</code></h4>
<p>This will handle a couple of things, first it will look at the content its given (either by local directory or a git clone) and determine how it should be built. It will then generate a <code>Dockerfile</code> which it will then build.</p>
<p>The following will build a Container Image from a local directory</p>
<pre><code class="language-sh">$ s2i build . registry.access.redhat.com/ubi8/python-38 somefuns2i
---&gt; Installing application source ...
---&gt; Installing dependencies ...
Collecting click==8.1.2
Downloading click-8.1.2-py3-none-any.whl (96 kB)
Collecting Flask==2.1.1
Downloading Flask-2.1.1-py3-none-any.whl (95 kB)
Collecting itsdangerous==2.1.2
Downloading itsdangerous-2.1.2-py3-none-any.whl (15 kB)
Collecting Jinja2==3.1.1
Downloading Jinja2-3.1.1-py3-none-any.whl (132 kB)
Collecting MarkupSafe==2.1.1
Downloading MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (25 kB)
Collecting Werkzeug==2.1.1
Downloading Werkzeug-2.1.1-py3-none-any.whl (224 kB)
Colleing gunicorn-20.1.0-py3-none-any.whl (79 kB)
Collecting importlib-metadata&gt;=3.6.0
Downloading importlib_metadata-4.12.0-py3-none-any.whl (21 kB)
Requirement already satisfied: setuptools&gt;=3.0 in /opt/app-root/lib/python3.8/site-packages (from gunicorn==20.1.0-&gt;-r requirements.txt (line 7)) (41.6.0)
Collecting zipp&gt;=0.5
Downloading zipp-3.8.1-py3-none-any.whl (5.6 kB)
Installing collected packages: zipp, MarkupSafe, Werkzeug, Jinja2, itsdangerous, importlib-metadata, click, gunicorn, Flask
Successfully installed Flask-2.1.1 Jinja2-3.1.1 MarkupSafe-2.1.1 Werkzeug-2.1.1 click-8.1.2 gunicorn-20.1.0 importlib-metadata-4.12.0 itsdangerous-2.1.2 zipp-3.8.1
WARNING: You are using pip version 21.2.3; however, version 22.2.2 is available.
You should consider upgrading via the &apos;/opt/app-root/bin/python3.8 -m pip install --upgrade pip&apos; command.
Build completed successfully
</code></pre>
<p>Same as above, but will pull from a Git repository:</p>
<pre><code class="language-sh">$ s2i build https://github.com/someuser/somerepo registry.access.redhat.com/ubi8/python-38 somefuns2i
</code></pre>
<p>If we check our local images, we will see that we have a new image:</p>
<pre><code class="language-sh">$ docker images
REPOSITORY       TAG          IMAGE ID       CREATED          SIZE
somefuns2i       latest       e1f3884208c1   34 seconds ago   861MB
</code></pre>
<h4 id="s2i-generate"><code>s2i generate</code></h4>
<p>What if you don&apos;t want to build the image right away and want to have it handled somewhere else down the line such as some CI/CD tool? You can use the <code>generate</code> function to generate a <code>Dockerfile</code> (but you can call it whatever you want) which can then be handed off to something else (such as <code>buildah</code>, <code>podman</code> or even <code>docker</code> itself).</p>
<pre><code class="language-sh">$ s2i generate registry.access.redhat.com/ubi8/python-38 Dockerfile
$ cat Dockerfile
FROM registry.access.redhat.com/ubi8/python-38
LABEL &quot;io.openshift.s2i.build.image&quot;=&quot;registry.access.redhat.com/ubi8/python-38&quot; \
      &quot;io.openshift.s2i.scripts-url&quot;=&quot;image:///usr/libexec/s2i&quot;
      
USER root
# Copying in source code
COPY upload/src /tmp/src
# # Change file ownership to the assemble user. Builder image must support chown command.
RUN chown -R 1001:0 /tmp/src
USER 1001
# # Assemble script sourced from builder image based on user input or image metadata.
# # If this file does not exist in the image, the build will fail.
RUN /usr/libexec/s2i/assemble
# # Run script sourced from builder image based on user input or image metadata.
# # If this file does not exist in the image, the build will fail.
CMD /usr/libexec/s2i/run
</code></pre>
<h3 id="using-an-s2i-base-image">Using an <code>s2i</code> base image</h3>
<p>If you wish to customize the <code>Dockerfile</code> for your build, there are base images that Red Hat has created that can be used for free based on the Universasl Base Image (UBI). These images can be found from the Red Hat Container Catalaog located here: <a href="https://catalog.redhat.com/software/containers/search">https://catalog.redhat.com/software/containers/search</a></p>
<p>Although there are plenty of languages that have pre-built <code>s2i</code> images ready for use, we will be focusing on Python. For reference we will be using this <a href="https://catalog.redhat.com/software/containers/rhel8/python-38/5dde9cb15a13461646f7e6a2">Python 3.8</a> image for our testing. At the bottom of this post you will find <a href="#links">links</a> which point to the various files that these images use.</p>
<p>Let&apos;s create a Dockerfile to make use of this image (found in the Python SCLORG link down below):</p>
<pre><code class="language-Docker">FROM registry.access.redhat.com/ubi8/python-38:latest

# Add application sources to a directory that the assemble script expects them
# and set permissions so that the container runs without root access
USER 0
ADD . /tmp/src
RUN /usr/bin/fix-permissions /tmp/src
USER 1001

# Install the dependencies
RUN /usr/libexec/s2i/assemble

# Set the default command for the resulting image
CMD /usr/libexec/s2i/run
</code></pre>
<p>The neat thing about the above is that we don&apos;t have to change anything about our code or change anything in our <code>Dockerfile</code>. This <code>Dockerfile</code> will stay the same althroughout our work with Python and never really need to change.</p>
<p>Here is the output of building with an image like this:</p>
<pre><code>user@workstation-$ docker build -t quay.io/kywa/container-development:latest .
#1 [internal] load build definition from Dockerfile
#1 sha256:355e2d734470eaccba3699918560cf74249427fab8fc8285115d7e37b9a2b02d
#1 transferring dockerfile: 452B done
#1 DONE 0.1s

#2 [internal] load .dockerignore
#2 sha256:f4561062f8f809345cf9a5a1d5a6d5423c7a4da4ee4d612192a431a0793e307e
#2 transferring context: 2B done
#2 DONE 0.1s

#3 [internal] load metadata for registry.access.redhat.com/ubi8/python-39:latest
#3 sha256:ce534b0fd64e6c22a3c7a7247a4d676ce56b73f1d40fc33c3418b830c2abbbc9
#3 DONE 0.6s

#5 [internal] load build context
#5 sha256:2a59b27c38b259862c7e3fd8e68c83e6e5cdde8834f720a528ef6dcd51308060
#5 transferring context: 2.47kB done
#5 DONE 0.0s

#4 [1/4] FROM registry.access.redhat.com/ubi8/python-39:latest@sha256:84639af111ab2b3fd7ca325f0912e405c117df31c976446b2a1902948f65a61d
#4 sha256:a46474a8524b7c703c05ae33a46c18a083294a470688705fe751eb7a26b2b9d4
#4 CACHED

#6 [2/4] ADD . /tmp/src
#6 sha256:57bd845075e1b8191c3629d7ad152b942f2c095798c555198a95876d545eb180
#6 DONE 0.0s

#7 [3/4] RUN /usr/bin/fix-permissions /tmp/src
#7 sha256:8189bedac5eec60275eafe95f067ae5eed3d0015100bf6496467d160526f9b0e
#7 DONE 0.5s

#8 [4/4] RUN /usr/libexec/s2i/assemble
#8 sha256:408bdc819e3ee26297474bac423e5a3f0be03ba1399f7b324c96a346a4928790
#8 0.521 ---&gt; Installing application source ...
#8 0.562 ---&gt; Installing dependencies ...
#8 0.877 Collecting click==8.1.2
#8 0.991   Downloading click-8.1.2-py3-none-any.whl (96 kB)
#8 1.048 Collecting Flask==2.1.1
#8 1.063   Downloading Flask-2.1.1-py3-none-any.whl (95 kB)
#8 1.096 Collecting itsdangerous==2.1.2
#8 1.111   Downloading itsdangerous-2.1.2-py3-none-any.whl (15 kB)
#8 1.168 Collecting Jinja2==3.1.1
#8 1.183   Downloading Jinja2-3.1.1-py3-none-any.whl (132 kB)
#8 1.295 Collecting MarkupSafe==2.1.1
#8 1.308   Downloading MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (25 kB)
#8 1.373 Collecting Werkzeug==2.1.1
#8 1.389   Downloading Werkzeug-2.1.1-py3-none-any.whl (224 kB)
#8 1.435 Collecting gunicorn==20.1.0
#8 1.449   Downloading gunicorn-20.1.0-py3-none-any.whl (79 kB)
#8 1.561 Collecting importlib-metadata&gt;=3.6.0
#8 1.572   Downloading importlib_metadata-4.12.0-py3-none-any.whl (21 kB)
#8 1.586 Requirement already satisfied: setuptools&gt;=3.0 in /opt/app-root/lib/python3.9/site-packages (from gunicorn==20.1.0-&gt;-r requirements.txt (line 7)) (50.3.2)
#8 1.645 Collecting zipp&gt;=0.5
#8 1.659   Downloading zipp-3.8.1-py3-none-any.whl (5.6 kB)
#8 1.729 Installing collected packages: zipp, MarkupSafe, Werkzeug, Jinja2, itsdangerous, importlib-metadata, click, gunicorn, Flask
#8 2.113 Successfully installed Flask-2.1.1 Jinja2-3.1.1 MarkupSafe-2.1.1 Werkzeug-2.1.1 click-8.1.2 gunicorn-20.1.0 importlib-metadata-4.12.0 itsdangerous-2.1.2 zipp-3.8.1
#8 2.235 WARNING: You are using pip version 21.2.3; however, version 22.2.2 is available.
#8 2.235 You should consider upgrading via the &apos;/opt/app-root/bin/python3.9 -m pip install --upgrade pip&apos; command.
#8 DONE 2.3s

#9 exporting to image
#9 sha256:e8c613e07b0b7ff33893b694f7759a10d42e180f2b4dc349fb57dc6b71dcab00
#9 exporting layers 0.1s done
#9 writing image sha256:a4e6b75c423b51b4708f5ab99bfc7178e5fe4ac0c37035b869b695e344e532d6 done
#9 naming to quay.io/kywa/container-development:latest done
#9 DONE 0.1s

Use &apos;docker scan&apos; to run Snyk tests against images to find vulnerabilities and learn how to fix them
</code></pre>
<h3 id="customizing-s2i">Customizing <code>s2i</code></h3>
<p>One of the other neat components of <code>s2i</code> images is that when it goes to build or run, it detects if your repository has its own <code>.s2i/</code> directory and will use whatever scripts you have in there. Sometimes your build may need something other than just language libraries/modules. What if your application needs something else to run, such as ODBC drivers for a database connection? This is how we would do this, by customizing <code>s2i</code> with our own scripts that can extend upon what already exists.</p>
<p>If you are already using the <code>s2i</code> scripts to build and run your images, you do not have to actually change anything and can keep what is already there. Let&apos;s see how to do that:</p>
<pre><code class="language-sh">user@workstation-$ tree
.
&#x251C;&#x2500;&#x2500; Dockerfile
&#x251C;&#x2500;&#x2500; .s2i
&#x2502;   &#x2514;&#x2500;&#x2500; bin
&#x2502;       &#x251C;&#x2500;&#x2500; assemble
&#x2502;       &#x2514;&#x2500;&#x2500; run
&#x251C;&#x2500;&#x2500; app
&#x2502;   &#x251C;&#x2500;&#x2500; __init__.py
&#x2502;   &#x251C;&#x2500;&#x2500; routes.py
&#x2502;   &#x2514;&#x2500;&#x2500; templates
&#x2502;       &#x251C;&#x2500;&#x2500; base.html
&#x2502;       &#x2514;&#x2500;&#x2500; index.html
&#x251C;&#x2500;&#x2500; main.py
&#x251C;&#x2500;&#x2500; requirements.txt
&#x2514;&#x2500;&#x2500; wsgi.py
</code></pre>
<p>[[In]] our repository, we have our main application code as well as a <code>.s2i/bin</code> directory with 2 files in it, <code>assemble</code> and <code>run</code>. These 2 files have different uses but are both picked up by the <code>ENTRYPOINT</code> scripts of the existing image. These 2 files will be run at build and run time (<code>assemble</code> during build and <code>run</code> during runtime), but you can also have these files just be specific customizations you want and then have them run the main <code>s2i</code> scripts after or before the customizations. For the <code>assemble</code> script, it could look something like this:</p>
<pre><code class="language-sh">user@workstation-$ cat .s2i/bin/assemble
#!/bin/bash

# insert code sause for OS based depends
# this file can be used in place of a Dockerfile
# to patch s2i

echo &quot;I&apos;m a customization before the actual application build&quot;
echo &quot;&quot;

${STI_SCRIPTS_PATH}/assemble
</code></pre>
<p>The above <code>s2i/bin/assemble</code> script will run during build time and will go and get some RPM file and install it, but right after will carry on with the rest of the <code>s2i</code> assemble process.</p>
<p>And the same is done for the <code>run</code> script:</p>
<pre><code>user@workstation-$ cat .s2i/bin/run
#!/bin/bash

# this file can be used in place of a Dockerfile
# to patch s2i

${STI_SCRIPTS_PATH}/run
</code></pre>
<p>There is no real limit to what you can customize, just the limits of your imagination. You could even not call the existing <code>run</code> script and could have it do something very specific:</p>
<pre><code>user@workstation-$ cat .s2i/bin/run
#!/bin/bash
export SOMEVAR=prod
python3 app.py
</code></pre>
<p>The above is a horrible example, but gets the idea across you could do whatever you need/want to do for your application. Just note this is going to be run during a step where you are a &quot;mortal&quot; user. If you are needing to complete tasks at an OS level, you will need to make use of a <code>Dockerfile</code> where you can specify the <code>USER</code>.</p>
<p>Using these custimzations looks like this:</p>
<pre><code>user@workstation-$ s2i build ./ registry.access.redhat.com/ubi8/python-39:latest
I&apos;m a customization before the actual application build

---&gt; Installing application source ...
---&gt; Installing dependencies ...
Collecting click==8.1.2
Downloading click-8.1.2-py3-none-any.whl (96 kB)
Collecting Flask==2.1.1
Downloading Flask-2.1.1-py3-none-any.whl (95 kB)
Collecting itsdangerous==2.1.2
Downloading itsdangerous-2.1.2-py3-none-any.whl (15 kB)
Collecting Jinja2==3.1.1
Downloading Jinja2-3.1.1-py3-none-any.whl (132 kB)
Collecting MarkupSafe==2.1.1
Downloading MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (25 kB)
Collecting Werkzeug==2.1.1
Downloading Werkzeug-2.1.1-py3-none-any.whl (224 kB)
Collecting gunicorn==20.1.0
Downloading gunicorn-20.1.0-py3-none-any.whl (79 kB)
Collecting importlib-metadata&gt;=3.6.0
Downloading importlib_metadata-4.12.0-py3-none-any.whl (21 kB)
Requirement already satisfied: setuptools&gt;=3.0 in /opt/app-root/lib/python3.9/site-packages (from gunicorn==20.1.0-&gt;-r requirements.txt (line 7)) (50.3.2)
Collecting zipp&gt;=0.5
Downloading zipp-3.8.1-py3-none-any.whl (5.6 kB)
Installing collected packages: zipp, MarkupSafe, Werkzeug, Jinja2, itsdangerous, importlib-metadata, click, gunicorn, Flask
Successfully installed Flask-2.1.1 Jinja2-3.1.1 MarkupSafe-2.1.1 Werkzeug-2.1.1 click-8.1.2 gunicorn-20.1.0 importlib-metadata-4.12.0 itsdangerous-2.1.2 zipp-3.8.1
WARNING: You are using pip version 21.2.3; however, version 22.2.2 is available.
You should consider upgrading via the &apos;/opt/app-root/bin/python3.9 -m pip install --upgrade pip&apos; command.
Build completed successfully
</code></pre>
<h2 id="conclusion">Conclusion</h2>
<p>This has been a brief look at a technology for building and running Container Images that is outside the norm, but can provide some value to those who may not want or need to know &quot;what&quot; exactly is going on when building an image. I would however highly recommend that you do understand what is actually going on so that if the need arises when things get more complex.</p>
<h2 id="links">Links</h2>
<ul>
<li><code>s2i</code> - <a href="https://github.com/openshift/source-to-image">https://github.com/openshift/source-to-image</a></li>
<li><code>s2i</code> Deep Dive - <a href="https://www.youtube.com/watch?v=flI6zx9wH6M&amp;ab_channel=OpenShift">https://www.youtube.com/watch?v=flI6zx9wH6M&amp;ab_channel=OpenShift</a> (Somewhat older, but still valid)</li>
<li>SCLORG - <a href="https://www.softwarecollections.org/en/">https://www.softwarecollections.org/en/</a></li>
<li>GitHub SCLORG - <a href="https://github.com/sclorg">https://github.com/sclorg</a></li>
</ul>
<p>The following are for reproduceable Docker images in the following languages. Note there are others available, but these are the most common. Check the link above for PHP, Ruby and others.</p>
<h3 id="python">Python</h3>
<p><a href="https://github.com/sclorg/s2i-python-container">https://github.com/sclorg/s2i-python-container</a></p>
<h3 id="nodejs">NodeJS</h3>
<p><a href="https://github.com/sclorg/s2i-nodejs-container">https://github.com/sclorg/s2i-nodejs-container</a></p>
<h3 id="net">.NET</h3>
<p><a href="https://github.com/redhat-developer/s2i-dotnetcore">https://github.com/redhat-developer/s2i-dotnetcore</a></p>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[Tekton is groovy...]]></title><description><![CDATA[<p>Lets not upset anyone, Tekton doesn&apos;t use &quot;Groovy&quot; as its configuration language (thankfully). Tekton is groovy in its own right however. &#xA0;I believe Ash said it best:</p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/05/tekton-groovy.png" class="kg-image" alt loading="lazy" width="575" height="307"></figure><h2 id="short-history-of-cicd-tooling">Short History of CI/CD Tooling</h2><p>For those that don&apos;t understand what <a href="https://www.jenkins.io/doc/book/pipeline/getting-started/">Groovy</a> is (the syntax</p>]]></description><link>https://blog.kywa.io/tekton-is-groovy/</link><guid isPermaLink="false">61a441cc02da49a7d4f76b7c</guid><dc:creator><![CDATA[Kyle Walker]]></dc:creator><pubDate>Sat, 18 Jun 2022 14:49:07 GMT</pubDate><content:encoded><![CDATA[<p>Lets not upset anyone, Tekton doesn&apos;t use &quot;Groovy&quot; as its configuration language (thankfully). Tekton is groovy in its own right however. &#xA0;I believe Ash said it best:</p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/05/tekton-groovy.png" class="kg-image" alt loading="lazy" width="575" height="307"></figure><h2 id="short-history-of-cicd-tooling">Short History of CI/CD Tooling</h2><p>For those that don&apos;t understand what <a href="https://www.jenkins.io/doc/book/pipeline/getting-started/">Groovy</a> is (the syntax used for Jenkins Pipelines), I&apos;m happy for you. For those who don&apos;t know the person or the film the above image is from, please stop whatever it is you are doing (including reading this) and go watch <a href="https://www.imdb.com/title/tt0106308/?ref_=fn_al_tt_1">Army of Darkness</a>.</p><p>Before we dive into Tekton, let&apos;s take a quick look at &quot;the big one&quot; that has been used for many years now, Jenkins. Sure there are other tools out there that have been used over the years, but Jenkins has probably been the biggest one and had the widest use for the longest time. So what is Jenkins? Jenkins is an open-source &quot;automation server&quot;, which really translates to a platform based on Java that is extensible with many plugins to do numerous things. It could be run anywhere and years ago would have many VMs dedicated to running large instances for an organization, or you could have it running in your Kubernetes cluster in containers.</p><p>One of the things that made Jenkins popular was a platform that was &quot;extensible&quot;, but allowed you to have a someone custom execution environment. For the most part it was basically the ability to run shell commands with some plugins that took arguments. Here is a brief example of what a Jenkins pipeline would look like to build and deploy a new application:</p><pre><code class="language-groovy">pipeline {
   agent any
   stages {
       stage(&apos;Build&apos;) {
           steps {
               sh &apos;make&apos;
           }
       }
       stage(&apos;Test&apos;){
           steps {
               sh &apos;make check&apos;
               junit &apos;reports/**/*.xml&apos;
           }
       }
       stage(&apos;Deploy&apos;) {
           steps {
               sh &apos;make publish&apos;
           }
       }
   }
}</code></pre><p>As you can see from above, its somewhat logically laid out and clearly defines each stage of the pipeline. You may notice though that this basic pipeline is essentially running shell commands and nothing really special. The <code>agent</code> line up above specifies which &quot;instance&quot; to run on. One of the great things about this is you could have certain agents that had different tooling and were lighter weight. The big thing here I want to point out is the syntax of Groovy, which looks like JSON. Nothing is wrong with JSON except for the humans having to write it. Just to be clear, you can technically get this out of the Jenkins UI, but that isn&apos;t very declarative or repeatable, so we&apos;re going to focus on the manifest pipelines like above.</p><p>JSON style formatting is great for machines, but for humans it isn&apos;t exactly easy to read/write. Now the above is somewhat easy, but its only 1-2 line objects for each stage. As this would grow, or having other types of components in the various stages, the above would go from somewhat readable to a large collection of curly braces. Here is roughly the same pipeline, but in Tekton format:</p><pre><code class="language-yaml">apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
  name: my-pipeline
spec:
  workspaces:
  - name: shared-dir
  tasks:
  - name: build
    taskRef:
      name: app-build
    workspaces:
    - name: source
      workspace: shared-dir
  - name: test
    taskRef:
      name: app-test
    workspaces:
    - name: source
      workspace: shared-dir
  - name: deploy
    taskRef:
      name: app-deploy
    workspaces:
    - name: source
      workspace: shared-dir</code></pre><p><strong>NOTE:</strong> I want to call out in the above is that it is 5 lines longer than the Jenkins pipeline and you are getting technically the same pipeline of tasks in the end.</p><p>For those who are familiar with Kubernetes manifests (and I hope you are at least somewhat, since we are looking at a tool/platform for Kubernetes), you will notice that the Tekton example above is just that, a Kubernetes manifest whereas the Jenkins file further above is &quot;its own thing&quot;. Tekton itself is a &quot;cloud-native&quot; platform (aka Kubernetes native) that installs and becomes an extension of the Kubernetes cluster itself. The huge benefit here on its own over Jenkins is that, you can use your existing tools to work with Tekton since its &quot;in cluster&quot; and native. </p><p>Enough looking at Jenkins compared to Tekton, let&apos;s actually start digging into Tekton itself. If you do not understand the Tekton <code>Pipeline</code> example above, fear not, we will be doing a deep dive on that very shortly. For all examples (except the pseudo-code here and there) the manifests can all be found in the supporting repository for this post:</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/KyWa/tekton-is-groovy"><div class="kg-bookmark-content"><div class="kg-bookmark-title">GitHub - KyWa/tekton-is-groovy</div><div class="kg-bookmark-description">Contribute to KyWa/tekton-is-groovy development by creating an account on GitHub.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://github.com/fluidicon.png" alt><span class="kg-bookmark-author">GitHub</span><span class="kg-bookmark-publisher">KyWa</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://opengraph.githubassets.com/21586f1634c2681a68e19137345d67cb2e1cd3a5a7b6e40dd7f07ef82b6d12bf/KyWa/tekton-is-groovy" alt></div></a></figure><h2 id="a-brief-overview-of-tekton">A Brief overview of Tekton</h2><p>If you are familiar with Ansible (this is not a CI/CD tool, although some try to use it for that purpose), there are some super easy comparisons to make so you can grasp the components of Tekton quite easily:</p><p>A lot of the components of Tekton can easily be translated to Ansible items (and almost easily to Jenkins items). For those who are unfamiliar with Ansible, then this is a great little listing of some of the components of Ansible. I do also have a primer written for Ansible in a <a href="https://blog.kywa.io/mineops-part-2/">previous post</a> during the MineOps series that can be read if you are curious about Ansible.</p><!--kg-card-begin: markdown--><table>
<thead>
<tr>
<th>Tekton</th>
<th>Jenkins</th>
<th>Ansible</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Task</code> / <code>ClusterTask</code></td>
<td><code>stage</code> in <code>Jenkinsfile</code></td>
<td><code>task</code></td>
<td>Definition of &quot;what to do&quot;</td>
</tr>
<tr>
<td><code>TaskRun</code></td>
<td><code>N/A</code></td>
<td><code>ansible -m shell</code></td>
<td>Execute a <code>Task</code></td>
</tr>
<tr>
<td><code>Pipeline</code></td>
<td><code>Jenkinsfile</code></td>
<td><code>playbook.yml</code></td>
<td>A collection of <code>Tasks</code></td>
</tr>
<tr>
<td><code>PipelineRun</code></td>
<td>Jenkins UI or CLI</td>
<td><code>ansible-playbook playbook.yml</code></td>
<td>Executes a <code>Pipeline</code></td>
</tr>
</tbody>
</table>
<!--kg-card-end: markdown--><p>I try to make the comparison to Ansible due to the nature of how Tekton works as its very &quot;Ansible-esque&quot;. You put your tasks with their parameters and send them off to do whatever it is you want. Ansible has the ability to only run certain tasks &quot;when&quot; something occurs or if a certain variable is set, but they are run in order from top down through the playbook or &quot;pipeline&quot;. In Tekton you also get a <code>when</code> field, which functions exactly like you would expect, but you also get a different field you can add, <code>runAfter</code>. This allows you to run certain tasks only after others have run. Most devs will probably build out their <code>Pipelines</code> in order top down, but it does give you the ability to only run certain tasks after others have run (such as a test cant be run until the build has been run).</p><h3 id="tekton-component-task">Tekton Component: Task</h3><p>Let&apos;s take a look at what a Tekton <code>Task</code> looks like so we can better understand what a <code>Pipeline</code> is and how it works.</p><figure class="kg-card kg-code-card"><pre><code class="language-yaml">---
apiVersion: tekton.dev/v1beta1
kind: Task
metadata:
  name: basic-task
spec:
  params:
    - name: URL
      type: string
      description: &quot;What URL you wish to curl&quot;
  steps:
  - name: curl-something
    image: someimage:tag
    command:
    - curl
    - $(params.URL)</code></pre><figcaption>a basic Tekton Task</figcaption></figure><p>In the above manifests, we have very few fields, but they are all very self explanatory. No need to explain anything outside of the <code>spec</code> field except that the <code>apiVersion</code> will most likely eventually update to no longer be a <code>v1beta1</code> endpoint, so just keep that in mind.</p><p>A breakdown of what we have in the <code>Task</code> is fairly straightforward as this is a very basic <code>Task</code>. Let&apos;s cover the fields we have and what they do:</p><!--kg-card-begin: markdown--><ul>
<li><code>params</code> - This is where parameters for the <code>Task</code> can be specified as far as what it can make use of. You must specify at least the <code>name</code> and <code>type</code> fields, but you can also specify a <code>description</code> and <code>default</code> field. The <code>default</code> field is where you can specify a default value if none are provided when the <code>Task</code> is run (either from a <code>PipelineRun</code> or <code>TaskRun</code>, but more on this later)</li>
<li><code>steps</code> - Determines the steps the task needs to take to complete. A note here, a <code>Task</code> can have many steps and each step is a container in the Pod that is created for the <code>Task</code>
<ul>
<li><code>steps.name</code> - This names the step in the Task. In the above example the container name would be <code>step-curl-something</code></li>
<li><code>steps.image</code> - Which container image to use for this <code>Task</code> (we will explore this a little bit more)</li>
<li><code>steps.command</code> - Just like a container manifest you specify what command to execute when the container starts up, or in this case the step. You could also use <code>script</code> instead of <code>command</code>, but again, more on this later just know that neither is required as the image may have its own command you want to use</li>
</ul>
</li>
</ul>
<!--kg-card-end: markdown--><p>One of the unique things you may have noticed is in the <code>command</code> arguments a very &quot;variable&quot; looking line that shows <code>$(params.URL)</code>. This is one of the components of Tekton that allows you to pass in parameters to the <code>Task</code> and not even just lines in the <code>command</code> field, you can also use this for the <code>image</code> line and others. Tekton has a webhook that validates all of the objects passed in prior to creating the <code>Task</code>. This gives you the ability to &quot;templatize&quot; your Kubernetes manifests (at least Tekton manifests) by having some fields be dynamic. One unique way to utilize this feature is by having the <code>image</code> line(s) of a <code>Tasks</code> step(s) have a tag that is specified by a parameter. An example of this, would be having a <code>Task</code> which does &quot;something&quot; for you, but could be re-used for a different purpose if a different image/tag was used (maybe testing different versions of <code>NodeJS</code>). You can keep the same <code>Task</code> with the same test logic, but you can have the image used be different when you run it depending on the <code>NodeJS</code> version you are looking to test.</p><p>There is no way we can go over all of the specifications for a Tekton <code>Task</code> in this post, but here are 2 resources that may help for a more detailed dive into the components of a Tekton <code>Task</code>:</p><ul><li><a href="https://tekton.dev/docs/pipelines/tasks/">Tekton Task docs</a></li><li><a href="https://github.com/KyWa/blogs/blob/master/tekton-is-groovy/tasks/task-exploded.yaml">An exploded view of a Task with all options and comments</a></li></ul><h3 id="tekton-component-taskrun">Tekton Component: TaskRun</h3><p>A <code>TaskRun</code> is just what it sounds like, it runs a <code>Task</code>. Thankfully it doesn&apos;t get much more straight forward than this, so lets take a look at what a <code>TaskRun</code> looks like to run our simple <code>curl</code> task above:</p><pre><code>---
apiVersion: tekton.dev/v1beta1
kind: TaskRun
metadata:
  name: basic-task-run
spec:
  params:
  - name: URL
    value: https://github.com
  taskRef:
    kind: Task
    name: basic-task</code></pre><p>The <code>TaskRun</code> looks very similar to our <code>Task</code> (being how basic and minimalistic it is) and we can see it has a <code>params</code> spec just like a <code>Task</code> does. The <code>params</code> field(s) here are used to &quot;pass&quot; values to a <code>Task</code> that is being run through the <code>TaskRun</code> object. If a <code>TaskRun</code> is created without having all the <code>params</code> required by the <code>Task</code> that do not have default values set, the <code>TaskRun</code> will fail due to missing `params. It is important to know the <code>Tasks</code> you are attempting to run, although that should go without saying.</p><p>Since we mentioned having your Kubernetes manifest templatized, it would be important to point out you can use a <code>podTemplate</code> in a <code>TaskRun</code> (and <code>PipelineRun</code>) objects allowing you even finer control. This gives you the ability to call out specific things such as a <code>dnsConfig</code> or other Pod specific fields you cannot add to a <code>Task</code>. We wont really get into that, but its important </p><h3 id="tekton-component-pipeline">Tekton Component: Pipeline</h3><p><code>Pipelines</code> are what make Tekton fun to use as you don&apos;t have to declare how you want all of the <code>Tasks</code> to run and in what order. A <code>Pipeline</code> can be described by the following:</p><ul><li>A collection of <code>Tasks</code> to be run</li><li>The order in which said <code>Tasks</code> run</li><li>What parameters it provides to the <code>Tasks</code> when they are run</li></ul><p>I said the word &quot;run&quot; quite a bit there, that is because everything in Tekton has to be &quot;run&quot; in some capacity (with the &quot;technical&quot; exception of <code>Triggers</code>). </p><p>A <code>Pipeline</code> looks very similar to a <code>Task</code> in its structure, but is typically &quot;trimmed&quot; down somewhat in comparison. Unlike <code>Tasks</code> however, a <code>Pipeline</code> doesn&apos;t necessarily run through its list of <code>Tasks</code> serially. Below we have an example <code>Pipeline</code> to run our <code>basic-task</code> to <code>curl</code>:</p><pre><code>---
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
  name: basic-pipeline
spec:
  params:
    - name: URL
      type: string
      description: Which URL to curl
  tasks:
    - name: get-url
      taskRef:
        name: basic-task
      params:
        - name: URL
          value: $(params.URL)</code></pre><p>Now some of this may look like a &quot;duplication&quot; of what is in the <code>Task</code> and you&apos;d be right to say that at first glance. A single <code>Task</code> and a Pipeline really don&apos;t mix as <code>Pipelines</code> are meant to be a compilation of <code>Tasks</code>, so at first glance you seem to have duplicates of parameters. In reality you are specifying what parameters a <code>Pipeline</code> accepts (similar to a <code>Task</code>), but these parameters can have their own names unique to the <code>Pipeline</code>. When you pass in a parameter to a <code>Task</code> in the <code>tasks</code> field of the <code>Pipeline</code> you will pass the parameter name the <code>Task</code> needs, but you can give it a predefined value, or as in the example above, give it a value equal to the <code>Pipelines</code> parameter. This is done through <a href="https://tekton.dev/docs/pipelines/variables/">Tekton&apos;s variable substitution</a> which looks like <code>$(params.URL)</code>. We could change the <code>Pipelines</code> parameters to something completely different and as long as we tell the <code>Task</code> what its parameter is getting its value from, it doesn&apos;t matter what we call it. Although obviously it would be smart to keep the names somewhat relevant for the sanity of you and your team later down the road.</p><p>So let&apos;s look at the &quot;final core&quot; component of Tekton, the <code>PipelineRun</code>.</p><h3 id="tekton-component-pipelinerun">Tekton Component: PipelineRun</h3><p>Much in the same as the relation of a <code>Task</code> and a <code>TaskRun</code>, so to is a <code>Pipeline</code> and a <code>PipelineRun</code>. There is one difference however in that a <code>Pipeline</code> doesn&apos;t get &quot;run&quot; in the same sense. A <code>Pipeline</code> is just a collection of <code>Tasks</code> so when a <code>PipelineRun</code> runs a <code>Pipeline</code> it is going to create the <code>TaskRuns</code> for those <code>Tasks</code> with only the <code>Pipeline</code> being a reference for the creation of these objects.</p><pre><code>---
apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
  name: basic-pipeline-run
spec:
  params:
  - name: URL
    value: https://github.com
  pipelineRef:
    name: basic-pipeline</code></pre><p>The above is a <code>PipelineRun</code> for the <code>Pipeline</code> we are using for our example, <code>basic-pipeline</code>. What this manifest is going to do is generate <code>TaskRuns</code> from the <code>tasks</code> in the <code>Pipeline</code> after generating all the variables being set by the <code>Pipeline</code> and then passing them to the <code>TaskRuns</code>. The order of object creation from running a <code>Pipeline</code> looks like this:</p><p><code>Task &lt;- Pipeline &lt;- PipelineRun -&gt; TaskRun</code></p><p>Remember that we said each <code>TaskRun</code> generates a <code>Pod</code> to do its &quot;tasks&quot; and each <code>step</code> is a Container in that <code>Pod</code>. This last bit is important when planning your <code>Pipelines</code> if you need to carry over &quot;data&quot; from one <code>Task</code> to another. This will be handled in a later section of this article for <code>workspaces</code>.</p><h2 id="installing-tekton">Installing Tekton</h2><p>As with most &quot;add-ons&quot; to Kubernetes, Tekton can be installed very easily through a single command (assuming you have some level of cluster-admin privileges):</p><pre><code>kubectl apply -f https://storage.googleapis.com/tekton-releases/pipeline/previous/v0.35.1/release.yaml</code></pre><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/05/tekton-install.gif" class="kg-image" alt loading="lazy" width="2000" height="956" srcset="https://blog.kywa.io/content/images/size/w600/2022/05/tekton-install.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/05/tekton-install.gif 1000w, https://blog.kywa.io/content/images/size/w1600/2022/05/tekton-install.gif 1600w, https://blog.kywa.io/content/images/size/w2400/2022/05/tekton-install.gif 2400w" sizes="(min-width: 720px) 720px"></figure><p>With that you will install all the components for Tekton to run in your cluster. After a few moments (or minutes depending on network speed), Tekton will be installed in your cluster. At the time of this writing, <code>v0.35.1</code> is the latest release of Tekton so that is what we will be using. To look into other installation methods, the <a href="https://tekton.dev/docs/pipelines/install/">official Installation Documentation</a> is your best place to look.</p><p>By default the above installs into the <code>tekton-pipelines</code> namespace, but you could modify the YAML manifest prior to applying it, but the default namespace should be fine for most teams so that is where we will work out of. In a vanilla install of Tekton, you really only get the controller and the validating webhook (which converts the Tekton manifests as stated previously). There are no <code>Tasks</code>, <code>ClusterTasks</code> or <code>Pipelines</code> for you to utilize out of the box. There are a few ways (outside of creating your own) to get some base <code>Tasks</code> in your cluster. One of those is by using the Tekton CLI, <code>tkn</code>. The CLI is not required for use with Tekton, but does make some things easier such as installing <code>Tasks</code> from <a href="https://hub.tekton.dev/">Tekton Hub</a>.</p><h3 id="tekton-cli-install">Tekton CLI Install</h3><p>Installing the CLI is OS dependent and there are even other ways outside of the usual &quot;install the binary&quot; for <code>tkn</code> (the CLI for Tekton). You can technically make it a plugin for <code>kubectl</code> if you choose, but that is outside the scope of this article. For OS specific information, check the official <a href="https://tekton.dev/docs/cli/">Tekton CLI documentation</a>. I&apos;ve already got <code>tkn</code> installed (via <code>brew</code>) so let&apos;s see the above in action.</p><h4 id="installing-tasks-from-tekton-hub">Installing <code>Tasks</code> from Tekton Hub</h4><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.kywa.io/content/images/2022/05/tkn-install-task.gif" class="kg-image" alt loading="lazy" width="2000" height="972" srcset="https://blog.kywa.io/content/images/size/w600/2022/05/tkn-install-task.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/05/tkn-install-task.gif 1000w, https://blog.kywa.io/content/images/size/w1600/2022/05/tkn-install-task.gif 1600w, https://blog.kywa.io/content/images/size/w2400/2022/05/tkn-install-task.gif 2400w" sizes="(min-width: 720px) 720px"><figcaption><code>tkn</code> install task from Tekton Hub</figcaption></figure><p>The commands run above were:</p><pre><code>tkn hub install task git-clone -n tekton-is-groovy
kubectl get task -n tekton-is-groovy</code></pre><p>And now we have the <a href="https://hub.tekton.dev/tekton/task/git-clone">git-clone</a> <code>Task</code> in our namespace so we can make use of it later (which we will). The <code>-n</code> is not necessary, but does make it easier to install <code>Tasks</code> to a specific namespace.</p><h4 id="installing-tekton-tasks-without-tkn">Installing Tekton <code>Tasks</code> without <code>tkn</code></h4><p>The above can also be done without the tkn CLI utility by running:</p><pre><code>kubectl apply -f https://raw.githubusercontent.com/tektoncd/catalog/main/task/git-clone/0.6/git-clone.yaml</code></pre><hr><p>As stated before, <code>tkn</code> isn&apos;t required to do anything, but when getting started or just testing things quickly, it can make life a little easier. We won&apos;t be diving into <code>tkn</code> too much, but for some of our initial <code>Task</code> and <code>Pipeline</code> testing we will use it here and there. We will also be looking at YAML manifest equivalent&apos;s of the <code>tkn</code> commands just for comparison sake. Obviously if you are going to have <code>CronJobs</code> or some sort of &quot;runner&quot; handle your Tekton runs, then the CLI may make sense to make available to a runner. The choice is up to you and your team in how you want to make use of Tekton.</p><h2 id="real-world-tekton-usage">Real World Tekton Usage</h2><p>What we&apos;ve gone over so far has been pretty basic, so basic in fact, the name of our objects all start with basic (a terrible joke I know). We haven&apos;t even scratched the surface of what makes Tekton a great CI/CD tool. Before we dive into some of the cooler features, lets look at some <code>Tasks</code> and a <code>Pipeline</code> that do a little bit more than curl a URL. Let&apos;s setup a common pipeline to build and rollout an application.</p><h3 id="build-and-rollout">Build and Rollout</h3><p>For those who are unaware, you can build container images on Kubernetes. If you are familiar with Red Hat <a href="https://www.redhat.com/en/technologies/cloud-computing/openshift">OpenShift</a> (or its <a href="https://www.okd.io/">upstream project OKD</a>), then you&apos;ve most likely already been using the <code>BuildConfig</code> object&apos;s and this will be nothing new. What about those who are using a vanilla Kubernetes cluster and don&apos;t have the <code>BuildConfig</code> object in their cluster? Who would even need to build an image on Kubernetes when you can just run Docker locally? Well obviously, your organization isn&apos;t going to use your laptop for its pipeline&apos;s (hopefully that isn&apos;t the case), but also more importantly, there are organizations where you aren&apos;t allowed &quot;normal&quot; developer tooling on your machine for &quot;security&quot; reasons. Whatever the case, let&apos;s look at building a container image with Tekton.</p><p><strong>NOTE</strong>: Tekton itself does not possess any magical ability to build Container images. We are just using Tekton to do things you could normally do in Kubernetes, but inside of a Tekton <code>Task</code> and <code>Pipeline</code>. Many have used &quot;Docker in Docker&quot; in their CI/CD pipelines over the years to accomplish this, but still is foreign to many people I&apos;ve met and talked with over the years.</p><h4 id="building-a-container-image-in-a-container">Building a Container Image in a Container</h4><p>The above may sound silly and redundant, but its extremely powerful and important for modern CI/CD pipelines. Since everyone has an executive who say &quot;We need Kubernetes now!&quot;, obviously you are dealing with containers whether you want to or not (I&apos;m kidding of course). And because of that, you should try to use tooling that is native to whatever platform you are running on (which I hope is Kubernetes if you&apos;re reading this article). &quot;Docker in Docker&quot;, or <code>dind</code> for short, is something that has been around for years at this point, but as with most things, there are different options and arguably better tools to use for some tasks. Enter <a href="https://buildah.io/">Buildah</a>. </p><p>Buildah is a project managed by the <a href="https://github.com/containers">Containers organization</a> and unlike <code>docker</code> (the application) it seeks to focus on one core task as opposed to &quot;everything&quot; involved with containers. Buildah has many features that expound upon building container images that are OCI compliant and will run in a &quot;Docker&quot; environment. We are not going to go into a deep dive of Buildah here, but we are going to use Buildah to build our container image. To build a container image with Buildah, the flags are very similar to how you would build an image with <code>docker</code> or <code>podman</code>:</p><pre><code># Docker
docker build -f Dockerfile -t quay.io/your-repo/some-image:tag

# Buildah
buildah build -f Dockerfile -t quay.io/your-repo/some-image:tag</code></pre><p>Historically <code>docker</code> only looked for a file called <code>Dockerfile</code> when running a build, but this is no longer the case these days (thankfully). Tools like <code>buildah</code> and <code>podman</code> allow you to make use of any file as long as it is in the traditional Dockerfile format and since their inception. A <code>Task</code> to use <code>buildah</code> looks pretty straight forward and we will use the <code>script</code> field as opposed to <code>command</code> as it gives us slightly better control over the <code>step</code> in our <code>Task</code>:</p><pre><code>  steps:
    - name: build
      image: quay.io/containers/buildah
      workingDir: $(workspaces.builder.path)
      securityContext:
        privileged: true
      script: |
        #!/usr/bin/env bash
        buildah --storage-driver=overlay build --no-cache -f $(params.DOCKERFILE_PATH) -t $(params.REGISTRY)/$(params.REPOSITORY)/$(params.IMAGE):$(params.IMAGE_TAG) .
      volumeMounts:
        - name: varlibcontainers
          mountPath: /var/lib/containers</code></pre><p>Its pretty straight forward, but the core things here is that our image we are building, we are going to name and tag with <code>params</code> for the <code>Task</code> (not shown, but you can see their names so that is all that really matters). There is a follow up task, which runs <code>buildah push</code>, which will push the image to the container registry that is specified through the <code>params</code>. One thing you may notice is the field that is granting the <code>privileged</code> securityContext. This is required to use parts of the filesystem that containers need to &quot;run&quot;. There are ways to do this in a &quot;rootless&quot; or privilege-less environment, but for the scope of this we are going to focus on using this method.</p><p>We have a method of building a container image, but where is our <code>Dockerfile</code> going to come from? Typically this is going to be in a Git repository somewhere (or some other source control mechanism) and so long as its publicly reachable that is is all we need to do. So we need to pull down our source code along with the Dockerfile so the application can be built. If we follow best practices, we would have one <code>Task</code> acquire our source code, another handle the build and finally a task to rollout our application for testing (which would be its own task, but outside the scope of what we are looking at here). Since each <code>Task</code> is a new Pod, how is the <code>Task</code> that gets our source code going to make it available to our buildah <code>Task</code>? The typical obvious answer is a <code>volume</code> created through a PVC, but Tekton has something &quot;better&quot; that can be used. It&apos;s time to look at <code>workspaces</code>.</p><h3 id="workspaces">Workspaces</h3><p>Tekton allows you to make use of <code>workspaces</code>, which as stated above can function just like a <code>volume</code> would in your Kubernetes <code>Pods</code>. When using a <code>workspace</code> it functions quite similarly to a <code>volumeMount</code> in how its used inside of a <code>Task</code> step, but you can have it be a dynamic path without needing to &quot;hard code&quot; the path inside of the container. Let&apos;s take a look at how you would use a <code>workspace</code> and then we will look at declaring one:</p><pre><code>apiVersion:
kind: Task
metadata:
  name: some-task
spec:
  steps:
    - name: persist-code
      image: quay.io/example/someimage:latest
      script: |
        cat $(workspaces.mydir.path)/somefile.txt
  workspaces:
    - name: mydir</code></pre><p>The <code>Task</code> above will run a very basic step that runs <code>cat</code> on <code>somefile.txt</code> which is in the <code>workspace</code> named <code>mydir</code>. The path in which that lives inside of the actual running container will be (by default) <code>/workspaces/mydir</code>. Obviously at the end of the day everything boils down to a mount path in the container, the one big bonus of using <code>workspaces</code> over a normal <code>volumeMount</code> is that you do not have to worry about where things are mounted. You can just use <code>$(workspaces.NAME.path)</code> and it will use the actual path in its place during the <code>steps</code> of your <code>Task</code>. </p><p>Something to remember with <code>workspaces</code> is that just like <code>volumes</code> this doesn&apos;t have to be a <code>PersistentVolumeClaim</code>, it could be anything you would normally mount to a container such as a <code>Secret</code>, <code>ConfigMap</code> or even an <code>emptyDir</code>. For each <code>Task</code> there may be different reasons to use one over the other, but the main benefit is that you don&apos;t have to specify a path for your <code>steps</code> and can just rely on the variables as shown above.</p><p>For actually declaring <code>workspaces</code> and how they work is quite simple and does give you some flexibility (again its almost exactly like a <code>volume</code> in the configuration):</p><figure class="kg-card kg-code-card"><pre><code>workspaces:
  - name: some-name
    description: What goes here or lives here
    mountPath: /some/alternative/mount
    optional: true
    readOnly: false</code></pre><figcaption>Workspace declaration in a Tekton Task</figcaption></figure><p>The only field above that is mandatory is the <code>name</code> field, all else is optional. Now what this <code>workspace</code> is depends on how its mapped from a <code>TaskRun</code> or a <code>PipelineRun</code> (which creates a <code>TaskRun</code> for each <code>Task</code> in a <code>Pipeline</code>). You have quite a few options (again just like a <code>volume</code>):</p><figure class="kg-card kg-code-card"><pre><code>workspaces:
- name: pvc-workspace
  persistentVolumeClaim:
    claimName: my-pvc

- name: volumeclaim-workspace
  volumeClaimTemplate:
    spec:
      accessModes:
        - ReadWriteMany
      resources:
        requests:
          storage: 1Gi

- name: emptydir-workspace
  emptyDir: {}

- name: secret-workspace
  secret:
    secretName: my-secret

- name: configmap-workspace
  configMap:
    name: some-configmap</code></pre><figcaption>Defining Workspaces in a Tekton TaskRun</figcaption></figure><p>Above is pretty much the options you could/would use with a Tekton <code>workspace</code> and for those who have used Kubernetes for some time, looks identical to what you would/could do with a <code>volume</code>. I keep bringing this comparison of <code>volumes</code> and <code>workspaces</code> to show they roughly the same with the main benefit of being more declarative with <code>workspaces</code> for your &quot;pipelines&quot; as a whole. </p><p>After all of that explanation, here is a quick tl;dr on <code>volumes</code> vs <code>workspaces</code>:</p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/05/same-same-but-different.gif" class="kg-image" alt loading="lazy" width="480" height="204"></figure><p>With that covered, let&apos;s see using <code>workspaces</code> in action.</p><p>I&apos;ve created a simple <code>Pipeline</code>, which we will extend later to our complete build and rollout <code>Pipeline</code>, but for now we can use it to test the <code>workspace</code> feature for getting our source code and then building an image with it.</p><figure class="kg-card kg-code-card"><pre><code>---
apiVersion: tekton.dev/v1beta1
kind: Pipeline
metadata:
  name: build
spec:
  workspaces:
    - name: ssh-creds
    - name: build
    - name: registry-credentials
  tasks:
    - name: fetch-repository
      taskRef:
        name: git-clone
      workspaces:
        - name: ssh-directory
          workspace: ssh-creds
        - name: output
          workspace: build
    - name: build
      taskRef:
        name: buildah
      runAfter:
        - fetch-repository
      workspaces:
        - name: builder
          workspace: build
        - name: registry-secret
          workspace: registry-credentials</code></pre><figcaption>Buildah Pipeline</figcaption></figure><p>For ease of readability I took out the <code>params</code> to focus on the <code>tasks</code> and <code>workspaces</code> and how they all interact. The <code>git-clone</code> task (which we pulled in earlier through the <code>tkn</code> CLI) really only needs one <code>workspace</code> and that is <code>output</code> which is where the Git repository will be cloned into. If the repository is private however, you will need to provide credentials for the <code>git-clone</code> task to be able to pull from that repository. We will be using SSH based authentication, but this will be defined in the <code>PipelineRun</code> and only the <code>ssh-directory</code> workspace is needed for the <code>task</code> declaration in the <code>Pipeline</code>. </p><p>The <code>buildah</code> task we&apos;ve created, which is based on <a href="https://hub.tekton.dev/tekton/task/buildah">this one from Tekton Hub</a>, technically only needs a workspace that contains the code to build a container, but does require some form of credential for pushing the built image to a container registry. Let&apos;s see our <code>PipelineRun</code> to get some more clarity on how the <code>workspace</code> declaration works:</p><figure class="kg-card kg-code-card"><pre><code>---
apiVersion: tekton.dev/v1beta1
kind: PipelineRun
metadata:
  generateName: build-pipeline-run-
spec:
  params:
    - name: SOURCE_URL
      value: git@github.com:KyWa/kywa-website.git
    - name: IMAGE
      value: &quot;kywa-website&quot;
    - name: IMAGE_REGISTRY
      value: docker.io
    - name: IMAGE_REPOSITORY
      value: mineops
    - name: DOCKERFILE
      value: &quot;Dockerfile&quot;
  pipelineRef:
    name: build
  workspaces:
    - name: ssh-creds
      secret:
        secretName: ssh-auth
        items:
          - key: ssh-privatekey
            path: id_rsa
    - name: build
      volumeClaimTemplate:
        spec:
          accessModes:
            - ReadWriteMany
          resources:
            requests:
              storage: 2Gi
    - name: registry-credentials
      secret:
        secretName: docker-hub
        items:
          - key: .dockerconfigjson
            path: auth</code></pre><figcaption>Buildah PipelineRun</figcaption></figure><p>This <code>Pipeline</code> is being run with 3 total <code>workspaces</code>:</p><ul><li><code>ssh-creds</code> - This is a Kubernetes Secret which contains SSH credentials</li><li><code>build</code> - A <code>volumeClaimTemplate</code> which will request a PVC be created and used for source code storage</li><li><code>registry-credentials</code> - Another Kubernetes Secret containing a <code>.dockercfg</code> for the image registry credentials</li></ul><p>Let&apos;s give it a run and see what it looks like:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.kywa.io/content/images/2022/06/buildah-pipeline.gif" class="kg-image" alt loading="lazy" width="2000" height="983" srcset="https://blog.kywa.io/content/images/size/w600/2022/06/buildah-pipeline.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/06/buildah-pipeline.gif 1000w, https://blog.kywa.io/content/images/size/w1600/2022/06/buildah-pipeline.gif 1600w, https://blog.kywa.io/content/images/size/w2400/2022/06/buildah-pipeline.gif 2400w" sizes="(min-width: 720px) 720px"><figcaption>Buildah Pipeline Running</figcaption></figure><p>Now that we see how we can build a container in a container, the last &quot;task&quot; would be to see about using this image we built to update an existing Kubernetes <code>Deployment</code> with the image digest for it to rollout. This <code>Pipeline</code> can be quite powerful as being able to handle automatic rollouts for a Deployment is a great start to getting towards a fully automated CI/CD pipeline.</p><h3 id="planning-our-build-and-rollout">Planning our Build and Rollout</h3><p>This <code>Pipeline</code> is only adding a single <code>task</code> to what we just did previously with our <code>buildah</code> <code>Pipeline</code>. The manifests used for the &quot;build and rollout&quot; can be found in the <a href="https://github.com/KyWa/blogs/tree/master/tekton-is-groovy">supporting repository</a> for this post.</p><h3 id="running-our-build-and-rollout">Running our Build and Rollout</h3><p>With our <code>Pipeline</code> defined, we can in theory actually go to run it with a <code>PipelineRun</code> object:</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.kywa.io/content/images/2022/06/cicd-pipeline.gif" class="kg-image" alt loading="lazy" width="2000" height="983" srcset="https://blog.kywa.io/content/images/size/w600/2022/06/cicd-pipeline.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/06/cicd-pipeline.gif 1000w, https://blog.kywa.io/content/images/size/w1600/2022/06/cicd-pipeline.gif 1600w, https://blog.kywa.io/content/images/size/w2400/2022/06/cicd-pipeline.gif 2400w" sizes="(min-width: 720px) 720px"><figcaption>CI-CD Pipeline Running</figcaption></figure><p>This is a very basic CI/CD pipeline, but could be easily (and should be) extended with another task after the deploy to run some tests and once that is done &quot;prod&quot; could be rolled out with that image on a successful test. Tekton really can do whatever you want it to without much effort. It all comes down to &quot;how do you want to do it?&quot;.</p><h2 id="dashboard-beta">Dashboard (BETA)</h2><p>If you&apos;re tired of the terminal and wan&apos;t to see a GUI element, I have just one thing to say to you, let&apos;s see what the Tekton Dashboard looks like!. Didn&apos;t think I was going there did you? Although the terminal is the greatest place on Earth (sorry Disney, also not sorry), there are times when you just want to get some visibility outside of <code>kubectl</code>.</p><p>The Tekton Dashboard has some pretty nice features and is great for visibility. The project can be found in its <a href="https://github.com/tektoncd/dashboard">GitHub repository</a> and is installed much in the same way Tekton itself is, the trusty <code>kubectl</code> one-liner:</p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/06/dashboard-install.gif" class="kg-image" alt loading="lazy" width="2000" height="983" srcset="https://blog.kywa.io/content/images/size/w600/2022/06/dashboard-install.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/06/dashboard-install.gif 1000w, https://blog.kywa.io/content/images/size/w1600/2022/06/dashboard-install.gif 1600w, https://blog.kywa.io/content/images/size/w2400/2022/06/dashboard-install.gif 2400w" sizes="(min-width: 720px) 720px"></figure><p>Much like with other &quot;web&quot; services running in Kubernetes, you will need some form of Ingress to access it. That is outside the scope of this tutorial, but was covered in a <a href="https://blog.kywa.io/mineops-part-5/">previous post during the MineOps series</a>. Already having an Ingress setup for this cluster, we can now view the Dashboard. Again at the time of this writing, the Tekton Dashboard is still in &quot;BETA&quot;, but it is quite feature rich and can do pretty much all the things you&apos;d expect it to do (view and run <code>Pipelines</code>!):</p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/06/dashboard-walkthrough.gif" class="kg-image" alt loading="lazy" width="960" height="689" srcset="https://blog.kywa.io/content/images/size/w600/2022/06/dashboard-walkthrough.gif 600w, https://blog.kywa.io/content/images/2022/06/dashboard-walkthrough.gif 960w" sizes="(min-width: 720px) 720px"></figure><h2 id="conclusion">Conclusion</h2><p>There are many other features of Tekton, but this guide was meant to be a light primer and get you aware of this powerful tool with its capabilities. One of the other <a href="https://tekton.dev/docs/triggers/">powerful features of Tekton is Triggers</a> (as one would expect from a CI/CD tool), but we will not be covering that in this post. I do recommend reviewing Tekton Triggers as they can really help flesh out an amazing CI/CD pipeline for your organization by enabling the running of Pipelines with events such as a Git commit being pushed into a branch among other things.</p><p>Just remember one thing, Tekton is groovy.</p>]]></content:encoded></item><item><title><![CDATA[mineOps - Part 6: ArgoCD to the Rescue]]></title><description><![CDATA[<figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/mineops-5.png" class="kg-image" alt loading="lazy" width="383" height="78"></figure><!--kg-card-begin: markdown--><h1 id="argocd-for-mineops">ArgoCD for mineOps</h1>
<p>Since it seems we are now using Kubernetes and our CEO is happy and doesn&apos;t appear to be playing buzzword bingo anymore, its time to do something for us to make our lives easier. After spending some time scouring the internet, it looks like we</p>]]></description><link>https://blog.kywa.io/mineops-part-6/</link><guid isPermaLink="false">61d450a9c040b4060b5bb09c</guid><category><![CDATA[mineops]]></category><dc:creator><![CDATA[Kyle Walker]]></dc:creator><pubDate>Sat, 19 Mar 2022 03:06:03 GMT</pubDate><content:encoded><![CDATA[<figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/mineops-5.png" class="kg-image" alt loading="lazy" width="383" height="78"></figure><!--kg-card-begin: markdown--><h1 id="argocd-for-mineops">ArgoCD for mineOps</h1>
<p>Since it seems we are now using Kubernetes and our CEO is happy and doesn&apos;t appear to be playing buzzword bingo anymore, its time to do something for us to make our lives easier. After spending some time scouring the internet, it looks like we may have found what we need, <a href="https://argoproj.github.io/cd/">ArgoCD</a>. It seems like we&apos;ve heard it come up in our Twitter feeds, Reddit and wherever else dope technology is talked about. It seems like it handles GitOps, whatever that is, but most importantly it keeps manifests applied and correct in our Kubernetes cluster(s). But what does that really mean? Let&apos;s take a tour of what ArgoCD is and how it is going to help us manage all of these customer Minecraft Servers running on Kubernetes.</p>
<h2 id="what-are-we-trying-to-solve">What are we trying to solve?</h2>
<p>Since we now have our customer Minecraft Servers running on Kubernetes and we can create new ones with <code>kubectl create</code>, do we wan&apos;t to keep up with a <code>kubecofig</code> for each person on our development team, what do we do? We can keep doing this, but it gets tiring having to get more <code>kubeconfig</code>s for each new team member and really doesn&apos;t make auditing super easy (really borderline impossible in a vanilla Kubernetes cluster). There may be some other items, but here are our core focuses on what we are needing for our sanity:</p>
<ul>
<li>An easy way to deploy new customer Minecraft Servers</li>
<li>Not require giving new team members a <code>kubeconfig</code> file</li>
<li>Some form of auditing of who created/deployed what</li>
</ul>
<h3 id="how-does-argocd-help-us">How does ArgoCD help us?</h3>
<p>Thankfully based on the items we have determined above, ArgoCD can handle most of these items (and more) without really having to change much of what we do. That is fine and dandy saying that, but what exactly does ArgoCD &quot;do&quot;? Argo itself is actually just the name of a group of projects (and a company I guess?) with ArgoCD being one of those projects. The CD stands for Continuous Delivery, which is something that up until now &quot;we in this company&quot; haven&apos;t really ever dealt with. In short it takes changes you&apos;ve made (ideally in a Git repository somewhere) and then... continuously delivers/deploys them. On the surface it is actually quite simple, but the how and when is what is important. So that seems to lead mostly to an answer for the first item in our list, but does leave us still with the &quot;how&quot; of it. In <a href="https://blog.kywa.io/mineops-part-3/">Part 3</a> we moved all of our configuration files into Git and they are just sitting there doing nothing. What if they could &quot;do&quot; something? This is where ArgoCD comes in and the &quot;how&quot; of the CD portion of its namesake. ArgoCD can be utilized to implement a neat framework that some may consider a &quot;buzzword&quot; (we can use those too if we want Mr. CEO) called GitOps, but it is so much more than just a buzzword. It is honestly an amazing way of handling the automation of your CI/CD changes and really shines in a matured DevOps culture, which hopefully by now we can consider ourselves that.</p>
<p>For the issue of getting new team members access to Kubernetes to be able to run <code>kubectl</code> commands against, we don&apos;t even need to bother with that. ArgoCD itself (typically) lives in the Kubernetes cluster as its own application and is checking a Git repository (or even a Helm Repository for those who&apos;ve gone a little further in their Kubernetes maturity) for any changes. Since every team member in theory who is going to be making changes, would use Git, nothing needs to be changed in terms of an access model to our Kubernetes clusters. ArgoCD only cares about what is in the cluster and what is in Git. You don&apos;t even really need to grant anyone access to ArgoCD itself unless there is some form of an issue.</p>
<p>With the previous question answered, we technically also have our answer as to what we need for some form of audit. Git handles auditing very, very well, so well in fact as stated before, you can literally use the word <code>blame</code> to find out who changed what in a file. Between this existing functionality of Git and using Pull Requests that require approvals/reviews, we can easily audit what changes actually go into our cluster. On top of those things, ArgoCD itself has a pretty amazing view of who did what right from the UI, which is really just reporting what Git says.</p>
<p>We&apos;ve roughly got our what we believe we need to make this all work right out of the box with ArgoCD and Git. And we&apos;ve got our &quot;buzzword&quot; GitOps, which is something we are really going to focus on. In a perfect world, no one would have direct access to our Kubernetes clusters and we would have everything audited through Git, applied via ArgoCD and happily running with 0 manual intervention. We can absolutely get here and there is nothing stopping us from doing that outside of a little growing pain.</p>
<h3 id="the-endgame-of-gitops">The &quot;Endgame&quot; of GitOps</h3>
<p>Without violating an Hollywood IP, the &quot;End Game&quot; of GitOps is quite simple, Git is the single source of truth. In theory its simple, in practice it can be challenging. Thankfully with ArgoCD it becomes quite doable. How this would look fully implmeneted is this:</p>
<ul>
<li>k8s Cluster is created</li>
<li>ArgoCD is installed and configured</li>
<li><code>kubeadmin</code> config is stored in some secrets management tool that no one can access directly</li>
<li>Any and all changes to the cluster are made through ArgoCD</li>
<li>No one uses <code>kubectl</code> to get or do anything</li>
</ul>
<p>For most of us who are admins, or who used to be admins, the idea of not having any visibility into our clusters or workloads sounds horribly scary. There are times where it can be for sure, but so long as there is a good Git review process and competent people are writing code changes, it doesn&apos;t have to be. With ArgoCD (as we will see) you have quite a bit of visibility into what is going on with your cluster(s) and if you have done some Day 2 activities such as having monitoring for your cluster, you don&apos;t really &quot;need&quot; <code>kubectl</code>. I will admit though not having direct access does make for a nerve racking scenario sometimes, but if done correctly, implementing GitOps will keep manual &quot;tweaks&quot; from being performed without anyone&apos;s knowing.</p>
<p>For more information on GitOps, check out <a href="https://about.gitlab.com/topics/gitops/">this</a> post from GitLab.</p>
<h2 id="installing-argocd">Installing ArgoCD</h2>
<p>As discussed during the outline of ArgoCD I mentioned that ArgoCD typically lives in the Kubernetes cluster where it is deployed. There are other methods where you could technically have a &quot;central&quot; ArgoCD that manages multiple Kubernetes clusters. We will not be focusing on that method as it requires quite a bit more setup and there is nothing lacking if we have 1 ArgoCD instance per Kubernetes cluster (especially since we aren&apos;t growing past this during the mineOps series).</p>
<h3 id="raw-manifests">Raw Manifests</h3>
<p>Ideally the best way to install ArgoCD is up to the team who is going to be doing the actual install. The method we are going to pursue is through just normal YAML manifests. I would personally use Helm to deploy this, but learning Helm isn&apos;t a necessary for the mineOps series, but a follow up post may be in the works. The fastest way to get the ArgoCD manifests is to obtain them from the <a href="https://github.com/argoproj/argo-cd">Argo Project&apos;s GitHub repository for ArgoCD</a>. In this repository there are many great things to look at, but for the most part we are primarly focused on one directory and that is the <code>manifests</code> directory which contains the most obvious of YAML files, <code>install.yaml</code>. We breifly mentioned this in Part 5, but <code>kubectl</code> can be used to target not just local files, but also URLs as well. It looks just like how we would normally apply a manifest, so lets get it going:</p>
<pre><code class="language-yaml">$ kubectl create namespace argocd
namespace/argocd created
$ kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/master/manifests/install.yaml
customresourcedefinition.apiextensions.k8s.io/applications.argoproj.io created
customresourcedefinition.apiextensions.k8s.io/applicationsets.argoproj.io created
customresourcedefinition.apiextensions.k8s.io/appprojects.argoproj.io created
serviceaccount/argocd-application-controller created
serviceaccount/argocd-applicationset-controller created
serviceaccount/argocd-dex-server created
serviceaccount/argocd-notifications-controller created
serviceaccount/argocd-redis created
serviceaccount/argocd-server created
role.rbac.authorization.k8s.io/argocd-application-controller created
role.rbac.authorization.k8s.io/argocd-applicationset-controller created
role.rbac.authorization.k8s.io/argocd-dex-server created
role.rbac.authorization.k8s.io/argocd-notifications-controller created
role.rbac.authorization.k8s.io/argocd-server created
clusterrole.rbac.authorization.k8s.io/argocd-application-controller created
clusterrole.rbac.authorization.k8s.io/argocd-server created
rolebinding.rbac.authorization.k8s.io/argocd-application-controller created
rolebinding.rbac.authorization.k8s.io/argocd-applicationset-controller created
rolebinding.rbac.authorization.k8s.io/argocd-dex-server created
rolebinding.rbac.authorization.k8s.io/argocd-notifications-controller created
rolebinding.rbac.authorization.k8s.io/argocd-redis created
rolebinding.rbac.authorization.k8s.io/argocd-server created
clusterrolebinding.rbac.authorization.k8s.io/argocd-application-controller created
clusterrolebinding.rbac.authorization.k8s.io/argocd-server created
configmap/argocd-cm created
configmap/argocd-cmd-params-cm created
configmap/argocd-gpg-keys-cm created
configmap/argocd-notifications-cm created
configmap/argocd-rbac-cm created
configmap/argocd-ssh-known-hosts-cm created
configmap/argocd-tls-certs-cm created
secret/argocd-notifications-secret created
secret/argocd-secret created
service/argocd-applicationset-controller created
service/argocd-dex-server created
service/argocd-metrics created
service/argocd-notifications-controller-metrics created
service/argocd-redis created
service/argocd-repo-server created
service/argocd-server created
service/argocd-server-metrics created
deployment.apps/argocd-applicationset-controller created
deployment.apps/argocd-dex-server created
deployment.apps/argocd-notifications-controller created
deployment.apps/argocd-redis created
deployment.apps/argocd-repo-server created
deployment.apps/argocd-server created
statefulset.apps/argocd-application-controller created
networkpolicy.networking.k8s.io/argocd-application-controller-network-policy created
networkpolicy.networking.k8s.io/argocd-dex-server-network-policy created
networkpolicy.networking.k8s.io/argocd-redis-network-policy created
networkpolicy.networking.k8s.io/argocd-repo-server-network-policy created
networkpolicy.networking.k8s.io/argocd-server-network-policy created
</code></pre>
<p>Based on the output above, we have just created quite a few new objects in our cluster. For the most part this is just a bunch of RBAC rules to allow the various compontents of ArgoCD to do all of the things it does. The rest is really just the actual deployments of the ArgoCD components of which there are a few. We will break down each component just a little bit, but knowing each component isn&apos;t 100% necessary for using and working with ArgoCD so we will only go over the main ones that you&apos;d realistically ever need to look at:</p>
<ul>
<li>ArgoCD Server - This handles the UI elements and is where the API server lives that hanldes the &quot;running&quot; of ArgoCD</li>
<li>ArgoCD Application Controller - Continuously monitors running <code>Applications</code> and compares their state with what is cached in Git</li>
<li>ArgoCD Repo Server - Manages the repositories and provides a local cache of each Git repository holding application manifests</li>
</ul>
<h3 id="accessing-argocd">Accessing ArgoCD</h3>
<p>Now that we&apos;ve covered the various components that we&apos;ve installed, it is time to actually &quot;see&quot; and use ArgoCD, but with all things Kubernetes that have some form of GUI component, we will need some form of Ingress to access it. To access the web front end of ArgoCD, we will need an <code>Ingress</code> object to route our traffic to ArgoCD. Here is a simple manifest for such an Ingress object. Please note you do not actually need to use the web front end to make use of ArgoCD as it will work on its own, but if you wish to use the <code>argocd</code> client or view from the web front end to manage things, you will need an <code>Ingress</code> object (or a Gateway/VirtualService etc...). You could also technically use the <code>kubectl port-forward</code> function, but that isn&apos;t very realistic or scalable since we are using this in a cluster used by many (in theory).</p>
<pre><code class="language-yaml">apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: argocd
  namespace: argocd
  annotations:
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/force-ssl-redirect: &quot;true&quot;
    nginx.ingress.kubernetes.io/ssl-passthrough: &quot;true&quot;
    nginx.ingress.kubernetes.io/backend-protocol: &quot;HTTPS&quot;
spec:
  rules:
  - host: &quot;argo.kywa.io&quot;
    http:
      paths:
        - path: /
          pathType: Prefix
          backend:
            service:
              name: argocd-server
              port:
                name: https
  tls:
  - hosts:
    - argo.kywa.io
</code></pre>
<p><strong>NOTE</strong> Some of these annotations can be removed by changing the configuration of ArgoCD&apos;s settings in the ConfigMap <code>argocd-cm</code>, but are required for a &quot;default&quot; installation of ArgoCD along with <code>ingress-nginx</code>.</p>
<p>ArgoCD is deployed and seems to be up and running, we can get to the web application, but we don&apos;t have any login credentials. By default ArgoCD creates an initial admin password which can be found in a secret in the namespace that ArgoCD is installed in. To view the <code>Secret</code> and get the password to login initially here is a quick handy one liner:</p>
<pre><code class="language-sh">$ kubectl get secret -n argocd argocd-initial-admin-secret -o jsonpath=&apos;{.data.password}&apos; | base64 -d
Z1Ha6-z8QVR2jC0b
</code></pre>
<p>Later in this guide we will be doing things in a more declarative process and will not be needing to obtain this secret manually or even use it potentially for that matter. Now let&apos;s try to login to our newly installed ArgoCD instance.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.kywa.io/content/images/2022/03/argocd-login-ui.gif" class="kg-image" alt loading="lazy" width="1338" height="960" srcset="https://blog.kywa.io/content/images/size/w600/2022/03/argocd-login-ui.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/03/argocd-login-ui.gif 1000w, https://blog.kywa.io/content/images/2022/03/argocd-login-ui.gif 1338w" sizes="(min-width: 720px) 720px"><figcaption>Logging into ArgoCD - UI</figcaption></figure><!--kg-card-begin: markdown--><h2 id="using-argocd">Using ArgoCD</h2>
<p>Now that we&apos;ve logged into the UI, we are met with an amazingly detailed blank screen that tells us &quot;No applications yet&quot;. So let&apos;s breakdown what an <code>Application</code> is real quick. An <code>Application</code> is a CustomResource provided by the ArgoCD controller which manages an &quot;application&quot;. For some an &quot;application&quot; could be a Helm Chart, a Kustomize &quot;application&quot; or a directory of manifests. We will be looking at 2 of these style <code>Applications</code> with a primary focus on the directory style as it is probably the more useful for us deploying Minecraft Servers. Since ArgoCD is being used as a GitOps tool, we need to get ArgoCD to talk to Git somehow and from the Applications page we are on, there is no obvious path to this.</p>
<h3 id="creating-an-application">Creating an Application</h3>
<p>With ArgoCD there are many ways to create an <code>Application</code> and we will outline the 3 most used (and currently the only 3 known) methods for creating an <code>Application</code> in ArgoCD.</p>
<h4 id="argocd-ui">ArgoCD UI</h4>
<p>First up is the ArgoCD UI which we are already in, so this makes the most sense to do first. Let&apos;s see what we get if the click the &quot;CREATE APPLICATION&quot; button in the middle of our screen.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/03/argo-add-app.gif" class="kg-image" alt loading="lazy" width="1338" height="960" srcset="https://blog.kywa.io/content/images/size/w600/2022/03/argo-add-app.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/03/argo-add-app.gif 1000w, https://blog.kywa.io/content/images/2022/03/argo-add-app.gif 1338w" sizes="(min-width: 720px) 720px"></figure><!--kg-card-begin: markdown--><p>After passing in some information such as which Git repository to target and the name of our <code>Application</code> we get more changes than we were probably expecting. As we can see the UI has changed drastically from whwen we first logged into ArgoCD. There are now filters and statuses we can view, with labels and all sorts of things to click on. Since we have added an <code>Application</code> to our cluster and we can view what the <code>Application</code> is doing by clicking on it from the ArgoCD UI. This first <code>Application</code> is really nothing more than &quot;applying&quot; itself based on what we gave ArgoCD during its creation. Upon clicking the <code>Application</code> called &quot;argocd-mineops&quot;, we are taken to a different view where we can see the name of our application pointing to another item named &quot;argocd-mineops&quot;. This &quot;other&quot; object sharing the name is showing that the <code>Application</code> we created is applying manifests in the <code>files/ArgoCD/basic</code> directory.</p>
<h4 id="declarative">Declarative</h4>
<p>As with literally everything in the Kubernetes world, it can all be boiled down to a manifest and ArgoCD is no different. The same application we created from the UI can also be created through a Kubernetes manifest. Here is what it would look like and can be applied via the normal <code>kubectl create -f</code> method as with everything:</p>
<pre><code class="language-yaml">apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: argocd-mineops
  namespace: argocd
spec:
  destination:
    server: https://kubernetes.default.svc
  project: default
  source:
    directory:
      jsonnet: {}
      recurse: true
    path: files/ArgoCD/basic
    repoURL: https://github.com/KyWa/mineOps.git
    targetRevision: master
</code></pre>
<h4 id="argocd-cli">ArgoCD CLI</h4>
<p>And for the &quot;last&quot; option to work with ArgoCD is the <code>argocd</code> CLI. This may be useful to some, but once you have ArgoCD setup and your Git repository the way you and your team want it, you may not have much reason (initially) to use the <code>argocd</code> CLI. It does have some neat features for more advanced usage, but for what we will be doing, it isn&apos;t needed. To get the <code>argocd</code> CLI installed check the <a href="https://argo-cd.readthedocs.io/en/stable/cli_installation/">official ArgoCD docs</a>.</p>
<p>Let&apos;s at least see how to use it though and as with most tools you will need to authenticate so you can actually communicate:</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.kywa.io/content/images/2022/03/argocd-login-cli.gif" class="kg-image" alt loading="lazy" width="2000" height="986" srcset="https://blog.kywa.io/content/images/size/w600/2022/03/argocd-login-cli.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/03/argocd-login-cli.gif 1000w, https://blog.kywa.io/content/images/size/w1600/2022/03/argocd-login-cli.gif 1600w, https://blog.kywa.io/content/images/size/w2400/2022/03/argocd-login-cli.gif 2400w" sizes="(min-width: 720px) 720px"><figcaption>Logging into ArgoCD - CLI</figcaption></figure><!--kg-card-begin: markdown--><p>To create the same <code>argocd-minops</code> <code>Application</code> as above, we would run the following:</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.kywa.io/content/images/2022/03/argocd-cli-app.gif" class="kg-image" alt loading="lazy" width="2000" height="490" srcset="https://blog.kywa.io/content/images/size/w600/2022/03/argocd-cli-app.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/03/argocd-cli-app.gif 1000w, https://blog.kywa.io/content/images/size/w1600/2022/03/argocd-cli-app.gif 1600w, https://blog.kywa.io/content/images/size/w2400/2022/03/argocd-cli-app.gif 2400w" sizes="(min-width: 720px) 720px"><figcaption>Create an ArgoCD Application - CLI</figcaption></figure><!--kg-card-begin: markdown--><p>As you can see, the <code>argocd</code> CLI is pretty straightforward and has very logically named flags for the various commands, but takes way more typing to get an <code>Application</code> created than having a manifest to apply or using the UI. The option is there though for those who wish to use it.</p>
<hr>
<p>With an <code>Application</code> defined and the ability to check its current status we have done the core of what you will ever need to do with ArgoCD. However you may have noticed that our <code>Application</code> showed &quot;Out of Sync&quot; and that it had a Yellow color as opposed to Green. Most of us know that Green is good and Yellow is typically a warning of some kind. This is still the case with ArgoCD and we can dig into why that is in a later section.</p>
<h3 id="using-the-argocd-ui">Using the ArgoCD UI</h3>
<p>Now that we&apos;ve created an <code>Application</code> and have it in our ArgoCD instance, what do all the fancy UI elements mean? Thankfully they are quite self-explanitory, but we can look at them for a little bit. On the top row of the default layout we have a few buttons to look at:</p>
<ul>
<li><code>NEW APP</code> - This takes you to the same screen we were on previously when adding our <code>Application</code></li>
<li><code>SYNC APPS</code> - Initiates a manual sync on all <code>Applications</code> (or a selection)</li>
<li><code>REFRESH APPS</code> - Tells ArgoCD to check in with Git and refresh the state of <code>Desired</code> for the selection <code>Applications</code>. This is normally done on its own every 5 minutes by default</li>
</ul>
<p>When using the UI, the latter 2 are probably the only buttons you will click from the main <code>Application</code> page. Things get a little deeper once you click on an <code>Application</code> and can see more choices.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/03/argocd-ui-opts.png" class="kg-image" alt loading="lazy" width="1341" height="491" srcset="https://blog.kywa.io/content/images/size/w600/2022/03/argocd-ui-opts.png 600w, https://blog.kywa.io/content/images/size/w1000/2022/03/argocd-ui-opts.png 1000w, https://blog.kywa.io/content/images/2022/03/argocd-ui-opts.png 1341w" sizes="(min-width: 720px) 720px"></figure><!--kg-card-begin: markdown--><ul>
<li><code>APP DETAILS</code> - Takes you to the <code>Application</code> itself to view its status and information</li>
<li><code>APP DIFF</code> - Also takes you to the above page, but specifically to the <code>DIFF</code> tab</li>
<li><code>SYNC</code> - Same as the <code>SYNC APPS</code> button from the main <code>Application</code> page, but only for this <code>Application</code>. Can also choose which resources to sync</li>
<li><code>SYNC STATUS</code> - Looks at the sync status of all objects in the <code>Application</code></li>
<li><code>HISTORY AND ROLLBACK</code> - Shows the history of the <code>Application</code> and each Git Commit that was targeted. Rollback is only available for non Automated sync policies</li>
<li><code>DELETE</code> - Should be self-explanitory, but deletes the <code>Application</code> (and its resources if <code>prune</code> is enabled)</li>
<li><code>REFRESH</code> - Also the same as the <code>REFRESH APPS</code> button from the main <code>Application</code> page</li>
</ul>
<p>The UI does have some handy features, but as stated previously, once everything is &quot;up and running&quot; there may not really be a need to revisit the UI unless an issue occurs. And even still, the <code>Pod</code> logs for each component will tell you what the issue is, but may be easier to track down via the UI.</p>
<h2 id="argocd-sync-features">ArgoCD Sync Features</h2>
<p>Previously we added an <code>Application</code> to ArgoCD that immediately showed &quot;Out of Sync&quot;. That isn&apos;t because our application was extremely complex or that our co-worker was updating the same manifest behind our back, but it was because of something that ArgoCD does on its own we didn&apos;t account for. The way in which ArgoCD manages the state of <code>Applications</code> is by checking its <code>Live</code> state against the <code>Desired</code> state. The <code>Desired</code> state is what we have in our Git repository, but ArgoCD also does some magic on its side after it gets the manifests from Git. ArgoCD, when it creates/applies manifests, adds labels to identify that it is managing those objects and updates the <code>Desired</code> state to reflect that. When the <code>Live</code> and <code>Desired</code> states do not match, we are &quot;Out of Sync&quot;, but since our <code>Live</code> manifest is the one out of sync, why doesn&apos;t ArgoCD do something about it?</p>
<h3 id="manual">Manual</h3>
<p>For those who looked closely at what what our UI showed when creating our argocd-mineops <code>Application</code>, you will remember that the &quot;Sync Policy&quot; was set to manual and no options were checked just as in the screenshot below:</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/03/argocd-sync-opts.png" class="kg-image" alt loading="lazy" width="1056" height="761" srcset="https://blog.kywa.io/content/images/size/w600/2022/03/argocd-sync-opts.png 600w, https://blog.kywa.io/content/images/size/w1000/2022/03/argocd-sync-opts.png 1000w, https://blog.kywa.io/content/images/2022/03/argocd-sync-opts.png 1056w" sizes="(min-width: 720px) 720px"></figure><!--kg-card-begin: markdown--><p>The Manual sync policy is there to be more of a &quot;hey things are good, bad or mostly ok&quot; and does not do anything outside of warn the user. In some cases it has merit, but really defeats the purpose of ArgoCD and GitOps in general in my own personal opinion.</p>
<h3 id="automated">Automated</h3>
<p>An Automated sync policy is where ArgoCD really shines as it will enforce any deviation from the <code>Defined</code> application and the <code>Live</code> state of the <code>Application</code> in the cluster. Essentially how this works is if you had an object in your Kubernetes cluster, lets say a <code>ConfigMap</code>, defined in Git, if someone modified the <code>ConfigMap</code> via <code>kubectl</code>, ArgoCD would immedietly detect the change and apply the <code>Desired</code> manifest. This is extremely powerful and very handy, but there are some things to be aware of. If you put your Kubernetes cluster resources inside of Git (certificates, CRDs, etc..) ArgoCD will enforce them as they are, but there may be times where you run into an issue where Kubernetes updates a specific resource. It could be something as simple as a label or annotation, or it could be a block of configuration, but if ArgoCD detects a deviation from what is defined in Git it will continually re-apply the <code>Desired</code> manifest, sometimes leading to ArgoCD having issues or the cluster being put into a &quot;unique&quot; state. These &quot;differences&quot; were thought about when ArgoCD was designed and thankfully can be handled easily in the <code>Application</code> specification through an <code>ignoreDifferences</code> block. The <a href="https://argo-cd.readthedocs.io/en/stable/user-guide/diffing/">official ArgoCD docs</a> outline how to use it and when to use it, but the quick and dirty is that you can use it in the <code>Application</code> spec or at a global level (which can be handy depending on the type of object).</p>
<p>The 2 main options when selecting an Automated sync policy that we will care about for now are the 2 check boxes beneath AUTOMATED when choosing it from the drop down. These 2 options are:</p>
<ul>
<li><code>prune</code> - Tells ArgoCD to delete objects no longer defined in Git (disabled by default)</li>
<li><code>selfHeal</code> - Enforces <code>Desired</code> to <code>Live</code> self healing (semi-disabled by default)</li>
</ul>
<p>With an Automated sync policy, by default the manifests will not be force synced without <code>selfHeal</code> being set to true except in a few scenarios:</p>
<ul>
<li>An automated sync will only be performed if the <code>Application</code> is OutOfSync. <code>Applications</code> in a Synced or error state will not attempt automated sync.</li>
<li>Automated sync will only attempt one synchronization per unique combination of commit SHA1 and application parameters. If the most recent successful sync in the history was already performed against the same commit-SHA and parameters, a second sync will not be attempted, unless selfHeal flag is set to true.</li>
<li>If <code>selfHeal</code> flag is set to true then sync will be attempted again after self heal timeout (5 seconds by default) which is controlled by <code>--self-heal-timeout-seconds</code> flag of argocd-application-controller deployment.</li>
<li>Automatic sync will not reattempt a sync if the previous sync attempt against the same commit-SHA and parameters had failed.</li>
<li>Rollback cannot be performed against an application with automated sync enabled.</li>
</ul>
<p>As you can see the Automated sync policy on its own will handle most of the common use-cases, but there is typically 0 reason to not set <code>selfHeal</code> to true as it really does enforce the configuration. The only time you may wish to consider not setting <code>selfHeal</code> to true is when initially testing new items and do not want to cause issues (as seen above with differences).</p>
<p>In YAML form an automated sync policy will look something like this:</p>
<pre><code class="language-yaml">spec:
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
</code></pre>
<p>There isn&apos;t much else to outline here on the Automated sync policy for our uses, but there are plenty more definitions for sync options that can be read up on here: <a href="https://argo-cd.readthedocs.io/en/stable/user-guide/sync-options/">https://argo-cd.readthedocs.io/en/stable/user-guide/sync-options/</a>)</p>
<h2 id="repository-setup">Repository Setup</h2>
<p>So we&apos;ve got a decent understanding of ArgoCD, how it works and what it targets to enforce a desired state. So let&apos;s look at what our setup looks like under the guise of our Minecraft Servers as we will have a few variations here very shortly. Our first configuration will be what we&apos;ve already seen, a single <code>Application</code> that targets a directory in a repository and then recursively enforces the manifests in that directory. We will call this the &quot;basic&quot; setup of our Git repository. This is without a doubt the simpliest setup we can use, but does have some shortcomings. With our one <code>Application</code> we will get what we need with our customer Minecraft Servers being applied, but we have one <code>Application</code> that will grow in size and not be very &quot;modular&quot;. Let&apos;s look at what this looks like in the UI using a single ArgoCD <code>Application</code> to manage all of our customer deployments.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/03/argocd-basic-app.png" class="kg-image" alt loading="lazy" width="1338" height="902" srcset="https://blog.kywa.io/content/images/size/w600/2022/03/argocd-basic-app.png 600w, https://blog.kywa.io/content/images/size/w1000/2022/03/argocd-basic-app.png 1000w, https://blog.kywa.io/content/images/2022/03/argocd-basic-app.png 1338w" sizes="(min-width: 720px) 720px"></figure><!--kg-card-begin: markdown--><p>As you can see by the very zoomed out screenshot, we have our one <code>Application</code> enforcing all 3 of the customer Minecraft Servers, but thats it, it is one single <code>Application</code> with one Status. This gives us much less insight into what is actually going on, what if a customer Minecraft Server gets changed by someone on our team, or what if there are issues with one of the manifests? It is very difficult (assuming at a much larger scale than 3) to track down which instance is having issues. This is where the concept of &quot;App of Apps&quot; comes in. You have an ArgoCD <code>Application</code> manage the state of other ArgoCD <code>Applications</code>. Let&apos;s see what this looks like from a realish scenario.</p>
<h3 id="app-of-apps">App of Apps</h3>
<p>App of Apps does create more initial overhead and requires a little bit of design work, but the payoff is huge in the long run as it allows us to have an <code>Application</code> per customer (in our case). In a more real world Kubernetes scenario, the possibilities are endless and the benefits remain the same. Maybe you have one <code>Application</code> with its sole purpose to keep all cluster configuration <code>Applications</code> in place. Or you could have one <code>Application</code> manage an actual application such as Hashicorp Vault or a VS Code Server group for your developers.</p>
<p>For our test Git repository, here is what the layout will look like:</p>
<pre><code class="language-sh">$ tree appofapps/
appofapps/
&#x251C;&#x2500;&#x2500; apps
&#x2502;   &#x251C;&#x2500;&#x2500; customer-0.yaml
&#x2502;   &#x251C;&#x2500;&#x2500; customer-1.yaml
&#x2502;   &#x2514;&#x2500;&#x2500; customer-2.yaml
&#x251C;&#x2500;&#x2500; argocd-mineops.yaml
&#x2514;&#x2500;&#x2500; mineops-servers
    &#x251C;&#x2500;&#x2500; customer-0
    &#x2502;   &#x2514;&#x2500;&#x2500; mineops-0
    &#x2502;       &#x251C;&#x2500;&#x2500; mineops-configmap.yaml
    &#x2502;       &#x251C;&#x2500;&#x2500; mineops-deployment.yaml
    &#x2502;       &#x251C;&#x2500;&#x2500; mineops-namespace.yaml
    &#x2502;       &#x251C;&#x2500;&#x2500; mineops-pvc.yaml
    &#x2502;       &#x2514;&#x2500;&#x2500; mineops-service.yaml
    &#x251C;&#x2500;&#x2500; customer-1
    &#x2502;   &#x2514;&#x2500;&#x2500; mineops-1
    &#x2502;       &#x251C;&#x2500;&#x2500; mineops-configmap.yaml
    &#x2502;       &#x251C;&#x2500;&#x2500; mineops-deployment.yaml
    &#x2502;       &#x251C;&#x2500;&#x2500; mineops-namespace.yaml
    &#x2502;       &#x251C;&#x2500;&#x2500; mineops-pvc.yaml
    &#x2502;       &#x2514;&#x2500;&#x2500; mineops-service.yaml
    &#x2514;&#x2500;&#x2500; customer-2
        &#x2514;&#x2500;&#x2500; mineops-2
            &#x251C;&#x2500;&#x2500; mineops-configmap.yaml
            &#x251C;&#x2500;&#x2500; mineops-deployment.yaml
            &#x251C;&#x2500;&#x2500; mineops-namespace.yaml
            &#x251C;&#x2500;&#x2500; mineops-pvc.yaml
            &#x2514;&#x2500;&#x2500; mineops-service.yaml
</code></pre>
<p>Our &quot;main&quot; <code>Application</code> is the <code>argocd-mineops.yaml</code> manifest which watches the <code>apps/</code> directory and enforces the manifests there. The <code>apps/</code> directory itself contains its own <code>Application</code> for each customer which is watching the respective directory under <code>mineops-servers</code> and enforces those manifests. Below is what each <code>Application</code> is targeting so you can see the &quot;tiered&quot; approach:</p>
<pre><code class="language-yaml"># argocd-mineops Application
spec:
  source:
    path: files/ArgoCD/appofapps/apps

# customer-0 Customer Application
spec:
  source:
    path: files/ArgoCD/appofapps/mineops-servers/customer-0

</code></pre>
<p>From a UI perspective, lets take a look at how this plays out:</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/03/argocd-appofapps-app.png" class="kg-image" alt loading="lazy" width="1337" height="803" srcset="https://blog.kywa.io/content/images/size/w600/2022/03/argocd-appofapps-app.png 600w, https://blog.kywa.io/content/images/size/w1000/2022/03/argocd-appofapps-app.png 1000w, https://blog.kywa.io/content/images/2022/03/argocd-appofapps-app.png 1337w" sizes="(min-width: 720px) 720px"></figure><!--kg-card-begin: markdown--><p>We ultimately still get the same result, but having each customer in its own <code>Application</code> it makes it easier to group and single out a specific configuration. There are some who say it would be fine from having a single <code>Application</code> manage an entire Git repository full of manifests, but this approach is cleaner and allows for more granular and potentially less destructive changes in the event of an accidental Git deletion.</p>
<h2 id="extras-and-nice-to-haves">Extras and Nice to Haves</h2>
<p>We&apos;ve gone over just about every core thing in ArgoCD that we will need to manage our customer Minecraft Servers and everything to get going with your own ArgoCD instance for managing Kubernetes clusters. There are a few things that we didn&apos;t look at during Part 5 of the MineOps series and that is Kubernetes <code>Secrets</code> because we didn&apos;t have a need for them. Well, we will be looking at them here as they are important in the grand scheme of Kubernetes management.</p>
<h3 id="secrets">Secrets</h3>
<p>A Kubernetes <code>Secret</code> is basically the same thing as a <code>ConfigMap</code> with one exception, they are <code>base64</code> encoded. Spoiler time for those who don&apos;t know, but encoding != encryption. They do serve a purpose however and that is being a separate and unique object that can be managed via RBAC so other&apos;s cannot actuall view the data inside. Almost all Kubernetes native applications make use of Secrets. They can be blank objects that get updated by something in the cluster for dynamic secrets that are more secure as they are dynamically generated and updated not by a user. Well if we are going to add Secrets to Kubernetes and store them in Git, this presents a huge issue. Any <code>base64</code> encoded value can be easily obtained by simply running the following:</p>
<pre><code class="language-sh">$ echo &quot;c2VjcmV0cGFzc3dvcmQ=&quot; | base64 -d
secretpassword
</code></pre>
<p>With that in mind, how do we store a <code>Secret</code> in Git if it is easily retrieved? Well, you don&apos;t technically because of that very reason, however there are many amazing tools to make this not so much of an issue. <a href="https://github.com/bitnami-labs/sealed-secrets">Bitnami Sealed Secrets</a> is a pretty awesome one as it allows you to encrypt values and store them in Git which can then be decrypted in cluster via <code>kubeseal</code>. Another great one is <a href="https://www.vaultproject.io/">Hashicorp Vault</a> which does things a little different and has way more overhead than the Bitnami option. We will not be playing around with either tool as we have no real need to store <code>Secrets</code> in Git, but it is important to know how you would handle these in your own real world scenario. ArgoCD can make use of both of the aforementioned tools and decrypt the <code>Secret</code> values and inject them into the cluster without anything being exposed in Git or otherwsise, so have fun and choose a tool that works for you and your team.</p>
<p>As for actually creating <code>Secrets</code>, there are a few ways. The easiest is through <code>kubectl create secret</code> as this allows you to &quot;target&quot; a file and create a <code>Secret</code> with its data being the value and the filename being the key. You can also create it declaratively through a manifest and this can be done in two different ways actually. The first of which is creating it with pre-encoded values and you specify it like so:</p>
<pre><code class="language-yaml">kind: Secret
data:
  type: Z2l0
</code></pre>
<p>Or you can have Kubernetes encode it for us by using the <code>stringData</code> type:</p>
<pre><code class="language-yaml">kind: Secret
stringData:
  type: git
</code></pre>
<pre><code class="language-sh">$ oc get secret somesecret -o yaml
kind: Secret
data:
  type: Z2l0
</code></pre>
<p>There are a few other ways, but these are the more common ways of creating <code>Secrets</code>.</p>
<h3 id="private-repositories">Private Repositories</h3>
<p>What if our Git repository isn&apos;t public? How does ArgoCD get the credentials to communicate with a private repository or one behind an organization that isn&apos;t public? We know that ArgoCD manages objects through an <code>Application</code>, but nowhere during its creation did we get a place to put in credentials of any kind. This is where we get to explore the ArgoCD UI a little more and look at a way to do this declaratively through a <code>Secret</code> manifest. The reason its done through a <code>Secret</code> is that is how ArgoCD stores private repositories.</p>
<h4 id="gui">GUI</h4>
<p>Since we are probably still in the UI somewhere, lets see where we go and how we add in a Private Repository, which are just listed as Repositories inside of ArgoCD.</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/03/argocd-repo-ui.gif" class="kg-image" alt loading="lazy" width="1338" height="960" srcset="https://blog.kywa.io/content/images/size/w600/2022/03/argocd-repo-ui.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/03/argocd-repo-ui.gif 1000w, https://blog.kywa.io/content/images/2022/03/argocd-repo-ui.gif 1338w" sizes="(min-width: 720px) 720px"></figure><!--kg-card-begin: markdown--><p>Now that we&apos;ve created a repository (please note I did not include a username/password as I&apos;m too lazy to blur out the data) we can see how ArgoCD stored that in our cluster:</p>
<pre><code class="language-sh">$ kubectl get secret repo-1593074683 -o yaml
apiVersion: v1
data:
  project: ZGVmYXVsdA==
  type: Z2l0
  url: aHR0cHM6Ly9naXRodWIuY29tL0t5V2EvbWluZU9wcy5naXQ=
kind: Secret
metadata:
  annotations:
    managed-by: argocd.argoproj.io
  creationTimestamp: &quot;2022-03-19T01:11:24Z&quot;
  labels:
    argocd.argoproj.io/secret-type: repository
  name: repo-1593074683
  namespace: argocd
  resourceVersion: &quot;545945&quot;
  uid: 73dc7ef2-fadf-4279-9e3c-ba69cea10445
type: Opaque
</code></pre>
<p>Knowing this is how ArgoCD stores it, that leads us into the other way to add a private Git repository to ArgoCD.</p>
<h4 id="declarative">Declarative</h4>
<p>Using the previous example as our &quot;base&quot; and using a different repository style (git vs http) we can create a new <code>Secret</code> which contains a repository configuration for ArgoCD to use.</p>
<pre><code class="language-yaml">apiVersion: v1
stringData:
  name: argo-repo
  sshPrivateKey: ssh-rsa AAAA0192k301j0192j3019j2301923-----
  type: git
  url: git@github.com:KyWa/mineOps.git
kind: Secret
metadata:
  name: mineops-repo
  namespace: argocd
  annotations:
    managed-by: argocd.argoproj.io
  labels:
    argocd.argoproj.io/secret-type: repository
type: Opaque
</code></pre>
<p>The data inside is obviously important, but what is key here is the label <code>argocd.argoproj.io/secret-type: repository</code> as this is picked up by ArgoCD and knows that this is a &quot;repository&quot; for it to use. The rest of the data is self-explanitory and using a Git repository and connecting over SSH. <strong>NOTE</strong> If you are using a private Git repository over SSH, your ArgoCD <code>Applications</code> will need to reflect this as well.</p>
<p>Let&apos;s create the repository from our <code>Secret</code> and see if ArgoCD picks up the Git repository:</p>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/03/argocd-private-repo-1.gif" class="kg-image" alt loading="lazy" width="1338" height="960" srcset="https://blog.kywa.io/content/images/size/w600/2022/03/argocd-private-repo-1.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/03/argocd-private-repo-1.gif 1000w, https://blog.kywa.io/content/images/2022/03/argocd-private-repo-1.gif 1338w" sizes="(min-width: 720px) 720px"></figure><!--kg-card-begin: markdown--><p>And that is the other method for creating a private Git repository for ArgoCD. Now obviously for similar reasons as mentioned before, you wouldn&apos;t store this in Git itself because its completely readable by all (at least those who know how to pass a value to <code>base64</code>) so a different solution for storing this will be required for the sake of security.</p>
<h2 id="wrapping-up">Wrapping Up</h2>
<p>Hopefully throughout this post you can see that ArgoCD isn&apos;t all that complicated and for most teams, this is the core use of it, but can easily be expounded upon as teams grow. There are other alternatives (or even pairings) such as <a href="https://fluxcd.io/">Flux</a>, but that is outside the scope of this series.</p>
<p>I hope you&apos;ve enjoyed this blog series on MineOps and exploring the growth a company may see moving up the DevOps tool chain. There may be a follow up post to cover a few other items in small detail, but this final post wraps up the official series. It is possible that I may go over this in a livestream and go through the entire series, step by step for those who wish to take part and see all of this happen in real-time.</p>
<h3 id="series-links">Series Links:</h3>
<ul>
<li><a href="https://blog.kywa.io/mineops-part-1/">Part 1 - Manual Minecraft Server Installation</a></li>
<li><a href="https://blog.kywa.io/mineops-part-2/">Part 2 - Automating with Ansible</a></li>
<li><a href="https://blog.kywa.io/mineops-part-3/">Part 3 - Keeping it Altogether with Git</a></li>
<li><a href="https://blog.kywa.io/mineops-part-4/">Part 4 - Containers Have Joined the Party</a></li>
<li><a href="https://blog.kywa.io/mineops-part-5/">Part 5 - Making Containers Highly Available</a></li>
<li><a href="https://blog.kywa.io/mineops-part-6/">Part 6 - ArgoCD to the Rescue</a></li>
</ul>
<!--kg-card-end: markdown-->]]></content:encoded></item><item><title><![CDATA[mineOps - Part 5: Making Containers Highly Available]]></title><description><![CDATA[<figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/mineops-4.png" class="kg-image" alt loading="lazy" width="383" height="78"></figure><p>Before this post begins, I would like to apologize for the long delay in getting this part of the series done. There were a few challenges in the way I was attempting to write this and life got in the way as well. But we are done with the Kubernetes</p>]]></description><link>https://blog.kywa.io/mineops-part-5/</link><guid isPermaLink="false">61d3c21dc040b4060b5bb046</guid><dc:creator><![CDATA[Kyle Walker]]></dc:creator><pubDate>Thu, 24 Feb 2022 22:52:03 GMT</pubDate><content:encoded><![CDATA[<figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/mineops-4.png" class="kg-image" alt loading="lazy" width="383" height="78"></figure><p>Before this post begins, I would like to apologize for the long delay in getting this part of the series done. There were a few challenges in the way I was attempting to write this and life got in the way as well. But we are done with the Kubernetes portion of the series and I hope you enjoy it.</p><hr><p>Since Containers were such a hit (and presented us with many challenges), the CEO has unleashed the new buzzword he heard (and says we need for some reason), Kubernetes. In Part 4 we took a long dive into Containers and how to do just about everything with them. In this part of the series, we will see how to make better use of Containers and handle some of the shortcomings of single node Container Hosts.</p><hr><h2 id="what-is-kubernetes">What is Kubernetes?</h2><p>Kubernetes is a collection of open-source software that handles the deployment and management of containers. This may or may not be a 1:1 copy of the info page of <a href="https://kubernetes.io/">https://kubernetes.io</a>, but it is honestly the best &quot;simple&quot; answer. Kubernetes is wildly complex beyond most peoples imaginations. For most people though, it doesn&apos;t have to be.</p><p>One of the many great things about Kubernetes is the scheduler, which handles the placement of applications on nodes in the Kubernetes cluster. How it does it is quite interesting and has some pretty deep engineering designs under the hood that I am quite literally not capable of explaining. So I&apos;ll just let Kelsey Hightower explain it in a simple elegant outline.</p><figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/HlAXp0-M6SY?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></figure><p>I&apos;ve heard it said that Kubernetes is really just the &quot;next Linux&quot; and truth be told having been working with Kubernetes for quite some time now, I have to agree. Linux gave the data center its freedom to do many things and Kubernetes has now done this for the &quot;Cloud&quot; (and data centers). Many long time Linux admins and engineers will easily see the comparisons of Linux and Kubernetes</p><h2 id="do-we-need-it">Do We Need It?</h2><p>As we go to talk with our CEO about Kubernetes and if we really need to go down this route, we are told: &quot;don&apos;t ask silly questions, just implement this buzzword so we can keep up with all the new tech&quot;. Since it seems we have no choice lets dig in and find out what Kubernetes is going to be able to do for us that Containers couldn&apos;t on their own and try to find the benefits.</p><p>With Containers we had some interesting challenges around where to run containers and having to deploy them individually, which we could do with Ansible just fine already. Containers didn&apos;t really improve anything for us outside of learning a new technology. And that is because deploying and managing containerized applications is not an easy task, but thankfully that is quite literally what Kubernetes does and does it well.</p><p>If you watched the video above where Kelsey Hightower explained Kubernetes to folks at PuppetConf, you should have seen the concept of how Kubernetes schedules containers and frees up resources for &quot;completed&quot; workloads. With running Minecraft Servers, we typically wont see the latter part of that, but the former is really what we are looking for.</p><p>One of the biggest problems with running Containers, is just that, running them. Not from the perspective of <code>docker run mycontainer</code>, but running multiple workloads and managing multiple servers that run Docker/Podman and managing the Containers they have. This is something we didn&apos;t really touch on in the last article, but should have been obvious in the way in which we had to deploy Containers with Ansible (please note it would have been the same even if we had done it manually). Kubernetes at the end of the day is a scheduler, and the issue we had was scheduling our customer Containers at any form of scale. </p><p>No time like the present, so lets dig in.</p><h2 id="basics-of-kubernetes">Basics of Kubernetes</h2><p>Just like Linux there are many &quot;distributions&quot; of Kubernetes and even a <a href="https://www.linuxfromscratch.org/">Linux from Scratch</a> style approach. So what is a distribution of Kubernetes then? Since Kubernetes is grouping of software, there are specific groupings that can be had. Inside these groups of software remain some common and core components which we can dig into below.</p><p>Despite the complexity of Kubernetes and all the software that it makes up, for those who deploy applications on it or manage it, we can describe it quite easily. Just like when describing the &quot;cloud&quot;, Kubernetes can be broken down into the 3 core pillars:</p><ul><li><strong>Compute</strong></li><li><strong>Storage</strong></li><li><strong>Networking</strong></li></ul><p>Now how each of these is done can vary greatly, but each of these has a common item that can be used for each of them. Let&apos;s go over these a little:</p><h3 id="compute">Compute</h3><p>In Kubernetes the smallest compute resource you can create is a <code>Pod</code>, which is comprised by a minimum of one Container. They are called <code>Pods</code> and not Containers because a group of whales is called a &quot;pod&quot; and if you noticed the logo for Docker is a whale. So there is some fun/silly trivia since when Kubernetes was created, Docker was really the only Container game in town at the time.</p><p>You can create <code>Pods</code> on their own or use other types of objects in Kubernetes to deploy and manage pods. We will be going into great detail here later, but just know the most common is called a <code>Deployment</code>.</p><h3 id="storage">Storage</h3><p>Storage is always a fun concept as this is where our data is stored. It is even more interesting in Kubernetes as there are mechanisms to automatically provision storage for you depending on where you have Kubernetes installed and what you have installed on top of it. In the big 3 cloud providers, you get automatic provisioning of storage utilizing each of the big 3&apos;s &quot;cloud storage&quot; options.</p><p>There are other ways to utilize storage inside of Kubernetes. You can connect to NFS servers, use paths from the host, or even mount &quot;files&quot; directly into the Containers! And yes for the people running Windows file servers (or Samba for some reason), you can even use SMB/CIFS if you wanted to, just know that the node running Kubernetes will need a driver installed for SMB/CIFS.</p><h3 id="networking">Networking</h3><p>For most SysAdmins and &quot;DevOps&quot; people alike, networking is the great equalizer (or the great headache depending on how you feel about it) when it comes to technologies. Kubernetes has some very complex networking going on under the hood, but unless you are trying to get into engineering or becoming a maintainer, we don&apos;t need to dig that deep. </p><p>Cluster traffic is handled by objects called a <code>Service</code>. The core usage of a <code>Service</code> is &quot;intra-cluster&quot; communication, but depending on the environment can spin up Load Balancers external to the cluster allowing access to the compute resources behind the <code>Service</code>.</p><p>To get <code>HTTP</code> traffic into a cluster (so you can see your pretty web apps) you will need something called an <code>IngressController</code>. We will dive into <code>Ingress Controllers</code> later, but just know they are an additional item in Kubernetes that gets traffic into a cluster through an object called <code>Ingress</code>. </p><p>There is also another method that is can be used called <code>Gateways</code>. This one is extremely important to many because it goes a step further than <code>HTTP</code> traffic by allowing us to ingress <code>TCP</code> traffic easily. There are other methods to ingress <code>TCP</code> traffic, but they are clumsy and cumbersome in comparison to <code>Gateway</code>s.</p><p>Sadly however, we cannot get the full k8s Ingress experience with Minecraft Server as it uses its own version of the TCP protocol so even a <code>Gateway</code> cannot help us here. Thankfully there are still ways to run and use Minecraft on Kubernetes. Remember those cumbersome methods I just mentioned? Yeah thats us now.</p><h2 id="not-so-basics-of-kubernetes">Not so basics of Kubernetes</h2><p>For us, this section isn&apos;t entirely as important as understanding the basics of Kubernetes and how we can use it, but we will outline a lot of the components and how they really bring it all together.</p><p>The core of Kubernetes is the components that run in whats called the &quot;Control Plane&quot;. This is aptly named as it controls all of the cluster and its components and sometimes items external to the cluster (depending what components are installed). A Control Plane node is a node in the cluster that runs one or more of the components that make up the control plane. Let&apos;s outline these components below:</p><h4 id="worker-node-components">Worker Node Components</h4><ul><li><a href="https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/">kubelet</a> - The Kubernetes agent that runs on the node</li><li><a href="https://kubernetes.io/docs/reference/command-line-tools-reference/kube-proxy/">kube-proxy</a> - Handles cluster network rules at the node level</li><li>Container Runtime - We should remember this from <a href="https://blog.kywa.io/mineops-part-4/">Part 4</a> of this series</li></ul><h4 id="control-plane-node-components">Control Plane Node Components</h4><p>Control Plan nodes have all of the components of a Worker Node including:</p><ul><li><a href="https://kubernetes.io/docs/reference/generated/kube-apiserver/">kube-apiserver</a> - This is the front end of the Control Plane</li><li><a href="https://etcd.io/docs/">etcd</a> - High performance key-value store that contains all cluster data</li><li><a href="https://kubernetes.io/docs/reference/command-line-tools-reference/kube-scheduler/">kube-scheduler</a> - Handles the scheduling of Pods on Nodes</li><li><a href="https://kubernetes.io/docs/concepts/architecture/controller/">kube-controller-manager</a> - Control loops that watch the state of the cluster</li></ul><p>And here is a nice little visualization of the components from <a href="https://kubernetes.io/docs/concepts/overview/components/">Kubernetes</a>.</p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/components-of-kubernetes-1.svg" class="kg-image" alt loading="lazy" width="1252" height="585"></figure><h3 id="building-on-top-of-kubernetes">Building on top of Kubernetes</h3><p>One of the great things about open-source software like Kubernetes is the ability to have customizations and distributions tuned to your liking. Out of the box, Kubernetes provides a pretty awesome platform to run containerized workloads, but does lack a few things that require other services outside of the cluster to fully function. Remember how we built out Minecraft Server image? Can&apos;t do that in a Kubernetes cluster out of the box and must use either your own Docker/Podman/Buildah instance to build these images. What about Security? Kubernetes has some basic security, but really doesn&apos;t provide any extensible security features outside of some very basic features.</p><p><a href="https://www.redhat.com/en/technologies/cloud-computing/openshift">OpenShift from Red Hat</a> handles these shortcomings of a vanilla Kubernetes cluster by providing a much more complete Kubernetes package that you can deploy in all the major cloud providers or on-premise. If you are more of the DIY support group team, you can deploy <a href="https://www.okd.io/">OKD</a>, the upstream project of OpenShift.</p><p>Here are just a few of the &quot;add-ons&quot; that OpenShift and OKD provide over a vanilla Kubernetes installation:</p><ul><li>Extensible security through <code>SecurityContextConstraints</code> utilizing SELinux</li><li>A Container Build platform</li><li>An internal Container Registry</li><li>Cluster authentication built in (OIDC, LDAP and more)</li></ul><h2 id="getting-access-to-a-kubernetes-cluster">Getting access to a Kubernetes Cluster</h2><p>A few years ago, getting access to a Kubernetes cluster was pretty much left to the big 3 cloud providers, but now we have many other methods and the ability to easily spin up our own cluster locally on our machine. For serving customers, we obviously aren&apos;t going to focus on any of the options for running a local cluster, so let&apos;s keep our focus to cloud providers. And now for the big players in the space:</p><h4 id="vanilla-kubernetes-clusters">Vanilla Kubernetes Clusters</h4><ul><li><a href="https://cloud.google.com/kubernetes-engine">GKE</a> - Google Kubernetes Engine</li><li><a href="https://aws.amazon.com/eks/">EKS</a> - Elastic Kubernetes Service</li><li><a href="https://azure.microsoft.com/en-us/services/kubernetes-service/#overview">AKS</a> - Azure Kubernetes Service</li></ul><h4 id="openshift-platforms">OpenShift Platforms</h4><ul><li><a href="https://cloud.redhat.com/products/amazon-openshift">Amazon</a></li><li><a href="https://cloud.redhat.com/products/azure-openshift">Azure</a></li><li><a href="https://cloud.redhat.com/products/openshift-ibm-cloud">IBM Cloud</a></li><li><a href="https://cloud.redhat.com/products/dedicated/">OpenShift Dedicated</a></li><li><a href="https://www.redhat.com/en/technologies/cloud-computing/openshift/container-platform">Self-Hosted</a></li></ul><h4 id="self-hosted-smaller-cloud-development">Self-Hosted, Smaller Cloud, Development</h4><ul><li><a href="https://www.digitalocean.com/products/kubernetes/">DigitalOcean</a></li><li><a href="https://developers.redhat.com/products/codeready-containers/overview">CRC</a></li><li><a href="https://minikube.sigs.k8s.io/docs/start/">Minikube</a></li><li><a href="https://docs.docker.com/desktop/kubernetes/">Docker-Desktop</a></li></ul><p>And now for the &quot;Linux from Scratch&quot; style Kubernetes cluster if you are so bold, <a href="https://github.com/kelseyhightower/kubernetes-the-hard-way">https://github.com/kelseyhightower/kubernetes-the-hard-way</a>. This repo is a project known as possibly the best deep dive DIY guide to Kubernetes to date. It is not for the faint of heart, but is truly an amazing guide to really building your own Kubernetes cluster.</p><p>There is even an upstream software called <a href="https://kubevirt.io/">KubeVirt</a> which lets you create Virtual Machines and treat them as Kubernetes Objects. Red Hat has a product that handles this through OpenShift and you can read more about that <a href="https://cloud.redhat.com/learn/topics/virtualization/">here</a>. We will for sure not be diving into this during this series, but is something to keep in mind as we go throughout our DevOps journey.</p><h2 id="using-kubernetes">Using Kubernetes</h2><p>When it comes to actually using Kubernetes, all you really need is the ability to authenticate with your cluster, usually through a <code>kubeconfig</code> and a client to connect with, typically <code>kubectl</code>. </p><p><strong>Note:</strong> The next few examples outline <code>minikube</code> as the nodes and not a cloud provider. All commands and demonstrations will be the same between Kubernetes clusters unless otherwise noted.</p><h3 id="kubeconfig">Kubeconfig</h3><p>Let&apos;s take a look at the <code>kubeconfig</code> our Cloud Provider gave us when the cluster was provisioned. This file should be placed in <code>~/.kube/config</code> on your machine as <code>kubectl</code> by default will pick it up natively, or can be set through ENV vars (<code>KUBECONFIG</code>) or with the <code>--kubeconfig</code> flag for <code>kubectl</code>. I&apos;m a big fan of not having to modify anything I don&apos;t have to, so mine goes into <code>~/.kube/config</code>. Let&apos;s look at what it we have:</p><p>Depending on the Cloud Provider and how they grant access, the <code>kubeconfig</code> may look slightly different, but the core of each is the same. The breakdown of the sections is fairly straightforward:</p><pre><code>apiVersion: v1
clusters:
- cluster:
    certificate-authority: /minikube/ca.crt
    server: https://172.16.171.129:8443
  name: minikube
contexts:
- context:
    cluster: minikube
    namespace: default
    user: minikube
  name: minikube
current-context: minikube
kind: Config
preferences: {}
users:
- name: minikube
  user:
    client-certificate: /minikube/client.crt
    client-key: /minikube/client.key</code></pre><ul><li><code>clusters</code> - Lists the clusters and their information. <code>certificate-authority</code>, server and name are what commonly fit into these fields</li><li><code>users</code> - Outlines username a token or certificate belongs to</li><li><code>contexts</code> - This groups Kubernetes access data in an easy name. It combines data from the <code>clusters</code> and <code>users</code> sections to determine your &quot;context&quot; and is used to set the <code>current-context</code> which is where <code>kubectl</code> will talk with</li></ul><h4 id="getting-kubectl">Getting <code>kubectl</code></h4><p>If your cluster was deployed in the cloud, chances are you will have a <code>kubeconfig</code> file prior to getting the <code>kubectl</code> binary, so let&apos;s go ahead and get the <code>kubectl</code> binary so we can actually communicate with Kubernetes.</p><p>For Mac users, you can install through a few different methods:</p><pre><code>### Via Homebrew
$ brew install kubernetes-cli

### Direct from Kubernetes.io
$ curl -LO &quot;https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/darwin/amd64/kubectl&quot;</code></pre><p>Linux users can obtain it through curl as well and is the easiest method:</p><pre><code>$ curl -LO &quot;https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl&quot;</code></pre><p>And for other platforms and methods, you can always view the kubectl installation documentation at <a href="https://kubernetes.io/docs/tasks/tools/#kubectl">https://kubernetes.io/docs/tasks/tools/#kubectl</a>.</p><h3 id="first-commands-with-kubectl">First Commands with <code>kubectl</code></h3><p>So we have a <code>kubeconfig</code> file and <code>kubectl</code> installed, now its time to try to see what happens if we issue a command to Kubernetes. </p><pre><code class="language-sh">$ kubectl get nodes
NAME           STATUS   ROLES                  AGE   VERSION
minikube       Ready    control-plane,master   11m   v1.23.1
minikube-m02   Ready    worker                 11m   v1.23.1
minikube-m03   Ready    worker                 10m   v1.23.1</code></pre><p>Well it looks like we&apos;ve connected and got some data back, but lets break down the command we ran to get an idea of what is going on.</p><ul><li><code>get</code> - this is one of the many commands or &quot;verbs&quot; for <code>kubectl</code></li><li><code>nodes</code> - the target object of the <code>get</code> command</li></ul><p>The output we received from Kubernetes tells us that it has 3 nodes, they are all &quot;Ready&quot; (the <code>kubelet</code> on each node is up and connected with the Kubernetes API), what their roles are (not entirely important for most users), how old they are and what version of Kubernetes they are running.</p><p>Some of the most common verbs you will use with <code>kubectl</code> are:</p><ul><li><code>get</code> - Displays a resource (has many flag options)</li><li><code>describe</code> - Shows specific details about a resource(s)</li><li><code>create</code> - Creates a resource (or from a file with <code>-f</code>)</li><li><code>apply</code> - Applies a manifest</li><li><code>delete</code> - Deletes a resource (or from a file with <code>-f</code>)</li></ul><p>With some of the common verbs out of the way, lets see what all else we can find in the cluster.</p><pre><code>$ kubectl get namespaces
NAME              STATUS   AGE
default           Active   25h
kube-node-lease   Active   25h
kube-public       Active   25h
kube-system       Active   25h

$ kubectl get all -n kube-system
NAME                                   READY   STATUS    RESTARTS   AGE
pod/coredns-64897985d-p7j5s            1/1     Running   0          25h
pod/etcd-minikube                      1/1     Running   0          25h
pod/kindnet-47kjw                      1/1     Running   0          25h
pod/kindnet-pknbj                      1/1     Running   0          25h
pod/kindnet-rdq4h                      1/1     Running   0          25h
pod/kube-apiserver-minikube            1/1     Running   0          25h
pod/kube-controller-manager-minikube   1/1     Running   0          25h
pod/kube-proxy-4pgbq                   1/1     Running   0          25h
pod/kube-proxy-n4ksg                   1/1     Running   0          25h
pod/kube-proxy-smjzx                   1/1     Running   0          25h
pod/kube-scheduler-minikube            1/1     Running   0          25h
pod/storage-provisioner                1/1     Running   0          25h

NAME               TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                  AGE
service/kube-dns   ClusterIP   10.96.0.10   &lt;none&gt;        53/UDP,53/TCP,9153/TCP   25h

NAME                        DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
daemonset.apps/kindnet      3         3         3       3            3           &lt;none&gt;                   25h
daemonset.apps/kube-proxy   3         3         3       3            3           kubernetes.io/os=linux   25h

NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/coredns   1/1     1            1           25h

NAME                                DESIRED   CURRENT   READY   AGE
replicaset.apps/coredns-64897985d   1         1         1       25h</code></pre><p>The first command above looked for something called a <code>namespace</code> which is a Kubernetes object that groups resources. In a normal Kubernetes cluster a <code>namespace</code> could contain a single application, a teams applications, a customer&apos;s applications or any number of combinations and doesn&apos;t really matter. The only thing to note about <code>namespaces</code> is that the objects in a namespace are &quot;sectioned&quot; off in that namespace and cannot be accessed by other objects in other namespaces except where explicitly granted via <code>RBAC</code> or some other mechanism (more on this later).</p><p>For the second command <code>kubectl get all -n kube-system</code>, the <code>-n</code> flag specifies to run the command against the namespace, <code>kube-system</code> (we can see that is a valid namespace from the first command). With the <code>get</code> command, you can specify what you want specifically (such as with <code>get namespaces</code>) or you can specify <code>all</code> which returns &quot;most&quot; objects in a <code>namespace</code>. When I say most, I do not mean that it will only return some of the objects, what I mean is that Kubernetes and <code>kubectl</code> have a specific set of objects in the <code>namespace</code> it displays. There are other objects that can be &quot;within&quot; a <code>namespace</code> that can be viewed by calling them out specifically via the <code>get</code> command. The focus of <code>get all</code> revolves around mostly the <code>pods</code>, what manages those <code>pods</code> (such as <code>Deployments</code>, <code>ReplicaSets</code> and <code>DaemonSets</code>) and <code>Services</code>.</p><p>If you are ever curious about all the <code>namespace</code> level objects and want to actually <code>get</code> all of them, here is a handy <code>kubectl</code> command you can run, but be warned, this really does get every single object that exists in a <code>namespace</code>.</p><pre><code class="language-sh"># Put this in your .bashrc or .bash_profile
# Function is run like this: kubegetall NAMESPACENAME

kubegetall () {
for i in $(kubectl api-resources --verbs=list --namespaced -o name | grep -v &quot;events&quot; | sort | uniq); do
  echo &quot;Resource:&quot; $i
  kubectl -n ${1} get --ignore-not-found ${i}
  echo &quot;&quot;
done
}</code></pre><p>For an example of the output of the above command, check out this <a href="https://gist.github.com/KyWa/d71f3e6011653a8ccbbf7b178084ec58">GitHub Gist</a>. And note the output in that Gist is on a minikube cluster with practically no additional Kubernetes addons (again, more on this later). Hardly any of those outputs in a <code>namespace</code> are needed for normal cluster/namespace operation, but do come into play occasionally. You will most typically <code>get</code> the specific object you need as opposed to getting everything.</p><p>The commands above, excluding the <code>kubegetall</code> function, are the most common that you will use, but we need to start diving a little deeper into Kubernetes objects and some more verbose commands.</p><h2 id="kubernetes-objects">Kubernetes Objects</h2><p>All Kubernetes objects can be viewed much like our <code>kubeconfig</code> was and in different ways. Let&apos;s take a look at one object, the <code>kubernetes</code> Service in the default <code>namespace</code>. </p><p><strong>NOTE</strong>: Just a quick discussion, default is just what it sounds like. If there is no <code>context</code> set, <code>kubectl</code> will always target the default <code>namespace</code>. It is not best practice to put your applications here, but there are many guides out in the Internet that &quot;teach&quot; you to just put your applications here. Please do not do this and utilize <code>namespaces</code> as they are there to save you. If you feel like blowing away your namespace with a <code>kubectl delete all</code>, well that will also delete the <code>kubernetes</code> Service which could have less than desirable side effects.</p><pre><code>$ kubectl get svc kubernetes -o yaml
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: &quot;2022-01-26T03:48:38Z&quot;
  labels:
    component: apiserver
    provider: kubernetes
  name: kubernetes
  namespace: default
  resourceVersion: &quot;207&quot;
  uid: 91e5f024-5267-4210-a595-3f6368db0c0b
spec:
  clusterIP: 10.96.0.1
  clusterIPs:
  - 10.96.0.1
  internalTrafficPolicy: Cluster
  ipFamilies:
  - IPv4
  ipFamilyPolicy: SingleStack
  ports:
  - name: https
    port: 443
    protocol: TCP
    targetPort: 8443
  sessionAffinity: None
  type: ClusterIP
status:
  loadBalancer: {}
</code></pre><p>The command above was a normal <code>kubectl get</code> command, but we&apos;ve added a new flag, <code>-o</code>. More info on the <code>-o</code> flag will be next. All of the information above is broken down into a few &quot;core&quot; fields that are common with almost every single Kubernetes object. Let&apos;s break down the &quot;core&quot; fields here to get a better understanding:</p><!--kg-card-begin: markdown--><ul>
<li><code>apiVersion</code> - Specifies which <code>apiVersion</code> the object is requesting/using from the Kubernetes Controller(s)</li>
<li><code>kind</code> - This identifies what an object is and also identifies what the <code>get</code> command would look for</li>
<li><code>metadata</code> - There are many sub fields in here, but the most common to be aware of are:
<ul>
<li><code>name</code> - the actual name of the object</li>
<li><code>namespace</code> - where the object &quot;lives&quot; (cluster level objects do not have a <code>namespace</code> attribute)</li>
<li><code>labels</code> - key/value labels for adding attributes to an object</li>
<li><code>annotations</code> - similar to labels, but are typically used by Operators and additional Kubernetes Controllers that &quot;watch&quot; for objects with specific annotations</li>
</ul>
</li>
<li><code>spec</code> - The <code>spec</code> are is where the real identify of the object comes from. This section is where most Kubernetes objects differ from each other as each object has its own unique specification. Just know that the fields contained within the <code>spec</code> section are what makes up the configuration of an object</li>
<li><code>status</code> - Status is not something you can &quot;apply&quot; to an object as this field is managed by Kubernetes itself and will be updated live with various attributes depending on the Kubernetes Object. Examples would include a Deployment <code>status</code> of <code>availableReplicas</code>, <code>conditions</code> which shows the last time an object was modified/updated.</li>
</ul>
<!--kg-card-end: markdown--><p>With that massive wall of text out of the way, the rest of the Kubernetes objects you deal with will ultimately be the same except for the <code>spec</code> field. Knowing these &quot;core&quot; fields will make looking at and creating Kubernetes manifests much easier. If you ever find yourself unsure of an object or the fields it can contain, <code>kubectl</code> has an amazing built-in function called <code>explain</code> which does just as it sounds. I will not put an example in the blog here, but I will post a <a href="https://gist.github.com/KyWa/5657fa752764bd211b4c2379ad3f3e59">GitHub Gist</a> you can review. Just know that each object follows the format of a <code>json</code> style path: <code>object.spec.configitem1.configitem2</code></p><p>Now for some more information regarding the <code>-o</code> flag from above. The <code>-o</code> flag stands for output and here are some of the common options you will most likely use:</p><ul><li><code>-o yaml</code> - Prints out the YAML of a Kubernetes object(s)</li><li><code>-o json</code> - Same as above, but in JSON format (best when piped to <code>jq</code> imo)</li><li><code>-o name</code> - Prints the object(s) without any other data except <code>type/name</code> such as <code>pod/etcd-minikube</code></li><li><code>-o wide</code> - Prints out similar to with no <code>-o</code> flag but provides more details in line. For <code>pods</code> this includes the node they are running on</li><li><code>-o jsonpath</code> - A little more advanced, but can be used to get specific fields from an objects manifest. Example: <code>kubectl get pod etcd-minikube -o jsonpath=&apos;{.spec.containers[].image}&apos;</code> will return the <code>image:</code> line of the manifest. This can be done with <code>get -o yaml | grep</code>, but is much cleaner </li></ul><p>So we&apos;ve looked at <code>kubectl get</code> and added the <code>-o</code> flag, but there is another command that gives more or less the same data, but in a different format and that is <code>kubectl describe</code>. This command gives all the usual manifest data from <code>kubectl get -o yaml</code>, but formats it differently and provides us with a new item to look at called <code>events</code>:</p><pre><code>$ kubectl describe svc kubernetes
Name:              kubernetes
Namespace:         default
Labels:            component=apiserver
                   provider=kubernetes
Annotations:       &lt;none&gt;
Selector:          &lt;none&gt;
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                10.96.0.1
IPs:               10.96.0.1
Port:              https  443/TCP
TargetPort:        8443/TCP
Endpoints:         172.16.171.129:8443
Session Affinity:  None
Events:            &lt;none&gt;</code></pre><p>As you can see if you were to compare the above output to the <code>kubectl get svc kubernetes -o yaml</code> you would see we haven&apos;t really &quot;gained&quot; any data except a new field called <code>Events</code> which shows the <code>events</code> an object has. For a Service, there probably won&apos;t ever be many (except in possible some bad scenarios), but for a <code>Pod</code>, this will show the <code>Pod</code> being scheduled, NIC assignment, pulling the image for the containers to run and starting each of the containers. The <code>events</code> are very useful for troubleshooting, but you do not need to see them via <code>kubectl describe</code> as they are an object you can also just <code>kubectl get events</code>. One other item to note on <code>describe</code> vs <code>get</code> is the <code>labels</code> section. If you compare the two outputs, you will notice that <code>describe</code> gives us labels with a <code>key=value</code> as opposed to a <code>key:value</code>.</p><p>Which brings me to the next example regarding Kubernetes Objects and that are selecting objects based on their labels. This next example is really more beneficial in <code>namespaces</code> with many applications living in it, but the core of what it does can be seen here. Since we saw that some objects have labels, lets use a label (that we would have either known about or obtained via <code>describe</code> or <code>get</code>) and use it as a selector via another flag <code>-l</code>:</p><pre><code>$ kubectl get all -n kube-system -l k8s-app=kube-dns
NAME                          READY   STATUS    RESTARTS   AGE
pod/coredns-64897985d-p7j5s   1/1     Running   0          3d10h

NAME               TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                  AGE
service/kube-dns   ClusterIP   10.96.0.10   &lt;none&gt;        53/UDP,53/TCP,9153/TCP   3d10h

NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/coredns   1/1     1            1           3d10h

NAME                                DESIRED   CURRENT   READY   AGE
replicaset.apps/coredns-64897985d   1         1         1       3d10h
</code></pre><p>With <code>-l</code> being a selector, we chose <code>k8s-app</code> as our label with the value of <code>kube-dns</code>. This shows all objects that share this label and help us identify all the components that make up the <code>kube-dns</code> application. This will be extremely handy to remember when working with Kubernetes and multiple applications as it allows you to grab just the objects that share the label. This can also be run with the <code>get all -A</code> instead of the <code>-n namespace-name</code> flag and will append the <code>namespace</code> of each object before the object:</p><pre><code>$ kubectl get all -A -l k8s-app=kube-dns
NAMESPACE     NAME                          
kube-system   pod/coredns-64897985d-p7j5s 

NAMESPACE     NAME              
kube-system   service/kube-dns 

NAMESPACE     NAME                     
kube-system   deployment.apps/coredns 

NAMESPACE     NAME
kube-system   replicaset.apps/coredns-64897985d</code></pre><p><strong>NOTE</strong>: On the above command I removed all data after the NAME field for readability, but it prints out the same as the prior command. You can even chain other flags onto the selector such as <code>-o wide</code> or <code>-o yaml</code>, but note the <code>-o yaml</code> will print all of those objects into a <code>List</code> which is typically unpleasant to scroll through, so I would recommend piping that to a file to review.</p><h3 id="where-do-objects-live">Where do objects live?</h3><p>So we have looked at Kubernetes Objects and how to <code>get</code> and <code>describe</code> them, but we haven&apos;t really discussed &quot;where&quot; they live. Now so far every single object has been a <code>namespace</code> level object, but there are objects in a Kubernetes cluster that are not at a <code>namespace</code> level and live at the cluster level. These objects have a flag in their definitions which state <code>namespaced: false</code>. For a list of Kubernetes objects in your cluster which can be created/applied you can run the <code>kubectl api-resources --verbs=list</code> command to identify them. You can also separate them into <code>namespaced=true</code> or <code>namespaced=false</code> to show which objects are for which area of a cluster.</p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/Screen-Shot-2022-01-29-at-8.43.07-AM.png" class="kg-image" alt loading="lazy" width="755" height="529" srcset="https://blog.kywa.io/content/images/size/w600/2022/01/Screen-Shot-2022-01-29-at-8.43.07-AM.png 600w, https://blog.kywa.io/content/images/2022/01/Screen-Shot-2022-01-29-at-8.43.07-AM.png 755w" sizes="(min-width: 720px) 720px"></figure><p>As you can see there are quite a few objects that can be created in a <code>namespace</code>, but hardly any of these will be used by most people directly, but each of them have their place and provide great value as you grow in your Kubernetes knowledge and administrative capabilities. Thankfully this output shows you the <code>apiVersion</code> and <code>kind</code> of the objects to help you identify and create your own if you are having issues.</p><h4 id="cluster-level-objects">Cluster Level Objects</h4><p>Now lets dig into the non-namespaced level objects commonly known as cluster-level objects. Depending on the cluster there may be less or more of these, but the common ones are here:</p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/Screen-Shot-2022-01-29-at-8.43.43-AM.png" class="kg-image" alt loading="lazy" width="911" height="394" srcset="https://blog.kywa.io/content/images/size/w600/2022/01/Screen-Shot-2022-01-29-at-8.43.43-AM.png 600w, https://blog.kywa.io/content/images/2022/01/Screen-Shot-2022-01-29-at-8.43.43-AM.png 911w" sizes="(min-width: 720px) 720px"></figure><p>As you can see in the output above its no different than the <code>namespace</code> level objects with the exception of the <code>NAMESPACED</code> field being false. Whichever cluster you are working with, you can use the <code>kubectl api-resources</code> command to identify which objects the cluster can have. This really comes handy in debugging as you get into more advanced Kubernetes objects, which in fairness most of the complexity comes from the Networking components as they can vary greatly depending on what has been installed on the cluster.</p><p>The more common cluster level objects that you really need to know about are, <code>nodes</code>, <code>clusterroles</code>, <code>clusterrolebindings</code>, and potentially <code>ingressclasses</code>. <code>Nodes</code> are important just to know the state of the cluster, <code>clusterroles</code> and <code>clusterrolebindings</code> are for managing RBAC in the cluster.</p><p>There really isn&apos;t much more to discuss about objects in the context of describing them. It is now time to start looking at some of the objects we will need to use and be aware of for deploying applications.</p><h2 id="deploying-an-application">Deploying an Application</h2><p>So we know about Kubernetes objects and how to look at their specifications to determine what they are and do, but we haven&apos;t seen anything outside of what Kubernetes itself needs. Just like in the Docker part of this series, we did a basic &quot;hello-world&quot; which actually provided some value as it outlined quite a few unique attributes of containers. There is not an equivalent in Kubernetes, but thankfully we don&apos;t really need one. As mentioned before, Kubernetes can easily be broken down into the 3 main components of a &quot;cloud&quot; (Compute, Storage, Networking), so let&apos;s look at manifests that generate those components. All manifests can be found here in the support <a href="https://github.com/KyWa/blogs/tree/master/mineOps/files/Kubernetes/basic-application">GitHub repo for MineOps</a>.</p><p>Instead of diving straight into a Minecraft Server deployment, we will look at a VS Code Server to get a feel for more Kubernetes components. Typically you would name your manifest files after what they are, but for the sake of comparing the 3 main components, I&apos;ve named them after what they &quot;do&quot;.</p><h4 id="compute-manifest">Compute Manifest</h4><p>Inside of the <a href="https://github.com/KyWa/blogs/blob/master/mineOps/files/Kubernetes/basic-application/compute.yaml">compute.yaml</a> file we have a single object which is our <code>Deployment</code>. A <code>Deployment</code> is responsible for, well, deploying a Pod which can have any number of Containers in it. This particular example only has one Container which uses the <code>image</code> for a VS Code server that can run in a browser. This image is hosted in <a href="https://quay.io/">Quay.io</a> and when the Pod goes to spin up, the <code>kubelet</code> on the <code>node</code> where the Pod has been scheduled will basically run a <code>docker pull</code> to get the image so it can then start the Container. A few items of note in this manifest, are the <code>volumeMounts</code>, <code>volumes</code>, and <code>env</code> sections.</p><ul><li><code>env</code> - This provides environment variables to the Container and in our case passes in a PASSWORD to login to the VS Code Server</li><li><code>volumes</code> - Volumes outline which objects are required to &quot;mount&quot; to a Pod. This can be a <code>persistentVolumeClaim</code> (which we will be using and discussing), ConfigMap, NFS share, iSCSI volumes, Secrets and quite literally anything. Note that <code>volumes</code> are declared at the Pod level and not the Containers</li><li><code>volumeMounts</code> - Identifies where a <code>volume</code> should be mounted inside a container. Depending on the type of <code>volume</code> used, if you were to run <code>df</code> inside the Container, you may see it as a literal mount like any other <code>mount</code> would be</li></ul><h4 id="storage-manifest">Storage Manifest</h4><p>Our storage manifest is quite simple as you can see in <a href="https://github.com/KyWa/blogs/blob/master/mineOps/files/Kubernetes/basic-application/storage.yaml">storage.yaml</a>. There is a single object called a <code>PersistentVolumeClaim</code> which will, based on its <code>spec</code>, request 5Gi of storage. Now the &quot;how&quot; it does this is unique for each platform, but a <code>PersistentVolumeClaim</code> will request a <code>PersistentVolume</code> from the cluster of the same size as the <code>requested</code> Claim and if one does not exist will look to the <code>StorageClass</code> to create one. A <code>StorageClass</code> is a provisioner that resides in a cluster to reach out to a storage platform and create a share/volume/disk which is then present in the cluster as a <code>PersistentVolume</code>. A cluster can have more than one <code>StorageClass</code> and one of these can be set to the <code>default</code> which will be where all <code>PersistentVolumes</code> are created from, unless in the <code>PersistentVolumeClaim</code> you specify the <code>storageClass</code> to use. Now if there is no <code>StorageClass</code> in a cluster, you will have a <code>PersistentVolumeClaim</code> perpetually stuck in a <code>Pending</code> state. </p><p>Most if not all clusters deployed in the cloud will always have a <code>StorageClass</code>, but there will be specific use-cases where you may want a different type of underlying storage. Sometimes you may be running a database which may not like being on Amazon S3 File storage and would be better suited performance wise with block storage. These choices will change depending on the application and the team running it and whatever architectural and platform decisions are made. </p><p>There are some storage platforms that do some &quot;interesting&quot; things as it relates to mounting the share/volume/disk to the running Pod. Some workarounds may have to be made to your application, or your deployment (which we will go into later) to have the storage work as expected. This is not the case with all storage platforms, but some do have permissions that make life interesting.</p><h4 id="network-manifest">Network Manifest</h4><p>The <a href="https://github.com/KyWa/blogs/blob/master/mineOps/files/Kubernetes/basic-application/networking.yaml">networking.yaml</a> file has 2 objects inside of it, a Service and an Ingress. This manifest is broken up by the YAML syntax of <code>---</code> which creates a &quot;break&quot; and tells the renderer (in our case <code>kubectl</code>) where the next object begins. </p><hr><p>This is one of a few ways to use a single file to house multiple objects, the other being an actual <code>List</code>. Here is a quick snippet of a <code>List</code>:</p><figure class="kg-card kg-code-card"><pre><code class="language-yaml">apiVersion: v1
kind: List
items:
- apiVersion: v1
  kind: Service
  ~~~~~
- apiVersion: networking.k8s.io/v1
  kind: Ingress
  ~~~~~</code></pre><figcaption>a Kubernetes List format</figcaption></figure><p>There are times depending on the <code>client</code> using the manifest where a <code>List</code> may be helpful, but for now its just another little bit of information and using the <code>---</code> to break up objects works fine.</p><hr><p>We explained earlier that a Service is for the most part meant for &quot;in cluster&quot; communication. A Kubernetes Service can actually have many different forms and how the work depends on the underlying platform they run on (see why some people say Kubernetes is hard/confusing?). The Service can have a few different <code>types</code> each doing something different with the most commonly used (and &quot;default&quot; type) being the <code>ClusterIP</code>. A Service with a <code>ClusterIP</code> basically gets a non-routable IP address (well, its routable, just only in the cluster) and is used as a front end for other services/applications in the cluster to be able to resolve the Service. Each Service, regardless of <code>type</code>, gets a DNS name in the cluster. For our example Service in the <code>networking.yaml</code> file the cluster DNS name would be: <code>code-server.code-server.svc.cluster.local</code> or even <code>code-server.code-server</code>. This comes in quite handy when building out multiple applications as you don&apos;t really have to worry about DNS, just your Service names and <code>namespaces</code>.</p><p>Even though we didn&apos;t cover this in the <a href="https://blog.kywa.io/mineops-part-4/">Containers part of this series</a>, you will eventually run across someone using <code>docker-compose</code> or just <code>docker</code> and something called <code>link</code> and/or <code>bridges</code>. What that accomplishes is allowing Containers to &quot;know&quot; about each other and be able to communicate. A Kubernetes Service, which gets a DNS entry the entire cluster can use by default, your applications can find other applications via their Service name without any real changes or worrying about hostnames and DNS. Since networking is a pretty huge part of most applications, lets dig into this concept just a little further, even though our Minecraft Server wont really be utilizing this, it is extremely important to understand for working with Kubernetes.</p><hr><p>Let&apos;s take a basic application as an example such as Wordpress. Wordpress has a web front end in PHP and needs a database as its backend (typically MySQL). In a normal installation on a VM or something similar, you have to give Wordpress the DNS name or IP of the database for it to be able to connect to. Depending on how and where you deploy Wordpress this could change drastically and be something you have to &quot;hunt down&quot; every single time you deploy a new Wordpress instance. In the Kubernetes world, your database will almost always have a Service regardless of any other factors making it simple and easy to deploy multiple Wordpress instances across a Kubernetes cluster.</p><figure class="kg-card kg-code-card"><pre><code class="language-yaml"># Wordpress Deployment snippet
spec:
  env:
  - name: MYSQL_SERVER
    value: mysql</code></pre><figcaption>Showing using Service DNS name as ENV variable</figcaption></figure><hr><p>Now for the important and &quot;complicated&quot; topic of Ingress. <code>Ingress</code> is an object created in a Kubernetes cluster that allows traffic into the cluster from outside of the cluster. Services do not do this, but technically can enable external traffic into the cluster, but in a very different way. Services that are of the <code>type: LoadBalancer</code> and on a supported platform, creates a Load Balancer on the platform with a Public IP Address that then connects it to the Service. This can work for some use-cases, but is very much a one to one of IP&lt;-&gt;Service. Using Ingress allows for far fewer external IPs (in theory only 1 for most) and allows you to use DNS names to route traffic into the cluster. The <code>IngressController</code> will then pass the traffic to the Services based on the <code>host</code> defined in the rules section.</p><p>Quick example to highlight this and how it works:</p><p>You have an <code>IngressController</code> with an external Load Balancer with an IP of 20.192.1.254, you create an external DNS record of <code>*.kube.example.com</code> which resolves to the IP of 20.192.1.254. Inside of your cluster you can create an Ingress object that has a configuration like so:</p><pre><code>~~~
spec:
  rules:
  - host: &quot;code.kube.example.com&quot;
~~~</code></pre><p>The above snippet should more or less outline what this is going to do, but basically the Ingress object allows the IngressController to do this:</p><ul><li>Listen for traffic coming in over the DNS/IP of 20.192.1.254</li><li>Check rules for existing Ingress objects</li><li>Route external traffic into the cluster to the Service with the matching rules for the <code>host</code> rule in the Ingress object</li><li>Service then passes traffic to the pod(s) matching its labels</li></ul><hr><p>That was a rough outline of what Ingress is and does, but there can be multiple IngressControllers that allow for different features and your Ingress objects can specify which one to use via an <code>annotation</code>. For us we will not be using <code>Ingress</code>, but there are other ways to do this which we do not need to go into here, but it is important to know the common ways to get traffic into your cluster.</p><h3 id="applying-the-manifests">Applying the Manifests</h3><p>So now that we did a semi-deep dive on the manifests and what they are going to do, lets go ahead and try to create them and see where we can get. There are a few ways to do this, but the easiest way to apply multiple manifests is to have them in a directory and target that directory with <code>kubectl</code> using the <code>-f</code> flag which specifies a &quot;filename&quot;. This filename can be a file, directory or even a URL (which is handy if you have a manifest on the Internet somewhere and wish to use it without having it locally). You can apply multiple manifests, but only through targeting a directory (sadly you cannot apply multiple individual files).</p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/02/k8sapply.gif" class="kg-image" alt loading="lazy" width="2000" height="978" srcset="https://blog.kywa.io/content/images/size/w600/2022/02/k8sapply.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/02/k8sapply.gif 1000w, https://blog.kywa.io/content/images/size/w1600/2022/02/k8sapply.gif 1600w, https://blog.kywa.io/content/images/size/w2400/2022/02/k8sapply.gif 2400w" sizes="(min-width: 720px) 720px"></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.kywa.io/content/images/2022/02/k8s-codeserver-1.gif" class="kg-image" alt loading="lazy" width="1280" height="820" srcset="https://blog.kywa.io/content/images/size/w600/2022/02/k8s-codeserver-1.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/02/k8s-codeserver-1.gif 1000w, https://blog.kywa.io/content/images/2022/02/k8s-codeserver-1.gif 1280w" sizes="(min-width: 720px) 720px"><figcaption>Quick look at our newly deployed application</figcaption></figure><p>So we applied our manifests and now have a working application in our Kubernetes cluster as seen by going to <code>code.kube.example.com</code>. Nothing is really too special about this except that we have a running application. Its just VS Code &#xA0;in a browser (a really cool project) running in a container. Now if we look at a few things we can see what all we created in these manifests:</p><pre><code>$ kubectl get all
NAME                               READY   STATUS    RESTARTS   AGE
pod/code-server-7c444dc854-9bfcw   1/1     Running   0          12h

NAME                  TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
service/code-server   ClusterIP   10.102.116.112   &lt;none&gt;        8080/TCP   12h
service/kubernetes    ClusterIP   10.96.0.1        &lt;none&gt;        443/TCP    41h

NAME                          READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/code-server   1/1     1            1           12h

NAME                                     DESIRED   CURRENT   READY   AGE
replicaset.apps/code-server-7c444dc854   1         1         1       12h


$ kubectl get ingress
NAME          CLASS    HOSTS                   ADDRESS          PORTS     AGE
code-server   &lt;none&gt;   code.kube.example.com   172.16.171.146   80, 443   12h

$ kubectl get pvc
NAME          STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
code-server   Bound    pvc-e443ccf4-6bcf-47cd-a998-88d007eed995   5Gi        RWO            standard       12h</code></pre><p>In the <code>compute.yaml</code> manifest we had a <code>Deployment</code>, but we now also see a <code>replicaset</code> and a <code>pod</code>. Without going into a super deep dive, a <code>Deployment</code> is more of a &quot;specification&quot; of how pod(s) should be created and managed. A <code>Deployment</code> will generate a <code>ReplicaSet</code> with the specification and it&apos;s the <code>ReplicaSet</code>s job to manage the state of the <code>Pod</code>. If the pod is deleted, the <code>ReplicaSet</code> will request a new one be created. Nearly all &quot;compute&quot; resources in Kubernetes follow this model to a degree. And to add fun to that, the <code>pod</code> we ultimately got has 1 Container in it. A <code>pod</code> can have multiple containers as we mentioned earlier in this blog.</p><figure class="kg-card kg-code-card"><pre><code>NAME                               READY   STATUS    RESTARTS   AGE
pod/code-server-7c444dc854-9bfcw   1/1     Running   0          12h</code></pre><figcaption>The 1/1 shows how many &quot;Ready&quot; Containers the pod has</figcaption></figure><p>The rest of the objects are all there and we can see details about them (which should match what we have in the manifests we applied). For the most of our use-case, this is just about all we really need to know about Kubernetes manifests and applying them as everything else you will ever deploy in a Kubernetes cluster follows basically the same logic.</p><h2 id="minecraft-on-kubernetes">Minecraft on Kubernetes</h2><p>Now that we&apos;ve gotten a somewhat decent outline of what Kubernetes is and what it looks like to deploy an application, its now time to actually deploy Minecraft Server on Kubernetes.</p><h3 id="building-our-manifests">Building our Manifests</h3><p>As with our example deployment of the VS Code Server above, we will need manifests to create and deploy our Minecraft Server, but instead of creating these objects in the default <code>namespace</code>, we will create them in a namespace dedicated to our application. We can have many Minecraft Servers per namespace or one per. Since we our now moving to putting Minecraft Server for our customers in Kubernetes, we will be using a namespace per customer to logically break up the applications (and more easily identify what goes where). So lets create our first namespace prior to deploying these manifests.</p><pre><code>$ kubectl create namespace mineops-0
namespace/mineops-0 created
$ kubectl config set-context --current --namespace mineops-0
Context &quot;minikube&quot; modified.</code></pre><p>We also ran another command and that was to set our current context to allow any manifest we apply to go into that namespace. The previous statement is true unless the manifest you apply has its <code>metadata</code> set to specify a namespace like this:</p><pre><code>metadata:
  namespace: someothernamespace</code></pre><p>The above snippet for whatever is in it will be applied only to the namespace <code>someothernamespace</code> regardless of the context of your <code>kubeconfig</code>. For our manifests we will be omitting this (to be explained later) so we will need to ensure we change our context as we did above.</p><h3 id="service-networking">Service (Networking)</h3><p>Now for the best part, but also the &quot;worst&quot; part about this whole endeavor. There is nothing stopping us from running Minecraft Server on Kubernetes, however there is one issue I outlined earlier that we will run into. Since Minecraft uses its own version of the TCP Protocol, we are unable to use any typical <code>Ingress</code> or <code>Gateway</code> method to route traffic to a specific Container. We are going to have to do essentially what we did during all previous iterations of deploying Minecraft Server for our customers and that is 1 Server per IP. Sadly there is no known way around this (much of the reason this article took so long to come out was trying to engineer a way around it). So lets get on with what we can do and go from there. Here is what our Manifest will be and keep in mind this assumes that our Kubernetes cluster is running on a platform/provider that can provision IPs via a Load Balancer service:</p><pre><code>apiVersion: v1
kind: Service
metadata:
  labels:
    app: mineops
  name: mineops
spec:
  ports:
  - port: 25565
    name: tcp-minecraft
    protocol: TCP
    targetPort: 25565
  selector:
    app: mineops
  sessionAffinity: None
  type: LoadBalancer</code></pre><p>The above <code>Service</code> will act as a normal service except that because its type is <code>LoadBalancer</code> it will trigger the underlying platform to provision an IP address and &quot;map&quot; it to this <code>Service</code> as we will see in a moment. There is not much else to cover here for the networking portion so lets move on to storage.</p><h3 id="persistentvolume-storage">PersistentVolume (Storage)</h3><p>For nearly all Kubernetes clusters, you are going to have some dynamic storage provisioner in the cluster and can be verified by checking for a <code>storageclass</code> like so:</p><pre><code>$ kubectl get storageclass
NAME                 PROVISIONER                RECLAIMPOLICY   VOLUMEBINDINGMODE   ALLOWVOLUMEEXPANSION   AGE
standard (default)   k8s.io/minikube-hostpath   Delete          Immediate           false                  6d5h</code></pre><p>Depending on the cluster and where it lives, the name and provisioner fields will most likely have different data inside of them, but for the most part they will all function the same. Similar to our previous &quot;storage&quot; manifest we will just be creating a <code>PersistentVolumeClaim</code> which will check to see if there is a volume matching our request and if not, go create one (using whatever provisioner is default or selected).</p><pre><code>apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mineops
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi</code></pre><p>Nearly all storage providers will be able to work fine for our scenarios, but there are some providers that work far better than others. Block level storage will grant us the best speeds for I/O of the Minecraft Server and is what we should look to use. If the platform we deploy to has the option of choosing a <code>StorageClass</code> and one of them is block, go for that one (which is what we will do). The <code>StorageClass</code> can be specified in the <code>PersistentVolumeClaim</code> manifest by adding this line to it:</p><pre><code>spec:
  storageClassName: standard</code></pre><p>Block storage is amazing with its I/O capability, but there are some minor things we need to work around prior to it working as some storage providers do. We will go over and discuss this in the next section.</p><h3 id="deployment-compute">Deployment (Compute)</h3><p>All workloads on Kubernetes come from somewhere and no different than the VS Code Server example was used previously, we will be using a Deployment. The manifest will look something like this:</p><pre><code>---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: mineops
  name: mineops
spec:
  selector:
    matchLabels:
      app: mineops
  replicas: 1
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: mineops
    spec:
      containers:
      - image: docker.io/mineops/minecraft-server:prod
        imagePullPolicy: Always
        name: minecraft-server
        ports:
        - name: minecraft
          containerPort: 25565
          protocol: TCP
        volumeMounts:
        - mountPath: /minecraft
          name: minecraft-data
        - mountPath: /minecraft/server.properties
          subPath: server.properties
          name: server-properties
      volumes:
      - name: minecraft-data
        persistentVolumeClaim:
          claimName: mineops
      - name: server-properties
        configMap:
          name: minecraft-server</code></pre><p>For most platforms this will be just fine and work as intended, however for us using block storage for that sweet read/write performance, we are going to run into something else that needs to be added to the <code>Deployment</code>. It is a new concept called an <code>initContainer</code>, which just as its name sounds will be run on &quot;initialization&quot; of the Pod. <code>initContainers</code> can be extremely powerful, but just know they are not a &quot;one and done&quot;. Every time a Pod starts up that has an <code>initContainer</code>, the <code>initContainer</code> will run. So if you have in the future an idea for an <code>initContainer</code> to handle some &quot;pre Pod work&quot;, just know that it will run every single time the Pod is spun up. </p><h4 id="initcontainer">initContainer</h4><p>As mentioned earlier when discussing the manifests, some storage platforms have small challenges to overcome. For both the built-in <code>storageClass</code> of Minikube and many cloud provider default block storage class, we must implement a workaround for our Minecraft Server deployment. The issue with these block storage providers is that they mount the volume as <code>root</code> and our Container does not run as <code>root</code> because that is a very bad security practice. If we use the default <code>storageClass</code> of each of these providers, we will run into a <code>permission denied</code> issue when attempting to write to the <code>PersistentVolume</code>. Our workaround will involve an <code>initContainer</code> which we are going to use to &quot;fix&quot; our permissions prior to starting the main Minecraft Server Container. It follows many of the same <code>spec</code> outlines of our normal Pod as we can see here:</p><figure class="kg-card kg-code-card"><pre><code>      initContainers:
      - name: data-permission-fix
        image: busybox
        command: [&quot;/bin/chmod&quot;,&quot;-R&quot;,&quot;777&quot;, &quot;/minecraft&quot;]
        volumeMounts:
        - name: minecraft-data
          mountPath: /minecraft</code></pre><figcaption>Our initContainer to fix permissions</figcaption></figure><p>The above code will go into our Deployment at the same &quot;level&quot; in the <code>spec</code> as the <code>containers</code> field. We could technically implement something like this into a startup-script called by the <code>ENTRYPOINT</code> of our Dockerfile, but there is no real need to do this, when its only needed on some platforms.</p><h3 id="ingress-for-minecraft">&quot;Ingress&quot; for Minecraft</h3><p>As I mentioned in the description of the <code>Service</code> manifest above, Ingress will have to be handled as a 1 IP to 1 Minecraft Server. In almost all other Kubernetes Ingress related scenarios this is not the case regardless of application type. Nothing wrong here, but ideally you would want to reduce the number of IPs for your services and be able to have your Ingress Controller or ServiceMesh be able to Ingress traffic and route it for you.</p><h3 id="getting-configuration-data-into-a-pod">Getting Configuration data into a Pod</h3><p>Since we have yet to discuss <code>ConfigMaps</code> lets do that now. <code>ConfigMaps</code> can be used to store data in a &quot;key:value&quot; style specification. How you use them is up to you, but for us we will be using <code>ConfigMaps</code> to store our <code>server.properties</code> configuration file. This will be a somewhat different change from our Containers part of this series, where we had a local directory on a server with a <code>server.properties</code> that we pushed out via Ansible and maintained in Git.</p><p>If you noticed in the Deployment manifest above, we are mounting a <code>ConfigMap</code> as a volume for our Pod, but how do we get one and whats in it? We can create <code>configMaps</code> as any other object by having a manifest to create it with, or we can do something pretty neat and that is having <code>kubectl</code> &quot;ingest&quot; a file as its manifest as we can see below:</p><pre><code>$ kubectl create configmap minecraft-config --from-file=server.properties
configmap/minecraft-config created</code></pre><p>And if we were to look at our <code>ConfigMaps</code> via <code>kubectl get -o yaml</code> we would see that we have something that looks like this (some lines omitted for readability):</p><pre><code>$ kubectl get cm -o yaml minecraft-config
apiVersion: v1
data:
  server.properties: |
    debug=true
    enable-jmx-monitoring=false
    rcon.port=25575
    gamemode=survival
    enable-command-block=false
    ~~~OMITTED~~~
kind: ConfigMap
metadata:
  name: minecraft-server
  namespace: mineops</code></pre><p>One thing to note is the field above <code>data</code> is something new and can basically be viewed as the <code>spec</code> for certain types of objects with <code>ConfigMaps</code> and <code>Secrets</code> being some of the main ones. Everything will boil down to a key and a value regardless of the size of the value. Since we didn&apos;t go over many items for <code>ConfigMaps</code> here are a few examples (excluding what we see in the above snippet).</p><pre><code>data:
  somevar: 42
  
  myscript.sh: |
    #!/bin/bash
    echo &quot;hi&quot;</code></pre><p><strong>NOTE: </strong>Although we didn&apos;t go into <code>Secrets</code> in this series, Kubernetes Secrets play a big role in storage sensitive data in a Kubernetes cluster and follow the same key:value model as <code>ConfigMaps</code> with one exception, the values inside must be <code>base64</code> encoded and have no trailing newlines. More information on Secrets can be found <a href="https://kubernetes.io/docs/concepts/configuration/secret/">here</a>.</p><h3 id="deploying-minecraft-server">Deploying Minecraft Server</h3><p>Now that we have all of our manifests its time to deploy our manifests into our cluster. Just as with every other manifest we can just run <code>kubectl apply</code> or <code>kubectl create</code> to get our manifests into the cluster. So here we go!</p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/02/k8s-mineops-apply.gif" class="kg-image" alt loading="lazy" width="2000" height="978" srcset="https://blog.kywa.io/content/images/size/w600/2022/02/k8s-mineops-apply.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/02/k8s-mineops-apply.gif 1000w, https://blog.kywa.io/content/images/size/w1600/2022/02/k8s-mineops-apply.gif 1600w, https://blog.kywa.io/content/images/size/w2400/2022/02/k8s-mineops-apply.gif 2400w" sizes="(min-width: 720px) 720px"></figure><p>Looks like we&apos;ve got everything created in our namespace and the Minecraft Server Pod seems to be up and running. Before we connect lets dig a little deeper and see whats going on with our Pod just for some more context and a bit more visibility using Kubernetes. We will watch the logs as we connect using <code>kubectl</code> since we aren&apos;t on a VM and don&apos;t have normal access to the local log file.</p><h3 id="connecting-to-our-minecraft-server">Connecting to our Minecraft Server</h3><p>So now comes the best time of our deployment and that is to see if it works as we anticipate. We will have <code>kubectl logs -f PODNAME</code> running alongside so we can see the status of the Minecraft Server as well as verifying that we&apos;ve connected.</p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/02/k8s-mineops-connect.gif" class="kg-image" alt loading="lazy" width="960" height="333" srcset="https://blog.kywa.io/content/images/size/w600/2022/02/k8s-mineops-connect.gif 600w, https://blog.kywa.io/content/images/2022/02/k8s-mineops-connect.gif 960w" sizes="(min-width: 720px) 720px"></figure><p>Whelp, looks like that is another one down in the history books. We&apos;ve officially done what our CEO has asked of us and deployed our Minecraft Server to Kubernetes. How do migrate our customer&apos;s existing data to Kubernetes from our existing Container infrastructure? In the next and final section of this post we will look at some of the extra things that never get talked about in a real world Kubernetes scenario.</p><h2 id="migrating-to-kubernetes">Migrating to Kubernetes</h2><p>Since we don&apos;t have direct access (at least over normal methods such as SSH) to the nodes in Kubernetes and our Minecraft Server data will live in a <code>PersistentVolumeClaim</code> from a storage provider, how do we get data into this PVC? As with most things, there are many methods and some may be cleaner than others. For us, we are going to open a simple HTTP Server that will serve the Minecraft Backup&apos;s we are taking (we do take backups right?). With that open, we will be able to retrieve the backup tarball from a Container using curl, extract it and then go stand up our Minecraft Server.</p><h4 id="exposing-our-backups">Exposing our backups</h4><p>The method in which we are going to do this may be frowned upon by some, but since it is just a Minecraft Server backup and no company intellectual property, it isn&apos;t really that big of a deal. For more sensitive data that would be migrated, we would come up with a more elegant solution that involves either HTTPS or SSH with keys of some kind (or even an Amazon S3 bucket through <a href="https://min.io/">Minio</a>).</p><p>Thankfully Python has an extremely simple HTTP server that can be stood up and accessed easily. From wherever we are hosting our backups, we would go into the directory where backups are stored and then run the following (for Python 2.X the command would be: <code>-m SimpleHTTPServer</code>).</p><pre><code>$ python -m http.server
Serving HTTP on :: port 8000 (http://[::]:8000/) ...</code></pre><p>With that up, we can now use <code>curl</code> to grab our Minecraft Server backups, but we will need a Container in our cluster to do this. I&apos;ve <a href="https://github.com/KyWa/blogs/tree/master/mineOps/files/Kubernetes/mineops-customer-manifests">created a new set of manifests</a> for deploying customer Minecraft Servers in a Kubernetes cluster. There are 2 manifests, a customer-init and a customer-run. &#xA0;The customer-init manifest will deploy a Pod that we can then use <code>kubectl exec -it customer-init -- bash</code> to get into an interactive terminal to the Pod and then run our <code>curl</code> and <code>tar</code> commands. If we were mass migrating customers, we would do something a little different, such as have a Pod for each customer that would run a script to curl the backup and then extract it for us as we spin up each namespace (and we might do this in the next part of the series). For showing a &quot;one off&quot;, this will do, but we will need to ensure when we do this, we have a clean environment, so we will start with a fresh namespace that has no running Minecraft Server (as if we were migrating a customer initially). We will have the PVC get created along with our Pod to be able to ensure a PVC is created in our namespace to be able to get data into it.</p><pre><code>$ kubectl create namespace mineops-0 &amp;&amp; kubectl create -f customer-init.yaml -n mineops-0
namespace/mineops-0 created
persistentvolumeclaim/mineops created
pod/customer-init created
$ kubectl exec -it -n mineops-0 customer-init -- bash
[root@customer-init /]# cd /mineops-data
[root@customer-init mineops-data]# curl -sL -o mineopsbackup.tgz http://somefileshare.com/backups/customer-0-server-1.tgz
[root@customer-init mineops-data]# tar -xf mineopsbackup.tgz
[root@customer-init mineops-data]# exit
$ kubectl delete pod -n mineops-0 customer-init
pod &quot;customer-init&quot; deleted</code></pre><p>We have now manually obtained our one customer backup and extracted it into the PVC in their namespace (mineops-0). The only thing left to do is create the rest of the objects so the Minecraft Server can spin up and the customer can start playing. Once the objects have applied and we get the Public IP for the Service, DNS can be updated to target that Load Balancer.</p><pre><code>$ kubectl create -f customer-run.yaml -n mineops-0
configmap/minecraft-server created
deployment.apps/mineops created
service/mineops created
$ kubectl get svc -n mineops-0 mineops
NAME              TYPE           CLUSTER-IP       EXTERNAL-IP      PORT(S)           AGE
service/mineops   LoadBalancer   10.245.225.155   143.244.222.81   25565:31568/TCP   9h</code></pre><p>The pod should be up and running and checking the logs should show that:</p><pre><code>$ kubectl logs -n mineops-0 mineops-64d95d95f4-kbpqf
[20:58:27] [Worker-Main-2/INFO]: Preparing spawn area: 42%
[20:58:27] [Worker-Main-2/INFO]: Preparing spawn area: 42%
[20:58:27] [Worker-Main-2/INFO]: Preparing spawn area: 42%
[20:58:27] [Server thread/INFO]: Time elapsed: 65986 ms
[20:58:27] [Server thread/INFO]: Done (66.234s)! For help, type &quot;help&quot;
[20:59:16] [Server thread/WARN]: handleDisconnection() called twice
[21:24:58] [Server thread/WARN]: handleDisconnection() called twice
[22:04:00] [User Authenticator #1/INFO]: UUID of player Hardcoreqq is fc138322-655d-4534-b351-7cb178e7c3da
[22:04:00] [Server thread/INFO]: Hardcoreqq[/10.108.0.3:35082] logged in with entity id 80 at (163.5, 67.0, 238.5)
[22:04:00] [Server thread/INFO]: Hardcoreqq joined the game
[22:04:06] [Server thread/INFO]: Hardcoreqq lost connection: Disconnected
[22:04:06] [Server thread/INFO]: Hardcoreqq left the game</code></pre><h2 id="wrapping-up">Wrapping Up</h2><p>We&apos;ve covered just about everything we need to know for using Kubernetes to deploy workloads (mainly Minecraft Server instances) for our customers and how to migrate them from somewhere else. There are many other topics in the Kubernetes world, but as long as you remember that each of them boils down to one of the big 3 (compute, networking and storage), it will make your lives all that much easier. </p><p>There are other areas that do relate to deploying Minecraft Servers on Kubernetes that we did not go into too much depth such as managing multiple namespaces for customers, but we will be discussing that in greater detail in the next and final part of this series. And remember whenever you have manifests or configuration files, always put it in Git!</p><h2 id="up-next">Up Next</h2><p>Since we have pretty much concluded our deployment to Kubernetes and have a path forward, our CEO seems to be pretty happy that all of his buzzwords have been implemented. We on the other hand now have to manually create namespaces and manifests as opposed to how we used to do all of this with Ansible even when using Containers. So it seems we still have some work to do, but fear not, there are similar tools for automating workloads on Kubernetes and we will be going over one of the major ones in the next part of this series.</p><p>Another item we will look at is how to we update the <code>server.properties</code> and restart our Minecraft Server to take in the new changes? Can we do it automatically or will we have to run <code>kubectl</code> commands? Find out the answer to this and more in the next and final installment of the mineOps series!</p><h3 id="series-links">Series Links:</h3><ul><li><a href="https://blog.kywa.io/mineops-part-1/">Part 1 - Manual Minecraft Server Installation</a></li><li><a href="https://blog.kywa.io/mineops-part-2/">Part 2 - Automating with Ansible</a></li><li><a href="https://blog.kywa.io/mineops-part-3/">Part 3 - Keeping it Altogether with Git</a></li><li><a href="https://blog.kywa.io/mineops-part-4/">Part 4 - Containers Have Joined the Party</a></li><li><a href="https://blog.kywa.io/mineops-part-5/">Part 5 - Making Containers Highly Available</a></li><li><a href="https://blog.kywa.io/mineops-part-6/">Part 6 - ArgoCD to the Rescue</a></li></ul><p></p>]]></content:encoded></item><item><title><![CDATA[Upcoming]]></title><description><![CDATA[<hr><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/mineops-6.png" class="kg-image" alt loading="lazy" width="383" height="78"></figure><p>The <a href="https://blog.kywa.io/mineops-intro/">mineOps series</a> is still ongoing and Part 5, Making Containers Highly Available, should be released here in the next two weeks or so. Following that the final part in the series, ArgoCD to the Rescue, will be out probably a week after. This final post will conclude the series</p>]]></description><link>https://blog.kywa.io/wdadwaawd/</link><guid isPermaLink="false">61f0045b1ec482d78d56ebbb</guid><category><![CDATA[newsletter]]></category><dc:creator><![CDATA[Kyle Walker]]></dc:creator><pubDate>Thu, 27 Jan 2022 04:21:30 GMT</pubDate><content:encoded><![CDATA[<hr><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/mineops-6.png" class="kg-image" alt loading="lazy" width="383" height="78"></figure><p>The <a href="https://blog.kywa.io/mineops-intro/">mineOps series</a> is still ongoing and Part 5, Making Containers Highly Available, should be released here in the next two weeks or so. Following that the final part in the series, ArgoCD to the Rescue, will be out probably a week after. This final post will conclude the series and follow more or less a typical journey a company and its engineers may take moving through the technology stack. </p><hr><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/tekton-horizontal-color.png" class="kg-image" alt loading="lazy" width="2000" height="641" srcset="https://blog.kywa.io/content/images/size/w600/2022/01/tekton-horizontal-color.png 600w, https://blog.kywa.io/content/images/size/w1000/2022/01/tekton-horizontal-color.png 1000w, https://blog.kywa.io/content/images/size/w1600/2022/01/tekton-horizontal-color.png 1600w, https://blog.kywa.io/content/images/size/w2400/2022/01/tekton-horizontal-color.png 2400w" sizes="(min-width: 720px) 720px"></figure><p>On the horizon the next few posts are in the wings and should pair well with those who followed along with the mineOps series or for those who are already on this journey. The upcoming posts will focus on <a href="https://tekton.dev">Tekton</a> and its use as a replacement for existing pipelines in your workflows specifically targeted at replacing Jenkins. <em>Please note that is me saying that and not a tagline from the Tekton group.</em></p><hr><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/terraform.png" class="kg-image" alt loading="lazy" width="480" height="240"></figure><p>And the next article coming after that will be focusing on <a href="https://www.terraform.io/">Terraform</a>, another great product from Hashicorp that has been used to great success for many companies since it was released. At first Terraform will appear to be no different than Ansible, however there is much more to it than this and we will dive into those similarities and differences.</p><hr>]]></content:encoded></item><item><title><![CDATA[mineOps - Part 4: Containers Have Joined the Party]]></title><description><![CDATA[<figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/mineops-3.png" class="kg-image" alt loading="lazy" width="383" height="78"></figure><p>In <a href="https://blog.kywa.io/mineops-part-3/">Part 3</a> we started to use Git to track our files for the company and then put these files in GitHub as our remote repository. Business is good, we&apos;ve got Minecraft Servers running and happy customers, but trouble is on the horizon. Despite things going well, our</p>]]></description><link>https://blog.kywa.io/mineops-part-4/</link><guid isPermaLink="false">61d3c1f5c040b4060b5bb041</guid><category><![CDATA[mineops]]></category><dc:creator><![CDATA[Kyle Walker]]></dc:creator><pubDate>Wed, 12 Jan 2022 05:24:03 GMT</pubDate><content:encoded><![CDATA[<figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/mineops-3.png" class="kg-image" alt loading="lazy" width="383" height="78"></figure><p>In <a href="https://blog.kywa.io/mineops-part-3/">Part 3</a> we started to use Git to track our files for the company and then put these files in GitHub as our remote repository. Business is good, we&apos;ve got Minecraft Servers running and happy customers, but trouble is on the horizon. Despite things going well, our CEO has heard about a buzzword called &quot;containers&quot; and its our job to make this a reality. So let&apos;s start this journey of containerizing our Minecraft Servers and ultimately cut-over our customers if we can make it work.</p><hr><h2 id="what-is-a-container">What is a Container</h2><p>Currently our company (in theory) is using virtual machines to host Minecraft Servers for our customers which is using a technology called &quot;Virtualization&quot;. It has been around for quite some time and has some amazing adoption as it really allows you to utilize the most out of your physical hardware. Containers are not virtual machines. This is a common misconception by most people making the jump to Containers, but is quite untrue. I know people in my industry who know this yet continue to treat their Containers like virtual machines and I hope during this guide we can cover why you wouldn&apos;t do that and how easy it is to go down that road.</p><p>Another misconception is that Containers are Docker Containers or that Docker is the only Container &quot;type&quot; out there. &#xA0;Docker has become synonymous with Containers just like the term SLI did for multiple GPUs (even if using AMD Crossfire) or Kleenex just meaning a tissue. This is unfortunate as there are other Container engines out there and this is something else I hope to dispel in this post.</p><p>We don&apos;t have to go over the entire history of Containers and what they are in reality (<a href="https://en.wikipedia.org/wiki/Linux_namespaces">kernel namespaces</a> which have been around in Linux since the turn of the millennium), but know that UNIX had their own equivalent (for the most part) developed back in 1979 and live by the early 80s. I will simply what they are for everyone&apos;s sake. Containers in theory are just another PID/Process on a server. Obviously there is a bit more to it than that, but it&apos;s best to think of them as processes. You can and will have more processes in side this Container, but the core of the Container should be thought of as a single process. Failing to do this will lead to bloat and have VM sized Containers and defeating a lot of the advantages to using Containers.</p><p>The large benefit of Containers over Virtual Machines is the overhead. A Virtual Machine is an entire Operating System with all of its libraries and software making its &quot;package&quot; the size of, well , an Operating System. If you wanted to run an Apache Web Server (<code>httpd</code> for the cool kids) on a Virtual Machine you would have to have an entire Virtual Machine or a physical server with Apache installed and then your website/web application along with it. A Container on the other hand can be as small as the actual application and data itself and nothing more. In the example of Apache, this could be as small as 10MB as opposed to the same VM with Apache installed being probably 4GB+. The benefits of containers is far more than just the size of the &quot;package&quot; (Virtual Machine vs Container Image) and hopefully we will see this benefit here shortly as we go through this guide.</p><h3 id="container-images">Container Images</h3><p>In the previous sentence I mentioned a new concept called a Container Image. If a Virtual Machine is comprised up of an entire Operating System and its installed software, a Container Image is just a packaged application and its dependencies along with information on what processes it runs when launched. Container Images typically tend to be quite small, but when starting out most people end up with larger Container Images as they learn how to build their own. We will go over building our own Minecraft Server Container Image later in this guide.</p><p>One interesting thing about Container Images (and other things in the modern tools and applications) is the concept of a <code>tag</code>. With Container Images since they are just a packaged application you can use <code>tags</code> to identify what version of your application a Container Image has. The format looks like this: <code>minecraft-server:1.18.1</code>. If a tag is not specified (both when building an Image or running a Container) the <code>tag</code> called <code>latest</code> will be used in its place. We will go over <code>tags</code> and <code>latest</code> later in this guide.</p><p>For Container Images, all we need to know now is that its a packaged application, which we can tag to show the version inside. For us we will end up building a Minecraft Server image.</p><h3 id="so-what-is-docker-then">So what is Docker then?</h3><p>Previously we stated that Docker != Containers and that Containers != Docker, but anytime we look at something related to Containers we see Docker. Why is that the case if the concept of Containers has been around longer than Docker the company? The reason Docker is so synonymous with Containers is that it was because of Docker (both the company and the application) that brought Containers to the forefront of IT nearly a decade ago now. A talk was made and people started using Containers through Docker (the application/engine) and here we are today, however Docker isn&apos;t the only Container &quot;runtime&quot; in town.</p><p>Without doing a large deep dive on the concept of what a <a href="https://opensource.com/article/21/9/container-runtimes">Container Runtime</a> is, essentially its the &quot;low level&quot; program that runs Containers. Now we will discuss what a Container Engine is. Container Engines are applications we humans can use to manage, package and run containers (Container Engines come with their own Container Runtime). Docker is one of these Container Engines (and arguably the most popular by far) and for sure the one most widely available for Desktop Operating System usage. But in the wise words of Yoda...</p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/yoder.jpeg" class="kg-image" alt loading="lazy" width="640" height="360" srcset="https://blog.kywa.io/content/images/size/w600/2022/01/yoder.jpeg 600w, https://blog.kywa.io/content/images/2022/01/yoder.jpeg 640w"></figure><p><a href="https://podman.io/">Podman</a> is fairly new to the scene of Container Engines, but it comes packed with a ton of features. For most beginners though, these features wont be realized until growing a little more in your journey with Containers. When it comes to developing Container Images and running Containers both Podman and Docker ultimately do the same thing, but how they do it is slightly different. </p><p>Docker runs in a daemon which whereas Podman has a daemon-less architecture. On the surface this may not seem like something important or meaningful, but it does grant some nice functionality mostly in the security space. Since Docker runs as a daemon (and that daemon runs as <code>root</code>) the containers it spins up run as the root user. Podman since it doesn&apos;t have a daemon it runs as, can spin up Containers as the user who spun up the Container as opposed to the root user. Docker did however recently add in this functionality, but Podman did it first and has been pushed as one of the primary features.</p><p>There is no right answer and for the sake of running and building Containers either will work for what we are doing here. Now for most people Docker Desktop is going to be the easiest option to get up and running as it can be installed on the two major Desktop Operating Systems whereas Podman can really only be installed on a Linux based OS. For our tests we will be using Docker, but the commands between Docker and Podman will be nearly a 1:1 mapping. Now let&apos;s get started actually using Containers!</p><h2 id="installing-docker">Installing Docker</h2><p>As with most applications we need to get it installed before we can use it. For Docker Desktop all you need to do is visit <a href="https://www.docker.com/products/docker-desktop">https://www.docker.com/products/docker-desktop</a> and click the link for your specific Operating System. Note if you are using a Linux based OS for your Desktop or using a Linux based server, installing Docker may be a little different depending on your OS. Here are some links for each of the major Linux distributions and how to install Docker on each of them:</p><ul><li><a href="https://docs.docker.com/engine/install/fedora/">Fedora</a></li><li><a href="https://docs.docker.com/engine/install/centos/">CentOS 8</a></li><li><a href="https://docs.docker.com/engine/install/ubuntu/">Debian and Ubuntu</a> variants</li></ul><p>Then just go through whichever installation method your OS uses and you are pretty much done. Just a side note for Mac and Windows that this installs a minimal Virtual Machine (yes I see the irony that it uses a Virtual Machine on an OS to then use Containers) for Docker to run in.</p><p>If you are using Linux and wish to not go through the various install guides for Docker, you can install Podman through your package manager with the package name being: <code>podman</code></p><h2 id="how-to-use-containers-docker-desktop">How to use Containers (Docker Desktop)</h2><p>Once Docker Desktop is installed you can open a Terminal (Mac) or Command Prompt/WSL (Windows) to start using Docker. If you are on Linux, most likely you already have a Terminal open because you are awesome.</p><p>All commands will be run as <code>docker</code> or <code>docker.exe</code> on Windows, although I believe Docker Desktop on Windows if you have WSL installed you can just run <code>docker</code> from WSL. </p><p>For those curious about Podman or are going to use Podman all the commands we will go over will be a 1:1 with Docker. If we do run commands that are not 1:1 I will be sure to call them out explicitly.</p><h4 id="common-commands">Common Commands</h4><ul><li><code>docker run httpd:latest</code> - Runs a container from the image <code>httpd</code> and tag</li><li><code>docker stop &lt;container-name&gt;</code> - Stops a running container</li><li><code>docker rm &lt;container-name&gt;</code> - Removes a stopped container</li><li><code>docker ps -a</code> - Lists all containers running or stopped</li><li><code>docker images</code> - Lists all container images</li><li><code>docker start &lt;container-name&gt;</code> - Start a stopped or created container</li><li><code>docker logs &lt;container-name&gt;</code> - View the logs of a container</li></ul><h3 id="running-a-container">Running a Container</h3><p>As with most training we must do the inevitable &quot;Hello World&quot; so lets just get it over with. I do see the irony of constantly saying no to typical &quot;Hello World&quot; tutorials, but this one actually helps quite a bit as you will see. From your terminal throw this command into it and hit enter:</p><pre><code>$ docker run hello-world
Unable to find image &apos;hello-world:latest&apos; locally
latest: Pulling from library/hello-world
2db29710123e: Pull complete
Digest: sha256:2498fce14358aa50ead0cc6c19990fc6ff866ce72aeb5546e1d59caac3d0d60f
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the &quot;hello-world&quot; image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/
</code></pre><p>See that wasn&apos;t so bad now was it? Heck this version of a &quot;Hello World&quot; even told us what it did, which is way cooler than most &quot;Hello World&quot; tutorials. As we can see in the output the Docker command we ran did the items above:</p><ul><li><code>docker</code> contacted the Docker daemon (remember it runs as a daemon)</li><li>Docker daemon pulled the <code>hello-world</code> image from Docker Hub. How and why it did that will be explained later, but notice the first few lines after the command ran and see if you can gather what occurred.</li><li>The Docker daemon created a new container from the Image it pulled and executed a command to spit out the output we saw</li><li>This output was streamed via <code>stdout</code> to our terminal</li></ul><p>Awesome, so we are done now right? We have now run a Container to tell us it ran a Containerized application so lets tell the CEO to get off our back. Sadly I think we all know we aren&apos;t there yet. Let&apos;s try something else with our <code>docker run hello-world</code> command to showcase something closer to what we will be doing in the &quot;real world&quot;. We will be running the same command and passing in a <code>-d</code> flag which runs the Container in a &quot;detached&quot; state. Without this flag our Container will run and any <code>stdout</code>, <code>stderr</code> or output the container creates will be spit out to our terminal. Detached Containers however (which is how all Containers are run in the real world) have a slightly different output. Let&apos;s check it out:</p><pre><code>$ docker run -d hello-world
6730c8a06b4b68c078d949cd230e44f301b6f356a3403628e75cb2ff7e5c38f1

$ docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

$ docker ps -a
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
6730c8a06b4b   hello-world   &quot;/hello&quot;   3 seconds ago   Exited (0) 3 seconds ago             priceless_khorana

$ docker logs 6730c8a06b4b

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the &quot;hello-world&quot; image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/</code></pre><p>A few things were done here, but its important to see the flow and how we got there. </p><ol><li>We ran <code>docker run -d hello-world</code> and got a long hash (kind of similar to the Git <code>commit</code> hashes we saw in the previous post), but no output like before</li><li><code>docker ps</code> showed no running containers</li><li><code>docker ps -a</code> which lists all containers showed us a container with the <code>Exited</code> status. Exited is an ok status and a good thing in most cases. More on this later</li><li>Through <code>docker logs</code> we were able to see the output that container gave us the first time we ran it.</li></ol><p>Since we ran this Container in a &quot;detached&quot; state the logs weren&apos;t just shown in our terminal, which for the sake of finding out whats going on may seem bad. When it comes to running containers, you want them to run in the background and not keep a terminal actively streaming output (because it can get noisy and wastes your ability to use the terminal). We&apos;ve seen some basics around running a Container, how to run them in a &quot;detached&quot; stated and get logs from Containers.</p><p>Some of this may seem silly, but we will pull all of this together later on in a following section.</p><h3 id="working-with-images">Working with Images</h3><p>In the first run of the <code>docker run hello-world</code> command we saw some output from Docker:</p><pre><code>Unable to find image &apos;hello-world:latest&apos; locally
latest: Pulling from library/hello-world
2db29710123e: Pull complete
Digest: sha256:2498fce14358aa50ead0cc6c19990fc6ff866ce72aeb5546e1d59caac3d0d60f
Status: Downloaded newer image for hello-world:latest</code></pre><p>When you attempt to run an image, Docker will check to see if it held locally in its image storage, if it does not find that image it will attempt to pull it from a &quot;Registry&quot; which in the case of Docker will only ever be <a href="https://hub.docker.com/">Docker Hub</a>. I do want to point out that <a href="https://github.com/containers/podman/blob/main/test/registries.conf">Podman gives you the ability to set</a> &quot;Registries&quot; to search from and pull images from. Let&apos;s not get to hung up on &quot;Registries&quot; just yet as we have a whole section dedicated to them coming up.</p><p>Notice how we ran <code>docker run hello-world</code> and the output said <code>Unable to find image &apos;hello-world:latest&apos; locally</code>? The <code>:latest</code> tag (which we briefly went over) was appended to the image we attempted to run, why is that? If a <code>tag</code> is not specified when doing anything at all with Containers, the Container engine will use the tag <code>latest</code> to specify the &quot;latest, last&quot; <code>tag</code> of the requested image. Nothing really more to say on that except possibly that using <code>latest</code> may not always have the best results. It is best to always specify the <code>tag</code> you wish to use to ensure you have a repeatable environment. What if an image you rely on gets a new major update and the <code>latest</code> has some major changes you weren&apos;t expecting? Let&apos;s be honest, we should all have a dev environment to test things like this prior to pushing to production, but let&apos;s be even more honest and admit hardly anyone does that. By the end of this series we will be able to do that and remove a lot of potential headaches in our lives.</p><p>Container Images like <code>hello-world</code> exist to be consumed or expanded upon through a Build (next section we get all up in that). The <code>hello-world</code> image is quite basic and doesn&apos;t do a whole lot, but it does help us see what is going on. So we know a Container Image is just a packaged application with a command it runs when a Container starts with that image, but how do we get these images? Docker will try to pull any images it doesn&apos;t have when you try to run a Container, but you can pull Images on your own if you wish to do things with them. The <code>docker pull</code> command does just this and if you aren&apos;t sure what to <code>pull</code> you can run a <code>docker search minecraft</code> to see what&apos;s out there. This command will pull down an image to your local Docker (or Podman) image store, let&apos;s see it in action (side note I have an alias for <code>docker image</code> as <code>dimage</code> because typing is hard):</p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/docker-pull.gif" class="kg-image" alt loading="lazy" width="1940" height="848" srcset="https://blog.kywa.io/content/images/size/w600/2022/01/docker-pull.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/01/docker-pull.gif 1000w, https://blog.kywa.io/content/images/size/w1600/2022/01/docker-pull.gif 1600w, https://blog.kywa.io/content/images/2022/01/docker-pull.gif 1940w" sizes="(min-width: 720px) 720px"></figure><p>In the previous commands we viewed our local images, searched for any images at <a href="https://hub.docker.com/">Docker Hub</a> containing the name <code>minecraft</code>, we pulled down an image named <code>itzg/minecraft-server</code> and then <code>pulled</code> the image to our local image store (notice the line at the beginning that said <code>Using default tag: latest</code>). Take note that unlike the <code>hello-world</code> image there is a prefix name, <code>itzg</code> with a trailing <code>/</code> before <code>minecraft-server</code>. </p><p>We will dig more into this in the &quot;Storing Images&quot; section in a moment, but for now all you need to know is that image <code>minecraft-server</code> we pulled came from the repository of <code>itzg</code> hosted in the Docker Registry of <a href="https://hub.docker.com/">Docker Hub</a>. There is one final command we will run to work with Images (outside of running or building them) and that is <code>docker inspect</code>. Note that Podman has an <code>inspect</code> as well, but there is a powerful tool called <code><a href="https://github.com/containers/skopeo">skopeo</a></code> that can do <code>inspect</code> on remote images without having to pull them. Let&apos;s look at what <code>inspect</code> does on the <code>hello-world</code> image as opposed to the <code>itzg/minecraft-server</code> image we just downloaded.</p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/docker-inspect.gif" class="kg-image" alt loading="lazy" width="2000" height="1008" srcset="https://blog.kywa.io/content/images/size/w600/2022/01/docker-inspect.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/01/docker-inspect.gif 1000w, https://blog.kywa.io/content/images/size/w1600/2022/01/docker-inspect.gif 1600w, https://blog.kywa.io/content/images/2022/01/docker-inspect.gif 2334w" sizes="(min-width: 720px) 720px"></figure><p>As you can see, there is quite a bit of output from <code>docker inspect</code> that we don&apos;t really need to go over here, but as you start to use Containers more, seeing what is in an Image and what makes it tick can be quite helpful. Now that we&apos;ve pulled an Image from somewhere else that was built already, let&apos;s see what it takes to build our own Image.</p><h4 id="building-images">Building Images</h4><p>We know how to get images via <code>pull</code> or indirectly through <code>run</code>, but what if the image we want (or think we want) doesn&apos;t exist? Enter Image Building 101 and it all starts with something called a <code>Dockerfile</code> and yes it is capitalized. Let&apos;s take a look at the <code>Dockerfile</code> of the <code>hello-world</code> image which can also be found <a href="https://github.com/docker-library/hello-world/blob/master/amd64/hello-world/Dockerfile">here</a>.</p><pre><code>$ cat Dockerfile
FROM scratch
COPY hello /
CMD [&quot;/hello&quot;]</code></pre><p>The <code>Dockerfile</code> above is quite simple and straight forward. Most people even those not in &quot;tech&quot; should be able to somewhat decipher what is going on here. Just as a side note, <code>Dockerfiles</code> have <a href="https://docs.docker.com/engine/reference/builder/">their own syntax</a> which is broken down like this: <code>INSTRUCTION argument</code>. But let&apos;s break down the one we have:</p><ul><li><code>FROM scratch</code> - <code>FROM</code> tells Docker/Podman which Image to use as the &quot;base&quot; for this build. The <code>scratch</code> image is the tiniest of images and is nothing more than an executable environment for, well, executables. </li><li><code>COPY hello /</code> - <code>COPY</code> does exactly what it sounds like. It copies the file <code>hello</code> to <code>/</code> in the Image being built. Normal <code>COPY</code> behavior is to look in the current directory where you initiated the <code>docker build</code>. We will for sure dive into this in just a moment</li><li><code>CMD [&quot;/hello&quot;]</code> - <code>CMD</code> is what we spoke about earlier about instructions on what to do when an Image is run. In this case its running <code>/hello</code> or in a different way of looking at it as <code>./hello</code>.</li></ul><p>We are going to build our own <code>Dockerfile</code> for Minecraft Server and we will use all of the above <code>Dockerfile</code> Instructions and showcase some new ones. Let&apos;s get our <code>Dockerfile</code> sorted for a Minecraft Server. We know that we need Java, so we will need to look for a Container Image that has Java in it. We know that we need at least Java version 17 and we can do this via <code>docker search</code> or checking through the GUI of <a href="https://hub.docker.com/">Docker Hub</a>. Either way will work, but for looking through Images it may be easier to use the GUI at Docker Hub as the <code>docker search</code> command doesn&apos;t have the ability to look through tags.</p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/docker-gui-browse.gif" class="kg-image" alt loading="lazy" width="1316" height="932" srcset="https://blog.kywa.io/content/images/size/w600/2022/01/docker-gui-browse.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/01/docker-gui-browse.gif 1000w, https://blog.kywa.io/content/images/2022/01/docker-gui-browse.gif 1316w" sizes="(min-width: 720px) 720px"></figure><p>As you can see, there were other options newer than 17, but just like I mentioned a while ago, using <code>latest</code> or the newest version of something may not always be a good idea. We know for a fact that <code>openjdk-17</code> works for our server and our customers, so lets begin with openjdk 17. You may have also noticed there were multiple tags/versions of the same <code>openjdk</code> version such as &quot;oraclelinux&quot;, &quot;slim&quot; and may have even noticed there were some Images with a <code>windows</code> architecture:</p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/arch-versions.png" class="kg-image" alt loading="lazy" width="1237" height="409" srcset="https://blog.kywa.io/content/images/size/w600/2022/01/arch-versions.png 600w, https://blog.kywa.io/content/images/size/w1000/2022/01/arch-versions.png 1000w, https://blog.kywa.io/content/images/2022/01/arch-versions.png 1237w" sizes="(min-width: 720px) 720px"></figure><p>We most certainly do not want Windows based Containers for multiple reasons the most obvious being they wont run on our platforms and the second being of their size in comparison. This is something to pay attention to when looking for Container Images to utilize.</p><p>So let&apos;s start our Dockerfile with the Image and <code>tag</code> we found:</p><figure class="kg-card kg-code-card"><pre><code class="language-Dockerfile"># Our base/starting Image
FROM openjdk:17.0.1-jdk-oraclelinux8

# Creating a dir for Minecraft Server
RUN mkdir /minecraft

# Telling Docker this is where we &quot;live&quot; currently. Similar to our &quot;current directory&quot; in Linux/Unix, all further commands &quot;take place&quot; in WORKDIR, which for us is &quot;/minecraft&quot;
WORKDIR /minecraft

# Copying over our files (eula and server.jar) to our /minecraft dir which is known due to the previous WORKDIR Instruction
# We will talk about server.jar being here in a moment
COPY eula.txt .
COPY server.jar .

# And the final line of our Dockerfile is &quot;what to do when I&apos;m run&quot;. Which this should look very similar to what we did way back in our manual setup of Minecraft Server.
CMD [&quot;java&quot;, &quot;-jar&quot;, &quot;server.jar&quot;]</code></pre><figcaption>Dockerfile</figcaption></figure><p>We have our Dockerfile (with some in-line comments to help identify some of the options) and we can technically go ahead and do a <code>docker build</code>, but first lets take a look at our directory.</p><pre><code>$ ls
Dockerfile  eula.txt    server.jar</code></pre><p>We have our <code>eula.txt</code> and the <code>server.jar</code> which I downloaded manually. We can have our Dockerfile go and do this for us, but it gets very complex in a Dockerfile if we start doing large commands inside of it (think of our long <code>curl</code> command to go grab the latest Minecraft Server <code>.jarfile</code>). There are ways to make this work which we will look at later. So let&apos;s try to build this Image and see what happens:</p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/docker-build.gif" class="kg-image" alt loading="lazy" width="2000" height="1008" srcset="https://blog.kywa.io/content/images/size/w600/2022/01/docker-build.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/01/docker-build.gif 1000w, https://blog.kywa.io/content/images/size/w1600/2022/01/docker-build.gif 1600w, https://blog.kywa.io/content/images/2022/01/docker-build.gif 2334w" sizes="(min-width: 720px) 720px"></figure><p>Let&apos;s look at the command that we ran to build this Container Image and examine each component: </p><pre><code>$ docker build -t mineops:17 .</code></pre><ul><li><code>docker build</code> - This first portion tells Docker we are going to build a Container Image from a Dockerfile</li><li><code>-t mineops:17</code> - We have told Docker the Image that is built is to be named <code>mineops</code> with the tag <code>:17</code>. This tag was chosen to represent <code>openjdk17</code></li><li><code>.</code> - This tells Docker the location of our Dockerfile. You can specify where your Dockerfile is and in a later article in this series we will discuss doing that. For now, building where you are works fine</li></ul><p>So with that explanation out of the way, it looks like we built our Minecraft Server Container Image. Now the real test, does it work?</p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/docker-mineops-test.gif" class="kg-image" alt loading="lazy" width="1280" height="378" srcset="https://blog.kywa.io/content/images/size/w600/2022/01/docker-mineops-test.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/01/docker-mineops-test.gif 1000w, https://blog.kywa.io/content/images/2022/01/docker-mineops-test.gif 1280w" sizes="(min-width: 720px) 720px"></figure><p>Looks like it does and we are one step closer to actually having a production ready Minecraft Server Container Image made. The command that was run to start this image was:</p><pre><code>$ docker run -d --name mineops -p 25565:25565 mineops:17
$ docker logs -f mineops</code></pre><p>A few new flags got introduced here via <code>--name</code> and <code>-p</code>. The <code>--name</code> flag starts the Container with a specific name, in this case it was <code>mineops</code> as opposed to the random generated name given by Docker. The <code>-p</code> flag exposes a port (or port range if needed) from the Container to the Docker host (our local machine in this instance). As we know from the first couple of installments of the mineOps series, Minecraft Server defaults to port 25565 to receive incoming connections. With <code>-p 25565:25565</code> the syntax is <code>host:container</code> in how the ports are exposed/mapped. If we wanted to have our customers access their server over <code>25575</code> instead, we could either change the <code>server.properties</code> file (not immediately easy to do in a Container) or we could just change the port the Minecraft Server is exposed on the host. </p><p>One final thing to note here and this is a big one, Minecraft Server did create the usual files we&apos;ve seen time and again inside the Container when it started. However Containers by default use &quot;ephemeral&quot; storage, which means that the files/data inside the Container are lost when the Container is deleted. We will be looking how to remedy this and keep our data towards the end of this article.</p><h3 id="storing-images">Storing Images</h3><p>Now that we have built a Container Image and it is on our local machine, I guess we can just ship our machine to the customers that need a Minecraft Server right? Nonsense, but it is a funny <a href="https://external-preview.redd.it/aR6WdUcsrEgld5xUlglgKX_0sC_NlryCPTXIHk5qdu8.jpg?auto=webp&amp;s=5fe64dd318eec71711d87805d43def2765dd83cd">meme</a> about how Docker was born. So if we aren&apos;t going to send customers our laptops to use, where do we put a Container Image so our servers that have Docker/Podman installed can run it?</p><p>Remember how I mentioned Docker will pull Images from a Registry? Well those images got there somehow and now we will learn what the somehow is along with how the <code>itzg/minecraft-server</code> image had <code>itzg/</code> in front of it, but the <code>hello-world</code> image did not. So what is a Container &quot;Registry&quot;? Much like a Git repository, a Container Registry stores Container Images and the various tags of those images. As you can imagine given that our one <code>mineops:17</code> image we built is ~500MB, a Container Registry if it holds all of these images and versions/tags it would need a massive amount of storage to &quot;contain&quot; all of this right? Well, obviously it does need a decent amount of storage, but thanks to some fancy built in technology called &quot;layers&quot; not every single version and layer is uniquely stored on disk somewhere.</p><p>Layers, much like Container Images (and Git <code>commits</code>), have what is known as a hash or SHA. Each unique Layer is stored, but Images (and their versions/tags) that share certain layers do just that, share it. When you pull down a specific <code>tag</code> of an Image from a Registry and you already have an Image locally that shares a few of the layers (rhyming is fun eh?) only the differencing layers are pulled down to give you the new Image <code>tag</code> you pulled. Let&apos;s see this in action:</p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/docker-layers-1.gif" class="kg-image" alt loading="lazy" width="2000" height="1008" srcset="https://blog.kywa.io/content/images/size/w600/2022/01/docker-layers-1.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/01/docker-layers-1.gif 1000w, https://blog.kywa.io/content/images/size/w1600/2022/01/docker-layers-1.gif 1600w, https://blog.kywa.io/content/images/2022/01/docker-layers-1.gif 2334w" sizes="(min-width: 720px) 720px"></figure><p>In that example, we pulled the Container Image <code>nginx:1.21.4</code> and <code>nginx:latest</code> to showcase the idea of reusable/shared layers. There was one line after pulling the second image that stated <code>Already exists</code>. These Images only shared the first layer with each other (the &quot;base&quot; layer), but some images may share more than others. Typically in your own builds quite a few of the layers will be shared. Each layer comes from an &quot;Instruction&quot; i.e. <code>RUN yum install java</code> in a Dockerfile, so the changes they are unique is higher, but not in our own environment as we release newer versions of our images.</p><p>So for our Image we built, <code>mineops:17</code>, we need to push this somewhere like Docker Hub. As a side note for my personal Image Registry, I use <a href="https://quay.io">Quay.io</a> as it works for me and provides features I like that Docker Hub doesn&apos;t. For this series we will just use Docker Hub, but I would suggest checking out Quay.io as it has some pretty awesome features once you get to using Containers more often.</p><p>First and foremost we will need a login as it isn&apos;t anonymous. When you create your account at <a href="https://hub.docker.com">Docker Hub</a>, the username you choose will be the prefix name of your repository, just like what we saw with <code>itzg/minecraft-server</code>. A Container Image Location is typically broken down like this:</p><figure class="kg-card kg-code-card"><pre><code>docker.io/itzg/minecraft-server:latest

docker.io = Container Registry

itzg/minecraft-server = Container Image Repository

:latest = Image Tag</code></pre><figcaption>Docker Image Location Breakdown</figcaption></figure><p>Once you&apos;ve created your account and chosen your username, we can get started. I created a new account on Docker Hub for this with the name &quot;mineops&quot;. The image we built previously was also named &quot;mineops&quot;, so to avoid a duplication, we are going to do some magic with <code>docker tag</code>.</p><pre><code>$ docker images mineops
REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
mineops      17        e4b0329ca176   9 hours ago   517MB

$ docker tag mineops:17 mineops/minecraft-server:17

$ docker images
REPOSITORY               TAG  IMAGE ID       CREATED        SIZE
mineops                  17   e4b0329ca176   9 hours ago    517MB
mineops/minecraft-server 17   e4b0329ca176   9 hours ago    517MB</code></pre><p>Notice how we have a new Image in our list, but every other bit of information is the same? All we did with the command <code>docker tag</code> was create a &quot;pointer&quot; locally. Let&apos;s try to login and see if we can push an image to Docker Hub.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.kywa.io/content/images/2022/01/docker-login-push.gif" class="kg-image" alt loading="lazy" width="1681" height="724" srcset="https://blog.kywa.io/content/images/size/w600/2022/01/docker-login-push.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/01/docker-login-push.gif 1000w, https://blog.kywa.io/content/images/size/w1600/2022/01/docker-login-push.gif 1600w, https://blog.kywa.io/content/images/2022/01/docker-login-push.gif 1681w" sizes="(min-width: 720px) 720px"><figcaption>docker login &amp;&amp; docker push mineops/minecraft-server:17</figcaption></figure><p></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.kywa.io/content/images/2022/01/docker-hub-repo.png" class="kg-image" alt loading="lazy" width="1280" height="741" srcset="https://blog.kywa.io/content/images/size/w600/2022/01/docker-hub-repo.png 600w, https://blog.kywa.io/content/images/size/w1000/2022/01/docker-hub-repo.png 1000w, https://blog.kywa.io/content/images/2022/01/docker-hub-repo.png 1280w" sizes="(min-width: 720px) 720px"><figcaption>Our new Image repo: mineops/minecraft-server</figcaption></figure><p>We can pull Images, run Containers, build Images, push Images to a registry and we&apos;ve just about covered every use-case for normal Container functionality for developing with Containers. Now there is one last thing to discuss before move on to building production grade Container Images for our customers to run their Minecraft Servers on. </p><p>Earlier I had mentioned about targeting a specific <code>tag</code> and not <code>latest</code> because you can ensure the version you are targeting will work for your use. Well if we build another image with some changes and <code>tag</code> it as <code>:17</code> just like we did before and <code>push</code> it to a registry, its no longer the same image, but it is the same <code>tag</code>. This isn&apos;t something you will typically see happen with &quot;Official&quot; images from big name development companies, but it is something to be aware of. </p><p>There is a workaround for this and that is using the hash/SHA (formally known as the &quot;digest&quot;) of the Image you know is good. You can get the digest of your specific Image (if its on your local image storage) by running a simple command:</p><pre><code>$ docker inspect mineops/minecraft-server:17  | jq -r &apos;.[].RepoDigests&apos;
&quot;mineops/minecraft-server@sha256:0e5242fbae2abf68df82f14a19d80f5e64d1afcc8241c84e2fe8e554831076d7&quot;</code></pre><p>You can use this digest in your <code>docker run</code> commands as well as in your Dockerfiles in the <code>FROM</code> line just as with any other image. You can basically think of the <code>@sha256:longhash</code> as a <code>git commit</code> hash since it targets a specific and unique point in history for the Image (just like a Git <code>commit</code>). This is just something to think about as you begin to use Container Images and not accidentally cause an outage because someone pushed to the same <code>tag</code> as before.</p><h2 id="making-the-ceo-happy">Making the CEO Happy</h2><p>The CEO has given us our time to learn about Containers and wants us to start onboarding customers and move existing customers to Containers. Obviously we have a little bit further to go, especially as he also that we should migrate our customers to Containers. The last thing he left us with was to not waste too much time on this effort. During a conference he heard a new buzzword that he wants to drop on us and some of you may know what that buzzword is (hint: it starts with a K). </p><h3 id="getting-existing-data-into-the-container">Getting Existing Data into the Container</h3><p>We have a functional image via <code>mineops/minecraft-server:17</code> we can use to test a few items before we move to our &quot;final&quot; Image, so let&apos;s get to it. We need to see how to get Docker to target existing data and &quot;mount&quot; it into the Container. Thankfully there isn&apos;t much to this and is a simple flag known as <code>-v</code>. This flag, similarly to the <code>-p</code> flag from above, mounts a host path or a Docker Volume into the Container. There are reasons to use either option, but for now we will focus on the host path variety as its easier to manage and &quot;see&quot; normally. Docker Volumes do have their place, but given that we know the CEO wants us to move to something else soon, we will stick with the easier of the two.</p><p>Our current customers have servers deployed through Ansible on a few servers such as <code>mineops-0.kywa.io</code>. These Minecraft Servers have their data stored in <code>/minecraft</code> and we are aware of all the files contained within, but if we are going to migrate customers we need to get this data into a Container. Since we are taking backups through Ansible lets go grab a copy of <code>mineops-0.kywa.io</code> and bring it to our local machine. Once we have the <code>/minecraft</code> directory contents on our local machine, we can begin testing out the <code>-v</code> flag for <code>docker run</code>.</p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/docker-volume.gif" class="kg-image" alt loading="lazy" width="2000" height="1008" srcset="https://blog.kywa.io/content/images/size/w600/2022/01/docker-volume.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/01/docker-volume.gif 1000w, https://blog.kywa.io/content/images/size/w1600/2022/01/docker-volume.gif 1600w, https://blog.kywa.io/content/images/2022/01/docker-volume.gif 2334w" sizes="(min-width: 720px) 720px"></figure><p>And as we can see by using <code>tail</code> on the log of the local file (not the Container log) we can verify we are using this local data and not the ephemeral storage of the Container. So now we have a way to keep our data and still back it up in a normal fashion (aka its on a filesystem we can see). If we were going to use Docker on its own, we would absolutely use Docker Volumes for this, but we aren&apos;t, so no need to really dive into this.</p><h3 id="automating-containers">Automating Containers</h3><p>Since we know how to use existing data for our Minecraft Servers, all we need to do is find out where we are going to store this data. The logical solution here on our new server running Docker (this is a fictitious server and we aren&apos;t creating another VM), would be to create a <code>/minecraft</code> directory as we always do, but to create a sub-folder for each customer&apos;s Minecraft Server data. If we look back at our <a href="https://github.com/KyWa/blogs/tree/master/mineOps/files/Ansible/modular_ansible/inventory">Ansible vars</a> we can come up with a naming scheme for something like this per customer server: <code>/minecraft/customerID/server0</code> for however many Minecraft Servers they happen to have. </p><p>So now we have a way to get existing customers into Containers by migrating their data to the Docker server and into the directory dedicated to that Minecraft Server. For new customers who want custom settings for <code>server.properties</code> or anything else, how do we go about getting them setup? With a &quot;vanilla&quot; Docker setup like we are using, BASH scripts are going to be a quick and easy friend for us, but we will &#xA0;use our original Ansible playbooks and role as its what the business is currently using for everything else.</p><p>Obviously from what we&apos;ve seen and learned from the Ansible deployment of a Minecraft Server to the Container deployment of a Minecraft Server they aren&apos;t exactly the same, so we will need to modify our existing Ansible to work with what we have. Since we have been tasked with migrating existing customers as well we will need to create some Ansible to copy their Minecraft Server files and data over to a new place where we will run Containers. I have created a basic script that will do more or less this for us:</p><figure class="kg-card kg-code-card"><pre><code class="language-yaml">---
###
#
# Playbook will be used to convert existing Minecraft Servers to Container Hosts
#
###
- hosts: all
  gather_facts: false
  tasks:
  - name: Container Prerequisites
    include_role:
      name: minecraft
      tasks_from: container-prep.yml

  - name: Stop existing server
    systemd:
      name: minecraft
      state: stopped
    ignore_errors: true

  - name: Container Create
    include_role:
      name: minecraft
      tasks_from: container-create.yml

  - name: Copy existing customer server
    copy:
      src: &quot;{{ minecraft_directory }}&quot;
      dest: &quot;/opt/{{ container_path }}/{{ item.0.key }}/{{ item.1 }}&quot;
      remote_src: true
    run_once: true
    loop: &quot;{{ containers | dict2items | subelements(&apos;value&apos;) }}&quot;
</code></pre><figcaption>migrate.yml</figcaption></figure><p>The above Ansible Playbook should be easy to read and see what is going to happen. You may notice, if you remember all that was done from <a href="https://blog.kywa.io/mineops-part-2/">Part 2</a>, the <code>include_role</code> for <code>minecraft</code>, but the <code>tasks_from</code> is a new line. This allows you to call a specific <code>tasks/file.yml</code> inside of a Role as opposed to letting it default to <code>tasks/main.yml</code> inside the Role. This can get quite powerful, but be warned you can start creating bloated roles if you have just a bunch of <code>tasks</code> that just get called on their own and don&apos;t tie into the core Role function. Thankfully these <code>tasks</code> do tie into the Role&apos;s core function of deploying Minecraft. On their own we see 2 new ones: <code>container-prep.yml</code> and <code>container-create.yml</code>.</p><ul><li><code>container-prep.yml</code> - This task installs the required tools and packages to run Containers on a Linux host. For us we are repurposing our existing servers as there was no need to provision new VMs</li><li><code>container-create.yml</code> - This is nearly identical to our original <code>server-prep.yml</code> adds the logic of <code>start-server.yml</code>. The focus is creating directories for our customers to store their Minecraft Servers since we can now through Containers (more easily anyways) run multiple instances per VM. This could have been done originally with Ansible, but required a little more &quot;engineering&quot; that wasn&apos;t required at the time</li></ul><p>In theory all we need to do is alert our customers of upcoming maintenance and run <code>playbooks/migrate.yml</code> and voila we are good to go! Now with this, there is one more caveat. Previously with 1 Minecraft Server per server customer ports for Minecraft Server (the default was 25565) it didn&apos;t matter if they just used the default port. Now if we have more than one customer, there can only be one port unique port per server (I hope we knew this). This creates a new challenge for us as we automate. We need to try to keep a running list of which servers have which ports open so as not to cause any conflicts or failures when starting Minecraft Servers. For now it will be a manual process for our business, but there are ways around this, just not in the scope of this article. We &quot;may&quot; get to look into ways around this with a new technology (spoiler: we will), but we don&apos;t know about it yet.</p><p>One last note about our Ansible, we have added a new directory in our role called <code>handlers</code>. A <code>handler</code> in Ansible is a task or set of tasks that run when &quot;notified&quot; by another task and run at the end of a play. To call a <code>handler</code> there are 2 requirements:</p><ol><li>The task that calls the handler must complete with a <code>changed</code> state (aka it actually did something and not be <code>skipped</code> or <code>ok</code>.</li><li>A new line needs to be added to the task: <code>notify</code></li></ol><p>The <code>notify</code> keyword must have the name of the <code>handler</code> task you wish to run. Here is ours for example</p><figure class="kg-card kg-code-card"><pre><code class="language-yaml">- name: Create the systemd service from template
  template:
    src: container.service.j2
    dest: &quot;/usr/lib/systemd/system/{{ item.0.key }}-{{ item.1 }}-container.service&quot;
  loop: &quot;{{ containers | dict2items | subelements(&apos;value&apos;) }}&quot;
  notify: start_containers

---

- name: start_containers
  systemd:
    name: &quot;{{ item.0.key }}-{{ item.1 }}-container.service&quot;
    enabled: yes
    state: started
  loop: &quot;{{ containers | dict2items | subelements(&apos;value&apos;) }}&quot;
</code></pre><figcaption>Handlers and Notify</figcaption></figure><p>The <code>notify</code> above if it causes a <code>change</code> will trigger the task called <code>start_containers</code> after the play it was called from has finished. This is a very powerful way to handle changes in your Ansible environment and I wanted to make sure you were aware of it prior to moving on. </p><p>We have all of this new Ansible code, but what do we do with it? Oh yeah, keep it in source control! </p><ul><li><code>git add -A . &amp;&amp; git commit &amp;&amp; git push</code></li></ul><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.kywa.io/content/images/2022/01/git-add.gif" class="kg-image" alt loading="lazy" width="2000" height="1008" srcset="https://blog.kywa.io/content/images/size/w600/2022/01/git-add.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/01/git-add.gif 1000w, https://blog.kywa.io/content/images/size/w1600/2022/01/git-add.gif 1600w, https://blog.kywa.io/content/images/2022/01/git-add.gif 2334w" sizes="(min-width: 720px) 720px"><figcaption>Keeping it together with Git</figcaption></figure><h3 id="building-a-production-ready-image">Building a Production Ready Image</h3><p>And back to Containers! (almost). We need to setup something to build our Container Images. We can do this manually, but having Ansible do all of this for us would be a big help in automation AND we get to learn a new tool for Ansible called <code>ansible-vault</code> (no extra installs required, its part of the Ansible package).</p><p>Let&apos;s look at how to build an Image with Ansible. There are 2 <code>collections</code> in Ansible we can use, one for Docker, the other for Podman. We are going to use the one for Podman as that is what we have chosen to run our Containers with (from an Automation standpoint) as it doesn&apos;t have a daemon as mentioned previously. There is no need to worry about differences as the <code>task</code> names are nearly identical and have the same syntax (if you&apos;re using Docker). Let&apos;s take a look at an example:</p><figure class="kg-card kg-code-card"><pre><code class="language-yaml">- name: Build mineOps - Minecraft Server
  containers.podman.podman_image:
    name: minecraft-server:{{ minecraft_build_tag }}
    path: &quot;/tmp/minecraft-builds&quot;
    build:
      cache: no
      force_rm: yes
      format: oci
      
- name: Build and push an image using username and password
  containers.podman.podman_image:
    name: minecraft-server:{{ minecraft_build_tag }}
    push: yes
    push_args:
      dest: docker.io/mineops/minecraft-server
    tag: &quot;{{ minecraft_build_tag }}&quot;
    username: &quot;{{ docker_hub_username }}&quot;
    password: &quot;{{ docker_hub_password }}&quot;</code></pre><figcaption>Basic Build and Push through Ansible</figcaption></figure><p>The above 2 tasks are the core foundation for building and pushing a Container Image to a Container Registry. There is some other logic we will build around this, but the core are those 2 tasks. Let&apos;s kick off a test run and see what we get. We will dig into the where <code>docker_hub_password</code> is later along with using <code>ansible-vault</code> to protect these secrets. </p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/ansible-build-push.gif" class="kg-image" alt loading="lazy" width="2000" height="1014" srcset="https://blog.kywa.io/content/images/size/w600/2022/01/ansible-build-push.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/01/ansible-build-push.gif 1000w, https://blog.kywa.io/content/images/size/w1600/2022/01/ansible-build-push.gif 1600w, https://blog.kywa.io/content/images/2022/01/ansible-build-push.gif 2320w" sizes="(min-width: 720px) 720px"></figure><p>Looks like we have ourselves a working Ansible task to build a Container Image and push it to a Registry! The full Ansible task for this can be found in the repo HERE. Now lets look at a few other tricks to make this a little more secure instead of having a seemingly plain text variable somewhere.</p><h3 id="keeping-things-secureansible-vault">Keeping things Secure - Ansible Vault</h3><p>In the variable <code>{{ docker_hub_password }}</code> it looks like any other variable that we&apos;ve seen in Ansible. However, its not just a plain text <code>var: string</code> like you would expect. Here is what it actually looks like:</p><pre><code>$ cat inventory/group_vars/all/vault/secrets.yml
$ANSIBLE_VAULT;1.1;AES256
32303733353062663135313337303436633138303162393534313637353937646335363133313039
3762333632393531663464376664333234643461646366660a343633353236366164386265353136
30343064346335663830383361326336393937376336616336663934323835623163376131326563
3635353332636235330a623765323966613935393130643839643562633436643763643238333466
62326537323032636533656135306430623363666536373531376636343037393535336134643466
33333537636435376363643261656134623236313131376632666236636138626231336661316438
61663364333135303630323664323061643561393862376339396532333739643665643436386138
63326430396463656464</code></pre><p>Now I don&apos;t know about you, but that doesn&apos;t look anything like a vars file that we&apos;ve seen so far, but somehow it has data in it. Let&apos;s do a quick primer on <code>ansible-vault</code> (note you can use this outside of Ansible for easy storing of secret data.)</p><p>When Ansible is installed another &quot;program&quot; is installed along with it called <code>ansible-vault</code>. This tool gives you a few capabilities:</p><ul><li><code>view</code> - View an encrypted file</li><li><code>decrypt</code> - Remove encryption from a file</li><li><code>encrypt</code> - Encrypt a file</li><li><code>edit</code> - Edit an encrypted file</li></ul><p>Now its time for practice! Here is a quick little example of what it looks like to use <code>ansible-vault</code> on its own.</p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/vault-demo.gif" class="kg-image" alt loading="lazy" width="2000" height="1014" srcset="https://blog.kywa.io/content/images/size/w600/2022/01/vault-demo.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/01/vault-demo.gif 1000w, https://blog.kywa.io/content/images/size/w1600/2022/01/vault-demo.gif 1600w, https://blog.kywa.io/content/images/2022/01/vault-demo.gif 2320w" sizes="(min-width: 720px) 720px"></figure><p>One of the neat things about <code>ansible-vault</code> is that the data inside doesn&apos;t have to be a <code>key: value</code> variable style set of data. It can be anything at all. Any time of file, which can come in super handy in case you wanted to encrypt an entire file of lets say, a <code>dockercfg.json</code> containing login data to a Container Registry or just data you don&apos;t want visible in Git in general.</p><p>Now lets talk about how its used inside of Ansible. If you remember from Part 2, any files placed inside of <code>inventory/group_vars/&lt;GROUP NAME&gt;</code> will be pulled into the <code>tasks</code> of the hosts being targeted if they belong in those Group(s). There is also a special group called <code>all</code> and it is there we are going to create a new directory called <code>vault</code>. NOTE: You do not need to have files encrypted by <code>ansible-vault</code> placed in a directory called <code>vault</code>. This is purely for humans working with the repo to know that all files under <code>vault</code> are encrypted.</p><p>So how does Ansible use these files if they are encrypted? When you run <code>ansible-playbook</code> and Ansible pulls in all the files in <code>inventory</code> its going to use, it will detect the files that are encrypted with <code>ansible-vault</code>. If you do not pass in a flag on what to do with encrypted files, you will get a message like this:</p><pre><code>$ cat vaulttest.yml
---
- hosts: all
  gather_facts: false
  tasks:
    - name: Check variable through ansible-vault
      debug:
        msg: &quot;echo {{ docker_hub_password }}&quot;
      delegate_to: localhost

$ ansible-playbook -i inventory/hosts vaulttest.yml
PLAY [all] ****************************************************************************************************************************************************
ERROR! Attempting to decrypt but no vault secrets found</code></pre><p>There are a few ways for Ansible to use these encrypted files. Each of the methods I will outline are used as parameters when running <code>ansible-playbook</code>:</p><ul><li><code>--ask-vault-password</code> - When running <code>ansible-playbook</code> you will be prompted with <code>Vault password:</code> for you to enter the vault password(s) needed. </li><li><code>--vault-password-file=/path/to/password_file</code> - You can store the password in a plain text file for Ansible to read as it runs. This isn&apos;t the most secure method, but for testing and rapid usage, it works fine</li></ul><p>There are even more secure and advanced methods, but will not be outlined in this guide. Check the <a href="https://docs.ansible.com/ansible/latest/user_guide/vault.html#storing-and-accessing-vault-passwords">Ansible Vault documentation</a> on implementing this on your own.</p><p>Let&apos;s run our playbook again, but this time giving it the vault password:</p><pre><code>$ ansible-playbook -i inventory/hosts vaulttest.yml --ask-vault-password
Vault password:

PLAY [all] ****************************************************************************************************************************************************

TASK [Check variable through ansible-vault] *******************************************************************************************************************
ok: [mineops-0.kywa.io -&gt; localhost] =&gt; {
    &quot;msg&quot;: &quot;echo PASSWORD_OR_TOKEN&quot;
}

PLAY RECAP ****************************************************************************************************************************************************
mineops-0.kywa.io          : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

$ ansible-vault view inventory/group_vars/all/vault/secrets.yml
docker_hub_password: PASSWORD_OR_TOKEN
docker_hub_username: mineops</code></pre><p>As you can see, Ansible was able to get the variable <code>docker_hub_password</code> out of encrypted file and gave us the value and it can be used as any other variable in Ansible. There is more to <code>ansible-vault</code>, but this covers most use cases and can be grown into your actual environment. For us, this is about as far as we will take it. And now its time for one of the final items and that is building a &quot;production grade&quot; Container Image.</p><h3 id="building-our-production-image">Building our Production Image</h3><p>We are going to create a template for our Dockerfile we will use for our final &quot;production&quot; grade image. There isn&apos;t much difference between this and our original Dockerfile, but there are a few things we are adding in. Some labels, a UBI openJDK image from Red Hat (targeted via digest!) and some other interesting tidbits.</p><figure class="kg-card kg-code-card"><pre><code class="language-Dockerfile">FROM registry.access.redhat.com/ubi8/openjdk-17-runtime@sha256:1a40771ffd18a688a4492f319814af5d2e3377eb293c69ca682f29a3a86635ab

USER 0

# Label Image with important information
LABEL builder=&quot;{{ mineops_email }}&quot; \
      application=&quot;minecraft-server&quot; \
      java_version=&quot;{{ minecraft_java_version }}&quot; \
      minecraft_version=&quot;{{ minecraft_version }}&quot;

EXPOSE 25565

COPY mc-start.sh /usr/local/bin/

RUN chmod +x /usr/local/bin/mc-start.sh &amp;&amp; curl -o /usr/local/bin/jq -sL https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 &amp;&amp; chmod +x /usr/local/bin/jq

VOLUME [&quot;/minecraft&quot;]

WORKDIR /minecraft

USER 1001

CMD [&quot;/usr/local/bin/mc-start.sh&quot;]</code></pre><figcaption>Dockerfile template</figcaption></figure><p>If we check out Docker Hub we can see our new tags:</p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/Screen-Shot-2022-01-11-at-10.07.57-PM.png" class="kg-image" alt loading="lazy" width="669" height="455" srcset="https://blog.kywa.io/content/images/size/w600/2022/01/Screen-Shot-2022-01-11-at-10.07.57-PM.png 600w, https://blog.kywa.io/content/images/2022/01/Screen-Shot-2022-01-11-at-10.07.57-PM.png 669w"></figure><p>We can use our same build Playbook as before except we have just a slightly different command as we will be passing in a variable through the CLI and not in a variable file:</p><p><code>ansible-playbook -i inventory/hosts playbooks/build_image.yml --ask-vault-pass --limit mineops-0.kywa.io -e minecraft_build_tag=&quot;prod&quot;</code></p><p>And essentially what we have here is a complete &quot;production ready&quot; build Container Image. We have slightly changed the way in which our image works, but the core is the same, a running Minecraft Server Container Image (again). This new method uses a startup script so we can mount a volume for persistent storage, or in our current case, a local directory.</p><p>Before moving to the final section here is the command to run this Container (NOTE: This will start the server in your current directory):</p><pre><code># If the system has SELinux enabled
docker run -d -p 25565:25565 -v &quot;${PWD}&quot;:/minecraft:z minecraft-server:latest

# And without SELinux enabled
docker run -d -p 25565:25565 -v &quot;${PWD}&quot;:/minecraft minecraft-server:latest</code></pre><h2 id="adding-visibility">Adding Visibility</h2><p>After getting all of this going, surely by now we can get a fancy UI to look at all of what we&apos;ve built and have running? You can run <code>docker ps -a</code> and <code>docker images</code> all day long, but sometimes you just want a &quot;single pane of glass&quot;. There are a few options here to do this. <a href="https://www.portainer.io/">Portainer</a>, <a href="https://rancher.com/">Rancher </a>and a few others, but we will focus on Portainer as its the &quot;easier&quot; to stand up and get going. Thankfully its also runs as a Container, so we already have our platform. Get it running quickly like this:</p><pre><code>docker run -d -p 8000:8000 -p 9443:9443 --name portainer \
    --restart=always \
    -v /var/run/docker.sock:/var/run/docker.sock \
    -v portainer_data:/data \
    cr.portainer.io/portainer/portainer-ce:2.9.3</code></pre><p>Once you have it up and running you can go to https://localhost:9443, create a password for the admin account and get a pretty UI like this:</p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/portainer.gif" class="kg-image" alt loading="lazy" width="1276" height="751" srcset="https://blog.kywa.io/content/images/size/w600/2022/01/portainer.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/01/portainer.gif 1000w, https://blog.kywa.io/content/images/2022/01/portainer.gif 1276w" sizes="(min-width: 720px) 720px"></figure><p>From the little gif there you can see a glimpse of some of the features it has. It is more features than most people or businesses need to run a Container Host, but has features that allow it to manage more than a single Container Host. </p><h2 id="here-we-go-again">Here we go again...</h2><p>As we migrate our final customer over to Containers, we get a knock on our door. We stand up and stretch after a long day&apos;s work to go open the door to find no one other than our CEO with a smile on his face. He tells us that after the huge success with Containers, we need to keep modernizing because we are using &quot;old technology&quot; now. We are told an email will be in our inbox soon for us to review since we are rockstars and can &quot;knock this out in a weekend&quot;.</p><p>The CEO lives and we go sit back at our desk (and cry ever so slightly) we open our email client and see a new email with this subject line:</p><pre><code>Subject: RE: Check out kubernetes.io</code></pre><h3 id="series-links">Series Links:</h3><ul><li><a href="https://blog.kywa.io/mineops-part-1/">Part 1 - Manual Minecraft Server Installation</a></li><li><a href="https://blog.kywa.io/mineops-part-2/">Part 2 - Automating with Ansible</a></li><li><a href="https://blog.kywa.io/mineops-part-3/">Part 3 - Keeping it Altogether with Git</a></li><li><a href="https://blog.kywa.io/mineops-part-4/">Part 4 - Containers Have Joined the Party</a></li><li><a href="https://blog.kywa.io/mineops-part-5/">Part 5 - Making Containers Highly Available</a></li><li><a href="https://blog.kywa.io/mineops-part-6">Part 6 - ArgoCD to the Rescue</a></li></ul>]]></content:encoded></item><item><title><![CDATA[mineOps - Part 3: Keeping it together with Git]]></title><description><![CDATA[<figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/mineops-2.png" class="kg-image" alt loading="lazy" width="383" height="78"></figure><p>In <a href="https://blog.kywa.io/mineops-part-2/">Part 2</a> of this series we began to use Ansible to manage our customer servers from deployment, backup and configuration. We ended that post discussing what to do with all of those files which has led us here, to Git. There are some out there who are not familiar</p>]]></description><link>https://blog.kywa.io/mineops-part-3/</link><guid isPermaLink="false">61d3bcb4c040b4060b5bb01c</guid><category><![CDATA[mineops]]></category><dc:creator><![CDATA[Kyle Walker]]></dc:creator><pubDate>Wed, 05 Jan 2022 06:54:38 GMT</pubDate><content:encoded><![CDATA[<figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/mineops-2.png" class="kg-image" alt loading="lazy" width="383" height="78"></figure><p>In <a href="https://blog.kywa.io/mineops-part-2/">Part 2</a> of this series we began to use Ansible to manage our customer servers from deployment, backup and configuration. We ended that post discussing what to do with all of those files which has led us here, to Git. There are some out there who are not familiar with Git would be turned away from its &quot;stigma&quot; of being complicated. These same people would attempt to use DropBox or Google Drive for their source control needs and would come up lacking down the line at some point. That isn&apos;t to say tools like them don&apos;t have their place, but when it comes to version control of source code they really do come up lacking.</p><p>Just a quick example to make a case for Git over file sync services such as those listed above, suppose you need to make changes to a large amount of files? In a file sync application, you can restore files to a specific point in history, but will have to do this to each file changed and make sure that each of the versions restored are matched to the same point in history. With Git you can rollback the entire project to a point in history.</p><p>Another great feature is something called <code>blame</code> which essentially tells you which line of a file was last edited by whom. We will touch on this feature more later, but there are many other reasons on why to use Git for your source control needs.</p><h2 id="installing-git">Installing Git</h2><p>As with Ansible there are a few ways to install Git, but the package manager of your system will probably be the best bet:</p><ul><li>yum install -y git</li></ul><p>You can also download and compile from source if you need a newer version than what your package manager has: <a href="https://github.com/git/git/tags">https://github.com/git/git/tags</a></p><p>Once you have Git installed, verify its installed and in your path. If you have verified you have Git installed we can proceed to the next section.</p><pre><code>$ git --version
git version 2.32.0</code></pre><h2 id="understanding-git">Understanding Git</h2><p>If you are familiar with Git, feel free to move along to the next section. If you wish to have a very in depth tutorial series on Git, I highly recommend checking out this YouTube channel:</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://www.youtube.com/c/DanGitschooldude"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Dan Gitschooldude</div><div class="kg-bookmark-description">This channel is dedicated to teaching the distributed version control system git, the best damn source control management tool that has ever existed. Git is quite powerful and can be complicated to learn, but fear not! Watch these tips and tutorials, and you&#x2019;ll be an expert in no time. In additio&#x2026;</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://www.youtube.com/s/desktop/f4861452/img/favicon_144x144.png" alt><span class="kg-bookmark-author">YouTube</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://yt3.ggpht.com/ytc/AKedOLQ6lkET3gRrsAhvjiwhEz_kdQruTlxZXWARfBCq=s900-c-k-c0x00ffffff-no-rj" alt></div></a></figure><p>For the Git basics that are recommended for our business, I&apos;ve created a quick primer for Git and its main commands and phrases used, but we will also go over some of these in detail in the next few sections: <a href="https://github.com/KyWa/blogs/blob/master/mineOps/docs/Git/git-primer.md">https://github.com/KyWa/blogs/blob/master/mineOps/docs/Git/git-primer.md</a></p><h3 id="working-directorylocal-repository">Working Directory - Local Repository</h3><p>The core feature of Git that most people will ultimately use can be outlined easily. There is the &quot;working directory&quot;, which for our purposes will be the directory where we keep our Ansible code from the last section. This &quot;working directory&quot; is also called the &quot;local repository&quot;. This local repository stores your files contained within, but most importantly the history of these files as a whole. To make a directory a Git repository, issue this command in the &quot;top level&quot; of the directory containing the files you wish Git to keep track of. For us in our example, we will be using the &quot;modular_ansible&quot; directory as our &quot;top level&quot;:</p><pre><code class="language-sh">$ pwd
~/modular_ansible

$ git init
Initialized empty Git repository in ~/modular_ansible/.git/</code></pre><p>Once the above has been run our &quot;working directory&quot; where we have all of our files and directories such as <code>playbooks</code>, <code>roles</code> and <code>inventory</code> now exist as part of the local repository, but these files aren&apos;t being tracked by Git yet. Let&apos;s take a look at a command <code>git status</code> and see what it tells us:</p><pre><code class="language-sh">$ git status
On branch master

No commits yet

Untracked files:
  (use &quot;git add &lt;file&gt;...&quot; to include in what will be committed)
        ansible.cfg
        inventory/
        playbooks/
        roles/

nothing added to commit but untracked files present (use &quot;git add&quot; to track)</code></pre><p>With the <code>git status</code> command we can get some very important information about our new local repository. One nice thing about Git is that it is very good (sometimes annoyingly so) about telling you what is going on and what you need to do to get a clean and up-to date repository.</p><ul><li><code>On branch master</code> - This first line tells us the name of the branch we are on (more on this topic later)</li><li><code>No commits yet</code> - Something you will only ever see on a fresh Git repository via <code>git init</code></li><li><code>Untracked files</code> - Now this is where Git starts to show its power. This output shows all the files in the working directory that are not part of the `index`. We will discuss the <code>index</code> further in one of the next sections.</li><li><code>nothing added to commit but untracked files present (use &quot;git add&quot; to track)</code> - This is Git telling us that there is nothing committed to the `index`, but it is aware of new files that are untracked. Git is also kind enough to tell us what we need to do to track these files.</li></ul><p><code>git status</code> is a command you will get familiar with very quickly as it tells you the &quot;state&quot; of your local repository. Let&apos;s move on to the next topic, <code>commits</code>, but to learn about <code>commits</code> we need to understand the <code>index</code>.</p><h3 id="index">Index</h3><p>The <code>index</code> in Git is a &quot;staging area&quot; for the files in the working directory. In the <code>git status</code> output from earlier, we noticed Git told us this: <code>Untracked files</code> and proceeded to list the files and directories in our local repository. These files are not part of the index, but are part of the working directory. If we issue the <code>git add</code> command from earlier that Git gave us, let&apos;s see what our <code>git status</code> looks like and we can see the <code>index</code> change.</p><pre><code class="language-sh">$ git add ansible.cfg
$ git status
On branch master

No commits yet

Changes to be committed:
  (use &quot;git rm --cached &lt;file&gt;...&quot; to unstage)
        new file:   ansible.cfg

Untracked files:
  (use &quot;git add &lt;file&gt;...&quot; to include in what will be committed)
        inventory/
        playbooks/
        roles/</code></pre><p>Notice we have a new part of output,<code>Changes to be committed</code>. This <code>status</code> gives us some very interesting information:</p><ul><li><code>use &quot;git rm --cached &lt;file&gt;...&quot; to unstage</code> - This shows us that we have a new staged file as part of our <code>index</code></li><li><code>new file: ansible.cfg</code> - Git tells us about the file we added via <code>git add</code> and is the file thats part of our <code>index</code></li></ul><p>Let&apos;s go ahead and do a <code>git add</code> on the rest of the files in our working directory:</p><pre><code class="language-sh">$ git add .
$ git status
On branch master

No commits yet

Changes to be committed:
  (use &quot;git rm --cached &lt;file&gt;...&quot; to unstage)
        new file:   ansible.cfg
        new file:   inventory/group_vars/customer-0/config.yml
        new file:   inventory/group_vars/customer-0/server-properties.yml
        new file:   inventory/group_vars/customer-1/config.yml
        new file:   inventory/group_vars/customer-1/server-properties.yml
        new file:   inventory/group_vars/minecraft/config.yml
        new file:   inventory/group_vars/minecraft/server-properties.yml
        new file:   inventory/host_vars/mineops-0.kywa.io/config.yml
        new file:   inventory/host_vars/mineops-0.kywa.io/server-properites.yml
        new file:   inventory/host_vars/mineops-1.kywa.io/config.yml
        new file:   inventory/host_vars/mineops-1.kywa.io/server-properites.yml
        new file:   inventory/hosts
        new file:   playbooks/minecraft-backup.yml
        new file:   playbooks/minecraft-config-change.yml
        new file:   playbooks/minecraft-install.yml
        new file:   playbooks/minecraft-restore.yml
        new file:   playbooks/minecraft-uninstall.yml
        new file:   roles/minecraft/files/eula.txt
        new file:   roles/minecraft/tasks/backup.yml
        new file:   roles/minecraft/tasks/config-change.yml
        new file:   roles/minecraft/tasks/main.yml
        new file:   roles/minecraft/tasks/restore.yml
        new file:   roles/minecraft/tasks/server-prep.yml
        new file:   roles/minecraft/tasks/start-server.yml
        new file:   roles/minecraft/tasks/uninstall.yml
        new file:   roles/minecraft/templates/minecraft.service.j2
        new file:   roles/minecraft/templates/server.properties.j2</code></pre><p>Our <code>index</code> has grown and we no longer have any <code>Untracked files</code> in our working directory and our entire repository. The only item we have left in our <code>git status</code> that Git is telling us about is <code>Changes to be committed</code>. With that, lets move on to learning about commits.</p><h3 id="commits">Commits</h3><p>Commits are &quot;snapshot&quot; of the working directory and the files contained inside that Git is keeping track of. As we saw in the <code>git status</code> above in the last section, Git told us we had <code>Changes to be committed</code> based on the files we added to the <code>index</code>. If we issue the <code>git commit</code> command on its own, we will be brought to your default text editor. This will have you input what is called a <code>commit message</code> which is human slang for &quot;what are you committing to this local repository?&quot;. There is another method using <code>git commit</code> where you do not go into an editor, but are able to pass in the <code>commit message</code> in-line via: <code>git commit -m &quot;my message&quot;</code>.</p><p>For the sake of this guide and getting going, lets use the quicker method:</p><pre><code>$ git commit -m &quot;initial commit, adding repo files&quot;
[master (root-commit) 7ba22bd] initial commit, adding repo files
 Committer: Kyle Walker &lt;kylewalker@Kyles-Mac-mini.kywa.io&gt;
Your name and email address were configured automatically based
on your username and hostname. Please check that they are accurate.
You can suppress this message by setting them explicitly. Run the
following command and follow the instructions in your editor to edit
your configuration file:

    git config --global --edit

After doing this, you may fix the identity used for this commit with:

    git commit --amend --reset-author

27 files changed, 437 insertions(+)
 create mode 100644 ansible.cfg
 create mode 100644 inventory/group_vars/customer-0/config.yml
 create mode 100644 inventory/group_vars/customer-0/server-properties.yml
 create mode 100644 inventory/group_vars/customer-1/config.yml
 create mode 100644 inventory/group_vars/customer-1/server-properties.yml
 create mode 100644 inventory/group_vars/minecraft/config.yml
 create mode 100644 inventory/group_vars/minecraft/server-properties.yml
 create mode 100644 inventory/host_vars/mineops-0.kywa.io/config.yml
 create mode 100644 inventory/host_vars/mineops-0.kywa.io/server-properites.yml
 create mode 100644 inventory/host_vars/mineops-1.kywa.io/config.yml
 create mode 100644 inventory/host_vars/mineops-1.kywa.io/server-properites.yml
 create mode 100644 inventory/hosts
 create mode 100644 playbooks/minecraft-backup.yml
 create mode 100644 playbooks/minecraft-config-change.yml
 create mode 100644 playbooks/minecraft-install.yml
 create mode 100644 playbooks/minecraft-restore.yml
 create mode 100644 playbooks/minecraft-uninstall.yml
 create mode 100644 roles/minecraft/files/eula.txt
 create mode 100644 roles/minecraft/tasks/backup.yml
 create mode 100644 roles/minecraft/tasks/config-change.yml
 create mode 100644 roles/minecraft/tasks/main.yml
 create mode 100644 roles/minecraft/tasks/restore.yml
 create mode 100644 roles/minecraft/tasks/server-prep.yml
 create mode 100644 roles/minecraft/tasks/start-server.yml
 create mode 100644 roles/minecraft/tasks/uninstall.yml
 create mode 100644 roles/minecraft/templates/minecraft.service.j2
 create mode 100644 roles/minecraft/templates/server.properties.j2
 
$ git status
On branch master
nothing to commit, working tree clean</code></pre><p>If we look at the top of the output, we will see a message stating who the <code>Committer</code> was. When using Git for the first time (and depending on version this output may be different), Git needs to know who is actually <code>committing</code> these changes. This information has important use later on and we will discuss the <code>git config</code> command in a few sections. The <code>git commit</code> command we ran primarily tells us how many files were &quot;changed&quot; in the local repository and how many &quot;insertions&quot;. As we will see later in this guide, when we edit files and remove lines, we will see &quot;deletions&quot; as well as &quot;insertions&quot;. This is a handy way of having a 30k foot view of the repository and its latest <code>commit</code> changes. <code>Commit</code> messages are very important as they can/should be used to outline what is contained within a <code>commit</code>. Our first <code>commit</code> we said it was the &quot;initial commit, adding repo files&quot;. Now, how do we view this commit message? Through <code>git log</code> we can see all <code>commits</code> in a local repository and their commit messages. Here is the <code>git log</code> from our test repo:</p><pre><code>$ git log
commit 7ba22bd8923180278deaf938785841b740d9eae4 (HEAD -&gt; master)
Author: Kyle Walker &lt;kywadev@gmail.com&gt;
Date:   Mon Jan 3 14:34:34 2022 -0600

    initial commit, adding repo files</code></pre><p>We have some new items here from Git, one of the largest being the long string you see: <code>7ba22bd8923180278deaf938785841b740d9eae4</code>. This is what is called the hash or SHA of the <code>commit</code>. This is a unique hash for this repository to identify &quot;that point in history&quot;. The <code>commit message</code> is for humans to be able to have some form of identifier for a specific <code>commit</code>, although unlike the <code>commit</code> hash, a <code>commit message</code> doesn&apos;t have to be unique. We&apos;ve discussed a lot about <code>commits</code> and mentioned them being a pointer to a specific place in time, so let&apos;s move on to <code>branches</code> which are pointers themselves, but to a specific <code>commit</code> history.</p><h3 id="branches">Branches</h3><p>As mentioned above Git <code>branches</code> as essentially a pointer to a specific <code>commit</code>. If all you ever do is use Git locally without ever working with a team or a remote repository, Git <code>branches</code> may not make a lot of sense at first. However, Git branches have some amazing benefits, but can be confusing if you aren&apos;t aware the current state of your local repository.</p><p>We see in our <code>git status</code> that our current <code>branch</code> is master. This happens to also be the default <code>branch</code> for most if not all Git repositories, but it doesn&apos;t have to be. You can actually set the default <code>branch</code> to be whatever you&apos;d like it to be called. Some have chosen to go with &quot;main&quot; or &quot;trunk&quot; or whatever they feel best describes the &quot;core&quot; of their repository. In our <code>git log</code> command in the previous section, we see a single <code>commit</code> hash. Our master <code>branch</code> in this local repository is currently targeting that <code>commit</code>. Remember when we first did <code>git status</code> and it said this:</p><pre><code>$ git status
On branch master

No commits yet</code></pre><p>This message <code>No commits yet</code> is telling us that currently the master <code>branch</code> isn&apos;t targeting anything, but this is no longer the case since our first <code>commit</code>. </p><pre><code class="language-sh">$ git status
On branch master
nothing to commit, working tree clean</code></pre><p>What if by now we have a few customers on our platform with their Minecraft Servers and we are going to try to update our platform. In theory we would have a development server to test these items on. For the sake of argument, I mean both the virtual server and the newer Minecraft Server we would have a development version of. In our Ansible examples we had just customer servers in place with some global variables for all Minecraft Servers to use. If we had a development environment we could have unique vars for those dev servers, or we could have a dev <code>branch</code> in Git that is used to test these changes before they get merged into the master <code>branch</code>.</p><h5 id="note">Note</h5><p>In this particular example, Ansible on its own may not be the best example for using a dev <code>branch</code> as we run all of this Ansible by hand for managing these Minecraft Servers. If we were using something like <a href="https://www.ansible.com/products/controller">Ansible Tower</a> (now called Red Hat Ansible Automation Platform) or <a href="https://github.com/ansible/awx">AWX</a> which allows you to automate and schedule your Ansible playbooks, this would make more sense. Later in this series, we will be discussing different applications which may benefit more from unique branches.</p><h3 id="git-config">Git Config</h3><p>In the <code>commits</code> section we spoke briefly about the <code>git config</code>, but went into no details about what it is. When you issue your first <code>git config --global</code> command this will create a hidden file in your home directory called <code>.gitconfig</code>.</p><pre><code>$ cat ~/.gitconfig
[user]
name = Kyle Walker
email = kywadev@gmail.com
[pull]
    rebase = false
[init]
    defaultBranch = master</code></pre><p>The output above is my <code>.gitconfig</code> for my local machine. To setup your user (as mentioned above during the <code>commit</code> section) you can issue these commands to generate do this for you:</p><ul><li><code>git config --global user.name &quot;Kyle Walker&quot;</code></li><li><code>git config --global user.email &quot;kywadev@gmail.com&quot;</code></li></ul><p>There are many other <code>git config</code> options you may end up using, but having your user set to identify you as the author of <code>commits</code> is all most people go with. If you wish to review all configuration options, the official Git docs have some very detailed information: <a href="https://git-scm.com/docs/git-config">https://git-scm.com/docs/git-config</a></p><h2 id="using-git">Using Git</h2><p>We&apos;ve discussed the major concepts and commands of working with a local Git repository, but before we move on I&apos;d like to go over what a typical &quot;workflow&quot; is like for someone using Git. In our business we just got a new customer who needs a Minecraft Server. Let&apos;s see what this process would be like now that we use Git.</p><p>First we would create our Ansible files in the same fashion as was done before, we would update the <code>inventory</code> file to include this new Minecraft Server. We would also create the required variable files in <code>group_vars</code> and <code>host_vars</code> for this new customer. Once we have done that, it&apos;s time to run our Ansible playbook and then add these files to Git. For the sake of it being &quot;just us&quot; on this team we will use these files being added to Git as our process of &quot;the work has been done&quot;. Let&apos;s see what our local repository looks like now:</p><pre><code class="language-sh">$ git status
On branch master
Changes not staged for commit:
  (use &quot;git add &lt;file&gt;...&quot; to update what will be committed)
  (use &quot;git restore &lt;file&gt;...&quot; to discard changes in working directory)
        modified:   inventory/hosts

Untracked files:
  (use &quot;git add &lt;file&gt;...&quot; to include in what will be committed)
        inventory/group_vars/customer-2/
        inventory/host_vars/mineops-2.kywa.io/

no changes added to commit (use &quot;git add&quot; and/or &quot;git commit -a&quot;)</code></pre><p>As we can see, we now have a different type of <code>git status</code>, but we&apos;ve seen all of this before when we did our first <code>git add</code> of <code>ansible.cfg</code>. We have <code>Changes not staged for commit</code>, this being the pre-existing and already tracked inventory hosts file, and we have more <code>Untracked files</code> which we added for our new customer.</p><p>Before we add any files to our local repository, let&apos;s take a look at a new Git command called <code>git diff</code> and see what the inventory hosts file shows us.</p><pre><code>$ git diff inventory/hosts
diff --git a/inventory/hosts b/inventory/hosts
index 510d09b..181a8ea 100644
--- a/inventory/hosts
+++ b/inventory/hosts
@@ -1,9 +1,13 @@
 [minecraft:children]
 customer-0
 customer-1
+customer-2

 [customer-0]
 mineops-0.kywa.io

 [customer-1]
 mineops-1.kywa.io
+
+[customer-2]
+mineops-2.kywa.io</code></pre><p><code>git diff</code> shows us the additions (remember this from earlier during our first <code>commit</code>?) via the <code>+</code> sign on each line of the inventory hosts file. This is quite handy in case you were working on something, had to step away, lost your place and were unsure of where you left off. It has more uses later on as we will see in this guide.</p><p>Once you&apos;ve created the new files to be tracked in Git, you would then add them via <code>git add</code>:</p><pre><code>$ git add -A
$ git status
On branch master
Changes to be committed:
  (use &quot;git restore --staged &lt;file&gt;...&quot; to unstage)
        new file:   inventory/group_vars/customer-2/config.yml
        new file:   inventory/host_vars/mineops-2.kywa.io/config.yml
        new file:   inventory/host_vars/mineops-2.kywa.io/server-properites.yml
        modified:   inventory/hosts</code></pre><p>We are now ready in theory to create another <code>commit</code> to this repository. This time, lets just use <code>git commit</code> with no <code>-m</code> flag being passed in. For my system I have an environment variable set called <code>EDITOR=vim</code>. If you do not want to alter your systems default editor via this environment variable, you can add something to your Git configuration file through this command <code>git config --global core.editor &quot;vim&quot;</code>. The other alternative is an environment variable called <code>GIT_EDITOR</code> which can be set to any editor as with the <code>EDITOR</code> env var. When we run <code>git commit</code>, we are met with a screen that looks somewhat like this (note I am using <code>vim</code> as my <code>EDITOR</code>):</p><pre><code class="language-sh">
# Please enter the commit message for your changes. Lines starting
# with &apos;#&apos; will be ignored, and an empty message aborts the commit.
#
# On branch master
# Changes to be committed:
#       new file:   inventory/group_vars/customer-2/config.yml
#       new file:   inventory/host_vars/mineops-2.kywa.io/config.yml
#       new file:   inventory/host_vars/mineops-2.kywa.io/server-properites.yml
#       modified:   inventory/hosts
#</code></pre><p>Git gives us a clear idea of what it is wanting, a <code>commit message</code>, but with this we are able to more easily create multi-line <code>commit messages</code> for the sake of being explicit. This isn&apos;t required, but its something to be aware of if you grow your team and you require more detailed <code>commit messages</code> for some tools in the future such as <a href="https://github.com/git-chglog/git-chglog">git-chglog</a>. As for the <code>commit message</code> in this format, you can create them like this:</p><pre><code class="language-sh">Adding new customer
- customerID 98765 via ticket #00005
- deployed by Kyle

# Please enter the commit message for your changes. Lines starting
# with &apos;#&apos; will be ignored, and an empty message aborts the commit.
#
# On branch master
# Changes to be committed:
#       new file:   inventory/group_vars/customer-2/config.yml
#       new file:   inventory/host_vars/mineops-2.kywa.io/config.yml
#       new file:   inventory/host_vars/mineops-2.kywa.io/server-properites.yml
#       modified:   inventory/hosts
#</code></pre><p>Once you are satisfied with your <code>commit message</code> you can write and quit out of the editor (in <code>vim</code> this is <code>:wq</code>). And the output afterwards is the same as any <code>commit</code>, we can also check the <code>git log</code> and see our multi-line <code>commit message</code>:</p><pre><code>$ git commit
[master 552b08d] Adding new customer - customerID 98765 via ticket #00005 - deployed by Kyle
 4 files changed, 9 insertions(+)
 create mode 100644 inventory/group_vars/customer-2/config.yml
 create mode 100644 inventory/host_vars/mineops-2.kywa.io/config.yml
 create mode 100644 inventory/host_vars/mineops-2.kywa.io/server-properites.yml
 
$ git log
commit 552b08d221fc9c6a1875f65d8bcfae7c1bf0aaa4 (HEAD -&gt; master)
Author: Kyle Walker &lt;kywadev@gmail.com&gt;
Date:   Mon Jan 3 15:24:35 2022 -0600

    Adding new customer
    - customerID 98765 via ticket #00005
    - deployed by Kyle

commit 7ba22bd8923180278deaf938785841b740d9eae4
Author: Kyle Walker &lt;kywadev@gmail.com&gt;
Date:   Mon Jan 3 14:34:34 2022 -0600

    initial commit, adding repo files</code></pre><p>As for using Git, there is really only one other item to address as it relates to a local repository and that is reverting <code>commits</code> with <code>git revert</code>. Depending on the scope and size of changes in the <code>commit</code> you wish to revert, you could either edit the files (if it was a small change) and create another <code>commit</code>, or you could use <code>git revert</code> if it was a large amount of files and changes. Please note that <code>git revert</code> can only be used to revert <code>commits</code>, but by doing so creates a new <code>commit</code> marking the revert. You will need to get the <code>commit hash</code> via <code>git log</code> or some other method and then you can run <code>git revert 552b08d221fc9c6a1875f65d8bcfae7c1bf0aaa4</code>. Let&apos;s see what that looks like and what happens when we run this:</p><pre><code>$ git revert 552b08d221fc9c6a1875f65d8bcfae7c1bf0aaa4
Revert &quot;Adding new customer&quot;

This reverts commit 552b08d221fc9c6a1875f65d8bcfae7c1bf0aaa4.

# Please enter the commit message for your changes. Lines starting
# with &apos;#&apos; will be ignored, and an empty message aborts the commit.
#
# On branch master
# Changes to be committed:
#       deleted:    inventory/group_vars/customer-2/config.yml
#       deleted:    inventory/host_vars/mineops-2.kywa.io/config.yml
#       deleted:    inventory/host_vars/mineops-2.kywa.io/server-properites.yml
#       modified:   inventory/hosts
#

$ git revert 552b08d221fc9c6a1875f65d8bcfae7c1bf0aaa4
Removing inventory/host_vars/mineops-2.kywa.io/server-properites.yml
Removing inventory/host_vars/mineops-2.kywa.io/config.yml
Removing inventory/group_vars/customer-2/config.yml
[master c6f7b7b] Revert &quot;Adding new customer&quot;
 4 files changed, 9 deletions(-)
 delete mode 100644 inventory/group_vars/customer-2/config.yml
 delete mode 100644 inventory/host_vars/mineops-2.kywa.io/config.yml
 delete mode 100644 inventory/host_vars/mineops-2.kywa.io/server-properites.yml

$ git status
On branch master
nothing to commit, working tree clean

$ git log
commit c6f7b7b392a52edbbde4be26e8ed00c8487a9403 (HEAD -&gt; master)
Author: Kyle Walker &lt;kywadev@gmail.com&gt;
Date:   Mon Jan 3 15:45:28 2022 -0600

    Revert &quot;Adding new customer&quot;

    This reverts commit 552b08d221fc9c6a1875f65d8bcfae7c1bf0aaa4.

commit 552b08d221fc9c6a1875f65d8bcfae7c1bf0aaa4
Author: Kyle Walker &lt;kywadev@gmail.com&gt;
Date:   Mon Jan 3 15:24:35 2022 -0600

    Adding new customer
    - customerID 98765 via ticket #00005
    - deployed by Kyle

commit 7ba22bd8923180278deaf938785841b740d9eae4
Author: Kyle Walker &lt;kywadev@gmail.com&gt;
Date:   Mon Jan 3 14:34:34 2022 -0600

    initial commit, adding repo files
</code></pre><p>Note we have a new <code>commit</code> showing the revert. If you do not wish to have revert <code>commits</code> in your <code>git log</code> (I would recommend that you do keep them there for the sake of history especially as you move to remote repositories and working with teams), you can use the <code>git reset</code> command which we are going to go over quickly below.</p><p>If you need to revert changes in your <code>index</code> to &quot;rollback&quot; changes you made to existing files you can use <code>git reset --hard hash</code>. This can potentially be destructive so you should probably use <code>git checkout .</code> to reset your index to <code>HEAD</code> (latest commit for your <code>branch</code>). Please note this only works if you haven&apos;t <code>commited</code> any changes locally yet.</p><p>That wraps up just about everything you would typically do with Git in a local repository. It is time to learn about remote repositories and then working with a team once we have a remote repository working.</p><h2 id="remote-repositories">Remote Repositories</h2><p>So we&apos;ve talked about Git a lot, but not once have I mentioned GitHub, GitLab, Bitbucket or any other of the big names in the Git repository world and that was on purpose. People often confuse GitHub and Git as being just different versions of the same thing and this couldn&apos;t be further from the truth. </p><h3 id="setting-up-a-remote-repository">Setting up a Remote Repository</h3><p>Each of the big 3 source-code repository hosting services GitHub, GitLab and Bitbucket ultimately function as a remote repository. Setting up a new repository is roughly the same in each one, but we will not outline how to do that for each one. All that you really need is an account and then a name for the new repository. Here are links to each of the docs to create a remote repository:</p><ul><li>GitHub - <a href="https://docs.github.com/en/get-started/quickstart/create-a-repo">https://docs.github.com/en/get-started/quickstart/create-a-repo</a></li><li>GitLab - <a href="https://docs.gitlab.com/ee/user/project/working_with_projects.html#create-a-project">https://docs.gitlab.com/ee/user/project/working_with_projects.html#create-a-project</a></li><li>BitBucket - <a href="https://support.atlassian.com/bitbucket-cloud/docs/create-a-git-repository/">https://support.atlassian.com/bitbucket-cloud/docs/create-a-git-repository/</a></li></ul><p>When a new repository is set up, each of the big 3 will give you instructions on how to use the new remote repository. For this guide I will be using GitHub, but any repository hosting will work. The setup instructions it gives you outlines how to create a new repository from the command line to use this new remote repository. We do not need to do this as we have our own local repository already. Thankfully there are instructions for this use case as well. We will dive into this in the next section.</p><h3 id="adding-a-remote-location-to-your-local-repository">Adding a Remote Location to your Local Repository</h3><p>Once you have created a new remote repository in GitHub/GitLab/BitBucket, you will need to let your local repository know about this remote location. Thankfully you get these instructions once you setup a remote repository:</p><pre><code class="language-sh">$ git remote add origin git@github.com:KyWa/mineOps.git</code></pre><p>There are other instructions given but these will be outlined in the following section. The only item to note about the above command is the name <code>origin</code>. This is the name used for the remote repository that your local repository can work with.</p><h3 id="working-with-a-remote-repository">Working with a Remote Repository</h3><p>In your local repo once you have run the above command to add a <code>remote</code> target we can now begin to work with it. First let&apos;s use <code>git push</code> to push our local repository to the remote.</p><pre><code>$ git push
fatal: The current branch master has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin master

$ git push --set-upstream origin master
Warning: Permanently added &apos;github.com&apos; (ED25519) to the list of known hosts.
Enumerating objects: 54, done.
Counting objects: 100% (54/54), done.
Delta compression using up to 12 threads
Compressing objects: 100% (45/45), done.
Writing objects: 100% (54/54), 7.43 KiB | 1.86 MiB/s, done.
Total 54 (delta 6), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (6/6), done.
To github.com:KyWa/mineops-modular_ansible.git
 * [new branch]      master -&gt; master
Branch &apos;master&apos; set up to track remote branch &apos;master&apos; from &apos;origin&apos;.</code></pre><p>Running <code>git push</code> on its own at the moment throws an error that the current branch master has no upstream branch. We can remedy this with the command that Git gives us (see how helpful Git can be?). Let&apos;s break down what we this command is doing. </p><ul><li><code>git push</code> - pushing local changes to an upstream branch</li><li><code>--set-upstream</code> - starts the setup of targeting the upstream branch</li><li><code>origin</code> - the remote target used by the <code>--set-upstream</code> flag</li><li><code>master</code> - the name of the remote branch to push your local branch to</li></ul><p>So we now have content in our remote repository, this is a huge step forward for our company. We no longer have our files being stored on our local machine and have Git tracking our files locally, with the big plus of being stored in GitHub (our remote repository).</p><p>Some of the other tasks you will do when working with a remote repository is getting changes from that remote repository. Now at the moment its just us in this company so there won&apos;t be much to get from our remote repository as we are the only ones doing any work. In the next section when we start &quot;working with a team&quot; we will dig more into pulling code from a remote repository after our &quot;teammates&quot; have made changes we need.</p><h2 id="working-with-a-team">Working With a Team</h2><p>Our company has grown and we need another team member. Once this new team member has gotten set up they will start working and adding code to our remote repository. We now need to get this code so we can have these changes locally to ensure we don&apos;t have any issues with our own changes. There are a few methods to do this with the more common one being <code>git pull</code>. This command will technically do 2 other commands a <code>git fetch</code> and a <code>git merge</code>. Fetch will &quot;fetch&quot; all the objects and references in &quot;remote&quot; if there are any, but will not be part of your local Git history just yet. If you were to run a <code>git merge</code> once you have this (and there were changes in the upstream branch matching your local branch) it will update your local history by &quot;merging&quot; each history together (local and remote). The more simpler way is <code>git pull</code> which will handle both of these for you and is the most common path used to do this.</p><p>A note about working with a team and <code>pulling</code> code down or pushing new code. There can be things known as a &quot;Merge Conflict&quot;. These are not fun, but we will not being dealing with these and you may not either if you ensure you have a clean &quot;working directory&quot; prior to doing a <code>push</code> or <code>pull</code>. For more information around &quot;Merge Conflicts&quot; check out this article: <a href="https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/addressing-merge-conflicts/about-merge-conflicts">https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/addressing-merge-conflicts/about-merge-conflicts</a></p><p>Now this is where more fun begins, but also where Git truly can shine if using a remote repository as outlined above. Why did I say its fun? Enter <code>git blame</code>. Despite its name, this doesn&apos;t send an email to your co-worker to tell them they did something wrong. What it does do however is show you all the lines of a file that Git tracks and who edited which line based on the current <code>commit</code>.</p><pre><code class="language-sh">$ git blame ansible.cfg
^7ba22bd (Kyle Walker 2022-01-04 14:34:34 -0600 1) [defaults]
^7ba22bd (Kyle Walker 2022-01-04 14:34:34 -0600 2) roles_path = roles
^7ba22bd (Kyle Walker 2022-01-04 14:34:34 -0600 3) retry_files_save_path = /tmp</code></pre><p>Now obviously there is more to working with a team through Git and <code>git blame</code> and <code>git pull</code>, but for our little company we have covered all we need for using Git. There isn&apos;t much more to discuss, but there will be more topics come up in later parts of this series.</p><h2 id="git-extra-credit">Git Extra Credit</h2><p>If you really want to get the most Git, there are tools you can use to make life easier and expand on the functionality of Git. I will list these below, but none are required to use Git.</p><ul><li><a href="https://opensource.com/article/19/6/what-tig">tig</a> - A text-based repository browser that really helps &quot;view&quot; a repository</li><li><a href="https://github.com/git-chglog/git-chglog">git-chglog</a> - CHANGELOG creator for your repository based on <code>commits</code></li><li><a href="https://desktop.github.com/">GitHub Desktop</a> - GUI repository manager from GitHub</li></ul><h2 id="moving-on">Moving On</h2><p>In the next installment in the series we are going to move away from Ansible and traditional &quot;servers&quot; and start our journey to containers because our CEO heard the buzzword &quot;containers&quot; so we need to migrate to &quot;containers&quot;. And if history has taught us anything, they are going to hear about another buzzword and we will probably have to migrate to that afterwards as well. See you in the next installment!</p><h3 id="series-links">Series Links:</h3><ul><li><a href="https://blog.kywa.io/mineops-part-1/">Part 1 - Manual Minecraft Server Installation</a></li><li><a href="https://blog.kywa.io/mineops-part-2/">Part 2 - Automating with Ansible</a></li><li><a href="https://blog.kywa.io/mineops-part-3/">Part 3 - Keeping it Altogether with Git</a></li><li><a href="https://blog.kywa.io/mineops-part-4/">Part 4 - Containers Have Joined the Party</a></li><li><a href="https://blog.kywa.io/mineops-part-5/">Part 5 - Making Containers Highly Available</a></li><li><a href="https://blog.kywa.io/mineops-part-6/">Part 6 - ArgoCD to the Rescue</a></li></ul>]]></content:encoded></item><item><title><![CDATA[mineOps - Part 2: Automation with Ansible]]></title><description><![CDATA[<figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/mineops.png" class="kg-image" alt loading="lazy" width="383" height="78"></figure><p>In <a href="https://blog.kywa.io/mineops-part-1/">Part 1</a> of this series we went over manually deploying a Minecraft Server as part of a fictitious business to show their journey of growth through DevOps. In this post we will go over automating all that was done in Part 1 through Ansible. We will also touch on</p>]]></description><link>https://blog.kywa.io/mineops-part-2/</link><guid isPermaLink="false">61d28670c040b4060b5bad9c</guid><category><![CDATA[mineops]]></category><dc:creator><![CDATA[Kyle Walker]]></dc:creator><pubDate>Tue, 04 Jan 2022 02:48:06 GMT</pubDate><content:encoded><![CDATA[<figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/mineops.png" class="kg-image" alt loading="lazy" width="383" height="78"></figure><p>In <a href="https://blog.kywa.io/mineops-part-1/">Part 1</a> of this series we went over manually deploying a Minecraft Server as part of a fictitious business to show their journey of growth through DevOps. In this post we will go over automating all that was done in Part 1 through Ansible. We will also touch on some nice to haves through automation and see where we can make our lives easier for this company.</p><hr><h2 id="installing-ansible">Installing Ansible</h2><p>There are a few ways to install Ansible and all of those methods can be found in the Ansible docs: <a href="https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html">https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html</a></p><p>I tend to use <code>pip</code> to install it Ansible, but the package manager of your system will work as well. Here are some quick examples on installing Ansible</p><ul><li>python -m pip install --user ansible</li><li>yum install -y ansible</li></ul><h2 id="setting-up-our-inventory">Setting up our inventory</h2><p>If you are familiar with Ansible or have read the <a href="https://github.com/KyWa/blogs/blob/master/mineOps/docs/Ansible/ansible-primer.md">primer I wrote in the supporting mineOps GitHub repo</a>, you should be aware of what an Ansible Inventory is. A quick overview is just a file containing the servers you wish to manage with Ansible. For the moment, we are going to create this inventory file wherever we are on our &quot;control node&quot; most likely our laptop or workstation for this business.</p><figure class="kg-card kg-code-card"><pre><code class="language-ini">[minecraft]
mineops-0.kywa.io</code></pre><figcaption>Inventory file named: hosts</figcaption></figure><p>So in this inventory file we have our single Minecraft Server called <code>mineops-0.kywa.io</code>. For now our inventory is quite simple, but we will be expounding on this later in this article.</p><h2 id="automating-the-install">Automating the install</h2><p>Let&apos;s look over what all we did during the manual install process and see about converting it to Ansible.</p><p>In order we:</p><ul><li>Installed Java 17 (or later)</li><li>Downloaded the Minecraft Server <code>jar</code> file</li><li>Accepted the EULA for Minecraft Server</li><li>Ran the server</li></ul><p>After that we did a few other things such as creating a dedicated directory, a dedicated user, creating a <code>systemd</code> unit to start/stop the server without using up a terminal and creating backups of the Minecraft Server. We will begin by just automating the install going forward from there. Each of the Ansible Modules used in the beginning here will have a link to their documentation in a comment above the task.</p><p>The &quot;basic&quot; Ansible playbook can be found in the supporting repo: <a href="https://github.com/KyWa/blogs/blob/master/mineOps/files/Ansible/basic_ansible/minecraft-install.yml">https://github.com/KyWa/blogs/blob/master/mineOps/files/Ansible/basic_ansible/minecraft-install.yml</a></p><p>As you can see this is fairly straightforward and essentially what we did manually, but all in one Ansible Playbook. We could technically do all of this in a <code>BASH</code> script and it would work just fine, but the issue with BASH scripts comes scalability and extensibility. There are some sysadmins I know who would argue with me to the end of time that <code>BASH</code> scripts can automate with the best of Ansible, but we will see here in a little bit why we would want to avoid that and stick with something like Ansible.</p><p>We can give our Ansible playbook a run and see what we get. Again note I&apos;m passing an argument to <code>ansible-playbook</code> to specify my <code>ssh</code> user. This can be done in a file called <code>ansible.cfg</code> and we will get to that later in this post:</p><pre><code class="language-sh">ansible-playbook -i hosts minecraft-install.yml -e ansible_ssh_user=root</code></pre><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/mineops-ansible-basic.gif" class="kg-image" alt loading="lazy" width="1940" height="848" srcset="https://blog.kywa.io/content/images/size/w600/2022/01/mineops-ansible-basic.gif 600w, https://blog.kywa.io/content/images/size/w1000/2022/01/mineops-ansible-basic.gif 1000w, https://blog.kywa.io/content/images/size/w1600/2022/01/mineops-ansible-basic.gif 1600w, https://blog.kywa.io/content/images/2022/01/mineops-ansible-basic.gif 1940w" sizes="(min-width: 720px) 720px"></figure><p>If all went according to plan (which it should have) Ansible will have completed all the tasks we gave it and a running Minecraft Server should exist at <code>mineops-0.kywa.io:25565</code>. Please note this server isn&apos;t publicly accessible and only reachable from inside my home lab so don&apos;t bother going to test it out because it will not be there.</p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/connect_to_minecraft-1.gif" class="kg-image" alt loading="lazy" width="320" height="180"></figure><p>We have a working Minecraft Server (again)! Even though we didn&apos;t create the playbook in the first part of this blog post, the effort to write this Ansible Playbook to do essentially what we already had through a <code>BASH</code> script was a little on the high side (as is most automation), but the power isn&apos;t in automating something you could do manually, the power is in how you can use and re-use that automation. In the next section we will expound upon our Ansible Playbook and break it out into more modular Ansible items such as a roles and inventory vars.</p><p>There are a few more things we would need to do for a customer, but we can move along to the more &quot;modular&quot; approach to Ansible and tackle these other needs. To summarize the &quot;basic&quot; Ansible approach we have a playbook, an inventory file and 2 Minecraft specific files we are copying over. All of this sits in a single directory and to make any lasting changes we will have to edit this file or copy it and run it the new playbook. In the next section we are going to break this all apart and spread out most of this into an Ansible Role.</p><h2 id="upgrading-our-automation">Upgrading our Automation</h2><p>Using the more &quot;basic&quot; Ansible approach we are able to do quite a bit and get us where we need to be in terms of deploying a Minecraft Server. Now let&apos;s say the customer needs to make some changes. Maybe they want the Message of the Day changed for the server, or want to disable/enable PVP. First let&apos;s convert our single playbook into a role. This isn&apos;t a going to initially grant us any new automation ability, however it does enable us to be able to move things around and fine tune our deployments (especially if growing to more than one customer).</p><p>Compare these two directories:</p><pre><code class="language-sh">$ tree basic_ansible/
basic_ansible/
&#x251C;&#x2500;&#x2500; eula.txt
&#x251C;&#x2500;&#x2500; hosts
&#x251C;&#x2500;&#x2500; minecraft-install.yml
&#x2514;&#x2500;&#x2500; minecraft.service

0 directories, 4 files


$ tree modular_ansible/
modular_ansible/
&#x251C;&#x2500;&#x2500; ansible.cfg
&#x251C;&#x2500;&#x2500; inventory
&#x2502;&#xA0;&#xA0; &#x251C;&#x2500;&#x2500; group_vars
&#x2502;&#xA0;&#xA0; &#x2502;&#xA0;&#xA0; &#x251C;&#x2500;&#x2500; customer-0
&#x2502;&#xA0;&#xA0; &#x2502;&#xA0;&#xA0; &#x2502;&#xA0;&#xA0; &#x251C;&#x2500;&#x2500; config.yml
&#x2502;&#xA0;&#xA0; &#x2502;&#xA0;&#xA0; &#x2502;&#xA0;&#xA0; &#x2514;&#x2500;&#x2500; server-properties.yml
&#x2502;&#xA0;&#xA0; &#x2502;&#xA0;&#xA0; &#x2514;&#x2500;&#x2500; minecraft
&#x2502;&#xA0;&#xA0; &#x2502;&#xA0;&#xA0;     &#x251C;&#x2500;&#x2500; config.yml
&#x2502;&#xA0;&#xA0; &#x2502;&#xA0;&#xA0;     &#x2514;&#x2500;&#x2500; server-properties.yml
&#x2502;&#xA0;&#xA0; &#x251C;&#x2500;&#x2500; host_vars
&#x2502;&#xA0;&#xA0; &#x2502;&#xA0;&#xA0; &#x2514;&#x2500;&#x2500; mineops-0.kywa.io
&#x2502;&#xA0;&#xA0; &#x2502;&#xA0;&#xA0;     &#x251C;&#x2500;&#x2500; config.yml
&#x2502;&#xA0;&#xA0; &#x2502;&#xA0;&#xA0;     &#x2514;&#x2500;&#x2500; server-properites.yml
&#x2502;&#xA0;&#xA0; &#x2514;&#x2500;&#x2500; hosts
&#x251C;&#x2500;&#x2500; playbooks
&#x2502;&#xA0;&#xA0; &#x251C;&#x2500;&#x2500; minecraft-backup.yml
&#x2502;&#xA0;&#xA0; &#x251C;&#x2500;&#x2500; minecraft-config-change.yml
&#x2502;&#xA0;&#xA0; &#x251C;&#x2500;&#x2500; minecraft-install.yml
&#x2502;&#xA0;&#xA0; &#x251C;&#x2500;&#x2500; minecraft-restore.yml
&#x2502;&#xA0;&#xA0; &#x2514;&#x2500;&#x2500; minecraft-uninstall.yml
&#x2514;&#x2500;&#x2500; roles
    &#x2514;&#x2500;&#x2500; minecraft
        &#x251C;&#x2500;&#x2500; files
        &#x2502;&#xA0;&#xA0; &#x2514;&#x2500;&#x2500; eula.txt
        &#x251C;&#x2500;&#x2500; tasks
        &#x2502;&#xA0;&#xA0; &#x251C;&#x2500;&#x2500; backup.yml
        &#x2502;&#xA0;&#xA0; &#x251C;&#x2500;&#x2500; config-change.yml
        &#x2502;&#xA0;&#xA0; &#x251C;&#x2500;&#x2500; main.yml
        &#x2502;&#xA0;&#xA0; &#x251C;&#x2500;&#x2500; restore.yml
        &#x2502;&#xA0;&#xA0; &#x251C;&#x2500;&#x2500; server-prep.yml
        &#x2502;&#xA0;&#xA0; &#x251C;&#x2500;&#x2500; start-server.yml
        &#x2502;&#xA0;&#xA0; &#x2514;&#x2500;&#x2500; uninstall.yml
        &#x2514;&#x2500;&#x2500; templates
            &#x251C;&#x2500;&#x2500; minecraft.service.j2
            &#x2514;&#x2500;&#x2500; server.properties.j2

12 directories, 23 files
</code></pre><p>As you can see there is much more going on in the &quot;modular&quot; section. There is one item to point out and that the files under <code>host_vars/servername</code> and <code>group_vars/groupname</code> do not matter. Their names and layout are purely for those who use Ansible to be able to identify which files contain which variables. Ultimately it is the same data as in the basic section, but more spread out and &quot;modular&quot;. One of the largest changes is in the inventory section. Our previous example had a single inventory file with nothing special about it and if we attempted to add variables to it, we would quickly lose our ability to easily see what we had. Enter group and host vars. Breaking up variables per group and even per host can help us tackle multiple instances and multiple customers with ease.</p><pre><code class="language-sh">$ cat inventory/hosts
[minecraft:children]
customer-0

[customer-0]
mineops-0.kywa.io


$ tree inventory/
inventory/
&#x251C;&#x2500;&#x2500; group_vars
&#x2502;&#xA0;&#xA0; &#x251C;&#x2500;&#x2500; customer-0
&#x2502;&#xA0;&#xA0; &#x2502;&#xA0;&#xA0; &#x2514;&#x2500;&#x2500; config.yml # Contains Customer specific variables
&#x2502;&#xA0;&#xA0; &#x2514;&#x2500;&#x2500; minecraft
&#x2502;&#xA0;&#xA0;     &#x2514;&#x2500;&#x2500; config.yml # Contains variables for ALL Minecraft instances
&#x251C;&#x2500;&#x2500; host_vars
&#x2502;&#xA0;&#xA0; &#x2514;&#x2500;&#x2500; mineops-0.kywa.io
&#x2502;&#xA0;&#xA0;     &#x2514;&#x2500;&#x2500; config.yml # Contains Server specific variables</code></pre><p>With our example inventory file and the comments of which <code>config.yml</code> file contains what, it should be apparent the benefits of using this model. Going forward with Ansible and having multiple customers, it may happen quickly where you end up with multiple variable files in your group/host var directories. This is expected and placing your variables in files related to what they are will help greatly as scale occurs. Obviously you can throw every variable into a <code>config.yml</code>, but that isn&apos;t very descriptive. Minecraft Server may not have many variable files, but in other projects where you would use Ansible, this can grow to some very large files and separating them out will greatly increase your sanity when hunting for them.</p><p>One such inventory variable file will be <code>server-properites.yml</code> and as you can probably guess, this will mirror the <code>server.properties</code> file that Minecraft Server creates. Remember when I mentioned things we would need to change for a customer? Well this is where we can do that (as this is really the only area they would want things changed in most circumstances). In the &quot;modular&quot; Ansible directory we can see I have converted the <code>server.properites</code> into a new file called <code>server.properites.j2</code>. Ansible utilizes the <code>Jinja2</code> templating language and we can make use of it here. As with any variable you can see all of the properties values now equal an Ansible variable:</p><pre><code class="language-sh">$ cat roles/minecraft/templates/server.properties.j2
enable-jmx-monitoring={{ enable_jmx_monitoring }}
rcon.port={{ rcon_port }}
gamemode={{ gamemode }}
enable-command-block={{ enable_command_block }}
enable-query={{ enable_query }}
level-name={{ level_name }}
motd={{ motd }}
query.port={{ minecraft_query_port }}
pvp={{ pvp }}
difficulty={{ difficulty }}
~~~~~OMITTED~~~~~</code></pre><p>Each of these variables (outlined by their <code>{{ name }}</code> syntax) has a corresponding variable set in <code>modular_ansible/inventory/group_vars/minecraft/server-properties.yml</code>. </p><pre><code class="language-sh">$ cat inventory/group_vars/minecraft/server-properties.yml
enable_jmx_monitoring: &quot;false&quot;
rcon_port: 25575
gamemode: survival
enable_command_block: &quot;false&quot;
enable_query: &quot;false&quot;
level_name: world
motd: A Minecraft Server
minecraft_query_port: 25565
pvp: &quot;true&quot;
difficulty: easy
~~~~~OMITTED~~~~~</code></pre><p>Handling our variables in this way allows us to push out changes per customer as we will see in just a moment. As we saw in our breakdown earlier of the inventory file <code>hosts</code> up above we see that we have a group called: <code>customer-0</code>. This is a unique group that is part of the <code>minecraft</code> group and will inherit all variables in the <code>inventory/group_vars/minecraft/</code> directory. However with the way that <a href="https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#understanding-variable-precedence">variable precedence</a> works in Ansible, if we place customer specific variables in their groups_vars or host_vars they will be what is ultimately pushed out to the server. Here is an example with our &quot;modular&quot; Ansible:</p><pre><code class="language-sh">$ grep -r motd inventory/
modular_ansible/inventory/group_vars/customer-0/server-properties.yml:motd: &quot;Welcome to customer-0 Minecraft Servers!&quot;
modular_ansible/inventory/group_vars/minecraft/server-properties.yml:motd: &quot;A Minecraft Server&quot;

$ cat modular_ansible/inventory/hosts
[minecraft:children]
customer-0

[customer-0]
mineops-0.kywa.io</code></pre><p>In the above output we have 2 <code>motd</code> variables set, only one of them will be used. From what we see in our inventory file, the child group <code>customer-0</code> will take precedence over the &quot;parent&quot; <code>minecraft</code> group and the <code>server.properties</code> file will have the <code>motd</code> of: &quot;Welcome to customer-0 Minecraft Servers!&quot;. This will remain the same for all Ansible you utilize so there is not much more need for examples around this. If you wish to know more about Ansible and variables always check the docs: <a href="https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html">https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html</a>. Another item of note around variables is their names, with some of them being special and reserved inside of Ansible. There is a doc outlining each of them and their use: <a href="https://docs.ansible.com/ansible/latest/reference_appendices/special_variables.html">https://docs.ansible.com/ansible/latest/reference_appendices/special_variables.html</a></p><h2 id="operating-a-minecraft-server">Operating a Minecraft Server</h2><p>I&apos;ve created a few tasks in the role <code>minecraft</code> to handle a few nice things for us and our customers. We have a backup, restore and config-change task as well as an unfortunate uninstall if a customer decides to leave us. We can demo these out, but they are fairly straightforward. Let&apos;s at least look at the backup task to get an idea of what we are trying to accomplish.</p><h3 id="backing-up-a-minecraft-server">Backing up a Minecraft Server</h3><pre><code>- name: Mount the Minecraft Server Backup Share
  mount:
    path: /mnt
    src: &quot;{{ minecraft_backup_share }}&quot;
    fstype: nfs
    state: mounted

- name: Backup the systemd servie
  copy:
    remote_src: yes
    src: /usr/lib/systemd/system/minecraft.service
    dest: /mnt/minecraft.service

- name: Stop the Minecraft Server Service
  systemd:
    name: minecraft
    state: stopped

- name: Archive and Backup the Minecraft Server Directory
  archive:
    path: &quot;{{ minecraft_directory }}/*&quot;
    dest: &quot;/mnt/minecraft-backup.tgz&quot;
    format: gz
    mode: 0644

- name: Start the Minecraft Server Service
  systemd:
    name: minecraft
    state: started

- name: Unmount the Minecraft Server Backup Share
  mount:
    path: /mnt
    state: unmounted
</code></pre><p>In this particular instance we are using an external NFS server to hold our backup data, but this could technically also be Google Drive, a Windows SMB server or anywhere where files are accepted. I will be using NFS as its the easiest to setup and get working in a Linux environment. The task above mounts a share (NFS) stops Minecraft Server, copies the data over to the share along with its <code>systemd</code> service, then restarts Minecraft Server and finally removes the backup share to create an &quot;air gap&quot; of sorts. This last step is important to not leave your backup shares mounted to any system where you hold your backups dear to you. Most Ransomware goes through all mounted directories and disks in Linux and Windows. Keeping your backups disconnected from your production servers will save you heartache, time and money down the road.</p><h3 id="reconfiguring-a-minecraft-server">Reconfiguring a Minecraft Server</h3><p>Now that we have a customer with a running server and we&apos;ve automated its install, they want to make a change. What do we need to change and where do we keep that data? As mentioned previously, most changes will take place in the <code>server.properties</code> file which we have templatized and put on the each server during install time. If a customer wants to make changes away from the default configuration we provide, the customers server should have their own <code>server-properties.yml</code> updated in the host_vars section of the inventory. Let&apos;s take a look at this example, the customer wishes to remove PVP from their world because they want to invite a bunch of people, but don&apos;t want to have to worry about their friends losing their stuff. Here is what we would add:</p><pre><code class="language-sh">$ cat inventory/host_vars/mineops-0.kywa.io/server-properites.yml
pvp: &quot;false&quot;</code></pre><p>In the &quot;basic&quot; Ansible to push this change out, we would have to do quite a bit to go and edit the <code>server.properties</code> file on the Minecraft Server and restart the server. Here is what it would look to complete this in the &quot;basic&quot; Ansible fashion:</p><ul><li>Add a task to run the <code>lineinfile</code> module against the <code>server.properties</code> file to change the requested property</li><li>Add another task to the playbook to stop the server and another to start it back</li></ul><p>If we add this to our existing playbook we won&apos;t disrupt anything, but a lot of steps will have to be gone through again (even if they are already completed) just to make this one change. By using the &quot;modular&quot; Ansible approach via a role we can have a task file for a start of the server such as <code>start-server.yml</code>. This task file can be called from other tasks in a role via the <code>include_tasks: mytask.yml</code> parameter making it far easier to extend your roles with other tasks you have created. This makes it far easier to add in tasks without having your primary playbook grow to unnecessary lengths.</p><p>For the sake of reconfiguring a Minecraft Server, we can use the task in the &quot;modular&quot; Ansible role called <code>config-change.yml</code>. Let&apos;s take a look at this task:</p><pre><code>---
- name: Create ResourcePack directory
  file:
    path: &quot;{{ minecraft_directory }}/resourcepack&quot;
    owner: &quot;{{ minecraft_user }}&quot;
    group: &quot;{{ minecraft_group }}&quot;
    state: directory
    mode: 0755
  when: resource_pack is defined

- name: Create server.properties from template
  template:
    src: server.properties.j2
    dest: &quot;/tmp/server.properties&quot;

- name: Create server.properties from template
  copy:
    remote_src: true
    src: /tmp/server.properties
    dest: &quot;{{ minecraft_directory }}/server.properties&quot;

- name: Stop the Minecraft Server
  systemd:
    name: minecraft
    state: stopped

- name: Starting up Minecraft
  include_tasks: start-server.yml</code></pre><p>This task will create a directory for resource packs if this config has been defined, create a new <code>server.properties</code> file from our template, stop the Minecraft Server, put the new <code>server.properties</code> file in place and issue the task to start the Minecraft Server up. The last section as you can see is calling the <code>include_tasks</code> we discussed earlier. This particular task is the same task we use during our main install in the &quot;modular&quot; Ansible configuration.</p><h2 id="adding-a-new-customer">Adding a new Customer</h2><p>As a business we&apos;ve grown and through word of mouth, we have another customer at last! Using our &quot;modular&quot; Ansible we can add in their new group/host vars and update our inventory file and we can provision their new instance. Let&apos;s look at the updated inventory file and inventory vars:</p><pre><code class="language-sh">$ cat inventory/hosts
[minecraft:children]
customer-0
customer-1

[customer-0]
mineops-0.kywa.io

[customer-1]
mineops-1.kywa.io

$ tree inventory/
inventory/
&#x251C;&#x2500;&#x2500; group_vars
&#x2502;&#xA0;&#xA0; &#x251C;&#x2500;&#x2500; customer-0
&#x2502;&#xA0;&#xA0; &#x2502;&#xA0;&#xA0; &#x251C;&#x2500;&#x2500; config.yml
&#x2502;&#xA0;&#xA0; &#x2502;&#xA0;&#xA0; &#x2514;&#x2500;&#x2500; server-properties.yml
&#x2502;&#xA0;&#xA0; &#x251C;&#x2500;&#x2500; customer-1
&#x2502;&#xA0;&#xA0; &#x2502;&#xA0;&#xA0; &#x251C;&#x2500;&#x2500; config.yml
&#x2502;&#xA0;&#xA0; &#x2502;&#xA0;&#xA0; &#x2514;&#x2500;&#x2500; server-properties.yml
&#x2502;&#xA0;&#xA0; &#x2514;&#x2500;&#x2500; minecraft
&#x2502;&#xA0;&#xA0;     &#x251C;&#x2500;&#x2500; config.yml
&#x2502;&#xA0;&#xA0;     &#x2514;&#x2500;&#x2500; server-properties.yml
&#x251C;&#x2500;&#x2500; host_vars
&#x2502;&#xA0;&#xA0; &#x251C;&#x2500;&#x2500; mineops-0.kywa.io
&#x2502;&#xA0;&#xA0; &#x2502;&#xA0;&#xA0; &#x251C;&#x2500;&#x2500; config.yml
&#x2502;&#xA0;&#xA0; &#x2502;&#xA0;&#xA0; &#x2514;&#x2500;&#x2500; server-properites.yml
&#x2502;&#xA0;&#xA0; &#x2514;&#x2500;&#x2500; mineops-1.kywa.io
&#x2502;&#xA0;&#xA0;     &#x251C;&#x2500;&#x2500; config.yml
&#x2502;&#xA0;&#xA0;     &#x2514;&#x2500;&#x2500; server-properites.yml
&#x2514;&#x2500;&#x2500; hosts

7 directories, 11 files</code></pre><p>We&apos;ve created our new variables and updated our inventory file, how do we deploy this new customer server? There are a few ways with the first of which being to just run the <code>playbooks/minecraft-install.yml</code> again, but this will attempt to do this for all servers in the inventory file. This isn&apos;t going to cause any issues because we have built our Ansible to not do any damaging or disruptive tasks. Ansible itself only &quot;makes changes&quot; to items that require changes. For instance the <code>Install Java</code> task which uses the <code>dnf</code> module will check to see if <code>java-17-openjdk</code> is installed and only install it IF it isn&apos;t there already. Now there is a caveat to this, the <code>shell</code> and <code>cmd</code> modules just run whatever commands you pass them so this is just something to be aware of.</p><p>Due to the caveat above regarding the <code>shell</code> and <code>cmd</code> modules in our <code>server-prep.yml</code> task file we have a <code>stat</code> task which will verify the existence of the <code>server.jar</code>. This will prevent the task downloading a new <code>server.jar</code> and ultimately ensure we do not replace it with a newer or different version. </p><p>To deploy just the new customers server we can run our install playbook with a flag at runtime called <code>--limit</code>. This limits the playbook to just the group or host you specify. So to deploy the new customers server, all we would have to do is run <code>ansible-playbook</code> like so:</p><pre><code>ansible-playbook -i inventory/hosts playbooks/minecraft-install.yml --limit mineops-1.kywa.io</code></pre><p>You can use the host or the group name inside of <code>--limit</code>, but for making changes to an entire customer&apos;s group of servers, you would most likely (depending on the change) run the <code>--limit</code> against their group as opposed to a single host.</p><h2 id="storing-our-ansible-files">Storing our Ansible files</h2><p>By this point we have our business running with our customers happy and all of these Ansible files (hopefully the &quot;modular&quot; kind), but what happens to these files if your computer crashes? Where are these files being stored? What if the team grows beyond you and you have other people updating these config files? Do you use DropBox or Google Drive and share out these files? You could do that and many businesses get away with this, but there is a better way and that way is through a Version Control System.</p><p>There are a few VCS or Source Control systems out there, but there really is only one that people use across the board and that is called <code>git</code>. Git has pretty much taken over every workflow out there and is even used for greater deeds than just versioning source code. This however will be tackled in the next blog post. See you for the next one!</p><h3 id="series-links">Series Links:</h3><ul><li><a href="https://blog.kywa.io/mineops-part-1/">Part 1 - Manual Minecraft Server Installation</a></li><li><a href="https://blog.kywa.io/mineops-part-2/">Part 2 - Automating with Ansible</a></li><li><a href="https://blog.kywa.io/mineops-part-3/">Part 3 - Keeping it Altogether with Git</a></li><li><a href="https://blog.kywa.io/mineops-part-4/">Part 4 - Containers Have Joined the Party</a></li><li><a href="https://blog.kywa.io/mineops-part-5/">Part 5 - Making Containers Highly Available</a></li><li><a href="https://blog.kywa.io/mineops-part-6/">Part 6 - ArgoCD to the Rescue</a></li></ul>]]></content:encoded></item><item><title><![CDATA[mineOps - Part 1: Manual Minecraft Server Installation]]></title><description><![CDATA[<figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2021/12/mineops-2.png" class="kg-image" alt loading="lazy" width="383" height="78"></figure><p>As was discussed in the <a href="https://blog.kywa.io/mineops-intro/">Introduction</a> to this series, we will be focusing on deploying Minecraft Servers as part of a fictitious business. This business needs to get up and going with their first customer, so we need to get at least one Minecraft Server up and running. Let&apos;</p>]]></description><link>https://blog.kywa.io/mineops-part-1/</link><guid isPermaLink="false">61cbc4d0291871bea17de644</guid><category><![CDATA[mineops]]></category><dc:creator><![CDATA[Kyle Walker]]></dc:creator><pubDate>Mon, 03 Jan 2022 06:01:47 GMT</pubDate><content:encoded><![CDATA[<figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2021/12/mineops-2.png" class="kg-image" alt loading="lazy" width="383" height="78"></figure><p>As was discussed in the <a href="https://blog.kywa.io/mineops-intro/">Introduction</a> to this series, we will be focusing on deploying Minecraft Servers as part of a fictitious business. This business needs to get up and going with their first customer, so we need to get at least one Minecraft Server up and running. Let&apos;s get going!</p><hr><h3 id="note">NOTE:</h3><p>Throughout this series we will be focusing our efforts on <strong>RPM based Operating Systems</strong> (RHEL, CentOS, Fedora etc...). Almost all of the examples will carry over with a few exceptions to Debian based Operating Systems (Debian, Ubuntu etc...) and we will call out those differences where they come up.</p><p>Another item to note is this guide will not outline the creation of any Virtual Machines or the use of any cloud providers. This series will use oVirt for the virtual machines used, but will not come into play. Note that Ansible can be used to provision Virtual Machines in whichever platform you happen to utilize, but will not be discussed in this series.</p><p>One final item of note is that &quot;sudo&quot; will not be used throughout this guide. All commands will assume you have root privileges on the servers.</p><hr><h3 id="minecraft-server-prerequisites">Minecraft Server Prerequisites</h3><p>CPU and Memory resources will be low for this series, but in a real world deployment you would want to ensure you have ample resources for each of the servers being used. 2 vCPU and 4GB of Ram per server will be enough for the purposes of this series.</p><p>Outside of resources one of the primary and nearly only requirement for running a Minecraft Server is <code>java</code>. There are a few ways to do this, but the easiest of which is to use the package manager of your system. For RPM based operating systems this can be done with a simple command:</p><pre><code class="language-sh">dnf install -y java-17-openjdk</code></pre><p>Java 17 is installed as it is the &quot;oldest&quot; version compatible with Minecraft Server.</p><h3 id="acquiring-minecraft-server">Acquiring Minecraft Server</h3><p>To run a Minecraft Server you will need to obtain the <code>server.jar</code> from Minecraft.net. You can obtain the URL for the file by copying the download link <a href="https://www.minecraft.net/en-us/download/server/">here</a>. You can either obtain the link and download it directly from the &quot;to be&quot; Minecraft Server (using <code>curl</code> or <code>wget</code>) or you can download it to your local machine and copy it over via <code>scp</code>, <code>rsync</code> or some other method.</p><p>In the spirit of automating and making life easier overall, we have a much quicker path. The server will need <code>jq</code> installed and can be handled in a simple one-liner (if you already have <code>jq</code> installed on your server you can skip this step):</p><pre><code class="language-sh">curl -o /usr/bin/jq -sL https://github.com/stedolan/jq/releases/download/jq-1.6/jq-linux64 &amp;&amp; chmod +x /usr/bin/jq</code></pre><p>This will place <code>jq</code> in your <code>/usr/bin/</code> which should be in the path, but this now enables us to run and grab the latest Minecraft Server <code>.jar</code> file. Here is a handy script to grab the latest release of Minecraft Server:</p><pre><code class="language-sh">curl -o server.jar $(curl `curl -sL https://launchermeta.mojang.com/mc/game/version_manifest.json | jq -r &apos;.latest.release as $release | .versions[] | select (.id == $release) | .url&apos;` | jq -r &apos;.downloads.server.url&apos;)</code></pre><p>The above script will reach out to Mojang servers and look through the latest releases and grab whichever happens to be the latest. </p><p>If you wish to obtain a specific version of Minecraft Server (which would need to for some customers) you can run the same script, but with a slightly different lookup. Notice we have removed early parts of the first <code>jq</code> query and turned <code>$release</code> into <code>&quot;1.18.1&quot;</code> where <code>&quot;1.18.1&quot;</code> is the specific version you are requiring:</p><pre><code class="language-sh">curl -o server.jar $(curl `curl -sL https://launchermeta.mojang.com/mc/game/version_manifest.json | jq -r &apos;.versions[] | select (.id == &quot;1.18.1&quot;) | .url&apos;` | jq -r &apos;.downloads.server.url&apos;)</code></pre><h3 id="starting-minecraft-server">Starting Minecraft Server</h3><p>We are just about to spin up our first Minecraft Server, but there is only one item that will block us from starting. If you attempt to run Minecraft Server without accepting the <a href="https://account.mojang.com/documents/minecraft_eula">EULA</a>, you will get a failure similar to what you see below:</p><pre><code class="language-sh">java -jar server.jar
&lt;OMITTED OUTPUT&gt;
[22:59:42] [ServerMain/ERROR]: Failed to load properties from file: server.properties
[22:59:42] [ServerMain/WARN]: Failed to load eula.txt
[22:59:42] [ServerMain/INFO]: You need to agree to the EULA in order to run the server. Go to eula.txt for more info.</code></pre><p>However you can prevent this message by accepting the EULA with another simple one-liner:</p><pre><code class="language-sh">echo &quot;eula=true&quot; &gt; eula.txt</code></pre><p>Once you have &quot;accepted&quot; the EULA, Minecraft Server will be able to start, thus getting us one step closer to having customers paying for this service. Let&apos;s give it a go. Fair warning depending on the server&apos;s resources, the initial start-up may take a minute or more.</p><pre><code class="language-sh">$ java -jar server.jar
Starting net.minecraft.server.Main
[23:03:08] [ServerMain/INFO]: Environment: authHost=&apos;https://authserver.mojang.com&apos;, accountsHost=&apos;https://api.mojang.com&apos;, sessionHost=&apos;https://sessionserver.mojang.com&apos;, servicesHost=&apos;https://api.minecraftservices.com&apos;, name=&apos;PROD&apos;
[23:03:09] [ServerMain/INFO]: Reloading ResourceManager: Default
[23:03:10] [Worker-Main-2/INFO]: Loaded 7 recipes
[23:03:11] [Worker-Main-2/INFO]: Loaded 1141 advancements
[23:03:13] [Server thread/INFO]: Starting minecraft server version 1.18.1
[23:03:13] [Server thread/INFO]: Loading properties
[23:03:13] [Server thread/INFO]: Default game type: SURVIVAL
[23:03:13] [Server thread/INFO]: Generating keypair
[23:03:14] [Server thread/INFO]: Starting Minecraft server on *:25565
[23:03:14] [Server thread/INFO]: Using epoll channel type
[23:03:14] [Server thread/INFO]: Preparing level &quot;world&quot;
[23:03:22] [Server thread/INFO]: Preparing start region for dimension minecraft:overworld
[23:03:24] [Worker-Main-2/INFO]: Preparing spawn area: 0%
&lt;OMITTED OUTPUT&gt;
[23:04:27] [Worker-Main-2/INFO]: Preparing spawn area: 84%
[23:04:27] [Worker-Main-2/INFO]: Preparing spawn area: 84%
[23:04:31] [Server thread/INFO]: Time elapsed: 68584 ms
[23:04:31] [Server thread/INFO]: Done (77.021s)! For help, type &quot;help&quot;</code></pre><h3 id="verifying-connectivity-to-minecraft-server">Verifying Connectivity to Minecraft Server</h3><p>At this point we can try to connect to our newly deployed Minecraft Server. If you are familiar with Minecraft, you will know to click &quot;Multiplayer&quot; and then for our tests click &quot;Direct Connect&quot;. You will need to input the IP address of your server and the default port for Minecraft Server which is <code>25565</code>. This will attempt a connection to your Minecraft Server. </p><p>If you are unsure of the IP address of your Minecraft Server you can run <code>ip address</code> on your system and look for the primary NIC:</p><figure class="kg-card kg-code-card"><pre><code class="language-sh">$ ip address
1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: enp1s0: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 56:6f:0d:9a:00:00 brd ff:ff:ff:ff:ff:ff
    inet 10.0.0.50/24 brd 10.0.0.255 scope global noprefixroute enp1s0
       valid_lft forever preferred_lft forever
    inet6 fe80::c0ea:7070:3ce9:7c0f/64 scope link noprefixroute
       valid_lft forever preferred_lft forever</code></pre><figcaption>Obtaining the primary NIC IP address</figcaption></figure><p>We may run into one more blocker before the Minecraft Server is operational and that is a firewall blocking the port.</p><p>If you attempt to connect to your server by putting into the &quot;Server Address:&quot; field the <code>ip-address:port</code> of the server you deployed Minecraft Server to, you may run into something like you see in the video &quot;Connection Refused&quot;. To prevent this from being an issue, we will need to open up a port in the firewall for Minecraft.</p><pre><code class="language-sh">firewall-cmd --add-port=25565/tcp</code></pre><p>Please note the above command is not persistent and will not survive a server reboot. To have this persist your command will look slightly different:</p><pre><code class="language-sh">firewall-cmd -add-port=25565/tcp --permanent &amp;&amp; firewall-cmd --reload</code></pre><p>The default port for Minecraft is <code>25565</code> and can be changed in a file we are about to look at (before runtime). With the firewall opened and the server running, we should be able to connect to our newly deployed Minecraft Server.</p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2022/01/connect_to_minecraft.gif" class="kg-image" alt loading="lazy" width="320" height="180"></figure><h3 id="tuning-minecraft-server">Tuning Minecraft Server</h3><p>At this point we have a running Minecraft Server, but what is granting configuration to it? If we hit <code>CTRL+C</code> the server will exit, but we can take a look at the files that now exist in our directory. There are a few files of note, but the rest can be reviewed at the <a href="https://minecraft.fandom.com/wiki/Minecraft_Wiki">Minecraft Wiki</a>. For the purposes of configuring this Minecraft Server for our customer we will focus on <code>server.properties</code>, but as you can see there are many configuration files that now exist in our directory:</p><pre><code class="language-sh">$ ls -lhF
total 45M
-rw-r--r--.  1 root root    2 Dec 28 23:08 banned-ips.json
-rw-r--r--.  1 root root    2 Dec 28 23:08 banned-players.json
-rw-r--r--.  1 root root   10 Dec 28 23:03 eula.txt
drwxr-xr-x.  8 root root   77 Dec 28 22:59 libraries/
drwxr-xr-x.  2 root root   78 Dec 28 23:08 logs/
-rw-r--r--.  1 root root    2 Dec 28 23:08 ops.json
-rw-r--r--.  1 root root  45M Dec 28 22:59 server.jar
-rw-r--r--.  1 root root 1.1K Dec 28 23:08 server.properties
-rw-r--r--.  1 root root  109 Dec 28 23:12 usercache.json
drwxr-xr-x.  3 root root   20 Dec 28 22:59 versions/
-rw-r--r--.  1 root root    2 Dec 28 23:03 whitelist.json
drwxr-xr-x. 12 root root 4.0K Dec 28 23:15 world/
</code></pre><p>Inside <code>server.properties</code> we have quite a few tunable variables to look into. Some of these include the port in which Minecraft Server runs on, or whether or not to enable PVP (Player Versus Player actions). Each of these settings would be up to the customer to decide which to use, but we will need to tune these to meet customer demands.</p><h3 id="operating-a-minecraft-server">Operating a Minecraft Server</h3><p>Now that we have a running server and are able to make changes to its configuration, how do we keep it running? What about backing up the data for the customer in case there is an issue either from their side or yours? We will quickly focus on these items here.</p><p>For keeping a server running and having it run without taking up a shell/terminal, we will need to have a system service run it for us. There are a few things to consider to ensure we have a clean running environment. Let&apos;s outline those below:</p><ul><li>A directory dedicated to this server</li><li>A user to be the &quot;owner&quot; of the server</li><li>A <code>systemd</code> unit file to manage the state of the server</li></ul><p>Some of these can be handled quite easily. For the dedicated directory we can create it via <code>mkdir -p /opt/minecraft</code> as its a logical place and out of the normal paths. If we are going to deploy multiple servers for customers, they will each need their own unique location. A unique directory should be created for each server using their customer ID or email address. If a customer has multiple servers, you could create a directory with a unique world name (customer choice) like so: <code>mkdir -p /opt/myfunworld-00012345</code>.</p><p>For the user to be the &quot;manager&quot; of the Minecraft Server we can just run <code>useradd minecraft</code>. However if we are going to have multiple customer servers running, it may be more logical to have a username that is inline with the customer. This would be most likely their customer ID or email address of the customer. For now we will just use <code>minecraft</code>.</p><p>The final piece would be the <code>systemd</code> service unit to keep the server running for us without having a terminal dedicated to running <code>java -jar server.jar</code>. Let&apos;s create a systemd unit file.</p><pre><code class="language-sh">cat &lt;&lt; EOF &gt;&gt; minecraft.service
[Unit]
Description=start and stop the minecraft-server

[Service]
WorkingDirectory=/opt/minecraft/

User=minecraft
Group=minecraft
Restart=on-failure
RestartSec=20 5

ExecStart=/usr/bin/java -Xms512M -Xmx3048M -jar server.jar nogui

[Install]
WantedBy=multi-user.target
EOF</code></pre><p>If you notice the line <code>ExecStart</code> you will see a similar command to what we used to run the server the first time, however this command has a few new flags included. <code>-Xms</code> and <code>-Xmx</code> are the minimum and maximum amounts of memory the JVM can use while running Minecraft. The <code>nogui</code> flag tells Java to not run a Java GUI for the application. You may have noticed (if your server has a GUI) when you initially ran the commands for <code>java -jar server.jar</code> a window pop-up showing logs and a possibly resource utilization. This isn&apos;t something we want running as its wasting resources for what needs to be used for our Minecraft Server.</p><p>Now lets get this <code>systemd</code> unit file into place and enable it. </p><pre><code class="language-sh">cp minecraft.service /usr/lib/systemd/system/ &amp;&amp; systemctl enable --now minecraft</code></pre><p>So something that will need to be done from time to time is to back-up this server and its configurations you will need to copy the contents of <code>/opt/minecraft</code> to some other location (ideally not connected to the server). We will not be going over a script to accomplish this because it will be very different for each environment. However the principal is the same, copy the data somewhere else except to cleanly grab the data, you will need to stop the Minecraft Server first.</p><h3 id="outcome">Outcome</h3><p>After all this effort to get one server going, what about the next customer? Quite a bit was done just to get a single server running and if we had to do this for multiple customers or multiple customers with multiple servers, it would become quite chaotic very quickly. What if all of these servers had multiple configuration changes? How would we manage and maintain all of them? </p><p>The next two items we are going to look at in the next few blog posts are Ansible for automating all of the deploys and Git to keep it all in line.</p><h3 id="series-links">Series Links:</h3><ul><li><a href="https://blog.kywa.io/mineops-part-1/">Part 1 - Manual Minecraft Server Installation</a></li><li><a href="https://blog.kywa.io/mineops-part-2/">Part 2 - Automating with Ansible</a></li><li><a href="https://blog.kywa.io/mineops-part-3/">Part 3 - Keeping it Altogether with Git</a></li><li><a href="https://blog.kywa.io/mineops-part-4/">Part 4 - Containers Have Joined the Party</a></li><li><a href="https://blog.kywa.io/mineops-part-5/">Part 5 - Making Containers Highly Available</a></li><li><a href="https://blog.kywa.io/mineops-part-6/">Part 6 - ArgoCD to the Rescue</a></li></ul>]]></content:encoded></item><item><title><![CDATA[DevOps Through Minecraft]]></title><description><![CDATA[<h2 id="what-is-devops">What is DevOps?</h2><p>Development + Operations aka DevOps has been a buzz word for some quite some time now. Most people in the Technology industry probably have some idea of what it is or what they perceive it to be. Kelsey Hightower has once called it &quot;Support groups for sysadmins&</p>]]></description><link>https://blog.kywa.io/mineops-intro/</link><guid isPermaLink="false">61cb7cbd291871bea17de540</guid><category><![CDATA[mineops]]></category><dc:creator><![CDATA[Kyle Walker]]></dc:creator><pubDate>Mon, 03 Jan 2022 06:01:27 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1593642632559-0c6d3fc62b89?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wxfDF8YWxsfDExfHx8fHx8Mnx8MTY0MDc0MjEyMA&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<h2 id="what-is-devops">What is DevOps?</h2><img src="https://images.unsplash.com/photo-1593642632559-0c6d3fc62b89?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wxfDF8YWxsfDExfHx8fHx8Mnx8MTY0MDc0MjEyMA&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" alt="DevOps Through Minecraft"><p>Development + Operations aka DevOps has been a buzz word for some quite some time now. Most people in the Technology industry probably have some idea of what it is or what they perceive it to be. Kelsey Hightower has once called it &quot;Support groups for sysadmins&quot; which isn&apos;t entirely untrue. A more specific definition can be found here from Red Hat:</p><blockquote>DevOps is an approach to culture, automation, and platform design intended to deliver increased business value and responsiveness through rapid, high-quality service delivery. This is all made possible through fast-paced, iterative IT service delivery. DevOps means linking legacy apps with newer cloud-native apps and infrastructure. <a href="https://www.redhat.com/en/topics/devops">https://www.redhat.com/en/topics/devops</a></blockquote><h2 id="mineopsexploring-devops-through-minecraft">mineOps - Exploring DevOps through Minecraft</h2><p>Throughout this series, we will explore DevOps in a few ways. First of which, we will see what scenarios would make us want to adopt DevOps tooling and methods. Secondly, we will explore the progression from manual installations all the way up to deploying to Kubernetes and using GitOps to maintain said deployment. There is a long way to go from manual installs to using ArgoCD. Let&apos;s take a look at the technologies that will be used and discussed in this series.</p><h3 id="technology-overview">Technology Overview</h3><ul><li><a href="https://www.minecraft.net/en-us/download/server/">Minecraft Server</a> - Java Game Server</li><li><a href="https://www.ansible.com/">Ansible</a> - Configuration Management</li><li><a href="https://git-scm.com/">Git</a> - Version Control System</li><li>Containers (<a href="https://www.docker.com/">Docker</a>/<a href="https://podman.io/">Podman</a>) - Application Deployment</li><li><a href="https://kubernetes.io/">Kubernetes</a> - Scheduled and Manages Containerized Workloads</li><li><a href="https://argoproj.github.io/cd/">ArgoCD</a> - Declarative, GitOps continuous delivery tool for Kubernetes.</li></ul><p>Each of the above technologies have a unique place in the world of IT. Some of them are powerful in their own right even if not combined with others in this list. Git for instance is used by <a href="https://medium.com/@vanessainpixels/git-for-writers-write-fiction-like-a-good-programmer-ea6f0309a69a">writers</a> and others not even in the technology field, while others on this list can do nothing without other items on the list (ArgoCD for example only working with Kubernetes).</p><h2 id="principles-of-this-series">Principles of this series</h2><p>Focusing on using modern automation tools to deploy and manage Minecraft Server(s) through the lens of a business. This business will be attempting to grow by deploying and managing Minecraft Servers for their customers. Here are some of the questions we hope to answer throughout this series:</p><ul><li>What are the shortcomings of manual deployment/installations?</li><li>How does automation helps solve this?</li><li>Why version control your configurations?</li><li>How do containers help this effort?</li><li>Why add GitOps into the DevOps mix?</li></ul><p>One of the other key points I&apos;d like to point out is that this series (as with most) will primarily focus on the &quot;Ops&quot; side of DevOps. Some of the principals learned through this will undoubtedly apply to the &quot;Dev&quot; side, but Ops will be the focus, hence the name of this series: &quot;mineOps&quot;.</p><p>The end focus of all of this will be showing the DevOps journey a business might take and the trials therein.</p><h2 id="conclusion">Conclusion</h2><p>This series hopes to bring examples on &quot;how to&quot; manage an application most people can relate to. Tutorials can be found everywhere regarding a basic HTTP application that says &quot;Hello World&quot; and gets changed to &quot;Hello &lt;something else&gt;&quot;. Not everyone relates to those changes or sees the value in what is being presented. I myself was one of those people years ago and the hope of this series is to showcase a real world application (Minecraft Servers) in a DevOps focused way.</p><p>For those who wish to skip ahead or follow along, the main GitHub repository for all files and documents being used can be found here: <a href="https://github.com/KyWa/blogs/tree/master/mineOps">https://github.com/KyWa/mineOps</a></p><h3 id="series-links">Series Links:</h3><ul><li><a href="https://blog.kywa.io/mineops-part-1/">Part 1 - Manual Minecraft Server Installation</a></li><li><a href="https://blog.kywa.io/mineops-part-2/">Part 2 - Automating with Ansible</a></li><li><a href="https://blog.kywa.io/mineops-part-3/">Part 3 - Keeping it Altogether with Git</a></li><li><a href="https://blog.kywa.io/mineops-part-4/">Part 4 - Containers Have Joined the Party</a></li><li><a href="https://blog.kywa.io/mineops-part-5/">Part 5 - Making Containers Highly Available</a></li><li><a href="https://blog.kywa.io/mineops-part-6/">Part 6 - ArgoCD to the Rescue</a></li></ul>]]></content:encoded></item><item><title><![CDATA[VS Code - The Best IDE?]]></title><description><![CDATA[<p>Unless you&apos;ve been living under a rock for the past few years, you&apos;ve undoubtedly heard about Microsoft&apos;s open source code editor, Visual Studio Code. In typical Microsoft fashion it bears a name similar to that of one of its products, but is not in</p>]]></description><link>https://blog.kywa.io/vs-codeserver/</link><guid isPermaLink="false">610be89f02da49a7d4f769cc</guid><dc:creator><![CDATA[Kyle Walker]]></dc:creator><pubDate>Tue, 31 Aug 2021 02:22:55 GMT</pubDate><media:content url="https://blog.kywa.io/content/images/2021/08/vscode.PNG" medium="image"/><content:encoded><![CDATA[<img src="https://blog.kywa.io/content/images/2021/08/vscode.PNG" alt="VS Code - The Best IDE?"><p>Unless you&apos;ve been living under a rock for the past few years, you&apos;ve undoubtedly heard about Microsoft&apos;s open source code editor, Visual Studio Code. In typical Microsoft fashion it bears a name similar to that of one of its products, but is not in the same product line. Unlike Visual Studio, this tool is not a &quot;full featured IDE to code, debug, and deploy to any platform&quot; that only runs on Windows (taken directl from the product page). VS Code (the shorthand I will use from here on out) is all of that and more. It is extensible and can run on nearly any platform natively.</p><figure class="kg-card kg-bookmark-card kg-card-hascaption"><a class="kg-bookmark-container" href="https://code.visualstudio.com/"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Visual Studio Code - Code Editing. Redefined</div><div class="kg-bookmark-description">Visual Studio Code is a code editor redefined and optimized for building and debugging modern web and cloud applications. Visual Studio Code is free and available on your favorite platform - Linux, macOS, and Windows.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://code.visualstudio.com/favicon.ico" alt="VS Code - The Best IDE?"><span class="kg-bookmark-author">Microsoft</span><span class="kg-bookmark-publisher">Microsoft</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://code.visualstudio.com/opengraphimg/opengraph-home.png" alt="VS Code - The Best IDE?"></div></a><figcaption>Click here to visit VS Code&apos;s webpage</figcaption></figure><p>So what is the big deal about using VS Code over Visual Studio outside of the fun fact of being able to run it anywhere? In my mind, its the extensibility and ease of use to be able to add in functionality for almost anything you can imagine. One of the great things is that its open source. Not just open source, but open source from a company that has historically (prior to 5 years ago) shivered at the use of the phrase.</p><h2 id="getting-started">Getting Started</h2><p>Most of the time VS Code is used as a code editor as one would expect. It has some very nice built in features that do not even require any extensions to be installed (more on that later). </p><h3 id="installation">Installation</h3><p>By following the link above you can download the installer for your appropriate OS. I am not going to show how to do that on each system because I hope you are capable of that on your own.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.kywa.io/content/images/2021/08/vscodeinstall.PNG" class="kg-image" alt="VS Code - The Best IDE?" loading="lazy" width="903" height="308" srcset="https://blog.kywa.io/content/images/size/w600/2021/08/vscodeinstall.PNG 600w, https://blog.kywa.io/content/images/2021/08/vscodeinstall.PNG 903w" sizes="(min-width: 720px) 720px"><figcaption>Installation Options for VS Code</figcaption></figure><p>When installing VS Code for the first time you get a nice insightful start up page with some useful information. This start up page will guide you on setting up a few things such as a default theme (it better be dark mode!) and showing you some of the built in features of VS Code.</p><p>I will not be pouring over all the features as there is documentation for this and honestly better guides out there (and would be better suited to a video as opposed to a blog). Below are links to the documentation as well a link to VS Code&apos;s videos (also found in their documentation page):</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://code.visualstudio.com/docs/getstarted/introvideos"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Visual Studio Code Introductory Videos</div><div class="kg-bookmark-description">Overview of Visual Studio Code&#x2019;s introductory videos.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://code.visualstudio.com/favicon.ico" alt="VS Code - The Best IDE?"><span class="kg-bookmark-author">Microsoft</span><span class="kg-bookmark-publisher">Microsoft</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://code.visualstudio.com/assets/docs/getstarted/introvideos/opengraph_introvideos.png" alt="VS Code - The Best IDE?"></div></a></figure><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://code.visualstudio.com/docs/editor/codebasics"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Basic Editing in Visual Studio Code</div><div class="kg-bookmark-description">Learn about the basic editing features of Visual Studio Code. Search, multiple selection, code formatting.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://code.visualstudio.com/favicon.ico" alt="VS Code - The Best IDE?"><span class="kg-bookmark-author">Microsoft</span><span class="kg-bookmark-publisher">Microsoft</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://code.visualstudio.com/assets/docs/editor/codebasics_CodeBasics.png" alt="VS Code - The Best IDE?"></div></a></figure><p>One thing I will mention is I would recommend having <strong><a href="https://git-scm.com/">Git</a></strong> installed on your system as VS Code can make full use of a Git repository and will let you know if you&apos;ve made any changes to your repository when open as a workspace. It also grants you another way to interact with Git and use a single tool for nearly everything related to development. This is somewhat talked about in the video on Version Control in the link above.</p><h3 id="extensions-superpowers">Extensions = Superpowers</h3><p>Now for the real fun of VS Code, extensions! Extensions give you some of the best tools available for more than just editing code (Vim can do that for you if its all you need). Say you have a Kubernetes cluster you maintain and you are writing YAML files, you can use the Kubernetes extension (developed by Microsoft) to have a page on VS Code available to look at the resources of your Kubernetes cluster. There are other extensions for specific types of syntax highlighting or linters (if your into that sort of thing).</p><p>Once you&apos;ve gotten VS Code to where you like it, with all your extensions and custom settings, what do you do if your hard drive crashes? Or what if you have multiple devices and you want the same experience, say your work device and your home machine? Very recently VS Code has had functionality added to be able to login and sync your settings between installs, but if history has taught us anything, its that Microsoft can lose data. Time to take matters into our own hands.</p><h2 id="running-in-a-container">Running in a Container</h2><h3 id="containers-101">Containers 101</h3><p>Containers are still new for a lot of developers in my experience. You&apos;ve most likely heard about <a href="https://www.docker.com/">Docker</a> by now and maybe have some interest in learning more about it. Very quickly, containers are not virtual machines (despite most people treating them as such). They are processes that run in a siloed off portion of the system through something called a <a href="https://en.wikipedia.org/wiki/Linux_namespaces">namespace</a>. Here is a great video for a getting started guide on Docker and some of its most common commands:</p><figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/-tHeJbGP8e0?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></figure><p>By default a container runs using ephemeral storage meaning that whatever new data gets added into the running container will be gone once the container exits. This behavior is why you shouldn&apos;t try to work out of a container and run commands such as &quot;yum install git&quot; or the like. There are a few ways to alter this, but for the most part with a basic Docker installation (or <a href="https://podman.io/">Podman</a> if you are running on Linux) you can use what is called a <a href="https://docs.docker.com/storage/bind-mounts/">bind mount</a>. It is also one of the easiest methods to use without getting into the nitty gritty. The tl;dr of a bind mount is it mounts a host filesystem into the container at a specified point. Example below:</p><!--kg-card-begin: markdown--><pre><code>docker run -v /home/kyle:/tmp/kyle nginx
</code></pre>
<!--kg-card-end: markdown--><p>This command above will mount <code>/home/kyle</code> into the container which will see it as <code>/tmp/kyle</code> and will not appear as a mounted volume using a tool such as <code>df</code>. This can be quite handy and makes it easy to spin up development containers for web development and much more. Another example of use would be something like so:</p><!--kg-card-begin: markdown--><pre><code>docker run -v /home/kyle/mynewwebsite:/usr/share/nginx/html nginx:alpine
</code></pre>
<!--kg-card-end: markdown--><h3 id="vs-code-in-a-container">VS Code in a Container?</h3><p>VS Code can be run in a web browser instead of a locally installed piece of software. This sounds awesome and there are a few ways to do it. I personally prefer running it in a container while being aware of the caveats of ephemeral storage mentioned above. For me this is accomplished by doing the following (and I will outline what is going on).</p><!--kg-card-begin: markdown--><pre><code>docker run -d -p 8080:8080 -e PASSWORD=&quot;CHANGEME&quot; --name vscode -v /home/kwalker/.ssh/:/home/coder/.ssh -v ${PWD}:/home/coder quay.io/kywa/kcode:latest
</code></pre>
<!--kg-card-end: markdown--><p>The above code snippet does a few things outlined here:</p><ul><li>docker run -d = this runs the container in a daemon mode (background)</li><li>-p 8080:8080 = exposes port 8080 from the host to the container on 8080</li><li>-e PASSWORD=&quot;CHANGEME&quot; = sets the password for the image</li><li>--name vscode = names the container vscode</li><li>-v /home/kwalker.ssh/:/home/coder/.ssh = mounts my .ssh as .ssh for coder</li><li>-v ${PWD}:/home/coder = makes the directory I&apos;m in /home/coder</li><li>quay.io/kywa/kcode:latest = the image I use for VS Code</li></ul><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.kywa.io/content/images/2021/08/vscodebrowser.PNG" class="kg-image" alt="VS Code - The Best IDE?" loading="lazy" width="1228" height="377" srcset="https://blog.kywa.io/content/images/size/w600/2021/08/vscodebrowser.PNG 600w, https://blog.kywa.io/content/images/size/w1000/2021/08/vscodebrowser.PNG 1000w, https://blog.kywa.io/content/images/2021/08/vscodebrowser.PNG 1228w" sizes="(min-width: 720px) 720px"><figcaption>VS Code running in the browser!</figcaption></figure><p>Having stated that you would want to keep your configurations in sync between workstations, this on the surface looks like it is still targeting a single directory (where ${PWD} was run) and you&apos;d be correct. Thankfully ${PWD} could be an NFS share, a CIFS share or even a mounted shared storage provider (Google Drive, Microsoft OneDrive, Dropbox, etc...). For now, I will leave it up to your imagination on how to accomplish this and what will work best for you and your environment. </p><p>The image I used is a custom built image which has a few other tools installed to the VS Code instance baked into the image, Helm being the main thing here. The main repository where this came from can be found <a href="https://github.com/cdr/code-server">here</a> and the base container image can be found <a href="https://hub.docker.com/r/codercom/code-server">here</a>.</p><h2 id="kubernetes">Kubernetes</h2><p>And now for the main event, running VS Code on Kubernetes (or <a href="https://www.redhat.com/en/technologies/cloud-computing/openshift">OpenShift</a>). The gist and reasoning is the same as above, but creating a manifest for everyone in your org to use has some pretty awesome benefits. It can allow your developers and teammates to no longer worry about which device they are running on and the environment will be the same. Stuck with just your iPad instead of your awesome Dell XPS 15? &#xA0;Doesn&apos;t matter, a browser handles it all for you.</p><p>Getting started is fairly straightforward, but will need a few things to be done beforehand (or all at once if you modify the manifest below). We will create a namespace and then a Secret to store a password more secure than &quot;CHANGEME&quot;</p><p>The output of the following command will result in a hash to be used in your Secret below in the manifest marked REPLACE (do not use quotes when replacing REPLAC in the manifest): </p><p><code>echo &quot;yourSecretPassword&quot; | base64</code></p><p>Below is a manifest containing all of the objects required to run code-server on a Kubernetes cluster and can be modified/extended to be templatized for multiple users to have their own unique environments able to be used from anywhere they can access the Kubernetes cluster.</p><figure class="kg-card kg-code-card"><pre><code class="language-yaml">---
apiVersion: v1
kind: Namespace
metadata:
  name: code-server
---
apiVersion: v1
kind: Secret
metadata:
  name: code-server
  namespace: code-server
data:
  password: REPLACE
---
apiVersion: v1
kind: Service
metadata:
 name: code-server
 namespace: code-server
spec:
 ports:
 - port: 80
   targetPort: 8080
 selector:
   app: code-server
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: code-server
  namespace: code-server
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: code-server
  name: code-server
  namespace: code-server
spec:
  selector:
    matchLabels:
      app: code-server
  replicas: 1
  template:
    metadata:
      labels:
        app: code-server
    spec:
      containers:
      - env:
        - name: PASSWORD
          valueFrom:
            secretKeyRef:
              name: code-server
              key: password
        image: codercom/code-server:latest
        imagePullPolicy: Always
        name: code-server
        volumeMounts:
        - mountPath: /home/coder
          name: coder
      volumes:
      - name: coder
        persistentVolumeClaim:
          claimName: code-server
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: code-server
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - host: &quot;kubernetes.example.com&quot;
    http:
      paths:
      - path: /codeserver
        pathType: Prefix
        backend:
          service:
            name: code-server
            port:
              number: 80
</code></pre><figcaption>https://gist.github.com/KyWa/b103d5e6f361145db6467d69c962fbe9</figcaption></figure><p>The last thing you need to do is update the Ingress object at the bottom of the manifest with the <code>host</code> DNS name you would use for your Kubernetes Ingress objects (if using this method). If you are not using Ingress objects in your Kubernetes cluster, I trust you will be able to change this object to one more suitable to your environment.</p><p>If you copy the contents of the code block above into a file called <code>code-server.yaml</code> you can then create the objects (after replacing the Secret data REPLACE with the base64 output run above) by running the following: <code>kubectl create -f code-server.yaml</code></p><p>After a minute or so depending on network speeds, you should be able to access your code-server running at whichever domain you specified in the Ingress object at the bottom of the manifest.</p><h2 id="conclusion">Conclusion</h2><p>I hope this has given some ideas and sparked the imagination of those reading in hopes of finding new ways to improve the developer experience for your users and friends. There are many potential tweaks that can be made to the code given above and numerous ways to use VS Code in your environment. The last portion around Kubernetes primarily applies to those running Kubernetes in an IT organization, but could be run locally with Docker Desktop (or adventurous admins playing around at home).</p><p>Even if using it in just a local container, this could be run on a home server (or a VPS like <a href="https://www.linode.com/">Linode</a> or <a href="https://www.digitalocean.com/">DigitalOcean</a>) and be used when out of your normal environment. It could be a handy solution for those who move around a lot and don&apos;t always carry their laptop with them. </p><p>Happy Coding!</p>]]></content:encoded></item><item><title><![CDATA[My HomeLab]]></title><description><![CDATA[<p>Working for Red Hat and focusing on Red Hat products, specifically OpenShift, I try to utilize the same products at home. This allows me to have a similar environment to what my customers use and keep my mind sharp on various products (and potential pitfalls). Every VM and host runs</p>]]></description><link>https://blog.kywa.io/my-homelab/</link><guid isPermaLink="false">60fa058a02da49a7d4f768fd</guid><dc:creator><![CDATA[Kyle Walker]]></dc:creator><pubDate>Thu, 05 Aug 2021 13:17:38 GMT</pubDate><content:encoded><![CDATA[<p>Working for Red Hat and focusing on Red Hat products, specifically OpenShift, I try to utilize the same products at home. This allows me to have a similar environment to what my customers use and keep my mind sharp on various products (and potential pitfalls). Every VM and host runs CentOS 8 (was deployed a year ago before the CentOS Stream fiasco began). When I upgrade, I will be deploying CentOS Stream and not any of the new upstarts such as Rocky Linux etc. Maybe there could be a blog post in there somewhere later.</p><h2 id="hardware">Hardware</h2><p>Currently my homelab is on revision 5428 which consists of a single &quot;server&quot;. This server is an AMD Ryzen 2700X with 64GB of RAM backed by a 1TB NVMe SSD. The storage for my environment is handled by a Synology DS1618+ filled with 16TB Seagate Exos drives. This nets me ~43TB usable and currently consuming 20TB of that (I run a large media server). I also have a single 2TB RAID1 SSD array for VM storage over NFS which is currently served over 1Gbps, but will be upgrading to 10Gbps soon. I already have the 10GB card for the Synology, I just need to acquire a 10GB card for the server (or servers if I upgrade anytime soon).</p><p>For networking, I have a dumb 24-port NetGear switch and a Synology RT2600AC router. This router is pretty dope and allows for all the functionality I need which is really just a DNS server I don&apos;t have to install myself. It is also managing DHCP for the network and gives pretty decent data visualization.</p><figure class="kg-card kg-image-card"><img src="https://blog.kywa.io/content/images/2021/08/IMG_0640.jpg" class="kg-image" alt loading="lazy" width="2000" height="1500" srcset="https://blog.kywa.io/content/images/size/w600/2021/08/IMG_0640.jpg 600w, https://blog.kywa.io/content/images/size/w1000/2021/08/IMG_0640.jpg 1000w, https://blog.kywa.io/content/images/size/w1600/2021/08/IMG_0640.jpg 1600w, https://blog.kywa.io/content/images/size/w2400/2021/08/IMG_0640.jpg 2400w" sizes="(min-width: 720px) 720px"></figure><h2 id="storage">Storage</h2><p>As mentioned above, I have a Synology DS1618+ unit. Prior to purchasing an actual NAS, I was using MDADM to manage a software raid and manually configured all of the shares I needed. Later on when I started storing our family photos and video, it became a pain to deal with Samba to share out so my Wife&apos;s Windows laptop could access it. For me in the long run, it was just easier to get a unit that did all of this for me (and had the ability to do a little bit more if needed). So far it has worked out amazingly and I wouldn&apos;t go back to managing my own storage without having a Synology.</p><p>This unit has 4 RJ45 Ports that can all be configured if required and has expandability for either a 10Gbe NIC or I believe an SSD NVMe expansion drive (I chose the 10Gbe route).</p><h2 id="virtualization">Virtualization</h2><p>For virtualization the single server has oVirt installed (Red Hat Virtualization) and runs as a single host (with the hosted-engine running locally). The configuration is a single node cluster in a single datacenter. This presents some challenges I will outline later on.</p><p>Ovirt has a pretty awesome UI and functions quite well as far as virtualization platforms go, especially with it being open source. Currently here are the VMs that live on this node:</p><ul><li>hosted-engine (Virtualization Manager)</li><li>bastion (Proxy and only VM exposed to the internet)</li><li>Plex</li><li>Minecraft</li><li>OKD Nodes (3 nodes in total)</li><li>Services (just a VM for testing things)</li></ul><h2 id="containerization">Containerization</h2><p>Continuing on with the usage of open source projects, I run my own OpenShift Origin cluster (Red Hat OpenShift 3.11). For those that don&apos;t know or have been living under a rock, Kubernetes is a container orchestration system and has more or less gone from buzzword to the standard for application platforms. OpenShift is a distribution of Kubernetes and has quite a few handy add-ons built in. Some of these may not make sense to small groups or solo developers, but the features that OpenShift provides for an Enterprise (or any business looking to have some real features) cannot be matched. </p><h3 id="openshift">OpenShift</h3><p>First and foremost, OpenShift puts container security at the top of everything it does. Containers are somewhat secure by just existing, but can easily be exploited if just running Docker or a more vanilla Kubernetes platform. OpenShift enforces by default that containers/pods cannot run as root. This is already a leap ahead of every Kubernetes deployment in the wild. It also brings some challenges to newcomers to the platform as when most people get started, they find a tutorial on Kubernetes and just apply the code. They will be shocked to find that most tutorials out there assume that your container will run as root (although this is getting better as time goes on). There are other nice things in security, but I will leave those for another post.</p><p>Another neat item built in is around image registries and a build system. For most developers, you wouldn&apos;t think of having a build system as being an important item in Kubernetes since you will most likely have Docker installed locally. What if you work in an environment where you can&apos;t install Docker locally? Or don&apos;t have the option of a VM running Docker to do a build for you? That is where the build system comes in for OpenShift. It exists and allows you to build container images from source, Dockerfile or other means and then push them into the built in container registry built into OpenShift. You do not need to worry about having to spin up your own image registry when OpenShift just gives you one. Obviously for multiple clusters this becomes its own interesting problem and a central registry (Quay.io maybe?) would be more suitable. But for the sake of a homelab, this is more than sufficient.</p><h3 id="applications">Applications</h3><p>In my lab, OpenShift Origin is used for me to test various tools on a live Kubernetes cluster. I have attempted using Docker Desktop on both Mac and Windows, but prefer to have a real multi-node cluster as it just sits a little closer to what is being done in the wild. Some of the projects I have running here are outlined below:</p><ul><li>ArgoCD</li><li>Jenkins</li><li>MongoDB</li><li>Ghost blog (not this one you are reading)</li><li>MySQL</li><li>VS CodeServer (super cool project <a href="https://github.com/cdr/code-server">https://github.com/cdr/code-server</a>)</li></ul><p>Not all of these are in &quot;production&quot; use, but I have them around for testing and experimenting.</p><h2 id="challenges">Challenges</h2><p>One of the biggest issues I have is the current state of the single oVirt node. If there is an issue with the node, which rarely happens but can, the hosted-engine goes down with it. Getting this started back up due to it running on &quot;local&quot; storage, requires some finesse with Linux services starting up. I have a script to do this for me, but the beginning was quite troublesome. </p><p>The other issue faced is just lack of resources. I understand that there are devs and others out there who would kill to have 64GB on their main machine, let alone on an entirely independent server. However for me, I hit resource limits quite often (mostly memory related) and am unable to deploy the latest version of OKD the way I would like. Hopefully soon when chips and things become more available, this can be rectified in homelab v2.0. </p><hr><h2 id="conclusion">Conclusion</h2><p>This is the current iteration of my homelab with more to come in the near future as nature heals and chips become available. The next iteration will be 10Gbe with x2 servers for the oVirt cluster so I can utilize failover and not jump through hoops when the hosted engine fails. Please feel free to ask questions or links for any of the items/software listed here.</p>]]></content:encoded></item><item><title><![CDATA[Let's get going...]]></title><description><![CDATA[<p>Long time reader, first time blogger (in a decade or so). For a more detailed insight into who I am, checkout the about page here: <a href="https://blog.kywa.io/about/">https://blog.kywa.io/about/</a></p><hr><p>This blog will be focusing on a few different things. I will try to keep this blog as tech focused</p>]]></description><link>https://blog.kywa.io/lets-get-going/</link><guid isPermaLink="false">60f83025fb24f21708a65494</guid><dc:creator><![CDATA[Kyle Walker]]></dc:creator><pubDate>Thu, 22 Jul 2021 15:47:05 GMT</pubDate><content:encoded><![CDATA[<p>Long time reader, first time blogger (in a decade or so). For a more detailed insight into who I am, checkout the about page here: <a href="https://blog.kywa.io/about/">https://blog.kywa.io/about/</a></p><hr><p>This blog will be focusing on a few different things. I will try to keep this blog as tech focused as possible. As I have grown in tech I have found the value that good blogs and articles provide for those learning or looking to grow.</p><h2 id="technology">Technology</h2><p>The first tech related article will be over my homelab and how I use it. From then on out it will focus on most things around Kubernetes or OpenShift and tooling as it relates to those platforms.</p><h2 id="automotive">Automotive</h2><p>I will also have some articles posted around my primary hobby, cars. I will be documenting my build and usage of my main car and the side project I have. I no longer have my Camaro, but here is a video of it at Houston Raceway Park back in 2020:</p><figure class="kg-card kg-embed-card kg-card-hascaption"><iframe width="200" height="113" src="https://www.youtube.com/embed/8I-9S8Tb1CY?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><figcaption>Fun times</figcaption></figure><h2 id="photography">Photography</h2><p>I am no photographer, but I do have an interest in it and a great interest in drones. Recently I purchased a DJI Mini 2 and have grown to love the freedom they provide and the ease in which you can get some truly amazing shots. This video is a quick flight from my house to the local lake in my neighborhood.</p><figure class="kg-card kg-embed-card kg-card-hascaption"><iframe width="200" height="113" src="https://www.youtube.com/embed/HmD6QvsE_fg?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><figcaption>177 Lake Estates</figcaption></figure><hr><p>I will do my best to post weekly, and look forward to hearing feedback as we move forward.</p>]]></content:encoded></item></channel></rss>