Setup

June 5, 2024 ยท View on GitHub

Create a Kubernetes cluster with a minimum of 3 nodes and ~8+GB per node (e.g., Standard_DS3_v2)

Fork this repository (needed to enable CD) and clone it

Nginx Ingress controller Installation

# Create a namespace for your ingress resources
kubectl create namespace ingress-basic

# Add the ingress-nginx repository
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update

# Use Helm to deploy an NGINX ingress controller
helm install nginx-ingress ingress-nginx/ingress-nginx \
--version 4.7.1 \
--namespace ingress-basic \
--set controller.replicaCount=2 \
--set controller.electionID=ingress-controller-leader \
--set controller.ingressClassResource.name=nginx \
--set controller.ingressClassResource.enabled=true \
--set controller.ingressClassResource.default=true \
--set controller.ingressClassResource.controllerValue=k8s.io/nginx \
--set controller.ingressClass=nginx \
--set controller.nodeSelector."kubernetes\.io/os"=linux \
--set defaultBackend.nodeSelector."kubernetes\.io/os"=linux \
--set controller.admissionWebhooks.patch.nodeSelector."kubernetes\.io/os"=linux \
--set controller.service.annotations."service\.beta\.kubernetes\.io/azure-load-balancer-health-probe-request-path"="/healthz" 

Retrieve the public IP of the Loadbalancer service

kubectl get svc -n ingress-basic

Assign a DNS label to the Ingress Public IP and update it for the registryHost variable

Set the variable to be used as the top level domain for this exercise. Use a custom domain or a cloud service provided domain name.

If using AKS, a DNS name label can be assigned to the public IP of the Loadbalancer

  • Open the Public IP resource associated with the EXTERNAL-IP address of the LoadBalancer service
  • Navigate to the Configuration blade and set a unique name in the DNS name label
  • Use the FQDN. For ex. {uniquename}.{region}.cloudapp.azure.com
topLevelDomain={FQDN DNS label Name to be updated here}

Cert Manager Installation

# Create namespace for cert manager
kubectl create namespace cert-manager
kubectl label namespace cert-manager cert-manager.io/disable-validation=true

# Add the Jetstack Helm repository
helm repo add jetstack https://charts.jetstack.io
helm repo update

# Install the cert-manager Helm chart
helm install cert-manager jetstack/cert-manager\
  --namespace cert-manager \
  --version v1.12.1 \
  --set installCRDs=true \
  --set nodeSelector."kubernetes\.io/os"=linux

Create the ClusterIssuer by applying the below YAML with the email address changed


cat <<EOF | kubectl create -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt
  namespace: cert-manager
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: MY_EMAIL_ADDRESS
    privateKeySecretRef:
      name: letsencrypt
    solvers:
    - http01:
        ingress:
          class: nginx
          podTemplate:
            spec:
              nodeSelector:
                "kubernetes.io/os": linux      
EOF

Rook Installation

# Create a namespace for rook
kubectl create namespace rook-ceph

# Add the rook helm repo
helm repo add rook-release https://charts.rook.io/release
helm repo update

# Use Helm to deploy an rook
helm install rook-ceph rook-release/rook-ceph \
    --version 1.11.9 \
    --namespace rook-ceph \
    -f yml/rook-values.yaml    

kubectl apply -f yml/rook-cluster.yaml 
kubectl apply -f yml/rook-storageclass.yaml

Wait for the installation to complete by watching the pods in the rook-ceph namespace

kubectl get po -n rook-ceph -A -w

Harbor Installation

Install Ingress for Harbor.

# Create namespace for the harbor nginx ingress controller 
kubectl create namespace harbor-ingress-system

