Overboard


Running a script in a Pod

In this tutorial we will look at running a script in a Pod, running it as a Job and then finally running that Job as a CronJob.

Topics covered

Requirements

  1. A Kubernetes cluster to learn on such as Kubernetes on Docker or minikube.
  2. A basic knowledge of using kubectl
  3. Ingress enabled for your clusterW
  4. dotnet SDK installed
  5. Optionally, any IDE that supports F# (Visual Studio Code, IntelliJ Rider, Visual Studio, NeoVim)

Visual Studio Code with the Ionide is a great choice. See Setup your environment for more details.

Shipping a script in a Deployment

In this first step we are going to show what is required to bundle a script into a Deployment and run it in a Pod.

Overboard had a nice addition on ConfigMaps that allows you to add a file from the machine creating the config.

The file we will be adding to the ConfigMap is called .write-date.fsx and has the following contents:

Note: The fsx file has a . at the start of it's name purely so it is ignored by this documentation system. It is not required you place a . at the start of your filename.

open System
Console.WriteLine($"Hello from FSX script at {DateTimeOffset.UtcNow}")

To leverage this script we create the following resources:

#r "nuget:Overboard"

open System
open Overboard
open Overboard.Common
open Overboard.Workload
open Overboard.Storage

let scriptPath = IO.Path.Combine(".write-date.fsx")
let labels = ["app","script"]
let k8sDeploymentConfig = 
    k8s {
        // config map containing write-date.fsx
        configMap {
            _name "script-configmap"
            add_file ("write-date.fsx", scriptPath)
        }
        // a deployment
        deployment {
            _name "script-deployment"
            add_matchLabels labels
            pod {
                _name "script-pod"
                _labels labels
                // container with an image with dotnet that runs the script 
                container {
                    name "script-runner"
                    image "mcr.microsoft.com/dotnet/sdk:7.0-alpine"
                    command ["dotnet"]
                    args ["fsi"; "./.write-date.fsx"]
                    workingDir "/scripts"
                    volumeMount {
                        name "script-volume"
                        mountPath "/scripts"
                    }
                }
                // configMap is mounted as a volume
                configMapVolume {
                    name "script-volume"
                    configName "script-configmap"
                    defaultMode 0700
                }
            }
        }
    }

// write the config YAML file
let configDir = __SOURCE_DIRECTORY__
KubeCtlWriter.toYamlFile k8sDeploymentConfig (IO.Path.Combine( configDir, "script-deployment.yaml"))

See how in the ConfigMap definition we use the add_file operation.

add_file ("write-date.fsx", scriptPath)

The first element of the tuple is the name of the file as it will appear in the ConfigMap (which will be mounted as a file in the volume). The second element is the path to the file on the machine that will be building this config. In this case, your machine. In a production environment this may be a CI build agent.

Let's test this Deployment out:

kubectl apply -f .\script-deployment.yaml

Here is an example of what the run can look like

> kubectl get pods
NAME                                 READY   STATUS      RESTARTS      AGE
script-deployment-655f668d57-g777w   0/1     Completed   5 (85s ago)   2m53s

> kubectl logs script-deployment-655f668d57-g777w
Hello from FSX script at 11/20/2022 11:17:40 +00:00

We can see from the logs that the script has been run (use your pod name when fetching the logs).

Using a Job

We can create a Deployment and run a script like we did but Kubernetes has a resource that is a better match for this kind of task. A Kubernetes Job is specifically for a single execution workload like we a re doing here. One reason to prefer a Job is the cleanup of the pods after the work is done.

You could swap out the deployment for a Job leaving the Pod and ConfigMap unchanged but Overboard has a simpler way to use a Job. In the Overboard.Extras namespace is a high-level resource called fsJob which takes a F# script file as am entryPoint and will create a Job from it.

open Overboard.Extras

let fsJobConfig = fsJob {
        entryPoint "./.write-date.fsx"
    }

KubeCtlWriter.toYamlFile fsJobConfig (IO.Path.Combine( configDir, "script-job.yaml"))

This can be a very simple way to run one-off jobs.

