<p>I have a Kubernetes cluster that is running a Jenkins Pod with a service set up for Metallb. Currently when I try to hit the <code>loadBalancerIP</code> for the pod outside of my cluster I am unable to. I also have a <code>kube-verify</code> pod that is running on the cluster with a service that is also using Metallb. When I try to hit that pod outside of my cluster I can hit it with no problem.</p>
<p>When I switch the service for the Jenkins pod to be of type <code>NodePort</code> it works but as soon as I switch it back to be of type <code>LoadBalancer</code> it stops working. Both the Jenkins pod and the working <code>kube-verify</code> pod are running on the same node.</p>
<p>Cluster Details:
The master node is running and is connected to my router wirelessly. On the master node I have dnsmasq setup along with iptable rules that forward the connection from the wireless port to the Ethernet port. Each of the nodes is connected together via a switch via Ethernet. Metallb is setup up in layer2 mode with an address pool that is on the same subnet as the ip address of the wireless port of the master node. The <code>kube-proxy</code> is set to use <code>strictArp</code> and <code>ipvs</code> mode.</p>
<p><strong>Jenkins Manifest:</strong></p>
<pre><code>apiVersion: v1
kind: ServiceAccount
metadata:
  name: jenkins-sa
  namespace: &quot;devops-tools&quot;
  labels:
    app: jenkins
    version: v1
    tier: backend
---
apiVersion: v1
kind: Secret
metadata:
  name: jenkins-secret
  namespace: &quot;devops-tools&quot;
  labels:
    app: jenkins
    version: v1
    tier: backend
type: Opaque
data:
  jenkins-admin-password: ***************
  jenkins-admin-user: ********
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: jenkins
  namespace: &quot;devops-tools&quot;
  labels:
    app: jenkins
    version: v1
    tier: backend
data:
  jenkins.yaml: |-
    jenkins:
      authorizationStrategy:
        loggedInUsersCanDoAnything:
          allowAnonymousRead: false
      securityRealm:
        local:
          allowsSignup: false
          enableCaptcha: false
          users:
          - id: &quot;${jenkins-admin-username}&quot;
            name: &quot;Jenkins Admin&quot;
            password: &quot;${jenkins-admin-password}&quot;
      disableRememberMe: false
      mode: NORMAL
      numExecutors: 0
      labelString: &quot;&quot;
      projectNamingStrategy: &quot;standard&quot;
      markupFormatter:
        plainText
      clouds:
      - kubernetes:
          containerCapStr: &quot;10&quot;
          defaultsProviderTemplate: &quot;jenkins-base&quot;
          connectTimeout: &quot;5&quot;
          readTimeout: 15
          jenkinsUrl: &quot;jenkins-ui:8080&quot;
          jenkinsTunnel: &quot;jenkins-discover:50000&quot;
          maxRequestsPerHostStr: &quot;32&quot;
          name: &quot;kubernetes&quot;
          serverUrl: &quot;https://kubernetes&quot;
          podLabels:
          - key: &quot;jenkins/jenkins-agent&quot;
            value: &quot;true&quot;
          templates:
            - name: &quot;default&quot;
          #id: eeb122dab57104444f5bf23ca29f3550fbc187b9d7a51036ea513e2a99fecf0f
              containers:
              - name: &quot;jnlp&quot;
                alwaysPullImage: false
                args: &quot;^${computer.jnlpmac} ^${computer.name}&quot;
                command: &quot;&quot;
                envVars:
                - envVar:
                    key: &quot;JENKINS_URL&quot;
                    value: &quot;jenkins-ui:8080&quot;
                image: &quot;jenkins/inbound-agent:4.11-1&quot;
                ttyEnabled: false
                workingDir: &quot;/home/jenkins/agent&quot;
              idleMinutes: 0
              instanceCap: 2147483647
              label: &quot;jenkins-agent&quot;
              nodeUsageMode: &quot;NORMAL&quot;
              podRetention: Never
              showRawYaml: true
              serviceAccount: &quot;jenkins-sa&quot;
              slaveConnectTimeoutStr: &quot;100&quot;
              yamlMergeStrategy: override
      crumbIssuer:
        standard:
          excludeClientIPFromCrumb: true
    security:
      apiToken:
        creationOfLegacyTokenEnabled: false
        tokenGenerationOnCreationEnabled: false
        usageStatisticsEnabled: true
    unclassified:
      location:
        adminAddress:
        url: jenkins-ui:8080
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: jenkins-pv-volume
  labels:
    type: local
