<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Raj Saxena</title>
    <description>The latest articles on Forem by Raj Saxena (@therajsaxena).</description>
    <link>https://forem.com/therajsaxena</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F138612%2F67d6374c-c481-4905-afca-ca10b3a51b3c.jpeg</url>
      <title>Forem: Raj Saxena</title>
      <link>https://forem.com/therajsaxena</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/therajsaxena"/>
    <language>en</language>
    <item>
      <title>Continuously deploying to Kubernetes with Github Actions.</title>
      <dc:creator>Raj Saxena</dc:creator>
      <pubDate>Tue, 25 Aug 2020 12:00:00 +0000</pubDate>
      <link>https://forem.com/therajsaxena/continuous-deployment-to-gke-with-github-actions-2g0n</link>
      <guid>https://forem.com/therajsaxena/continuous-deployment-to-gke-with-github-actions-2g0n</guid>
      <description>&lt;h3&gt;
  
  
  Background
&lt;/h3&gt;

&lt;p&gt;We are a young Berlin-based startup that is on the Google Cloud Platform. We find GCP to be much more developer-friendly compared to the other cloud providers that we had used at our previous companies. Google Cloud offers &lt;code&gt;gcloud&lt;/code&gt;, a CLI tool that can be used to interact &amp;amp; manage cloud resources on the platform. It can be used to authorize and configure credentials for various other tools like &lt;code&gt;docker&lt;/code&gt; and &lt;code&gt;kubectl&lt;/code&gt;. GCP has a concept of &lt;a href="https://cloud.google.com/iam/docs/service-accounts"&gt;Service-Accounts&lt;/a&gt; that can be used to manage services by assigning the appropriate IAM permissions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Github Actions?
&lt;/h3&gt;

&lt;p&gt;One of the biggest pain point of the SRE/DevOps/Platform team at a previous company was managing the Jenkins cluster running CI/CD jobs. A typical self-managed installation has the following challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Auto-scaling with the needs of the day. You need increased capacity to run jobs immediately during the core business hours and reduced capacity otherwise.&lt;/li&gt;
&lt;li&gt;Jobs might need different versions of software installed. Example - one service might need &lt;code&gt;Java&lt;/code&gt; 11 &amp;amp; the other service might have adapted &lt;code&gt;Java&lt;/code&gt; 15 already.&lt;/li&gt;
&lt;li&gt;If the jobs running on the same shared worker node are not isolated properly, they can interfere with each other by relying on the same files and libraries and overwriting them.&lt;/li&gt;
&lt;li&gt;Jobs running on the same worker node can compete for resources leading to an increase in the build times or sometimes even timeouts causing flakiness in the pipeline.&lt;/li&gt;
&lt;li&gt;Constant software maintenance of the main server and the worker nodes. This includes upgrading base OS, patching, or manually updating installed software.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When we were discussing the architecture of the platform, people involved brought up their (painful) experiences with CD servers that they had in the past. We all agreed that we didn't want to manage a CI/CD server cluster of our own.&lt;br&gt;
To solve all of the above problems and for a few other reasons, we decided to go with &lt;a href="https://github.com/features/actions"&gt;Github Actions&lt;/a&gt;&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--dGC5741b--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/jpsu7hhdkv47mdiyw2fr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--dGC5741b--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/jpsu7hhdkv47mdiyw2fr.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Github Actions provide a simple YAML based syntax to configure jobs that can trigger on any Github event like &lt;code&gt;push&lt;/code&gt;, &lt;code&gt;merge&lt;/code&gt; to the main branch, etc. These jobs run on one of the available servers on Microsoft Azure.&lt;/p&gt;

&lt;p&gt;We started by having a simple job to continuously integrate, build &amp;amp; test, create a docker container &amp;amp; push to the container registry. Here's a sample from a &lt;code&gt;java&lt;/code&gt; service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;master&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt; &lt;span class="c1"&gt;# checkout repo&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-java@v1&lt;/span&gt; &lt;span class="c1"&gt;# Set up latest Java 14&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;java-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;14&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build with Gradle&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./gradlew clean build&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GoogleCloudPlatform/github-actions/setup-gcloud@master&lt;/span&gt; &lt;span class="c1"&gt;# Setup gcloud&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;service_account_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GCP_SA_KEY }}&lt;/span&gt; &lt;span class="c1"&gt;# ServiceAccount key with necessary rights added as a secret on Github.&lt;/span&gt;

        &lt;span class="c1"&gt;# Configure credentials for docker&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gcloud auth configure-docker&lt;/span&gt;

        &lt;span class="c1"&gt;# Build the Docker image&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker build -t gcr.io/example.com/${{ github.event.repository.name }}:${{ github.sha }} .&lt;/span&gt;

        &lt;span class="c1"&gt;# Push the Docker image to Google Container Registry&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker push gcr.io/example.com/${{ github.event.repository.name }}:${{ github.sha }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;






&lt;h3&gt;
  
  
  Deploying apps on GKE
&lt;/h3&gt;

&lt;p&gt;Another key piece in the architecture is our &lt;strong&gt;Kubernetes&lt;/strong&gt; cluster where the services are deployed. We are proudly using &lt;a href="https://cloud.google.com/kubernetes-engine"&gt;Google Kubernetes Engine&lt;/a&gt; and so far the experience has been positive as it is easy to manage and scale and eliminates the operational overhead.&lt;br&gt;
Kubernetes is managed with &lt;code&gt;kubectl&lt;/code&gt; &amp;amp; &lt;code&gt;gcloud&lt;/code&gt; CLI can be used to configure credentials for it by generating the necessary &lt;a href="https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/"&gt;kubeconfig&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Applications are packaged as &lt;a href="https://helm.sh/"&gt;Helm&lt;/a&gt; charts and configured as per the environment needs. It's important to mention this because &lt;code&gt;helm&lt;/code&gt; utilises &lt;code&gt;kubeconfig&lt;/code&gt; to interact with the cluster.&lt;/p&gt;

&lt;p&gt;Deployments are created and updated with the latest image from the container registry as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Creating a deployment&lt;/span&gt;
helm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; values.yml &lt;span class="nt"&gt;--set&lt;/span&gt; image.version&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;latest&amp;gt; example-service example-service

&lt;span class="c"&gt;# Updating a deployment&lt;/span&gt;
helm upgrade &lt;span class="nt"&gt;-f&lt;/span&gt; values.yml &lt;span class="nt"&gt;--set&lt;/span&gt; image.version&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;latest&amp;gt; example-service example-service

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://cloud.google.com/kubernetes-engine/docs/how-to/hardening-your-cluster"&gt;GKE hardening guide&lt;/a&gt; suggests restricting the network access to the Kubernetes cluster and creating it as a &lt;a href="https://cloud.google.com/kubernetes-engine/docs/concepts/private-cluster-concept"&gt;private cluster&lt;/a&gt;. This isolates the nodes from the public internet and the access to Kubernetes API server can be further restricted to specific trusted network IPs only.&lt;/p&gt;

