Skip to content

Commit

Permalink
Merge pull request #9069 from justinsb/healthcheck
Browse files Browse the repository at this point in the history
kube-apiserver: healthcheck via sidecar container
  • Loading branch information
k8s-ci-robot committed May 7, 2020
2 parents 3df14f2 + 75fd939 commit c736595
Show file tree
Hide file tree
Showing 29 changed files with 1,112 additions and 60 deletions.
30 changes: 28 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ unexport SKIP_REGION_CHECK S3_ACCESS_KEY_ID S3_ENDPOINT S3_REGION S3_SECRET_ACCE
DNS_CONTROLLER_TAG=1.18.0-alpha.3
# Keep in sync with upup/models/cloudup/resources/addons/kops-controller.addons.k8s.io/
KOPS_CONTROLLER_TAG=1.18.0-alpha.3
# Keep in sync with pkg/model/components/kubeapiserver/model.go
KUBE_APISERVER_HEALTHCHECK_TAG=1.18.0-alpha.3

# Keep in sync with logic in get_workspace_status
# TODO: just invoke tools/get_workspace_status.sh?
Expand Down Expand Up @@ -732,6 +734,13 @@ bazel-protokube-export:
cp -fp bazel-bin/images/protokube.tar.gz.sha1 ${BAZELIMAGES}/protokube.tar.gz.sha1
cp -fp bazel-bin/images/protokube.tar.gz.sha256 ${BAZELIMAGES}/protokube.tar.gz.sha256

.PHONY: bazel-kube-apiserver-healthcheck-export
bazel-kube-apiserver-healthcheck-export:
mkdir -p ${BAZELIMAGES}
DOCKER_REGISTRY="" DOCKER_IMAGE_PREFIX="kope/" KUBE_APISERVER_HEALTHCHECK_TAG=${KUBE_APISERVER_HEALTHCHECK_TAG} bazel build ${BAZEL_CONFIG} --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64 //cmd/kube-apiserver-healthcheck:image-bundle.tar.gz //cmd/kube-apiserver-healthcheck:image-bundle.tar.gz.sha256
cp -fp bazel-bin/cmd/kube-apiserver-healthcheck/image-bundle.tar.gz ${BAZELIMAGES}/kube-apiserver-healthcheck.tar.gz
cp -fp bazel-bin/cmd/kube-apiserver-healthcheck/image-bundle.tar.gz.sha256 ${BAZELIMAGES}/kube-apiserver-healthcheck.tar.gz.sha256

.PHONY: bazel-kops-controller-export
bazel-kops-controller-export:
mkdir -p ${BAZELIMAGES}
Expand All @@ -749,7 +758,7 @@ bazel-dns-controller-export:
cp -fp bazel-bin/dns-controller/cmd/dns-controller/image-bundle.tar.gz.sha256 ${BAZELIMAGES}/dns-controller.tar.gz.sha256