# Install nginx ingress for Harbor
helm install harbor-nginx-ingress ingress-nginx/ingress-nginx \
    --version 4.7.1 \
    --namespace harbor-ingress-system \
    --set controller.replicaCount=2 \
    --set controller.electionID=harbor-ingress-controller-leader \
    --set controller.ingressClassResource.name=harbor \
    --set controller.ingressClassResource.enabled=true \
    --set controller.ingressClassResource.default=true \
    --set controller.ingressClassResource.controllerValue=k8s.io/harbor \
    --set controller.ingressClass=harbor \
    --set controller.service.annotations."service\.beta\.kubernetes\.io/azure-load-balancer-health-probe-request-path"="/healthz" \
    --set controller.nodeSelector."kubernetes\.io/os"=linux \
    --set defaultBackend.nodeSelector."kubernetes\.io/os"=linux \
    --set controller.admissionWebhooks.patch.nodeSelector."kubernetes\.io/os"=linux

# Label the harbor-ingress-system namespace to disable cert resource validation
kubectl label namespace harbor-ingress-system cert-manager.io/disable-validation=true

Retrieve the public IP of the Loadbalancer service

kubectl get svc -n harbor-ingress-system

Assign a DNS label to the Ingress Public IP and update it for the registryHost variable

If using AKS, a DNS name label can be assigned to the public IP of the Loadbalancer created in the harbor-ingress-system namespace.

  • Open the Public IP resource associated with the EXTERNAL-IP address of the LoadBalancer service for harbor ingress
  • Navigate to the Configuration blade and set a unique name in the DNS name label
  • Use the FQDN. For ex. {uniquenameforharboringress}.{region}.cloudapp.azure.com
registryHost={FQDN DNS label Name to be updated here}
externalUrl=https://$registryHost

# Create the namespace for harbor installation
kubectl create namespace harbor-system
# Add the harbor helm repo 
helm repo add harbor https://helm.goharbor.io
helm repo update

# Install Harbor
helm install harbor harbor/harbor \
    --namespace harbor-system \
    --version 1.12.2 \
    --set expose.tls.certSource=secret \
    --set expose.tls.secret.secretName=ingress-cert-harbor \
    --set expose.ingress.hosts.core=$registryHost \
    --set expose.ingress.annotations."cert-manager\.io/cluster-issuer"=letsencrypt  \
    --set expose.ingress.annotations."acme\.cert-manager\.io/http01-ingress-class"=harbor \
    --set expose.ingress.className=harbor \
    --set notary.enabled=false \
    --set trivy.enabled=false \
    --set externalURL=$externalUrl \
    --set harborAdminPassword=admin \
    --set persistence.enabled=true \
    --set persistence.persistentVolumeClaim.registry.storageClass=rook-ceph-block \
    --set persistence.persistentVolumeClaim.chartmuseum.storageClass=rook-ceph-block \
    --set persistence.persistentVolumeClaim.jobservice.storageClass=rook-ceph-block \
    --set persistence.persistentVolumeClaim.database.storageClass=rook-ceph-block \
    --set persistence.persistentVolumeClaim.redis.storageClass=rook-ceph-block 

Confirm harbor installed and running then create the harbor project and user

#Create conexp project in Harbor
 curl -u admin:admin -i -k -X POST "$externalUrl/api/v2.0/projects" \
      -d "@json/harbor-project.json" \
      -H "Content-Type: application/json"

#Create conexp user in Harbor
 curl -u admin:admin -i -k -X POST "$externalUrl/api/v2.0/users" \
      -d "@json/harbor-project-user.json" \
      -H "Content-Type: application/json"

#Add the conexp user to the conexp project in Harbor

conexpid=$(curl -u admin:admin -k -s -X GET "$externalUrl/api/v2.0/projects?name=conexp" | jq '.[0].project_id')
echo "project_id: $conexpid"

 curl -u admin:admin -i -k -X POST "$externalUrl/api/v2.0/projects/$conexpid/members" \
      -d "@json/harbor-project-member.json" \
      -H "Content-Type: application/json"

Now retrieve the Harbor Registry URL:

echo $externalUrl

Use the following credentials to login: admin
admin

