Skip to main content

Kubernetes Getting Started

Introduction

The Kubernetes Grucloud provider allows to define and describe Kubernetes manifests in Javascript, removing the need to write YAML or template files.

The GruCloud Command Line Interface gc reads a description in Javascript and connects to the k8s control plane to apply the new or updated resource definitions.

For this tutorial, we will define a Namespace, a Service, and a Deployment to deploy an Nginx web server.

diagram-target

This diagram is generated from the code with gc graph

Requirements

Ensure kubectl is installed, and minikube is started: K8s Requirements

It is always a good idea to verify the current kubectl context, especially when switching k8s clusters:

kubectl config current-context

Getting GruCloud CLI: gc

The GruCloud CLI, gc, is written in Javascript and run on Node.js, hence node is required:

node --version

Install gc in just one command:

npm i -g @grucloud/core

Verify gc is installed properly by displaying the version:

gc --version

Project Content

We'll describe in the next sections the 4 files required for this infrastructure as code project:

The link to the source code for this tutorial

Let's create a new project directory

mkdir tuto
cd tuto

package.json

The npm init command will create a basic package.json:

npm init

Let's install the GruCloud Kubernetes provider and the SDK. We'll also install axios and rubico, needed for the post-deployment hooks, which does some final health check.

npm install @grucloud/core @grucloud/provider-k8s rubico axios

config.js

Create the config.js which contains the configuration for this project:

// config.js
const pkg = require("./package.json");
module.exports = () => ({
projectName: pkg.name,
namespace: "myapp",
appLabel: "nginx-label",
service: { name: "nginx-service" },
deployment: {
name: "nginx-deployment",
container: { name: "nginx", image: "nginx:1.14.2" },
},
});

iac.js

Let's create the iac.js with the following content:

// iac.js
const { K8sProvider } = require("@grucloud/provider-k8s");

// Create a namespace, service and deployment
const createResources = async ({ provider }) => {
const { config } = provider;

const namespace = provider.makeNamespace({
properties: ({}) => ({
metadata: {
name: config.namespace,
},
}),
});

const service = provider.makeService({
name: config.service.name,
properties: () => ({
metadata: {
name: config.service.name,
namespace: config.namespace,
},
spec: {
selector: {
app: config.appLabel,
},
type: "NodePort",
ports: [
{
protocol: "TCP",
port: 80,
targetPort: 80,
nodePort: 30020,
},
],
},
}),
});

const deployment = provider.makeDeployment({
properties: ({}) => ({
metadata: {
name: config.deployment.name,
namespace: config.namespace,
labels: {
app: config.appLabel,
},
},
spec: {
replicas: 1,
selector: {
matchLabels: {
app: config.appLabel,
},
},
template: {
metadata: {
labels: {
app: config.appLabel,
},
},
spec: {
containers: [
{
name: config.deployment.container.name,
image: config.deployment.container.image,
ports: [
{
containerPort: 80,
},
],
},
],
},
},
},
}),
});

return { namespace, service, deployment };
};

exports.createStack = async ({ createProvider }) => {
return {
provider: createProvider(K8sProvider, {
createResources,
config: require("./config"),
}),
hooks: [require("./hook")],
};
};

hook.js

When the resources are created, any code can be invoked, defined in hook.js, useful to perform some final health check.

In this case, the kubectl port-forward is called with the right option:

kubectl --namespace myapp port-forward svc/nginx-service 8081:80

Then, we'll use the axios library to perform HTTP calls to the web server, retrying if necessary.

When the website is up, it will open a browser at http://localhost:8081

// hook.js
const assert = require("assert");
const Axios = require("axios");
const { pipe, tap, eq, get, or } = require("rubico");
const { first } = require("rubico/x");
const { retryCallOnError } = require("@grucloud/core").Retry;
const shell = require("shelljs");