spec:
  storageClassName: local-storage
  claimRef:
    name: jenkins-pv-claim
    namespace: devops-tools
  capacity:
    storage: 16Gi
  accessModes:
    - ReadWriteMany
  local:
    path: /mnt
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - heine-cluster1
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: jenkins-pv-claim
  namespace: devops-tools
  labels:
    app: jenkins
    version: v1
    tier: backend
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 8Gi
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: jenkins-cr
rules:
- apiGroups: [&quot;&quot;]
  resources: [&quot;*&quot;]
  verbs: [&quot;*&quot;]
---
# This role is used to allow Jenkins scheduling of agents via Kubernetes plugin. 
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: jenkins-role-schedule-agents
  namespace: devops-tools
  labels:
    app: jenkins
    version: v1
    tier: backend
rules:
- apiGroups: [&quot;&quot;]
  resources: [&quot;pods&quot;, &quot;pods/exec&quot;, &quot;pods/log&quot;, &quot;persistentvolumeclaims&quot;, &quot;events&quot;]
  verbs: [&quot;get&quot;, &quot;list&quot;, &quot;watch&quot;]
- apiGroups: [&quot;&quot;]
  resources: [&quot;pods&quot;, &quot;pods/exec&quot;, &quot;persistentvolumeclaims&quot;]
  verbs: [&quot;create&quot;, &quot;delete&quot;, &quot;deletecollection&quot;, &quot;patch&quot;, &quot;update&quot;]
---
# The sidecar container which is responsible for reloading configuration changes
# needs permissions to watch ConfigMaps
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: jenkins-casc-reload
  namespace: devops-tools
  labels:
    app: jenkins
    version: v1
    tier: backend
rules:
- apiGroups: [&quot;&quot;]
  resources: [&quot;configmaps&quot;]
  verbs: [&quot;get&quot;, &quot;watch&quot;, &quot;list&quot;]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: jenkins-crb
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: jenkins-cr
subjects:
- kind: ServiceAccount
  name: jenkins-sa
  namespace: &quot;devops-tools&quot;
---
# We bind the role to the Jenkins service account. The role binding is created in the namespace
# where the agents are supposed to run.
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: jenkins-schedule-agents
  namespace: &quot;devops-tools&quot;
  labels:
    app: jenkins
    version: v1
    tier: backend
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: jenkins-role-schedule-agents
subjects:
- kind: ServiceAccount
  name: jenkins-sa
  namespace: &quot;devops-tools&quot;
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: jenkins-watch-configmaps
  namespace: &quot;devops-tools&quot;
  labels:
    app: jenkins
    version: v1
    tier: backend
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: jenkins-casc-reload
subjects:
- kind: ServiceAccount
  name: jenkins-sa
  namespace: &quot;devops-tools&quot;
---
apiVersion: v1
kind: Service
metadata:
  name: jenkins
  namespace: &quot;devops-tools&quot;
  labels:
    app: jenkins
    version: v1
    tier: backend
  annotations:
    metallb.universe.tf/address-pool: default
spec:
  type: LoadBalancer
  loadBalancerIP: 172.16.1.5
  ports:
  - name: ui
    port: 8080
    targetPort: 8080
  externalTrafficPolicy: Local
  selector:
    app: jenkins
---
apiVersion: v1
kind: Service
metadata: 
  name: jenkins-agent
  namespace: &quot;devops-tools&quot;
  labels:
    app: jenkins
    version: v1
    tier: backend