&lt;p&gt;When we started, we created an &lt;code&gt;allow-list&lt;/code&gt; with the IP of a trusted machine used to manage the cluster and deploy upgrades manually. We also built a (very cool) UI tool that could be used to deploy, revert &amp;amp; scale deployments.&lt;/p&gt;

&lt;p&gt;At this point, this is how it all looked together&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GmONxGlk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/l464vtdanniqtzxrriy3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GmONxGlk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/l464vtdanniqtzxrriy3.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a long time, this worked really well until we started feeling the need for Continuous Deployment. I share the argument for it &lt;a href="https://suspendfun.com/2020/A-brief-argument-for-continuous-deployment/"&gt;here&lt;/a&gt;.&lt;/p&gt;


&lt;h3&gt;
  
  
  The challenge
&lt;/h3&gt;

&lt;p&gt;The simplest way to add Continuous Deployment to the pipeline was to add another step in the Github Action pipeline that would deploy the latest image to the cluster. Let's update the action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="c1"&gt;# same as before&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GoogleCloudPlatform/github-actions/setup-gcloud@master&lt;/span&gt; &lt;span class="c1"&gt;# Setup gcloud&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;service_account_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GCP_SA_KEY }}&lt;/span&gt; &lt;span class="c1"&gt;# ServiceAccount key with necessary rights added as a secret on Github.&lt;/span&gt;

        &lt;span class="c1"&gt;# Configure credentials for docker&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gcloud auth configure-docker&lt;/span&gt;

        &lt;span class="c1"&gt;# Build the Docker image&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker build -t gcr.io/example.com/${{ github.event.repository.name }}:${{ github.sha }} .&lt;/span&gt;

        &lt;span class="c1"&gt;# Push the Docker image to Google Container Registry&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker push gcr.io/example.com/${{ github.event.repository.name }}:${{ github.sha }}&lt;/span&gt;

        &lt;span class="c1"&gt;# Generate kubeconfig entry&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gcloud container clusters get-credentials &amp;lt;cluster-name&amp;gt; --zone &amp;lt;zone&amp;gt; --project &amp;lt;project&amp;gt;&lt;/span&gt;

        &lt;span class="c1"&gt;# Install helm&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/setup-helm@v1&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;install&lt;/span&gt;

        &lt;span class="c1"&gt;# Deploy latest version&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;helm upgrade -f values.yaml --set image.version=${{ github.sha }} example-service example-service&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Unfortunately, this doesn't work &amp;amp; the pipeline fails at the last step. This is because the Kubernetes server is unreachable over the public internet. &lt;/p&gt;

&lt;h4&gt;
  
  
  Possible solutions (&amp;amp; the bag of problems they come with)
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Make the cluster public&lt;/strong&gt; One easy way of making the setup work, would be to make the cluster reachable over the internet. However, this would be at the expense of exposing the cluster and would have made the setup less secure. The trade-off would not be worth it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Add the Action Runner IPs to the Allow-list&lt;/strong&gt; While the idea seemed good in the beginning, when I dug a little deeper, I found that the servers that run the job could be any from the &lt;a href="https://docs.github.com/en/actions/reference/virtual-environments-for-github-hosted-runners#ip-addresses-of-github-hosted-runners"&gt;5 Azure regions in the US&lt;/a&gt;. The Github page lists a way to download a JSON file containing the IP ranges that is updated periodically. Going down this path would have meant constantly fetching the list &amp;amp; updating the allow-list.&lt;br&gt;&lt;br&gt;
An automated solution would have been the best but I didn't want to parse the file. Luckily, Azure cloud allows to get the IP ranges for &lt;a href="https://docs.microsoft.com/en-us/azure/virtual-network/service-tags-overview#available-service-tags"&gt;Service Tags&lt;/a&gt;. The problem is that all the IP ranges combined for all 5 regions are 1100+ (the last time I checked) and GKE can only have &lt;a href="https://cloud.google.com/kubernetes-engine/docs/how-to/authorized-networks#limitations"&gt;50 entries for authorized networks&lt;/a&gt;.&lt;br&gt;
&lt;em&gt;&amp;lt;insert-image--banging-head-against-the-wall&amp;gt;&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Have a light proxy server&lt;/strong&gt; running that only allows access from the Azure Ip ranges. There wasn't an easy, out of the box solution available that could be configured programmatically to keep updated with the list of IPs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I considered if I should create &lt;strong&gt;a publicly accessible API&lt;/strong&gt; that can be called to trigger deploy? Maybe, I could authenticate with &lt;code&gt;Basic Auth&lt;/code&gt; and call it secure. (&lt;a href="https://security.stackexchange.com/a/990/207084"&gt;It's not!&lt;/a&gt;). It's just didn't feel right to go down this path.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All of these seemed unnatural solutions for various reasons. Unable to programmatically configure the firewall, I seemed to have hit a wall.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Solution
&lt;/h3&gt;

&lt;p&gt;I kept thinking for a solution at the back of my head while working on other tasks. I didn't want to manage CD servers, I didn't want to risk exposing the cluster &amp;amp; I wanted &lt;code&gt;helm&lt;/code&gt; to reach the Kubernetes control server.&lt;br&gt;
I stared at the action's YAML looking for a solution more times than I would like to admit, till it finally hit me - I only needed to run the deployment step from a trusted IP.&lt;/p&gt;

&lt;p&gt;I split the job into 2 - &lt;code&gt;build&lt;/code&gt; &amp;amp; &lt;code&gt;deploy&lt;/code&gt;. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;build&lt;/code&gt; part is the CPU &amp;amp; memory intensive part that compiles code, runs tests, creates artifacts, builds docker image &amp;amp; then pushes it to the container registry. &lt;/li&gt;
&lt;li&gt;The &lt;code&gt;deploy&lt;/code&gt; part just deploys the latest image with &lt;code&gt;helm&lt;/code&gt;.
Once &lt;code&gt;build&lt;/code&gt; is done on Github hosted action runner, &lt;code&gt;deploy&lt;/code&gt; is run on a &lt;a href="https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners"&gt;self-hosted action-runner&lt;/a&gt;. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This self-hosted runner is a compute machine inside Google Cloud having a static IP. I could now configure this static IP into the allow-list of the Kubernetes authorized IP list.&lt;/p&gt;