MySQL Installation

Deploy MySQL DB. Please replace ENTERYOURPASSWORD with your chosen password. Please ensure that same password is used in all to be replaced values before executing the commands.

kubectl create ns mysql

helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update

helm install mysql bitnami/mysql \
    --namespace mysql \
    --version 9.10.5 \
    --set auth.rootPassword=ENTERYOURPASSWORD \
    --set auth.username=ftacncf  \
    --set auth.password=ENTERYOURPASSWORD \
    --set global.storageClass=rook-ceph-block 

Wait for the mysql instance to be ready and create the databases

kubectl run -n mysql -i -t ubuntu --image=ubuntu:18.04 --restart=Never -- bash -il
apt-get update && apt-get install mysql-client -y
mysql -h mysql --password=ENTERYOURPASSWORD
show databases;

CREATE DATABASE conexpweb;

CREATE DATABASE conexpapi;
USE conexpapi;

CREATE TABLE CostCenters(
   CostCenterId int(11)  NOT NULL,
   SubmitterEmail text NOT NULL,
   ApproverEmail text NOT NULL,
   CostCenterName text NOT NULL,
   PRIMARY KEY ( CostCenterId )
);

INSERT INTO CostCenters (CostCenterId, SubmitterEmail,ApproverEmail,CostCenterName)  values (1, 'user1@mycompany.com', 'user1@mycompany.com','123E42');
INSERT INTO CostCenters (CostCenterId, SubmitterEmail,ApproverEmail,CostCenterName)  values (2, 'user2@mycompany.com', 'user2@mycompany.com','456C14');
INSERT INTO CostCenters (CostCenterId, SubmitterEmail,ApproverEmail,CostCenterName)  values (3, 'user3@mycompany.com', 'user3@mycompany.com','456C14');

USE conexpapi;
GRANT ALL PRIVILEGES ON *.* TO 'ftacncf'@'%';

USE conexpweb;
GRANT ALL PRIVILEGES ON *.* TO 'ftacncf'@'%';

# Exit from mysql cli
exit;

# Exit from the pod
exit;

Knative Installation

# Install the Knative Serving component
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.10.2/serving-crds.yaml
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.10.2/serving-core.yaml

# Install a networking layer
kubectl apply -f https://github.com/knative/net-kourier/releases/download/knative-v1.10.0/kourier.yaml

# Configure Kourier Networking
kubectl patch configmap/config-network \
  --namespace knative-serving \
  --type merge \
  --patch '{"data":{"ingress-class":"kourier.ingress.networking.knative.dev"}}'

# Configure the No DNS
kubectl patch configmap/config-domain \
      --namespace knative-serving \
      --type merge \
      --patch '{"data":{"example.com":""}}'

# Install Eventing
kubectl apply -f https://github.com/knative/eventing/releases/download/knative-v1.10.1/eventing-crds.yaml
kubectl apply -f https://github.com/knative/eventing/releases/download/knative-v1.10.1/eventing-core.yaml

# Install Channel and Broker
kubectl apply -f https://github.com/knative/eventing/releases/download/knative-v1.10.1/in-memory-channel.yaml
kubectl apply -f https://github.com/knative/eventing/releases/download/knative-v1.10.1/mt-channel-broker.yaml

Prometheus Installation

kubectl create ns monitoring
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install prometheus prometheus-community/kube-prometheus-stack -f yml/prometheus-values.yaml  \
  -n monitoring \
  --version 47.3.0
kubectl port-forward deploy/prometheus-grafana 8070:3000 -n monitoring
Browse to http://localhost:8070 and use the username/password as admin/FTA@CNCF0n@zure3

kubectl port-forward svc/prometheus-kube-prometheus-prometheus 9090:9090 -n monitoring 
Browse to http://localhost:9090

Jaeger Installation

helm repo add jaegertracing https://jaegertracing.github.io/helm-charts
helm repo update

