Small app that watches for kubernetes services and endpoints in a target cluster and mirrors them in a local namespace. Can be used in conjunction with coredns in cases where pod networks are reachable between clusters and one needs to be able to route virtual services for remote pods.
Usage of ./semaphore-service-mirror:
-config string
(required)Path to the json config file
-kube-config string
Path of a kube config file, if not provided the app will try to get in cluster config
-label-selector string
Label of services and endpoints to watch and mirror
-log-level string
Log level (default "info")
-mirror-ns string
The namespace to create dummy mirror services in
You can set most flags via envvars instead, format: "SSM_FLAG_NAME". Example:
-config
can be set as SSM_CONFIG
and -kube-config
can be set as
SSM_KUBE_CONFIG
.
If both are present, flags take precedence over envvars.
The only mandatory flag is -config
to point to a json formatted config file.
Label selector and mirror namespace must also be set, but there is the option to
do this via the json config (more details in the next section below). Flags will
take precedence over static configuration from the file.
The operator expects a configuration file in json format. Here is a description of the configuration keys by scope:
Contains configuration globally shared by all runners.
globalSvcLabelSelector
: Labels used to select global servicesglobalSvcRoutingStrategyLabel
: Labels used to instruct controller to try utilising Kubernetes topology aware hints to select local cluster targets first when routing global services.mirrorSvcLabelSelector
: Label used to select services to mirrormirrorNamespace
: Namespace used to locate/place mirrored objectsserviceSync
: Whether to sync services on startup and delete records that cannot be located based on the label selector. Defaults to false
Contains configuration needed to manage resources in the local cluster, where this operator runs.
name
: A name for the local clusterzones
: A list of the availability zones for the local cluster. This will be used to allow topology aware routing for global services and the values should derive from kuberenetes nodes'topology.kubernetes.io/zone
label.kubeConfigPath
: Path to a kube config file to access the local cluster. If not specified the operator will try to use in-cluster configuration with the pod's service account.
Contains a list of keys to configure access to all remote cluster. Each list can include the following:
name
: A name for the remote clusterkubeConfigPath
: Path to a kube config file to access the remote cluster.remoteAPIURL
: Address of the remote cluster API serverremoteCAURL
: Address from where to fetch the public CA certificate to talk to the remote API server.remoteSATokenPiath
: Path to a service account token that will be used to access remote cluster resources.resyncPeriod
: Will trigger anonUpdate
event for everything that is stored in the respective watchers cache. Defaults to 0 which equals disabled.servicePrefix
: How to prefix service names mirrored from that remote locally.
Either kubeConfigPath
or remoteAPIURL
,remoteCAURL
and remoteSATokenPiath
should be set to be able to successfully create a client to talk to the remote
cluster.
{
"global": {
"globalSvcLabelSelector": "mirror.semaphore.uw.io/global-service=true",
"globalSvcRoutingStrategyLabel": "mirror.semaphore.uw.io/global-service-routing-strategy=local-first",
"mirrorSvcLabelSelector": "mirror.semaphore.uw.io/mirror-service=true",
"mirrorNamespace": "semaphore",
"serviceSync": true,
"endpointSliceSync": true
},
"localCluster": {
"name": "local",
"kubeConfigPath": "/path/to/local/kubeconfig"
},
"remoteClusters": [
{
"name": "clusterA",
"remoteCAURL": "remote_ca_url",
"remoteAPIURL": "remote_api_url",
"remoteSATokenPath": "/path/to/token",
"resyncPeriod": "10s",
"servicePrefix": "cluster-A"
}
]
}
In order to make regex matching easier for dns rewrite purposes (see coredns
example bellow) we use a hardcoded separator between service names and
namespaces on the generated name for mirrored service: 73736d
.
The format of the generated name is: <prefix>-<namespace>-73736d-<name>
.
It's possible for this name to exceed the 63 character limit imposed by Kubernetes so the operator should have a Gatekeeper / Kyverno rule to guard against exceeding this Service name length.
To create a smoother experience when accessing a service coredns can be
configured using the rewrite
functionality:
cluster.example {
errors
health
rewrite continue {
name regex ([a-zA-Z0-9-_]*\.)?([a-zA-Z0-9-_]*)\.([a-zv0-9-_]*)\.svc\.cluster\.example {1}example-{3}-73736d-{2}.<namespace>.svc.cluster.local
answer name ([a-zA-Z0-9-_]*\.)?example-([a-zA-Z0-9-_]*)-73736d-([a-zA-Z0-9-_]*)\.<namespace>\.svc\.cluster\.local {1}{3}.{2}.svc.cluster.example
}
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
endpoint_pod_names
upstream
fallthrough in-addr.arpa ip6.arpa
}
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}
.:53 {
errors
health
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
endpoint_pod_names
upstream
fallthrough in-addr.arpa ip6.arpa
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}
that way all queries for services under domain cluster.target will be rewritten to match services on the local namespace that the services are mirrored.
- note that
<target>
and<namespace>
should be replaced with a name for the target cluster and the local namespace that contains the mirrored services. - note that the above example assumes that you are running the mirroring service with a prefix flag that matches the target cluster name.
The operator is also watching services based on a separate label, in order to create global services. A global service will gather endpoints from multiple remote clusters that live under the "same" namespace and name, into a single ocal service with endpoints in multiple clusters. For that purpose, it will create a single ClusterIP (or headless) service and mirror endpointslices from remote clusters to target the new "global" service.
The format of the name used for the global service is:
gl-<namespace>-73736d-<name>
.
For example, if we have the following services:
- cluster: cA, namespace: example-ns, name: my-svc, endpoints: [eA]
- cluster: cB, namespace: example-ns, name: my-svc, endpoints: [eB1, eB2] The operator will create a global service under the local "semaphore" namespace with a corresponding list of endpoints: [eA, eB1, eB2].
- Global services will include endpoints from the local cluster as well, provided they are using the mirror label.
- Global services will try to utilise Kubernetes topology aware hints to route to local endpoints first.
In order to be able to resolve the global services under cluster.global
domain, the following CoreDNS block is needed:
cluster.global {
cache 30
errors
forward . /etc/resolv.conf
kubernetes cluster.local {
pods insecure
endpoint_pod_names
}
loadbalance
loop
prometheus
reload
rewrite continue {
name regex ([\w-]*\.)?([\w-]*)\.([\w-]*)\.svc\.cluster\.global {1}gl-{3}-73736d-{2}.sys-semaphore.svc.cluster.local
answer name ([\w-]*\.)?gl-([\w-]*)-73736d-([\w-]*)\.sys-semaphore\.svc\.cluster\.local {1}{3}.{2}.svc.cluster.global
}
}
In some cases, it is preferable to route to endpoints which live closer to the
caller when addressing global services (first hit available endpoints in the
same cluster). For that purpose, one can use a label to instruct the controller
to set service.kubernetes.io/topology-aware-hints=auto
label in the generated
global service and instruct Kubernetes to use topology hints for routing traffic
to the service. In order for the hints to be effective, the operator reads the
local configuration zones
field and uses the list of zones defined there as
hints for local endpoints. If this is not set, a dummy value will be used and
topology aware routing will not be feasible. The operator also uses the dummy
"remote" zone value as a hint for endpoits mirrored from remote clusters, to
make sure that no routing decisions will be made on those and kube-proxy will
not complain about missing hints.
The label to enable the above is configurable via globalSvcTopologyLabel
field
in the global configuration.
Since service endpoints that will be involved in a global service come from multiple services in different clusters, based on the service name and namespace, certain parameters need to match across all those service definitions. In particular, service ports and topology labels values are fungible and if their values differ between definitions of services that feed endpoints to the same global service, there will be a race between services to force their attributes to the global service. For a predictable behaviour, make sure that ports match between services and either all or none set the topology label.
There are separate metrics available that one can use to determine the status of the controller. The available metrics can give a visibility on errors from the Kubernetes clients, the watchers and the controller's queues.
semaphore_service_mirror_kube_http_request_total
: Total number of HTTP requests to the Kubernetes API by host, code and method.semaphore_service_mirror_kube_http_request_duration_seconds
: Histogram of latencies for HTTP requests to the Kubernetes API by host and method
semaphore_service_mirror_kube_watcher_objects
: Number of objects watched by watcher and kindsemaphore_service_mirror_kube_watcher_events_total
: Number of events handled by watcher, kind and event_type
Because the controller runs multiple watchers in parallel, both for watching the remote clusters and the mirrored local objects, we use 2 labels to be able to distinguish between them.
watcher
label follows the pattern<cluster-name>-[mirror]<watcherType>
. For examplewatcher="aws-serviceWatcher"
will contain metrics for watching services on a cluster called "aws", andwatcher="aws-mirrorServiceWatcher"
will contain metrics for the mirrored local services from "aws" cluster.runner
label follows the pattern[mirror|global]-<cluster-name>
and should help distinguish if a watcher is used to create service mirrors or global services. Based on the above, one could use the following expression:semaphore_service_mirror_kube_watcher_objects{watcher=~".*-mirror.*"} - ignoring(watcher) semaphore_service_mirror_kube_watcher_objects{watcher!~".*-mirror.*"}
to monitor if controllers are lagging. Therunner
label comes handy in the above query, to avoid finding duplicate series for the match group.
semaphore_service_mirror_queue_depth
: Workqueue depth, by queue name.semaphore_service_mirror_queue_adds_total
: Workqueue adds, by queue name.semaphore_service_mirror_queue_latency_duration_seconds
: Workqueue latency, by queue name.semaphore_service_mirror_queue_work_duration_seconds
: Workqueue work duration, by queue name.semaphore_service_mirror_queue_unfinished_work_seconds
: Unfinished work in seconds, by queue name.semaphore_service_mirror_queue_longest_running_processor_seconds
: Longest running processor, by queue name.semaphore_service_mirror_queue_retries_total
: Workqueue retries, by queue name.semaphore_service_mirror_queue_requeued_items
: Items that have been requeued but not reconciled yet, by queue name.