Skip to content

Commit

Permalink
Merge pull request #4190 from juanluisvaladas/keepalived-vrrp
Browse files Browse the repository at this point in the history
Implement VIPs using Keepalived vrrp_instances
  • Loading branch information
juanluisvaladas authored Mar 27, 2024
2 parents 5df5279 + 7285a9d commit 2e48c6b
Show file tree
Hide file tree
Showing 22 changed files with 1,446 additions and 8 deletions.
8 changes: 8 additions & 0 deletions cmd/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,14 @@ func (c *command) start(ctx context.Context) error {
nodeComponents.Add(ctx, controllerLeaseCounter)
}

enableCPLB := !c.SingleNode && !slices.Contains(c.DisableComponents, constant.CPLBComponentName)
if enableCPLB {
nodeComponents.Add(ctx, &controller.Keepalived{
K0sVars: c.K0sVars,
Config: nodeConfig.Spec.Network.ControlPlaneLoadBalancing,
})
}

enableKonnectivity := !c.SingleNode && !slices.Contains(c.DisableComponents, constant.KonnectivityServerComponentName)
disableEndpointReconciler := !slices.Contains(c.DisableComponents, constant.APIEndpointReconcilerComponentName) &&
nodeConfig.Spec.API.ExternalAddress != ""
Expand Down
23 changes: 23 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,29 @@ node-local load balancing.
| `apiServerBindPort` | Port number on which to bind the Envoy load balancer for the Kubernetes API server to on a worker's loopback interface. Default: `7443`. |
| `konnectivityServerBindPort` | Port number on which to bind the Envoy load balancer for the konnectivity server to on a worker's loopback interface. Default: `7132`. |
##### `spec.network.controlPlaneLoadBalancing`
Configuration options related to k0s's [control plane load balancing] feature

| Element | Description |
| --------------- | ---------------------------------------------------------------------------------------------------------- |
| `vrrpInstances` | Configuration options related to the VRRP. This is an array which allows to configure multiple virtual IPs |

[control plane load balancing]: cplb.md

##### `spec.network.controlPlaneLoadBalancing.VRRPInstances`

Configuration options required for using VRRP to configure VIPs in control plane load balancing.

| Element | Description |
| ----------------- | ----------------------------------------------------------------------------------------------------------------- |
| `name` | The name of the VRRP instance. If omitted it generates a predictive name shared across all nodes. |
| `virtualIPs` | A list of the CIDRs handled by the VRRP instance. |
| `interface` | The interface used by each VRRPInstance. If undefined k0s will try to auto detect it based on the default gateway |
| `virtualRouterId` | Virtual router ID for the instance. Default: `51` |
| `advertInterval` | Advertisement interval in seconds. Default: `1`. |
| `authPass` | The password used for accessing vrrpd. This field is mandatory and must be under 8 characters long |

### `spec.controllerManager`

| Element | Description |
Expand Down
329 changes: 329 additions & 0 deletions docs/cplb.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions docs/networking.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,12 @@ One goal of k0s is to allow for the deployment of an isolated control plane, whi
| TCP | 10250 | kubelet | controller, worker => host `*` | Authenticated kubelet API for the controller node `kube-apiserver` (and `heapster`/`metrics-server` addons) using TLS client certs
| TCP | 9443 | k0s-api | controller <-> controller | k0s controller join API, TLS with token auth
| TCP | 8132 | konnectivity | worker <-> controller | Konnectivity is used as "reverse" tunnel between kube-apiserver and worker kubelets
| TCP | 112 | keepalived | controller <-> controller | Only required for control plane load balancing vrrpInstances for ip address 224.0.0.18. 224.0.0.18 is a multicast IP address defined in [RFC 3768].

You also need enable all traffic to and from the [podCIDR and serviceCIDR] subnets on nodes with a worker role.

[podCIDR and serviceCIDR]: configuration.md#specnetwork
[RFC 3768]: https://datatracker.ietf.org/doc/html/rfc3768#section-5.2.2

## iptables

Expand Down
3 changes: 2 additions & 1 deletion embedded-bins/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export TARGET_OS
SOURCE_DATE_EPOCH ?= $(shell git log -1 --pretty=%ct || date -u +%s)

bindir = staging/${TARGET_OS}/bin
posix_bins = runc kubelet containerd containerd-shim containerd-shim-runc-v1 containerd-shim-runc-v2 kube-apiserver kube-scheduler kube-controller-manager etcd kine konnectivity-server xtables-legacy-multi xtables-nft-multi
posix_bins = runc kubelet containerd containerd-shim containerd-shim-runc-v1 containerd-shim-runc-v2 kube-apiserver kube-scheduler kube-controller-manager etcd kine konnectivity-server xtables-legacy-multi xtables-nft-multi keepalived
windows_bins = kubelet.exe kube-proxy.exe containerd.exe containerd-shim-runhcs-v1.exe