&lt;p&gt;The updated setup looks like this:&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NYZLlm9p--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/g9vtvoeu1c0kxvoy73ix.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NYZLlm9p--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/g9vtvoeu1c0kxvoy73ix.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the updated &lt;code&gt;actions.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;master&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt; &lt;span class="c1"&gt;# checkout repo&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-java@v1&lt;/span&gt; &lt;span class="c1"&gt;# Set up latest Java 14&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;java-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;14&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build with Gradle&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./gradlew clean build&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GoogleCloudPlatform/github-actions/setup-gcloud@master&lt;/span&gt; &lt;span class="c1"&gt;# Setup gcloud&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;service_account_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GCP_SA_KEY }}&lt;/span&gt; &lt;span class="c1"&gt;# ServiceAccount key with necessary rights added as a secret on Github.&lt;/span&gt;

        &lt;span class="c1"&gt;# Configure credentials for docker&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gcloud auth configure-docker&lt;/span&gt;

        &lt;span class="c1"&gt;# Build the Docker image&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker build -t gcr.io/example.com/${{ github.event.repository.name }}:${{ github.sha }} .&lt;/span&gt;

        &lt;span class="c1"&gt;# Push the Docker image to Google Container Registry&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker push gcr.io/example.com/${{ github.event.repository.name }}:${{ github.sha }}&lt;/span&gt;

  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;self-hosted&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# to block for the build step to complete successfully&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GoogleCloudPlatform/github-actions/setup-gcloud@master&lt;/span&gt; &lt;span class="c1"&gt;# Setup gcloud&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;service_account_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GCP_SA_KEY }}&lt;/span&gt;

        &lt;span class="c1"&gt;# Generate kubeconfig entry&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gcloud container clusters get-credentials &amp;lt;cluster-name&amp;gt; --zone &amp;lt;zone&amp;gt; --project &amp;lt;project&amp;gt;&lt;/span&gt;

        &lt;span class="c1"&gt;# Install helm&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/setup-helm@v1&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;install&lt;/span&gt;

        &lt;span class="c1"&gt;# Deploy latest version&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;helm upgrade -f values.yaml --set image.version=${{ github.sha }} example-service example-service&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;I tried it and it worked perfectly 🎉. &lt;/p&gt;

&lt;p&gt;Since then, we have updated all of the services to use a similar pattern of splitting &lt;code&gt;build&lt;/code&gt; &amp;amp; &lt;code&gt;deploy&lt;/code&gt; jobs &amp;amp; are now happily shipping code to all environments with each change.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2WheGtsb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/4c1ie2isvhxhdzdw1i5r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2WheGtsb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/4c1ie2isvhxhdzdw1i5r.png" alt="Photo by Mike Benna on Unsplash"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have read so far, I am curious to learn from you?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What do you think of the solution? &lt;/li&gt;
&lt;li&gt;Do you use Github Actions as well? &lt;/li&gt;
&lt;li&gt;Do you deploy continuously to Kubernetes as well? &lt;/li&gt;
&lt;li&gt;How do you do it now? And if you can, how would you change it?&lt;/li&gt;
&lt;/ul&gt;




&lt;h4&gt;
  
  
  Disclaimer
&lt;/h4&gt;

&lt;p&gt;&lt;em&gt;The setup shown here is a simplified version that excludes other key components present in the actual environments to focus on the topic discussed above.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>github</category>
      <category>googlecloud</category>
      <category>actionshackathon</category>
      <category>devops</category>
    </item>
    <item>
      <title>A brief argument for Continuous Deployment</title>
      <dc:creator>Raj Saxena</dc:creator>
      <pubDate>Tue, 11 Aug 2020 19:55:41 +0000</pubDate>
      <link>https://forem.com/therajsaxena/a-brief-argument-for-continuous-deployment-5d4h</link>
      <guid>https://forem.com/therajsaxena/a-brief-argument-for-continuous-deployment-5d4h</guid>
      <description>&lt;p&gt;I work at a young startup. The engineering team consists of experienced and smart engineers who come from different backgrounds. We understood the importance of continuously integrating from the start. I want to share our journey of how we moved from &lt;code&gt;Continuous Delivery&lt;/code&gt; to &lt;code&gt;Continuous Deployment&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Let's do a quick refresher of the differences between &lt;strong&gt;Continuous Delivery &amp;amp; Continuous Deployment&lt;/strong&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Continuous Delivery
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Continuous Delivery&lt;/strong&gt; is when you integrate your code often, run the automated test suite and if they pass, proceed to run the build step to create artifacts with each merge to master. Examples of this includes compiling Java code into JARs, building JS bundles, packaging them into docker containers or creating golden machine images. The main purpose is to be able to deploy easily, at any time to any of the environments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--LEaf2LqZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/cpxujdk62swwm5xiam2e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LEaf2LqZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/cpxujdk62swwm5xiam2e.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Continuous Deployment
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Continuous Deployment&lt;/strong&gt; takes it one step further and means taking each change all the way through the deployment pipeline and releasing it in all the environments automatically. The key principle is to release things in smaller chunks so that there are less chances of things breaking because of multiple changes aggregated over since the last release. This helps to reduce the Mean Time To Recovery (&lt;strong&gt;MTTR&lt;/strong&gt;) as you have a narrow set to inspect in case something breaks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2-lPBhHs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/p1mh82ju9cujh39wn9nv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2-lPBhHs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/p1mh82ju9cujh39wn9nv.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Although it is not a novel idea, people inexperienced with it are sometimes apprehensive about it if they don't have experience with it. These concerns are valid and having them is understandable as there are multiple technical, organisational, operational or legal challenges at the company level that might prevent having a Continuous Deployment pipeline.&lt;/p&gt;

&lt;p&gt;It's a shift in thinking and what's important is to understand that Continuous Deployment when done right actually improves stability. Once developers are used to the idea, they are more conscious when they merge code to the &lt;code&gt;main&lt;/code&gt; branch, to test &amp;amp; ensure that the change is working as expected. In case, if/when something happens to brake, developers have the changes fresh in memory &amp;amp; are already in the context to root-cause, fix and recover.&lt;/p&gt;

&lt;p&gt;A Continuous Deployment pipeline ensures that the latest changes are always deployed &amp;amp; keeps the different environments always in sync. It also avoids having to chase and remind people to deploy the latest versions.&lt;/p&gt;




&lt;p&gt;Let's take an example to contrast with the alternative of not deploying immediately - A developer works on a task and merges the code to master. But, it's 5 PM and they don't want to risk deploy the changes to &lt;code&gt;production&lt;/code&gt; now. They promise to do it first thing in the morning. Now, there are a lot of things that can happen between the time that code got merged to when it is actually deployed. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some other dev working on a different task could have merged to master and acting responsibly to test their changes could have unknowingly deployed the latest version that includes the untested change of the previous task.&lt;/li&gt;
&lt;li&gt;The original dev could have fallen sick. In this case, other members of the team would not know if the original developer has verified the changes or not.&lt;/li&gt;
&lt;li&gt;The original dev could have moved on to a high priority bug fix. By the time, the developer came back to test the changes, they could have forgotten all the edge cases.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If something breaks now, it will take more time for the developer to get back into the context and resolve, compared to when they were already working on it and the knowledge was fresh.&lt;/p&gt;