spec:
  ports:
  - name: agents
    port: 50000
    targetPort: 50000
  selector:
    app: jenkins 
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: jenkins
  namespace: &quot;devops-tools&quot;
  labels:
    app: jenkins
    version: v1
    tier: backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: jenkins
  template:
    metadata:
      labels:
        app: jenkins
        version: v1
        tier: backend
      annotations:
        checksum/config: c0daf24e0ec4e4cb59c8a66305181a17249770b37283ca8948e189a58e29a4a5
    spec:
      securityContext:
        runAsUser: 1000
        fsGroup: 1000
        runAsNonRoot: true
      containers:
        - name: jenkins
          image: &quot;heineza/jenkins-master:2.323-jdk11-1&quot;
          imagePullPolicy: Always
          args: [ &quot;--httpPort=8080&quot;]
          env:
          - name: POD_NAME
            valueFrom:
              fieldRef:
                fieldPath: metadata.name
          - name: JAVA_OPTS
            value: -Djenkins.install.runSetupWizard=false -Dorg.apache.commons.jelly.tags.fmt.timeZone=America/Chicago
          - name: JENKINS_SLAVE_AGENT_PORT
            value: &quot;50000&quot;
          ports:
          - containerPort: 8080
            name: ui
          - containerPort: 50000
            name: agents
          resources:
            limits:
              cpu: 2000m
              memory: 4096Mi
            requests:
              cpu: 50m
              memory: 256Mi
          volumeMounts:
          - mountPath: /var/jenkins_home
            name: jenkins-home
            readOnly: false
          - name: jenkins-config
            mountPath: /var/jenkins_home/jenkins.yaml
          - name: admin-secret
            mountPath: /run/secrets/jenkins-admin-username
            subPath: jenkins-admin-user
            readOnly: true
          - name: admin-secret
            mountPath: /run/secrets/jenkins-admin-password
            subPath: jenkins-admin-password
            readOnly: true
      serviceAccountName: &quot;jenkins-sa&quot;
      volumes:
        - name: jenkins-cache
          emptyDir: {}
        - name: jenkins-home
          persistentVolumeClaim:
            claimName: jenkins-pv-claim
        - name: jenkins-config
          configMap: 
            name: jenkins
        - name: admin-secret
          secret:
            secretName: jenkins-secret
</code></pre>
<p>This Jenkins manifest is a modified version of what the Jenkins helm-chart generates. I redacted my secret but in the actual manifest there are <code>base64</code> encoded strings. Also, the docker image I created and use in the deployment uses the Jenkins 2.323-jdk11 image as a base image and I just installed some plugins for Configuration as Code, kubernetes, and Git. What could be preventing the Jenkins pod from being accessible outside of my cluster when using Metallb?</p>

<p>MetalLB doesn't allow by default to re-use/share the same LoadBalancerIP addresscase.</p>
<p>According to <a href="https://metallb.universe.tf/usage/" rel="nofollow noreferrer">MetalLB documentation</a>:</p>
<blockquote>
<p>MetalLB respects the <code>spec.loadBalancerIP</code> parameter, so if you want your service to be set up with a specific address, you can request it by setting that parameter.</p>
<p>If MetalLB <strong>does not own</strong> the requested address, or if the address is <strong>already in use</strong> by another service, assignment will fail and MetalLB will log a warning event visible in <code>kubectl describe service &lt;service name&gt;</code>.<a href="https://metallb.universe.tf/usage/#requesting-specific-ips" rel="nofollow noreferrer">[1]</a></p>
</blockquote>
<p>In case you need to have services on a single IP you can enable selective IP sharing. To do so you have to add the <code>metallb.universe.tf/allow-shared-ip</code> annotation to services.</p>
<blockquote>
<p>The value of the annotation is a “sharing key.” Services can share an IP address under the following conditions:</p>
<ul>
<li>They both have the same sharing key.</li>
<li>They request the use of different ports (e.g. tcp/80 for one and tcp/443 for the other).</li>
<li>They both use the  <code>Cluster</code>  external traffic policy, or they both point to the  <em>exact</em>  same set of pods (i.e. the pod selectors are identical).  <a href="https://metallb.universe.tf/usage/#ip-address-sharing" rel="nofollow noreferrer">[2]</a></li>
</ul>
</blockquote>
<hr />
<p><strong>UPDATE</strong></p>
<p>I tested your setup successfully with one minor difference -
I needed to remove: <code>externalTrafficPolicy: Local</code> from Jenkins Service spec.</p>
<p>Try this solution, if it still doesn't work then it's a problem with your cluster environment.</p>