> kubectl apply -f .\script-job.yaml
> kubectl get pods
script-write-date-job-4gstg          0/1     Completed          0               2m12s
> kubectl logs script-write-date-job-4gstg
Hello from FSX script at 11/20/2022 19:52:29 +00:00

Creating a CronJob

If we had manually created a Job we could use that job to create a CronJob. With the fsJob abstraction through all you need do is add a Cron schedule and your Job will become a CronJob.

let fsCronJobConfig = fsJob {
        entryPoint "./.write-date.fsx"
        schedule "* * * * *"
    }

KubeCtlWriter.toYamlFile fsCronJobConfig (IO.Path.Combine( configDir, "script-cronjob.yaml"))

Run the get all command to see what is created. Each occurrence of the Job runs in a new Pod.

kubectl apply -f .\script-cronjob.yaml
kubectl get all

Conclusion

In this tutorial we created a Deployment that runs a script once. You then leveraged fsJob abstraction to easily run the same script as a Job. You then easily changed that Job to a CronJob by adding a schedule.

namespace System
type Console = static member Beep: unit -> unit + 1 overload static member Clear: unit -> unit static member GetCursorPosition: unit -> struct (int * int) static member MoveBufferArea: sourceLeft: int * sourceTop: int * sourceWidth: int * sourceHeight: int * targetLeft: int * targetTop: int -> unit + 1 overload static member OpenStandardError: unit -> Stream + 1 overload static member OpenStandardInput: unit -> Stream + 1 overload static member OpenStandardOutput: unit -> Stream + 1 overload static member Read: unit -> int static member ReadKey: unit -> ConsoleKeyInfo + 1 overload static member ReadLine: unit -> string ...
<summary>Represents the standard input, output, and error streams for console applications. This class cannot be inherited.</summary>
Console.WriteLine() : unit
   (+0 other overloads)
Console.WriteLine(value: uint64) : unit
   (+0 other overloads)
Console.WriteLine(value: uint32) : unit
   (+0 other overloads)
Console.WriteLine(value: string) : unit
   (+0 other overloads)
Console.WriteLine(value: float32) : unit
   (+0 other overloads)
Console.WriteLine(value: obj) : unit
   (+0 other overloads)
Console.WriteLine(value: int64) : unit
   (+0 other overloads)
Console.WriteLine(value: int) : unit
   (+0 other overloads)
Console.WriteLine(value: float) : unit
   (+0 other overloads)
Console.WriteLine(value: decimal) : unit
   (+0 other overloads)
Multiple items
[<Struct>] type DateTimeOffset = new: dateTime: DateTime -> unit + 5 overloads member Add: timeSpan: TimeSpan -> DateTimeOffset member AddDays: days: float -> DateTimeOffset member AddHours: hours: float -> DateTimeOffset member AddMilliseconds: milliseconds: float -> DateTimeOffset member AddMinutes: minutes: float -> DateTimeOffset member AddMonths: months: int -> DateTimeOffset member AddSeconds: seconds: float -> DateTimeOffset member AddTicks: ticks: int64 -> DateTimeOffset member AddYears: years: int -> DateTimeOffset ...
<summary>Represents a point in time, typically expressed as a date and time of day, relative to Coordinated Universal Time (UTC).</summary>