&lt;p&gt;Convinced, yet?&lt;/p&gt;

&lt;p&gt;Continuous Deployment requires having sufficient automated tests (which you should be doing anyway, right?) to have enough confidence and investment in automated tooling to enable pipelines.&lt;/p&gt;

&lt;p&gt;I understand that there cases where you actually want to be able to change a risky change in a non-production environment. For this, we first built a mechanism to 🔒 lock deployments from proceeding beyond a certain stage if we weren't comfortable deploying them all the way. This ensured that nobody else could accidentally deploy either to push a different change.&lt;br&gt;&lt;br&gt;
We also invested into a &lt;code&gt;feature-toggle&lt;/code&gt; service that allows toggling things on/off at runtime without redeploying.&lt;/p&gt;

&lt;h3&gt;
  
  
  A reasonable approach to decision making.
&lt;/h3&gt;

&lt;p&gt;Because people came from different experiences, they had concerns, fears and mixed opinions about the Continuous Deployment idea. We discussed and at the end, instead of relying on opinions &amp;amp; thoughts, we relied on data to help us make our decision.&lt;br&gt;
We measured the percentage of non-locked deployments and the percentage of locked deployments over a period of time and concluded to optimise for the normal use-case rather than the exception.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--m7rrRq_x--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/7490w63b0ffkzu2kncn3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--m7rrRq_x--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/7490w63b0ffkzu2kncn3.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to convince people in your team &amp;amp; organisation, perhaps a similar approach could help you.&lt;/p&gt;

&lt;p&gt;What is your experience?&lt;/p&gt;

&lt;h4&gt;
  
  
  References:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://martinfowler.com/bliki/ContinuousDelivery.html"&gt;https://martinfowler.com/bliki/ContinuousDelivery.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Originally published on &lt;a href="https://suspendfun.com/2020/A-brief-argument-for-continuous-deployment/"&gt;https://suspendfun.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>discuss</category>
      <category>continuousdelivery</category>
      <category>continuousdeployment</category>
    </item>
    <item>
      <title>SpringBoot fat-jar fails, Shadow 🕵🏽‍♂️ to the rescue.</title>
      <dc:creator>Raj Saxena</dc:creator>
      <pubDate>Mon, 06 Jul 2020 19:57:55 +0000</pubDate>
      <link>https://forem.com/therajsaxena/springboot-fat-jar-fails-shadow-to-the-rescue-ic1</link>
      <guid>https://forem.com/therajsaxena/springboot-fat-jar-fails-shadow-to-the-rescue-ic1</guid>
      <description>&lt;h3&gt;
  
  
  Problem
&lt;/h3&gt;

&lt;p&gt;I faced a peculiar problem where a fat-jar wouldn't run on remote runners. I described the details in my previous post - &lt;a href="https://suspendfun.com/2020/Dataflow-Springboot-app-fails-to-run-when-dockerised/"&gt;Dataflow + SpringBoot app fails to run when Dockerized&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--D2uHcr9l--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/gae9r52417rp314txeth.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--D2uHcr9l--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/gae9r52417rp314txeth.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To unblock myself and deliver on time, I hacked together a solution that during the build step instead of compiling the source to create a jar and containerize the jar, I packaged the source repository and used the gradle wrapper to run that code when the container starts. &lt;br&gt;
It worked but it had a few problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;❗️The images were almost 1 GB. Our typical service is around 350 MB out of which 195 MB is the Distroless Java 11 itself.&lt;/li&gt;
&lt;li&gt;❗️The startup was slow as gradle was starting first and then booting the app.&lt;/li&gt;
&lt;li&gt;❗️The image wasn't self-sufficient and I had to inject Artifactory credentials at runtime to start the container so that gradle can authenticate and validate dependencies.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All of these made the solution less than ideal. &lt;/p&gt;

&lt;p&gt;In the following days, when I had more time, I kept looking into the issue and found the following issues on Github and Apache BEAM project&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/GoogleCloudPlatform/DataflowJavaSDK/issues/538"&gt;Files from classpath are not properly resolved when classpath JAR contains META-INF with references to other dependencies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://issues.apache.org/jira/browse/BEAM-1325"&gt;DataflowRunner support for Class-Path jars&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The above seemed related but still focused on different problems and not related specifically to SpringBoot so I also created a 🐞 &lt;a href="https://issues.apache.org/jira/browse/BEAM-9669"&gt;BUG ticket&lt;/a&gt; with details.&lt;/p&gt;
&lt;h3&gt;
  
  
  Solution
&lt;/h3&gt;

&lt;p&gt;Reading through different comments, I saw a suggestion to use &lt;code&gt;maven-shade-plugin&lt;/code&gt;. We use gradle and I learnt that there is something similar for it called &lt;a href="https://imperceptiblethoughts.com/shadow/"&gt;Shadow Gradle plugin&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sharing what it is directly from their page:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Shadow is a Gradle plugin for combining a project's dependency classes and resources into a single output Jar. The combined Jar is often referred to a fat-jar or uber-jar. Shadow utilizes JarInputStream and JarOutputStream to efficiently process dependent libraries into the output jar without incurring the I/O overhead of expanding the jars to disk.&lt;/p&gt;

&lt;p&gt;Shadowing a project output has 2 major use cases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Creating an executable JAR distribution.&lt;/li&gt;
&lt;li&gt;Bundling and relocating common dependencies in libraries to avoid classpath conflicts&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;👨🏻‍💻I gave it a try and it worked. ✨&lt;/p&gt;
&lt;h3&gt;
  
  
  Configuration
&lt;/h3&gt;

&lt;p&gt;This is my &lt;code&gt;build.gradle.kts&lt;/code&gt; configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// shadow config&lt;/span&gt;
&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;withType&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ShadowJar&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;isZip64&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
    &lt;span class="c1"&gt;// Required for Spring&lt;/span&gt;
    &lt;span class="nf"&gt;mergeServiceFiles&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"META-INF/spring.handlers"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"META-INF/spring.schemas"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"META-INF/spring.tooling"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PropertiesFileTransformer&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;paths&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"META-INF/spring.factories"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;mergeStrategy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"append"&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;With this configuration in place, the build pipeline runs &lt;code&gt;./gradlew build&lt;/code&gt; in the step to compile and create the merged jar. Shadow compiles the file to a jar with &lt;code&gt;-all.jar&lt;/code&gt; suffix which is then copied to the Docker image. &lt;br&gt;