.PHONY: bazel-version-dist
bazel-version-dist: bazel-crossbuild-nodeup bazel-crossbuild-kops bazel-kops-controller-export bazel-dns-controller-export bazel-protokube-export bazel-utils-dist
bazel-version-dist: bazel-crossbuild-nodeup bazel-crossbuild-kops bazel-kops-controller-export bazel-kube-apiserver-healthcheck-export bazel-dns-controller-export bazel-protokube-export bazel-utils-dist
rm -rf ${BAZELUPLOAD}
mkdir -p ${BAZELUPLOAD}/kops/${VERSION}/linux/amd64/
mkdir -p ${BAZELUPLOAD}/kops/${VERSION}/darwin/amd64/
Expand All @@ -765,6 +774,8 @@ bazel-version-dist: bazel-crossbuild-nodeup bazel-crossbuild-kops bazel-kops-con
cp ${BAZELIMAGES}/kops-controller.tar.gz ${BAZELUPLOAD}/kops/${VERSION}/images/kops-controller.tar.gz
cp ${BAZELIMAGES}/kops-controller.tar.gz.sha1 ${BAZELUPLOAD}/kops/${VERSION}/images/kops-controller.tar.gz.sha1
cp ${BAZELIMAGES}/kops-controller.tar.gz.sha256 ${BAZELUPLOAD}/kops/${VERSION}/images/kops-controller.tar.gz.sha256
cp ${BAZELIMAGES}/kube-apiserver-healthcheck.tar.gz ${BAZELUPLOAD}/kops/${VERSION}/images/kube-apiserver-healthcheck.tar.gz
cp ${BAZELIMAGES}/kube-apiserver-healthcheck.tar.gz.sha256 ${BAZELUPLOAD}/kops/${VERSION}/images/kube-apiserver-healthcheck.tar.gz.sha256
cp ${BAZELIMAGES}/dns-controller.tar.gz ${BAZELUPLOAD}/kops/${VERSION}/images/dns-controller.tar.gz
cp ${BAZELIMAGES}/dns-controller.tar.gz.sha1 ${BAZELUPLOAD}/kops/${VERSION}/images/dns-controller.tar.gz.sha1
cp ${BAZELIMAGES}/dns-controller.tar.gz.sha256 ${BAZELUPLOAD}/kops/${VERSION}/images/dns-controller.tar.gz.sha256
Expand Down Expand Up @@ -856,6 +867,14 @@ dev-upload-dns-controller: bazel-dns-controller-export # Upload kops to GCS
cp -fp ${BAZELIMAGES}/dns-controller.tar.gz.sha256 ${BAZELUPLOAD}/kops/${VERSION}/images/dns-controller.tar.gz.sha256
${UPLOAD_CMD} ${BAZELUPLOAD}/ ${UPLOAD_DEST}

# dev-upload-kube-apiserver-healthcheck uploads kube-apiserver-healthcheck to GCS
.PHONY: dev-upload-kube-apiserver-healthcheck
dev-upload-kube-apiserver-healthcheck: bazel-kube-apiserver-healthcheck-export # Upload kops to GCS
mkdir -p ${BAZELUPLOAD}/kops/${VERSION}/images/
cp -fp ${BAZELIMAGES}/kube-apiserver-healthcheck.tar.gz ${BAZELUPLOAD}/kops/${VERSION}/images/kube-apiserver-healthcheck.tar.gz
cp -fp ${BAZELIMAGES}/kube-apiserver-healthcheck.tar.gz.sha256 ${BAZELUPLOAD}/kops/${VERSION}/images/kube-apiserver-healthcheck.tar.gz.sha256
${UPLOAD_CMD} ${BAZELUPLOAD}/ ${UPLOAD_DEST}

# dev-copy-utils copies utils from a recent release
# We don't currently have a bazel build for them, and the build is pretty slow, but they change rarely.
.PHONE: dev-copy-utils
Expand All @@ -869,7 +888,7 @@ dev-copy-utils:
# dev-upload does a faster build and uploads to GCS / S3
# It copies utils instead of building it
.PHONY: dev-upload
dev-upload: dev-upload-nodeup dev-upload-protokube dev-upload-dns-controller dev-upload-kops-controller dev-copy-utils
dev-upload: dev-upload-nodeup dev-upload-protokube dev-upload-dns-controller dev-upload-kops-controller dev-copy-utils dev-upload-kube-apiserver-healthcheck
echo "Done"

.PHONY: crds
Expand All @@ -882,3 +901,10 @@ crds:
.PHONY: kops-controller-push
kops-controller-push:
DOCKER_REGISTRY=${DOCKER_REGISTRY} DOCKER_IMAGE_PREFIX=${DOCKER_IMAGE_PREFIX} KOPS_CONTROLLER_TAG=${KOPS_CONTROLLER_TAG} bazel run --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64 //cmd/kops-controller:push-image