--------------------
DateTimeOffset ()
DateTimeOffset(dateTime: DateTime) : DateTimeOffset
DateTimeOffset(dateTime: DateTime, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(ticks: int64, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, offset: TimeSpan) : DateTimeOffset
DateTimeOffset(year: int, month: int, day: int, hour: int, minute: int, second: int, millisecond: int, calendar: Globalization.Calendar, offset: TimeSpan) : DateTimeOffset
property DateTimeOffset.UtcNow: DateTimeOffset with get
<summary>Gets a <see cref="T:System.DateTimeOffset" /> object whose date and time are set to the current Coordinated Universal Time (UTC) date and time and whose offset is <see cref="F:System.TimeSpan.Zero" />.</summary>
<returns>An object whose date and time is the current Coordinated Universal Time (UTC) and whose offset is <see cref="F:System.TimeSpan.Zero" />.</returns>
namespace Overboard
namespace Overboard.Common
namespace Overboard.Workload
namespace Overboard.Storage
val scriptPath: string
namespace System.IO
type Path = static member ChangeExtension: path: string * extension: string -> string static member Combine: path1: string * path2: string -> string + 3 overloads static member EndsInDirectorySeparator: path: ReadOnlySpan<char> -> bool + 1 overload static member GetDirectoryName: path: ReadOnlySpan<char> -> ReadOnlySpan<char> + 1 overload static member GetExtension: path: ReadOnlySpan<char> -> ReadOnlySpan<char> + 1 overload static member GetFileName: path: ReadOnlySpan<char> -> ReadOnlySpan<char> + 1 overload static member GetFileNameWithoutExtension: path: ReadOnlySpan<char> -> ReadOnlySpan<char> + 1 overload static member GetFullPath: path: string -> string + 1 overload static member GetInvalidFileNameChars: unit -> char[] static member GetInvalidPathChars: unit -> char[] ...
<summary>Performs operations on <see cref="T:System.String" /> instances that contain file or directory path information. These operations are performed in a cross-platform manner.</summary>
IO.Path.Combine([<ParamArray>] paths: string[]) : string
IO.Path.Combine(path1: string, path2: string) : string
IO.Path.Combine(path1: string, path2: string, path3: string) : string
IO.Path.Combine(path1: string, path2: string, path3: string, path4: string) : string
val labels: (string * string) list
val k8sDeploymentConfig: K8s
val k8s: K8sBuilder
val configMap: ConfigMapBuilder
custom operation: _name (string) Calls ConfigMapBuilder.Name
<summary> Name of the ConfigMap. Name must be unique within a namespace. https://kubernetes.io/docs/reference/kubernetes-api/common-definitions/object-meta/#ObjectMeta </summary>
custom operation: add_file (string * string) Calls ConfigMapBuilder.AddFileToBinaryData
<summary> Adds a file to the ConfigMap binary data </summary>
val deployment: DeploymentBuilder
custom operation: _name (string) Calls DeploymentBuilder.Name
<summary> Name of the Deployment. Name must be unique within a namespace. https://kubernetes.io/docs/reference/kubernetes-api/common-definitions/object-meta/#ObjectMeta </summary>
custom operation: add_matchLabels ((string * string) list) Calls DeploymentBuilder.MatchLabels
<summary> Add multiple label selectors to the Deployment. </summary>
val pod: PodBuilder
custom operation: _name (string) Calls PodBuilder.Name
<summary> Name of the Pod. Name must be unique within a namespace. https://kubernetes.io/docs/reference/kubernetes-api/common-definitions/object-meta/#ObjectMeta </summary>
custom operation: _labels ((string * string) list) Calls PodBuilder.Labels
<summary> Labels for the Pod </summary>
val container: ContainerBuilder
<summary> A single application container that you want to run within a pod. https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-v1/#Container </summary>
custom operation: name (string) Calls ContainerBuilder.Name
custom operation: image (string) Calls ContainerBuilder.Image
custom operation: command (string list) Calls ContainerBuilder.Command
custom operation: args (string list) Calls ContainerBuilder.Args
custom operation: workingDir (string) Calls ContainerBuilder.WorkingDir
val volumeMount: VolumeMountBuilder
custom operation: name (string) Calls VolumeMountBuilder.Name
custom operation: mountPath (string) Calls VolumeMountBuilder.MountPath
val configMapVolume: ConfigMapVolumeBuilder
custom operation: name (string) Calls ConfigMapVolumeBuilder.Name
custom operation: configName (string) Calls ConfigMapVolumeBuilder.ConfigName
custom operation: defaultMode (int) Calls ConfigMapVolumeBuilder.DefaultMode
val configDir: string
module KubeCtlWriter from Overboard.K8s
val toYamlFile: k8s: K8s -> filePath: string -> ValidationProblem list
namespace Overboard.Extras
val fsJobConfig: K8s
val fsJob: FsharpJobBuilder
custom operation: entryPoint (string) Calls FsharpJobBuilder.EntryPoint
val fsCronJobConfig: K8s
custom operation: schedule (string) Calls FsharpJobBuilder.Schedule