This is the Dockerfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM gcr.io/distroless/java:11
VOLUME /tmp
COPY build/libs/dataExtractor-0.0.1-all.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-Dspring.profiles.active=${ENV_SPRING_PROFILE}", "-jar", "/app.jar"]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;This solved the 3 problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ The image is 340 MB and that is close to expected.&lt;/li&gt;
&lt;li&gt;✅ Startup times are similar to expected.&lt;/li&gt;
&lt;li&gt;✅ No more injected Artifactory credentials. I rotated the credentials that were used for the hack.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was an interesting problem because the app worked every time from the IDE or when run locally via gradle but failed when it was containerized and deployed.&lt;/p&gt;

&lt;p&gt;Please let me know if you think there is something that can be improved. &lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://suspendfun.com/2020/Shadow-gradle-plugin-to-create-fat-jar/"&gt;https://suspendfun.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>java</category>
      <category>docker</category>
      <category>dataflow</category>
    </item>
    <item>
      <title>Dataflow + SpringBoot app fails to run when Dockerized.</title>
      <dc:creator>Raj Saxena</dc:creator>
      <pubDate>Mon, 06 Jul 2020 19:57:42 +0000</pubDate>
      <link>https://forem.com/therajsaxena/dataflow-springboot-app-fails-to-run-when-dockerized-1fo5</link>
      <guid>https://forem.com/therajsaxena/dataflow-springboot-app-fails-to-run-when-dockerized-1fo5</guid>
      <description>&lt;h3&gt;
  
  
  The Setup and Problem
&lt;/h3&gt;

&lt;p&gt;We have a Microservices architecture with each service having its separate CloudSQL instance (like how it should be). These databases are accessible from only inside the Virtual Private Cloud (VPC) network and Hashicorp's Vault is used to generate dynamic credentials for them.&lt;br&gt;&lt;br&gt;
We needed a DataWarehouse for analysis and reporting. However, the setup made it a bit tricky as we didn't want to use an external ETL service or have long-living database credentials.&lt;br&gt;
We agreed that the data needs to be extracted to a &lt;a href="https://cloud.google.com/bigquery"&gt;BigQuery&lt;/a&gt; Dataset from where we would expose it with &lt;a href="https://www.metabase.com"&gt;Metabase&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The extraction needs to happen incrementally and periodically so the service would need to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;1) Connect to Vault to generate the read-only credentials for each database.&lt;br&gt;
2) Read the required tables and data. Apply any transformations like type conversion, anonymization or pseudonymization.&lt;br&gt;
3) Write to the BigQuery dataset.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Since data could be huge, I decided to use &lt;a href="https://cloud.google.com/dataflow"&gt;Dataflow&lt;/a&gt; which is Google's Stream and Batch Processing service. &lt;br&gt;
Dataflow is built on &lt;a href="https://beam.apache.org/"&gt;Apache Beam&lt;/a&gt;'s programming and execution model that expects you to define a &lt;code&gt;Pipeline&lt;/code&gt; that connects the &lt;code&gt;source&lt;/code&gt;, and &lt;code&gt;sink&lt;/code&gt; of the data and allows you to apply any &lt;code&gt;transformations&lt;/code&gt; or processing logic between the data flow.&lt;br&gt;&lt;br&gt;
It then executes the &lt;code&gt;Pipeline&lt;/code&gt; job over an auto-scaling fleet of compute instances.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Solution
&lt;/h3&gt;

&lt;p&gt;My solution is a SpringBoot service (fondly named as &lt;code&gt;data-extractor&lt;/code&gt;) that would run periodically and create the necessary Dataflow Pipelines. I used Gradle's KotlinDSL for dependency management.&lt;/p&gt;

&lt;p&gt;The microservice would need access permissions to the following Google services -  BigQuery, Google storage (used to store temporary files), Dataflow, Compute instances. So, I created a dedicated service-account with the necessary permissions. &lt;a href="https://cloud.google.com/iam/docs/service-accounts"&gt;Service accounts&lt;/a&gt; are a special kind of account used by an application or a virtual machine (VM) instance, not a person.&lt;/p&gt;

&lt;p&gt;I built the service and tested multiple runs which were all &lt;strong&gt;successful when I ran the service from the IDE&lt;/strong&gt; or when I did &lt;code&gt;./gradlew run&lt;/code&gt; from the command line.&lt;/p&gt;

&lt;p&gt;The service would successfully boot, connect to Vault to get the credentials, create the Dataflow job which would define the &lt;code&gt;Pipeline&lt;/code&gt; and create necessary compute instance(s) that would process the data and then shut them down after the ETL job was done. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--iaTbh4DI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/t3nbi1d1k9s3vl8n08n2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--iaTbh4DI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/t3nbi1d1k9s3vl8n08n2.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This roughly took 5 minutes for the entire job. The Google console UI shows a nice graph of the entire data flow with multiple user-defined and internal stages and has the logs and metrics for each step.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--QHaj0ttl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/60yfn0jzqasbqq5hngyg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--QHaj0ttl--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/60yfn0jzqasbqq5hngyg.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  The Trouble:
&lt;/h3&gt;

&lt;p&gt;We deploy all of our microservices into Google's Kubernetes Engine(GKE) and so once the service was in good shape, I did the usual process of building an uber fat-jar and dockerizing it.&lt;br&gt;
Here's the Dockerfile that I copied from one of our other SpringBoot repositories.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM gcr.io/distroless/java:11
VOLUME /tmp
COPY build/libs/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-Dspring.profiles.active=${ENV_SPRING_PROFILE}", "-jar", "/app.jar"]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Assuming that &lt;code&gt;./gradlew build&lt;/code&gt; has run before successfully, it copies the fat-jar inside the image and then runs it when the container is started.&lt;br&gt;&lt;br&gt;
Pretty standard, right? &lt;/p&gt;

&lt;p&gt;This is where everything started to fail. When I ran the container (locally as well as in the GKE), I observed that the service would boot successfully, connect to Vault to read the credentials, create the Dataflow job, start the instance but the job would just remain stuck. &lt;br&gt;
The logs would just say that&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Checking permissions granted to controller Service Account. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There would be a log of this printed every 5-6 minutes for up to an hour at which point the job would fail.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Et9jzN0j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/ar1oeehpigo8vohlz6pz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Et9jzN0j--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/ar1oeehpigo8vohlz6pz.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;Here started the painful process of debugging. I desperately searched for any known bugs or issues or relevant StackOverflow answers, or anything I could find in Google documents but couldn't find anything. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The job would work successfully from IDE or with Gradle but will fail when Dockerized&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This happened 10/10 times&lt;/strong&gt; or rather the hundreds of times that I tried.&lt;/p&gt;

&lt;p&gt;The logs hinted at a possible permissions issue with the service-account. However, it did not make total sense as the execution was successful from within the IDE or when ran with Gradle. I made sure that there were no other GoogleCredentials present in my local environment and the service-account was the only credential injected as an environment variable. All the environment variables were the same except the part of containerization. &lt;/p&gt;

