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.