<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Jeff's Chronicles]]></title><description><![CDATA[Practical insights on DevOps, DataOps and AIOps. Hands-on experimentation on the evolving.]]></description><link>https://blog.kakarukeys.quest/</link><image><url>https://blog.kakarukeys.quest/favicon.png</url><title>Jeff&apos;s Chronicles</title><link>https://blog.kakarukeys.quest/</link></image><generator>Ghost 5.82</generator><lastBuildDate>Thu, 29 Jan 2026 03:23:31 GMT</lastBuildDate><atom:link href="https://blog.kakarukeys.quest/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Learn Kubernetes with k3s - part5 - Helm]]></title><description><![CDATA[<p>This post is the continuation of <a href="https://blog.kakarukeys.quest/learn-kubernetes-k3s-part4-services/" rel="noreferrer">part 4</a>.</p><p>Previously, I have used <strong>helm</strong>, the Kubernetes package manager to install an nginx application. To list all the releases</p><pre><code class="language-bash">$ helm list
NAME      	NAMESPACE	REVISION	UPDATED                                	STATUS  	CHART        	APP VERSION
my-release	default  	1       	2024-03-19 22:58:08.794126266 +0800 +08	deployed	nginx-15.14.</code></pre>]]></description><link>https://blog.kakarukeys.quest/learn-kubernetes-k3s-p5-helm/</link><guid isPermaLink="false">6602b91869b40300070d5115</guid><category><![CDATA[k8s]]></category><dc:creator><![CDATA[Jeff Wong]]></dc:creator><pubDate>Wed, 27 Mar 2024 15:10:42 GMT</pubDate><content:encoded><![CDATA[<p>This post is the continuation of <a href="https://blog.kakarukeys.quest/learn-kubernetes-k3s-part4-services/" rel="noreferrer">part 4</a>.</p><p>Previously, I have used <strong>helm</strong>, the Kubernetes package manager to install an nginx application. To list all the releases</p><pre><code class="language-bash">$ helm list
NAME      	NAMESPACE	REVISION	UPDATED                                	STATUS  	CHART        	APP VERSION
my-release	default  	1       	2024-03-19 22:58:08.794126266 +0800 +08	deployed	nginx-15.14.0	1.25.4
</code></pre>
<p>We can see the only one release <code>my-release</code> in the list. To get the release notes which was displayed on the initial installation</p><pre><code class="language-bash">$ helm get notes my-release
NOTES:
CHART NAME: nginx
CHART VERSION: 15.14.0
APP VERSION: 1.25.4
...
</code></pre>
<p>To modify the application&apos;s config, modify <code>values.yaml</code> to </p><pre><code class="language-bash">$ cat values.yaml
service:
  type: LoadBalancer
</code></pre>
<p>and run</p><pre><code class="language-bash">$ helm upgrade my-release \
  oci://registry-1.docker.io/bitnamicharts/nginx \
  -f values.yaml
</code></pre>
<p>We see a confirmation message of the upgrade, and the content of the release notes has changed too. Run</p><pre><code class="language-bash">$ kubectl get services/my-release-nginx
NAME               TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
my-release-nginx   LoadBalancer   10.43.206.241   &lt;pending&gt;     80:30080/TCP   6d22h
</code></pre>
<p>The service type of <code>my-release-nginx</code> has changed from NodePort to LoadBalancer. Note that this way of resource provisioning is a break from the imperative model of <code>kubectl create</code> as we now manage the application config in code: our <code>values.yaml</code> file.</p><p>To rollback</p><pre><code class="language-bash">$ helm rollback my-release 1
$ kubectl get services/my-release-nginx
NAME               TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
my-release-nginx   NodePort   10.43.206.241   &lt;none&gt;        80:30080/TCP   6d22h
</code></pre>
<p>We see that we are back to release 1. </p><p>Now we are using the imperative model again, since the actual application config no longer aligns with what is in the code. We will find a more permanent solution to enforce IaC in future.</p><p>Finally to uninstall the application</p><pre><code class="language-bash">$ helm uninstall my-release
release &quot;my-release&quot; uninstalled
</code></pre>
<p>You can find helm charts of many open-source applications on the public registry <a href="https://artifacthub.io/?ref=blog.kakarukeys.quest" rel="noreferrer">Artifact Hub</a>. Sometimes the helm chart can be installed from an OCI link such as nginx. </p><p>Sometimes you would need to add a repository first. For example, to install <a href="https://artifacthub.io/packages/helm/milvus/milvus?ref=blog.kakarukeys.quest" rel="noreferrer">milvus db</a>,</p><pre><code class="language-bash">$ helm repo add milvus https://milvus-io.github.io/milvus-helm/

$ helm repo list
NAME  	URL                                     
milvus	https://milvus-io.github.io/milvus-helm/

$ helm repo update

$ helm upgrade --install my-release milvus/milvus
Release &quot;my-release&quot; does not exist. Installing it now.
NAME: my-release
LAST DEPLOYED: Wed Mar 27 22:51:20 2024
NAMESPACE: default
STATUS: deployed
REVISION: 1
TEST SUITE: None
</code></pre>
<p>An organization who deploys frequently to Kubernetes would probably use a private registry, in which case, you need to login to the registry first, by <code>helm registry login</code> before you run <code>helm repo add</code>.</p><h3 id="summary">Summary</h3><p>A Helm chart is a collection of YAML files that describe a related set of Kubernetes resources, such as deployments, services, secrets, configmaps, and ingresses. It packages all the necessary resources for an application into a single unit.</p><p>When you install a Helm chart, Helm renders all the YAML manifests in the chart and deploys them to the Kubernetes cluster together. This ensures that all the required resources are created together.</p><p>Helm charts use templating to reduce duplication of YAML code. This allows you to define common configurations in one place and reuse them across different environments or applications.</p><p>To be continued in part 6, the common Helm chart parameters.</p>]]></content:encoded></item><item><title><![CDATA[Private PyPI repository on AWS - part 2]]></title><description><![CDATA[<p>This post is the continuation of <a href="https://blog.kakarukeys.quest/private-pypi-repository-aws-p1/" rel="noreferrer">part 1</a>.</p><p>After creating the repository, we can test it by publishing a package to it and then installing that package in another project. This can be done using <strong>poetry</strong>, the python package manager.</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://python-poetry.org/?ref=blog.kakarukeys.quest"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Poetry - Python dependency management and packaging made easy</div><div class="kg-bookmark-description">Python</div></div></a></figure>]]></description><link>https://blog.kakarukeys.quest/private-pypi-repository-aws-p2/</link><guid isPermaLink="false">6601873869b40300070d5087</guid><category><![CDATA[AWS DevOps]]></category><category><![CDATA[Python]]></category><category><![CDATA[Terraform]]></category><dc:creator><![CDATA[Jeff Wong]]></dc:creator><pubDate>Mon, 25 Mar 2024 15:14:56 GMT</pubDate><content:encoded><![CDATA[<p>This post is the continuation of <a href="https://blog.kakarukeys.quest/private-pypi-repository-aws-p1/" rel="noreferrer">part 1</a>.</p><p>After creating the repository, we can test it by publishing a package to it and then installing that package in another project. This can be done using <strong>poetry</strong>, the python package manager.</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://python-poetry.org/?ref=blog.kakarukeys.quest"><div class="kg-bookmark-content"><div class="kg-bookmark-title">Poetry - Python dependency management and packaging made easy</div><div class="kg-bookmark-description">Python dependency management and packaging made easy</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://python-poetry.org/images/favicon-origami-32.png" alt><span class="kg-bookmark-author">Python dependency management and packaging made easy</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://python-poetry.org/images/logo-origami.svg" alt></div></a></figure><h2 id="create-and-publish-a-package">Create and publish a package</h2><p>Create a project containing the following files, and define the dependencies with poetry commands</p><pre><code class="language-bash">.
&#x2514;&#x2500;&#x2500; my_package
&#xA0;&#xA0;&#xA0; &#x251C;&#x2500;&#x2500; hello.py
&#xA0;&#xA0;&#xA0; &#x2514;&#x2500;&#x2500; __init__.py

$ cat my_package/hello.py 
def f():
    return 4
    
$ pyenv local 3.9.18
$ poetry init
$ poetry add requests boto3
</code></pre>
<p>We have used <strong>pyenv</strong> to set the virtual environment python version.</p><figure class="kg-card kg-bookmark-card"><a class="kg-bookmark-container" href="https://github.com/pyenv/pyenv?ref=blog.kakarukeys.quest"><div class="kg-bookmark-content"><div class="kg-bookmark-title">GitHub - pyenv/pyenv: Simple Python version management</div><div class="kg-bookmark-description">Simple Python version management. Contribute to pyenv/pyenv development by creating an account on GitHub.</div><div class="kg-bookmark-metadata"><img class="kg-bookmark-icon" src="https://github.githubassets.com/assets/pinned-octocat-093da3e6fa40.svg" alt><span class="kg-bookmark-author">GitHub</span><span class="kg-bookmark-publisher">pyenv</span></div></div><div class="kg-bookmark-thumbnail"><img src="https://opengraph.githubassets.com/199057bf921950113e00686e8a859e3a7ccb77b638f23a2716b61af782144a06/pyenv/pyenv" alt></div></a></figure><p>To add our private PyPI repository to poetry</p><pre><code class="language-bash">$ poetry config repositories.private-pypi \
    https://example-111122223333.d.codeartifact.us-west-2.amazonaws.com/private-pypi/
</code></pre>
<p>The URL is the endpoint URL from the Terraform outputs in <a href="https://blog.kakarukeys.quest/private-pypi-repository-aws-p1/" rel="noreferrer">part 1</a>.</p><p>To connect to the PyPI repository, we need to get an authorization token which will be valid for only 12 hours. The IAM user we use for requesting the token requires a special IAM permission <code>sts:GetServiceBearerToken.</code></p><p>I don&apos;t like to mess around with my root user, so I created a test user without a login profile just for testing.</p><pre><code class="language-hcl">module &quot;test_user&quot; {
  source  = &quot;terraform-aws-modules/iam/aws//modules/iam-user&quot;
  version = &quot;5.37.0&quot;

  name          = &quot;test_user&quot;
  force_destroy = true

  create_iam_user_login_profile = false
  create_iam_access_key         = true
}

resource &quot;aws_iam_policy&quot; &quot;allow_sts_get_service_bearer_token&quot; {
 name        = &quot;AllowSTSGetServiceBearerToken&quot;
 description = &quot;Policy to allow sts:GetServiceBearerToken&quot;

 policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [
      {
        Effect = &quot;Allow&quot;
        Action = &quot;sts:GetServiceBearerToken&quot;
        Resource = &quot;*&quot;
      },
    ]
 })
}