&lt;p&gt;I enabled &lt;code&gt;debug&lt;/code&gt; logs but that didn't give any useful insight either. The job would get stuck with the same error. Google console displayed failed job with the message:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Workflow failed. Causes: The Dataflow job appears to be stuck because no worker activity has been seen in the last 1h. Please check the worker logs in Stackdriver Logging. You can also get help with Cloud Dataflow at &lt;a href="https://cloud.google.com/dataflow/support"&gt;https://cloud.google.com/dataflow/support&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Frustrated, I tried everything from ssh-ing into the machine to checking network connectivity to reading syslogs but there was no clue anywhere.&lt;br&gt;
I even opened a Google support case from our account but didn't get a helpful answer. Mostly, because they couldn't find anything wrong on their side.&lt;/p&gt;

&lt;p&gt;I joked during the standup about this that I might have to deploy my laptop. &lt;/p&gt;

&lt;p&gt;Between other tasks, I wasted 2 days on this thing - getting more and more frustrated and tired.&lt;/p&gt;


&lt;h3&gt;
  
  
  The Experiment
&lt;/h3&gt;

&lt;p&gt;Working on a tight deadline, I decided to Dockerize my whole repo and run &lt;code&gt;/gradlew run&lt;/code&gt; from inside it. &lt;br&gt;
Creating a fat-jar and Dockerizing it &lt;/p&gt;

&lt;p&gt;This shouldn't work. Right?&lt;/p&gt;

&lt;p&gt;To my surprise it did.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--zsaLisbQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/2yliqg593tlw312vu64d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zsaLisbQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/2yliqg593tlw312vu64d.png" alt="Alt Text"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It now worked 10/10 times. I was flabbergasted.&lt;/p&gt;

&lt;p&gt;I was now more puzzled than before but also relieved that I was unblocked. I had wasted 2 days stuck on the issue - an issue I did not understand, unable to deploy the app. Those were 2 days of my life that I wasn't gonna get back.&lt;/p&gt;

&lt;p&gt;Turns out there is something about the generated &lt;code&gt;fat-jar&lt;/code&gt; from &lt;code&gt;./gradlew build&lt;/code&gt; that makes it fail to run on Dataflow. &lt;br&gt;
I verified this by directly running the fat jar from the command line as well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# data-extractor.jar is the fat uber jar containing all dependencies.
java -jar data-extractor.jar
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;It failed like the Docker container and at the same point.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Checking permissions granted to controller Service Account. &lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;It had been a wild journey. I decided that I am going to deploy what's working while in parallel, I try to understand and figure out what's going on.&lt;/p&gt;

&lt;p&gt;Here's my updated &lt;code&gt;Dockerfile&lt;/code&gt; that copies the code into the container and runs &lt;code&gt;gradlew classes&lt;/code&gt; to fetch the dependencies and compile the code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;FROM adoptopenjdk:11-jdk-hotspot

VOLUME /app/data-extractor
WORKDIR /app/data-extractor
&lt;span class="c"&gt;# Copy source code&lt;/span&gt;
COPY src /app/data-extractor/src
&lt;span class="c"&gt;# Copy gradle configuration and runner&lt;/span&gt;
COPY &lt;span class="k"&gt;*&lt;/span&gt;.gradle.kts /app/data-extractor/
COPY gradlew /app/data-extractor/
COPY gradle /app/data-extractor/gradle

&lt;span class="c"&gt;# This will install the dependencies in the image&lt;/span&gt;
RUN ./gradlew  &lt;span class="nt"&gt;--no-daemon&lt;/span&gt; classes

EXPOSE 8080
ENTRYPOINT &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"./gradlew"&lt;/span&gt; &lt;span class="s2"&gt;"--no-daemon"&lt;/span&gt; &lt;span class="s2"&gt;"run"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;I don't know why a fat-jar doesn't work here. This is the first time I faced this and I would want to understand better. Perhaps, someone else in the community knows why and share it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are there more such cases?&lt;/li&gt;
&lt;li&gt;How do people workaround them?&lt;/li&gt;
&lt;li&gt;Is there something I can do better?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sharing this so that someone else working on something similar can save time and frustration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Computers and modern software are complex and hard. In the entire feat of integrating a mix of complex technologies like Vault, Kubernetes, BigQuery, Dataflow and the service logic itself, this little thing caused me the most pain. Parts that you think you understand well, can fail in unexpected ways when working together.&lt;/li&gt;
&lt;li&gt;In the cloud-native environment with multiple moving parts, logs can sometimes be misleading. When that happens, it is important to inspect even the smallest things carefully. For most of the time, I suspected something around permissions and did a thorough inspection of roles and permissions. Taking a break and looking in a different direction helps.&lt;/li&gt;
&lt;li&gt;I am sure there's a perfectly reasonable explanation of this. Maybe, it is even documented. But, I couldn't find it easily nor was the error obvious. Sharing such experiences is a good way of learning from each other without making the same mistakes. I have learnt and accepted that I don't know everything and it's fine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;If you have made so far, please let me know what you think.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Originally published on &lt;a href="https://suspendfun.com/2020/Dataflow-Springboot-app-fails-to-run-when-dockerised/"&gt;https://suspendfun.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>springboot</category>
      <category>java</category>
      <category>docker</category>
      <category>dataflow</category>
    </item>
    <item>
      <title>Java 9 to Java 13 - Top features</title>
      <dc:creator>Raj Saxena</dc:creator>
      <pubDate>Fri, 25 Oct 2019 17:05:05 +0000</pubDate>
      <link>https://forem.com/therajsaxena/java-9-to-java-13-top-features-362l</link>
      <guid>https://forem.com/therajsaxena/java-9-to-java-13-top-features-362l</guid>
      <description>&lt;p&gt;Programming tools, frameworks are becoming more and more developer friendly and offer better and modern features to boost developer productivity.&lt;br&gt;&lt;br&gt;
Java was for a long time infamous for having slow release trains. &lt;br&gt;
However, keeping up with times, Java has moved to a cadence of releasing new features with a version upgrade every 6 months (every March and September). &lt;br&gt;
Since then there have been a lot of cool features and tools that have been added to every java developer's toolset. &lt;/p&gt;

