Payment of ₹ requested by HONEY VIG. Click the link to pay securely via Razorpay: Make 5000 payment Payment of ₹ requested by HONEY VIG. Click the link to pay securely via Razorpay: Make 5000 payment ⭐ If you would like to buy me a coffee, well thank you very much that is mega kind! : https://www.buymeacoffee.com/honeyvig
Hire a web Developer and Designer to upgrade and boost your online presence with cutting edge Technologies

Friday, July 1, 2022

The Ultimate Free Solo Blog Setup With Ghost And Gatsby

 When it comes to tools for publishing a blog, it can seem like there’s never a perfect solution that mixes customization with easy admin. In this article, we will see step-by-step how you can get the best of both worlds by using Ghost as a headless CMS for a Gatsby static site. We will cover all the tricky parts in-depth and show you can do everything for free.

These days it seems there are an endless number of tools and platforms for creating your own blog. However, lots of the options out there lean towards non-technical users and abstract away all of the options for customization and truly making something your own.

If you are someone who knows their way around front-end development, it can be frustrating to find a solution that gives you the control you want, while removing the admin from managing your blog content.

Enter the Headless Content Management System (CMS). With a Headless CMS, you can get all of the tools to create and organize your content, while maintaining 100% control of how it is delivered to your readers. In other words, you get all of the backend structure of a CMS while not being limited to its rigid front-end themes and templates.

When it comes to Headless CMS systems, I’m a big fan of Ghost. Ghost is open-source and simple to use, with lots of great APIs that make it flexible to use with static site builders like Gatsby.

In this article, I will show you how you can use Ghost and Gatsby together to get the ultimate personal blog setup that lets you keep full control of your front-end delivery, but leaves all the boring content management to Ghost.

Oh, and it’s 100% free to set up and run. That’s because we will be running our Ghost instance locally and then deploying to Netlify, taking advantage of their generous free tier.

Let’s dive in!

Setting Up Ghost And Gatsby #

I’ve written a starter post on this before that covers the very basics, so I won’t go too in-depth into them here. Instead, I will focus on the more advanced issues and gotchas that come up when running a headless blog.

But in short, here’s what we need to do to get a basic set-up up and running that we can work from:

  • Install a local version of the Gatsby Starter Blog
  • Install Ghost locally
  • Change the source data from Markdown to Ghost (swap out gatsby-source-file system for gatsby-source-ghost)
  • Modify the GraphQL queries in your gatsby-node, templates, and pages to match the gatsby-source-ghost schema

For more details on any of these steps, you can check out my previous article.

Or you can just start from the code in this Github repository.

Dealing With Images #

With the basics out of the way, the first issue we run into with a headless blog that builds locally is what to do with images.

Ghost by default serves images from its own server. So when you go headless with a static site, you will run into a situation where your content is built and served from an edge provider like Netlify, but your images are still being served by your Ghost server.

This isn’t ideal from a performance perspective and it makes it impossible to build and deploy your site locally (which means you would have to pay monthly fees for a Digital Ocean droplet, AWS EC2 instance, or some other server to host your Ghost instance).

But we can get around that if we can find another solution to host our images &mdash, and thankfully, Ghost has storage converters that enable you to store images in the cloud.

For our purposes, we are going to use an AWS S3 converter, which enables us to host our images on AWS S3 along with Cloudfront to give us a similar performance to the rest of our content.

There are two open-source options available: ghost-storage-adapter-s3 and ghost-s3-compat. I use ghost-storage-adapter-s3 since I find the docs easier to follow and it was more recently updated.

That being said, if I followed the docs exactly, I got some AWS errors, so here’s the process that I followed that worked for me:

  • Create a new S3 Bucket in AWS and select Disable Static Hosting
  • Next, create a new Cloudfront Distribution and select the S3 Bucket as the Origin
  • When configuring the Cloudfront Distribution, under S3 Bucket Access:

    • Select “Yes, use OAI (bucket can restrict access to only Cloudfront)”
    • Create a New OAI
    • And finally, select “Yes, update the bucket policy”

Then, you just need to create an IAM User for Ghost that will enable it to write new images to your new S3 Bucket. To do this, create a new Programmatic IAM User and attach this policy to it:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "s3:ListBucket",
            "Resource": "arn:aws:s3:::YOUR-S3-BUCKET-NAME"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject",
                "s3:PutObjectVersionAcl",
                "s3:DeleteObject",
                "s3:PutObjectAcl"
            ],
            "Resource": "arn:aws:s3:::YOUR-S3-BUCKET-NAME/*"
        }
    ]
} 

With that, our AWS setup is complete, we just need to tell Ghost to read and write our images there instead of to its local server.

To do that, we need to go to the folder where our Ghost instance is installed and open the file: ghost.development.json orghost.production.json.(depending on what environment you’re currently running)