kubectl create ns tracing
helm install jaeger jaegertracing/jaeger -f yml/jaeger-values.yaml \
  -n tracing \
  --version 0.71.8
# Wait for at least ~5 minutes before browsing to the Jaeger UI
kubectl port-forward svc/jaeger-query 8060:80 -n tracing
Browse to http://localhost:8060

Linkerd Installation

Deploy Linkered

# Install cli
curl --tlsv1.2 -sSfL https://run.linkerd.io/install | sed s/LINKERD2_VERSION=.\*/LINKERD2_VERSION=${LINKERD2_VERSION:-stable-2.13.5}/ | sh
export PATH=$PATH:$HOME/.linkerd2/bin
linkerd version
linkerd check --pre

# Generate certificates.
wget https://github.com/smallstep/cli/releases/download/v0.23.4/step-cli_0.23.4_amd64.deb
sudo dpkg -i step-cli_0.23.4_amd64.deb

step certificate create identity.linkerd.cluster.local ca.crt ca.key --profile root-ca --no-password --insecure
step certificate create identity.linkerd.cluster.local issuer.crt issuer.key --ca ca.crt --ca-key ca.key --profile intermediate-ca --not-after 8760h --no-password --insecure

# Install linkerd CRDs
linkerd install --crds | kubectl apply -f -

# Install linkerd
linkerd install --identity-trust-anchors-file ca.crt --identity-issuer-certificate-file issuer.crt --identity-issuer-key-file issuer.key | kubectl apply -f -

# Install linkerd dashboard
linkerd viz install | kubectl apply -f -

Integrate Knative with Linkerd (need to wait for Linker do to come up)

#TODO - Linkerd/Jaeger injection 

Integrate Nginx Ingress controller with Linkerd

kubectl get deploy/nginx-ingress-ingress-nginx-controller -n ingress-basic -o yaml | linkerd inject - | kubectl apply -f - 

Linkerd metrics integration with Prometheus

kubectl create secret generic prometheus-kube-prometheus-prometheus-scrape-confg-linkerd --from-file=additional-scrape-configs.yaml=yml/linkerd-prometheus-additional.yaml -n monitoring

kubectl get prometheus prometheus-kube-prometheus-prometheus -n monitoring -o yaml | sed s/prometheus-kube-prometheus-prometheus-scrape-confg/prometheus-kube-prometheus-prometheus-scrape-confg-linkerd/ | kubectl apply -f -

Linkerd integration with Jaeger

linkerd jaeger install --set collector.jaegerAddr='http://jaeger-collector.tracing:14268/api/traces' | kubectl apply -f -

kubectl annotate namespace ingress-basic config.linkerd.io/trace-collector=collector.linkerd-jaeger:55678

Open the dashboard in browser (linkerd-viz may take up to ~12 minutes to start)

linkerd viz dashboard

Tekton Installation

# Install Tekton pipelines
kubectl apply -f https://storage.googleapis.com/tekton-releases/pipeline/previous/v0.49.0/release.yaml

kubectl apply -f yml/tekton-default-configmap.yaml  -n  tekton-pipelines
kubectl apply -f yml/tekton-feature-flags-configmap.yaml -n  tekton-pipelines

# Install Tekton Triggers

kubectl apply -f https://storage.googleapis.com/tekton-releases/triggers/previous/v0.24.0/release.yaml
kubectl apply -f https://storage.googleapis.com/tekton-releases/triggers/previous/v0.24.0/interceptors.yaml

# Install Tekton Dashboard

kubectl apply -f https://storage.googleapis.com/tekton-releases/dashboard/previous/v0.37.0/release.yaml

kubectl port-forward svc/tekton-dashboard 8080:9097  -n tekton-pipelines

Browse to http://localhost:8080

Prepare for App Installation

Create a namespace for app deployment and annotate it for linkerd and jaeger operations