&lt;p&gt;This is &lt;strong&gt;a quick summary of the latest features introduced between Java 9 to Java 13&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Java 9
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Module system&lt;/strong&gt;: Helps in modularisation of large apps. This helps to limit exposure of classes that are public in the module vs the true public api of the module. &lt;br&gt;
Explicitly define dependencies and exports in &lt;code&gt;module-info.java&lt;/code&gt;. Eg:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="n"&gt;chef&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;exports&lt;/span&gt; &lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tbst&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;recipe&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

  &lt;span class="n"&gt;requires&lt;/span&gt; &lt;span class="n"&gt;kitchen&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, the module &lt;code&gt;chef&lt;/code&gt; depends on module &lt;code&gt;kitchen&lt;/code&gt; and exports module &lt;code&gt;recipe&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Jlink&lt;/strong&gt;: Having explicit dependencies and modularized JDK means that we can easily come up with the entire dependency graph of the application. This, in turn, enables having a minimal runtime environment containing only the necessary to run the application. This can help to reduce the overall size of the executable jar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;jshell&lt;/strong&gt;: An interactive REPL (Read-Eval-Print-Loop) for quickly playing with java code. Just say &lt;code&gt;jshell&lt;/code&gt; on the terminal after installing JDK 9+.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collection factory methods&lt;/strong&gt;: Earlier, one had to initialize an implementation of a collection (Map, Set, List) first and then add objects to it. Finally, it's possible to create the &lt;strong&gt;&lt;em&gt;immutable&lt;/em&gt;&lt;/strong&gt; collections with static factory methods of the signature &lt;code&gt;&amp;lt;Collection&amp;gt;.of()&lt;/code&gt;. This is achieved by having a bunch of static methods in each of the respective interfaces. Eg:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Creates an immutable list&lt;/span&gt;
&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;abcs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"A"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"B"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"C"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Creates an immutable set&lt;/span&gt;
&lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;xyzs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Y"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Z"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Creates an immutable Map&lt;/span&gt;
&lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;mappings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"key1"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"value1"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"key2"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"value2"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Other features&lt;/strong&gt;&lt;br&gt;
    - Stream API gets more functions like &lt;code&gt;dropWhile&lt;/code&gt;, &lt;code&gt;takeWhile&lt;/code&gt;, &lt;code&gt;ofNullable&lt;/code&gt;.&lt;br&gt;
    - Private interface methods to write clean code and keep things DRY when using default methods in interfaces.&lt;br&gt;
    - new HTTP2 API that supports streams and server based pushes.&lt;/p&gt;


&lt;h2&gt;
  
  
  Java 10
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Local-Variable Type Inference&lt;/strong&gt;: This enables us to write more modern Kotlin/Scala/Typescript like syntax where you don't have to explicitly declare the variable type without compromising type safety. Here, the compiler is able to figure out the type because of the type of the value on the right hand side in case of assignments. Eg:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// infers ArrayList&amp;lt;String&amp;gt;&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;          &lt;span class="c1"&gt;// infers Stream&amp;lt;String&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In cases where the compiler cannot infer the value or it's ambiguous, you need to explicitly declare it. More details &lt;a href="http://openjdk.java.net/jeps/286" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parallel Full GC for G1&lt;/strong&gt;: Improves G1 worst-case latencies by making the full GC parallel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Experimental Java-Based JIT Compiler&lt;/strong&gt;: Enables the Java-based JIT compiler, Graal, to be used as an experimental JIT compiler on the Linux/x64 platform.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Heap Allocation on Alternative Memory Devices&lt;/strong&gt;: Enables the HotSpot VM to allocate the Java object heap on an alternative memory device, such as an NV-DIMM, specified by the user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Root certificates in JDK&lt;/strong&gt;: Open-source the root certificates in Oracle's Java SE Root CA program in order to make OpenJDK builds more attractive to developers, and to reduce the differences between those builds and Oracle JDK builds.&lt;/p&gt;




&lt;h2&gt;
  
  
  Java 11
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;New String methods&lt;/strong&gt;: String class gets new methods like &lt;code&gt;isBlank()&lt;/code&gt;, &lt;code&gt;lines()&lt;/code&gt;, &lt;code&gt;repeat(int)&lt;/code&gt;, unicode aware &lt;code&gt;strip()&lt;/code&gt;, &lt;code&gt;stripLeading()&lt;/code&gt; and &lt;code&gt;stripTrailing()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;New File Methods&lt;/strong&gt;: &lt;code&gt;writeString()&lt;/code&gt;, &lt;code&gt;readString()&lt;/code&gt; and &lt;code&gt;isSameFile()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local-Variable Syntax for Lambda Parameters&lt;/strong&gt;: Allow var to be used when declaring the formal parameters of implicitly typed lambda expressions. &lt;br&gt;
This is introduced to have uniformity with use of &lt;code&gt;var&lt;/code&gt; for local variables. Eg:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;process&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// implicit typed lambda expression&lt;/span&gt;

&lt;span class="c1"&gt;// One benefit of uniformity is that modifiers, notably annotations, can be applied&lt;/span&gt;
&lt;span class="c1"&gt;// to local variables and lambda variables without losing brevity.&lt;/span&gt;
&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@Nonnull&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;@Nullable&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;process&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;JEP 328: Flight Recorder&lt;/strong&gt;: JFR is a profiling tool used to gather diagnostics and profiling data from a running Java application.&lt;br&gt;
Its performance overhead is negligible and that’s usually below 1%. Hence it can be used in production applications.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Removed the Java EE and CORBA Modules&lt;/strong&gt;:  Following packages are removed: &lt;code&gt;java.xml.ws&lt;/code&gt;, &lt;code&gt;java.xml.bind&lt;/code&gt;, &lt;code&gt;java.activation&lt;/code&gt;, &lt;code&gt;java.xml.ws.annotation&lt;/code&gt;, &lt;code&gt;java.corba&lt;/code&gt;, &lt;code&gt;java.transaction&lt;/code&gt;, &lt;code&gt;java.se.ee&lt;/code&gt;, &lt;code&gt;jdk.xml.ws&lt;/code&gt;, &lt;code&gt;jdk.xml.bind&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implicitly compile and run&lt;/strong&gt; No need to compile files with &lt;code&gt;javac&lt;/code&gt; first. You can directly use &lt;code&gt;java&lt;/code&gt; command and it implicitly compiles. This is done to run a program supplied as a single file of Java source code, including usage from within a script by means of "shebang" files and related techniques. Of course for any project bigger than a file, you would use a build tool like gradle, maven, etc.&lt;/p&gt;