Then we just need to add the following:

{
  "storage": {
  "active": "s3",
  "s3": {
    "accessKeyId": "[key]",
    "secretAccessKey": "[secret]",
    "region": "[region]",
    "bucket": "[bucket]",
    "assetHost": "https://[subdomain].example.com", // cloudfront
    "forcePathStyle": true,
    "acl": "private"
  }
}

The values for accessKeyId and secretAccessKey can be found from your IAM setup, while the region and bucket refer to the region and bucket name of your S3 bucket. Finally, the assetHost is the URL of your Cloudfront distribution.

Now, if you restart your Ghost instance, you will see that any new images you save are in your S3 bucket and Ghost knows to link to them there. (Note: Ghost won’t make updates retroactively, so be sure to do this first thing after a fresh Ghost install so you don’t have to re-upload images later)

With Images out of the way, the next tricky thing we need to think about is internal links. As you are writing content in Ghost and inserting links in Posts and Pages, Ghost will automatically add the site’s URL to all internal links.

So for example, if you put a link in your blog post that goes to /my-post/, Ghost is going to create a link that goes to https://mysite.com/my-post/.

Normally, this isn’t a big deal, but for Headless blogs this causes problems. This is because your Ghost instance will be hosted somewhere separate from your front-end and in our case it won’t even be reachable online since we will be building locally.

This means that we will need to go through each blog post and page to correct any internal links. Thankfully, this isn’t as hard as it sounds.

First, we will add this HTML parsing script in a new file called replaceLinks.js and put it in a new utils folder at src/utils:

const url = require(`url`);
const cheerio = require('cheerio');

const replaceLinks = async (htmlInput, siteUrlString) => {
  const siteUrl = url.parse(siteUrlString);
  const $ = cheerio.load(htmlInput);
  const links = $('a');
  links.attr('href', function(i, href){
    if (href) {
      const hrefUrl = url.parse(href);
      if (hrefUrl.protocol === siteUrl.protocol && hrefUrl.host === siteUrl.host) {
        return hrefUrl.path
      }

      return href;
    }

  });
  return $.html();
}

module.exports = replaceLinks;

Then we will add the following to our gatsby-node.js file:

exports.onCreateNode = async ({ actions, node, getNodesByType }) => {
  if (node.internal.owner !== `gatsby-source-ghost`) {
    return
  }
  if (node.internal.type === 'GhostPage' || node.internal.type === 'GhostPost') {
    const settings = getNodesByType(`GhostSettings`);
    actions.createNodeField({
      name: 'html',
      value: replaceLinks(node.html, settings[0].url),
      node
    })
  }
}

You will see that we are adding two new packages in replaceLinks.js, so let’s start by installing those with NPM:

npm install --save url cheerio

In our gatsby-node.js file, we are hooking into Gatsby’s onCreateNode, and specifically into any nodes that are created from data that comes from gatsby-source-ghost (as opposed to metadata that comes from our config file that we don’t care about for now).

Then we are checking the node type, to filter out any nodes that are not Ghost Pages or Posts (since these are the only ones that will have links inside their content).

Next, we are getting the URL of the Ghost site from the Ghost settings and passing that to our removeLinks function along with the HTML content from the Page/Post.

In replaceLinks, we are using cheerio to parse the HTML. Then we can then select all of the links in this HTML content and map through their href attributes. We can then check if the href attribute matches the URL of the Ghost Site — if it does, we will replace the href attribute with just the URL path, which is the internal link that we are looking for (e.g. something like /my-post/).

Finally, we are making this new HTML content available through GraphQL using Gatsby’s createNodeField (Note: we must do it this way since Gatsby does not allow you to overwrite fields at this phase in the build).

Now our new HTML content will be available in our blog-post.js template and we can access it by changing our GraphQL query to:

ghostPost(slug: { eq: $slug }) {
  id
  title
  slug
  excerpt
  published_at_pretty: published_at(formatString: "DD MMMM, YYYY")
  html
  meta_title
  fields {
  html
  } 
}

And with that, we just need to tweak this section in the template:

<section
  dangerouslySetInnerHTML={{ __html: post.html }}
  itemProp="articleBody"
/>

To be:

<section
 dangerouslySetInnerHTML={{ __html: post.fields.html }}
  itemProp="articleBody"
/>

This makes all of our internal links reachable, but we still have one more problem. All of these links are <a>anchor tags while with Gatsby we should be using Gatsby Link for internal links (to avoid page refreshes and to provide a more seamless experience).

Thankfully, there is a Gatsby plugin that makes this really easy to solve. It’s called gatsby-plugin-catch-links and it looks for any internal links and automatically replaces the <a> anchor tags with Gatsby <Link>.

All we need to do is install it using NPM:

npm install --save gatsby-plugin-catch-links

And add gatsby-plugin-catch-links into our plugins array in our gatsby-config file.

Adding Templates And Styles

Now the big stuff is technically working, but we are missing out on some of the content from our Ghost instance.

The Gatsby Starter Blog only has an Index page and a template for Blog Posts, while Ghost by default has Posts, Pages, as well as pages for Tags and Authors. So we need to create templates for each of these.

For this, we can leverage the Gatsby starter that was created by the Ghost team.

As a starting point for this project, we can just copy and paste a lot of the files directly into our project. Here’s what we will take:

The meta files are adding JSON structured data markup to our templates. This is a great benefit that Ghost offers by default on their platform and they’ve transposed it into Gatsby as part of their starter template.

Then we took the Pagination and PostCard.js components that we can drop right into our project. And with those components, we can take the template files and drop them into our project and they will work.

The fragments.js file makes our GraphQL queries a lot cleaner for each of our pages and templates — we now just have a central source for all of our GraphQL queries. And the siteConfig.js file has a few Ghost configuration options that are easiest to put in a separate file.

Now we will just need to install a few npm packages and update our gatsby-node file to use our new templates.

The packages that we will need to install are gatsby-awesome-pagination, @tryghost/helpers, and @tryghost/helpers-gatsby.

So we will do:

npm install --save gatsby-awesome-pagination @tryghost/helpers @tryghost/helpers-gatsby

Then we need to make some updates to our gatsby-node file.

First, we will add the following new imports to the top of our file:

const { paginate } = require(`gatsby-awesome-pagination`);
const { postsPerPage } = require(`./src/utils/siteConfig`);

Next, in our exports.createPages, we will update our GraphQL query to:

{
  allGhostPost(sort: { order: ASC, fields: published_at }) {
      edges {
          node {
              slug
          }
      }
  }
  allGhostTag(sort: { order: ASC, fields: name }) {
      edges {
          node {
              slug
              url
              postCount
          }
      }
  }
  allGhostAuthor(sort: { order: ASC, fields: name }) {
      edges {
          node {
              slug
              url
              postCount
          }
      }
  }
  allGhostPage(sort: { order: ASC, fields: published_at }) {
      edges {
          node {
              slug
              url
          }
      }
  }
}

This will pull all of the GraphQL data we need for Gatsby to build pages based on our new templates.

To do that, we will extract all of those queries and assign them to variables:

// Extract query results
  const tags = result.data.allGhostTag.edges
  const authors = result.data.allGhostAuthor.edges
  const pages = result.data.allGhostPage.edges
  const posts = result.data.allGhostPost.edges

Then we will load all of our templates:

// Load templates
  const tagsTemplate = path.resolve(`./src/templates/tag.js`)
  const authorTemplate = path.resolve(`./src/templates/author.js`)
  const pageTemplate = path.resolve(`./src/templates/page.js`)
  const postTemplate = path.resolve(`./src/templates/post.js`)

Note here that we are replacing our old blog-post.js template with post.js, so we can go ahead and delete blog-post.js from our templates folder.

Finally, we will add this code to build pages from our templates and GraphQL data:

// Create tag pages
tags.forEach(({ node }) => {
    const totalPosts = node.postCount !== null ? node.postCount : 0

    // This part here defines, that our tag pages will use
    // a `/tag/:slug/` permalink.
    const url = `/tag/${node.slug}`

    const items = Array.from({length: totalPosts})

    // Create pagination
    paginate({
        createPage,
        items: items,
        itemsPerPage: postsPerPage,
        component: tagsTemplate,
        pathPrefix: ({ pageNumber }) => (pageNumber === 0) ? url : `${url}/page`,
        context: {
            slug: node.slug
        }
    })
})

// Create author pages
authors.forEach(({ node }) => {
    const totalPosts = node.postCount !== null ? node.postCount : 0

    // This part here defines, that our author pages will use
    // a `/author/:slug/` permalink.
    const url = `/author/${node.slug}`

    const items = Array.from({length: totalPosts})

    // Create pagination
    paginate({
        createPage,
        items: items,
        itemsPerPage: postsPerPage,
        component: authorTemplate,
        pathPrefix: ({ pageNumber }) => (pageNumber === 0) ? url : `${url}/page`,
        context: {
            slug: node.slug
        }
    })
})

// Create pages
pages.forEach(({ node }) => {
  // This part here defines, that our pages will use
  // a `/:slug/` permalink.
  node.url = `/${node.slug}/`

  createPage({
      path: node.url,
      component: pageTemplate,
      context: {
          // Data passed to context is available
          // in page queries as GraphQL variables.
          slug: node.slug,
      },
  })
})

// Create post pages
posts.forEach(({ node }) => {
    // This part here defines, that our posts will use
    // a `/:slug/` permalink.
    node.url = `/${node.slug}/`
    createPage({
        path: node.url,
        component: postTemplate,
        context: {
            // Data passed to context is available
            // in page queries as GraphQL variables.
            slug: node.slug,
        },
    })
})

Here, we are looping in turn through our tags, authors, pages, and posts. For our pages and posts, we are simply creating slugs and then creating a new page using that slug and telling Gatsby what template to use.

For the tags and author pages, we are also adding pagination info using gatsby-awesome-pagination that will be passed into the page’s pageContext.

With that, all of our content should now be successfully built and displayed. But we could use a bit of work on styling. Since we copied over our templates directly from the Ghost Starter, we can use their styles as well.

Not all of these will be applicable, but to keep things simple and not get too bogged down in styling, I took all of the styles from Ghost’s src/styles/app.css starting from the section Layout until the end. Then you will just paste these into the end of your src/styles.css file.

Observe all of the styles starting with kg — this refers to Koening which is the name of the Ghost editor. These styles are very important for the Post and Page templates, as they have specific styles that handle the content that is created in the Ghost editor. These styles ensure that all of the content you are writing in your editor is translated over and displayed on your blog correctly.

Lastly, we need our page.js and post.js files to accommodate our internal link replacement from the previous step, starting with the queries:

Page.js

ghostPage(slug: { eq: $slug } ) {
  …GhostPageFields
    fields {
      html
     }
}

Post.js

ghostPost(slug: { eq: $slug } ) {
  …GhostPostFields
    fields {
      html
    }
}

And then the sections of our templates that are using the HTML content. So in our post.js we will change:

<section
className="content-body load-external-scripts"
dangerouslySetInnerHTML={{ __html: post.html }} />

To:

<section
className="content-body load-external-scripts"
dangerouslySetInnerHTML={{ __html: post.fields.html }} />

And similarly, in our page.js file, we will change page.html to page.fields.html.

Dynamic Page Content

One of the disadvantages of Ghost when used as a traditional CMS, is that it is not possible to edit individual pieces of content on a page without going into your actual theme files and hard coding it.

Say you have a section on your site that is a Call-to-Action or customer testimonials. If you want to change the text in these boxes, you will have to edit the actual HTML files.

One of the great parts of going headless is that we can make dynamic content on our site that we can easily edit using Ghost. We are going to do this by using Pages that we will mark with ‘internal’ tags or tags that start with a # symbol.

So as an example, let’s go into our Ghost backend, create a new Page called Message, type something as content, and most importantly, we will add the tag #message.

Now let’s go back to our gatsby-node file. Currently, we are building pages for all of our tags and pages, but if we modify our GraphQL query in createPages, we can exclude everything internal:

allGhostTag(sort: { order: ASC, fields: name }, **filter: {slug: {regex: "/^((?!hash-).)*$/"}}**) {
    edges {
        node {
            slug
            url
            postCount
        }
    }
}
//...
allGhostPage(sort: { order: ASC, fields: published_at }, **filter: {tags: {elemMatch: {slug: {regex: "/^((?!hash-).)*$/"}}}}**) {
    edges {
        node {
            slug
            url
            html
        }
    }
}

We are adding a filter on tag slugs with the regex expression /^((?!hash-).)*$/. This expression is saying to exclude any tag slugs that include hash-.

Now, we won’t be creating pages for our internal content, but we can still access it from our other GraphQL queries. So let’s add it to our index.js page by adding this to our query:

query GhostIndexQuery($limit: Int!, $skip: Int!) {
    site {
      siteMetadata {
        title
      }
    }
    message: ghostPage
      (tags: {elemMatch: {slug: {eq: "hash-message"}}}) {
        fields {
          html
        }
    }
    allGhostPost(
        sort: { order: DESC, fields: [published_at] },
        limit: $limit,
        skip: $skip
    ) {
      edges {
        node {
          ...GhostPostFields
        }
      }
    }
  }

Here we are creating a new query called “message” that is looking for our internal content page by filtering specifically on the tag #message. Then let’s use the content from our #message page by adding this to our page:

//...
const BlogIndex = ({ data, location, pageContext }) => {
  const siteTitle = data.site.siteMetadata?.title || `Title`
  const posts = data.allGhostPost.edges
  const message = data.message;
//...
return (
  <Layout location={location} title={siteTitle}>
    <Seo title="All posts" />
    <section
      dangerouslySetInnerHTML={{
        __html: message.fields.html,
      }}
    />
  )
}

Finishing Touches

Now we’ve got a really great blog setup, but we can add a few final touches: pagination on our index page, a sitemap, and RSS feed.

First, to add pagination, we will need to convert our index.js page into a template. All we need to do is cut and paste our index.js file from our src/pages folder over to our src/templates folder and then add this to the section where we load our templates in gatsby-node.js:

// Load templates
 const indexTemplate = path.resolve(`./src/templates/index.js`)

Then we need to tell Gatsby to create our index page with our index.js template and tell it to create the pagination context.

Altogether we will add this code right after where we create our post pages:

// Create Index page with pagination
  paginate({
      createPage,
      items: posts,
      itemsPerPage: postsPerPage,
      component: indexTemplate,
      pathPrefix: ({ pageNumber }) => {
          if (pageNumber === 0) {
            return `/`
          } else {
              return `/page`
            }
      },
  })

Now let’s open up our index.js template and import our Pagination component and add it right underneath where we map through our posts:

import Pagination from '../components/pagination'
//...
      </ol>
      <Pagination pageContext={pageContext} />
    </Layout>
//...

Then we just need to change the link to our blog posts from:

<Link to={post.node.slug} itemProp="url">

to:

<Link to={`/${post.node.slug}/`} itemProp="url">

This prevents Gatsby Link from prefixing our links on pagination pages — in other words, if we didn’t do this, a link on page 2 would show as /page/2/my-post/ instead of just /my-post/ like we want.

With that done, let’s set up our RSS feed. This is a pretty simple step, as we can use a ready-made script from the Ghost team’s Gatsby starter. Let’s copy their file generate-feed.js into our src/utils folder.

Then let’s use it in our gatsby-config.js by replacing the existing gatsby-plugin-feed section with:

{
  resolve: `gatsby-plugin-feed`,
  options: {
      query: `
      {
          allGhostSettings {
              edges {
                  node {
                      title
                      description
                  }
              }
          }
      }
    `,
      feeds: [
          generateRSSFeed(config),
      ],
  },
}

We will need to import our script along with our siteConfig.js file:

const config = require(`./src/utils/siteConfig`);
const generateRSSFeed = require(`./src/utils/generate-feed`);
//...

Finally, we need to make one important addition to our generate-feed.js file. Right after the GraphQL query and the output field, we need to add a title field:

#...
output: `/rss.xml`,
title: "Gatsby Starter Blog RSS Feed",
#...

Without this title field, gatsby-plugin-feed will throw an error on the build.

Then for our last finishing touch, let’s add our sitemap by installing the package gatsby-plugin-advanced-sitemap:

npm install --save gatsby-plugin-advanced-sitemap

And adding it to our gatsby-config.js file:

{
  resolve: `gatsby-plugin-advanced-sitemap`,
  options: {
      query: `
        {
            allGhostPost {
                edges {
                    node {
                        id
                        slug
                        updated_at
                        created_at
                        feature_image
                    }
                }
            }
            allGhostPage {
                edges {
                    node {
                        id
                        slug
                        updated_at
                        created_at
                        feature_image
                    }
                }
            }
            allGhostTag {
                edges {
                    node {
                        id
                        slug
                        feature_image
                    }
                }
            }
            allGhostAuthor {
                edges {
                    node {
                        id
                        slug
                        profile_image
                    }
                }
            }
        }`,
        mapping: {
            allGhostPost: {
                sitemap: `posts`,
            },
            allGhostTag: {
                sitemap: `tags`,
            },
            allGhostAuthor: {
                sitemap: `authors`,
            },
            allGhostPage: {
                sitemap: `pages`,
            },
        },
        exclude: [
            `/dev-404-page`,
            `/404`,
            `/404.html`,
            `/offline-plugin-app-shell-fallback`,
        ],
        createLinkInHead: true,
        addUncaughtPages: true,
    }
}
}