#------------------------------------------------------
# kube-apiserver-healthcheck

.PHONY: kube-apiserver-healthcheck-push
kube-apiserver-healthcheck-push:
DOCKER_REGISTRY=${DOCKER_REGISTRY} DOCKER_IMAGE_PREFIX=${DOCKER_IMAGE_PREFIX} KUBE_APISERVER_HEALTHCHECK_TAG=${KUBE_APISERVER_HEALTHCHECK_TAG} bazel run --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64 //cmd/kube-apiserver-healthcheck:push-image
70 changes: 70 additions & 0 deletions cmd/kube-apiserver-healthcheck/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test")

go_library(
name = "go_default_library",
srcs = ["main.go"],
importpath = "k8s.io/kops/cmd/kube-apiserver-healthcheck",
visibility = ["//visibility:private"],
deps = ["//vendor/k8s.io/klog:go_default_library"],
)

go_binary(
name = "kube-apiserver-healthcheck",
embed = [":go_default_library"],
visibility = ["//visibility:public"],
)

load(
"@io_bazel_rules_docker//container:container.bzl",
"container_image",
"container_push",
"container_bundle",
)

container_image(
name = "image",
base = "@distroless_base//image",
cmd = ["/usr/bin/kube-apiserver-healthcheck"],
user = "10012",
directory = "/usr/bin/",
files = [
"//cmd/kube-apiserver-healthcheck",
],
stamp = True,
)

container_push(
name = "push-image",
format = "Docker",
image = ":image",
registry = "{STABLE_DOCKER_REGISTRY}",
repository = "{STABLE_DOCKER_IMAGE_PREFIX}kube-apiserver-healthcheck",
tag = "{STABLE_KUBE_APISERVER_HEALTHCHECK_TAG}",
)

container_bundle(
name = "image-bundle",
images = {
"{STABLE_DOCKER_IMAGE_PREFIX}kube-apiserver-healthcheck:{STABLE_KUBE_APISERVER_HEALTHCHECK_TAG}": "image",
},
)

load("//tools:gzip.bzl", "gzip")

gzip(
name = "image-bundle.tar.gz",
src = "image-bundle.tar",
)

load("//tools:hashes.bzl", "hashes")

hashes(
name = "image-bundle.tar.gz.hashes",
src = "image-bundle.tar.gz",
)

go_test(
name = "go_default_test",
srcs = ["proxy_test.go"],
embed = [":go_default_library"],
)
18 changes: 18 additions & 0 deletions cmd/kube-apiserver-healthcheck/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
## kube-apiserver-healthcheck

This is a small sidecar container that allows for health-checking the
kube-apiserver without enabling anonymous authentication and without
enabling the unauthenticated port.

It listens on port 8080 (http), and proxies a few known-safe requests
to the real apiserver listening on 443. It uses a client certificate
to authenticate itself to the apiserver.

This lets us turn off the unauthenticated kube-apiserver endpoint, but
it also lets us have better load-balancer health-checks.

