Jekyll2023-01-26T19:00:53+00:00https://kosyfrances.com/feed.xmlKosy AnyanwuA blog about technology and stuff relatedSyncing cert-manager certificate secret across Namespaces using kubed2021-05-11T12:05:00+00:002021-05-11T12:05:00+00:00https://kosyfrances.com/single-cert-secret<h2 id="introduction">Introduction</h2>
<p><a href="https://cert-manager.io/docs/">cert-manager</a> is a native Kubernetes certificate management controller. It can help with issuing certificates from a variety of sources, such as <a href="https://letsencrypt.org/">Let’s Encrypt</a>.</p>
<p>A common use-case for cert-manager is requesting TLS signed certificates to secure your ingress resources. This can be done by simply adding this annotation <code class="language-plaintext highlighter-rouge">cert-manager.io/cluster-issuer: nameOfClusterIssuer</code> to your Ingress resources and cert-manager will facilitate creating the Certificate resource for you.</p>
<p>When you have multiple ingresses in separate namespaces, this annotation ends up creating certificate resources in each of the namespaces. If you use Let’s encrypt, the limit of <a href="https://letsencrypt.org/docs/rate-limits/">Duplicate Certificates</a> is 5 per week, and it is easy to hit this limit if you have a lot of namespaces. You can decide to create the TLS secret for the certificate in one namespace and copy manually across all namespaces, but that will be a pain to manage and maintain. <a href="https://cert-manager.io/docs/faq/kubed/">cert-manager docs</a> recommends using <a href="https://appscode.com/products/kubed/v0.12.0/welcome/">kubed</a> with its <a href="https://appscode.com/products/kubed/v0.12.0/guides/config-syncer/intra-cluster/">secret syncing feature</a> to solve this problem.</p>
<p>This article was written based on <a href="https://cert-manager.io/docs/">cert-manager v1.3</a> and <a href="https://appscode.com/products/kubed/v0.12.0/welcome/">kubed v0.12.0</a>. This article does not address wildcard certificate usecase.</p>
<h2 id="sync-ingress-certificate-secret-across-namespaces-using-kubed">Sync ingress certificate secret across namespaces using kubed</h2>
<p>In order for the target Secret to be synced, the Secret resource must first be created with the correct annotations before the creation of the Certificate, else the Secret will need to be edited instead. When I tried this out, I created the Certificate first and edited the secret annotation afterwards.</p>
<p>The example below creates a certificate resource in <code class="language-plaintext highlighter-rouge">sandbox</code> namespace. This in turn creates a secret <code class="language-plaintext highlighter-rouge">cert-secret</code>.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">cert-manager.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Certificate</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">cert-secret</span>
<span class="na">namespace</span><span class="pi">:</span> <span class="s">sandbox</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">secretName</span><span class="pi">:</span> <span class="s">cert-secret</span>
<span class="na">issuerRef</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">letsencrypt-prod</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ClusterIssuer</span>
<span class="na">dnsNames</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">domain.example.com</span>
</code></pre></div></div>
<p>Now apply the <code class="language-plaintext highlighter-rouge">kubed.appscode.com/sync: ""</code> annotation to the <code class="language-plaintext highlighter-rouge">cert-secret</code> created by the cert-manager Certificate resource.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl annotate secret cert-secret kubed.appscode.com/sync<span class="o">=</span><span class="s2">""</span>
</code></pre></div></div>
<p>This annotation will cause Kubed operator to copy the secret to all existing and new namespaces. If you want to synchronize this secret to some selected namespaces instead of all namespaces, you can do that by specifying namespace label-selector in the annotation. For example: <code class="language-plaintext highlighter-rouge">kubed.appscode.com/sync: "app=kubed"</code>.</p>
<p>You can now create your ingress resources to use the copied secret and you will no longer need this cert-manager annotation <code class="language-plaintext highlighter-rouge">cert-manager.io/cluster-issuer: nameOfClusterIssuer</code> in your ingress.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">networking.k8s.io/v1beta1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Ingress</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">test-ingress</span>
<span class="na">annotations</span><span class="pi">:</span>
<span class="s">kubernetes.io/ingress.class</span><span class="pi">:</span> <span class="s2">"</span><span class="s">nginx"</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">tls</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">hosts</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">domain.example.com</span>
<span class="na">secretName</span><span class="pi">:</span> <span class="s">cert-secret</span>
<span class="na">rules</span><span class="pi">:</span>
<span class="s">...</span>
</code></pre></div></div>kosyanyanwuIntroduction cert-manager is a native Kubernetes certificate management controller. It can help with issuing certificates from a variety of sources, such as Let’s Encrypt.GKE Ingress with Let’s Encrypt using Cert-Manager2021-04-08T12:05:00+00:002021-04-08T12:05:00+00:00https://kosyfrances.com/ingress-gce-letsencrypt<h2 id="introduction">Introduction</h2>
<p><a href="https://cloud.google.com/kubernetes-engine">Google Kubernetes Engine (GKE)</a> provides a built-in and managed Ingress controller called GKE Ingress. When you create an Ingress object, the GKE Ingress controller creates a Google Cloud HTTP(S) load balancer and configures it according to the information in the Ingress and its associated Services.</p>
<p>This article describes how to setup Ingress for External HTTP(S) Load Balancing, install cert-manager certificate provisioner and setup up a Let’s Encrypt certificate. This was written based on GKE <a href="https://cloud.google.com/kubernetes-engine/docs/release-notes-stable#february_11_2020">v1.17.17-gke.3000</a>, <a href="https://cert-manager.io/">cert-manager</a> v1.20 and <a href="https://helm.sh/">Helm</a> v3.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li><a href="https://cloud.google.com/kubernetes-engine/docs/how-to/creating-a-cluster">A GKE Kubernetes cluster</a></li>
<li><a href="https://helm.sh/docs/intro/install/">Helm</a></li>
<li><a href="https://kubernetes.io/docs/tasks/tools/install-kubectl/">Kubectl</a></li>
<li><a href="https://cloud.google.com/compute/docs/ip-addresses/reserve-static-external-ip-address">A global static IP</a> with <a href="https://cloud.google.com/kubernetes-engine/docs/tutorials/configuring-domain-name-static-ip#step_4_configure_your_domain_name_records">DNS configured</a> for your domain for example, as example.your-domain.com. Regional IP addresses do not work with GKE Ingress.</li>
</ul>
<p>Note that a Service exposed through an Ingress must respond to health checks from the load balancer. According to the <a href="https://cloud.google.com/kubernetes-engine/docs/concepts/ingress#health_checks">docs</a>, your app must either serve a response with an HTTP 200 status to GET requests on the / path, or you can configure an HTTP readiness probe, serving a response with an HTTP 200 status to GET requests on the path specified by the readiness probe.</p>
<h2 id="create-a-deployment">Create a deployment</h2>
<p>Here is an example of a sample deployment manifest.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">apps/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Deployment</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">sample-deployment</span>
<span class="na">labels</span><span class="pi">:</span>
<span class="na">app</span><span class="pi">:</span> <span class="s">sampleApp</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">replicas</span><span class="pi">:</span> <span class="m">1</span>
<span class="na">selector</span><span class="pi">:</span>
<span class="na">matchLabels</span><span class="pi">:</span>
<span class="na">app</span><span class="pi">:</span> <span class="s">sampleApp</span>
<span class="na">template</span><span class="pi">:</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">labels</span><span class="pi">:</span>
<span class="na">app</span><span class="pi">:</span> <span class="s">sampleApp</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">containers</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">sampleContainer</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">nginx:1.7.9</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">http</span>
<span class="na">containerPort</span><span class="pi">:</span> <span class="m">8080</span>
<span class="na">protocol</span><span class="pi">:</span> <span class="s">TCP</span>
<span class="na">readinessProbe</span><span class="pi">:</span>
<span class="na">httpGet</span><span class="pi">:</span>
<span class="na">path</span><span class="pi">:</span> <span class="s">/healthz</span>
<span class="na">port</span><span class="pi">:</span> <span class="m">8080</span>
<span class="na">initialDelaySeconds</span><span class="pi">:</span> <span class="m">5</span>
<span class="na">periodSeconds</span><span class="pi">:</span> <span class="m">5</span>
</code></pre></div></div>
<h2 id="create-a-service">Create a service</h2>
<p>Here is an example of a sample service manifest.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Service</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">sampleApp-service</span>
<span class="na">labels</span><span class="pi">:</span>
<span class="na">app</span><span class="pi">:</span> <span class="s">sampleApp</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">type</span><span class="pi">:</span> <span class="s">NodePort</span>
<span class="na">selector</span><span class="pi">:</span>
<span class="na">app</span><span class="pi">:</span> <span class="s">sampleApp</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">http</span>
<span class="na">protocol</span><span class="pi">:</span> <span class="s">TCP</span>
<span class="na">port</span><span class="pi">:</span> <span class="m">8080</span>
<span class="na">targetPort</span><span class="pi">:</span> <span class="m">8080</span>
</code></pre></div></div>
<h2 id="install-cert-manager">Install cert-manager</h2>
<p>cert-manager runs within your Kubernetes cluster as a series of deployment resources. It utilizes <a href="https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/">CustomResourceDefinitions</a> to configure Certificate Authorities and request certificates. The following steps <a href="https://cert-manager.io/docs/installation/kubernetes/">installs cert-manager</a> on your Kubernetes cluster.</p>
<ul>
<li>Install the CustomResourceDefinition resources separately.
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> kubectl apply <span class="nt">-f</span> https://github.com/jetstack/cert-manager/releases/download/v1.2.0/cert-manager.crds.yaml
</code></pre></div> </div>
</li>
<li>Add the Jetstack Helm repository.
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> helm repo add jetstack https://charts.jetstack.io
</code></pre></div> </div>
</li>
<li>Update your local Helm chart repository cache.
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> helm repo update
</code></pre></div> </div>
</li>
<li>Install the cert-manager Helm chart.
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> helm <span class="nb">install</span> <span class="se">\</span>
cert-manager jetstack/cert-manager <span class="se">\</span>
<span class="nt">--namespace</span> cert-manager <span class="se">\</span>
<span class="nt">--create-namespace</span> <span class="se">\</span>
<span class="nt">--version</span> v1.2.0
</code></pre></div> </div>
</li>
<li>Verify the installation.
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nv">$ </span>kubectl get pods <span class="nt">--namespace</span> cert-manager
NAME READY STATUS RESTARTS AGE
cert-manager-5c6866597-zw7kh 1/1 Running 0 2m
cert-manager-cainjector-577f6d9fd7-tr77l 1/1 Running 0 2m
cert-manager-webhook-787858fcdb-nlzsq 1/1 Running 0 2m
</code></pre></div> </div>
<p>You should see the cert-manager, cert-manager-cainjector, and cert-manager-webhook pod in a Running state. It may take a minute or so for the TLS assets required for the webhook to function to be provisioned.</p>
</li>
<li>Create an <a href="https://cert-manager.io/docs/concepts/issuer/">Issuer</a> to test the webhook works okay.
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="s">cat <<EOF > test-resources.yaml</span>
<span class="s">apiVersion</span><span class="pi">:</span> <span class="s">v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Namespace</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">cert-manager-test</span>
<span class="s">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">cert-manager.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Issuer</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">test-selfsigned</span>
<span class="na">namespace</span><span class="pi">:</span> <span class="s">cert-manager-test</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">selfSigned</span><span class="pi">:</span> <span class="pi">{}</span>
<span class="s">---</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">cert-manager.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Certificate</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">selfsigned-cert</span>
<span class="na">namespace</span><span class="pi">:</span> <span class="s">cert-manager-test</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">dnsNames</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">example.com</span>
<span class="na">secretName</span><span class="pi">:</span> <span class="s">selfsigned-cert-tls</span>
<span class="na">issuerRef</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">test-selfsigned</span>
<span class="s">EOF</span>
</code></pre></div> </div>
</li>
<li>Create the test resources.
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> kubectl apply <span class="nt">-f</span> test-resources.yaml
</code></pre></div> </div>
</li>
<li>Check the status of the newly created certificate. You may need to wait a few seconds before cert-manager processes the certificate request.
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nv">$ </span>kubectl describe certificate <span class="nt">-n</span> cert-manager-test
...
Spec:
Common Name: example.com
Issuer Ref:
Name: test-selfsigned
Secret Name: selfsigned-cert-tls
Status:
Conditions:
Last Transition Time: 2020-01-29T17:34:30Z
Message: Certificate is up to <span class="nb">date </span>and has not expired
Reason: Ready
Status: True
Type: Ready
Not After: 2020-04-29T17:34:29Z
Events:
Type Reason Age From Message
<span class="nt">----</span> <span class="nt">------</span> <span class="nt">----</span> <span class="nt">----</span> <span class="nt">-------</span>
Normal CertIssued 4s cert-manager Certificate issued successfully
</code></pre></div> </div>
</li>
<li>Clean up the test resources.
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> kubectl delete <span class="nt">-f</span> test-resources.yaml
</code></pre></div> </div>
</li>
</ul>
<p>If all the above steps have completed without error, you are good to go!</p>
<h2 id="create-issuer">Create issuer</h2>
<p>The Let’s Encrypt production issuer has very strict <a href="https://letsencrypt.org/docs/rate-limits/">rate limits</a>. When you are experimenting and learning, it is very easy to hit those limits, and confuse rate limiting with errors in configuration or operation. Start with <a href="https://letsencrypt.org/docs/staging-environment/">Let’s Encrypt staging</a> environment and switch to Let’s Encrypt production after it works fine. In this article, we will be creating a <a href="https://docs.cert-manager.io/en/release-0.11/reference/clusterissuers.html">ClusterIssuer</a>.</p>
<p>Create a clusterissuer definition and update the email address to your own. This email is required by Let’s Encrypt and used to notify you of certificate expiration and updates.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">cat <<EOF > clusterissuer.yaml</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">cert-manager.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ClusterIssuer</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">letsencrypt-staging</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">acme</span><span class="pi">:</span>
<span class="c1"># The ACME server URL</span>
<span class="na">server</span><span class="pi">:</span> <span class="s">https://acme-staging-v02.api.letsencrypt.org/directory</span>
<span class="c1"># Email address used for ACME registration</span>
<span class="na">email</span><span class="pi">:</span> <span class="s">you@youremail.com</span> <span class="c1"># Update to yours</span>
<span class="c1"># Name of a secret used to store the ACME account private key</span>
<span class="na">privateKeySecretRef</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">letsencrypt-staging</span>
<span class="c1"># Enable the HTTP-01 challenge provider</span>
<span class="na">solvers</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">http01</span><span class="pi">:</span>
<span class="na">ingress</span><span class="pi">:</span>
<span class="na">class</span><span class="pi">:</span> <span class="s">ingress-gce</span>
<span class="s">EOF</span>
</code></pre></div></div>
<p>Once edited, apply the custom resource:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> clusterissuer.yaml
</code></pre></div></div>
<p>Check on the status of the clusterissuer after you create it:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl describe clusterissuer letsencrypt-staging
Name: letsencrypt-staging
...
Status:
Acme:
Last Registered Email: you@youremail.com
Uri: https://acme-staging-v02.api.letsencrypt.org/acme/acct/123456
Conditions:
Last Transition Time: 2020-02-24T18:33:56Z
Message: The ACME account was registered with the ACME server
Reason: ACMEAccountRegistered
Status: True
Type: Ready
Events: <none>
</code></pre></div></div>
<p>You should see the issuer listed with a registered account.</p>
<h2 id="deploy-a-tls-ingress-resource">Deploy a TLS Ingress Resource</h2>
<p>Create an <a href="https://kubernetes.io/docs/concepts/services-networking/ingress/">ingress</a> definition.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">cat <<EOF > ingress.yaml</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">networking.k8s.io/v1beta1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Ingress</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">sampleApp-ingress</span>
<span class="na">annotations</span><span class="pi">:</span>
<span class="c1"># specify the name of the global IP address resource to be associated with the HTTP(S) Load Balancer.</span>
<span class="s">kubernetes.io/ingress.global-static-ip-name</span><span class="pi">:</span> <span class="s">sampleApp-ip</span>
<span class="c1"># add an annotation indicating the issuer to use.</span>
<span class="s">cert-manager.io/cluster-issuer</span><span class="pi">:</span> <span class="s">letsencrypt-staging</span>
<span class="c1"># controls whether the ingress is modified ‘in-place’,</span>
<span class="c1"># or a new one is created specifically for the HTTP01 challenge.</span>
<span class="s">acme.cert-manager.io/http01-edit-in-place</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
<span class="na">labels</span><span class="pi">:</span>
<span class="na">app</span><span class="pi">:</span> <span class="s">sampleApp</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">tls</span><span class="pi">:</span> <span class="c1"># < placing a host in the TLS config will indicate a certificate should be created</span>
<span class="pi">-</span> <span class="na">hosts</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">example.example.com</span>
<span class="na">secretName</span><span class="pi">:</span> <span class="s">sampleApp-cert-secret</span> <span class="c1"># < cert-manager will store the created certificate in this secret</span>
<span class="na">rules</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">host</span><span class="pi">:</span> <span class="s">example.example.com</span>
<span class="na">http</span><span class="pi">:</span>
<span class="na">paths</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s">sample/app/path/*</span>
<span class="na">backend</span><span class="pi">:</span>
<span class="na">serviceName</span><span class="pi">:</span> <span class="s">sampleApp-service</span>
<span class="na">servicePort</span><span class="pi">:</span> <span class="m">8080</span>
<span class="s">EOF</span>
</code></pre></div></div>
<p>Once edited, apply ingress resource.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> ingress.yaml
</code></pre></div></div>
<h2 id="verify">Verify</h2>
<p>View certificate.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl get certificate
NAME READY SECRET AGE
sampleApp-cert-secret True sampleApp-cert-secret 6m34s
</code></pre></div></div>
<p>Describe certificate.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl describe certificate sampleApp-cert-secret
Name: sampleApp-cert-secret
...
Status:
Conditions:
Last Transition Time: 2020-03-02T16:30:01Z
Message: Certificate is up to <span class="nb">date </span>and has not expired
Reason: Ready
Status: True
Type: Ready
Not After: 2020-05-24T17:55:46Z
Events: <none>
</code></pre></div></div>
<p>Describe secrets created by cert manager.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl describe secret sampleApp-cert-secret
Name: sampleApp-cert-secret
...
Type: kubernetes.io/tls
Data
<span class="o">====</span>
ca.crt: 0 bytes
tls.crt: 3598 bytes
tls.key: 1675 bytes
</code></pre></div></div>
<h2 id="switch-to-lets-encrypt-prod">Switch to Let’s Encrypt Prod</h2>
<p>Update the clusterissuer to use Let’s encrypt prod.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">cat <<EOF > clusterissuer.yaml</span>
<span class="na">apiVersion</span><span class="pi">:</span> <span class="s">cert-manager.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ClusterIssuer</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">letsencrypt-prod</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">acme</span><span class="pi">:</span>
<span class="c1"># The ACME server URL</span>
<span class="na">server</span><span class="pi">:</span> <span class="s">https://acme-v02.api.letsencrypt.org/directory</span>
<span class="c1"># Email address used for ACME registration</span>
<span class="na">email</span><span class="pi">:</span> <span class="s">you@youremail.com</span> <span class="c1"># Update to yours</span>
<span class="c1"># Name of a secret used to store the ACME account private key</span>
<span class="na">privateKeySecretRef</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">letsencrypt-prod</span>
<span class="c1"># Enable the HTTP-01 challenge provider</span>
<span class="na">solvers</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">http01</span><span class="pi">:</span>
<span class="na">ingress</span><span class="pi">:</span>
<span class="na">class</span><span class="pi">:</span> <span class="s">ingress-gce</span>
<span class="s">EOF</span>
</code></pre></div></div>
<p>Once edited, apply the custom resource:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl apply <span class="nt">-f</span> clusterissuer.yaml
</code></pre></div></div>
<p>Now that we are sure that everything is configured correctly, you can update the annotations in the ingress to specify the production issuer:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">networking.k8s.io/v1beta1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Ingress</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">sampleApp-ingress</span>
<span class="na">annotations</span><span class="pi">:</span>
<span class="s">kubernetes.io/ingress.global-static-ip-name</span><span class="pi">:</span> <span class="s">sampleApp-ip</span>
<span class="s">cert-manager.io/cluster-issuer</span><span class="pi">:</span> <span class="s">letsencrypt-prod</span>
<span class="s">acme.cert-manager.io/http01-edit-in-place</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
<span class="na">labels</span><span class="pi">:</span>
<span class="na">app</span><span class="pi">:</span> <span class="s">sampleApp</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">tls</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">hosts</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">example.example.com</span>
<span class="na">secretName</span><span class="pi">:</span> <span class="s">sampleApp-cert-secret</span>
<span class="na">rules</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">host</span><span class="pi">:</span> <span class="s">example.example.com</span>
<span class="na">http</span><span class="pi">:</span>
<span class="na">paths</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s">sample/app/path/*</span>
<span class="na">backend</span><span class="pi">:</span>
<span class="na">serviceName</span><span class="pi">:</span> <span class="s">sampleApp-service</span>
<span class="na">servicePort</span><span class="pi">:</span> <span class="m">8080</span>
</code></pre></div></div>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl create <span class="nt">--edit</span> <span class="nt">-f</span> ingress.yaml
</code></pre></div></div>
<p>You will also need to delete the existing secret, which cert-manager is watching. This will cause it to reprocess the request with the updated issuer.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl delete secret sampleApp-cert-secret
</code></pre></div></div>
<p>This will start the process to get a new certificate. Use <code class="language-plaintext highlighter-rouge">describe</code> to see the status.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl describe certificate sampleApp-cert-secret
</code></pre></div></div>
<p>You can see the current state of the ACME Order by running <code class="language-plaintext highlighter-rouge">kubectl describe</code> on the <a href="https://docs.cert-manager.io/en/release-0.11/reference/orders.html">Order resource</a> that cert-manager has created for your Certificate:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl describe order sampleApp-cert-secret-889745041
...
Events:
Type Reason Age From Message
<span class="nt">----</span> <span class="nt">------</span> <span class="nt">----</span> <span class="nt">----</span> <span class="nt">-------</span>
Normal Created 90s cert-manager Created Challenge resource <span class="s2">"sampleApp-cert-secret-889745041-0"</span> <span class="k">for </span>domain <span class="s2">"example.example.com"</span>
</code></pre></div></div>
<p>You can <code class="language-plaintext highlighter-rouge">describe</code> the challenge to see the status of events by doing:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl describe challenge sampleApp-cert-secret-889745041-0
</code></pre></div></div>
<p>Once the challenge(s) have been completed, their corresponding challenge resources will be deleted, and the ‘Order’ will be updated to reflect the new state of the Order. You can <code class="language-plaintext highlighter-rouge">describe</code> order to verify this.</p>
<p>Finally, the ‘Certificate’ resource will be updated to reflect the state of the issuance process. ‘describe’ the Certificate and verify that the status is <code class="language-plaintext highlighter-rouge">true</code> and <code class="language-plaintext highlighter-rouge">type</code> and <code class="language-plaintext highlighter-rouge">reason</code> are <code class="language-plaintext highlighter-rouge">ready</code>.</p>kosyanyanwuIntroduction Google Kubernetes Engine (GKE) provides a built-in and managed Ingress controller called GKE Ingress. When you create an Ingress object, the GKE Ingress controller creates a Google Cloud HTTP(S) load balancer and configures it according to the information in the Ingress and its associated Services.DNS01 Challenge Provider for Let’s Encrypt Issuer using Google CloudDNS2021-04-08T12:05:00+00:002021-04-08T12:05:00+00:00https://kosyfrances.com/letsencrypt-dns01<h2 id="introduction">Introduction</h2>
<p>This article explains how to set up a ClusterIssuer to use Google CloudDNS to solve <a href="https://letsencrypt.org/docs/challenge-types/#dns-01-challenge">DNS01 ACME challenge</a>. It assumes that your cluster is hosted on Google Cloud Platform (GCP) and that you already have a <a href="https://cloud.google.com/kubernetes-engine/docs/tutorials/configuring-domain-name-static-ip#step_4_configure_your_domain_name_records">domain set up with CloudDNS</a>. It also assumes that you have <a href="/ingress-gce-letsencrypt/#install-cert-manager">cert-manager installed</a> on your cluster.</p>
<p>This was written based on GKE <a href="https://cloud.google.com/kubernetes-engine/docs/release-notes-stable#february_11_2020">v1.17.17-gke.3000</a> and <a href="https://cert-manager.io/">cert-manager</a> v1.20.</p>
<h2 id="create-a-service-account">Create a Service Account</h2>
<p>Create a <a href="https://cloud.google.com/iam/docs/creating-managing-service-accounts#creating">service account</a> with <code class="language-plaintext highlighter-rouge">dns.admin</code> role. This is required for cert-manager to be able to add records to CloudDNS in order to solve the DNS01 challenge.</p>
<h2 id="use-service-account">Use Service Account</h2>
<p>To use this service account, you can either <a href="#create-a-service-account-secret">create a service account secret</a>, or use <a href="#gke-workload-identity">GKE workload identity</a>.</p>
<h3 id="create-a-service-account-secret">Create a Service Account secret</h3>
<p>To access the service account you created in the previous step, cert-manager uses a key stored in a Kubernetes Secret. First, create a key for the service account and download it as a JSON file, then <a href="https://kubernetes.io/docs/concepts/configuration/secret/#creating-a-secret-using-kubectl">create a Secret</a> from this file.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>gcloud iam service-accounts keys create service-account.json <span class="se">\</span>
<span class="nt">--iam-account</span> dns01-solver@<span class="nv">$PROJECT_ID</span>.iam.gserviceaccount.com
<span class="nv">$ </span>kubectl create secret generic clouddns-dns01-solver-svc-acct <span class="se">\</span>
<span class="nt">--from-file</span><span class="o">=</span>service-account.json
</code></pre></div></div>
<h2 id="create-clusterissuer-using-service-account-secret-setup">Create ClusterIssuer (using Service Account secret setup)</h2>
<p>Here is a sample manifest</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">cert-manager.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ClusterIssuer</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">letsencrypt-prod</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">acme</span><span class="pi">:</span>
<span class="na">email</span><span class="pi">:</span> <span class="s">you@youremail.com</span>
<span class="na">server</span><span class="pi">:</span> <span class="s">https://acme-v02.api.letsencrypt.org/directory</span>
<span class="na">privateKeySecretRef</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">letsencrypt-prod-account-key</span>
<span class="na">solvers</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">dns01</span><span class="pi">:</span>
<span class="na">cloudDNS</span><span class="pi">:</span>
<span class="c1"># The ID of the GCP project</span>
<span class="na">project</span><span class="pi">:</span> <span class="s">$PROJECT_ID</span>
<span class="c1"># Secret key reference to the service account created above</span>
<span class="na">serviceAccountSecretRef</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">clouddns-dns01-solver-svc-acct</span>
<span class="na">key</span><span class="pi">:</span> <span class="s">service-account.json</span>
</code></pre></div></div>
<h3 id="gke-workload-identity">GKE Workload Identity</h3>
<p>If your GKE cluster already has <a href="https://cloud.google.com/kubernetes-engine/docs/how-to/workload-identity">workload identity</a> enabled, you can leverage workload identity to avoid creating and managing static service account credentials. Follow these steps to do this:</p>
<ul>
<li>
<p>Link Kubernetes Service Account to Google Service Account in GCP:</p>
<p>If you followed the <a href="https://cert-manager.io/docs/installation/kubernetes/">standard methods for deploying cert-manger to Kubernetes</a>, run the following command:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>gcloud iam service-accounts add-iam-policy-binding <span class="se">\</span>
<span class="nt">--role</span> roles/iam.workloadIdentityUser <span class="se">\</span>
<span class="nt">--member</span> <span class="s2">"serviceAccount:</span><span class="nv">$PROJECT_ID</span><span class="s2">.svc.id.goog[cert-manager/cert-manager]"</span> <span class="se">\</span>
dns01-solver@<span class="nv">$PROJECT_ID</span>.iam.gserviceaccount.com
</code></pre></div> </div>
<p>If your cert-manager pods are running under a different service account, replace goog[cert-manager/cert-manager] with goog[NAMESPACE/SERVICE_ACCOUNT], where NAMESPACE is the namespace of the service account and SERVICE_ACCOUNT is the name of the service account.</p>
</li>
<li>
<p>Link Kubernetes Service Account to Google Service Account in Kubernetes:</p>
<p>To do this, add the proper workload identity annotation to the cert-manager service account.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl annotate serviceaccount <span class="nt">--namespace</span><span class="o">=</span>cert-manager cert-manager <span class="se">\</span>
<span class="s2">"iam.gke.io/gcp-service-account=dns01-solver@</span><span class="nv">$PROJECT_ID</span><span class="s2">.iam.gserviceaccount.com"</span>
</code></pre></div> </div>
<p>If your cert-manager pods are running under a different service account, replace –namespace=cert-manager cert-manager with –namespace=NAMESPACE SERVICE_ACCOUNT, where NAMESPACE is the namespace of the service account and SERVICE_ACCOUNT is the name of the service account.</p>
</li>
</ul>
<h2 id="create-clusterissuer-using-workload-identity-setup">Create ClusterIssuer (using Workload Identity setup)</h2>
<p>Here is a sample manifest</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">cert-manager.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ClusterIssuer</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">letsencrypt-prod</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">acme</span><span class="pi">:</span>
<span class="na">email</span><span class="pi">:</span> <span class="s">you@youremail.com</span>
<span class="na">server</span><span class="pi">:</span> <span class="s">https://acme-v02.api.letsencrypt.org/directory</span>
<span class="na">privateKeySecretRef</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">letsencrypt-prod-account-key</span>
<span class="na">solvers</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">dns01</span><span class="pi">:</span>
<span class="na">cloudDNS</span><span class="pi">:</span>
<span class="c1"># The ID of the GCP project</span>
<span class="na">project</span><span class="pi">:</span> <span class="s">$PROJECT_ID</span>
</code></pre></div></div>
<p>Note that the issuer does not include a serviceAccountSecretRef property. Excluding this instructs cert-manager to use the default credentials provided by GKE workload identity.</p>
<h2 id="verify">Verify</h2>
<p>View the clusterissuer object:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl get clusterissuer
NAME READY AGE
letsencrypt-prod True 9s
<span class="nv">$ </span>kubectl describe clusterissuer
Name: letsencrypt-prod
...
Status:
Acme:
Last Registered Email: you@youremail.com
Uri: https://acme-v02.api.letsencrypt.org/acme/acct/12345678
Conditions:
Last Transition Time: 2020-03-18T15:05:26Z
Message: The ACME account was registered with the ACME server
Reason: ACMEAccountRegistered
Status: True
Type: Ready
Events: <none>
</code></pre></div></div>
<h2 id="create-test-certificate">Create test certificate</h2>
<p>After successful creation of the ClusterIssuer, you can create a test Certificate to verify that everything works.
Here is a sample manifest:</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">cert-manager.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Certificate</span>
<span class="na">metadata</span><span class="pi">:</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">example-com</span>
<span class="na">namespace</span><span class="pi">:</span> <span class="s">default</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">secretName</span><span class="pi">:</span> <span class="s">example-com-tls</span>
<span class="na">issuerRef</span><span class="pi">:</span>
<span class="c1"># The issuer created previously</span>
<span class="na">name</span><span class="pi">:</span> <span class="s">letsencrypt-prod</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">ClusterIssuer</span>
<span class="na">dnsNames</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">example.com</span>
</code></pre></div></div>
<p>It may take a few minutes for the certificate to be issued. While you are waiting, you can view the Certificate object and the associated resources being created:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl get certificate
NAME READY SECRET AGE
example-com False example-com-tls 65s
</code></pre></div></div>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl describe certificate
Name: example-com
...
Status:
Conditions:
Last Transition Time: 2020-03-18T15:19:42Z
Message: Waiting <span class="k">for </span>CertificateRequest <span class="s2">"example-com-123456787"</span> to <span class="nb">complete
</span>Reason: InProgress
Status: False
Type: Ready
Events:
Type Reason Age From Message
<span class="nt">----</span> <span class="nt">------</span> <span class="nt">----</span> <span class="nt">----</span> <span class="nt">-------</span>
Normal Requested 71s cert-manager Created new CertificateRequest resource <span class="s2">"example-com-123456787"</span>
</code></pre></div></div>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl get certificaterequest
NAME READY AGE
example-com-123456787 False 88s
</code></pre></div></div>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl describe certificaterequest example-com-123456787
Name: example-com-123456787
...
Status:
Conditions:
Last Transition Time: 2020-03-18T15:19:42Z
Message: Waiting on certificate issuance from order default/example-com-123456787-123456787: <span class="s2">"pending"</span>
Reason: Pending
Status: False
Type: Ready
Events:
Type Reason Age From Message
<span class="nt">----</span> <span class="nt">------</span> <span class="nt">----</span> <span class="nt">----</span> <span class="nt">-------</span>
Normal OrderCreated 96s cert-manager Created Order resource default/example-com-123456787-123456787
</code></pre></div></div>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl get order
NAME STATE AGE
example-com-123456787-123456787 pending 114s
</code></pre></div></div>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl describe order example-com-123456787-123456787
Name: example-com-123456787-123456787
...
Events:
Type Reason Age From Message
<span class="nt">----</span> <span class="nt">------</span> <span class="nt">----</span> <span class="nt">----</span> <span class="nt">-------</span>
Normal Created 118s cert-manager Created Challenge resource <span class="s2">"example-com-123456787-123456787-123456787"</span> <span class="k">for </span>domain <span class="s2">"example.com"</span>
</code></pre></div></div>
<p>From the <code class="language-plaintext highlighter-rouge">order</code> event, we can see that the challenge resource has been created. When the order creation is complete, you will see this in its event.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl describe order example-com-123456787-123456787
...
Events:
Type Reason Age From Message
<span class="nt">----</span> <span class="nt">------</span> <span class="nt">----</span> <span class="nt">----</span> <span class="nt">-------</span>
...
Normal Complete 4m2s cert-manager Order completed successfully
</code></pre></div></div>
<p>The state of <code class="language-plaintext highlighter-rouge">order</code> should change to <code class="language-plaintext highlighter-rouge">valid</code>.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl get order
NAME STATE AGE
example-com-123456787-123456787 valid 7m59s
</code></pre></div></div>
<p>CertificateRequest should have <code class="language-plaintext highlighter-rouge">READY</code> status as <code class="language-plaintext highlighter-rouge">True</code>.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl get CertificateRequest
NAME READY AGE
example-com-123456787 True 8m14s
</code></pre></div></div>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl describe CertificateRequest
...
Events:
Type Reason Age From Message
<span class="nt">----</span> <span class="nt">------</span> <span class="nt">----</span> <span class="nt">----</span> <span class="nt">-------</span>
Normal OrderCreated 8m37s cert-manager Created Order resource default/example-com-123456787-123456787
Normal CertificateIssued 5m49s cert-manager Certificate fetched from issuer successfully
</code></pre></div></div>
<p>Certificate should also have <code class="language-plaintext highlighter-rouge">READY</code> status as <code class="language-plaintext highlighter-rouge">True</code>.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl get certificate
NAME READY SECRET AGE
example-com True example-com-tls 9m53s
</code></pre></div></div>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>kubectl describe certificate example-com
...
Events:
Type Reason Age From Message
<span class="nt">----</span> <span class="nt">------</span> <span class="nt">----</span> <span class="nt">----</span> <span class="nt">-------</span>
Normal Requested 10m cert-manager Created new CertificateRequest resource <span class="s2">"example-com-123456787"</span>
Normal Issued 7m38s cert-manager Certificate issued successfully
</code></pre></div></div>kosyanyanwuIntroduction This article explains how to set up a ClusterIssuer to use Google CloudDNS to solve DNS01 ACME challenge. It assumes that your cluster is hosted on Google Cloud Platform (GCP) and that you already have a domain set up with CloudDNS. It also assumes that you have cert-manager installed on your cluster.Using kubeadm to create a Kubernetes 1.20 cluster on VirtualBox with Ubuntu2021-01-28T20:36:00+00:002021-01-28T20:36:00+00:00https://kosyfrances.com/kubernetes-cluster<h2 id="introduction">Introduction</h2>
<p><a href="https://kubernetes.io/docs/reference/setup-tools/kubeadm/">kubeadm</a> is a tool that helps you bootstrap a best-practice Kubernetes cluster in an easy, reasonably secure and extensible way.</p>
<p>This article explains how you can use kubeadm to set up a Kubernetes cluster on three VirtualBox virtual machines (one master and two workers) running Ubuntu 20.04 LTS. To follow through with this article, you will need to have <a href="https://www.virtualbox.org/">Virtualbox</a> and <a href="https://www.vagrantup.com/">Vagrant</a> installed.</p>
<p>If you just want a quick setup without wanting to know how it is done, you can skip the rest of this article and use this <a href="https://github.com/kosyfrances/kubeclust">Github repo</a>.</p>
<h2 id="create-virtual-machines">Create Virtual Machines</h2>
<p>Let us set up 3 VMs, one being the master (kubemaster) and the remaining two being the workers (worker1 and worker2). All VMs will be set up with 2 CPUs and 2GB RAM each. You can use this <a href="https://github.com/kosyfrances/kubeclust/blob/master/Vagrantfile">Vagrantfile</a> to spin up the VMs by simply running <code class="language-plaintext highlighter-rouge">vagrant up</code>.</p>
<p>The hostnames and IP addresses of the machines are as follows:</p>
<p><strong>kubemaster</strong> — 192.168.99.20</p>
<p><strong>worker1</strong> – 192.168.99.21</p>
<p><strong>worker2</strong> – 192.168.99.22</p>
<h2 id="install-docker">Install Docker</h2>
<p>The following steps should be done on all of the running VMs.</p>
<p>Install Docker v19.03 from Ubuntu’s repositories:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get update
<span class="nb">sudo </span>apt-get <span class="nb">install </span><span class="nv">containerd</span><span class="o">=</span>1.3.3-0ubuntu2 <span class="c"># because docker.io depends on it</span>
<span class="nb">sudo </span>apt-get <span class="nb">install</span> <span class="nt">-y</span> docker.io<span class="o">=</span>19.03.8-0ubuntu1.20.04.1
</code></pre></div></div>
<p>Create the <code class="language-plaintext highlighter-rouge">docker</code> group and add the <code class="language-plaintext highlighter-rouge">Vagrant</code> user:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>groupadd docker
<span class="nb">sudo </span>usermod <span class="nt">-aG</span> docker <span class="nv">$USER</span>
</code></pre></div></div>
<p>Log out and log back in so that your group membership is re-evaluated.</p>
<p>Configure docker to start on boot.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl <span class="nb">enable </span>docker
</code></pre></div></div>
<p>Refer to the <a href="https://docs.docker.com/install/linux/linux-postinstall/">official Docker installation</a> guides for more information.</p>
<h2 id="install-kubeadm-kubelet-and-kubectl">Install kubeadm, kubelet and kubectl</h2>
<p>Install these packages on all of the running VMs:</p>
<ul>
<li>
<p><strong>kubeadm</strong>: the command to bootstrap the cluster.</p>
</li>
<li>
<p><strong>kubelet</strong>: the component that runs on all of the machines in your cluster and does things like starting pods and containers.</p>
</li>
<li>
<p><strong>kubectl</strong>: the command line util to talk to your cluster.</p>
</li>
</ul>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt-get update <span class="o">&&</span> <span class="nb">sudo </span>apt-get <span class="nb">install</span> <span class="nt">-y</span> apt-transport-https curl
curl <span class="nt">-s</span> https://packages.cloud.google.com/apt/doc/apt-key.gpg | <span class="nb">sudo </span>apt-key add -
<span class="nb">cat</span> <span class="o"><<</span><span class="no">EOF</span><span class="sh"> | sudo tee /etc/apt/sources.list.d/kubernetes.list
deb https://apt.kubernetes.io/ kubernetes-xenial main
</span><span class="no">EOF
</span><span class="nb">sudo </span>apt-get update
<span class="nb">sudo </span>apt-get <span class="nb">install</span> <span class="nt">-y</span> <span class="nv">kubelet</span><span class="o">=</span>1.20.2-00 <span class="nv">kubeadm</span><span class="o">=</span>1.20.2-00 <span class="nv">kubectl</span><span class="o">=</span>1.20.2-00
<span class="nb">sudo </span>apt-mark hold kubelet kubeadm kubectl
</code></pre></div></div>
<h2 id="initialise-master-node">Initialise Master node</h2>
<p>This is where you decide what Container Network Interface (CNI) plugin to use as some of them require you to specify parameters when initialising the master node.</p>
<p>In this article we will be using <a href="https://docs.projectcalico.org/v3.1/getting-started/kubernetes/">Calico</a> as our network plugin.</p>
<p>When initialising kubeadm, the value given to it must match the value of <code class="language-plaintext highlighter-rouge">CALICO_IPV4POOL_CIDR</code> in the <a href="https://docs.projectcalico.org/manifests/calico.yaml">calico manifest</a>. This is required for Network Policy to work correctly with the calico plugin. As at the time of writing this article, the default value is <code class="language-plaintext highlighter-rouge">192.168.0.0/16</code>.</p>
<p>We also need to specify the advertise address of the API server (our master node) because we have multiple network adapters on the host and we need it to advertise on the host-only network IP.</p>
<p>Now run</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubeadm init <span class="nt">--apiserver-advertise-address</span><span class="o">=</span>192.168.99.20 <span class="nt">--pod-network-cidr</span><span class="o">=</span>192.168.0.0/16
</code></pre></div></div>
<p>After the initialisation finishes, make a record of the <code class="language-plaintext highlighter-rouge">kubeadm join</code> command that <code class="language-plaintext highlighter-rouge">kubeadm init</code> outputs. You will need it in a moment.</p>
<p>The <a href="https://kubernetes.io/docs/reference/setup-tools/kubeadm/kubeadm-token/">token</a> is used for mutual authentication between the master and the joining nodes. The token included here is secret, keep it safe as anyone with this token can add authenticated nodes to your cluster. These tokens can be listed, created and deleted with the <code class="language-plaintext highlighter-rouge">kubeadm token</code> command.</p>
<p>To start using your cluster, you need to run the following as a regular user:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">mkdir</span> <span class="nt">-p</span> <span class="nv">$HOME</span>/.kube
<span class="nb">sudo cp</span> <span class="nt">-i</span> /etc/kubernetes/admin.conf <span class="nv">$HOME</span>/.kube/config
<span class="nb">sudo chown</span> <span class="si">$(</span><span class="nb">id</span> <span class="nt">-u</span><span class="si">)</span>:<span class="si">$(</span><span class="nb">id</span> <span class="nt">-g</span><span class="si">)</span> <span class="nv">$HOME</span>/.kube/config
</code></pre></div></div>
<h2 id="deploy-pod-network">Deploy pod network</h2>
<p>You can now <a href="https://kubernetes.io/docs/concepts/cluster-administration/addons/">deploy a pod network</a> (Calico in our case) to the cluster. Deploying Calico is as easy as applying the <a href="https://docs.projectcalico.org/manifests/calico.yaml">manifest</a>. On the master node, download the manifest and apply.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>wget https://docs.projectcalico.org/manifests/calico.yaml
kubectl apply <span class="nt">-f</span> calico.yaml
</code></pre></div></div>
<p>Confirm that the pod network is installed by running:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl get pods <span class="nt">--all-namespaces</span>
</code></pre></div></div>
<p>You should get something like this:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>vagrant@kubemaster:~<span class="nv">$ </span>kubectl get pods <span class="nt">--all-namespaces</span>
NAMESPACE NAME READY STATUS RESTARTS AGE
kube-system calico-kube-controllers-744cfdf676-8npmm 1/1 Running 0 3m56s
kube-system calico-node-7v48f 1/1 Running 0 3m56s
kube-system coredns-74ff55c5b-lh5xs 1/1 Running 0 11m
kube-system coredns-74ff55c5b-tmq8g 1/1 Running 0 11m
kube-system etcd-kubemaster 1/1 Running 0 11m
kube-system kube-apiserver-kubemaster 1/1 Running 0 11m
kube-system kube-controller-manager-kubemaster 1/1 Running 0 11m
kube-system kube-proxy-dd9zv 1/1 Running 0 11m
kube-system kube-scheduler-kubemaster 1/1 Running 0 11m
</code></pre></div></div>
<p>If there are issues, check out <a href="https://kubernetes.io/docs/setup/independent/troubleshooting-kubeadm/">kubeadm troubleshooting docs</a>.</p>
<h2 id="join-cluster">Join cluster</h2>
<p>Remember the <code class="language-plaintext highlighter-rouge">kubeadm join</code> command that <code class="language-plaintext highlighter-rouge">kubeadm init</code> gave us … Get that command and run on worker1 and worker2 nodes. It should be something like this:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubeadm join --token <token> <master-ip>:<master-port> --discovery-token-ca-cert-hash sha256:<hash>
</code></pre></div></div>
<p>You can run <code class="language-plaintext highlighter-rouge">watch kubectl get nodes</code> on the kubemaster to see the worker machines join.</p>
<p>Eventually you should see something like this:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>NAME STATUS ROLES AGE VERSION
kubemaster Ready control-plane,master 7m2s v1.20.2
worker1 Ready <none> 6m33s v1.20.2
worker2 Ready <none> 6m33s v1.20.2
</code></pre></div></div>
<h2 id="tear-down">Tear down</h2>
<p>if you want to deprovision your cluster more cleanly, you should first <a href="https://kubernetes.io/docs/reference/generated/kubectl/kubectl-commands#drain">drain the node</a> and then deconfigure the node.</p>
<p>On kubemaster, with <code class="language-plaintext highlighter-rouge"><node name></code> being <code class="language-plaintext highlighter-rouge">kubemaster</code>, <code class="language-plaintext highlighter-rouge">worker1</code> and <code class="language-plaintext highlighter-rouge">worker2</code>, run:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>kubectl drain <node name> --delete-local-data --force --ignore-daemonsets
kubectl delete node <node name>
</code></pre></div></div>
<p>After draining all nodes, reset all kubeadm installed state:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sudo kubeadm reset
</code></pre></div></div>
<p>If you wish to start over simply run <code class="language-plaintext highlighter-rouge">kubeadm init</code> or <code class="language-plaintext highlighter-rouge">kubeadm join</code> with the appropriate arguments.</p>
<p>More options and information about the <a href="https://kubernetes.io/docs/reference/setup-tools/kubeadm/kubeadm-reset/">kubeadm reset</a> command.</p>kosyanyanwuIntroduction kubeadm is a tool that helps you bootstrap a best-practice Kubernetes cluster in an easy, reasonably secure and extensible way.Automating logs export to BigQuery with Terraform2020-12-11T22:05:00+00:002020-12-11T22:05:00+00:00https://kosyfrances.com/bigquery-logsink<h2 id="introduction">Introduction</h2>
<p>On <a href="https://cloud.google.com/logging/docs/export/configure_export_v2">Google cloud</a>, you export logs by creating one or more sinks that include a logs filter and an export destination. As Cloud Logging receives new log entries, they are compared against each sink. If a log entry matches a sink’s filter, then a copy of the log entry is written to the export destination.</p>
<p>This article describes how you can automate logs exports to <a href="https://cloud.google.com/bigquery/docs">BigQuery</a> with Terraform.</p>
<h2 id="create-bigquery-dataset">Create BigQuery dataset</h2>
<p>Using the <a href="https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/bigquery_dataset">google_bigquery_dataset</a> resource, create a BigQuery dataset.</p>
<div class="language-tf highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="k">resource</span> <span class="s2">"google_bigquery_dataset"</span> <span class="s2">"dataset"</span> <span class="p">{</span>
<span class="nx">dataset_id</span> <span class="p">=</span> <span class="s2">"example_dataset"</span>
<span class="nx">friendly_name</span> <span class="p">=</span> <span class="s2">"test"</span>
<span class="nx">description</span> <span class="p">=</span> <span class="s2">"This is a test description"</span>
<span class="nx">location</span> <span class="p">=</span> <span class="s2">"EU"</span>
<span class="nx">labels</span> <span class="p">=</span> <span class="p">{</span>
<span class="nx">env</span> <span class="p">=</span> <span class="s2">"default"</span>
<span class="p">}</span>
<span class="nx">access</span> <span class="p">{</span>
<span class="nx">role</span> <span class="p">=</span> <span class="s2">"OWNER"</span>
<span class="nx">user_by_email</span> <span class="p">=</span> <span class="nx">google_service_account</span><span class="p">.</span><span class="nx">bqowner</span><span class="p">.</span><span class="nx">email</span>
<span class="p">}</span>
<span class="nx">access</span> <span class="p">{</span>
<span class="nx">role</span> <span class="p">=</span> <span class="s2">"READER"</span>
<span class="nx">domain</span> <span class="p">=</span> <span class="s2">"hashicorp.com"</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="create-log-sink">Create log sink</h2>
<p>You can create a <a href="https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/logging_project_sink">project-level</a>, <a href="https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/logging_organization_sink">organization-level</a> or <a href="https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/logging_folder_sink">folder-level</a> logging sink. In this article, we will be creating a project-level logging sink using the <a href="https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/logging_project_sink">google_logging_project_sink</a> resource.</p>
<div class="language-tf highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"google_logging_project_sink"</span> <span class="s2">"log_sink"</span> <span class="p">{</span>
<span class="nx">name</span> <span class="p">=</span> <span class="s2">"my_log_sink"</span>
<span class="nx">description</span> <span class="p">=</span> <span class="s2">"some explaination on what this is"</span>
<span class="nx">destination</span> <span class="p">=</span> <span class="s2">"bigquery.googleapis.com/projects/[PROJECT_ID]/datasets/</span><span class="k">${</span><span class="nx">google_bigquery_dataset</span><span class="p">.</span><span class="nx">dataset</span><span class="p">.</span><span class="nx">dataset_id</span><span class="k">}</span><span class="s2">"</span>
<span class="nx">filter</span> <span class="p">=</span> <span class="s2">"resource.type = gce_instance AND resource.labels.instance_id = mylabel"</span>
<span class="c1"># Whether or not to create a unique identity associated with this sink.</span>
<span class="nx">unique_writer_identity</span> <span class="p">=</span> <span class="kc">true</span>
<span class="nx">bigquery_options</span> <span class="p">{</span>
<span class="c1"># options associated with big query</span>
<span class="c1"># Refer to the resource docs for more information on the options you can use</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="grant-unique-writer-access-to-bigquery">Grant unique writer access to BigQuery</h2>
<p>Because the sink above uses a <code class="language-plaintext highlighter-rouge">unique_writer</code>, we must grant that writer <a href="https://cloud.google.com/iam/docs/understanding-roles#bigquery-roles">bigquery.dataEditor</a> role for it to access BigQuery. You can use the <a href="https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_project_iam#google_project_iam_member">google_project_iam_member</a> resource.</p>
<div class="language-tf highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"google_project_iam_member"</span> <span class="s2">"log_writer"</span> <span class="p">{</span>
<span class="nx">role</span> <span class="p">=</span> <span class="s2">"roles/bigquery.dataEditor"</span>
<span class="nx">member</span> <span class="p">=</span> <span class="nx">google_logging_project_sink</span><span class="p">.</span><span class="nx">log_sink</span><span class="p">.</span><span class="nx">writer_identity</span>
<span class="p">}</span>
</code></pre></div></div>
<h2 id="conclusion">Conclusion</h2>
<p>After applying the steps above in a terraform file, in a short while, you should see a table created automatically in the BigQuery dataset with the logs exported to it.</p>kosyanyanwuIntroduction On Google cloud, you export logs by creating one or more sinks that include a logs filter and an export destination. As Cloud Logging receives new log entries, they are compared against each sink. If a log entry matches a sink’s filter, then a copy of the log entry is written to the export destination.One tunnel IPSec Site to site VPN with AWS and Strongswan2020-11-09T22:05:00+00:002020-11-09T22:05:00+00:00https://kosyfrances.com/site-to-site<h2 id="introduction">Introduction</h2>
<p>This article outlines the steps to set up a one tunnel IPSec Site to site VPN on AWS and a VM on another cloud provider running Strongswan.
I used a bare metal machine running on <a href="https://www.packet.com/">Packet</a> when I tried this out.</p>
<h2 id="spin-up-vm-and-install-strongswan">Spin up VM and install strongswan</h2>
<p>Create an small server with Ubuntu 18.01 LTS, and associate your public SSH key with it to be able to SSH.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># ssh into server</span>
<span class="nv">$ </span><span class="nb">sudo </span>apt update
<span class="nv">$ </span><span class="nb">sudo </span>apt <span class="nb">install </span>strongswan <span class="c"># strongswan will be active(running) by default</span>
<span class="c"># Version of strongswan in this case is 5.6.2-1</span>
</code></pre></div></div>
<p>We will refer to this VM henceforth as <code class="language-plaintext highlighter-rouge">Strongswan VPN server</code>.</p>
<h2 id="set-up-vpn-connection-on-aws">Set up VPN connection on AWS</h2>
<p>On AWS, Select the VPC service from the list of Services.</p>
<p>Create a VPC (using a private IP address range e.g 172.17.0.0/16).</p>
<p>On the <code class="language-plaintext highlighter-rouge">Virtual Private Network(VPN)</code> section on the left, select <code class="language-plaintext highlighter-rouge">Customer Gateways</code>.</p>
<p>Click <code class="language-plaintext highlighter-rouge">Create Customer Gateway</code>, on <code class="language-plaintext highlighter-rouge">Routing</code>, select <code class="language-plaintext highlighter-rouge">Static</code>, then add the internet-routable IP address of the Strongswan VPN server.</p>
<p>Next select <code class="language-plaintext highlighter-rouge">Virtual Private Gateways</code> on the section on the left.</p>
<p>Click on <code class="language-plaintext highlighter-rouge">Create Virtual Private Gateway</code> (VPG), leave the <code class="language-plaintext highlighter-rouge">Amazon default ASN</code> checked. Then click <code class="language-plaintext highlighter-rouge">Create Virtual Private Gateway</code>.</p>
<p>Select the VPG you created, click <code class="language-plaintext highlighter-rouge">Actions</code>, then click <code class="language-plaintext highlighter-rouge">Attach to VPC</code>.</p>
<p>Attach to the VPC you created earlier.</p>
<p>Following the creation of your VPG, visit the <code class="language-plaintext highlighter-rouge">Route Tables</code> section under <code class="language-plaintext highlighter-rouge">Virtual Private Cloud</code>.</p>
<p>Select the routing table corresponding to your subnet(s). (In this case, select the one associated with your VPC)</p>
<p>Afterwards, click the <code class="language-plaintext highlighter-rouge">Route Propagation</code> tab and then select the <code class="language-plaintext highlighter-rouge">vgw identifier</code> for the virtual private gateway that was created earlier.</p>
<p>Click <code class="language-plaintext highlighter-rouge">Edit route propagation</code> to view the <code class="language-plaintext highlighter-rouge">Propagate</code> checkbox, click the checkbox and choose <code class="language-plaintext highlighter-rouge">Save</code>.</p>
<p>Update security group to allow access to instances in your VPC from your network. Enable SSH, RDP and ICMP access.</p>
<p>To do that, in the navigation pane, choose <code class="language-plaintext highlighter-rouge">Security Groups</code> under <code class="language-plaintext highlighter-rouge">Security</code>.</p>
<p>Select the default security group for the VPC and enable SSH, RDP and ICMP access.</p>
<p>Finally, visit the <code class="language-plaintext highlighter-rouge">VPN</code> section on the left, choose <code class="language-plaintext highlighter-rouge">Site-to-Site VPN Connections</code>.</p>
<p>Click <code class="language-plaintext highlighter-rouge">Create VPN Connection</code>.</p>
<p>In the dialog that results, select the <code class="language-plaintext highlighter-rouge">virtual private gateway</code> (vgw) and the <code class="language-plaintext highlighter-rouge">customer gateway</code> that you have previously created.</p>
<p>For routing options, select <code class="language-plaintext highlighter-rouge">Static</code> then enter the IP CIDR of the Strongswan VPN server (e.g 147.75.44.166/31) i.e enter all of the IP prefixes of your on-premises network.</p>
<p>Leave the <code class="language-plaintext highlighter-rouge">Tunnel Options</code> to be automatically generated by Amazon.</p>
<p>Then click <code class="language-plaintext highlighter-rouge">Create VPN Connection</code>.</p>
<p>After the VPN connection has been created, the State of the connection should switch to available in a few minutes.</p>
<p>As the connections have not been made yet to the VPN servers, it is perfectly normal for the icons to be red.</p>
<p>Select the VPN connection that was created, and then note the Tunnel 1 and Tunnel 2 IP addresses.</p>
<p>Click the <code class="language-plaintext highlighter-rouge">Download Configuration</code> button when finished.</p>
<p>In the dialog box, select <code class="language-plaintext highlighter-rouge">Strongswan</code> for <code class="language-plaintext highlighter-rouge">Vendor</code>, Platform <code class="language-plaintext highlighter-rouge">Ubuntu 16.04</code> (this is all I saw), Software <code class="language-plaintext highlighter-rouge">Strongswan 5.5.1+</code>.</p>
<p>Then click <code class="language-plaintext highlighter-rouge">Download</code>.</p>
<h2 id="continue-setup-on-strongswan-vpn-server">Continue setup on Strongswan VPN server</h2>
<p>Follow the instructions on the downloaded file from AWS.</p>
<p>I did the steps in IPSEC Tunnel #1, did not do setup for Tunnel #2. (You can do the setup for Tunnel 2 as well for high availability.)</p>
<p>I omitted steps #2 - #5 then moved on to the <code class="language-plaintext highlighter-rouge">Automated Tunnel Healthcheck and Failover</code> section of the document.</p>
<h2 id="checking-connectivity">Checking connectivity</h2>
<p>On Strongswan VPN server</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>root@kosy:/etc# ipsec status <span class="c"># to ensure both of your tunnels are ESTABLISHED</span>
Security Associations <span class="o">(</span>1 up, 0 connecting<span class="o">)</span>:
Tunnel1[1]: ESTABLISHED 56 seconds ago, 147.75.102.25[147.75.102.25]...35.156.127.214[35.156.127.214]
Tunnel1<span class="o">{</span>1<span class="o">}</span>: INSTALLED, TUNNEL, reqid 1, ESP <span class="k">in </span>UDP SPIs: c59e6b8b_i 5ad4d103_o
Tunnel1<span class="o">{</span>1<span class="o">}</span>: 0.0.0.0/0 <span class="o">===</span> 0.0.0.0/0
<span class="nv">$ </span>root@kosy:/etc# ip route <span class="c"># to ensure route table entries were created for each of your tunnel interfaces, and the destination is the default via 147.75.102.24 dev bond0 onlink</span>
10.0.0.0/8 via 10.80.166.0 dev bond0
10.80.166.0/31 dev bond0 proto kernel scope <span class="nb">link </span>src 10.80.166.1
147.75.102.24/31 dev bond0 proto kernel scope <span class="nb">link </span>src 147.75.102.25
169.254.40.124/30 dev Tunnel1 proto kernel scope <span class="nb">link </span>src 169.254.40.126
172.17.0.0/16 dev Tunnel1 scope <span class="nb">link </span>metric 100
<span class="nv">$ </span>root@kosy:/etc# iptables <span class="nt">-t</span> mangle <span class="nt">-L</span> <span class="nt">-n</span> <span class="c"># to ensure entries were made for both of your tunnels in both the INPUT and FORWARD chain</span>
Chain PREROUTING <span class="o">(</span>policy ACCEPT<span class="o">)</span>
target prot opt <span class="nb">source </span>destination
Chain INPUT <span class="o">(</span>policy ACCEPT<span class="o">)</span>
target prot opt <span class="nb">source </span>destination
MARK esp <span class="nt">--</span> 35.156.127.214 147.75.102.25 MARK <span class="nb">set </span>0x64
Chain FORWARD <span class="o">(</span>policy ACCEPT<span class="o">)</span>
target prot opt <span class="nb">source </span>destination
TCPMSS tcp <span class="nt">--</span> 0.0.0.0/0 0.0.0.0/0 tcp flags:0x06/0x02 TCPMSS clamp to PMTU
Chain OUTPUT <span class="o">(</span>policy ACCEPT<span class="o">)</span>
target prot opt <span class="nb">source </span>destination
Chain POSTROUTING <span class="o">(</span>policy ACCEPT<span class="o">)</span>
target prot opt <span class="nb">source </span>destination
<span class="nv">$ </span>root@kosy:/etc# ifconfig <span class="c"># to ensure the correct 169.254.x addresses were assigned to each end of your peer-to-peer virtual tunnel interfaces</span>
Tunnel1: <span class="nv">flags</span><span class="o">=</span>209<UP,POINTOPOINT,RUNNING,NOARP> mtu 1419
inet 169.254.40.126 netmask 255.255.255.252 destination 169.254.40.125
inet6 fe80::200:5efe:934b:6619 prefixlen 64 scopeid 0x20<<span class="nb">link</span><span class="o">></span>
tunnel txqueuelen 1000 <span class="o">(</span>IPIP Tunnel<span class="o">)</span>
RX packets 0 bytes 0 <span class="o">(</span>0.0 B<span class="o">)</span>
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 <span class="o">(</span>0.0 B<span class="o">)</span>
TX errors 6 dropped 0 overruns 0 carrier 6 collisions 0
bond0: <span class="nv">flags</span><span class="o">=</span>5187<UP,BROADCAST,RUNNING,MASTER,MULTICAST> mtu 1500
inet 147.75.102.25 netmask 255.255.255.254 broadcast 255.255.255.255
inet6 2604:1380:2001:2e00::1 prefixlen 127 scopeid 0x0<global>
inet6 fe80::ec4:7aff:fee5:48fe prefixlen 64 scopeid 0x20<<span class="nb">link</span><span class="o">></span>
ether 0c:c4:7a:e5:48:fe txqueuelen 1000 <span class="o">(</span>Ethernet<span class="o">)</span>
RX packets 5110 bytes 3775495 <span class="o">(</span>3.7 MB<span class="o">)</span>
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 7377 bytes 765096 <span class="o">(</span>765.0 KB<span class="o">)</span>
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
bond0:0: <span class="nv">flags</span><span class="o">=</span>5187<UP,BROADCAST,RUNNING,MASTER,MULTICAST> mtu 1500
inet 10.80.166.1 netmask 255.255.255.254 broadcast 255.255.255.255
ether 0c:c4:7a:e5:48:fe txqueuelen 1000 <span class="o">(</span>Ethernet<span class="o">)</span>
enp0s20f0: <span class="nv">flags</span><span class="o">=</span>6211<UP,BROADCAST,RUNNING,SLAVE,MULTICAST> mtu 1500
ether 0c:c4:7a:e5:48:fe txqueuelen 1000 <span class="o">(</span>Ethernet<span class="o">)</span>
RX packets 5110 bytes 3775495 <span class="o">(</span>3.7 MB<span class="o">)</span>
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 7377 bytes 765096 <span class="o">(</span>765.0 KB<span class="o">)</span>
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
device memory 0xdf120000-df13ffff
enp0s20f1: <span class="nv">flags</span><span class="o">=</span>6147<UP,BROADCAST,SLAVE,MULTICAST> mtu 1500
ether 0c:c4:7a:e5:48:ff txqueuelen 1000 <span class="o">(</span>Ethernet<span class="o">)</span>
RX packets 0 bytes 0 <span class="o">(</span>0.0 B<span class="o">)</span>
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 <span class="o">(</span>0.0 B<span class="o">)</span>
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
device memory 0xdf100000-df11ffff
lo: <span class="nv">flags</span><span class="o">=</span>73<UP,LOOPBACK,RUNNING> mtu 65536
inet 127.0.0.1 netmask 255.0.0.0
inet6 ::1 prefixlen 128 scopeid 0x10<host>
loop txqueuelen 1000 <span class="o">(</span>Local Loopback<span class="o">)</span>
RX packets 0 bytes 0 <span class="o">(</span>0.0 B<span class="o">)</span>
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 0 bytes 0 <span class="o">(</span>0.0 B<span class="o">)</span>
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
</code></pre></div></div>
<p>On AWS VPN <code class="language-plaintext highlighter-rouge">Site-to-Site VPN Connections</code> section, check that the status of the one of the tunnels in <code class="language-plaintext highlighter-rouge">Tunnel details</code> is <code class="language-plaintext highlighter-rouge">UP</code> (since we made just one tunnel).</p>
<h2 id="test-traffic-on-tunnel">Test traffic on tunnel</h2>
<h3 id="on-aws">On AWS</h3>
<p>Create an AWS t2.nano EC2 instance in your VPC. (Your VPC should already have a subnet else you will be asked to create one)</p>
<p>Let’s call this instance <code class="language-plaintext highlighter-rouge">AWS test host</code>.</p>
<p>Verify that there is an internet gateway attached to your VPC.</p>
<p>Otherwise, choose <code class="language-plaintext highlighter-rouge">Create Internet Gateway</code> to create an internet gateway.</p>
<p>Select the internet gateway you created, and then choose <code class="language-plaintext highlighter-rouge">Attach to VPC</code> and follow the directions to attach it to your VPC.</p>
<p>In the navigation pane, choose <code class="language-plaintext highlighter-rouge">Subnets</code>, and then select your subnet.</p>
<p>On the <code class="language-plaintext highlighter-rouge">Route Table</code> tab, verify that there is a route with <code class="language-plaintext highlighter-rouge">0.0.0.0/0</code> as the destination and the internet gateway for your VPC as the target.</p>
<p>Otherwise, do the following:</p>
<p>Choose the ID of the route table (rtb-xxxxxxxx) to navigate to the route table.</p>
<p>On the <code class="language-plaintext highlighter-rouge">Routes</code> tab, choose <code class="language-plaintext highlighter-rouge">Edit routes</code>.</p>
<p>Choose <code class="language-plaintext highlighter-rouge">Add route</code>, use <code class="language-plaintext highlighter-rouge">0.0.0.0/0</code> as the destination and the internet gateway as the target.</p>
<p>Choose <code class="language-plaintext highlighter-rouge">Save routes</code>.</p>
<p>You should now be able to ssh into your instance.</p>
<p>If you still cannot ssh into your instance, check the instance inbound rules for SSH.</p>
<h3 id="on-packet-or-your-cloud-provider">On Packet (or your Cloud Provider)</h3>
<p>Create a new server. We will call this <code class="language-plaintext highlighter-rouge">Strongswan test host</code>.</p>
<p>This means we have two machines from Packet. Previous was <code class="language-plaintext highlighter-rouge">Strongswan VPN server</code> and new one is <code class="language-plaintext highlighter-rouge">Strongswan test host</code>.</p>
<p>In this scenario, both machines are running Ubuntu 18.04.</p>
<p>We need to configure routing tables on both of them (static routes in this case since we are not using BGP).</p>
<p>SSH into Strongswan VPN server.</p>
<p>Add route to the tunnel IP on Packet side for requests going to the AWS VPC side.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># ip route add AWS_VPC_CIDR via PACKET_TUNNEL_IP</span>
<span class="c"># e.g</span>
ip route add 172.17.0.0/16 via 169.254.40.126
</code></pre></div></div>
<p>SSH into Strongswan test host.</p>
<p>As at when I tried this, Packet creates machines in separate subnets by default, so trying to fix routing tables here is somehow complicated. Strongswan test host can ping Strongswan VPN server but cannot reach AWS test host. In this case, we need to figure out how to tell the routing table of Strongswan test host that any request to anything in our AWS VPC should be routed through Strongswan VPN server. But how can this be done when both machines are on different subnets?</p>
<p><a href="https://www.packet.com/">Packet</a>’s L3-only networking creates some challenges with routing packets from our private strongswan cluster to AWS. In this case, hosts are not in the same subnet. Instead, the host only sees a /31 network with an invisible datacenter-internal routing peer. In order to route packets to and from the VPN, we need to set up a tunnel between <a href="https://www.packet.com/">Packet</a> nodes and the VPN server. Those tunnels work peer-to-peer; i.e. while we need only one tunnel endpoint per node, the VPN server needs to have tunnel endpoints for all <a href="https://www.packet.com/">Packet</a> nodes.</p>
<p>You can find an example configuration (server and one node) below. Note that the interface names <code class="language-plaintext highlighter-rouge">node-1</code> (for Strongswan test host) and <code class="language-plaintext highlighter-rouge">vpn-server</code> are arbitrary. We will use 192.168.0.0/30 for the internal tunnel transfer network, and we will assume that the VPN server private IP is 10.80.166.1, and the node’s IP is 10.80.166.3.</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># VPN Server</span>
vpnserver<span class="nv">$ </span>ip tunnel add node-1 mode ipip remote 10.80.166.3 <span class="nb">local </span>10.80.166.1
vpnserver<span class="nv">$ </span>ip <span class="nb">link set </span>up dev node-1
vpnserver<span class="nv">$ </span>ip address add 192.168.0.1 dev node-1
vpnserver<span class="nv">$ </span>ip route add 192.168.0.0/30 dev node-1
vpnserver<span class="nv">$ </span>iptables <span class="nt">-t</span> nat <span class="nt">-A</span> POSTROUTING <span class="nt">-s</span> 192.168.0.2 <span class="nt">-j</span> SNAT <span class="nt">--to</span> 10.80.166.3
<span class="c"># Note that the last line (iptables) creates a source NAT rule which hides the internal IPIP tunnel transfer network from the AWS network.</span>
<span class="c"># Node</span>
node1<span class="nv">$ </span>ip tunnel add vpn-server mode ipip remote 10.80.166.1 <span class="nb">local </span>10.80.166.3
node1<span class="nv">$ </span>ip <span class="nb">link set </span>up dev vpn-server
node1<span class="nv">$ </span>ip address add 192.168.0.2 dev vpn-server
node1<span class="nv">$ </span>ip route add 192.168.0.0/30 dev vpn-server
</code></pre></div></div>
<p>With this, you should be able to ping the AWS test host from Strongswan test host using their private IP addresses.</p>
<h3 id="references">References:</h3>
<ul>
<li>
<p>https://docs.aws.amazon.com/vpn/latest/s2svpn/SetUpVPNConnections.html</p>
</li>
<li>
<p>https://openvpn.net/vpn-server-resources/extending-vpn-connectivity-to-amazon-aws-vpc-using-aws-vpc-vpn-gateway-service/</p>
</li>
</ul>kosyanyanwuIntroduction This article outlines the steps to set up a one tunnel IPSec Site to site VPN on AWS and a VM on another cloud provider running Strongswan. I used a bare metal machine running on Packet when I tried this out.How to make your workplace toxic for women.2020-09-04T13:32:00+00:002020-09-04T13:32:00+00:00https://kosyfrances.com/workplace-toxic-women<p>Was I hired for diversity sake? Or was I hired because of my skills? Am I the only one who has thought about things like these?</p>
<p>In my tech journey, I have heard stories of women complaining about being treated differently in a male-dominated work environment and I would like to share tips on how you can make your team a toxic one for women, based on my own observations.</p>
<h3 id="mansplain">Mansplain</h3>
<p>You should mansplain a lot. This is a common attitude in every “tech bro” team and it is important that you do this, so that you don’t stand out. An example of how to do this is, when she says something in the architecture design meeting, ignore her until a guy repeats the same thing, then you can have your <em>aha moment</em>! Isn’t that awesome? You have to make sure the boys speak over her during sprint planning. It is important. Remember, if she does not make noise, then she is probably not smart and not a good team fit.</p>
<h3 id="micromanagement">Micromanagement</h3>
<p>Will your work environment be toxic enough if you don’t micromanage? Whenever she is given a task to work on, be sure to sit beside her looking over her shoulder, because she has no clue about how to accomplish her task. Always ask her, every minute, every second, whether she has done something. Make sure she writes notes on every step she takes while solving the problem, and do not read it but make her repeat it again to you each time you see her. This alone is bound to make her feel like she is not good enough, and remember, that is the goal.</p>
<h3 id="diversity">Diversity</h3>
<p>When your company needs to go for a conference, ask her to apply for a diversity scholarship. After all, you hired her for diversity sake, right? You can get conference tickets for the guys, but it is not necessary to get one for her. You should be certain that she will get a diversity scholarship particularly when she satisfies all of the requirements for diversity in tech.</p>
<h3 id="promotion">Promotion</h3>
<p>Does she deserve a promotion? No way? Should you give her a raise? Nope! Even though she always gets her tasks done, how are you sure she even knows what she is doing? When she asks for a promotion or a raise, bring up other things you want her to do, but don’t allow her to do it. Remember the micromanagement cycle has to continue, and you want her to know that you don’t trust her well enough to handle tasks, so don’t even allow her to take ownership of those tasks. Make sure you only assign routine, boring tasks and minor bug fixes to her. The big, challenging and interesting refactoring or feature work should always go to the boys - that way, she won’t have any claims for promotion or a raise.</p>
<h3 id="leadership">Leadership</h3>
<p>When there is a need for a leadership position like team lead or manager, do not give her the opportunity to play that role. Even if it means bringing a guy from a different team to manage her team alongside his, do it! Remember, the goal is to prove to her that she is not good enough to handle leadership responsibilities.</p>
<hr />
<p>You may have read this article and wondered what the point of the whole sarcasm is, but, as a team lead, manager, boss, or whatever leadership role you might be in, it is very important to take time and reflect on whether your work environment or team is a toxic one for women in tech. Many women, including me, have experienced some or all of the points listed above. Because we are not screaming about it, does not mean it should remain as the norm. Sometimes, it is the things you overlook and consider irrelevant, that can cause your workplace to be an uncomfortable place for women.</p>kosyanyanwuWas I hired for diversity sake? Or was I hired because of my skills? Am I the only one who has thought about things like these?Importing firestore index to terraform2020-04-08T12:43:00+00:002020-04-08T12:43:00+00:00https://kosyfrances.com/firestore-index-terraform<h2 id="introduction">Introduction</h2>
<p><a href="https://firebase.google.com/docs/firestore/query-data/indexing">Google Cloud Firestore</a> ensures query performance by requiring an index for every query. Index creation takes a few minutes, depending on how much data needs to be updated. The more documents that match the fields being indexed, the longer creation takes. Also, for each unique collection ID, you can have only one index build in progress. Multiple index builds on the same collection ID complete sequentially.</p>
<p>Terraform has a <a href="https://www.terraform.io/docs/providers/google/r/firestore_index.html">google_firestore_index</a> resource used to manage composite indexes, with a default timeout of 10 mins. However, it supports the use of a timeout configuration option, where you can specify your own default. In a situation where you do not know how long the index build will take, and you use a lesser default, terraform will time out before the index build completes. If you try to apply terraform again, it will give the following error:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Error: Error creating Index: googleapi: Error 409: index already exists
</code></pre></div></div>
<p>This error is due to the fact that the index already exists as it is already being created, but you don’t have the final state of the resource when creation is complete. In this case, <a href="https://www.terraform.io/docs/import/index.html">terraform import</a> comes to the rescue. Terraform is able to import existing infrastructure. This allows you to take resources you have created by some other means and bring it under Terraform management.</p>
<p>This article explains how you can import the existing firestore index infrastructure to be managed by Terraform. As at the time of writing this, the current implementation of Terraform import can only import resources into the state. It does not generate configuration.</p>
<h2 id="import-existing-firestore-index-infrastructure">Import existing firestore index infrastructure</h2>
<p>Assume you have a configuration like this:</p>
<div class="language-tf highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"google_firestore_index"</span> <span class="s2">"my-index"</span> <span class="p">{</span>
<span class="nx">project</span> <span class="p">=</span> <span class="s2">"my-project-name"</span>
<span class="nx">collection</span> <span class="p">=</span> <span class="s2">"chatrooms"</span>
<span class="nx">fields</span> <span class="p">{</span>
<span class="nx">field_path</span> <span class="p">=</span> <span class="s2">"name"</span>
<span class="nx">order</span> <span class="p">=</span> <span class="s2">"ASCENDING"</span>
<span class="p">}</span>
<span class="nx">fields</span> <span class="p">{</span>
<span class="nx">field_path</span> <span class="p">=</span> <span class="s2">"description"</span>
<span class="nx">order</span> <span class="p">=</span> <span class="s2">"DESCENDING"</span>
<span class="p">}</span>
<span class="nx">fields</span> <span class="p">{</span>
<span class="nx">field_path</span> <span class="p">=</span> <span class="s2">"__name__"</span>
<span class="nx">order</span> <span class="p">=</span> <span class="s2">"DESCENDING"</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>And you want to import the state for it in this form</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>terraform import <span class="s1">'google_firestore_index.my-index'</span> <name>
</code></pre></div></div>
<p>where <a href="https://www.terraform.io/docs/providers/google/r/firestore_index.html#name">name</a> is a server defined name for this index with the format <code class="language-plaintext highlighter-rouge">projects/{project}/databases/{database}/collectionGroups/{collection}/indexes/{server_generated_id}</code>.</p>
<p>In this format, <code class="language-plaintext highlighter-rouge">{project}</code> is your GCP project ID, <code class="language-plaintext highlighter-rouge">{database}</code> is the name of the database e.g default, <code class="language-plaintext highlighter-rouge">{collection}</code> is the name of the collection in the example configuration above i.e <code class="language-plaintext highlighter-rouge">chatrooms</code>, and <code class="language-plaintext highlighter-rouge">{server_generated_id}</code> is the <code class="language-plaintext highlighter-rouge">NAME</code> gotten when you do:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>gcloud firestore indexes composite list
┌──────────────┬──────────────────┬─────────────┬───────┬────────────────────────────────┬────────────┬──────────────┐
│ NAME │ COLLECTION_GROUP │ QUERY_SCOPE │ STATE │ FIELD_PATHS │ ORDER │ ARRAY_CONFIG │
├──────────────┼──────────────────┼─────────────┼───────┼────────────────────────────────┼────────────┼──────────────┤
│ CICAgPilZUK │ chatrooms │ COLLECTION │ READY │ name │ ASCENDING │ │
│ │ │ │ │ description │ ASCENDING │ │
├──────────────┼──────────────────┼─────────────┼───────┼────────────────────────────────┼────────────┼──────────────┤
</code></pre></div></div>
<p>So we end up having:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>terraform import <span class="s1">'google_firestore_index.my-index'</span> <span class="s1">'projects/your_gcp_project_id/databases/(default)/collectionGroups/chatrooms/indexes/CICAgPilZUK'</span>
google_firestore_index.my-index: Importing from ID <span class="s2">"projects/your_gcp_project_id/databases/(default)/collectionGroups/chatrooms/indexes/CICAgPilZUK"</span>...
google_firestore_index.my-index: Import prepared!
Prepared google_firestore_index <span class="k">for </span>import
google_firestore_index.my-index: Refreshing state... <span class="o">[</span><span class="nb">id</span><span class="o">=</span>projects/your_project_id/databases/<span class="o">(</span>default<span class="o">)</span>/collectionGroups/chatrooms/indexes/CICAgPilZUK]
Import successful!
The resources that were imported are shown above. These resources are now <span class="k">in
</span>your Terraform state and will henceforth be managed by Terraform.
</code></pre></div></div>
<p>This should import your existing resource into terraform state, and when you do terraform plan, you should not see any changes to apply.</p>kosyanyanwuIntroduction Google Cloud Firestore ensures query performance by requiring an index for every query. Index creation takes a few minutes, depending on how much data needs to be updated. The more documents that match the fields being indexed, the longer creation takes. Also, for each unique collection ID, you can have only one index build in progress. Multiple index builds on the same collection ID complete sequentially.Using local images with Minikube2020-02-29T12:05:00+00:002020-02-29T12:05:00+00:00https://kosyfrances.com/minikube-registry<h2 id="introduction">Introduction</h2>
<p><a href="https://kubernetes.io/docs/setup/learning-environment/minikube/">Minikube</a> is a tool that makes it easy to run Kubernetes locally. Minikube runs a single-node Kubernetes cluster inside a Virtual Machine (VM) on your laptop for users looking to try out Kubernetes or develop with it day-to-day.</p>
<p>This article explains how you can use Minikube’s built-in Docker daemon without having to push images to a remote registry when trying out things locally, which speeds up local experiments.</p>
<h2 id="steps">Steps</h2>
<p>Start Minikube with:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>minikube start
</code></pre></div></div>
<p>To be able to work with Minikube’s docker daemon on your mac/linux host, use the <code class="language-plaintext highlighter-rouge">docker-env</code> command in your current shell:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">eval</span> <span class="si">$(</span>minikube docker-env<span class="si">)</span>
</code></pre></div></div>
<p>You can see containers in the Minikube registry with:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker ps
</code></pre></div></div>
<p>Build your image with the <code class="language-plaintext highlighter-rouge">docker build</code> command.</p>
<p>On your deployment manifest, set <code class="language-plaintext highlighter-rouge">spec.containers.imagePullPolicy</code> to <code class="language-plaintext highlighter-rouge">Never</code>, otherwise kubernetes won’t use the images you built locally.</p>
<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nn">...</span>
<span class="na">spec</span><span class="pi">:</span>
<span class="na">containers</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">sampleApp</span>
<span class="na">image</span><span class="pi">:</span> <span class="s">sampleApp:0.0.1</span>
<span class="na">imagePullPolicy</span><span class="pi">:</span> <span class="s">Never</span>
<span class="na">ports</span><span class="pi">:</span>
<span class="s">...</span>
</code></pre></div></div>
<p>When you do not wish to use Minikube docker registry, you can unset the Minikube variables from your shell with:</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">eval</span> <span class="si">$(</span>minikube docker-env <span class="nt">-u</span><span class="si">)</span>
</code></pre></div></div>kosyanyanwuIntroduction Minikube is a tool that makes it easy to run Kubernetes locally. Minikube runs a single-node Kubernetes cluster inside a Virtual Machine (VM) on your laptop for users looking to try out Kubernetes or develop with it day-to-day.Building software for the trash2018-10-24T12:05:00+00:002018-10-24T12:05:00+00:00https://kosyfrances.com/software-for-trash<p><em>Originally published on <a href="https://medium.com/@kosyfrances/building-software-for-the-trash-2b4c596f9ac2">Medium</a></em>.</p>
<p>If you are a software developer, then you have probably found yourself in this situation, where you build a piece of software that never gets used. The patterns are similar — It could begin with a new idea or your project manager discussing the possibility of a new idea/task, the thought of how it would look when complete, engulfs you as your creative juices begin to flow. You take your time breaking the goal down into little tasks that you arrange in a beautiful project management tool with associated tags. You are a consummate professional after all. You take your time building, writing tests along the way because, TDD, duh! A couple of weeks go by, months even, and when you are done, your tiny piece of software lives. It lives 🎊 🎊 🎊. But then the software never gets a look, forget about it being used.</p>
<p>Yea that happened to me so much I coined a term for it, the title of this article. The rest of this piece details my past experience with this phenomenon and what I learnt in the process of building software for the trash.</p>
<p>I was part of a team, let’s call it Team Awesome. Team Awesome was actually awesome because every team member was awesome ❤️. Our team lead, let’s call them Awesome Lead, had really cool ideas of what we needed to build. I began my first task of building this wrapper tool, let’s call it Awesome Tool. Awesome Tool was meant to automate some of the things we did by hand, as a result of architectural technical debt.</p>
<p>After about three months of research and learning, I finished building Awesome Tool and was really proud of it 🎊 🎊 🎊.</p>
<p><img src="https://user-images.githubusercontent.com/10073270/92233883-60455500-eeb1-11ea-8eb2-3fae925f25bf.png" alt="image" /></p>
<p>Out of excitement, I asked Awesome Lead about using Awesome Tool, as I wanted some sort of feedback based on its usage. Awesome Lead said they would let me know when we could start using it. A side effect from working on Awesome Tool was a blossoming interest in building Infrastructure tooling, and although desperate for feedback on Awesome Tool, my desire to create more won and I moved on to a new task, a new idea. And though I asked a few more times about Awesome Tool’s status after a while, it became glaringly obvious that we were not going to use Awesome Tool :(</p>
<p>I continued with building more tools, that never got used. 😢</p>
<p><em>The effect of this cycle of building for the trash was a hit on my motivational levels, and so it was that my once brimful creative juices were at an all-time low. I stopped looking forward to work :(</em></p>
<p>As the saying goes, “One man’s trash, another’s treasure”. One of the many treasures was that I got the chance of learning while working on things I had not done before. Another was the luxury of time. Building for the trash came with no sense of urgency. I spent all that “free time” studying and refining my career objectives.</p>
<p>It is easy to lose motivation at what you do, especially when it always gets thrown into the trash or goes unseen by the intended audience. But, it is essential to find something that you enjoy doing, in the meantime, to keep you going.</p>kosyanyanwuOriginally published on Medium.