ifeq ($(TARGET_OS),windows)
Expand Down Expand Up @@ -59,6 +59,7 @@ $(bindir)/konnectivity-server: .container.konnectivity
$(bindir)/kubelet $(bindir)/kube-apiserver $(bindir)/kube-scheduler $(bindir)/kube-controller-manager: .container.kubernetes
$(bindir)/xtables-legacy-multi: .container.iptables
$(bindir)/xtables-nft-multi: .container.iptables
$(bindir)/keepalived: .container.keepalived

$(bindir)/kubelet.exe $(bindir)/kube-proxy.exe: .container.kubernetes.windows
$(bindir)/containerd.exe $(bindir)/containerd-shim-runhcs-v1.exe: .container.containerd.windows
Expand Down
3 changes: 3 additions & 0 deletions embedded-bins/Makefile.variables
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ konnectivity_build_go_ldflags_extra = "-extldflags=-static"
iptables_version = 1.8.9
iptables_buildimage = docker.io/library/alpine:$(alpine_patch_version)

keepalived_version = 2.2.8
keepalived_buildimage = docker.io/library/alpine:$(alpine_patch_version)

clean-iid-files = \
for i in $(IID_FILES); do \
[ -f "$$i" ] || continue; \
Expand Down
20 changes: 20 additions & 0 deletions embedded-bins/keepalived/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
ARG BUILDIMAGE
FROM $BUILDIMAGE AS build

RUN apk add build-base curl \
linux-headers \
openssl-dev openssl-libs-static \
libnl3-dev libnl3-static

ARG VERSION
RUN curl -L https://www.keepalived.org/software/keepalived-$VERSION.tar.gz \
| tar -C / -zx

RUN cd /keepalived-$VERSION \
&& CFLAGS='-static -s' LDFLAGS=-static ./configure --disable-dynamic-linking \
&& make -j$(nproc)