&lt;h2&gt;
  
  
  Java 12 - Released March 19, 2019
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Switch expression😎&lt;/strong&gt; The new &lt;code&gt;switch&lt;/code&gt; expression expects a returns value. Multiple matches can go on the same line separated by comma and what happens on match is marked with &lt;code&gt;-&amp;gt;&lt;/code&gt;.&lt;br&gt;
Unlike the traditional &lt;code&gt;switch&lt;/code&gt;, matches don't fall through to the next match. So you don't have to use &lt;code&gt;break;&lt;/code&gt; and this helps prevent bugs. Eg:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;(...,&lt;/span&gt; &lt;span class="o"&gt;...,&lt;/span&gt; &lt;span class="o"&gt;...);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;isCompleted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"PROCESSED"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"COMPLETED"&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"WAITING"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"STUCK"&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InconsistentProcessingStateException&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The switch expression is introduced as a preview and requires &lt;code&gt;--enable-preview&lt;/code&gt; flag to the javac or enabling it in your IDE.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;File byte comparison&lt;/strong&gt; with &lt;code&gt;File.mismatch()&lt;/code&gt;. (&lt;a href="https://docs.oracle.com/en/java/javase/12/docs/api/java.base/java/nio/file/Files.html#mismatch(java.nio.file.Path,java.nio.file.Path)" rel="noopener noreferrer"&gt;From Javadoc&lt;/a&gt;) Finds and returns the position of the first mismatched byte in the content of two files, or -1L if there is no mismatch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collections.teeing&lt;/strong&gt; Streams API gets a new function that applies 2 functions (consumers) on the items and then merges/combines the result of those 2 functions using a third function to produce the final result.&lt;br&gt;&lt;br&gt;
&lt;a href="https://docs.oracle.com/en/java/javase/12/docs/api/java.base/java/util/stream/Collectors.html#teeing(java.util.stream.Collector,java.util.stream.Collector,java.util.function.BiFunction)" rel="noopener noreferrer"&gt;From Javadoc&lt;/a&gt; - Returns a Collector that is a composite of two downstream collectors. Every element passed to the resulting collector is processed by both downstream collectors, then their results are merged using the specified merge function into the final result. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;String methods&lt;/strong&gt;: &lt;code&gt;indent(int n)&lt;/code&gt;, &lt;code&gt;transform(Function f)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Smart cast&lt;/strong&gt; &lt;code&gt;instanceOf&lt;/code&gt; can be used now to do a smart cast as below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt; &lt;span class="n"&gt;instanceOf&lt;/span&gt; &lt;span class="nc"&gt;InconsistentProcessingStateException&lt;/span&gt; &lt;span class="n"&gt;ipse&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// use ipse directly as InconsistentProcessingStateException&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;JVM improvments&lt;/strong&gt;: Low pause GC with Shenandoah, micro-benchmark capabilities, constants API and other improvements.&lt;/p&gt;




&lt;h2&gt;
  
  
  Java 13 - Released September 17, 2019
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Multi-line texts&lt;/strong&gt;: It's now possible to define multiline strings without ugly escape sequences &lt;code&gt;\&lt;/code&gt; or appends. Eg:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;jsonBody&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""
    {
        "name": "Foo",
        "age": 22
    }
"""&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is introduced as a preview and requires &lt;code&gt;--enable-preview&lt;/code&gt; flag to the javac or enabling it in your IDE.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;String&lt;/strong&gt; gets more methods like &lt;code&gt;formatted()&lt;/code&gt;, &lt;code&gt;stripIndent()&lt;/code&gt; and &lt;code&gt;translateEscapes()&lt;/code&gt; for working with multi-line texts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Switch expression&lt;/strong&gt; Still in preview and based on feedback now supports having &lt;code&gt;: yield&lt;/code&gt; syntax in addition to &lt;code&gt;-&amp;gt;&lt;/code&gt; syntax. Hence, we can write&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;(...,&lt;/span&gt; &lt;span class="o"&gt;...,&lt;/span&gt; &lt;span class="o"&gt;...);&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;isCompleted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"PROCESSED"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"COMPLETED"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;yield&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s"&gt;"WAITING"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"STUCK"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;yield&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;RuntimeException&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;span class="o"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Socket API&lt;/strong&gt; reimplemented with modern NIO implementation. This is being done to overcome limitations of legacy api and build a better path towards Fiber as part of &lt;a href="https://cr.openjdk.java.net/~rpressler/loom/Loom-Proposal.html" rel="noopener noreferrer"&gt;Project Loom&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Z GC&lt;/strong&gt; improved to release unused memory.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fay0d2z4wjhuok7j69j9z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fay0d2z4wjhuok7j69j9z.png" alt="duke thinking"&gt;&lt;/a&gt;&lt;br&gt;
Wow, it's getting crazy out there. &lt;br&gt;
If you are a developer that started a decent size Java project recently in the hopes that you would use the latest features and keep yourself and the project updated with the latest versions, do you feel a pressure to catchup with these frequent releases?&lt;br&gt;&lt;br&gt;
Add your comments below or tweet them to me.&lt;/p&gt;

&lt;p&gt;A parting gift - I use &lt;a href="https://github.com/jenv/jenv" rel="noopener noreferrer"&gt;Jenv&lt;/a&gt; to easily switch between different jdk versions locally while switching between different projects. It's pretty cool to manage multiple jdk versions.&lt;/p&gt;

&lt;p&gt;Please note, the features that I talk about here are the ones that, I believe, either add cool features or increase developer productivity the most.&lt;br&gt;&lt;br&gt;
This is &lt;em&gt;not&lt;/em&gt; an exhaustive list. &lt;/p&gt;




&lt;h3&gt;
  
  
  References and good articles:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;I originally posted it &lt;a href="https://suspendfun.com/Latest-Java-9-to-Java-13-features/" rel="noopener noreferrer"&gt;here&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Java 9 - &lt;a href="https://www.pluralsight.com/blog/software-development/java-9-new-features" rel="noopener noreferrer"&gt;pluralsite&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Java 10 - &lt;a href="https://www.techworld.com/developers/java-10-features-whats-new-in-java-10-3680317/" rel="noopener noreferrer"&gt;techworld&lt;/a&gt;, &lt;a href="https://dzone.com/articles/java-10-new-features-and-enhancements" rel="noopener noreferrer"&gt;dzone&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Java 11 - &lt;a href="https://www.geeksforgeeks.org/java-11-features-and-comparison/" rel="noopener noreferrer"&gt;geeksforgeeks&lt;/a&gt;, &lt;a href="https://www.journaldev.com/24601/java-11-features" rel="noopener noreferrer"&gt;journaldev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Java 12 - &lt;a href="https://www.journaldev.com/28666/java-12-features" rel="noopener noreferrer"&gt;journaldev&lt;/a&gt;, &lt;a href="https://stackify.com/java-12-new-features-and-enhancements-developers-should-know/" rel="noopener noreferrer"&gt;stackify&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Java 13 - &lt;a href="https://jaxenter.com/java-13-jdk-deep-dive-new-features-162272.html" rel="noopener noreferrer"&gt;jaxenter&lt;/a&gt;, &lt;a href="https://www.infoworld.com/article/3340052/jdk-13-the-new-features-in-java-13.html" rel="noopener noreferrer"&gt;infoworld&lt;/a&gt;, &lt;a href="https://dzone.com/articles/81-new-features-and-apis-in-jdk-13" rel="noopener noreferrer"&gt;dzone&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>java</category>
      <category>openjdk</category>
      <category>jvm</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