The query, which also comes from the Ghost team’s Gatsby starter, creates individual sitemaps for our pages and posts as well as our author and tag pages.

Now, we just have to make one small change to this query to exclude our internal content. Same as we did in the prior step, we need to update these queries to filter out tag slugs that contain ‘hash-’:

allGhostPage(filter: {tags: {elemMatch: {slug: {regex: "/^((?!hash-).)*$/"}}}}) {
    edges {
        node {
            id
            slug
            updated_at
            created_at
            feature_image
        }
    }
}
allGhostTag(filter: {slug: {regex: "/^((?!hash-).)*$/"}}) {
    edges {
        node {
            id
            slug
            feature_image
        }
    }
}

Wrapping Up #

With that, you now have a fully functioning Ghost blog running on Gatsby that you can customize from here. You can create all of your content by running Ghost on your localhost and then when you are ready to deploy, you simply run:

gatsby build

And then you can deploy to Netlify using their command-line tool:

netlify deploy -p

Since your content only lives on your local machine, it is also a good idea to make occasional backups, which you can do using Ghost’s export feature.

This exports all of your content to a json file. Note, it doesn’t include your images, but these will be saved on the cloud anyway so you don’t need to worry as much about backing these up.

I hope you enjoyed this tutorial where we covered:

  • Setting up Ghost and Gatsby;
  • Handling Ghost Images using a storage converter;
  • Converting Ghost internal links to Gatsby Link;
  • Adding templates and styles for all Ghost content types;
  • Using dynamic content created in Ghost;
  • Setting up RSS feeds, sitemaps, and pagination.