FROM scratch
ARG VERSION
COPY --from=build /keepalived-$VERSION/bin/keepalived \
/bin/keepalived
1 change: 1 addition & 0 deletions inttest/Makefile.variables
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ smoketests := \
check-cnichange \
check-configchange \
check-containerdimports \
check-cplb \
check-ctr \
check-custom-cidrs \
check-customca \
Expand Down
10 changes: 5 additions & 5 deletions inttest/common/bootloosesuite.go
Original file line number Diff line number Diff line change
Expand Up @@ -1286,22 +1286,22 @@ func newSuiteContext(t *testing.T) (context.Context, context.CancelCauseFunc) {

// GetControllerIPAddress returns controller ip address
func (s *BootlooseSuite) GetControllerIPAddress(idx int) string {
return s.getIPAddress(s.ControllerNode(idx))
return s.GetIPAddress(s.ControllerNode(idx))
}

func (s *BootlooseSuite) GetWorkerIPAddress(idx int) string {
return s.getIPAddress(s.WorkerNode(idx))
return s.GetIPAddress(s.WorkerNode(idx))
}

func (s *BootlooseSuite) GetLBAddress() string {
return s.getIPAddress(s.LBNode())
return s.GetIPAddress(s.LBNode())
}

func (s *BootlooseSuite) GetExternalEtcdIPAddress() string {
return s.getIPAddress(s.ExternalEtcdNode())
return s.GetIPAddress(s.ExternalEtcdNode())
}

func (s *BootlooseSuite) getIPAddress(nodeName string) string {
func (s *BootlooseSuite) GetIPAddress(nodeName string) string {
ssh, err := s.SSH(s.Context(), nodeName)
s.Require().NoError(err)
defer ssh.Disconnect()
Expand Down
159 changes: 159 additions & 0 deletions inttest/cplb/cplb_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// Copyright 2024 k0s authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package keepalived

import (
"context"
"fmt"
"strconv"
"strings"
"testing"
"time"

"github.com/k0sproject/k0s/inttest/common"

"github.com/stretchr/testify/suite"
)

type keepalivedSuite struct {
common.BootlooseSuite
}

const haControllerConfig = `
spec:
api:
externalAddress: %s
network:
controlPlaneLoadBalancing:
vrrpInstances:
- virtualIPs: ["%s/24"]
authPass: "123456"
`

// SetupTest prepares the controller and filesystem, getting it into a consistent
// state which we can run tests against.
func (s *keepalivedSuite) TestK0sGetsUp() {
ipAddress := s.getLBAddress()
ctx := s.Context()
var joinToken string

for idx := 0; idx < s.BootlooseSuite.ControllerCount; idx++ {
s.Require().NoError(s.WaitForSSH(s.ControllerNode(idx), 2*time.Minute, 1*time.Second))
s.PutFile(s.ControllerNode(idx), "/tmp/k0s.yaml", fmt.Sprintf(haControllerConfig, ipAddress, ipAddress))

// Note that the token is intentionally empty for the first controller
s.Require().NoError(s.InitController(idx, "--config=/tmp/k0s.yaml", "--disable-components=metrics-server", joinToken))
s.Require().NoError(s.WaitJoinAPI(s.ControllerNode(idx)))

// With the primary controller running, create the join token for subsequent controllers.
if idx == 0 {
token, err := s.GetJoinToken("controller")
s.Require().NoError(err)
joinToken = token
}
}

// Final sanity -- ensure all nodes see each other according to etcd
for idx := 0; idx < s.BootlooseSuite.ControllerCount; idx++ {
s.Require().Len(s.GetMembers(idx), s.BootlooseSuite.ControllerCount)
}

// Create a worker join token
workerJoinToken, err := s.GetJoinToken("worker")
s.Require().NoError(err)

// Start the workers using the join token
s.Require().NoError(s.RunWorkersWithToken(workerJoinToken))

client, err := s.KubeClient(s.ControllerNode(0))
s.Require().NoError(err)

s.Require().NoError(s.WaitForNodeReady(s.WorkerNode(0), client))

// Verify that all servers have the dummy interface
for idx := 0; idx < s.BootlooseSuite.ControllerCount; idx++ {
s.checkDummy(ctx, s.ControllerNode(idx), ipAddress)
}

// Verify that only one controller has the VIP in eth0
count := 0
for idx := 0; idx < s.BootlooseSuite.ControllerCount; idx++ {
if s.hasVIP(ctx, s.ControllerNode(idx), ipAddress) {
count++
}
}
s.Require().Equal(1, count, "Expected only one controller to have the VIP")

}

// getLBAddress returns the IP address of the controller 0 and it adds 100 to
// the last octet unless it's bigger or equal to 154, in which case it
// subtracts 100. Theoretically this could result in an invalid IP address.
func (s *keepalivedSuite) getLBAddress() string {
ip := s.GetIPAddress(s.ControllerNode(0))
parts := strings.Split(ip, ".")
if len(parts) != 4 {
s.T().Fatalf("Invalid IP address: %q", ip)
}
lastOctet, err := strconv.Atoi(parts[3])
s.Require().NoErrorf(err, "Failed to convert last octet '%s' to int", parts[3])
if lastOctet >= 154 {
lastOctet -= 100
} else {
lastOctet += 100
}

return fmt.Sprintf("%s.%d", strings.Join(parts[:3], "."), lastOctet)
}

// checkDummy checks that the dummy interface is present on the given node and
// that it has only the virtual IP address.
func (s *keepalivedSuite) checkDummy(ctx context.Context, node string, vip string) {
ssh, err := s.SSH(ctx, node)
s.Require().NoError(err)
defer ssh.Disconnect()

output, err := ssh.ExecWithOutput(ctx, "ip --oneline addr show dummyvip0")
s.Require().NoError(err)

s.Require().Equal(0, strings.Count(output, "\n"), "Expected only one line of output")

expected := fmt.Sprintf("inet %s/32", vip)
s.Require().Contains(output, expected)
}

// hasVIP checks that the dummy interface is present on the given node and
// that it has only the virtual IP address.
func (s *keepalivedSuite) hasVIP(ctx context.Context, node string, vip string) bool {
ssh, err := s.SSH(ctx, node)
s.Require().NoError(err)
defer ssh.Disconnect()

output, err := ssh.ExecWithOutput(ctx, "ip --oneline addr show eth0")
s.Require().NoError(err)

return strings.Contains(output, fmt.Sprintf("inet %s/24", vip))
}

// TestKeepAlivedSuite runs the keepalived test suite. It verifies that the
// virtual IP is working by joining a node to the cluster using the VIP.
func TestKeepAlivedSuite(t *testing.T) {
suite.Run(t, &keepalivedSuite{
common.BootlooseSuite{
ControllerCount: 3,
WorkerCount: 1,
},
})
}
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ nav:
- IPv4/IPv6 Dual-Stack: dual-stack.md
- Control Plane High Availability: high-availability.md
- Node-local load balancing: nllb.md
- Control plane load balancing: cplb.md
- Shell Completion: shell-completion.md
- User Management: user-management.md
- Configuration of Environment Variables: environment-variables.md
Expand Down
Loading

0 comments on commit 2e48c6b

Please sign in to comment.