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
.
- A Kubernetes cluster to learn on such as Kubernetes on Docker or minikube.
- A basic knowledge of using kubectl
- Ingress enabled for your clusterW
- dotnet SDK installed
- 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.
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 ConfigMap
s 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).
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
|
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
|
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