Video Thumbnail for Lesson
7.1: Authoring Kubernetes Manifests

Authoring Kubernetes Manifests

Transcript:

Once we have our container images built and pushed, we need to translate the different components of our architecture into a set of Kubernetes resources that we can deploy onto the cluster. For the various stateless components, those would include our two backend APIs, as well as the server for our React client files, and the load generator. None of those have any state that we need to store in the cluster, so those we can use deployments for. Our database represents a stateful application and therefore should be deployed as a stateful set. In this case, we're going to use a Helm chart to do so. We'll deploy services in front of each of our deployments and stateful set to provide stable network endpoints, and then we'll deploy an ingress controller and ingress routes to route traffic from outside the cluster to the appropriate services inside the cluster. Finally, we'll add config maps and secrets for any configuration we want to decouple from our application deployment. Let's jump in, define these resources, and apply them to our cluster. All right, let's navigate to our 07 deploying demo application directory. As you can see, I've got a number of tasks defined. For each service, I have an apply command that's going to run a kubectl apply on all the underlying resources. I also have a common subdirectory, which is going to deploy my ingress controller. In this case, I'm going to deploy an ingress controller called traffic just to show another example of a different ingress controller. And then for Postgres, I'm going to install it via Helm chart as well as apply my initial database migration. Let's start at the bottom of that architecture diagram and work our way up. So we'll start with Postgres. In this case, we'll run the postgresql install postgres command. It's going to add the Helm repo and then call a Helm upgrade install on version 15.3.2 using the set command to pass in auth.postgres password to the value that I want it to. This dash dash set command is another option. Instead of passing in a values file, you can set specific values via the command line in this way. For a credential like this, where you don't necessarily want to store it in a file, passing it at deploy time can be a good option. You can see that was deployed into the Postgres namespace. And the Helm chart deployed a relatively small, but it'll work for our purposes, persistent volume claim associated with that stateful set. Now, in order to run the database migration, the type of Kubernetes resource that makes the most sense is a job. It is a task which we want to run one time to completion. And so what I did here is I defined a Kubernetes job. I'm calling it the dbMigrator. I'm going to get in the demo app namespace. And so before I can deploy this, I'll need to create the demo app namespace. I have this common apply namespace, which is going to apply the namespace.yaml. And that namespace.yaml contains demo app. All the services are going to go into the demo app namespace, except for Postgres, which is going into its own namespace. Now that namespace exists, let's take a look at the template here for the job. It's going to have one container, which I'm naming migrate. It's using that container that I built and pushed, which has all of my migration scripts. In this case, it's just that one. In this case, it's just this one create users table script. And then the args for that migrator command line tool, I'm giving a path in my container. This is where the migrations live, slash app slash migrations. And then I'm passing in a database URL as an environment variable containing the credentials and host name for my database. I'm also turning off SSL mode because I'm not running SSL on the database. I also want to run the up migrations rather than the down migrations. In order to get this database URL into the container, I'm passing in a reference to a secret named db password, which I've defined in this file. Here I have a secret named db password in the namespace demo app, and I'm using string data to define my database URL. This was the password that I created. If I look at the services in the Postgres namespace, I've got a cluster IP service and a headless cluster IP service. In this case, I want to address the normal cluster IP service. This is the name of the service, the namespace, service cluster local on part 5432 in the Postgres database. Awesome. Let me apply both of those files. It's starting with the secret, and then it's creating the job. Let's look at the pods. It's already completed. We've got one of one completions. Great. And let's look at the logs from that pod. We created our users table in 14 milliseconds. Awesome. Now we've got our database running, and the initial migration has run. This migration job, we would want to run every time we have a new migration before we make the corresponding application deployments. Now I'm going to move on and deploy the things in my common subdirectory. We already deployed the namespace. However, I want to also deploy the traffic ingress controller. I can do that with t common deploy traffic. I'm adding my helm repository, and then I'm running my helm upgrade install command, naming my release traffic from this helm chart using version 28.8.0. I'll look at everything in the traffic namespace. We can see it created a deployment, which in turn created a replica set, which in turn created a pod. We also have a load balancer type service, which will deploy. In this case, I'm running in my GKE cluster, so that will deploy a Google Cloud load balancer. Calling that command again, the external IP has been set up. There's one additional common element that is shared between my two APIs, and that is a middleware. The middleware is a custom resource that is defined by traffic, which enables us to do things like strip a path prefix from the incoming requests. For example, if I navigate to whatever my domain name is and go to slash API slash node, I'm going to define an ingress to route that traffic to my Node.js application. However, my Node.js application is not expecting to have that prefix in the request. So this middleware allows me to strip that out, so that by the time it hits my Node API, it'll look as though it's coming from the root path. So I've created that middleware. Let's now take a look at the Go API resources. We've got a deployment. This will look very familiar to the deployments that we learned in module 4. I'm calling it API Golang. It's in my demo app namespace. I'm including this app label just to say that this resource is associated with my Golang API. It's going to have a single replica, and then I'm specifying my selector, which this label needs to match the label on my container as specified here. The container image is the one from Docker Hub that I built and pushed. I'm specifying to listen on port 8000. I'm pulling in my database secret from a secret named API Golang database URL, which is defined alongside it here. I'm specifying a container port listening on port 8000 and a readiness probe for the kubelet to ping to check if my application is healthy. I'm specifying the resources that this application is requesting. In this case, 100 megabytes of memory and 50 millicores of CPU. I'm limiting the privileges of the pod, which is good from a security perspective, and that should be about it. I also have a service defined. This is just a standard cluster IP service listening on port 8000 and passing those requests to port 8000 in my pod using that same selector that deployment is using in order to connect it to those underlying pods. If I run T API Golang apply, it's going to call kubectl apply-f on that directory. If you do a kubectl apply on a directory, it's going to try to apply every YAML file within that directory. I've created my deployment, my ingress route, my secret, and my service. If we look at the pods in the demo app, the DB migrator is the one we created a few minutes ago, and then my Golang API is up and running. If we look at my ingress route, you'll notice this is a little different than the ingress that we deployed in module 4. In this case, traffic has defined their own custom resource to avoid having to use those annotations for all the custom behaviors. Instead, you can define this ingress route and traffic will interpret it to determine where network requests should be routed. This is the domain that it's listening for, and this is the path prefix that it's using to decide if a request should go to my Go API. I'm using that common middleware that I deployed to strip out those prefixes, and then it's routing it to the service that I've defined alongside this. One thing I just noticed here is that this port should actually be 8,000. I had it going to 8080, but I've updated my application to be listening on port 8,000. I'll save that and then apply. Just to validate that things are working, I'm going to port forward to the service. Then we can access it on localhost 8,000. There we go. We've got our API responding. I can then take this external IP address, go over to my DNS provider of CloudFlare, update my A record to point to that IP address, and then if we navigate to kubernetescourse.devopsdirector.com slash apigolang, we can see the traffic is getting routed to our API. Let's move on to our Node.js application. The resources are going to look pretty much identical to the Golang one. The main difference being that we're running our API node container image, and we're listening on a different port. Let's port 3,000. We have a secret, which has the same contents. It's just pointing us with the credentials to the proper service inside the cluster. And our ingress route, instead of using the path prefix apigolang, it's using the path prefix apinode. We go ahead and apply this. We can see our pod being created. It's now running. Let's try and access it. Okay. We see the Node API giving us a response now. For our client React app, it's going to use that nginx container that we built, which contains the output of our npm build process. Again, it should all look very familiar. The main difference here being we're using the specific container image associated with this application. I'm listening on port 8080. I've got my readiness probe set up. I've got my resource requests and my security context set up. I'm also mounting in this config map as a volume. So this config map is mounted in as a volume and then is located at etsy nginx-conf-d, which is the default location that nginx is going to look for a configuration file. And then within that config map, I'm defining that it should listen on port 8080. I have my health check route here. These two locations are not actually necessary here because the routing to my backend APIs is happening at the ingress layer. And then this tells us where the files in the container for my React client live, and that should be sufficient. So we've created our config map. We've created a deployment and ingress route. So this ingress route is essentially saying, match all traffic to this domain and is not specifying any sort of path prefix. So any request to the root path is going to get routed here. I'm pointing it to my service that's defined right here that's in front of my deployment, and that should be sufficient to get traffic into my application. So if I remove this prefix now, you can see there's my application. The final service that I haven't deployed yet is the load generator. For this, I have a deployment. I'm specifying the labels accordingly to indicate that it is its own application. I'm using the container image here, specifically the version that I built and pushed, pointing it to the cluster IP service for my node API, and specifying a half-second delay between each call. If we wanted to, we could move these types of configurations into a config map. In a simple case like this, it's okay to define them directly here in the deployment. If there were many different environment variables, it might make sense to extract them out and put them into a config map. I've got my resources defined as well as my security context. Now this is an important one. On Docker Hub, I have the registry containing this container image specified as private. I did that so that I could demonstrate how image pull secrets work and show you first it's going to fail to pull, then we'll create an image pull secret, and then it will succeed. Let's start by commenting out this image pull secrets, and we'll deploy those resources. Now I've got this warning here, warning you about the fact that this is a private container image repository. If I now look at my pods, this pod is now in an image pull back-off state, meaning it tried to pull the image and it failed. If we describe it, you can see it failed to pull. That's because it's a private container image registry. In order to solve that, we need to do two things. We need to uncomment this. We need to create a secret containing the proper credentials that this can use. In order to do that, I can use this task, load generator Python create image pull secret, but it's expecting me to specify the following three environment variables, docker username, docker email, and docker password. I'm going to specify those and cut that out. I'll rerun this command. Now that I've exported those environment variables, this command can succeed. It's calling kubectl create secret in my demo app namespace. It is of type docker registry. I'm naming it docker config json, and then passing in all of the values that it needs. With our docker config json secret created, we can now reapply our load generator configuration. We can do k get pods, and we can see it was able to successfully pull and run our container image. Now we have our load generator pod. Let's check the logs of it. As you can see, it's just requesting over and over on that internal cluster IP. If we go back to our website here, we'll expect the request count for the node API to grow much faster than that of the go API. It's set to delay a half a second between each one, so it's not going to climb super rapidly, but you can see those requests are happening in the background. It's at 182, and now it jumps up to 200. To recap, we created our four stateless applications using deployments. Three of them have cluster IP services in front of them. We've got our traffic ingress controller installed, routing traffic via those ingress routes. We've got our Postgres database that we installed via the Helm chart, and then we performed our database migrations via a job, and those credentials for the database are stored within secrets, one for each service. The ingress controller provisioned an external load balancer for which we were able to create a public DNS record and now access the application. Because the load generator Python container image registry was private, we had to create that image pull secret in order to pull it successfully, and now everything is running within the cluster.