Kubernetes For Frontend Developers

 Developers are migrating from on-premises technology and embracing the benefits cloud infrastructure offers. Moreso, there is a rapid shift from a monolithic architecture to microservices architecture; this ensures that apps have higher availability, are simple to install, and easy to upgrade with the microservices architecture approach. In this post, we introduced Kubernetes to front-end engineers and explained why Kubernetes is integral in a production-ready microservice architecture. Some key terms that are important for team collaboration were discussed and concluded the post with a quick deployment to getting started as a beginner.

Kubernetes, also known as k8s, was coined by a Google engineer in mid-2014 and is now widely used throughout the developer ecosystem. According to the Kuberenetes Documentation:

“Kubernetes is an open-source platform for managing containerized workloads and services that allow declarative configuration and automation. It enables developers to build containerized applications that can react to critical application needs in the event of a traffic spike or a service failure.”

Dockers and Containers

Docker is the most popular container technology tool. It is a tool used for building, running, and deploying containerized applications. An application’s code, libraries, tools, dependencies, and other files are all contained in a Docker image; when a user executes an image, it turns into a container. An image built will always only run one container (an image can have multiple layers and then be used to build multiple images, but an image will always only create one container).

Docker images can be likened to a template of instructions to build a container. It helps to abstract application code from the underlying infrastructure, simplifying version management and enabling portability across various deployment environments.