Because it runs as a sidecar next to kube-apiserver, it is in the same
network namespace, and thus it can reach apiserver on
https://127.0.0.1 . The kube-apiserver-healthcheck process listens on
8080, but the health checks for the apiserver container are configured
for :8080 and actually go via the sidecar.
191 changes: 191 additions & 0 deletions cmd/kube-apiserver-healthcheck/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/*
Copyright 2020 The Kubernetes 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 main

import (
"crypto/tls"
"crypto/x509"
"flag"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"

"k8s.io/klog"
)

// healthCheckServer is the http server
type healthCheckServer struct {
transport *http.Transport
}

// handler processes a single http request
func (s *healthCheckServer) handler(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" && r.URL.Path == "/.kube-apiserver-healthcheck/healthz" {
// This is a check for our own health
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
return
}

if proxyRequest := mapToProxyRequest(r); proxyRequest != nil {
s.proxyRequest(w, proxyRequest)
return
}

klog.Infof("unknown request: %s %s", r.Method, r.URL.Path)
http.Error(w, "not found", http.StatusNotFound)
}

// httpClient builds an isolated http.Client
func (s *healthCheckServer) httpClient() *http.Client {
return &http.Client{Transport: s.transport}
}

// mapToProxyRequest returns the request we should make to the apiserver,
// or nil if the query is not on the safelist
func mapToProxyRequest(r *http.Request) *http.Request {
if r.Method == "GET" {
switch r.URL.Path {
case "/livez", "/healthz", "/readyz":
// This is a health-check we will proxy
return sanitizeRequest(r, []string{"exclude"})
}
}
return nil
}

// sanitizeRequest builds the request we should pass to the target apiserver,
// passing through only allowedQueryParameters
func sanitizeRequest(r *http.Request, allowedQueryParameters []string) *http.Request {
u := &url.URL{
Scheme: "https",
Host: "127.0.0.1",
Path: r.URL.Path,
}

// Pass-through (only) the parameters in allowedQueryParameters
{
in := r.URL.Query()
out := make(url.Values)

for _, k := range allowedQueryParameters {
for _, v := range in[k] {
out.Add(k, v)
}
}
u.RawQuery = out.Encode()
}

req := &http.Request{
Method: r.Method,
URL: u,
}

return req
}

// proxyRequest forwards a request, that has been sanitized by mapToProxyRequest/buildProxyRequest
func (s *healthCheckServer) proxyRequest(w http.ResponseWriter, forwardRequest *http.Request) {
httpClient := s.httpClient()

resp, err := httpClient.Do(forwardRequest)
if err != nil {
klog.Infof("error from %s: %v", forwardRequest.URL, err)
http.Error(w, "internal error", http.StatusBadGateway)
return
}

defer resp.Body.Close()

w.WriteHeader(resp.StatusCode)
if _, err := io.Copy(w, resp.Body); err != nil {
klog.Warningf("error writing response body: %v", err)
return
}

switch resp.StatusCode {
case 200:
klog.V(2).Infof("proxied to %s %s: %s", forwardRequest.Method, forwardRequest.URL, resp.Status)
default:
klog.Infof("proxied to %s %s: %s", forwardRequest.Method, forwardRequest.URL, resp.Status)
}
}

func run() error {
listen := ":8080"

clientCert := ""
clientKey := ""
caCert := ""

flag.StringVar(&clientCert, "client-cert", clientCert, "path to client certificate")
flag.StringVar(&clientKey, "client-key", clientKey, "path to client key")
flag.StringVar(&caCert, "ca-cert", caCert, "path to ca certificate")

klog.InitFlags(nil)

flag.Parse()

tlsConfig := &tls.Config{}

if caCert != "" {
b, err := ioutil.ReadFile(caCert)
if err != nil {
return fmt.Errorf("error reading certificate %q: %v", caCert, err)
}
rootCAs := x509.NewCertPool()
rootCAs.AppendCertsFromPEM(b)
tlsConfig.RootCAs = rootCAs
}

if clientKey != "" {
keypair, err := tls.LoadX509KeyPair(clientCert, clientKey)
if err != nil {
return fmt.Errorf("error reading client keypair: %v", err)
}

tlsConfig.Certificates = []tls.Certificate{keypair}
}

transport := &http.Transport{
TLSClientConfig: tlsConfig,
}

s := &healthCheckServer{
transport: transport,
}

http.HandleFunc("/", s.handler)

klog.Infof("listening on %s", listen)

if err := http.ListenAndServe(listen, nil); err != nil {
return fmt.Errorf("error listening on %q: %v", listen, err)
}

return fmt.Errorf("unexpected return from ListenAndServe")
}

func main() {
if err := run(); err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
}
Loading

0 comments on commit c736595

Please sign in to comment.