module.exports = ({ resources, provider }) => {
const localPort = 8081;
const url = `http://localhost:${localPort}`;

const servicePort = pipe([
() => resources.service.properties({}),
get("spec.ports"),
first,
get("port"),
])();

const kubectlPortForwardCommand = `kubectl --namespace ${resources.namespace.name} port-forward svc/${resources.service.name} ${localPort}:${servicePort}`;

const axios = Axios.create({
timeout: 15e3,
withCredentials: true,
});

return {
onDeployed: {
init: async () => {},
actions: [
{
name: `exec: '${kubectlPortForwardCommand}', check web server at ${url}`,
command: async () => {
// start kubectl port-forward
var child = shell.exec(kubectlPortForwardCommand, { async: true });
child.stdout.on("data", function (data) {});

// Get the web page, retry until it succeeds
await retryCallOnError({
name: `get ${url}`,
fn: () => axios.get(url),
shouldRetryOnException: ({ error }) =>
or([
eq(get("code"), "ECONNREFUSED"),
eq(get("response.status"), 404),
])(error),
isExpectedResult: (result) => {
assert(result.headers["content-type"], `text/html`);
return [200].includes(result.status);
},
config: { retryCount: 20, retryDelay: 5e3 },
});
// Open a browser
shell.exec(`open ${url}`, { async: true });
},
},
],
},
onDestroyed: {
init: () => {},
},
};
};

Workflow

We'll describe the most useful gc commands: apply, list, destroy, and plan.

Deploy

We are now ready to deploy the resources with the apply command:

gc apply

The first part is to find out the plan, i.e what is going to be deployed. You will be prompted if you accept or abort. When typing: 'y', the resources will be deployed: a namespace, a service, and a deployment.

Querying resources on 1 provider: k8s
✓ k8s
✓ Initialising
✓ Listing 7/7
✓ Querying
✓ Namespace 1/1
✓ Service 1/1
✓ Deployment 1/1
┌──────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 1 Namespace from k8s │
├──────────┬──────────┬────────────────────────────────────────────────────────────────────────────────┤
│ Name │ Action │ Data │
├──────────┼──────────┼────────────────────────────────────────────────────────────────────────────────┤
│ myapp │ CREATE │ apiVersion: v1 │
│ │ │ kind: Namespace │
│ │ │ metadata: │
│ │ │ name: myapp │
│ │ │ annotations: │
│ │ │ Name: myapp │
│ │ │ ManagedBy: GruCloud │
│ │ │ CreatedByProvider: k8s │
│ │ │ stage: dev │
│ │ │ projectName: @grucloud/example-k8s-tuto1 │
│ │ │ │
└──────────┴──────────┴────────────────────────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 1 Service from k8s │
├──────────────────────┬──────────┬────────────────────────────────────────────────────────────────────┤
│ Name │ Action │ Data │
├──────────────────────┼──────────┼────────────────────────────────────────────────────────────────────┤
│ myapp::nginx-service │ CREATE │ spec: │
│ │ │ selector: │
│ │ │ app: nginx-label │
│ │ │ type: NodePort │
│ │ │ ports: │
│ │ │ - protocol: TCP │
│ │ │ port: 80 │
│ │ │ targetPort: 8080 │
│ │ │ apiVersion: v1 │
│ │ │ kind: Service │
│ │ │ metadata: │
│ │ │ name: nginx-service │
│ │ │ annotations: │
│ │ │ Name: nginx-service │
│ │ │ ManagedBy: GruCloud │
│ │ │ CreatedByProvider: k8s │
│ │ │ stage: dev │
│ │ │ projectName: @grucloud/example-k8s-tuto1 │
│ │ │ namespace: myapp │
│ │ │ │
└──────────────────────┴──────────┴────────────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 1 Deployment from k8s │
├─────────────────────────┬──────────┬─────────────────────────────────────────────────────────────────┤
│ Name │ Action │ Data │
├─────────────────────────┼──────────┼─────────────────────────────────────────────────────────────────┤
│ myapp::nginx-deployment │ CREATE │ metadata: │
│ │ │ labels: │
│ │ │ app: nginx-label │
│ │ │ name: nginx-deployment │
│ │ │ annotations: │
│ │ │ Name: nginx-deployment │
│ │ │ ManagedBy: GruCloud │
│ │ │ CreatedByProvider: k8s │
│ │ │ stage: dev │
│ │ │ projectName: @grucloud/example-k8s-tuto1 │
│ │ │ namespace: myapp │
│ │ │ spec: │
│ │ │ replicas: 1 │
│ │ │ selector: │
│ │ │ matchLabels: │
│ │ │ app: nginx-label │
│ │ │ template: │
│ │ │ metadata: │
│ │ │ labels: │
│ │ │ app: nginx-label │
│ │ │ spec: │
│ │ │ containers: │
│ │ │ - name: nginx │
│ │ │ image: nginx:1.14.2 │
│ │ │ ports: │
│ │ │ - containerPort: 80 │
│ │ │ apiVersion: apps/v1 │
│ │ │ kind: Deployment │
│ │ │ │
└─────────────────────────┴──────────┴─────────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Plan summary for provider k8s │
├─────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ DEPLOY RESOURCES │
├────────────────────┬────────────────────────────────────────────────────────────────────────────────┤
│ Namespace │ myapp │
├────────────────────┼────────────────────────────────────────────────────────────────────────────────┤
│ Service │ myapp::nginx-service │
├────────────────────┼────────────────────────────────────────────────────────────────────────────────┤
│ Deployment │ myapp::nginx-deployment │
└────────────────────┴────────────────────────────────────────────────────────────────────────────────┘
✔ Are you sure to deploy 3 resources, 3 types on 1 provider? … yes
Deploying resources on 1 provider: k8s
✓ k8s
✓ Initialising
✓ Deploying
✓ Namespace 1/1
✓ Service 1/1
✓ Deployment 1/1
3 resources deployed of 3 types and 1 provider
Running OnDeployedGlobal resources on 1 provider: k8s
Command "gc a" executed in 30s