Basically, a containerized application is stateless, i.e. it does not retain session information. And since multiple instances of a container image can run simultaneously, developers use containers as instances that can be initiated to replace failed ones without disruption to the application’s operation.

In terms of resource management, it is very important to know that the use of resources is highly constrained, unlike VMs, as its access to physical resources (memory, storage, and CPU) is limited by the host OS. As a result, containers are lighter and more scalable than virtual machines.

“Containers are the foundation of modern application design and development, which means it will be almost impossible for any IT organization to avoid a container commitment.”

— Tom Nolle, “Explore Container Deployment Benefits And Core Components”

For a container deployment, there is the server for Container hosting with OS support together with a container management tool like Docker or ContainerD. When there is a need for application capabilities, such as the ability to scale under load and recover from hardware failures without disruption or human intervention, then an orchestration tool would be needed.

Kubernetes(K8s) is a container orchestration tool that takes over tasks that would otherwise require manual intervention, preventing lots of time-consuming routine checks, changes in configuration, updates, and other software maintenance work. Using Kubernetes deployment would significantly help to automate such repetitive processes and makes loads of manual jobs a breeze when working on a production-ready application.

Why Would A Frontend Developer Use Kubernetes?

For an improved digital experience, Microservices have been successfully implemented by tech giants, such as Netflix, Google, Amazon, and other industry leaders; businesses see this architecture as a cost-effective means of expanding their operations. On the other hand, microservice architecture has gained popularity in recent years due to its effectiveness in developing, deploying, and scaling multiple application backends.