resource &quot;aws_iam_policy_attachment&quot; &quot;attach_policy_to_user&quot; {
 name       = &quot;AttachPolicyToUser&quot;
 users      = [module.test_user.iam_user_name]
 policy_arn = aws_iam_policy.allow_sts_get_service_bearer_token.arn
}

output &quot;iam_access_key_id&quot; {
  description = &quot;iam_access_key_id&quot;
  value       = module.test_user.iam_access_key_id
}

output &quot;iam_access_key_secret&quot; {
  description = &quot;iam_access_key_secret&quot;
  value       = module.test_user.iam_access_key_secret
  sensitive   = true
}
</code></pre>
<p>Remember to add this IAM user&apos;s ARN to <code>codeartifact_readwrite_access_arns</code>.</p><pre><code class="language-hcl">codeartifact_readwrite_access_arns  = [
  module.example_github_oidc_role.arn,
  module.test_user.iam_user_arn,
]
</code></pre>
<p>After terraform plan &amp; apply, run the following to get the value for <code>iam_access_key_secret</code></p><pre><code class="language-bash">$ terraform output iam_access_key_secret
</code></pre>
<p>Setup the AWS commandline with <code>iam_access_key_id</code> and <code>iam_access_key_secret</code>.</p><pre><code class="language-bash">$ aws configure
</code></pre>
<p>Now we can request for a token.</p><pre><code class="language-bash">$ TOKEN=`aws codeartifact get-authorization-token --domain example --domain-owner 111122223333 --query authorizationToken --output text`
$ poetry config http-basic.private-pypi aws $TOKEN
</code></pre>
<p>We can then build the project and publish the resulting packages</p><pre><code class="language-bash">$ poetry build -v
$ poetry publish -v --repository=private-pypi
</code></pre>
<p>Login to AWS console, look for CodeArtifact service, browse into <code>private-pypi</code> repository and you should see</p>
<figure class="kg-card kg-image-card"><img src="https://blog.kakarukeys.quest/content/images/2024/03/my_package.png" class="kg-image" alt loading="lazy" width="1924" height="798" srcset="https://blog.kakarukeys.quest/content/images/size/w600/2024/03/my_package.png 600w, https://blog.kakarukeys.quest/content/images/size/w1000/2024/03/my_package.png 1000w, https://blog.kakarukeys.quest/content/images/size/w1600/2024/03/my_package.png 1600w, https://blog.kakarukeys.quest/content/images/2024/03/my_package.png 1924w" sizes="(min-width: 720px) 720px"></figure><p>To be continued in part 3, installing from private PyPI.</p>]]></content:encoded></item><item><title><![CDATA[Private PyPI repository on AWS - part 1]]></title><description><![CDATA[<p>Development teams often find themselves needing to share common code across different projects. This is a common challenge in software development.</p><p>Reusing code not only saves time but also ensures consistency and improves software reliability. </p><p>Consider the scenario where a development team has a set of utility functions that are</p>]]></description><link>https://blog.kakarukeys.quest/private-pypi-repository-aws-p1/</link><guid isPermaLink="false">65e875f969b40300070d4c6a</guid><category><![CDATA[AWS DevOps]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Jeff Wong]]></dc:creator><pubDate>Sun, 24 Mar 2024 01:44:42 GMT</pubDate><content:encoded><![CDATA[<p>Development teams often find themselves needing to share common code across different projects. This is a common challenge in software development.</p><p>Reusing code not only saves time but also ensures consistency and improves software reliability. </p><p>Consider the scenario where a development team has a set of utility functions that are used across multiple projects. These functions could be anything from data processing utilities to logging and error handling functions.</p><p>The team needs a way to share these functions without duplicating the code in every project.</p><h2 id="the-different-approaches">The Different Approaches</h2><p>There are several ways to share common code among projects, each with its own set of pros and cons:</p><ul><li><strong>Copy-pasting the common code in every project</strong>: This is the simplest approach but can lead to code duplication and inconsistencies. It&apos;s also difficult to maintain and update the code across all projects.</li><li><strong>Using <code>pip install</code> directly from GitHub repositories</strong>: This method is straightforward but can be slow, especially for large projects. It also requires SSH credentials for private repositories, adding an extra layer of complexity.</li><li><strong>Using git submodules</strong>: This approach allows you to include a repository as a submodule in another repository, making it easy to keep track of changes. However, it adds complexity when managing changes across repositories and can lead to issues with dependency management.</li></ul><h2 id="turning-common-code-into-installable-packages">Turning Common Code into Installable Packages</h2><p>A more scalable and maintainable solution is to turn the common code into installable packages. This approach allows teams to easily share, update, and manage common code across projects. </p><p>However, the initial step of setting up a central artifact repository can be time-consuming. This is an upfront cost that the team needs to bear.</p><h2 id="aws-codeartifact">AWS CodeArtifact</h2><p>If you&apos;re using AWS, AWS CodeArtifact provides a solution to host a PyPI repository. AWS CodeArtifact is a fully managed artifact repository service that makes it easy to store, publish, and share software packages used in your software development process. </p><p>It handles TLS, authentication, and authorization, and uses IAM to control access. The service is serverless, meaning you only pay for what you use.</p><h2 id="creating-a-private-pypi-repository">Creating a Private PyPI repository</h2><p>To create an AWS CodeArtifact repository, you can use Terraform. </p><h3 id="create-a-domain">Create a domain</h3><pre><code class="language-hcl">locals {
  codeartifact_domain_name     = &quot;example&quot;
  codeartifact_repository_name = &quot;private-pypi&quot;

  codeartifact_readonly_access_arns = [
    module.iam_role.arn
    module.example_glue_job_orchestrator_role.arn
  ]

  codeartifact_readwrite_access_arns = [
    module.example_github_oidc_role.arn
  ]

  codeartifact_packages_arn = &quot;arn:aws:codeartifact:${local.region}:${data.aws_caller_identity.current.account_id}:package/${local.codeartifact_domain_name}/${local.codeartifact_repository_name}/*&quot;
}

resource &quot;aws_codeartifact_domain&quot; &quot;example&quot; {
  domain = &quot;example&quot;
}

data &quot;aws_iam_policy_document&quot; &quot;example_domain_policy_document&quot; {
  statement {
    effect    = &quot;Allow&quot;
    actions   = [&quot;codeartifact:GetAuthorizationToken&quot;]
    resources = [aws_codeartifact_domain.example.arn]

    principals {
      type = &quot;AWS&quot;
      
      identifiers = concat(
        local.codeartifact_readonly_access_arns,
        local.codeartifact_readwrite_access_arns,
      )
    }
  }
}

resource &quot;aws_codeartifact_domain_permissions_policy&quot; &quot;example_domain_permissions_policy&quot; {
  domain          = aws_codeartifact_domain.example.domain
  policy_document = data.aws_iam_policy_document.example_domain_policy_document.json
}
</code></pre>
<p>I have defined two access groups:</p><ol><li>those who can perform pip install from the private PyPI.</li><li>those who can do the above and publish new packages into the private PyPI.</li></ol><p>Both groups require an authorization token (HTTP Basic Auth) to connect to the service.</p><h3 id="create-a-repository-within-the-domain">Create a repository within the domain</h3><p>Then we create the repository and grant the necessary permissions to the two groups at the repository and package levels.</p><pre><code class="language-hcl">resource &quot;aws_codeartifact_repository&quot; &quot;private_pypi&quot; {
  repository  = &quot;private-pypi&quot;
  description = &quot;Private PyPI repository&quot;
  domain      = aws_codeartifact_domain.example.domain
}

data &quot;aws_iam_policy_document&quot; &quot;private_pypi_policy_document&quot; {
  statement {
    effect    = &quot;Allow&quot;
    actions   = [&quot;codeartifact:ReadFromRepository&quot;]
    resources = [aws_codeartifact_repository.private_pypi.arn]

    principals {
      type = &quot;AWS&quot;
      
      identifiers = concat(
        local.codeartifact_readonly_access_arns,
        local.codeartifact_readwrite_access_arns,
      )
    }
  }

  statement {
    effect = &quot;Allow&quot;

    actions = [
      &quot;codeartifact:GetPackageVersionAsset&quot;,
      &quot;codeartifact:GetPackageVersionReadme&quot;,
    ]

    resources = [local.codeartifact_packages_arn]

    principals {
      type = &quot;AWS&quot;
      
      identifiers = concat(
        local.codeartifact_readonly_access_arns,
        local.codeartifact_readwrite_access_arns,
      )
    }
  }

  statement {
    effect    = &quot;Allow&quot;
    actions   = [&quot;codeartifact:PublishPackageVersion&quot;]
    resources = [local.codeartifact_packages_arn]

    principals {
      type        = &quot;AWS&quot;
      identifiers = local.codeartifact_readwrite_access_arns
    }
  }
}

resource &quot;aws_codeartifact_repository_permissions_policy&quot; &quot;private_pypi_permissions_policy&quot; {
  repository      = aws_codeartifact_repository.private_pypi.repository
  domain          = aws_codeartifact_domain.example.domain
  policy_document = data.aws_iam_policy_document.private_pypi_policy_document.json
}
</code></pre>
<h3 id="output-the-repository-endpoint-url">Output the repository endpoint URL</h3><pre><code class="language-hcl">data &quot;aws_codeartifact_repository_endpoint&quot; &quot;private_pypi_endpoint&quot; {
  domain     = aws_codeartifact_domain.example.domain
  repository = aws_codeartifact_repository.private_pypi.repository
  format     = &quot;pypi&quot;
}

output &quot;private_pypi_endpoint_url&quot; {
  description = &quot;Private PyPI repository endpoint URL&quot;
  value       = data.aws_codeartifact_repository_endpoint.private_pypi_endpoint.repository_endpoint
}
</code></pre>
<p>Create the resources with</p><pre><code class="language-bash">$ terraform plan
$ terraform apply
</code></pre>
<p>and take note of the endpoint URL in the outputs. It looks like this:</p>
<p><code>https://example-111122223333.d.codeartifact.us-west-2.amazonaws.com/private-pypi</code></p>
<p>To be continued in <a href="https://blog.kakarukeys.quest/private-pypi-repository-aws-p2/">part 2</a>, publishing packages to the private PyPI repository.</p>
]]></content:encoded></item><item><title><![CDATA[Learn Kubernetes with k3s - part4 - Services]]></title><description><![CDATA[<p>This post is the continuation of <a href="https://blog.kakarukeys.quest/learn-kubernetes-with-k3s-part3-kubectl/" rel="noreferrer">part 3</a>.</p><p>Previously I have explained what Kubernetes services are. I have used <code>kubectl</code> to reveal the services running on my cluster (in <a href="https://blog.kakarukeys.quest/learn-kubernetes-with-k3s-part2-networking/" rel="noreferrer">part2</a>).</p><pre><code class="language-bash">$ kubectl get services -A
NAMESPACE     NAME             TYPE           CLUSTER-IP    EXTERNAL-IP                 PORT(S)                      AGE
default       kubernetes       ClusterIP      10.43.0.1     &lt;</code></pre>]]></description><link>https://blog.kakarukeys.quest/learn-kubernetes-k3s-part4-services/</link><guid isPermaLink="false">65f8490d69b40300070d4f33</guid><category><![CDATA[k8s]]></category><dc:creator><![CDATA[Jeff Wong]]></dc:creator><pubDate>Tue, 19 Mar 2024 15:29:55 GMT</pubDate><content:encoded><![CDATA[<p>This post is the continuation of <a href="https://blog.kakarukeys.quest/learn-kubernetes-with-k3s-part3-kubectl/" rel="noreferrer">part 3</a>.</p><p>Previously I have explained what Kubernetes services are. I have used <code>kubectl</code> to reveal the services running on my cluster (in <a href="https://blog.kakarukeys.quest/learn-kubernetes-with-k3s-part2-networking/" rel="noreferrer">part2</a>).</p><pre><code class="language-bash">$ kubectl get services -A
NAMESPACE     NAME             TYPE           CLUSTER-IP    EXTERNAL-IP                 PORT(S)                      AGE
default       kubernetes       ClusterIP      10.43.0.1     &lt;none&gt;                      443/TCP                      7d1h
kube-system   kube-dns         ClusterIP      10.43.0.10    &lt;none&gt;                      53/UDP,53/TCP,9153/TCP       7d1h
kube-system   metrics-server   ClusterIP      10.43.22.2    &lt;none&gt;                      443/TCP                      7d1h
kube-system   traefik          LoadBalancer   10.43.158.9   192.168.1.53,192.168.1.54   80:30824/TCP,443:30872/TCP   7d1h
</code></pre>
<p>Let&apos;s explore the different types of Kubernetes services.</p><h2 id="clusterip">ClusterIP</h2>
<p>Try connecting to the metrics-server ClusterIP service.</p><p>on tab 1, run:</p>
<pre><code class="language-bash">$ kubectl port-forward svc/metrics-server 8443:443 -n kube-system
Forwarding from 127.0.0.1:8443 -&gt; 10250
Forwarding from [::1]:8443 -&gt; 10250
</code></pre>
<p>on tab 2:</p>
<pre><code class="language-bash">$ curl -k https://localhost:8443/
{
  &quot;kind&quot;: &quot;Status&quot;,
  &quot;apiVersion&quot;: &quot;v1&quot;,
  &quot;metadata&quot;: {},
  &quot;status&quot;: &quot;Failure&quot;,
  &quot;message&quot;: &quot;forbidden: User \&quot;system:anonymous\&quot; cannot get path \&quot;/\&quot;&quot;,
  &quot;reason&quot;: &quot;Forbidden&quot;,
  &quot;details&quot;: {},
  &quot;code&quot;: 403
}
</code></pre>
<p>403 Error! Nonetheless, the connection works. </p><blockquote><strong>ClusterIP</strong>: exposes the service on a cluster-internal IP. This type makes the service only reachable from within the cluster. <em>This is the default service type.</em></blockquote><p>On the cluster, you can connect to metrics-server directly by its hostname.</p><pre><code class="language-bash">$ kubectl run curl --image=curlimages/curl --restart=Never --rm -it -- sh
If you don&apos;t see a command prompt, try pressing enter.
~ $ curl -k https://metrics-server.kube-system
{
  &quot;kind&quot;: &quot;Status&quot;,
  &quot;apiVersion&quot;: &quot;v1&quot;,
  &quot;metadata&quot;: {},
  &quot;status&quot;: &quot;Failure&quot;,
  &quot;message&quot;: &quot;forbidden: User \&quot;system:anonymous\&quot; cannot get path \&quot;/\&quot;&quot;,
  &quot;reason&quot;: &quot;Forbidden&quot;,
  &quot;details&quot;: {},
  &quot;code&quot;: 403
}~ $ exit
pod &quot;curl&quot; deleted
</code></pre>
<h2 id="loadbalancer">LoadBalancer</h2>
<p>We have tried connecting to the traefik LoadBalancer service in <a href="https://blog.kakarukeys.quest/learn-kubernetes-with-k3s-part2-networking/" rel="noreferrer">part2</a>.</p><pre><code class="language-bash">$ curl -k https://192.168.1.53
404 page not found

$ curl -k https://192.168.1.54
404 page not found
</code></pre>
<blockquote><strong>LoadBalancer</strong>: exposes the service externally using a load balancer. Traffic is routed to the pods of a cluster-internal service. The load balancer tracks the endpoints associated with the service and balances traffic accordingly.</blockquote><p>k3s uses a loadbalancer solution called <a href="https://docs.k3s.io/networking?ref=blog.kakarukeys.quest#service-load-balancer" rel="noreferrer">ServiceLB</a>. It exposes the underlying service at selected nodes on a defined port range.</p><p>It appears we would need DNS to round-robin the external traffic to these nodes to complete the load-balancing act, which is a hassle. On a managed kubernetes cluster such as EKS, GKE, the loadbalancer will be provided by the cloud provider at a static IP.</p><h2 id="nodeport">NodePort</h2>
<p>We don&apos;t have a NodePort service for now. <a href="https://helm.sh/docs/intro/install/?ref=blog.kakarukeys.quest#through-package-managers" rel="noreferrer">Install Helm</a>, we will use it to install nginx server as a NodePort service.</p><blockquote><strong>Helm</strong> is a package manager for Kubernetes that makes deploying and managing Kubernetes applications easy. It automates the creation, packaging, configuration, and deployment of Kubernetes applications.</blockquote><blockquote>A <strong>Helm chart</strong> is a package of pre-configured Kubernetes resources. They comprise of templates for the Kubernetes manifest files that are usually involved in deploying complicated distributed applications or services.</blockquote><p>Create a file <code>values.yaml</code> with the following values:</p><pre><code class="language-yaml">service:
  type: NodePort
  nodePorts:
    http: 30080
</code></pre>
<p>You can check the parameters on the <a href="https://artifacthub.io/packages/helm/bitnami/nginx?ref=blog.kakarukeys.quest#traffic-exposure-parameters" rel="noreferrer">nginx helm chart page</a>. Run the helm command as given on the page with an additional <code>-f values.yaml</code> flag to use the values we just created.</p><pre><code class="language-bash">$ helm install my-release \
      oci://registry-1.docker.io/bitnamicharts/nginx \
      -f values.yaml
$ kubectl get services
NAME               TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
kubernetes         ClusterIP      10.43.0.1       &lt;none&gt;        443/TCP        8d
my-release-nginx   NodePort       10.43.206.241   &lt;none&gt;        80:30080/TCP   9m30s
</code></pre>
<p>After that test the service with</p><pre><code class="language-bash">$ curl http://192.168.1.53:30080
&lt;!DOCTYPE html&gt;
&lt;html&gt;
......
$ curl http://192.168.1.54:30080
&lt;!DOCTYPE html&gt;
&lt;html&gt;
......
</code></pre>
<blockquote><strong>NodePort</strong>: exposes the service on <strong>each Node&apos;s IP</strong> at a static port (the NodePort). A ClusterIP service, to which the NodePort service will route, is automatically created. This makes the service reachable from outside the cluster.</blockquote><p>NodePort does not perform any load balancing. It simply directs the traffic from whichever node the client connected to. This can cause some nodes to get overwhelmed with requests while other nodes remain idle. There is no mechanism in NodePort services to distribute load evenly across multiple nodes.</p><h2 id="externalname">ExternalName</h2>
<p>Create a file <code>service.yaml</code> with the following values:</p><pre><code class="language-yaml">apiVersion: v1
kind: Service 
metadata:
  name: my-google
  namespace: default
spec:
  type: ExternalName
  externalName: google.com
</code></pre>
<p>Run</p><pre><code class="language-bash">$ kubectl apply -f service.yaml
$ kubectl get services
NAME               TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
kubernetes         ClusterIP      10.43.0.1       &lt;none&gt;        443/TCP        8d
my-release-nginx   NodePort       10.43.206.241   &lt;none&gt;        80:30080/TCP   29m
my-google          ExternalName   &lt;none&gt;          google.com    &lt;none&gt;         77s
</code></pre>
<p>To test the service</p><pre><code class="language-bash">$ kubectl run curl --image=curlimages/curl \
      --restart=Never --rm -it -- \
      curl -k https://my-google
&lt;!DOCTYPE html&gt;
&lt;html lang=en&gt;
......
</code></pre>
<blockquote><strong>ExternalName</strong>: maps the service to the contents of the externalName field (e.g. foo.bar.example.com), by returning a CNAME record with its value. No proxying or load-balancing is set up. This allows external services to be referenced.</blockquote><p>To be continued in <a href="https://blog.kakarukeys.quest/learn-kubernetes-k3s-p5-helm/" rel="noreferrer">part 5</a>, more about Helm.</p>]]></content:encoded></item><item><title><![CDATA[Learn Kubernetes with k3s - part3 - kubectl]]></title><description><![CDATA[<p>This post is the continuation of <a href="https://blog.kakarukeys.quest/learn-kubernetes-with-k3s-part2-networking" rel="noreferrer">part 2</a>.</p><p><strong>kubectl</strong> is a command line tool that allows users to run commands against Kubernetes clusters. Specifically, kubectl enables communication between users and the control plane via Kubernetes API. So the mastery of kubectl is essential for handling real-world cluster operations and troubleshooting.</p>]]></description><link>https://blog.kakarukeys.quest/learn-kubernetes-with-k3s-part3-kubectl/</link><guid isPermaLink="false">65f536bc69b40300070d4e61</guid><category><![CDATA[k8s]]></category><dc:creator><![CDATA[Jeff Wong]]></dc:creator><pubDate>Sat, 16 Mar 2024 14:19:21 GMT</pubDate><content:encoded><![CDATA[<p>This post is the continuation of <a href="https://blog.kakarukeys.quest/learn-kubernetes-with-k3s-part2-networking" rel="noreferrer">part 2</a>.</p><p><strong>kubectl</strong> is a command line tool that allows users to run commands against Kubernetes clusters. Specifically, kubectl enables communication between users and the control plane via Kubernetes API. So the mastery of kubectl is essential for handling real-world cluster operations and troubleshooting. </p><p>Below is a table showing the common kubectl commands.</p>
<!--kg-card-begin: html-->
<table>
<thead>
  <tr>
    <th>Operation</th>
    <th>Subcommand</th>
    <th>Example</th>
  </tr>
</thead>
<tbody>
  <tr>
    <td>configuring access</td>
    <td>kubectl config</td>
    <td>set-cluster my-cluster</td>
  </tr>
  <tr>
    <td>listing and reading resources</td>
    <td>kubectl get<br>kubectl describe<br>kubectl logs</td>
    <td>pods -A<br>pod/abc<br>pod/abc</td>
  </tr>
  <tr>
    <td>creating resources</td>
    <td>kubectl create<br>kubectl apply</td>
    <td>namespace abc<br>-f my-manifest.yaml</td>
  </tr>
  <tr>
    <td>updating resources</td>
    <td>kubectl edit<br>kubectl replace<br>kubectl apply<br>kubectl scale</td>
    <td>svc/docker-registry<br>-f nginx-deployment.yaml<br>-f my-manifest.yaml<br>--replicas=3 rs/foo</td>
  </tr>
  <tr>
    <td>deleting resources</td>
    <td>kubectl delete</td>
    <td>pod unwanted --now</td>
  </tr>
  <tr>
    <td>running pods</td>
    <td>kubectl run<br>kubectl attach<br>kubectl exec</td>
    <td>nginx --image=nginx<br>my-pod -it<br>my-pod -- ls /</td>
  </tr>
  <tr>
    <td>connecting to resources</td>
    <td>kubectl port-forward<br>kubectl cp</td>
    <td>svc/my-service 5000<br>my-pod:/foo bar</td>
  </tr>
  <tr>
    <td>check resource usage<br>(requires metrics server)</td>
    <td>kubectl top</td>
    <td>nodes</td>
  </tr>
</tbody>
</table>

<!--kg-card-end: html-->
<h2 id="explanation">Explanation</h2>
<p>No doubt there is a lot to digest. I will explain some of the concepts involved here, and leave the rest for you to research about.</p><h3 id="namespace">Namespace</h3>
<p>A Kubernetes <strong>namespace</strong> provides a scope for names of resources within a Kubernetes cluster. It allows the isolation of groups of resources and management of cluster components like Pods, Services, and Deployments.</p><p>Namespaces are intended for use in environments with many users spread across multiple teams or projects. They prevent name collisions that can occur when different teams are using resources with the same name. Resources within one namespace are not visible to other namespaces unless steps are explicitly taken to allow that.</p><p>To get resources from all namespaces</p><pre><code class="language-bash">$ kubectl get pods -A
</code></pre>
<h3 id="pod">Pod</h3>
<p>A Kubernetes <strong>pod</strong> is the basic building block of applications in Kubernetes. It is a group of one or more containers (such as Docker containers), with shared storage/network resources, and a specification for how to run the containers.</p><p>Pods allow containers to run together on the same machine and share resources. This means containers within a pod can communicate easily using localhost.</p><h3 id="service">Service</h3>
<p>A Kubernetes <strong>service</strong> is an abstraction layer that defines a logical set of pods and enables external traffic exposure and load balancing to those pods, regardless of where on the cluster they are scheduled.</p><p>Services define rules about how groups of pods can be accessed (e.g. randomly, via round-robin load balancing). This provides a single IP address or DNS name endpoint for reaching multiple pods.</p><p>There are different types of services that support different methods of service discovery and load balancing: ClusterIP, NodePort, LoadBalancer.</p><h3 id="deployment">Deployment</h3>
<p>A Kubernetes <strong>deployment</strong> is an object that manages pods and replica sets in Kubernetes. It provides declarative updates for pods and replica sets.</p><p>Deployments are responsible for creating and updating pods based on the provided configuration. It helps in rolling out new updates or roll back to previous deployments.</p><p>It provides self-healing capabilities by automatically replacing pods that fail or are terminated. This ensures the application is always available.</p><h2 id="aliases">Aliases</h2>
<p>You can set aliases in <code>~/.zshrc</code> or <code>~/.bashrc</code> to make typing of kubectl commands a little bit faster. This is convenient for your work and passing CKA certification exam.</p>
<p>For examples</p>
<pre><code class="language-bash">alias kc=&apos;kubectl&apos;
alias kns=&apos;kubectl config set-context --current --namespace&apos;
alias kbb=&apos;kubectl run busybox --image=busybox --restart=Never -it --&apos;
</code></pre>
<p>So that we can do</p>
<pre><code class="language-bash">$ kc get pods      # get all pods
$ kns my-namespace # set default namespace
$ kbb ls -la       # run ad hoc linux commands
</code></pre>
<h2 id="infrastructure-as-code">Infrastructure as Code</h2>
<p>Do we need to memorize all kubectl commands out there? Not necessarily. In fact, you are almost never going to use the resource creation/update/delete commands.</p><p>The best practice is to manage and provision resources through code, not imperative commands. This is known as <strong>infrastructure as code</strong> (IaC). </p><p>IaC allows infrastructure to be treated as software which can be developed, tested, and reviewed like any other code. Creating Kubernetes resources with <code>kubectl create</code> or <code>kubectl apply</code> goes against the spirit of IaC. </p><p>So you can think of certain kubectl commands as relics of the past, although CKA certification exams test for them. Still it&apos;s best to gain some exposure, who knows you might need them for troubleshooting one day.</p><p>To be continued in <a href="https://blog.kakarukeys.quest/learn-kubernetes-k3s-part4-services/" rel="noreferrer">part 4</a>, about Kubernetes services.</p>]]></content:encoded></item><item><title><![CDATA[Learn Kubernetes with k3s - part2 - Networking]]></title><description><![CDATA[<p>This post is the continuation of <a href="https://blog.kakarukeys.quest/learn-kubernetes-k3s-p1-install/" rel="noreferrer">part 1</a>.</p><p>Let us look closely at the commands that we have used to install k3s.</p><pre><code class="language-python">master1.sudo( &quot;curl -sfL https://get.k3s.io | sh -s - server &quot;
              &quot;--write-kubeconfig-mode 644 &quot;
             f&quot;--node-external-ip={server_ip} &quot;
              &quot;--flannel-external-ip &quot;
              &quot;</code></pre>]]></description><link>https://blog.kakarukeys.quest/learn-kubernetes-with-k3s-part2-networking/</link><guid isPermaLink="false">65edb7ed69b40300070d4d6f</guid><category><![CDATA[k8s]]></category><category><![CDATA[Linux administration]]></category><dc:creator><![CDATA[Jeff Wong]]></dc:creator><pubDate>Wed, 13 Mar 2024 12:39:15 GMT</pubDate><content:encoded><![CDATA[<p>This post is the continuation of <a href="https://blog.kakarukeys.quest/learn-kubernetes-k3s-p1-install/" rel="noreferrer">part 1</a>.</p><p>Let us look closely at the commands that we have used to install k3s.</p><pre><code class="language-python">master1.sudo( &quot;curl -sfL https://get.k3s.io | sh -s - server &quot;
              &quot;--write-kubeconfig-mode 644 &quot;
             f&quot;--node-external-ip={server_ip} &quot;
              &quot;--flannel-external-ip &quot;
              &quot;--flannel-backend=wireguard-native &quot;
             f&quot;--tls-san {server_external_hostname}&quot;)
</code></pre>
<p>The first flag, <code>--write-kubeconfig-mode 644</code> tells the process to produce a <code>kubeconfig</code> file in <code>644</code> mode, so that we can read it, copy it to our workstation to make the first connection to the cluster.</p>
<blockquote>
<p>A <code>kubeconfig</code> file is used to configure access to Kubernetes clusters. It contains information such as authentication details, clusters, users and namespaces.</p>
</blockquote>
<p>Next,</p>
<pre><code class="language-python">f&quot;--node-external-ip={server_ip} &quot;
 &quot;--flannel-external-ip &quot;
 &quot;--flannel-backend=wireguard-native &quot;
</code></pre>
<p>These flags set the node external IP to the server&apos;s static ip (we&apos;ve set that to <code>192.168.1.53</code>) and configure the networking plugin (flannel) to use that for routing.</p>
<p>There is a corresponding flag in the worker node command:</p>
<pre><code class="language-python">f&quot;--node-external-ip={cxn.host}&quot;
</code></pre>
<p>To test whether the cluster networking is working fine. Run the following commands:</p>
<pre><code class="language-bash">$ curl -k https://192.168.1.53
404 page not found

$ curl -k https://192.168.1.54
404 page not found
</code></pre>
<p>You should see a 404 response for each. </p><p>This is because there is a traefik reverse-proxy service running at port 80 and 443 on every node, installed during cluster installation. Yet we have not defined any routing rules, hence the 404 errors. </p><p>Run this command to see all services running on the cluster:</p><pre><code class="language-bash">$ kubectl get service -A
NAMESPACE     NAME             TYPE           CLUSTER-IP    EXTERNAL-IP                              PORT(S)                      AGE
default       kubernetes       ClusterIP      10.43.0.1     &lt;none&gt;                                   443/TCP                      71m
kube-system   kube-dns         ClusterIP      10.43.0.10    &lt;none&gt;                                   53/UDP,53/TCP,9153/TCP       71m
kube-system   metrics-server   ClusterIP      10.43.22.2    &lt;none&gt;                                   443/TCP                      71m
kube-system   traefik          LoadBalancer   10.43.158.9   192.168.1.53,192.168.1.54                80:30824/TCP,443:30872/TCP   71m
</code></pre>
<p>Traefik is in the last row of the table.</p><p>If you did not get a reply for any curl command, it means the networking has failed. This was the case when I used the default installation command from the <a href="https://docs.k3s.io/quick-start?ref=blog.kakarukeys.quest" rel="noreferrer">official quick-start guide</a>.</p><p>I had to reinstall k3s with the above-mentioned flags to make networking works.</p><p>Note the special flag<code>--flannel-backend=wireguard-native</code></p><blockquote>Use WireGuard to encapsulate and encrypt network traffic. May require additional kernel modules.</blockquote><p>Sometimes the default installation command is not able to figure out which networking interface should be used for traffic routing. We remind it using the explicit flags.</p><p>For your curiosity, here are <a href="https://docs.k3s.io/installation/network-options?ref=blog.kakarukeys.quest#flannel-options" rel="noreferrer">all the different options for flannel</a>.</p><p>To fiddle with these, we need a way to uninstall k3s from all nodes and reinstall k3s. Add the following fabric tasks:</p><pre><code class="language-python">@task
def uninstall_agents(c):
    ThreadingGroup(*all_workers).sudo(&quot;/usr/local/bin/k3s-agent-uninstall.sh&quot;)


@task
def uninstall_server(c):
    Connection(&quot;k3s-master1&quot;).sudo(&quot;/usr/local/bin/k3s-uninstall.sh&quot;)
</code></pre>
<pre><code class="language-bash">$ fab uninstall-agents
$ fab uninstall-server
</code></pre>
<p>You can then tap on these to test which networking options work for your setup.</p><p>Read this in-depth article &quot;<a href="https://medium.com/itnext/deciphering-the-kubernetes-networking-maze-navigating-load-balance-bgp-ipvs-and-beyond-7123ef428572?ref=blog.kakarukeys.quest" rel="noreferrer">Deciphering the Kubernetes Networking Maze: Navigating Load-Balance, BGP, IPVS and Beyond</a>&quot; to understand more about Kubernetes networking.</p><p>The next flag, <code>--tls-san {server_external_hostname}</code> tells the process to generate a TLS certificate with <code>server_external_hostname</code> (<code>example.com</code>) as SAN, so that we can access the cluster through the external hostname for convenience.</p><p>Next,</p>
<pre><code class="language-python">master1.get(&quot;/etc/rancher/k3s/k3s.yaml&quot;, local=&quot;kubeconfig&quot;)

subprocess.run([
    &quot;sed&quot;, &quot;-i&quot;, f&quot;s/127.0.0.1/{server_external_hostname}/g&quot;, &quot;kubeconfig&quot;
])
subprocess.run([&quot;chmod&quot;, &quot;600&quot;, &quot;kubeconfig&quot;])
</code></pre>
<p>These lines copy <code>kubeconfig</code> to the workstation with the external hostname and file mode.</p>
<p>And finally the installation command at the worker node,</p><pre><code class="language-python">node_token = master1.sudo(
    &quot;cat /var/lib/rancher/k3s/server/node-token&quot;, 
    hide=True
).stdout.strip()
for cxn in ThreadingGroup(*all_workers):        
    cxn.sudo( &quot;curl -sfL https://get.k3s.io | &quot;
             f&quot;K3S_URL=https://{server_ip}:6443 &quot;
             f&quot;K3S_TOKEN={node_token} &quot;
......
</code></pre>
<p>We recover the node token from the server node, and use it to configure the worker node, as instructed in <a href="https://docs.k3s.io/quick-start?ref=blog.kakarukeys.quest">the official quick-start guide</a>.</p>
<p>To be continued in <a href="https://blog.kakarukeys.quest/learn-kubernetes-with-k3s-part3-kubectl/">part 3</a>, about <code>kubectl</code>.</p>
]]></content:encoded></item><item><title><![CDATA[Learn Kubernetes with k3s - part1 - Installation]]></title><description><![CDATA[<p>In 2024, as far as learning Kubernetes is concerned we have a myriad of tools at our disposal:</p><ol><li>mini distributions such as <a href="https://minikube.sigs.k8s.io/docs/?ref=blog.kakarukeys.quest" rel="noreferrer">minikube</a>, <a href="https://kind.sigs.k8s.io/?ref=blog.kakarukeys.quest" rel="noreferrer">kind</a>, <a href="https://k0sproject.io/?ref=blog.kakarukeys.quest" rel="noreferrer">k0s</a>, <a href="https://k3s.io/?ref=blog.kakarukeys.quest" rel="noreferrer">k3s</a>, etc.</li><li>Q&amp;A AI: <a href="https://chat.openai.com/?ref=blog.kakarukeys.quest" rel="noreferrer">ChatGPT</a>, <a href="https://gemini.google.com/app?ref=blog.kakarukeys.quest" rel="noreferrer">Gemini</a>, <a href="https://chat.mistral.ai/?ref=blog.kakarukeys.quest" rel="noreferrer">Mistral</a>, etc.</li></ol><p>It makes me wonder if one can learn Kubernetes administration and application development in <strong>under</strong></p>]]></description><link>https://blog.kakarukeys.quest/learn-kubernetes-k3s-p1-install/</link><guid isPermaLink="false">65e71ec869b40300070d4c11</guid><category><![CDATA[k8s]]></category><category><![CDATA[Linux administration]]></category><dc:creator><![CDATA[Jeff Wong]]></dc:creator><pubDate>Fri, 08 Mar 2024 13:56:55 GMT</pubDate><content:encoded><![CDATA[<p>In 2024, as far as learning Kubernetes is concerned we have a myriad of tools at our disposal:</p><ol><li>mini distributions such as <a href="https://minikube.sigs.k8s.io/docs/?ref=blog.kakarukeys.quest" rel="noreferrer">minikube</a>, <a href="https://kind.sigs.k8s.io/?ref=blog.kakarukeys.quest" rel="noreferrer">kind</a>, <a href="https://k0sproject.io/?ref=blog.kakarukeys.quest" rel="noreferrer">k0s</a>, <a href="https://k3s.io/?ref=blog.kakarukeys.quest" rel="noreferrer">k3s</a>, etc.</li><li>Q&amp;A AI: <a href="https://chat.openai.com/?ref=blog.kakarukeys.quest" rel="noreferrer">ChatGPT</a>, <a href="https://gemini.google.com/app?ref=blog.kakarukeys.quest" rel="noreferrer">Gemini</a>, <a href="https://chat.mistral.ai/?ref=blog.kakarukeys.quest" rel="noreferrer">Mistral</a>, etc.</li></ol><p>It makes me wonder if one can learn Kubernetes administration and application development in <strong>under 1 week. </strong>I did it in 3 months many years ago.</p><p>There is a systematic approach I have adopted successfully for many learning tasks: </p><blockquote>Hack a mini project and ask AI models the right questions.</blockquote><p>I believe when doing it right, it can help you quickly grasp Kubernetes&apos; core concepts, setting you on a path to becoming a proficient user.</p><p>Let&apos;s get started with hacking with k3s.</p><h2 id="k3s">K3s</h2>
<p>K3s is a lightweight Kubernetes distribution that aims to simplify deploying and managing Kubernetes clusters. It is developed by <strong>Rancher Labs</strong> and is certified by the Cloud Native Computing Foundation (CNCF) as being fully compatible with Kubernetes.</p><p>K3s packages Kubernetes into a single binary file that is only around 45MB in size, much smaller than a full Kubernetes installation. It is designed for running Kubernetes on low-resource environments like edge/IoT devices.</p><p>Even though it is lightweight, k3s maintains full Kubernetes functionality and is interoperable with tools that work with regular Kubernetes, making it an excellent choice for hands-on learning and experimentation.</p><h2 id="getting-the-servers">Getting the Servers</h2>
<p>There are many options, I leave it to you to decide.</p><ol><li>Get instances from a cloud provider</li><li><a href="https://medium.com/thinkport/how-to-build-a-raspberry-pi-kubernetes-cluster-with-k3s-76224788576c?ref=blog.kakarukeys.quest" rel="noreferrer">Set up Rasberry PIs at home</a></li><li>Install guest OS&apos; with <a href="https://www.vagrantup.com/?ref=blog.kakarukeys.quest" rel="noreferrer">Vagrant / Virtualbox</a></li><li>Or (if you happen to use a Linux desktop) use your OS directly</li></ol><p>I chose 3 and installed two guests on two home PCs separately, one as the server, another as the agent (worker node). Both host machines are on the same network.</p><p>The <code>Vagrantfile</code> I used looks like this:</p><pre><code class="language-ruby">Vagrant.configure(&quot;2&quot;) do |config|
  config.vm.hostname = &quot;k3s-master1&quot;
  config.vm.box = &quot;generic/ubuntu2204&quot;
  config.vm.network &quot;public_network&quot;, ip: &quot;192.168.1.53&quot;, bridge: &quot;en0: Ethernet&quot;

  config.vm.provider &quot;virtualbox&quot; do |v|
    v.memory = 8000
    v.cpus = 2
  end
end
</code></pre>
<p>I omit the <code>Vagrantfile</code> file for the worker node as it is only slightly different. Note that I have configured static IPs for both nodes.</p><p>To spin the guest nodes up, run this command on both hosts</p><pre><code class="language-bash">$ vagrant up
</code></pre>
<h2 id="install-k3s">Install K3s</h2>
<p>At this point, I have two vagrant nodes up. To install k3s, I use an automation tool <a href="https://www.fabfile.org/?ref=blog.kakarukeys.quest" rel="noreferrer">Fabric</a> to write the <a href="https://docs.k3s.io/quick-start?ref=blog.kakarukeys.quest" rel="noreferrer">necessary installation tasks</a> in a <code>fabfile.py</code>. </p><p>First some settings:</p><p>SSH config:</p>
<pre><code class="language-bash">$ cat ~/.ssh/config 
Host k3s-master1
	User vagrant
	HostName 192.168.1.53

Host k3s-worker1
	User vagrant
	HostName 192.168.1.54
</code></pre>
<p><code>fabfile.py</code> opening:</p>
<pre><code class="language-python">import subprocess

from fabric import ThreadingGroup, Connection, task
from fabric.exceptions import GroupException

ips = {
    &quot;k3s-master1&quot;: &quot;192.168.1.53&quot;,
    &quot;k3s-worker1&quot;: &quot;192.168.1.54&quot;,
}
server_ip = ips[&quot;k3s-master1&quot;]
server_external_hostname = &quot;example.com&quot;
all_workers = [host for host in ips if &quot;worker&quot; in host]
</code></pre>
<p><code>example.com</code> is a domain I registered for the k3s API server so that I can use it to access my cluster over the internet. I have set a DNS A record pointing it to the public IP address of my router.</p><p>Then the task to upgrade the OS on both nodes (optional)</p><pre><code class="language-python">@task
def upgrade_os(c):
    all_hosts = ThreadingGroup(*ips.keys())
    all_hosts.sudo(&quot;apt update -y&quot;)
    all_hosts.sudo(&quot;apt upgrade -y&quot;)

    try:
        all_hosts.sudo(&quot;reboot&quot;)
    except GroupException:
        pass
</code></pre>
<p>Run this command to execute the task</p><pre><code class="language-bash">$ fab upgrade-os
</code></pre>
<p>Then the tasks to install k3s, first on the server node, then on the worker node</p><pre><code class="language-python">@task
def install_server(c):
    master1 = Connection(&quot;k3s-master1&quot;)

    master1.sudo( &quot;curl -sfL https://get.k3s.io | sh -s - server &quot;
                  &quot;--write-kubeconfig-mode 644 &quot;
                 f&quot;--node-external-ip={server_ip} &quot;
                  &quot;--flannel-external-ip &quot;
                  &quot;--flannel-backend=wireguard-native &quot;
                 f&quot;--tls-san {server_external_hostname}&quot;)

    master1.get(&quot;/etc/rancher/k3s/k3s.yaml&quot;, local=&quot;kubeconfig&quot;)

    subprocess.run([
        &quot;sed&quot;, &quot;-i&quot;, f&quot;s/127.0.0.1/{server_external_hostname}/g&quot;, &quot;kubeconfig&quot;
    ])
    subprocess.run([&quot;chmod&quot;, &quot;600&quot;, &quot;kubeconfig&quot;])


@task
def install_agents(c):
    master1 = Connection(&quot;k3s-master1&quot;)

    node_token = master1.sudo(
        &quot;cat /var/lib/rancher/k3s/server/node-token&quot;, 
        hide=True
    ).stdout.strip()

    for cxn in ThreadingGroup(*all_workers):        
        cxn.sudo( &quot;curl -sfL https://get.k3s.io | &quot;
                 f&quot;K3S_URL=https://{server_ip}:6443 &quot;
                 f&quot;K3S_TOKEN={node_token} &quot;
                  &quot;sh -s - agent &quot;
                 f&quot;--node-external-ip={cxn.host}&quot;)
</code></pre>
<p>Run these commands to execute the tasks</p><pre><code class="language-bash">$ fab install-server
$ fab install-agents
</code></pre>
<h2 id="test-the-k3s-installation">Test the k3s Installation</h2>
<pre><code class="language-bash">$ mv kubeconfig ~/.kube/config
</code></pre>
<p>Note: the <code>mv</code> command will overwrite your existing <code>config</code> file.</p>
<p>Install your <a href="https://kubernetes.io/docs/tasks/tools/?ref=blog.kakarukeys.quest">kubectl command</a>, enable port forwarding from your home router to the server node at port 6443, then run</p>
<pre><code class="language-bash">$ kubectl get nodes
NAME          STATUS   ROLES                  AGE   VERSION
k3s-master1   Ready    control-plane,master   67m   v1.28.7+k3s1
k3s-worker1   Ready    &lt;none&gt;                 63m   v1.28.7+k3s1
</code></pre>
<p>You should see both nodes in Ready state.</p>
<p>Check all pods are running fine</p><pre><code class="language-bash">$ kubectl get pods -A
NAMESPACE     NAME                                      READY   STATUS      RESTARTS       AGE
kube-system   helm-install-traefik-8frcc                0/1     Completed   2              12h
kube-system   helm-install-traefik-crd-gbnjw            0/1     Completed   0              12h
kube-system   svclb-traefik-5d5abbc4-95jnq              2/2     Running     2 (10h ago)    12h
kube-system   traefik-f4564c4f4-hxws8                   1/1     Running     1 (10h ago)    12h
kube-system   coredns-6799fbcd5-zvmtd                   1/1     Running     1 (10h ago)    12h
kube-system   local-path-provisioner-6c86858495-gbnjx   1/1     Running     2 (10h ago)    12h
kube-system   metrics-server-78bcc4784b-gv7kp           1/1     Running     2 (10h ago)    12h
kube-system   svclb-traefik-5d5abbc4-sfww6              2/2     Running     6 (4m8s ago)   11h
kube-system   svclb-traefik-5d5abbc4-7bqn9              2/2     Running     0              7h59m
</code></pre>
<p>Try creating a pod</p><pre><code class="language-bash">$ kubectl run busybox --image=busybox --restart=Never -it -- uname -r
5.15.0-100-generic
</code></pre>
<p>We now have a working k3s cluster!</p><p>To be continued in <a href="https://blog.kakarukeys.quest/learn-kubernetes-with-k3s-part2-networking/" rel="noreferrer">part 2</a>, networking.</p>]]></content:encoded></item><item><title><![CDATA[GitHub Actions Amazon ECR Integration - part2]]></title><description><![CDATA[<p>This post is the continuation of <a href="https://blog.kakarukeys.quest/github-actions-amazon-elastic-container-registry-p1/" rel="noreferrer">part 1</a>.</p><p>In this post, I will show how to use a GitHub Actions workflow to automatically build and publish docker images to an ECR repository on a push or pull request merge.</p>
<p>We need a dummy project for illustration. I have <a href="https://python-poetry.org/docs/basic-usage/?ref=blog.kakarukeys.quest">bootstrapped my</a></p>]]></description><link>https://blog.kakarukeys.quest/github-actions-amazon-elastic-container-registry-p2/</link><guid isPermaLink="false">65d0a99c69b40300070d4983</guid><category><![CDATA[AWS DevOps]]></category><category><![CDATA[Github Actions]]></category><dc:creator><![CDATA[Jeff Wong]]></dc:creator><pubDate>Sun, 18 Feb 2024 12:45:00 GMT</pubDate><content:encoded><![CDATA[<p>This post is the continuation of <a href="https://blog.kakarukeys.quest/github-actions-amazon-elastic-container-registry-p1/" rel="noreferrer">part 1</a>.</p><p>In this post, I will show how to use a GitHub Actions workflow to automatically build and publish docker images to an ECR repository on a push or pull request merge.</p>
<p>We need a dummy project for illustration. I have <a href="https://python-poetry.org/docs/basic-usage/?ref=blog.kakarukeys.quest">bootstrapped my project using poetry</a>, my favourite python build tool.</p>
<p>The project structure looks like</p>
<pre><code class="language-bash">$ tree -I &quot;.git|.terrafom|terraform|tests|.pytest_cache&quot; -a
.
&#x251C;&#x2500;&#x2500; Dockerfile
&#x251C;&#x2500;&#x2500; .github
&#x2502;   &#x2514;&#x2500;&#x2500; workflows
&#x2502;       &#x2514;&#x2500;&#x2500; ci.yml
&#x251C;&#x2500;&#x2500; .gitignore
&#x251C;&#x2500;&#x2500; poetry.lock
&#x251C;&#x2500;&#x2500; pyproject.toml
&#x251C;&#x2500;&#x2500; .python-version
&#x2514;&#x2500;&#x2500; src
    &#x2514;&#x2500;&#x2500; handler.py
</code></pre>
<p>The project definition metadata, <code>pyproject.toml</code>:</p>
<pre><code class="language-toml">[tool.poetry]
name = &quot;learn-gha-ecr&quot;
version = &quot;0.1.0&quot;
description = &quot;&quot;

[tool.poetry.dependencies]
python = &quot;^3.11&quot;
boto3 = &quot;^1.34.11&quot;
jsonpickle = &quot;^3.0.2&quot;

[tool.poetry.group.dev.dependencies]
pytest = &quot;^8.0.0&quot;

[build-system]
requires = [&quot;poetry-core&quot;]
build-backend = &quot;poetry.core.masonry.api&quot;
</code></pre>
<p>The lambda handler, <code>handler.py</code>:</p>
<pre><code class="language-python">import os
import logging
import jsonpickle
import boto3

logger = logging.getLogger()
logger.setLevel(logging.INFO)

client = boto3.client(&apos;lambda&apos;)

def lambda_handler(event, context):
    logger.info(&apos;## ENVIRONMENT VARIABLES\r&apos; + jsonpickle.encode(dict(**os.environ)))
    logger.info(&apos;## EVENT\r&apos; + jsonpickle.encode(event))
    logger.info(&apos;## CONTEXT\r&apos; + jsonpickle.encode(context))

    response = client.get_account_settings()
    logger.info(response[&apos;AccountUsage&apos;])
</code></pre>
<p>At the moment the project isn&apos;t doing any real work, except logging some information when the lambda function executes. I proceed to write the GitHub Actions workflow file for the project, <code>.github/workflows/ci.yml</code></p><p>On any push to <code>main</code> branch, checking out the code,</p>
<pre><code class="language-yaml">name: Lambda Application CI workflow

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest

    permissions:
      id-token: write
      contents: read

    env:
      AWS_REGION: &lt;region&gt;
      AWS_ACCOUNT_ID: &lt;account-id&gt;
      BUILD_ROLE_NAME: GHA-Build-Role

    steps:
      - name: Checkout code
        uses: actions/checkout@v4
</code></pre>
<p>Take note of the environment variable <code>BUILD_ROLE_NAME</code>, this is the name of the IAM role I have created in <a href="https://blog.kakarukeys.quest/github-actions-amazon-elastic-container-registry-p1/">part1</a>.</p>
<p>Installing Python and Poetry build tool,</p>
<pre><code class="language-yaml">- name: Read Python version
  id: read-python-version
  run: printf &quot;version=$(cat .python-version)\n&quot; &gt;&gt; &quot;$GITHUB_OUTPUT&quot;

- name: Install Python
  id: install-python
  uses: actions/setup-python@v5
  with:
    python-version: &quot;${{ steps.read-python-version.outputs.version }}&quot;

- name: Install Poetry
  uses: snok/install-poetry@v1
  with:
    version: 1.7.1
    virtualenvs-in-project: true
</code></pre>
<p>Installing the project dependencies,</p>
<pre><code class="language-yaml">- name: Restore Python virtual environment from cache
  uses: actions/cache@v4
  with:
    path: .venv
    key: venv-${{ runner.os }}-${{ steps.install-python.outputs.python-version }}-${{ hashFiles(&apos;poetry.lock&apos;) }}

- name: Install project dependencies
  run: |
    poetry install --no-interaction --no-root
    poetry self add poetry-plugin-export
    poetry export --without-hashes --without dev -f requirements.txt &gt; requirements.txt
</code></pre>
<p>Here we use the caching function of GitHub Actions to speed things up. The cache is invalidated only if <code>poetry.lock</code>&apos;s content or python version has changed.</p>
<p>We also exports the project dependencies to a <code>requirements.txt</code> file for docker to use later.</p>
<p>Running unit tests,</p>
<pre><code class="language-yaml">- name: Run tests
  run: |
    poetry run pytest
</code></pre>
<p>The first run will break, as there isn&apos;t any tests written. You may want to write some dummy tests to rectify the build failure after that happens.</p>
<p>Building and publishing docker images</p>
<pre><code class="language-yaml">- name: Get temporary credentials from AWS STS
  uses: aws-actions/configure-aws-credentials@v4
  with:
    aws-region: ${{ env.AWS_REGION }}
    role-to-assume: &quot;arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/${{ env.BUILD_ROLE_NAME }}&quot;
    role-session-name: run-${{ github.run_id }}

- name: Login to Amazon ECR
  id: login-ecr
  uses: aws-actions/amazon-ecr-login@v2

- name: Build and publish docker image
  id: build-publish-docker
  env:
    DOCKER_REGISTRY_ENDPOINT: ${{ steps.login-ecr.outputs.registry }}
  run: |
    version=`poetry version`
    short_version=`poetry version --short`
    IMAGE_NAME=${version% $short_version}
    IMAGE_TAG=$short_version-build$GITHUB_RUN_NUMBER
    docker build --tag $DOCKER_REGISTRY_ENDPOINT/$IMAGE_NAME:$IMAGE_TAG .
    docker push --all-tags $DOCKER_REGISTRY_ENDPOINT/$IMAGE_NAME
    printf &quot;IMAGE_NAME=$IMAGE_NAME\nIMAGE_TAG=$IMAGE_TAG\n&quot; &gt;&gt; &quot;$GITHUB_OUTPUT&quot;
</code></pre>
<p>It is important here that you have created the necessary AWS resources with Terraform, otherwise these steps will fail.</p>
<p>Post-build steps to tag the GitHub repo on a successful build.</p>
<pre><code class="language-yaml">    outputs:
      IMAGE_NAME: ${{ steps.build-publish-docker.outputs.IMAGE_NAME }}
      IMAGE_TAG: ${{ steps.build-publish-docker.outputs.IMAGE_TAG }}

  post-build:
    needs: build
    runs-on: ubuntu-latest

    permissions:
      contents: write

    env:
      IMAGE_NAME: &quot;${{ needs.build.outputs.IMAGE_NAME }}&quot;
      IMAGE_TAG: &quot;${{ needs.build.outputs.IMAGE_TAG }}&quot;

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Tag Github repo
        run: |
          GIT_TAG=&quot;v${{ env.IMAGE_TAG }}&quot;
          git config --global user.email &quot;github-action@users.noreply.github.com&quot;
          git config --global user.name &quot;Github Action&quot;
          git tag -a &quot;$GIT_TAG&quot; -m &quot;tagged by GHA $GIT_TAG&quot;
          git push origin &quot;$GIT_TAG&quot;
</code></pre>
<p>The post-build job is separate because it requires elevated permission.</p>
<p>Bonus: trigger a deployment using a workflow in another GitHub repo. You need <a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens?ref=blog.kakarukeys.quest">personal access token</a> for this step to work.</p>
<pre><code class="language-yaml">- name: Invoke CD workflow
  uses: actions/github-script@v7
  with:
    github-token: ${{ secrets.PAT_WORKFLOW_DISPATCH }}
    script: |
      github.rest.actions.createWorkflowDispatch({
        owner: context.repo.owner,
        repo: &apos;infra_setup&apos;,
        ref: &apos;main&apos;,
        workflow_id: &apos;cd.yml&apos;,
        inputs: {
          env: &apos;dev&apos;,
          images: &apos;{&quot;${{ env.IMAGE_NAME }}&quot;: &quot;${{ env.IMAGE_TAG }}&quot;}&apos;
        }
      })
</code></pre>
<p>Once this is done, commit and push the changes to <code>main</code> branch to trigger the workflow. Enjoy coding.</p>]]></content:encoded></item><item><title><![CDATA[GitHub Actions Amazon ECR Integration - part1]]></title><description><![CDATA[<p>How do you publish docker images to an Amazon ECR repository from a GitHub Actions workflow? (<strong>without using any long-lived tokens</strong>)</p><p>GitHub Actions (GHA) is commonly used as a continuous integration tool, and docker image is a common format for distributing and deploying container-based software. AWS is a popular IaaS</p>]]></description><link>https://blog.kakarukeys.quest/github-actions-amazon-elastic-container-registry-p1/</link><guid isPermaLink="false">65d0aaad69b40300070d4999</guid><category><![CDATA[AWS DevOps]]></category><category><![CDATA[Terraform]]></category><category><![CDATA[Github Actions]]></category><dc:creator><![CDATA[Jeff Wong]]></dc:creator><pubDate>Sat, 17 Feb 2024 12:47:08 GMT</pubDate><content:encoded><![CDATA[<p>How do you publish docker images to an Amazon ECR repository from a GitHub Actions workflow? (<strong>without using any long-lived tokens</strong>)</p><p>GitHub Actions (GHA) is commonly used as a continuous integration tool, and docker image is a common format for distributing and deploying container-based software. AWS is a popular IaaS platform that offers a pay-on-use container registry service.</p><p>If you use all three, it&apos;s important to know how to set up your AWS environment in a secured way to allow seamless publishing of docker images. You need</p><ol><li>an Amazon ECR repository</li><li>to set up GitHub OpenID Connect provider in IAM</li><li>an assumable IAM role with</li><li>a policy that allows login from GHA and docker image push to the ECR repository.</li></ol><p>I will be using Terraform, my main IaC tool and <a href="https://registry.terraform.io/namespaces/terraform-aws-modules?ref=blog.kakarukeys.quest" rel="noreferrer">Terraform AWS modules</a> to make my code succinct and maintainable.</p><hr><h2 id="the-ecr-repo">The ECR repo</h2>
<p>Assuming the docker images will be used in AWS Lambda functions, I have defined a local variable <code>ecr-repo-names</code> to list the repo names. I have also defined <code>repository_lambda_read_access_arns</code> which lists the lambdas that need image pull permission (from the created ECR repos).</p>
<pre><code class="language-hcl">locals {
  ecr-repo-names = [
    &quot;example&quot;,
  ]
}

module &quot;ecr-repos&quot; {
  source  = &quot;terraform-aws-modules/ecr/aws&quot;
  version = &quot;~&gt; 1.6.0&quot;

  for_each = toset(local.ecr-repo-names)

  repository_name = each.value
  repository_lambda_read_access_arns = [
    &quot;arn:aws:lambda:${var.region}:${data.aws_caller_identity.current.account_id}:function:*&quot;
  ]

  repository_lifecycle_policy = jsonencode({
    rules = [{
      rulePriority = 1
      description = &quot;Expire images older than 1 year&quot;
      action = {
        type = &quot;expire&quot;
      }
      selection = {
        tagStatus = &quot;any&quot;
        countType = &quot;sinceImagePushed&quot;
        countUnit = &quot;days&quot;
        countNumber = 365
      }
    }]
  })
}
</code></pre>
<p>The repository lifecycle policy is to make sure the repository space usage is within control.</p>
<h2 id="github-oidc-provider">GitHub OIDC provider</h2>
<p>This will allow IAM to trust GitHub for authentication, so that long-lived tokens are not required.</p>
<pre><code class="language-hcl">locals {
  github-repos = [
    &quot;kakarukeys/example&quot;,
  ]
}

module &quot;iam_github_oidc_provider&quot; {
  source    = &quot;terraform-aws-modules/iam/aws//modules/iam-github-oidc-provider&quot;
  version = &quot;~&gt; 5.33.1&quot;
}
</code></pre>
<p>GitHub will send AWS certain OIDC subject claims during authentication, and it is important you define proper IAM policies to allow the right github repositories to publish docker images based on the claims!</p>
<blockquote>
<p>&#x1F4CE; An OIDC provider authenticates the user&apos;s identity and allows him to securely access other applications (when they are pre-configured to trust the provider).<br>
The OIDC provider does three main things:</p>
<ol>
<li>It performs the actual authentication of the user (e.g. by verifying their username and password)</li>
<li>It issues JSON Web Tokens (JWTs) containing information about the user&apos;s identity to other applications so they can recognize who the user is without storing passwords.</li>
<li>It protects the user&apos;s sensitive information like passwords and only shares necessary details about the user (like name, email) with other applications according to the user&apos;s consent.</li>
</ol>
</blockquote>
<h2 id="assumable-iam-role-and-policies">Assumable IAM role and policies</h2>
<p>The IAM policy is based on <a href="https://github.com/aws-actions/amazon-ecr-login?ref=blog.kakarukeys.quest#ecr-private">GitHub&apos;s recommendations</a>, with one exception: I have further allow GHA to perform <code>lambda:GetLayerVersion</code> action so that it can download and install Lambda extensions.</p>
<pre><code class="language-hcl">data &quot;aws_iam_policy_document&quot; &quot;ecr-publish-policy-document&quot; {
  statement {
    effect = &quot;Allow&quot;
  
    actions = [
      &quot;ecr:GetAuthorizationToken&quot;,
      &quot;lambda:GetLayerVersion&quot;,
    ]

    resources = [&quot;*&quot;]
  }

  statement {
    effect = &quot;Allow&quot;

    actions = [
      &quot;ecr:BatchCheckLayerAvailability&quot;,
      &quot;ecr:BatchGetImage&quot;,
      &quot;ecr:CompleteLayerUpload&quot;,
      &quot;ecr:GetDownloadUrlForLayer&quot;,
      &quot;ecr:InitiateLayerUpload&quot;,
      &quot;ecr:PutImage&quot;,
      &quot;ecr:UploadLayerPart&quot;,
    ]

    resources = [for r in module.ecr-repos : r.repository_arn]
  }
}

resource &quot;aws_iam_policy&quot; &quot;ecr-publish-policy&quot; {
  name        = &quot;ecr-publish-policy&quot;
  description = &quot;Allow principal to publish images into selected ECR repos&quot;
  policy = data.aws_iam_policy_document.ecr-publish-policy-document.json
}

module &quot;gha-build-role&quot; {
  source  = &quot;terraform-aws-modules/iam/aws//modules/iam-github-oidc-role&quot;
  version = &quot;~&gt; 5.33.1&quot;

  # specify for Github Enterprise
  # audience     = &quot;https://mygithub.com/&lt;GITHUB_ORG&gt;&quot;
  # provider_url = &quot;mygithub.com/_services/token&quot;

  subjects = [
    for repo in local.github-repos : 
    &quot;repo:${repo}:ref:refs/heads/main&quot;
  ]

  name = &quot;GHA-Build-Role&quot;
  description = &quot;IAM role for Github Action to assume, for performing tasks related to project build&quot;
  max_session_duration = 3600   # secs

  policies = {
    EcrPublish = aws_iam_policy.ecr-publish-policy.arn
  }
}
</code></pre>
<p>The assumable role is defined with a <a href="https://registry.terraform.io/modules/terraform-aws-modules/iam/aws/latest/submodules/iam-github-oidc-role?ref=blog.kakarukeys.quest">special AWS Terraform module called <code>iam-github-oidc-role</code></a> (many codes were abstracted away!). What it does is to allow GHA to make API calls to AWS STS service in order to assume the role, which is allowed by us for publishing images.</p>
<p>Pay attention to <code>subjects</code> which defines the repos and branches for which the role can be used for docker image publishing.</p>
<hr><h2 id="testing">Testing</h2>
<p>That&apos;s all. Use these scripts in a Terraform project, create a plan and apply it to create the necessary resources. Output the role ARN with</p><pre><code class="language-hcl">output &quot;gha_build_role_arn&quot; {
  description = &quot;arn of GHA Build Role&quot;
  value       = module.gha-build-role.arn
}
</code></pre>
<p>You can then build a docker image with a <code>Dockerfile</code> you have and test the integration with</p><pre><code class="language-bash">$ docker build -t &lt;account-id&gt;.dkr.ecr.&lt;region&gt;.amazonaws.com/example:0.1.0 .

$ aws ecr get-login-password --region &lt;region&gt; | docker login \
    --username AWS \
    --password-stdin &lt;account-id&gt;.dkr.ecr.&lt;region&gt;.amazonaws.com

$ docker push &lt;account-id&gt;.dkr.ecr.&lt;region&gt;.amazonaws.com/example:0.1.0
</code></pre>
<p>Note that the above commands actually uses your AWS profile user&apos;s permissions (likely a root user) to interact with ECR. The next step would be to use the role ARN in an actual GHA workflow. </p><p>To be continued in <a href="https://blog.kakarukeys.quest/github-actions-amazon-elastic-container-registry-p2/" rel="noreferrer">part 2</a>.</p>]]></content:encoded></item><item><title><![CDATA[My experiment with AI in DevOps]]></title><description><![CDATA[<p>You have heard how <a href="https://github.com/features/copilot?ref=blog.kakarukeys.quest" rel="noreferrer">the latest advances in AI can help with software development</a>. A natural question would be if AI could be integrated with DevOps to revolutionize the way infrastructure is managed and deployed.</p><p>So I did a simple experiment. </p><p><em>Can AI help automate some of my daily tasks?</em></p>]]></description><link>https://blog.kakarukeys.quest/experiment-with-ai-devops/</link><guid isPermaLink="false">65792575ca8f7900070de48d</guid><category><![CDATA[Terraform]]></category><category><![CDATA[AWS DevOps]]></category><category><![CDATA[LLM]]></category><dc:creator><![CDATA[Jeff Wong]]></dc:creator><pubDate>Wed, 13 Dec 2023 03:31:01 GMT</pubDate><content:encoded><![CDATA[<p>You have heard how <a href="https://github.com/features/copilot?ref=blog.kakarukeys.quest" rel="noreferrer">the latest advances in AI can help with software development</a>. A natural question would be if AI could be integrated with DevOps to revolutionize the way infrastructure is managed and deployed.</p><p>So I did a simple experiment. </p><p><em>Can AI help automate some of my daily tasks?</em></p><p>I use <strong>Terraform</strong> as my major IaC (Infrastructure as Code) tool. At the very least, if I feed the model with detailed instructions, can it write my IaC code flawlessly?</p><h2 id="the-llm-models-i-used">The LLM models I used</h2>
<p>I did some research, and found 3 cutting-edge language models suitable for Terraform coding:</p><ul>
<li><a href="https://kagi.com/fastgpt?ref=blog.kakarukeys.quest">Kagi FastGPT</a></li>
<li><a href="https://www.phind.com/search?home=true&amp;ref=blog.kakarukeys.quest">Phind</a></li>
<li><a href="https://chat.openai.com/?ref=blog.kakarukeys.quest">OpenAI ChatGPT</a></li>
</ul>
<p>I was on a budget, so I picked the free tier for each. Kagi FastGPT is a nimble research tool that gives superb results, as it performs <a href="https://aws.amazon.com/what-is/retrieval-augmented-generation/?ref=blog.kakarukeys.quest" rel="noreferrer">RAG</a> with Kagi&apos;s search index.</p><p>Phind does RAG too, and its underlying model was fine-tuned with codes. So it is being marketed as a coding assistant. OpenAI ChatGPT: you know what it is and its capability.</p><h2 id="the-task-to-do">The task to do</h2>
<p>I used the 3 models to research and later write Terraform and Python scripts, to set up an AWS Lambda function that opens and parses any new csv file in an existing S3 bucket, and appends the rows to a Redshift table.</p><p>The Lambda function is to be triggered by an EventBridge event which is emitted when a new file is added to the S3 bucket.</p><p>This is a very simplistic but realistic task, and to lessen the scope, I have not instructed the models to generate the deployment script. So I took care of the deployment myself, by manually packaging the python code and uploading it to another S3 bucket to be picked by the Lambda function.</p><p>But I expected the models to generate <strong>proper IAM roles and policies</strong>, so that the Lambda function had proper access to the various AWS services for its task. A DevOps engineer would find this task the most time-consuming.</p><h2 id="the-research-phase">The research phase</h2>
<p>I tried to imagine myself as a junior / intermediate DevOps engineer (it was harder than I thought) using AI. So I asked the basic questions, and then the detailed questions. For examples:</p><ol>
<li>what are the common ways where AWS lambda functions are used in data engineering?</li>
<li>can AWS EventBridge do what AWS Cloudwatch Events do?</li>
<li>how to deploy a lambda function that requires external python libraries as project dependencies?</li>
<li>what are the managed policies available for lambda function to write to Amazon Cloudwatch Logs?</li>
</ol>
<p>To my pleasant surprise, the models got most of the answers and facts correct. If one missed something, the other models usually filled in.</p><p>The RAG models: Kagi FastGPT and Phind did slightly better than OpenAI ChatGPT 3.5. I expected this, because it is common sense that the answers will be better if you feed the models with the relevant tech documentations.</p><p>One downside was that the models did not have a strong opinion on which approach was optimal or the best practice. They gave commandline or console instructions in some answers, but to be fair, I was conducting a research and so did not prompt the models for instructions conforming with IaC.</p><h2 id="the-coding-phase">The coding phase</h2>
<p>After studying the answers from the research phase, I formulated an approach and created the necessary prompts. I started asking the models to generate codes.</p><blockquote>
<p>write a Terraform script to achieve the following:</p>
<ol>
<li>set up an EventBridge event and rule to track changes in an existing S3 bucket: adding of new csv file</li>
<li>when a new csv file is added in the bucket, trigger a lambda function to run</li>
<li>the lambda function should parse the file and append the rows in the csv file to a Redshift table, using Python runtime.</li>
<li>the lambda function will be uploaded to another existing S3 bucket to be picked up by AWS</li>
<li>create a proper role with proper managed policies so that the lambda function has access to read from S3, and write to Redshift.</li>
</ol>
</blockquote>
<blockquote>
<p>write some python code to achieve the following:</p>
<ol>
<li>make a connection to an Amazon Redshift Serverless database. assuming the script will be run with appropriate IAM role to make the connection.</li>
<li>given a csv DictReader object, read all dictionary objects from it.</li>
<li>write all dictionary objects into a database table, appending to it.</li>
</ol>
</blockquote>
<p>I have asked the models to generate a single script instead of a whole project (did not think that was possible). I already had a project boilerplate that I could use.</p><h2 id="the-challenges-of-ai">The challenges of AI</h2>
<p>While AI demonstrated speed and versatility in providing code snippets, it struggled to deliver the most optimal solutions. I found that errors and inaccuracies often permeated the generated code.</p><p>In some cases, some lines of code were complete fabrications or <strong>&quot;hallucinations&quot;</strong> &#x2014; instances where the AI produces made-up results but presents them as if they were true. You need considerable DevOps knowledge in order to spot such fabrications.</p><p>A model gave an incorrect <code>aws_lambda_permission</code> resource block in its generated code:</p>
<pre><code class="language-hcl">resource &quot;aws_lambda_permission&quot; &quot;s3_event_permission&quot; {
  statement_id  = &quot;AllowExecutionFromS3Event&quot;
  action        = &quot;lambda:InvokeFunction&quot;
  function_name = aws_lambda_function.csv_parser_lambda.function_name
  principal     = &quot;s3.amazonaws.com&quot;
  source_arn    = aws_s3_bucket.existing_bucket.arn
}
</code></pre>
<p>The principal should be <code>events.amazonaws.com</code>.</p>
<p>Another glaring weakness of AI is its inability to leverage reusable third-party libraries and modules. It often causes the generated code to be very verbose.</p><p>In my experiment, the generated terraform script consists of many individual resource blocks. AI has failed to make use of any <a href="https://github.com/terraform-aws-modules?ref=blog.kakarukeys.quest" rel="noreferrer">official AWS terraform modules</a>.</p><p>Additional prompts and follow-up questioning were not able to force the models to use AWS terraform modules. It seemed the model had not been trained with data that included those modules.</p><p>In the generated python script, AI has opted to use the <code>INSERT</code> SQL statement instead of the more efficient <code>COPY</code>.</p><pre><code class="language-python">with open(download_path, &apos;r&apos;) as csv_file:
    csv_reader = csv.reader(csv_file)
    next(csv_reader)  # Skip header

    for row in csv_reader:
        # Assuming your Redshift table has columns col1, col2, col3
        cursor.execute(
            &quot;INSERT INTO your_redshift_table (col1, col2, col3) VALUES (%s, %s, %s)&quot;, 
            (row[0], row[1], row[2])
        )

redshift_conn.commit()
cursor.close()
redshift_conn.close()
</code></pre>
<p>DevOps principles emphasize modularity and efficiency. Unfortunately AI failed to grasp adequately these aspects.</p><h2 id="conclusion">Conclusion</h2>
<p>As the experiment unfolded, it became evident that while AI holds promise in augmenting DevOps workflows, it is not yet mature enough to replace human expertise entirely. A DevOps expert brings not just technical proficiency but also contextual understanding and oversight, qualities that AI struggles to replicate.</p><p>Despite its limitations, AI proved its ability to generate ideas swiftly and in abundance. This presents an opportunity for human experts to leverage AI as a tool for ideation and exploration, allowing them to sift through generated concepts and decide on their feasibility.<br></p>]]></content:encoded></item></channel></rss>