In the case of the Deployment type manifest, gc will query the pod that is started by the deployment through the replica set, when one of the container's pod is ready, the deployment can proceed.

Later on, when we deal with the Ingress type, gc will wait for the load balancer to be ready.

The command gc apply is the equivalent of kubectl apply -f mymanifest.yaml but it waits for resources to be up and running, ready to serve.

We could try to run the gc apply or the gc plan, we should not expect any deployment or destruction of resources.

In the mathematical and computer science world, we could say that apply (and destroy) commands are idempotent: "property of certain operations in mathematics and computer science whereby they can be applied multiple times without changing the result beyond the initial application".

List

Let's verify that the resources are deployed with the gc list command:

A live diagram will be also generated.

gc list --our --all --graph
List Summary:
Provider: k8s
┌─────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ k8s │
├────────────────────┬────────────────────────────────────────────────────────────────────────────────┤
│ Namespace │ myapp │
├────────────────────┼────────────────────────────────────────────────────────────────────────────────┤
│ Service │ myapp::nginx-service │
├────────────────────┼────────────────────────────────────────────────────────────────────────────────┤
│ Deployment │ myapp::nginx-deployment │
├────────────────────┼────────────────────────────────────────────────────────────────────────────────┤
│ ReplicaSet │ myapp::nginx-deployment-66cdc8d56b │
├────────────────────┼────────────────────────────────────────────────────────────────────────────────┤
│ Pod │ myapp::nginx-deployment-66cdc8d56b-4d8lz │
└────────────────────┴────────────────────────────────────────────────────────────────────────────────┘
5 resources, 15 types, 1 provider
Command "gc list --our --all --graph" executed in 0s

diagram-live

Notice the relationship between the Pod, ReplicaSet and Deployment.

The Deployment creates a ReplicaSet which creates a one or more Pod(s).

When querying the k8s-api-server for the live resources, the pod contains information about its ReplicaSet parent, who has itself information about its parent Deployment. This allows gc to find out the links between the resources.

Post Deploy Hook

Would like to check the health of the system? You can run the onDeployed hook any time with the following command:

gc run --onDeployed
Running OnDeployed resources on 1 provider: k8s
Forwarding from 127.0.0.1:8081 -> 80
Forwarding from [::1]:8081 -> 80
Handling connection for 8081
✓ k8s
✓ Initialising
✓ default::onDeployed
✓ exec: 'kubectl --namespace myapp port-forward svc/nginx-service 8081:80', check web server at http://localhost:8081
Command "gc run --onDeployed" executed in 5s

Update

Now that the initial deployment is successful, some changes will be made, for instance, let's change the Nginx container version, located at config.js.

Browse the list of Nginx images at https://hub.docker.com/_/nginx

Let's try the version nginx:1.20.0-alpine.

For a preview of the change that will be made, use the plan command:

gc plan
Querying resources on 1 provider: k8s
✓ k8s
✓ Initialising
✓ Listing 7/7
✓ Querying
✓ Namespace 1/1
✓ Service 1/1
✓ Deployment 1/1
┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 1 Deployment from k8s │
├─────────────────────────┬──────────┬──────────────────────────────────────────────────────────────────────────────┤
│ Name │ Action │ Data │
├─────────────────────────┼──────────┼──────────────────────────────────────────────────────────────────────────────┤
│ myapp::nginx-deployment │ UPDATE │ added: │
│ │ │ deleted: │
│ │ │ updated: │
│ │ │ spec: │
│ │ │ template: │
│ │ │ spec: │
│ │ │ containers: │
│ │ │ 0: │
│ │ │ image: nginx:1.20.0-alpine │
│ │ │ │
└─────────────────────────┴──────────┴──────────────────────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Plan summary for provider k8s │
├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ DEPLOY RESOURCES │
├────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────┤
│ Deployment │ myapp::nginx-deployment │
└────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────┘
? Are you sure to deploy 1 resource, 1 type on 1 provider? › (y/N)

Notice this time the action is not CREATE but UPDATE. gc fetched the live resources from the kubernetes-api-server, compared them with the target resources defined in the code, and has found out the deployment needs to be updated.

Now we can apply the change:

gc apply

The updated Nginx image should be up and running.

Let's double-check the state of the Nginx deployment, filtering by type and name

gc list -t Deployment --name nginx-deployment
Listing resources on 1 provider: k8s
✓ k8s
✓ Initialising
✓ Listing 6/6
┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ 1 Deployment from k8s │
├─────────────────────────┬──────────────────────────────────────────────────────────────────────────────────┬──────┤
│ Name │ Data │ Our │
├─────────────────────────┼──────────────────────────────────────────────────────────────────────────────────┼──────┤
│ myapp::nginx-deployment │ metadata: │ Yes │
│ │ name: nginx-deployment │ │
│ │ namespace: myapp │ │
│ │ uid: 7c9bf366-cbf4-47d9-a7b7-e3da900b75dc │ │
│ │ resourceVersion: 7111 │ │
│ │ generation: 2 │ │
│ │ creationTimestamp: 2021-04-28T19:51:37Z │ │
│ │ labels: │ │
│ │ app: nginx-label │ │
│ │ annotations: │ │
│ │ CreatedByProvider: k8s │ │
│ │ ManagedBy: GruCloud │ │
│ │ Name: nginx-deployment │ │
│ │ deployment.kubernetes.io/revision: 2 │ │
│ │ projectName: @grucloud/example-k8s-tuto1 │ │
│ │ stage: dev │ │
│ │ spec: │ │
│ │ replicas: 1 │ │
│ │ selector: │ │
│ │ matchLabels: │ │
│ │ app: nginx-label │ │
│ │ template: │ │
│ │ metadata: │ │
│ │ creationTimestamp: null │ │
│ │ labels: │ │
│ │ app: nginx-label │ │
│ │ spec: │ │
│ │ containers: │ │
│ │ - name: nginx │ │
│ │ image: nginx:1.20.0-alpine │ │
│ │ ports: │ │
│ │ - containerPort: 80 │ │
│ │ protocol: TCP │ │
│ │ resources: │ │
│ │ terminationMessagePath: /dev/termination-log │ │
│ │ terminationMessagePolicy: File │ │
│ │ imagePullPolicy: IfNotPresent │ │
│ │ restartPolicy: Always │ │
│ │ terminationGracePeriodSeconds: 30 │ │
│ │ dnsPolicy: ClusterFirst │ │
│ │ securityContext: │ │
│ │ schedulerName: default-scheduler │ │
│ │ strategy: │ │
│ │ type: RollingUpdate │ │
│ │ rollingUpdate: │ │
│ │ maxUnavailable: 25% │ │
│ │ maxSurge: 25% │ │
│ │ revisionHistoryLimit: 10 │ │
│ │ progressDeadlineSeconds: 600 │ │
│ │ status: │ │
│ │ observedGeneration: 2 │ │
│ │ replicas: 1 │ │
│ │ updatedReplicas: 1 │ │
│ │ readyReplicas: 1 │ │
│ │ availableReplicas: 1 │ │
│ │ conditions: │ │
│ │ - type: Available │ │
│ │ status: True │ │
│ │ lastUpdateTime: 2021-04-28T19:51:39Z │ │
│ │ lastTransitionTime: 2021-04-28T19:51:39Z │ │
│ │ reason: MinimumReplicasAvailable │ │
│ │ message: Deployment has minimum availability. │ │
│ │ - type: Progressing │ │
│ │ status: True │ │
│ │ lastUpdateTime: 2021-04-28T20:03:08Z │ │
│ │ lastTransitionTime: 2021-04-28T19:51:37Z │ │
│ │ reason: NewReplicaSetAvailable │ │
│ │ message: ReplicaSet "nginx-deployment-675bd9f4f7" has successfully progre… │ │
│ │ │ │
└─────────────────────────┴──────────────────────────────────────────────────────────────────────────────────┴──────┘