Adoption of the Microservice architecture in production would be generally considered good practice when an application is simply getting too large for any single developer to fully maintain, or there is an increase in orchestration and interaction between services after every release. It is important to know that not all levels of business organization would be required to use microservices to benefit from using Kubernetes.

K8s Or Docker Compose: Which One Should I Use?

Docker-compose is a tool that accepts a YAML file that specifies a cross container application and automates the creation and removal of all those containers without the need to write several docker commands for each one. It should be used for testing and development.

Kubernetes, on the other hand, is a platform for managing production-ready containerized workloads and services that allows for declarative setup as well as automation.

Getting Familiar With Terminology

To utilize Kubernetes efficiently, one must have a reasonable understanding of its terminologies. Here are a few key terminologies to get you started:

  • Docker
    A container resource referring to a Docker image and provides all of the necessary configurations that Kubernetes needs (deploy, execute, expose, monitor, and safeguard the Docker container).
  • Container
    The fundamental concept is the Container — since Kubernetes is a container orchestration tool. A container is a standard unit of software that packages up code and all that the application depends on to run reliably.
  • Pods
    A pod is a collection of one or more containers with common storage and network resources, as well as a set of rules for how the containers should be run. It is the smallest deployable unit that Kubernetes allows you to create and manage.
  • Nodes
    The components (physical computers or virtual machines) that run these applications are known as worker nodes. Worker nodes carry out the tasks that the master node has assigned to them.
  • Cluster
    A cluster is a collection of nodes that are used to run containerized applications. A Kubernetes cluster is made up of a set of master nodes and a number of worker nodes. Minikube is highly suggested for new users who want to start building a Kubernetes cluster.
  • Objects
    Kubernetes objects are persistent entities in the Kubernetes system. Kubernetes uses objects to represent the state of our cluster. They describe the desired state for running applications, the available resources for the running applications, and the policies guiding these applications. It holds information about cluster workload.
  • Namespaces
    Namespaces are a way to distribute cluster resources needed for use in situations with various teams or projects with a large number of users.
  • Ingress Controller
    Kubernetes Ingress is an API object that manages external users’ access to services in a Kubernetes cluster by providing routing rules. This external request is frequently made using HTTPS/HTTP. You can easily set up rules for traffic routing with Ingress without having to create a bunch of Load Balancers or expose each service on the node.