kubectl create ns conexp-mvp
kubectl annotate namespace conexp-mvp linkerd.io/inject=enabled
kubectl annotate namespace conexp-mvp config.linkerd.io/skip-outbound-ports="4222"
kubectl annotate namespace conexp-mvp config.linkerd.io/trace-collector=collector.linkerd-jaeger:55678

# Create namespace for function deployment by knative
kubectl create ns conexp-mvp-fn
#TODO - Linkerd/Jaeger injection    

Create the registry credentials in the deployment namespaces

kubectl create secret docker-registry regcred --docker-server="https://$registryHost" --docker-username=conexp  --docker-password=FTA@CNCF0n@zure3  --docker-email=user@mycompany.com -n conexp-mvp
kubectl create secret docker-registry regcred --docker-server="https://$registryHost" --docker-username=conexp  --docker-password=FTA@CNCF0n@zure3  --docker-email=user@mycompany.com -n conexp-mvp-fn

Tekton - App Pipelines Deployment

kubectl create ns conexp-mvp-devops
kubectl apply -f yml/tekton-limit-range.yaml

kubectl apply -f yml/app-admin-role.yaml -n conexp-mvp-devops

Create the docker secret for tekton pipelines to push images to the registry

CONFIG="\
{\n
    \"auths\": {\n
        \"${registryHost}\": {\n
            \"username\": \"conexp\",\n
            \"password\": \"FTA@CNCF0n@zure3\",\n
            \"email\": \"user@mycompany.com\",\n
            \"auth\": \"Y29uZXhwOkZUQUBDTkNGMG5AenVyZTM=\"\n
        }\n
    }\n
}\n"

printf "${CONFIG}" > config.json
kubectl create secret generic regcred --from-file=config.json=config.json -n conexp-mvp-devops

Update TriggerBinding for registry name in app-triggers.yaml. Create a SendGrid Account and set an API key for use. Reference this link to create a free SendGrid account and get the SendGrid API key.

sendGridApiKey=<<set the api key>>
appHostName=$topLevelDomain

sed -i "s/{registryHost}/$registryHost/g" yml/app-triggers.yaml

sed -i "s/{SENDGRIDAPIKEYRELACE}/$sendGridApiKey/g" yml/app-pipeline.yaml
sed -i "s/{APPHOSTNAMEREPLACE}/$appHostName/g" yml/app-pipeline.yaml

kubectl apply -f yml/app-pipeline.yaml -n conexp-mvp-devops
kubectl apply -f yml/app-triggers.yaml -n conexp-mvp-devops

Roles and bindings in the deployment namespace

kubectl apply -f https://raw.githubusercontent.com/tektoncd/catalog/main/task/kaniko/0.6/kaniko.yaml -n conexp-mvp-devops
kubectl apply -f yml/git-clone.yaml -n conexp-mvp-devops
kubectl apply -f yml/app-deploy-rolebinding.yaml -n conexp-mvp
kubectl apply -f yml/app-deploy-rolebinding.yaml -n conexp-mvp-fn

Expose the Tekton Event Listener externally through an Ingress for Github to dispatch the push events

cicdWebhookHost=$topLevelDomain

sed -i "s/{cicdWebhook}/$cicdWebhookHost/g" yml/tekton-el-ingress.yaml

kubectl apply -f yml/tekton-el-ingress.yaml -n conexp-mvp-devops

# Payload URL to be used for creating the webhook
echo https://$cicdWebhookHost/cd

Create a Webook in the GitHub repo of the source code by navigating to {Repo} -> Setting -> Webhook -> Add Webhook. Enter the Payload URL from above, select the Content type as application/json and leave the rest as defaults.

Make a change to the readme.md file and observe the deployment in Tekton dashboard.

Launch the Application

Navigate to the FQDN of the NGINX ingress controller set up in the first step, also refered to as the topLevelDomain in the first step. For example {uniquename}.{region}.cloudapp.azure.com.

This will launch the application and you can proceed to create, update, delete expenses.