List Summary:
Provider: k8s
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ k8s │
├────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────┤
│ Deployment │ myapp::nginx-deployment │
└────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────┘
1 resource, 5 types, 1 provider
Command "gc list -t Deployment --name nginx-deployment" executed in 0s

Great, as expected, the new version has been updated.

Destroy

To destroy the resources allocated in the right order:

gc destroy
┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Destroy summary for provider k8s │
├────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────┤
│ Namespace │ myapp │
├────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ Service │ myapp::nginx-service │
├────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────┤
│ Deployment │ myapp::nginx-deployment │
└────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────┘
✔ Are you sure to destroy 3 resources, 3 types on 1 provider? … yes
Destroying resources on 1 provider: k8s
✓ k8s
✓ Initialising
✓ Destroying
✓ Namespace 1/1
✓ Service 1/1
✓ Deployment 1/1
✓ default::onDestroyed
3 resources destroyed, 3 types on 1 provider
Running OnDestroyedGlobal resources on 1 provider: k8s
Command "gc d" executed in 1m 17s

At this stage, all the Kubernetes resources should have been destroyed. We could try to run gc destroy command again, nothing should be destroyed or deployed:

gc d
Find Deletable resources on 1 provider: k8s
✓ k8s
✓ Initialising
✓ Listing 7/7

No resources to destroy
Running OnDestroyedGlobal resources on 1 provider: k8s
Command "gc d" executed in 0s

As expected, the destroy command is idempotent.

Debugging

A benefit of using a general-purpose programming such as Javascript, is debugging. Thanks Visual Studio Code for providing such an easy way to debug Javascript applications.

This example contains a vs code file called launch.json, which defines various debug targets for gc apply, gc destroy and so on.

Conclusion

This tutorial described how to deploy, list, and destroy Kubernetes manifests from Javascript code. In this case, a namespace, a service, and a deployment.

What's next? Let's see how to deploy a full stack application on minikube.

Ready to try Kubernetes on EKS, the Amazon Elastic Kubernetes Service? Have a look at the project full stack application on EKS.

Maybe you prefer using kops to set up your cluster? The tutorial Setup Kops on AWS with Grucloud explains how to automate the kops setup

Are you looking to install the cert manager, web ui dashboard, prometheus, and more? Browse the GruCloud K8s Modules and find out how to install and use these npm packages into your code.