F# Suave app on dotnet core on Kubernetes on Google Cloud
I haven't been doing that much F# dotnet core development, but I think it was time for me to try it out. One of the scenarios that I think it will be used a lot is on kubernetes. I choose to run it on google cloud so I didn't have to set up the infrastructure myself. I could probably have used Azure as well since they now have support for it in preview, but I think that google's implementation looks more mature and easier to use from the command line. So let get this little tutorial started.
Creating the application
Before we need start we need to install the latest dotnet core bits, which we find here: https://www.microsoft.com/net/download/core. Clicking on Current tab we will find the latest bits (1.1.0 as of this moment). With dotnet installed we can get going.
Create project
The first is to create the project. Just navigate to a folder where you want your project and run this command to create a new F# project:
dotnet new -l F#
Note that the folder name will be the name of the project, in my case it is suavecore and that will also be the name of the dll file created.
Update references
I made some minor changes to the project.json file:
{
"version": "1.0.0-*",
"buildOptions": {
"debugType": "portable",
"emitEntryPoint": true,
"compilerName": "fsc",
"compile": {
"includeFiles": [
"Program.fs"
]
}
},
"dependencies": {
"Microsoft.FSharp.Core.netcore": "1.0.0-alpha-*",
"Suave": "2.0.0-rc2"
},
"tools": {
"dotnet-compile-fsc": "1.0.0-preview2.1-*"
},
"frameworks": {
"netcoreapp1.0": {
"dependencies": {
"Microsoft.NETCore.App": {
"type": "platform",
"version": "1.1.0"
},
"Microsoft.FSharp.Core.netcore": "1.0.0-alpha-161111"
}
}
}
}
I basically changed to the latest version of all the packages and added a reference to Suave.
After the update we need to run
dotnet restore
to install the dependencies.
Implementing the application
The application implemented is really simple, it is a basic Hello World application that also prints the host name. It is a single file application and it all fits in Program.fs:
open Suave
open System.Net
type CmdArgs = { IP: System.Net.IPAddress; Port: Sockets.Port }
[<EntryPoint>]
let main argv =
// parse arguments
let args =
let parse f str = match f str with (true, i) -> Some i | _ -> None
let (|Port|_|) = parse System.UInt16.TryParse
let (|IPAddress|_|) = parse System.Net.IPAddress.TryParse
//default bind to 127.0.0.1:8083
let defaultArgs = { IP = System.Net.IPAddress.Loopback; Port = 8083us }
let rec parseArgs b args =
match args with
| [] -> b
| "--ip" :: IPAddress ip :: xs -> parseArgs { b with IP = ip } xs
| "--port" :: Port p :: xs -> parseArgs { b with Port = p } xs
| invalidArgs ->
printfn "error: invalid arguments %A" invalidArgs
printfn "Usage:"
printfn " --ip ADDRESS ip address (Default: %O)" defaultArgs.IP
printfn " --port PORT port (Default: %i)" defaultArgs.Port
exit 1
argv |> List.ofArray |> parseArgs defaultArgs
let log x = printfn "%A" x; x
let getHostName() =
Dns.GetHostName()
// start suave
startWebServer
{ defaultConfig with
bindings = [ HttpBinding.create HTTP args.IP args.Port ] }
(Successful.OK (sprintf "Hello world: %s" (getHostName())))
0
That is all we need to try the application. If you run
dotnet run
you will start the application and you can now pay a visit to http://localhost:8083.
Publishing the application
The last thing we need to do is to publish the application, this will create the bits that we will add to our docker container later on. Run
dotnet publish -C Release
to publish the application to bin/Release/netcoreapp1.0/publish. If you navigate to that folder it is now possible to run the publish commands by executing
dotnet suavecore.dll
This will start the web server and you can now navigate to http://localhost:8083 again. Note that suavecore is the name of my project, if you have a different name of the project folder your name might differ.
Building the container
To be able to run this on kubernetes later on we will create a docker container. I have docker beta for OSX installed to build and try out the container. If you are following along I assume you to have that installed.
Creating the Dockerfile
The Dockerfile is based on the official dotnet core image from microsoft and looks like this:
FROM microsoft/dotnet:core
COPY ./bin/Release/netcoreapp1.0/publish /app
WORKDIR /app
EXPOSE 8083
ENTRYPOINT ["dotnet", "suavecore.dll"]
It is quite straightforward what is going on. We base our image on the one from Microsoft as mentioned, then we copy our published app to the app folder in the container. We expose port 8083 to be able to access it from the outside and lastly we set the entry point to the command to start the application.
Building the container
Building a container is as simple as
docker build . -t mastoj/suavecore:v1.5
The container is tagged with the name of my repo for this image on docker hub and a version number so we can access the correct version when publishing to kubernetes later on.
Testing the container
Before we publish the container it might be smart to try it out locally first. So to create a container of the newly created image we run
docker run -p=8083:8083 --name suave mastoj/suavecore:v1.5 --ip 0.0.0.0
The command above will start a running container of our image and name it suave. It will also map port 8083 on our local machine to port 8083 on the container. Lastly we will pass the arguments --ip 0.0.0.0 to the application to tell it to listen all request no matter what the IP is.
Again you can try http://localhost:8083, but this time you should get a little bit different response since the host name of the container is probably not the same as your machine.
Publish the container
We are now ready to publish the container. For this tutorial we are using a public repo to keep things simple.
docker push mastoj/suavecore:v1.5
This will upload the image to docker hub and making it accessible to the public.
Setting up google cloud
We are now ready to proceed to the google cloud and kubernetes part. The goal is to host the application with three replicas running behind nginx using https. To be able to try it out on google cloud you need to sign up and create a project. You also need to install the sdk.
Creating the kubernetes cluster
If you have the sdk installed it is really simple to set up a new basic cluster. To create a cluster named k1 run the following
gcloud container clusters create k1
When you have the cluster up and running you need to install kubectl, which is the CLI tool to work with kubernetes.
gcloud components install kubectl
Now you should be all set to operate your google cloud container cluster, hopefully.
Secrets and configmaps
For nginx to run correctly we need to configure it to use https and where our application is in the cluster.
The first part is generating cert.pem and key.pem files for tls to work. If you have openssl installed you can run:
openssl req -newkey rsa:4096 -nodes -sha512 -x509 -days 3650 -nodes -out cert.pem -keyout key.pem
The result from this have I stored in a folder in the repo: https://github.com/mastoj/suavecore/tree/master/kubernetes/tls. You should probably never publish these files, I'm just doing it for demo purpose.
When we have the files we can create a secret that we will be able to mount in our containers when they run in the cluster. To create a secret you use kubectl
kubectl create secret generic tls-certs --from-file=tls/
We will see later how we access the secrets.
Next step is to add the nginx configuration. The configuration is a file, that we will mount when the container starts. The file is also in repo with the name frontend.conf:
upstream hello {
server hello.default.svc.cluster.local;
}
server {
listen 443;
ssl on;
ssl_certificate /etc/tls/cert.pem;
ssl_certificate_key /etc/tls/key.pem;
location / {
proxy_pass http://hello;
}
}
This configuration file will configure nginx to listen to 443 with ssl enabled and route the traffic to hello.default.svc.cluster.local, which is where our hello nodes will be. You can also see where nginx now expect the secrets to be located.
With this done we can now continue on to creating the deployments and services.
Creating the deployments
If you want to know exactly what a deployment is you should read this: http://kubernetes.io/docs/user-guide/deployments/#what-is-a-deployment. I want go into details about all these concepts, just show you how to configure it.
The frontend deployment
The deployments/frontend.yaml defines the deployment for the frontend, which is the nginx part of our application.
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: frontend
spec:
replicas: 1
template:
metadata:
labels:
app: frontend
track: stable
spec:
containers:
- name: nginx
image: "nginx:1.9.14"
lifecycle:
preStop:
exec:
command: ["/usr/sbin/nginx","-s","quit"]
volumeMounts:
- name: "nginx-frontend-conf"
mountPath: "/etc/nginx/conf.d"
- name: "tls-certs"
mountPath: "/etc/tls"
volumes:
- name: "tls-certs"
secret:
secretName: "tls-certs"
- name: "nginx-frontend-conf"
configMap:
name: "nginx-frontend-conf"
items:
- key: "frontend.conf"
path: "frontend.conf"
In the file we first define that it is a deployment and some metadata. The interesting part is the containers part where we define that we will use nginx and also add a correct shutdown command when the container is stopped. In the volumes section we define that we want access to our secret named tls-certs, and the configMap named nginx-frontend-conf. For the configMap we are only interested in the key frontend.conf and we are going to name that file the same as the key. When we have defined the volumens we can reference them in the volumeMounts section of the file and define where they should go. That is it for the frontend deployment.
To create our deployment in the cluster run
kubectl create -f deployments/frontend.yaml
Application deployment
The application deployment is defined in deployments/hello.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: hello
spec:
replicas: 3
template:
metadata:
labels:
app: hello
track: stable
spec:
containers:
- name: hello
image: "mastoj/suavecore:v1.5"
args: ["--ip", "0.0.0.0"]
ports:
- name: http
containerPort: 8083
This is a little bit simple deployment. What interesting here is the number of replicas, 3, and that we are referencing the docker image we have created earlier. To create the deployment of the application we need to run
kubectl create -f deployments/hello.yaml
Creating services
Next up is creating services, which allow us to balance the load requests between our nodes and expose the application to the public.
The frontend service
The services/frontend.yaml is what defines the service for the frontend.
kind: Service
apiVersion: v1
metadata:
name: "frontend"
spec:
selector:
app: "frontend"
ports:
- protocol: "TCP"
port: 443
targetPort: 443
type: LoadBalancer
We create the service with
kubectl create -f services/frontend.yaml
When the service is created you can run the command
kubectl get services
to check the status. There you will see the public ip when it is available.
The hello application service
The definition in services/hello.yaml is similar to the frontend definition
kind: Service
apiVersion: v1
metadata:
name: "hello"
spec:
selector:
app: "hello"
ports:
- protocol: "TCP"
port: 80
targetPort: 8083
The difference here is that we don't have the LoadBalancer part, which means our app will NOT be accessible from the outside, you have to go through our frontend. We also routes the traffic from the service port 80 to the container port 8083 which our containers use. Creating the service is as easy as for the frontend
kubectl create -f services/hello.yaml
Win
Everything should now be configured and up and running. You can find the public IP that you should be able to navigate to be executing
kubectl get services
Remember that it is https://<your public ip>, and the cert we are using is self signed so you will probably get a warning about that as well.
The source code for everything is available here: https://github.com/mastoj/suavecore/
If you have any comments or questions, feel free to post them at the comment section.