“Enterprises are drifting further away from monoliths and closer toward a microservice architecture for every app, website, and digital experience they develop.”

— Kaya Ismail (2018)

Monolithic architecture is a common way of building an infrastructure as a single unit that includes a user interface, a server-side framework, and a relational database. Since all of the app’s layers and components are interconnected, changing one component would require you to update the entire app.

Another approach is the Microservice architecture. By using the microservices approach, a complex program is broken down into loosely linked parts. Loose coupling establishes a system in which each service or component has its own logically distinct lifecycle, protocols, and database. This standalone component can be designed, implemented, scaled, and managed separately from the rest of the app which will continue to function normally.

We begin to wonder how this can interact, as we consider the two methods. This is where containerization comes in: each stand-alone unit is packed into a container which is then enclosed in a pod (which contains multiple containers that are shared in a cluster of nodes). When a Pod contains several containers, they are handled as a single entity, and all containers in the Pod share the Pod’s resources, including namespace, IP address, and network 

(K8s) is a container-centric infrastructure manager. It manages container lifecycles, that is to say, it optimizes container orchestration deployment by provisioning Pods (creating and destroying them) based on the application’s requirements. Kubernetes exposes pods to requests using the Service object which maps IP addresses to a collection of pods. The service ensures routes to the Pods from any authorized source (within or outside the cluster) through a designated port.

As for frontend developers, we do need to have basic knowledge of setting up the inter-communication between this infrastructure and services. A clear understanding of how things function is essential.

Often referred to as an Ops problem and that by giving it up, we are missing out on opportunities to understand the possibilities of what we create and how we can improve its availability in production.

While the Ops role should be in charge of cluster setup, configuration, and management, the developer should be aware of and responsible for putting up the bare minimum necessary to run their app.

As a result, understanding the fundamentals of Kubernetes will allow you to optimize the configuration of your application and make it more scalable; and such to participate in the release process of what they created.

Teams would benefit from having a general understanding of Kubernetes around the software stack while sharing terminologies and addressing their project. It also allows you complete control over the entire project lifecycle — from code to implementation — allowing you to test the application’s deployment, understand how your project should be deployed, and assist in maintaining the layer as well as specifying the environment.

A good example is when a friend Cari (software engineer) described her experience working with a team a few years ago, where the backend engineers were only interested in getting involved in frontend development and Kubernetes alone. They willingly chose to learn Kubernetes and wouldn’t want to directly work with the backend layers, but only consume it. The team enjoyed having the ability to define how their application is deployed on Kubernetes.

Having this control over their project helps them be a part of the release journey of their projects, and more importantly will allow you to optimize the configuration of your application and make it more scalable. Also, understanding the production-deployment configuration allows developers to spot and fix crucial micro performance issues such as caching, amount of requests, and time-to-first-byte, as well as knowing how staging/testing differs from production before releasing the app.

There are scenarios where the terminology often overlaps between front-end and back-end teams, for example:

  • port and services
    When specifying a version of an app deployed, and then the ports exposed e.g. “we can interact with our application using the service name and port xx: xx”.
  • namespaces
    Knowing that our project sits in a namespace on Kubernetes. Namespaces are a way for multiple users to share cluster resources.
  • RBAC (Role-Based Access and Control)
    Knowing how the access control permissions on Kubernetes affect your projects.

An understanding of how Docker images build and run will allow teams to clearly communicate the requirements for each application in the cluster.

Deployment and service

The resources that you identified in a configuration are created by a deployment. All of the resources that make up a deployment are specified in a configuration file.

To make a deployment, you’ll need a configuration file. YAML syntax is required for a configuration file. It contains fields, such as version, name, kind, replicas field (the desired number of Pod resources), selector, and the label field, etc, as shown below:

....
  name: 
spec:
  selector:
    matchLabels:
      app: 
      tier: 
 
  replicas:
    metadata:
      labels:
        app: 
        tier: 
        
    spec:
      containers:
        - name: 
          image: 
          ...

A Kubernetes Service, on the other hand, is an abstraction layer that describes a logical group of Pods and allows for external traffic exposure, load balancing, and service discovery for such Pods.

