pkg/manifests/nginx.go (515 lines of code) (raw):

// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. package manifests import ( "path" "strconv" "github.com/Azure/aks-app-routing-operator/pkg/config" "github.com/Azure/aks-app-routing-operator/pkg/util" appsv1 "k8s.io/api/apps/v1" autov1 "k8s.io/api/autoscaling/v1" corev1 "k8s.io/api/core/v1" netv1 "k8s.io/api/networking/v1" policyv1 "k8s.io/api/policy/v1" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" ) var ( nginx1_11_5 = NginxIngressVersion{ name: "v1.11.5", tag: "v1.11.5", } nginxVersionsAscending = []NginxIngressVersion{nginx1_11_5} LatestNginxVersion = nginxVersionsAscending[len(nginxVersionsAscending)-1] ) var nginxLabels = util.MergeMaps( map[string]string{ k8sNameKey: "nginx", }, GetTopLevelLabels(), ) const ( prom = "prometheus" IngressControllerComponentName = "ingress-controller" ) var ( promServicePort = corev1.ServicePort{ Name: prom, Port: 10254, TargetPort: intstr.FromString(prom), } promPodPort = corev1.ContainerPort{ Name: prom, ContainerPort: promServicePort.Port, } promAnnotations = map[string]string{ "prometheus.io/scrape": "true", "prometheus.io/port": strconv.Itoa(int(promServicePort.Port)), } ) func GetNginxResources(conf *config.Config, ingressConfig *NginxIngressConfig) *NginxResources { if ingressConfig != nil && ingressConfig.Version == nil { ingressConfig.Version = &LatestNginxVersion } res := &NginxResources{ IngressClass: newNginxIngressControllerIngressClass(conf, ingressConfig), ServiceAccount: newNginxIngressControllerServiceAccount(conf, ingressConfig), ClusterRole: newNginxIngressControllerClusterRole(conf, ingressConfig), Role: newNginxIngressControllerRole(conf, ingressConfig), ClusterRoleBinding: newNginxIngressControllerClusterRoleBinding(conf, ingressConfig), RoleBinding: newNginxIngressControllerRoleBinding(conf, ingressConfig), Service: newNginxIngressControllerService(conf, ingressConfig), PromService: newNginxIngressControllerPromService(conf, ingressConfig), Deployment: newNginxIngressControllerDeployment(conf, ingressConfig), ConfigMap: newNginxIngressControllerConfigmap(conf, ingressConfig), HorizontalPodAutoscaler: newNginxIngressControllerHPA(conf, ingressConfig), PodDisruptionBudget: newNginxIngressControllerPDB(conf, ingressConfig), } switch ingressConfig.Version { // this doesn't do anything yet but when different versions have different resources we should change the resources here } for _, obj := range res.Objects() { l := util.MergeMaps(obj.GetLabels(), nginxLabels) obj.SetLabels(l) } // Can safely assume the namespace exists if using kube-system. // Purposefully do this after applying the labels, namespace isn't an Nginx-specific resource if conf.NS != "kube-system" { res.Namespace = Namespace(conf, conf.NS) } return res } func newNginxIngressControllerIngressClass(conf *config.Config, ingressConfig *NginxIngressConfig) *netv1.IngressClass { return &netv1.IngressClass{ TypeMeta: metav1.TypeMeta{ Kind: "IngressClass", APIVersion: "networking.k8s.io/v1", }, ObjectMeta: metav1.ObjectMeta{Name: ingressConfig.IcName, Labels: GetTopLevelLabels()}, Spec: netv1.IngressClassSpec{ Controller: ingressConfig.ControllerClass, }, } } func newNginxIngressControllerServiceAccount(conf *config.Config, ingressConfig *NginxIngressConfig) *corev1.ServiceAccount { return &corev1.ServiceAccount{ TypeMeta: metav1.TypeMeta{ Kind: "ServiceAccount", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: ingressConfig.ResourceName, Namespace: conf.NS, Labels: AddComponentLabel(GetTopLevelLabels(), "ingress-controller"), }, } } func newNginxIngressControllerClusterRole(conf *config.Config, ingressConfig *NginxIngressConfig) *rbacv1.ClusterRole { return &rbacv1.ClusterRole{ TypeMeta: metav1.TypeMeta{ Kind: "ClusterRole", APIVersion: "rbac.authorization.k8s.io/v1", }, ObjectMeta: metav1.ObjectMeta{ Name: ingressConfig.ResourceName, Labels: AddComponentLabel(GetTopLevelLabels(), "ingress-controller"), }, Rules: []rbacv1.PolicyRule{ { APIGroups: []string{""}, Resources: []string{"configmaps", "endpoints", "nodes", "pods", "secrets", "namespaces"}, Verbs: []string{"list", "watch"}, }, { APIGroups: []string{"coordination.k8s.io"}, Resources: []string{"leases"}, Verbs: []string{"list", "watch"}, }, { APIGroups: []string{""}, Resources: []string{"nodes"}, Verbs: []string{"get"}, }, { APIGroups: []string{""}, Resources: []string{"services"}, Verbs: []string{"get", "list", "watch"}, }, { APIGroups: []string{"networking.k8s.io"}, Resources: []string{"ingresses"}, Verbs: []string{"get", "watch", "list"}, }, { APIGroups: []string{""}, Resources: []string{"events"}, Verbs: []string{"create", "patch"}, }, { APIGroups: []string{"networking.k8s.io"}, Resources: []string{"ingresses/status"}, Verbs: []string{"update"}, }, { APIGroups: []string{"networking.k8s.io"}, Resources: []string{"ingressclasses"}, Verbs: []string{"get", "list", "watch"}, }, { APIGroups: []string{"discovery.k8s.io"}, Resources: []string{"endpointslices"}, Verbs: []string{"list", "watch", "get"}, }, }, } } func newNginxIngressControllerRole(conf *config.Config, ingressConfig *NginxIngressConfig) *rbacv1.Role { return &rbacv1.Role{ TypeMeta: metav1.TypeMeta{ Kind: "Role", APIVersion: "rbac.authorization.k8s.io/v1", }, ObjectMeta: metav1.ObjectMeta{ Name: ingressConfig.ResourceName, Labels: AddComponentLabel(GetTopLevelLabels(), "ingress-controller"), Namespace: conf.NS, }, Rules: []rbacv1.PolicyRule{ { APIGroups: []string{""}, Resources: []string{"namespaces"}, Verbs: []string{"get"}, }, // temporary permission used for update from 1.3.0->1.8.1 { APIGroups: []string{""}, Resources: []string{"configmaps"}, Verbs: []string{"update"}, }, { APIGroups: []string{""}, Resources: []string{"configmaps", "pods", "secrets", "endpoints"}, Verbs: []string{"get", "list", "watch"}, }, { APIGroups: []string{""}, Resources: []string{"services"}, Verbs: []string{"get", "list", "watch"}, }, { APIGroups: []string{"networking.k8s.io"}, Resources: []string{"ingresses"}, Verbs: []string{"get", "list", "watch"}, }, { APIGroups: []string{"networking.k8s.io"}, Resources: []string{"ingresses/status"}, Verbs: []string{"update"}, }, { APIGroups: []string{"networking.k8s.io"}, Resources: []string{"ingressclasses"}, Verbs: []string{"get", "list", "watch"}, }, { APIGroups: []string{"coordination.k8s.io"}, Resources: []string{"leases"}, ResourceNames: []string{ingressConfig.ResourceName}, Verbs: []string{"get", "update"}, }, { APIGroups: []string{"coordination.k8s.io"}, Resources: []string{"leases"}, Verbs: []string{"create"}, }, { APIGroups: []string{""}, Resources: []string{"events"}, Verbs: []string{"create", "patch"}, }, { APIGroups: []string{"discovery.k8s.io"}, Resources: []string{"endpointslices"}, Verbs: []string{"list", "watch", "get"}, }, }, } } func newNginxIngressControllerClusterRoleBinding(conf *config.Config, ingressConfig *NginxIngressConfig) *rbacv1.ClusterRoleBinding { return &rbacv1.ClusterRoleBinding{ TypeMeta: metav1.TypeMeta{ Kind: "ClusterRoleBinding", APIVersion: "rbac.authorization.k8s.io/v1", }, ObjectMeta: metav1.ObjectMeta{ Name: ingressConfig.ResourceName, Labels: AddComponentLabel(GetTopLevelLabels(), "ingress-controller"), }, RoleRef: rbacv1.RoleRef{ APIGroup: "rbac.authorization.k8s.io", Kind: "ClusterRole", Name: ingressConfig.ResourceName, }, Subjects: []rbacv1.Subject{{ Kind: "ServiceAccount", Name: ingressConfig.ResourceName, Namespace: conf.NS, }}, } } func newNginxIngressControllerRoleBinding(conf *config.Config, ingressConfig *NginxIngressConfig) *rbacv1.RoleBinding { return &rbacv1.RoleBinding{ TypeMeta: metav1.TypeMeta{ Kind: "RoleBinding", APIVersion: "rbac.authorization.k8s.io/v1", }, ObjectMeta: metav1.ObjectMeta{ Name: ingressConfig.ResourceName, Namespace: conf.NS, Labels: AddComponentLabel(GetTopLevelLabels(), "ingress-controller"), }, RoleRef: rbacv1.RoleRef{ APIGroup: "rbac.authorization.k8s.io", Kind: "Role", Name: ingressConfig.ResourceName, }, Subjects: []rbacv1.Subject{{ Kind: "ServiceAccount", Name: ingressConfig.ResourceName, Namespace: conf.NS, }}, } } func newNginxIngressControllerService(conf *config.Config, ingressConfig *NginxIngressConfig) *corev1.Service { annotations := make(map[string]string) if ingressConfig != nil && ingressConfig.ServiceConfig != nil { for k, v := range ingressConfig.ServiceConfig.Annotations { annotations[k] = v } } ret := &corev1.Service{ TypeMeta: metav1.TypeMeta{ Kind: "Service", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: ingressConfig.ResourceName, Namespace: conf.NS, Labels: AddComponentLabel(GetTopLevelLabels(), "ingress-controller"), Annotations: annotations, }, Spec: corev1.ServiceSpec{ ExternalTrafficPolicy: corev1.ServiceExternalTrafficPolicyTypeLocal, Type: corev1.ServiceTypeLoadBalancer, Selector: ingressConfig.PodLabels(), Ports: []corev1.ServicePort{ { Name: "https", Port: 443, TargetPort: intstr.FromString("https"), }, }, }, } if !ingressConfig.HTTPDisabled { ret.Spec.Ports = append([]corev1.ServicePort{ { Name: "http", Port: 80, TargetPort: intstr.FromString("http"), }, }, ret.Spec.Ports...) } return ret } func newNginxIngressControllerPromService(conf *config.Config, ingressConfig *NginxIngressConfig) *corev1.Service { annotations := make(map[string]string) for k, v := range promAnnotations { annotations[k] = v } return &corev1.Service{ TypeMeta: metav1.TypeMeta{ Kind: "Service", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: ingressConfig.ResourceName + "-metrics", Namespace: conf.NS, Labels: AddComponentLabel(GetTopLevelLabels(), "ingress-controller"), Annotations: annotations, }, Spec: corev1.ServiceSpec{ Type: corev1.ServiceTypeClusterIP, Selector: ingressConfig.PodLabels(), Ports: []corev1.ServicePort{ promServicePort, }, }, } } func newNginxIngressControllerDeployment(conf *config.Config, ingressConfig *NginxIngressConfig) *appsv1.Deployment { ingressControllerDeploymentLabels := AddComponentLabel(GetTopLevelLabels(), IngressControllerComponentName) ingressControllerPodLabels := AddComponentLabel(GetTopLevelLabels(), IngressControllerComponentName) for k, v := range ingressConfig.PodLabels() { ingressControllerPodLabels[k] = v } podAnnotations := map[string]string{} if !conf.DisableOSM { podAnnotations["openservicemesh.io/sidecar-injection"] = "disabled" } for k, v := range promAnnotations { podAnnotations[k] = v } selector := &metav1.LabelSelector{MatchLabels: ingressConfig.PodLabels()} deploymentArgs := []string{ "/nginx-ingress-controller", "--ingress-class=" + ingressConfig.IcName, "--controller-class=" + ingressConfig.ControllerClass, "--election-id=" + ingressConfig.ResourceName, "--publish-service=$(POD_NAMESPACE)/" + ingressConfig.ResourceName, "--configmap=$(POD_NAMESPACE)/" + ingressConfig.ResourceName, "--enable-annotation-validation=true", // https://cloud-provider-azure.sigs.k8s.io/topics/loadbalancer/#custom-load-balancer-health-probe // load balancer health probe checks in 5 second intervals. It requires 2 failing probes to fail so we need at least 10s of grace period. // we set it to 15s to be safe. Without this Nginx process exits but the LoadBalancer continues routing to the Pod until two health checks fail. "--shutdown-grace-period=15", } if ingressConfig.DefaultSSLCertificate != "" { deploymentArgs = append(deploymentArgs, "--default-ssl-certificate="+ingressConfig.DefaultSSLCertificate) } if ingressConfig.DefaultBackendService != "" { deploymentArgs = append(deploymentArgs, "--default-backend-service="+ingressConfig.DefaultBackendService) } ret := &appsv1.Deployment{ TypeMeta: metav1.TypeMeta{ Kind: "Deployment", APIVersion: "apps/v1", }, ObjectMeta: metav1.ObjectMeta{ Name: ingressConfig.ResourceName, Namespace: conf.NS, Labels: ingressControllerDeploymentLabels, }, Spec: appsv1.DeploymentSpec{ RevisionHistoryLimit: util.Int32Ptr(2), Selector: selector, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: ingressControllerPodLabels, Annotations: podAnnotations, }, Spec: *WithPreferSystemNodes(&corev1.PodSpec{ TopologySpreadConstraints: []corev1.TopologySpreadConstraint{ { MaxSkew: 1, TopologyKey: "kubernetes.io/hostname", // spread across nodes WhenUnsatisfiable: corev1.ScheduleAnyway, LabelSelector: selector, MatchLabelKeys: []string{ // https://kubernetes.io/blog/2024/08/16/matchlabelkeys-podaffinity/ // evaluate only pods of the same version (mostly applicable to rollouts) "pod-template-hash", }, }, }, ServiceAccountName: ingressConfig.ResourceName, Containers: []corev1.Container{*withPodRefEnvVars(withLivenessProbeMatchingReadinessNewFailureThresh(withTypicalReadinessProbe(10254, &corev1.Container{ Name: "controller", Image: path.Join(conf.Registry, "/oss/kubernetes/ingress/nginx-ingress-controller:"+ingressConfig.Version.tag), Args: deploymentArgs, SecurityContext: &corev1.SecurityContext{ AllowPrivilegeEscalation: util.ToPtr(false), Capabilities: &corev1.Capabilities{ Add: []corev1.Capability{"NET_BIND_SERVICE"}, // needed to bind to 80/443 ports https://github.com/kubernetes/ingress-nginx/blob/ca6d3622e5c2819a29f4a407ed272f42d10a91a9/docs/troubleshooting.md?plain=1#L369 Drop: []corev1.Capability{"ALL"}, }, RunAsNonRoot: util.ToPtr(true), RunAsUser: util.Int64Ptr(101), SeccompProfile: &corev1.SeccompProfile{ Type: corev1.SeccompProfileTypeRuntimeDefault, }, }, Ports: []corev1.ContainerPort{ { Name: "https", ContainerPort: 443, }, promPodPort, }, Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ corev1.ResourceCPU: resource.MustParse("500m"), corev1.ResourceMemory: resource.MustParse("127Mi"), }, }, }), 6))}, }), }, }, } if !ingressConfig.HTTPDisabled { ret.Spec.Template.Spec.Containers[0].Ports = append([]corev1.ContainerPort{ { Name: "http", ContainerPort: 80, }, }, ret.Spec.Template.Spec.Containers[0].Ports...) } return ret } func newNginxIngressControllerConfigmap(conf *config.Config, ingressConfig *NginxIngressConfig) *corev1.ConfigMap { confMap := &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ Kind: "ConfigMap", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: ingressConfig.ResourceName, Namespace: conf.NS, Labels: AddComponentLabel(GetTopLevelLabels(), "ingress-controller"), }, Data: map[string]string{ // Can't use 'allow-snippet-annotations=false' to reduce injection risk, since we require snippet functionality for OSM routing. // But we can still protect against leaked service account tokens. // See: https://kubernetes.github.io/ingress-nginx/user-guide/nginx-configuration/configmap/#annotation-value-word-blocklist "allow-snippet-annotations": "true", "annotation-value-word-blocklist": "load_module,lua_package,_by_lua,location,root,proxy_pass,serviceaccount,{,},'", }, } if ingressConfig.DefaultSSLCertificate != "" && ingressConfig.ForceSSLRedirect { confMap.Data["force-ssl-redirect"] = "true" } if ingressConfig.CustomHTTPErrors != "" { confMap.Data["custom-http-errors"] = ingressConfig.CustomHTTPErrors } return confMap } func newNginxIngressControllerPDB(conf *config.Config, ingressConfig *NginxIngressConfig) *policyv1.PodDisruptionBudget { maxUnavailable := intstr.FromInt(1) return &policyv1.PodDisruptionBudget{ TypeMeta: metav1.TypeMeta{ Kind: "PodDisruptionBudget", APIVersion: "policy/v1", }, ObjectMeta: metav1.ObjectMeta{ Name: ingressConfig.ResourceName, Namespace: conf.NS, Labels: AddComponentLabel(GetTopLevelLabels(), "ingress-controller"), }, Spec: policyv1.PodDisruptionBudgetSpec{ Selector: &metav1.LabelSelector{MatchLabels: ingressConfig.PodLabels()}, MaxUnavailable: &maxUnavailable, }, } } func newNginxIngressControllerHPA(conf *config.Config, ingressConfig *NginxIngressConfig) *autov1.HorizontalPodAutoscaler { return &autov1.HorizontalPodAutoscaler{ TypeMeta: metav1.TypeMeta{ Kind: "HorizontalPodAutoscaler", APIVersion: "autoscaling/v1", }, ObjectMeta: metav1.ObjectMeta{ Name: ingressConfig.ResourceName, Namespace: conf.NS, Labels: AddComponentLabel(GetTopLevelLabels(), "ingress-controller"), }, Spec: autov1.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autov1.CrossVersionObjectReference{ APIVersion: "apps/v1", Kind: "Deployment", Name: ingressConfig.ResourceName, }, MinReplicas: util.Int32Ptr(ingressConfig.MinReplicas), MaxReplicas: ingressConfig.MaxReplicas, TargetCPUUtilizationPercentage: &ingressConfig.TargetCPUUtilizationPercentage, }, } }