In the previous post, I created a simple port scanning tool with GO. Now it's time to run this tool in docker and scale/manage it with k8s!
First, I'll create a
Dockerfile
with multistage build to reduce image size:# stage 1
FROM golang:1.15.6 as builder
WORKDIR /app
# fetch dependeicnies first as they're not changing often and will get cached
COPY ./go.mod ./go.sum ./
RUN go mod download
# copy source to working dir of a container
COPY . .
# build the app
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server cmd/server/main.go
# stage 2
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8080
ENTRYPOINT ["./server"]
Let's build and run the scanner. From the root directory of the project:
$ # build the image first
$ docker build -t portscanner .
$ # now that image is built, run it
$ docker run --name portscanner --rm -p 8080:8080 portscanner
$ # logs:
$ INFO 2021/01/28 15:32:15 starting server at :8080
Port scanner has started on port 8080, and I've exposed the same port to the host with
-p 8080:8080
. I'll use that for testing. Since I know that the application is available on port 8080, my port scanning tool should be able to detect itself:$ curl -X GET http://localhost:8080/open-ports\?domain\=127.0.0.1\&toPort\=9000
$ # output:
{
"from_port": 0,
"to_port": 9000,
"domain": "127.0.0.1",
"open_ports": [
8080
]
}
$ # with missing query params
$ curl -X GET http://localhost:8080/open-ports | jq
$ # output:
{
"Result": "ERROR",
"Cause": "INVALID_REQUEST",
"InvalidFields": {
"domain": [
"can't be blank"
],
"toPort": [
"can't be blank",
"invalid decimal string"
]
}
}
It's all working as expected. Let's take it one step further and run it in Kubernetes! To do so, I'll use
minikube
and kubectl
. You can find OS-specific installation instructions for minikube - here and for kubectl - here. When all tools are installed, I'll run minikube start
and allow it some time to start with default params. Minikube runs a separate VM, and that VM doesn’t have access to the local Docker registry and hence no access to the previously built image. To fix this issue, I have to switch to minikube environment:$ eval $(minikube docker-env)
Now I'll build the image again, but this time, inside minikube env:
$ docker build -t portscanner .
$ # list all images to see if portscanner is there
$ docker image ls
Now that image is accessible to minikube; I'll create deployment:
$ kubectl create deployment portscanner --image=portscanner
This tells Kubernetes to create a deployment named portscanner and use a previously built portscanner image for it. There's few handy commands to check the status of deployment and pods:
$ # to check deployment status
$ kubectl get deployment portscanner
$ # to get list of pods
$ kubectl get pods
Both of the above have 0/1 in READY status, and
get pods
has ImagePullBackOff STATUS. From get pods
command, I can get the pod name and investigate what's wrong:kubectl describe pod portscanner-xxxx...
The above command has a more informative output where I can see that although the image is accessible in minikube VM, deployment still tries to pull it from the cloud. To fix this, I need to edit the default deployment configuration:
kubectl edit deployment portscanner
The above will open
.yaml
configuration file in your default editor. This part spec.template.spec.containers.0.imagePullPolicy
says Always
which means that no matter what, deployment will always try to pull image from the cloud. Change Always
to IfNotPresent
, save the file and check deployment status once again:kubectl get deployment portscanner
This time the Ready status is 1/1! Success! Now portscanner is running in Kubernetes. There's still one small issue, though. I can't access it. To get access to my tool, I'll have to create a service of a type
LoadBalancer
that will take incoming requests and distribute them to pods:kubectl expose deployment portscanner --type=LoadBalancer --port=8080
In most other environments, when you use a Kubernetes
LoadBalancer
, it will provision a load balancer external to your cluster, for example, an Elastic Load Balancer (ELB) in AWS. That's not the case when running it locally. Luckily I can simulate the connection with minikube service:minikube service portscanner
This will print out connection details and try to open the browser with the given url. This url can be used to issue curl commands to the port scanner, just like before. Let's try it out:
$ curl -X GET http://{your_service_ip}:{your_service_port}/open-ports\?domain\=127.0.0.1\&toPort\=9000 | jq
$ # Output:
{
"from_port": 0,
"to_port": 9000,
"domain": "127.0.0.1",
"open_ports": [
8080
]
}
Right, now I have 1 pod running and serving my application. So, for my last trick, I'll scale the deployment to 4 pods:
kubectl scale deployments/portscanner --replicas=4
Now, if I run
kubectl get pods
, I'll see that there are 4 pods running. To get log output for all pods:
kubectl logs -l app=portscanner -f
I hope this will get you going and help to build some exciting things. You can find the source code here.