You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by to...@apache.org on 2021/02/09 02:39:03 UTC

[apisix-ingress-controller] branch master updated: feat: support health check on ApisixUpstream (#247)

This is an automated email from the ASF dual-hosted git repository.

tokers pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git


The following commit(s) were added to refs/heads/master by this push:
     new 419a76a  feat: support health check on ApisixUpstream (#247)
419a76a is described below

commit 419a76acf7481c8cce6446439cfd21e95ea54085
Author: Alex Zhang <zc...@gmail.com>
AuthorDate: Tue Feb 9 10:38:57 2021 +0800

    feat: support health check on ApisixUpstream (#247)
---
 docs/CRD-specification.md                          | 179 +++++++++-
 go.mod                                             |   2 -
 go.sum                                             |   1 -
 pkg/apisix/resource.go                             |  15 +-
 pkg/apisix/upstream.go                             |  21 +-
 pkg/ingress/controller/endpoint.go                 |   8 +-
 pkg/kube/apisix/apis/config/v1/types.go            |  65 ++++
 .../apisix/apis/config/v1/zz_generated.deepcopy.go | 169 ++++++++++
 pkg/kube/apisix_upstream.go                        | 260 +++++++++++++++
 pkg/kube/apisix_upstream_test.go                   | 371 +++++++++++++++++++++
 pkg/kube/translator.go                             |  47 +--
 pkg/types/apisix/v1/types.go                       |  96 +++++-
 pkg/types/apisix/v1/zz_generated.deepcopy.go       | 148 ++++++++
 test/e2e/features/healthcheck.go                   | 140 ++++++++
 test/e2e/go.mod                                    |   1 -
 15 files changed, 1445 insertions(+), 78 deletions(-)

diff --git a/docs/CRD-specification.md b/docs/CRD-specification.md
index f5639af..9dffa0b 100644
--- a/docs/CRD-specification.md
+++ b/docs/CRD-specification.md
@@ -25,6 +25,10 @@ In order to control the behavior of the proxy ([Apache APISIX](https://github.co
 
 - [ApisixRoute](#apisixroute)
 - [ApisixUpstream](#apisixupstream)
+  - [Configuring Load Balancer](#configuring-load-balancer)
+  - [Configuring Health Check](#configuring-load-balancer)
+  - [Port Level Settings](#port-level-settings)
+  - [Configuration References](#configuration-references)
 - [ApisixTls](#apisixtls)
 
 ## ApisixRoute
@@ -102,30 +106,183 @@ spec:
 
 ## ApisixUpstream
 
-`ApisixUpstream` corresponds to the `Upstream` object in Apache APISIX.
-Upstream is a virtual host abstraction that performs load balancing on a given set of service nodes according to configuration rules.
-Upstream address information can be directly configured to `Route` (or `Service`). When Upstream has duplicates, you need to use "reference" to avoid duplication.
+ApisixUpstream is the decorator of Kubernetes Service. It's designed to have same name with its target Kubernetes Service, it makes the Kubernetes Service richer by adding
+load balancing, health check, retry, timeout parameters and etc.
+
+Resort to `ApisixUpstream` and the Kubernetes Service, apisix ingress controller will generates the APISIX Upstream(s).
 To learn more, please check the [Apache APISIX architecture-design docs](https://github.com/apache/apisix/blob/master/doc/architecture-design.md#upstream).
 
-Structure example:
+### Configuring Load Balancer
+
+A proper load balancing algorithm is required to scatter requests reasonably for a Kubernetes Service.
 
 ```yaml
 apiVersion: apisix.apache.org/v1
 kind: ApisixUpstream
 metadata:
-  name: httpserver
-  namespace: cloud
+  name: httpbin
+spec:
+  loadbalancer:
+    type: ewma
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: httpbin
 spec:
+  selector:
+    app: httpbin
   ports:
-    - port: 8080
-      loadbalancer: roundrobin
+  - name: http
+    port: 80
+    targetPort: 8080
+```
+
+The above example shows that [ewma](https://linkerd.io/2016/03/16/beyond-round-robin-load-balancing-for-latency/) is used as the load balancer for Service `httpbin`.
+
+Sometimes the session sticky is desired, and you can use the [Consistent Hashing](https://en.wikipedia.org/wiki/Consistent_hashing) load balancing algorithm.
+
+```yaml
+apiVersion: apisix.apache.org/v1
+kind: ApisixUpstream
+metadata:
+  name: httpbin
+spec:
+  loadbalancer:
+    type: chash
+    hashOn: header
+    key: "user-agent"
+```
+
+With the above settings, Apache APISIX will distributes requests according to the User-Agent header.
+
+### Configuring Health Check
+
+Although Kubelet already provides [probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#:~:text=The%20kubelet%20uses%20readiness%20probes,removed%20from%20Service%20load%20balancers.) to detect whether pods are healthy, you may still need more powerful health cheak mechanism,
+like the passive feedback capability.
+
+```yaml
+apiVersion: apisix.apache.org/v1
+kind: ApisixUpstream
+metadata:
+  name: httpbin
+spec:
+  healthCheck:
+    passive:
+      unhealthy:
+        httpCodes:
+          - 500
+          - 502
+          - 503
+          - 504
+        httpFailures: 3
+        timeout: 5s
+    active:
+      type: http
+      httpPath: /healthz
+      timeout: 5s
+      host: www.foo.com
+      healthy:
+        successes: 3
+        interval: 2s
+        httpCodes:
+          - 200
+          - 206
 ```
 
+The above YAML snippet defines a passive health checker to detech the unhealthy state for
+endpoints, once there are three consecutive requests with bad status code (one of `500`, `502`, `503`, `504`), the endpoint
+will be set to unhealthy and no requests can be routed there until it's healthy again.
+
+That's why the active health checker comes in, endpoints might be down for a short while and ready again, the active health checker detects these unhealthy endpoints continuously, and pull them
+up once the healthy conditions are met (three consecutive requests got good status codes, e.g. `200` and `206`).
+
+Note the active health checker is somewhat duplicated with the liveness/readiness probes but it's required if the passive feedback mechanism is in use. So once you use the health check feature in ApisixUpstream,
+the active health checker is mandatory.
+
+### Port Level Settings
+
+Once in a while a single Kubernetes Service might expose multiple ports which provides distinct functions and different Upstream configurations are required.
+In that case, you can create configurations for individual port.
+
+```yaml
+apiVersion: apisix.apache.org/v1
+kind: ApisixUpstream
+metadata:
+  name: foo
+spec:
+  loadbalancer:
+    type: roundrobin
+  portLevelSettings:
+  - port: 7000
+    scheme: http
+  - port: 7001
+    scheme: grpc
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: foo
+spec:
+  selector:
+    app: foo
+  portLevelSettings:
+  - name: http
+    port: 7000
+    targetPort: 7000
+  - name: grpc
+    port: 7001
+    targetPort: 7001
+```
+
+The `foo` service exposes two ports, one of them use HTTP protocol and the other uses grpc protocol.
+In the meanwhile, the ApisixUpstream `foo` sets `http` scheme for port `7000` and `grpc` scheme for `7001`
+(all ports are the service port). But both ports shares the load balancer configuration.
+
+`PortLevelSettings` is not mandatory if the service only exposes one port but is useful when multiple ports are defined.
+
+### Configuration References
+
 |     Field     |  Type    | Description    |
 |---------------|----------|----------------|
-| ports         | array    | Custom upstream collection.   |
-| port          | int      | Upstream service port number.    |
-| loadbalancer  | string/object   | The load balance algorithm of this upstream service, optional value can be `roundrobin` or `chash`.  |
+| scheme        | string   | The protocol used to talk to the Service, can be `http`, `grpc`, default is `http`.   |
+| loadbalancer  | object   | The load balancing algorithm of this upstream service |
+| loadbalancer.type | string | The load balancing type, can be `roundrobin`, `ewma`, `least_conn`, `chash`, default is `roundrobin`. |
+| loadbalancer.hashOn | string | The hash value source scope, only take effects if the `chash` algorithm is in use. Values can `vars`, `header`, `vars_combinations`, `cookie` and `consumers`, default is `vars`. |
+| loadbalancer.key | string | The hash key, only in valid if the `chash` algorithm is used.
+| healthCheck | object | The health check parameters, see [Health Check](https://github.com/apache/apisix/blob/master/doc/health-check.md) for more details. |
+| healthCheck.active | object | active health check configuration, which is a mandatory field. |
+| healthCheck.active.type | string | health check type, can be `http`, `https` and `tcp`, default is `http`. |
+| healthCheck.active.timeout | time duration in the form "72h3m0.5s" | the timeout settings for the probe, default is `1s`. |
+| healthCheck.active.concurrency | int | how many probes can be sent simultaneously, default is `10`. |
+| healthCheck.active.host | string | host header in http probe request, only in valid if the active health check type is `http` or `https`. |
+| healthCheck.active.port | int | target port to receive probes, it's necessary to specify this field if the health check service exposes by different port, note the port value here is the container port, not the service port. |
+| healthCheck.active.httpPath | string | the HTTP URI path in http probe, only in valid if the active health check type is `http` or `https`. |
+| healthCheck.active.strictTLS | boolean | whether to use the strict mode when use TLS, only in valid if the active health check type is `https`, default is `true`. |
+| healthCheck.active.requestHeaders | array of string | Extra HTTP requests carried in the http probe, only in valid if the active health check type is `http` or `https`. |
+| healthCheck.active.healthy | object | The conditions to judge an endpoint is healthy. |
+| healthCheck.active.healthy.successes | int | The number of consecutive requests needed to set an endpoint as healthy, default is `2`. |
+| healthCheck.active.healthy.httpCodes | array of integer | Good status codes list to check whether a probe is successful, only in valid if the active health check type is `http` or `https`, default is `[200, 302]`. |
+| healthCheck.active.healthy.interval | time duration in the form "72h3m0.5s" | The probes sent interval (for healthy endpoints). |
+| healthCheck.active.unhealthy | object | The conditions to judge an endpoint is unhealthy. |
+| healthCheck.active.unhealthy.httpFailures | int | The number of consecutive http requests needed to set an endpoint as unhealthy, only in valid if the active health check type is `http` or `https`, default is `5`. |
+| healthCheck.active.unhealthy.tcpFailures | int | The number of consecutive tcp connections needed to set an endpoint as unhealthy, only in valid if the active health check type is `tcp`, default is `2`. |
+| healthCheck.active.unhealthy.httpCodes | array of integer | Bad status codes list to check whether a probe is failed, only in valid if the active health check type is `http` or `https`, default is `[429, 404, 500, 501, 502, 503, 504, 505]`. |
+| healthCheck.active.unhealthy.interval | time duration in the form "72h3m0.5s" | The probes sent interval (for unhealthy endpoints). |
+| healthCheck.passive | object | passive health check configuration, which is an optional field. |
+| healthCheck.passive.type | string | health check type, can be `http`, `https` and `tcp`, default is `http`. |
+| healthCheck.passive.healthy | object | The conditions to judge an endpoint is healthy. |
+| healthCheck.passive.healthy.successes | int | The number of consecutive requests needed to set an endpoint as healthy, default is `5`. |
+| healthCheck.passive.healthy.httpCodes | array of integer | Good status codes list to check whether a probe is successful, only in valid if the active health check type is `http` or `https`, default is `[200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 300, 301, 302, 303, 304, 305, 306, 307, 308]`. |
+| healthCheck.passive.unhealthy | object | The conditions to judge an endpoint is unhealthy. |
+| healthCheck.passive.unhealthy.httpFailures | int | The number of consecutive http requests needed to set an endpoint as unhealthy, only in valid if the active health check type is `http` or `https`, default is `5`. |
+| healthCheck.passive.unhealthy.tcpFailures | int | The number of consecutive tcp connections needed to set an endpoint as unhealthy, only in valid if the active health check type is `tcp`, default is `2`. |
+| healthCheck.passive.unhealthy.httpCodes | array of integer | Bad status codes list to check whether a probe is failed, only in valid if the active health check type is `http` or `https`, default is `[429, 404, 500, 501, 502, 503, 504, 505]`. |
+| portLevelSettings | array | Settings for each individual port. |
+| portLevelSettings.port | int | The port number defined in the Kubernetes Service, must be a valid port. |
+| portLevelSettings.scheme | string | same as `scheme` but takes higher precedence. |
+| portLevelSettings.loadbalancer | object | same as `loadbalancer` but takes higher precedence. |
+| portLevelSettings.healthCheck | object | same as `healthCheck` but takes higher precedence. |
 
 ## ApisixTls
 
diff --git a/go.mod b/go.mod
index e0bc1ca..058bd4c 100644
--- a/go.mod
+++ b/go.mod
@@ -31,5 +31,3 @@ require (
 	k8s.io/apimachinery v0.20.2
 	k8s.io/client-go v0.20.2
 )
-
-replace github.com/gxthrj/apisix-ingress-types v0.1.3 => github.com/api7/ingress-types v0.1.3
diff --git a/go.sum b/go.sum
index 542312f..670ae5c 100644
--- a/go.sum
+++ b/go.sum
@@ -42,7 +42,6 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
 github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/api7/ingress-controller v0.1.0-rc1 h1:6EjrBu0r+ccVfYTnpGYj1txz1DJCJ/Q/k8pHigRkeu0=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
diff --git a/pkg/apisix/resource.go b/pkg/apisix/resource.go
index 7cbb97c..aa95aad 100644
--- a/pkg/apisix/resource.go
+++ b/pkg/apisix/resource.go
@@ -135,6 +135,17 @@ func (i *item) upstream(clusterName string) (*v1.Upstream, error) {
 		})
 	}
 
+	// This is a work around scheme to avoid APISIX's
+	// health check schema about the health checker intervals.
+	if ups.Checks != nil && ups.Checks.Active != nil {
+		if ups.Checks.Active.Healthy.Interval == 0 {
+			ups.Checks.Active.Healthy.Interval = int(v1.ActiveHealthCheckMinInterval.Seconds())
+		}
+		if ups.Checks.Active.Unhealthy.Interval == 0 {
+			ups.Checks.Active.Healthy.Interval = int(v1.ActiveHealthCheckMinInterval.Seconds())
+		}
+	}
+
 	fullName := genFullName(ups.Desc, clusterName)
 
 	return &v1.Upstream{
@@ -145,9 +156,11 @@ func (i *item) upstream(clusterName string) (*v1.Upstream, error) {
 			Name:     ups.Desc,
 		},
 		Type:   ups.LBType,
-		Key:    i.Key,
+		Key:    ups.Key,
+		HashOn: ups.HashOn,
 		Nodes:  nodes,
 		Scheme: ups.Scheme,
+		Checks: ups.Checks,
 	}, nil
 }
 
diff --git a/pkg/apisix/upstream.go b/pkg/apisix/upstream.go
index a339f14..137ab99 100644
--- a/pkg/apisix/upstream.go
+++ b/pkg/apisix/upstream.go
@@ -62,20 +62,16 @@ func (n *upstreamNodes) UnmarshalJSON(p []byte) error {
 }
 
 type upstreamReqBody struct {
-	LBType string        `json:"type"`
-	HashOn string        `json:"hash_on,omitempty"`
-	Key    string        `json:"key,omitempty"`
-	Nodes  upstreamNodes `json:"nodes"`
-	Desc   string        `json:"desc"`
-	Scheme string        `json:"scheme,omitempty"`
+	LBType string                  `json:"type"`
+	HashOn string                  `json:"hash_on,omitempty"`
+	Key    string                  `json:"key,omitempty"`
+	Nodes  upstreamNodes           `json:"nodes"`
+	Desc   string                  `json:"desc"`
+	Scheme string                  `json:"scheme,omitempty"`
+	Checks *v1.UpstreamHealthCheck `json:"checks,omitempty"`
 }
 
-type upstreamItem struct {
-	Nodes  upstreamNodes `json:"nodes"`
-	Desc   string        `json:"desc"`
-	LBType string        `json:"type"`
-	Scheme string        `json:"scheme"`
-}
+type upstreamItem upstreamReqBody
 
 func newUpstreamClient(c *cluster) Upstream {
 	return &upstreamClient{
@@ -202,6 +198,7 @@ func (u *upstreamClient) Create(ctx context.Context, obj *v1.Upstream) (*v1.Upst
 		Nodes:  nodes,
 		Desc:   obj.Name,
 		Scheme: obj.Scheme,
+		Checks: obj.Checks,
 	})
 	if err != nil {
 		return nil, err
diff --git a/pkg/ingress/controller/endpoint.go b/pkg/ingress/controller/endpoint.go
index 1406b72..b3bb3a4 100644
--- a/pkg/ingress/controller/endpoint.go
+++ b/pkg/ingress/controller/endpoint.go
@@ -152,14 +152,14 @@ func (c *endpointsController) syncToCluster(ctx context.Context, cluster apisix.
 		}
 	}
 
+	upstream.Nodes = nodes
+	upstream.FromKind = WatchFromKind
+
 	log.Debugw("upstream binds new nodes",
-		zap.String("upstream", upsName),
-		zap.Any("nodes", nodes),
+		zap.Any("upstream", upstream),
 		zap.String("cluster", cluster.String()),
 	)
 
-	upstream.Nodes = nodes
-	upstream.FromKind = WatchFromKind
 	upstreams := []*apisixv1.Upstream{upstream}
 	comb := state.ApisixCombination{Routes: nil, Services: nil, Upstreams: upstreams}
 
diff --git a/pkg/kube/apisix/apis/config/v1/types.go b/pkg/kube/apisix/apis/config/v1/types.go
index b9648ea..87ba3d7 100644
--- a/pkg/kube/apisix/apis/config/v1/types.go
+++ b/pkg/kube/apisix/apis/config/v1/types.go
@@ -16,6 +16,7 @@ package v1
 
 import (
 	"encoding/json"
+	"time"
 
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
@@ -103,6 +104,10 @@ type ApisixUpstreamConfig struct {
 	// Now value can be http, grpc.
 	// +optional
 	Scheme string `json:"scheme,omitempty" yaml:"scheme,omitempty"`
+
+	// The health check configurtions for the upstream.
+	// +optional
+	HealthCheck *HealthCheck `json:"healthCheck,omitempty" yaml:"healthCheck,omitempty"`
 }
 
 // PortLevelSettings configures the ApisixUpstreamConfig for each individual port. It inherits
@@ -125,6 +130,66 @@ type LoadBalancer struct {
 	Key string `json:"key,omitempty" yaml:"key,omitempty"`
 }
 
+// HealthCheck describes the upstream health check parameters.
+type HealthCheck struct {
+	Active  *ActiveHealthCheck  `json:"active" yaml:"active"`
+	Passive *PassiveHealthCheck `json:"passive,omitempty" yaml:"passive,omitempty"`
+}
+
+// ActiveHealthCheck defines the active kind of upstream health check.
+type ActiveHealthCheck struct {
+	Type           string                      `json:"type,omitempty" yaml:"type,omitempty"`
+	Timeout        time.Duration               `json:"timeout,omitempty" yaml:"timeout,omitempty"`
+	Concurrency    int                         `json:"concurrency,omitempty" yaml:"concurrency,omitempty"`
+	Host           string                      `json:"host,omitempty" yaml:"host,omitempty"`
+	Port           int32                       `json:"port,omitempty" yaml:"port,omitempty"`
+	HTTPPath       string                      `json:"httpPath,omitempty" yaml:"httpPath,omitempty"`
+	StrictTLS      *bool                       `json:"strictTLS,omitempty" yaml:"strictTLS,omitempty"`
+	RequestHeaders []string                    `json:"requestHeaders,omitempty" yaml:"requestHeaders,omitempty"`
+	Healthy        *ActiveHealthCheckHealthy   `json:"healthy,omitempty" yaml:"healthy,omitempty"`
+	Unhealthy      *ActiveHealthCheckUnhealthy `json:"unhealthy,omitempty" yaml:"unhealthy,omitempty"`
+}
+
+// PassiveHealthCheck defines the conditions to judge whether
+// an upstream node is healthy with the passive manager.
+type PassiveHealthCheck struct {
+	Type      string                       `json:"type,omitempty" yaml:"type,omitempty"`
+	Healthy   *PassiveHealthCheckHealthy   `json:"healthy,omitempty" yaml:"healthy,omitempty"`
+	Unhealthy *PassiveHealthCheckUnhealthy `json:"unhealthy,omitempty" yaml:"unhealthy,omitempty"`
+}
+
+// ActiveHealthCheckHealthy defines the conditions to judge whether
+// an upstream node is healthy with the active manner.
+type ActiveHealthCheckHealthy struct {
+	PassiveHealthCheckHealthy `json:",inline" yaml:",inline"`
+
+	Interval metav1.Duration `json:"interval,omitempty" yaml:"interval,omitempty"`
+}
+
+// ActiveHealthCheckUnhealthy defines the conditions to judge whether
+// an upstream node is unhealthy with the active mannger.
+type ActiveHealthCheckUnhealthy struct {
+	PassiveHealthCheckUnhealthy `json:",inline" yaml:",inline"`
+
+	Interval metav1.Duration `json:"interval,omitempty" yaml:"interval,omitempty"`
+}
+
+// PassiveHealthCheckHealthy defines the conditions to judge whether
+// an upstream node is healthy with the passive manner.
+type PassiveHealthCheckHealthy struct {
+	HTTPCodes []int `json:"httpCodes,omitempty" yaml:"httpCodes,omitempty"`
+	Successes int   `json:"successes,omitempty" yaml:"successes,omitempty"`
+}
+
+// PassiveHealthCheckUnhealthy defines the conditions to judge whether
+// an upstream node is unhealthy with the passive mannger.
+type PassiveHealthCheckUnhealthy struct {
+	HTTPCodes    []int         `json:"httpCodes,omitempty" yaml:"httpCodes,omitempty"`
+	HTTPFailures int           `json:"httpFailures,omitempty" yaml:"http_failures,omitempty"`
+	TCPFailures  int           `json:"tcpFailures,omitempty" yaml:"tcpFailures,omitempty"`
+	Timeout      time.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"`
+}
+
 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
 type ApisixUpstreamList struct {
 	metav1.TypeMeta `json:",inline"`
diff --git a/pkg/kube/apisix/apis/config/v1/zz_generated.deepcopy.go b/pkg/kube/apisix/apis/config/v1/zz_generated.deepcopy.go
index 82ccbb5..bff68aa 100644
--- a/pkg/kube/apisix/apis/config/v1/zz_generated.deepcopy.go
+++ b/pkg/kube/apisix/apis/config/v1/zz_generated.deepcopy.go
@@ -25,6 +25,76 @@ import (
 )
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ActiveHealthCheck) DeepCopyInto(out *ActiveHealthCheck) {
+	*out = *in
+	if in.StrictTLS != nil {
+		in, out := &in.StrictTLS, &out.StrictTLS
+		*out = new(bool)
+		**out = **in
+	}
+	if in.RequestHeaders != nil {
+		in, out := &in.RequestHeaders, &out.RequestHeaders
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	if in.Healthy != nil {
+		in, out := &in.Healthy, &out.Healthy
+		*out = new(ActiveHealthCheckHealthy)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Unhealthy != nil {
+		in, out := &in.Unhealthy, &out.Unhealthy
+		*out = new(ActiveHealthCheckUnhealthy)
+		(*in).DeepCopyInto(*out)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveHealthCheck.
+func (in *ActiveHealthCheck) DeepCopy() *ActiveHealthCheck {
+	if in == nil {
+		return nil
+	}
+	out := new(ActiveHealthCheck)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ActiveHealthCheckHealthy) DeepCopyInto(out *ActiveHealthCheckHealthy) {
+	*out = *in
+	in.PassiveHealthCheckHealthy.DeepCopyInto(&out.PassiveHealthCheckHealthy)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveHealthCheckHealthy.
+func (in *ActiveHealthCheckHealthy) DeepCopy() *ActiveHealthCheckHealthy {
+	if in == nil {
+		return nil
+	}
+	out := new(ActiveHealthCheckHealthy)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *ActiveHealthCheckUnhealthy) DeepCopyInto(out *ActiveHealthCheckUnhealthy) {
+	*out = *in
+	in.PassiveHealthCheckUnhealthy.DeepCopyInto(&out.PassiveHealthCheckUnhealthy)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ActiveHealthCheckUnhealthy.
+func (in *ActiveHealthCheckUnhealthy) DeepCopy() *ActiveHealthCheckUnhealthy {
+	if in == nil {
+		return nil
+	}
+	out := new(ActiveHealthCheckUnhealthy)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *ApisixRoute) DeepCopyInto(out *ApisixRoute) {
 	*out = *in
 	out.TypeMeta = in.TypeMeta
@@ -252,6 +322,11 @@ func (in *ApisixUpstreamConfig) DeepCopyInto(out *ApisixUpstreamConfig) {
 		*out = new(LoadBalancer)
 		**out = **in
 	}
+	if in.HealthCheck != nil {
+		in, out := &in.HealthCheck, &out.HealthCheck
+		*out = new(HealthCheck)
+		(*in).DeepCopyInto(*out)
+	}
 	return
 }
 
@@ -339,6 +414,32 @@ func (in *Backend) DeepCopy() *Backend {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *HealthCheck) DeepCopyInto(out *HealthCheck) {
+	*out = *in
+	if in.Active != nil {
+		in, out := &in.Active, &out.Active
+		*out = new(ActiveHealthCheck)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Passive != nil {
+		in, out := &in.Passive, &out.Passive
+		*out = new(PassiveHealthCheck)
+		(*in).DeepCopyInto(*out)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HealthCheck.
+func (in *HealthCheck) DeepCopy() *HealthCheck {
+	if in == nil {
+		return nil
+	}
+	out := new(HealthCheck)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Http) DeepCopyInto(out *Http) {
 	*out = *in
 	if in.Paths != nil {
@@ -378,6 +479,74 @@ func (in *LoadBalancer) DeepCopy() *LoadBalancer {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PassiveHealthCheck) DeepCopyInto(out *PassiveHealthCheck) {
+	*out = *in
+	if in.Healthy != nil {
+		in, out := &in.Healthy, &out.Healthy
+		*out = new(PassiveHealthCheckHealthy)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Unhealthy != nil {
+		in, out := &in.Unhealthy, &out.Unhealthy
+		*out = new(PassiveHealthCheckUnhealthy)
+		(*in).DeepCopyInto(*out)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PassiveHealthCheck.
+func (in *PassiveHealthCheck) DeepCopy() *PassiveHealthCheck {
+	if in == nil {
+		return nil
+	}
+	out := new(PassiveHealthCheck)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PassiveHealthCheckHealthy) DeepCopyInto(out *PassiveHealthCheckHealthy) {
+	*out = *in
+	if in.HTTPCodes != nil {
+		in, out := &in.HTTPCodes, &out.HTTPCodes
+		*out = make([]int, len(*in))
+		copy(*out, *in)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PassiveHealthCheckHealthy.
+func (in *PassiveHealthCheckHealthy) DeepCopy() *PassiveHealthCheckHealthy {
+	if in == nil {
+		return nil
+	}
+	out := new(PassiveHealthCheckHealthy)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *PassiveHealthCheckUnhealthy) DeepCopyInto(out *PassiveHealthCheckUnhealthy) {
+	*out = *in
+	if in.HTTPCodes != nil {
+		in, out := &in.HTTPCodes, &out.HTTPCodes
+		*out = make([]int, len(*in))
+		copy(*out, *in)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PassiveHealthCheckUnhealthy.
+func (in *PassiveHealthCheckUnhealthy) DeepCopy() *PassiveHealthCheckUnhealthy {
+	if in == nil {
+		return nil
+	}
+	out := new(PassiveHealthCheckUnhealthy)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *Path) DeepCopyInto(out *Path) {
 	*out = *in
 	out.Backend = in.Backend
diff --git a/pkg/kube/apisix_upstream.go b/pkg/kube/apisix_upstream.go
new file mode 100644
index 0000000..8697136
--- /dev/null
+++ b/pkg/kube/apisix_upstream.go
@@ -0,0 +1,260 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 kube
+
+import (
+	configv1 "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/apis/config/v1"
+	apisixv1 "github.com/apache/apisix-ingress-controller/pkg/types/apisix/v1"
+)
+
+func (t *translator) translateUpstreamScheme(scheme string, ups *apisixv1.Upstream) error {
+	if scheme == "" {
+		ups.Scheme = apisixv1.SchemeHTTP
+		return nil
+	}
+	switch scheme {
+	case apisixv1.SchemeHTTP, apisixv1.SchemeGRPC:
+		ups.Scheme = scheme
+		return nil
+	default:
+		return &translateError{field: "scheme", reason: "invalid value"}
+	}
+}
+
+func (t *translator) translateUpstreamLoadBalancer(lb *configv1.LoadBalancer, ups *apisixv1.Upstream) error {
+	if lb == nil || lb.Type == "" {
+		ups.Type = apisixv1.LbRoundRobin
+		return nil
+	}
+	switch lb.Type {
+	case apisixv1.LbRoundRobin, apisixv1.LbLeastConn, apisixv1.LbEwma:
+		ups.Type = lb.Type
+	case apisixv1.LbConsistentHash:
+		ups.Type = lb.Type
+		ups.Key = lb.Key
+		switch lb.HashOn {
+		case apisixv1.HashOnVars:
+			fallthrough
+		case apisixv1.HashOnHeader:
+			fallthrough
+		case apisixv1.HashOnCookie:
+			fallthrough
+		case apisixv1.HashOnConsumer:
+			fallthrough
+		case apisixv1.HashOnVarsCombination:
+			ups.HashOn = lb.HashOn
+		default:
+			return &translateError{field: "loadbalancer.hashOn", reason: "invalid value"}
+		}
+	default:
+		return &translateError{
+			field:  "loadbalancer.type",
+			reason: "invalid value",
+		}
+	}
+	return nil
+}
+
+func (t *translator) translateUpstreamHealthCheck(config *configv1.HealthCheck, ups *apisixv1.Upstream) error {
+	if config == nil || (config.Passive == nil && config.Active == nil) {
+		return nil
+	}
+	var hc apisixv1.UpstreamHealthCheck
+	if config.Passive != nil {
+		passive, err := t.translateUpstreamPassiveHealthCheck(config.Passive)
+		if err != nil {
+			return err
+		}
+		hc.Passive = passive
+	}
+
+	if config.Active != nil {
+		active, err := t.translateUpstreamActiveHealthCheck(config.Active)
+		if err != nil {
+			return err
+		}
+		hc.Active = active
+	} else {
+		return &translateError{
+			field:  "healthCheck.active",
+			reason: "not exist",
+		}
+	}
+
+	ups.Checks = &hc
+	return nil
+}
+
+func (t *translator) translateUpstreamActiveHealthCheck(config *configv1.ActiveHealthCheck) (*apisixv1.UpstreamActiveHealthCheck, error) {
+	var active apisixv1.UpstreamActiveHealthCheck
+	switch config.Type {
+	case apisixv1.HealthCheckHTTP, apisixv1.HealthCheckHTTPS, apisixv1.HealthCheckTCP:
+		active.Type = config.Type
+	case "":
+		active.Type = apisixv1.HealthCheckHTTP
+	default:
+		return nil, &translateError{
+			field:  "healthCheck.active.Type",
+			reason: "invalid value",
+		}
+	}
+
+	active.Timeout = int(config.Timeout.Seconds())
+	if config.Port < 0 || config.Port > 65535 {
+		return nil, &translateError{
+			field:  "healthCheck.active.port",
+			reason: "invalid value",
+		}
+	} else {
+		active.Port = config.Port
+	}
+	if config.Concurrency < 0 {
+		return nil, &translateError{
+			field:  "healthCheck.active.concurrency",
+			reason: "invalid value",
+		}
+	} else {
+		active.Concurrency = config.Concurrency
+	}
+	active.Host = config.Host
+	active.HTTPPath = config.HTTPPath
+	active.HTTPRequestHeaders = config.RequestHeaders
+
+	if config.StrictTLS == nil || *config.StrictTLS == true {
+		active.HTTPSVerifyCert = true
+	}
+
+	if config.Healthy != nil {
+		if config.Healthy.Successes < 0 || config.Healthy.Successes > apisixv1.HealthCheckMaxConsecutiveNumber {
+			return nil, &translateError{
+				field:  "healthCheck.active.healthy.successes",
+				reason: "invalid value",
+			}
+		}
+		active.Healthy.Successes = config.Healthy.Successes
+		if config.Healthy.HTTPCodes != nil && len(config.Healthy.HTTPCodes) < 1 {
+			return nil, &translateError{
+				field:  "healthCheck.active.healthy.httpCodes",
+				reason: "empty",
+			}
+		}
+		active.Healthy.HTTPStatuses = config.Healthy.HTTPCodes
+
+		if config.Healthy.Interval.Duration < apisixv1.ActiveHealthCheckMinInterval {
+			return nil, &translateError{
+				field:  "healthCheck.active.healthy.interval",
+				reason: "invalid value",
+			}
+		}
+		active.Healthy.Interval = int(config.Healthy.Interval.Seconds())
+	}
+
+	if config.Unhealthy != nil {
+		if config.Unhealthy.HTTPFailures < 0 || config.Unhealthy.HTTPFailures > apisixv1.HealthCheckMaxConsecutiveNumber {
+			return nil, &translateError{
+				field:  "healthCheck.active.unhealthy.httpFailures",
+				reason: "invalid value",
+			}
+		}
+		active.Unhealthy.HTTPFailures = config.Unhealthy.HTTPFailures
+
+		if config.Unhealthy.TCPFailures < 0 || config.Unhealthy.TCPFailures > apisixv1.HealthCheckMaxConsecutiveNumber {
+			return nil, &translateError{
+				field:  "healthCheck.active.unhealthy.tcpFailures",
+				reason: "invalid value",
+			}
+		}
+		active.Unhealthy.TCPFailures = config.Unhealthy.TCPFailures
+		active.Unhealthy.Timeouts = config.Unhealthy.Timeout.Seconds()
+
+		if config.Unhealthy.HTTPCodes != nil && len(config.Unhealthy.HTTPCodes) < 1 {
+			return nil, &translateError{
+				field:  "healthCheck.active.unhealthy.httpCodes",
+				reason: "empty",
+			}
+		}
+		active.Unhealthy.HTTPStatuses = config.Unhealthy.HTTPCodes
+
+		if config.Unhealthy.Interval.Duration < apisixv1.ActiveHealthCheckMinInterval {
+			return nil, &translateError{
+				field:  "healthCheck.active.unhealthy.interval",
+				reason: "invalid value",
+			}
+		}
+		active.Unhealthy.Interval = int(config.Unhealthy.Interval.Seconds())
+	}
+
+	return &active, nil
+}
+
+func (t *translator) translateUpstreamPassiveHealthCheck(config *configv1.PassiveHealthCheck) (*apisixv1.UpstreamPassiveHealthCheck, error) {
+	var passive apisixv1.UpstreamPassiveHealthCheck
+	switch config.Type {
+	case apisixv1.HealthCheckHTTP, apisixv1.HealthCheckHTTPS, apisixv1.HealthCheckTCP:
+		passive.Type = config.Type
+	case "":
+		passive.Type = apisixv1.HealthCheckHTTP
+	default:
+		return nil, &translateError{
+			field:  "healthCheck.passive.Type",
+			reason: "invalid value",
+		}
+	}
+	if config.Healthy != nil {
+		// zero means use the default value.
+		if config.Healthy.Successes < 0 || config.Healthy.Successes > apisixv1.HealthCheckMaxConsecutiveNumber {
+			return nil, &translateError{
+				field:  "healthCheck.passive.healthy.successes",
+				reason: "invalid value",
+			}
+		}
+		passive.Healthy.Successes = config.Healthy.Successes
+		if config.Healthy.HTTPCodes != nil && len(config.Healthy.HTTPCodes) < 1 {
+			return nil, &translateError{
+				field:  "healthCheck.passive.healthy.httpCodes",
+				reason: "empty",
+			}
+		}
+		passive.Healthy.HTTPStatuses = config.Healthy.HTTPCodes
+	}
+
+	if config.Unhealthy != nil {
+		if config.Unhealthy.HTTPFailures < 0 || config.Unhealthy.HTTPFailures > apisixv1.HealthCheckMaxConsecutiveNumber {
+			return nil, &translateError{
+				field:  "healthCheck.passive.unhealthy.httpFailures",
+				reason: "invalid value",
+			}
+		}
+		passive.Unhealthy.HTTPFailures = config.Unhealthy.HTTPFailures
+
+		if config.Unhealthy.TCPFailures < 0 || config.Unhealthy.TCPFailures > apisixv1.HealthCheckMaxConsecutiveNumber {
+			return nil, &translateError{
+				field:  "healthCheck.passive.unhealthy.tcpFailures",
+				reason: "invalid value",
+			}
+		}
+		passive.Unhealthy.TCPFailures = config.Unhealthy.TCPFailures
+		passive.Unhealthy.Timeouts = config.Unhealthy.Timeout.Seconds()
+
+		if config.Unhealthy.HTTPCodes != nil && len(config.Unhealthy.HTTPCodes) < 1 {
+			return nil, &translateError{
+				field:  "healthCheck.passive.unhealthy.httpCodes",
+				reason: "empty",
+			}
+		}
+		passive.Unhealthy.HTTPStatuses = config.Unhealthy.HTTPCodes
+	}
+	return &passive, nil
+}
diff --git a/pkg/kube/apisix_upstream_test.go b/pkg/kube/apisix_upstream_test.go
new file mode 100644
index 0000000..db0344a
--- /dev/null
+++ b/pkg/kube/apisix_upstream_test.go
@@ -0,0 +1,371 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 kube
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	configv1 "github.com/apache/apisix-ingress-controller/pkg/kube/apisix/apis/config/v1"
+	apisixv1 "github.com/apache/apisix-ingress-controller/pkg/types/apisix/v1"
+)
+
+func TestTranslateUpstreamHealthCheck(t *testing.T) {
+	tr := &translator{}
+	hc := &configv1.HealthCheck{
+		Active: &configv1.ActiveHealthCheck{
+			Type:        apisixv1.HealthCheckHTTP,
+			Timeout:     5 * time.Second,
+			Concurrency: 2,
+			HTTPPath:    "/healthz",
+			Unhealthy: &configv1.ActiveHealthCheckUnhealthy{
+				PassiveHealthCheckUnhealthy: configv1.PassiveHealthCheckUnhealthy{
+					HTTPCodes: []int{500, 502, 504},
+				},
+				Interval: metav1.Duration{Duration: time.Second},
+			},
+			Healthy: &configv1.ActiveHealthCheckHealthy{
+				PassiveHealthCheckHealthy: configv1.PassiveHealthCheckHealthy{
+					HTTPCodes: []int{200},
+					Successes: 2,
+				},
+				Interval: metav1.Duration{Duration: 3 * time.Second},
+			},
+		},
+		Passive: &configv1.PassiveHealthCheck{
+			Type: apisixv1.HealthCheckHTTP,
+			Healthy: &configv1.PassiveHealthCheckHealthy{
+				HTTPCodes: []int{200},
+				Successes: 2,
+			},
+			Unhealthy: &configv1.PassiveHealthCheckUnhealthy{
+				HTTPCodes: []int{500},
+			},
+		},
+	}
+
+	var ups apisixv1.Upstream
+	err := tr.translateUpstreamHealthCheck(hc, &ups)
+	assert.Nil(t, err, "translating upstream health check")
+	assert.Equal(t, ups.Checks.Active, &apisixv1.UpstreamActiveHealthCheck{
+		Type:            apisixv1.HealthCheckHTTP,
+		Timeout:         5,
+		Concurrency:     2,
+		HTTPPath:        "/healthz",
+		HTTPSVerifyCert: true,
+		Healthy: apisixv1.UpstreamActiveHealthCheckHealthy{
+			Interval: 3,
+			UpstreamPassiveHealthCheckHealthy: apisixv1.UpstreamPassiveHealthCheckHealthy{
+				HTTPStatuses: []int{200},
+				Successes:    2,
+			},
+		},
+		Unhealthy: apisixv1.UpstreamActiveHealthCheckUnhealthy{
+			Interval: 1,
+			UpstreamPassiveHealthCheckUnhealthy: apisixv1.UpstreamPassiveHealthCheckUnhealthy{
+				HTTPStatuses: []int{500, 502, 504},
+			},
+		},
+	})
+	assert.Equal(t, ups.Checks.Passive, &apisixv1.UpstreamPassiveHealthCheck{
+		Type: apisixv1.HealthCheckHTTP,
+		Healthy: apisixv1.UpstreamPassiveHealthCheckHealthy{
+			Successes:    2,
+			HTTPStatuses: []int{200},
+		},
+		Unhealthy: apisixv1.UpstreamPassiveHealthCheckUnhealthy{
+			HTTPStatuses: []int{500},
+		},
+	})
+}
+
+func TestTranslateUpstreamPassiveHealthCheckUnusually(t *testing.T) {
+	tr := &translator{}
+
+	// invalid passive health check type
+	hc := &configv1.HealthCheck{
+		Passive: &configv1.PassiveHealthCheck{
+			Type: "redis",
+		},
+	}
+
+	err := tr.translateUpstreamHealthCheck(hc, nil)
+	assert.Equal(t, err, &translateError{
+		field:  "healthCheck.passive.Type",
+		reason: "invalid value",
+	})
+
+	// invalid passive health check healthy successes
+	hc = &configv1.HealthCheck{
+		Passive: &configv1.PassiveHealthCheck{
+			Type: "http",
+			Healthy: &configv1.PassiveHealthCheckHealthy{
+				Successes: -1,
+			},
+		},
+	}
+	err = tr.translateUpstreamHealthCheck(hc, nil)
+	assert.Equal(t, err, &translateError{
+		field:  "healthCheck.passive.healthy.successes",
+		reason: "invalid value",
+	})
+
+	// empty passive health check healthy httpCodes.
+	hc = &configv1.HealthCheck{
+		Passive: &configv1.PassiveHealthCheck{
+			Type: "http",
+			Healthy: &configv1.PassiveHealthCheckHealthy{
+				Successes: 1,
+				HTTPCodes: []int{},
+			},
+		},
+	}
+	err = tr.translateUpstreamHealthCheck(hc, nil)
+	assert.Equal(t, err, &translateError{
+		field:  "healthCheck.passive.healthy.httpCodes",
+		reason: "empty",
+	})
+
+	// empty passive health check unhealthy httpFailures.
+	hc = &configv1.HealthCheck{
+		Passive: &configv1.PassiveHealthCheck{
+			Type: "http",
+			Unhealthy: &configv1.PassiveHealthCheckUnhealthy{
+				HTTPFailures: -1,
+			},
+		},
+	}
+	err = tr.translateUpstreamHealthCheck(hc, nil)
+	assert.Equal(t, err, &translateError{
+		field:  "healthCheck.passive.unhealthy.httpFailures",
+		reason: "invalid value",
+	})
+
+	// empty passive health check unhealthy tcpFailures.
+	hc = &configv1.HealthCheck{
+		Passive: &configv1.PassiveHealthCheck{
+			Type: "http",
+			Unhealthy: &configv1.PassiveHealthCheckUnhealthy{
+				TCPFailures: -1,
+			},
+		},
+	}
+	err = tr.translateUpstreamHealthCheck(hc, nil)
+	assert.Equal(t, err, &translateError{
+		field:  "healthCheck.passive.unhealthy.tcpFailures",
+		reason: "invalid value",
+	})
+
+	// empty passive health check unhealthy httpCodes.
+	hc = &configv1.HealthCheck{
+		Passive: &configv1.PassiveHealthCheck{
+			Type: "http",
+			Unhealthy: &configv1.PassiveHealthCheckUnhealthy{
+				HTTPCodes: []int{},
+			},
+		},
+	}
+	err = tr.translateUpstreamHealthCheck(hc, nil)
+	assert.Equal(t, err, &translateError{
+		field:  "healthCheck.passive.unhealthy.httpCodes",
+		reason: "empty",
+	})
+}
+
+func TestTranslateUpstreamActiveHealthCheckUnusually(t *testing.T) {
+	tr := &translator{}
+
+	// invalid active health check type
+	hc := &configv1.HealthCheck{
+		Active: &configv1.ActiveHealthCheck{
+			Type: "redis",
+		},
+	}
+	err := tr.translateUpstreamHealthCheck(hc, nil)
+	assert.Equal(t, err, &translateError{
+		field:  "healthCheck.active.Type",
+		reason: "invalid value",
+	})
+
+	// invalid active health check port value
+	hc = &configv1.HealthCheck{
+		Active: &configv1.ActiveHealthCheck{
+			Type: "http",
+			Port: 65536,
+		},
+	}
+	err = tr.translateUpstreamHealthCheck(hc, nil)
+	assert.Equal(t, err, &translateError{
+		field:  "healthCheck.active.port",
+		reason: "invalid value",
+	})
+
+	// invalid active health check concurrency value
+	hc = &configv1.HealthCheck{
+		Active: &configv1.ActiveHealthCheck{
+			Type:        "https",
+			Concurrency: -1,
+		},
+	}
+	err = tr.translateUpstreamHealthCheck(hc, nil)
+	assert.Equal(t, err, &translateError{
+		field:  "healthCheck.active.concurrency",
+		reason: "invalid value",
+	})
+
+	// invalid active health check healthy successes value
+	hc = &configv1.HealthCheck{
+		Active: &configv1.ActiveHealthCheck{
+			Type: "https",
+			Healthy: &configv1.ActiveHealthCheckHealthy{
+				PassiveHealthCheckHealthy: configv1.PassiveHealthCheckHealthy{
+					Successes: -1,
+				},
+			},
+		},
+	}
+	err = tr.translateUpstreamHealthCheck(hc, nil)
+	assert.Equal(t, err, &translateError{
+		field:  "healthCheck.active.healthy.successes",
+		reason: "invalid value",
+	})
+
+	// invalid active health check healthy successes value
+	hc = &configv1.HealthCheck{
+		Active: &configv1.ActiveHealthCheck{
+			Type: "https",
+			Healthy: &configv1.ActiveHealthCheckHealthy{
+				PassiveHealthCheckHealthy: configv1.PassiveHealthCheckHealthy{
+					HTTPCodes: []int{},
+				},
+			},
+		},
+	}
+	err = tr.translateUpstreamHealthCheck(hc, nil)
+	assert.Equal(t, err, &translateError{
+		field:  "healthCheck.active.healthy.httpCodes",
+		reason: "empty",
+	})
+
+	// invalid active health check healthy interval
+	hc = &configv1.HealthCheck{
+		Active: &configv1.ActiveHealthCheck{
+			Type: "https",
+			Healthy: &configv1.ActiveHealthCheckHealthy{
+				Interval: metav1.Duration{Duration: 500 * time.Millisecond},
+			},
+		},
+	}
+	err = tr.translateUpstreamHealthCheck(hc, nil)
+	assert.Equal(t, err, &translateError{
+		field:  "healthCheck.active.healthy.interval",
+		reason: "invalid value",
+	})
+
+	// missing active health check healthy interval
+	hc = &configv1.HealthCheck{
+		Active: &configv1.ActiveHealthCheck{
+			Type:    "https",
+			Healthy: &configv1.ActiveHealthCheckHealthy{},
+		},
+	}
+	err = tr.translateUpstreamHealthCheck(hc, nil)
+	assert.Equal(t, err, &translateError{
+		field:  "healthCheck.active.healthy.interval",
+		reason: "invalid value",
+	})
+
+	// invalid active health check unhealthy httpFailures
+	hc = &configv1.HealthCheck{
+		Active: &configv1.ActiveHealthCheck{
+			Type: "https",
+			Unhealthy: &configv1.ActiveHealthCheckUnhealthy{
+				PassiveHealthCheckUnhealthy: configv1.PassiveHealthCheckUnhealthy{
+					HTTPFailures: -1,
+				},
+			},
+		},
+	}
+	err = tr.translateUpstreamHealthCheck(hc, nil)
+	assert.Equal(t, err, &translateError{
+		field:  "healthCheck.active.unhealthy.httpFailures",
+		reason: "invalid value",
+	})
+
+	// invalid active health check unhealthy tcpFailures
+	hc = &configv1.HealthCheck{
+		Active: &configv1.ActiveHealthCheck{
+			Type: "https",
+			Unhealthy: &configv1.ActiveHealthCheckUnhealthy{
+				PassiveHealthCheckUnhealthy: configv1.PassiveHealthCheckUnhealthy{
+					TCPFailures: -1,
+				},
+			},
+		},
+	}
+	err = tr.translateUpstreamHealthCheck(hc, nil)
+	assert.Equal(t, err, &translateError{
+		field:  "healthCheck.active.unhealthy.tcpFailures",
+		reason: "invalid value",
+	})
+
+	// invalid active health check unhealthy httpCodes
+	hc = &configv1.HealthCheck{
+		Active: &configv1.ActiveHealthCheck{
+			Type: "https",
+			Unhealthy: &configv1.ActiveHealthCheckUnhealthy{
+				PassiveHealthCheckUnhealthy: configv1.PassiveHealthCheckUnhealthy{
+					HTTPCodes: []int{},
+				},
+			},
+		},
+	}
+	err = tr.translateUpstreamHealthCheck(hc, nil)
+	assert.Equal(t, err, &translateError{
+		field:  "healthCheck.active.unhealthy.httpCodes",
+		reason: "empty",
+	})
+
+	// invalid active health check unhealthy interval
+	hc = &configv1.HealthCheck{
+		Active: &configv1.ActiveHealthCheck{
+			Type: "https",
+			Unhealthy: &configv1.ActiveHealthCheckUnhealthy{
+				Interval: metav1.Duration{Duration: 500 * time.Millisecond},
+			},
+		},
+	}
+	err = tr.translateUpstreamHealthCheck(hc, nil)
+	assert.Equal(t, err, &translateError{
+		field:  "healthCheck.active.unhealthy.interval",
+		reason: "invalid value",
+	})
+
+	// missing active health check unhealthy interval
+	hc = &configv1.HealthCheck{
+		Active: &configv1.ActiveHealthCheck{
+			Type:      "https",
+			Unhealthy: &configv1.ActiveHealthCheckUnhealthy{},
+		},
+	}
+	err = tr.translateUpstreamHealthCheck(hc, nil)
+	assert.Equal(t, err, &translateError{
+		field:  "healthCheck.active.unhealthy.interval",
+		reason: "invalid value",
+	})
+}
diff --git a/pkg/kube/translator.go b/pkg/kube/translator.go
index 226bccd..1e940b1 100644
--- a/pkg/kube/translator.go
+++ b/pkg/kube/translator.go
@@ -74,47 +74,14 @@ func NewTranslator(opts *TranslatorOptions) Translator {
 
 func (t *translator) TranslateUpstreamConfig(au *configv1.ApisixUpstreamConfig) (*apisixv1.Upstream, error) {
 	ups := apisixv1.NewDefaultUpstream()
-
-	if au.Scheme == "" {
-		au.Scheme = apisixv1.SchemeHTTP
-	} else {
-		switch au.Scheme {
-		case apisixv1.SchemeHTTP, apisixv1.SchemeGRPC:
-			ups.Scheme = au.Scheme
-		default:
-			return nil, &translateError{field: "scheme", reason: "invalid value"}
-		}
+	if err := t.translateUpstreamScheme(au.Scheme, ups); err != nil {
+		return nil, err
 	}
-
-	if au.LoadBalancer == nil || au.LoadBalancer.Type == "" {
-		ups.Type = apisixv1.LbRoundRobin
-	} else {
-		switch au.LoadBalancer.Type {
-		case apisixv1.LbRoundRobin, apisixv1.LbLeastConn, apisixv1.LbEwma:
-			ups.Type = au.LoadBalancer.Type
-		case apisixv1.LbConsistentHash:
-			ups.Type = au.LoadBalancer.Type
-			ups.Key = au.LoadBalancer.Key
-			switch au.LoadBalancer.HashOn {
-			case apisixv1.HashOnVars:
-				fallthrough
-			case apisixv1.HashOnHeader:
-				fallthrough
-			case apisixv1.HashOnCookie:
-				fallthrough
-			case apisixv1.HashOnConsumer:
-				fallthrough
-			case apisixv1.HashOnVarsCombination:
-				ups.HashOn = au.LoadBalancer.HashOn
-			default:
-				return nil, &translateError{field: "loadbalancer.hashOn", reason: "invalid value"}
-			}
-		default:
-			return nil, &translateError{
-				field:  "loadbalancer.type",
-				reason: "invalid value",
-			}
-		}
+	if err := t.translateUpstreamLoadBalancer(au.LoadBalancer, ups); err != nil {
+		return nil, err
+	}
+	if err := t.translateUpstreamHealthCheck(au.HealthCheck, ups); err != nil {
+		return nil, err
 	}
 	return ups, nil
 }
diff --git a/pkg/types/apisix/v1/types.go b/pkg/types/apisix/v1/types.go
index 549e39f..1186fe5 100644
--- a/pkg/types/apisix/v1/types.go
+++ b/pkg/types/apisix/v1/types.go
@@ -18,6 +18,7 @@ import (
 	"bytes"
 	"encoding/json"
 	"strconv"
+	"time"
 )
 
 const (
@@ -47,6 +48,20 @@ const (
 	SchemeHTTP = "http"
 	// SchemeGRPC represents the GRPC protocol.
 	SchemeGRPC = "grpc"
+
+	// HealthCheckHTTP represents the HTTP kind health check.
+	HealthCheckHTTP = "http"
+	// HealthCheckHTTPS represents the HTTPS kind health check.
+	HealthCheckHTTPS = "https"
+	// HealthCheckTCP represents the TCP kind health check.
+	HealthCheckTCP = "tcp"
+
+	// HealthCheckMaxConsecutiveNumber is the max number for
+	// the consecutive success/failure in upstream health check.
+	HealthCheckMaxConsecutiveNumber = 254
+	// ActiveHealthCheckMinInterval is the minimum interval for
+	// the active health check.
+	ActiveHealthCheckMinInterval = time.Second
 )
 
 // Metadata contains all meta information about resources.
@@ -108,12 +123,13 @@ type Service struct {
 type Upstream struct {
 	Metadata `json:",inline" yaml:",inline"`
 
-	Type     string         `json:"type,omitempty" yaml:"type,omitempty"`
-	HashOn   string         `json:"hash_on,omitemtpy" yaml:"hash_on,omitempty"`
-	Key      string         `json:"key,omitempty" yaml:"key,omitempty"`
-	Nodes    []UpstreamNode `json:"nodes,omitempty" yaml:"nodes,omitempty"`
-	FromKind string         `json:"from_kind,omitempty" yaml:"from_kind,omitempty"`
-	Scheme   string         `json:"scheme,omitempty" yaml:"scheme,omitempty"`
+	Type     string               `json:"type,omitempty" yaml:"type,omitempty"`
+	HashOn   string               `json:"hash_on,omitemtpy" yaml:"hash_on,omitempty"`
+	Key      string               `json:"key,omitempty" yaml:"key,omitempty"`
+	Checks   *UpstreamHealthCheck `json:"checks,omitempty" yaml:"checks,omitempty"`
+	Nodes    []UpstreamNode       `json:"nodes,omitempty" yaml:"nodes,omitempty"`
+	FromKind string               `json:"from_kind,omitempty" yaml:"from_kind,omitempty"`
+	Scheme   string               `json:"scheme,omitempty" yaml:"scheme,omitempty"`
 }
 
 // Node the node in upstream
@@ -124,6 +140,74 @@ type UpstreamNode struct {
 	Weight int    `json:"weight,omitempty" yaml:"weight,omitempty"`
 }
 
+// UpstreamHealthCheck defines the active and/or passive health check for an Upstream,
+// with the upstream health check feature, pods can be kicked out or joined in quickly,
+// if the feedback of Kubernetes liveness/readiness probe is long.
+// +k8s:deepcopy-gen=true
+type UpstreamHealthCheck struct {
+	Active  *UpstreamActiveHealthCheck  `json:"active" yaml:"active"`
+	Passive *UpstreamPassiveHealthCheck `json:"passive,omitempty" yaml:"passive,omitempty"`
+}
+
+// UpstreamActiveHealthCheck defines the active kind of upstream health check.
+// +k8s:deepcopy-gen=true
+type UpstreamActiveHealthCheck struct {
+	Type               string                             `json:"type,omitempty" yaml:"type,omitempty"`
+	Timeout            int                                `json:"timeout,omitempty" yaml:"timeout,omitempty"`
+	Concurrency        int                                `json:"concurrency,omitempty" yaml:"concurrency,omitempty"`
+	Host               string                             `json:"host,omitempty" yaml:"host,omitempty"`
+	Port               int32                              `json:"port,omitempty" yaml:"port,omitempty"`
+	HTTPPath           string                             `json:"http_path,omitempty" yaml:"http_path,omitempty"`
+	HTTPSVerifyCert    bool                               `json:"https_verify_certificate,omitempty" yaml:"https_verify_certificate,omitempty"`
+	HTTPRequestHeaders []string                           `json:"req_headers,omitempty" yaml:"req_headers,omitempty"`
+	Healthy            UpstreamActiveHealthCheckHealthy   `json:"healthy,omitempty" yaml:"healthy,omitempty"`
+	Unhealthy          UpstreamActiveHealthCheckUnhealthy `json:"unhealthy,omitempty" yaml:"unhealthy,omitempty"`
+}
+
+// UpstreamPassiveHealthCheck defines the passive kind of upstream health check.
+// +k8s:deepcopy-gen=true
+type UpstreamPassiveHealthCheck struct {
+	Type      string                              `json:"type,omitempty" yaml:"type,omitempty"`
+	Healthy   UpstreamPassiveHealthCheckHealthy   `json:"healthy,omitempty" yaml:"healthy,omitempty"`
+	Unhealthy UpstreamPassiveHealthCheckUnhealthy `json:"unhealthy,omitempty" yaml:"unhealthy,omitempty"`
+}
+
+// UpstreamActiveHealthCheckHealthy defines the conditions to judge whether
+// an upstream node is healthy with the active manner.
+// +k8s:deepcopy-gen=true
+type UpstreamActiveHealthCheckHealthy struct {
+	UpstreamPassiveHealthCheckHealthy `json:",inline" yaml:",inline"`
+
+	Interval int `json:"interval,omitempty" yaml:"interval,omitempty"`
+}
+
+// UpstreamPassiveHealthCheckHealthy defines the conditions to judge whether
+// an upstream node is healthy with the passive manner.
+// +k8s:deepcopy-gen=true
+type UpstreamPassiveHealthCheckHealthy struct {
+	HTTPStatuses []int `json:"http_statuses,omitempty" yaml:"http_statuses,omitempty"`
+	Successes    int   `json:"successes,omitempty" yaml:"successes,omitempty"`
+}
+
+// UpstreamActiveHealthCheckUnhealthy defines the conditions to judge whether
+// an upstream node is unhealthy with the active mannger.
+// +k8s:deepcopy-gen=true
+type UpstreamActiveHealthCheckUnhealthy struct {
+	UpstreamPassiveHealthCheckUnhealthy `json:",inline" yaml:",inline"`
+
+	Interval int `json:"interval,omitempty" yaml:"interval,omitempty"`
+}
+
+// UpstreamPassiveHealthCheckUnhealthy defines the conditions to judge whether
+// an upstream node is unhealthy with the passive mannger.
+// +k8s:deepcopy-gen=true
+type UpstreamPassiveHealthCheckUnhealthy struct {
+	HTTPStatuses []int   `json:"http_statuses,omitempty" yaml:"http_statuses,omitempty"`
+	HTTPFailures int     `json:"http_failures,omitempty" yaml:"http_failures,omitempty"`
+	TCPFailures  int     `json:"tcp_failures,omitempty" yaml:"tcp_failures,omitempty"`
+	Timeouts     float64 `json:"timeouts,omitempty" yaml:"timeouts,omitempty"`
+}
+
 // Ssl apisix ssl object
 // +k8s:deepcopy-gen=true
 type Ssl struct {
diff --git a/pkg/types/apisix/v1/zz_generated.deepcopy.go b/pkg/types/apisix/v1/zz_generated.deepcopy.go
index 78ee480..41b461c 100644
--- a/pkg/types/apisix/v1/zz_generated.deepcopy.go
+++ b/pkg/types/apisix/v1/zz_generated.deepcopy.go
@@ -85,6 +85,11 @@ func (in *Ssl) DeepCopy() *Ssl {
 func (in *Upstream) DeepCopyInto(out *Upstream) {
 	*out = *in
 	out.Metadata = in.Metadata
+	if in.Checks != nil {
+		in, out := &in.Checks, &out.Checks
+		*out = new(UpstreamHealthCheck)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.Nodes != nil {
 		in, out := &in.Nodes, &out.Nodes
 		*out = make([]UpstreamNode, len(*in))
@@ -104,6 +109,89 @@ func (in *Upstream) DeepCopy() *Upstream {
 }
 
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *UpstreamActiveHealthCheck) DeepCopyInto(out *UpstreamActiveHealthCheck) {
+	*out = *in
+	if in.HTTPRequestHeaders != nil {
+		in, out := &in.HTTPRequestHeaders, &out.HTTPRequestHeaders
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+	in.Healthy.DeepCopyInto(&out.Healthy)
+	in.Unhealthy.DeepCopyInto(&out.Unhealthy)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpstreamActiveHealthCheck.
+func (in *UpstreamActiveHealthCheck) DeepCopy() *UpstreamActiveHealthCheck {
+	if in == nil {
+		return nil
+	}
+	out := new(UpstreamActiveHealthCheck)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *UpstreamActiveHealthCheckHealthy) DeepCopyInto(out *UpstreamActiveHealthCheckHealthy) {
+	*out = *in
+	in.UpstreamPassiveHealthCheckHealthy.DeepCopyInto(&out.UpstreamPassiveHealthCheckHealthy)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpstreamActiveHealthCheckHealthy.
+func (in *UpstreamActiveHealthCheckHealthy) DeepCopy() *UpstreamActiveHealthCheckHealthy {
+	if in == nil {
+		return nil
+	}
+	out := new(UpstreamActiveHealthCheckHealthy)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *UpstreamActiveHealthCheckUnhealthy) DeepCopyInto(out *UpstreamActiveHealthCheckUnhealthy) {
+	*out = *in
+	in.UpstreamPassiveHealthCheckUnhealthy.DeepCopyInto(&out.UpstreamPassiveHealthCheckUnhealthy)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpstreamActiveHealthCheckUnhealthy.
+func (in *UpstreamActiveHealthCheckUnhealthy) DeepCopy() *UpstreamActiveHealthCheckUnhealthy {
+	if in == nil {
+		return nil
+	}
+	out := new(UpstreamActiveHealthCheckUnhealthy)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *UpstreamHealthCheck) DeepCopyInto(out *UpstreamHealthCheck) {
+	*out = *in
+	if in.Active != nil {
+		in, out := &in.Active, &out.Active
+		*out = new(UpstreamActiveHealthCheck)
+		(*in).DeepCopyInto(*out)
+	}
+	if in.Passive != nil {
+		in, out := &in.Passive, &out.Passive
+		*out = new(UpstreamPassiveHealthCheck)
+		(*in).DeepCopyInto(*out)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpstreamHealthCheck.
+func (in *UpstreamHealthCheck) DeepCopy() *UpstreamHealthCheck {
+	if in == nil {
+		return nil
+	}
+	out := new(UpstreamHealthCheck)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *UpstreamNode) DeepCopyInto(out *UpstreamNode) {
 	*out = *in
 	return
@@ -118,3 +206,63 @@ func (in *UpstreamNode) DeepCopy() *UpstreamNode {
 	in.DeepCopyInto(out)
 	return out
 }
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *UpstreamPassiveHealthCheck) DeepCopyInto(out *UpstreamPassiveHealthCheck) {
+	*out = *in
+	in.Healthy.DeepCopyInto(&out.Healthy)
+	in.Unhealthy.DeepCopyInto(&out.Unhealthy)
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpstreamPassiveHealthCheck.
+func (in *UpstreamPassiveHealthCheck) DeepCopy() *UpstreamPassiveHealthCheck {
+	if in == nil {
+		return nil
+	}
+	out := new(UpstreamPassiveHealthCheck)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *UpstreamPassiveHealthCheckHealthy) DeepCopyInto(out *UpstreamPassiveHealthCheckHealthy) {
+	*out = *in
+	if in.HTTPStatuses != nil {
+		in, out := &in.HTTPStatuses, &out.HTTPStatuses
+		*out = make([]int, len(*in))
+		copy(*out, *in)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpstreamPassiveHealthCheckHealthy.
+func (in *UpstreamPassiveHealthCheckHealthy) DeepCopy() *UpstreamPassiveHealthCheckHealthy {
+	if in == nil {
+		return nil
+	}
+	out := new(UpstreamPassiveHealthCheckHealthy)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *UpstreamPassiveHealthCheckUnhealthy) DeepCopyInto(out *UpstreamPassiveHealthCheckUnhealthy) {
+	*out = *in
+	if in.HTTPStatuses != nil {
+		in, out := &in.HTTPStatuses, &out.HTTPStatuses
+		*out = make([]int, len(*in))
+		copy(*out, *in)
+	}
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpstreamPassiveHealthCheckUnhealthy.
+func (in *UpstreamPassiveHealthCheckUnhealthy) DeepCopy() *UpstreamPassiveHealthCheckUnhealthy {
+	if in == nil {
+		return nil
+	}
+	out := new(UpstreamPassiveHealthCheckUnhealthy)
+	in.DeepCopyInto(out)
+	return out
+}
diff --git a/test/e2e/features/healthcheck.go b/test/e2e/features/healthcheck.go
new file mode 100644
index 0000000..2751096
--- /dev/null
+++ b/test/e2e/features/healthcheck.go
@@ -0,0 +1,140 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You 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 features
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/onsi/ginkgo"
+	"github.com/stretchr/testify/assert"
+
+	"github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
+)
+
+var _ = ginkgo.Describe("health check", func() {
+	s := scaffold.NewDefaultScaffold()
+	ginkgo.It("active check", func() {
+		backendSvc, backendPorts := s.DefaultHTTPBackend()
+
+		au := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v1
+kind: ApisixUpstream
+metadata:
+  name: %s
+spec:
+  healthCheck:
+    active:
+      type: http
+      httpPath: /status/502
+      healthy:
+        httpCodes: [200]
+        httpFailures: 2
+        interval: 1s
+      unhealthy:
+        interval: 1s
+`, backendSvc)
+		err := s.CreateResourceFromString(au)
+		assert.Nil(ginkgo.GinkgoT(), err, "create ApisixUpstream")
+
+		ar := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v1
+kind: ApisixRoute
+metadata:
+ name: httpbin-route
+spec:
+ rules:
+ - host: httpbin.org
+   http:
+     paths:
+     - backend:
+         serviceName: %s
+         servicePort: %d
+       path: /*
+`, backendSvc, backendPorts[0])
+		err = s.CreateResourceFromString(ar)
+		assert.Nil(ginkgo.GinkgoT(), err)
+
+		time.Sleep(3 * time.Second)
+
+		ups, err := s.ListApisixUpstreams()
+		assert.Nil(ginkgo.GinkgoT(), err, nil, "listing upstreams")
+		assert.Len(ginkgo.GinkgoT(), ups, 1)
+		assert.Equal(ginkgo.GinkgoT(), ups[0].Checks.Active.Healthy.Interval, 1)
+		assert.Equal(ginkgo.GinkgoT(), ups[0].Checks.Active.Healthy.HTTPStatuses, []int{200})
+		assert.Equal(ginkgo.GinkgoT(), ups[0].Checks.Active.Unhealthy.Interval, 1)
+		assert.Equal(ginkgo.GinkgoT(), ups[0].Checks.Active.Unhealthy.Interval, 1)
+
+		// It's difficult to test healthchecker since we cannot let partial httpbin endpoints
+		// down, if all of them are down, apisix in turn uses all of them.
+	})
+
+	ginkgo.It("passive check", func() {
+		backendSvc, backendPorts := s.DefaultHTTPBackend()
+
+		au := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v1
+kind: ApisixUpstream
+metadata:
+  name: %s
+spec:
+  healthCheck:
+    active:
+      type: http
+      httpPath: /status/200
+      healthy:
+        httpCodes: [200]
+        httpFailures: 2
+        interval: 1s
+      unhealthy:
+        interval: 1s
+    passive:
+      healthy:
+        httpCodes: [200]
+      unhealthy:
+        httpCodes: [502]
+`, backendSvc)
+		err := s.CreateResourceFromString(au)
+		assert.Nil(ginkgo.GinkgoT(), err, "create ApisixUpstream")
+
+		ar := fmt.Sprintf(`
+apiVersion: apisix.apache.org/v1
+kind: ApisixRoute
+metadata:
+ name: httpbin-route
+spec:
+ rules:
+ - host: httpbin.org
+   http:
+     paths:
+     - backend:
+         serviceName: %s
+         servicePort: %d
+       path: /*
+`, backendSvc, backendPorts[0])
+		err = s.CreateResourceFromString(ar)
+		assert.Nil(ginkgo.GinkgoT(), err)
+
+		time.Sleep(3 * time.Second)
+		ups, err := s.ListApisixUpstreams()
+		assert.Nil(ginkgo.GinkgoT(), err, nil, "listing upstreams")
+		assert.Len(ginkgo.GinkgoT(), ups, 1)
+		assert.Equal(ginkgo.GinkgoT(), ups[0].Checks.Active.Healthy.Interval, 1)
+		assert.Equal(ginkgo.GinkgoT(), ups[0].Checks.Active.Healthy.HTTPStatuses, []int{200})
+		assert.Equal(ginkgo.GinkgoT(), ups[0].Checks.Active.Unhealthy.Interval, 1)
+		assert.Equal(ginkgo.GinkgoT(), ups[0].Checks.Passive.Healthy.HTTPStatuses, []int{200})
+		assert.Equal(ginkgo.GinkgoT(), ups[0].Checks.Passive.Unhealthy.HTTPStatuses, []int{502})
+	})
+})
diff --git a/test/e2e/go.mod b/test/e2e/go.mod
index 68e421b..410a084 100644
--- a/test/e2e/go.mod
+++ b/test/e2e/go.mod
@@ -8,7 +8,6 @@ require (
 	github.com/gruntwork-io/terratest v0.31.2
 	github.com/onsi/ginkgo v1.14.2
 	github.com/stretchr/testify v1.6.1
-	google.golang.org/grpc v1.27.1
 	k8s.io/api v0.20.2
 	k8s.io/apimachinery v0.20.2
 )