Updating a Deployment

A deployment can be updated to the desired object state from the current state, this is often done declaratively by updating the objects of interest in the configuration file and then deploying as an update. With a rolling update deployment strategy, old Pod resources will be gradually replaced with new ones. This means that two Pod resource versions can be deployed and accessed at the same time while ensuring that there is no downtime.

Backend Service Object

In this article, the Pods in the front-end Deployment will run an Nginx image that will be configured to proxy requests to a Service with the key of app: backend. We assume the backend team has already ensured that their pods are running on the cluster, and that we only want to create and connect our app via a deployment to the backend.

In order to allow access to the backend application, we need to create a service for it. A Service creates a persistent IP address and DNS name entry for the application in question which makes it accessible to other pods. It also uses selectors to find the Pods that it routes traffic to.

Here is an example of a backend-service.yaml configuration file which would expose the backend app to other pods in the cluster. Below is the sample backend service configuration file:

---
apiVersion: v1
kind: Service
metadata:
  name: backend-serv
spec:
  selector:
    app: hello
    tier: backend
  ports:
  - protocol: TCP
    port: 80
    ...

The above YAML file shows that the service is configured to route traffic to the Pods that have the label app: hello and tier: backend of the cluster through port 80 only.

Creating the Frontend

You typically create a container image of your application and push it to a registry (e.g. docker registry) before referring to it in a Pod. Since this is an introductory article, we will make use of a sample front-end image from the google container repository. The frontend sends requests to the backend worker Pods by using the DNS name given to the backend Service which is the value of the name field in the backend-service.yaml configuration file.

The Pods in the front-end Deployment run an Nginx image that is configured to proxy requests to the backend Service. The configuration file specifies the server and the listening port. When an ingress is created in Kubernetes, nginx upstreams point to services that match specified selectors.

nginx.conf configuration file

upstream Backend {   
    server backend-serv;
}
server {
    listen 80;
    location / {
                proxy_pass http://Backend;
    }
}

Note that the internal DNS name used by the backend Service inside Kubernetes is used to specify.

The frontend, like the backend, has a Deployment and a Service. The setup for the front-end Service has type: LoadBalancer which means that the Service would use a load balancer provisioned by the cloud service provider and would be reachable from outside the cluster.

---
apiVersion: v1
kind: Service
metadata:
  name: frontend-serv
spec:
  selector:
    app: hello
    tier: frontend
  ports:
  - protocol: "TCP"
    port: 80
    targetPort: 80
  type: LoadBalancer
...
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend-depl
spec:
  selector:
    matchLabels:
      app: hello
      tier: frontend
      track: stable
  replicas: 1
  template:
    metadata:
      labels:
        app: hello
        tier: frontend
        track: stable
    spec:
      containers:
        - name: nginx
          image: "gcr.io/google-samples/hello-frontend:1.0"
          ...

Create The Front-End Deployment And Service

Now, that the configuration files are ready, we can run a kubectl apply command to create the resources as specified:

kubectl apply -f [insert URL to saved frontend-deployment YAML file] 
 
kubectl apply -f [insert URL to saved frontend-service YAML file]

The output verifies that both resources were created:

deployment.apps/frontend-depl created
service/frontend-serv created

Interacting with the Front-end Deployment and Service

You can use this command to obtain the external IP address, once you’ve created a LoadBalancer Service:

kubectl get service frontend-serv –watch

This shows the front-end Service’s configurations and monitors for changes. The internal cluster IP would be immediately provisioned, while the external IP address is initially marked as pending:

  NAME          TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)  AGE
frontend-serv   LoadBalancer   10.xx.xxx.xxx   <pending>     80/TCP   10s

As soon as an external IP is provisioned, however, the configuration updates to include the new IP under theEXTERNAL-IPheading:

NAME       TYPE           CLUSTER-IP      EXTERNAL-IP        PORT(S)  AGE
frontend-serv   LoadBalancer   10.xx.xxx.xx   XXX.XXX.XXX.XXX    80/TCP   1m

The provisioned IP can be used to communicate with the front-end service from outside the cluster. We can now send traffic through the frontend, because the frontend and backend are now linked. Using the curl command on the external IP of your front-end Service, we can reach the endpoint.

Conclusion 

A company-wide vision for providing reliable software may be driven by a broad understanding of Kubernetes and how your application works on Kubernetes. The microservices architecture is more beneficial for complex and evolving applications. It offers effective solutions for handling a complicated system of different functions and services within one application.

However, it is critical to understand that the microservice design is not appropriate for every level of business organization. You should start with a monolith if your business idea is new and you want to validate it. For a small technical team attempting to design a basic and lightweight application, microservices can be considered superfluous, since monolith can be deployed via Kube without any problems, and you can still benefit from replication options and other features.