3 minute read

As part of a school lab, we were to Containerize a very simple node-js app and serve it with Azure K8s. But I thought to take it a step further and get my Jekyll blog live through Azure-piplines and Azure Kubernete Services.

If you are following along, I assume you have an Azure subscription, a Resource group, Container Registry, and an Azure Devops account.

The steps I took to get to the final stage:

  • Build the Docker image for the site
  • Push the image to the Azure Container Registry
  • Create an ingress controller for a proxy
  • Set up a Certificate Issuer + Certificate for SSL from LetsEncrypt
  • Deploy through Azure Devops pipelines

Create the Docker Image

I will be using a multi-stage Dockerfile setup - Using the official Ruby image, as my site is being run on Ruby (Jekyll). Then serving all my static content from an Nginx image on port 80. Simple enough right?

# First step: Copy all current directory files & gems into container
FROM ruby:2.5 AS build
RUN bundle config --global frozen 1
RUN gem install jekyll
WORKDIR /usr/src/app
COPY Gemfile Gemfile.lock ./
RUN bundle install


# Second step: from the build (first stage builds, then is destroyed)
# all static files are copied to the /usr/src/app 
# and served from nginx:80
FROM build AS publish
COPY . ./
ENV JEKYLL_ENV=production
RUN bundle exec jekyll build

FROM nginx AS serve
COPY --from=publish /usr/src/app/_site /usr/share/nginx/html

I also created a docker-compose.yml file so I can work on the site locally with the same build stage as the production deployment:

# This docker-compose file is to develop the site in the same build as the production

version: '3.6'
services:
  jekyll:
    build:
      context: .
      target: build
    command: bundle exec jekyll serve -H 0.0.0.0 --force_polling
    volumes:
    - type: bind
      source: ./
      target: /usr/src/app
    ports:
    - "4000:4000"
    - "35729:35729"
    - "3000:3000"

Now we can build and tag the container with docker build . -t blog and if we want to view it locally to confirm the image worked use the docker-compose file with docker-compose up -d, it should now be viewable on localhost.

The last step of the ‘docker’ stage is to push the image up to the Azure registry. To do this, first tag the image to the ACR with docker tag blog abblogregistry.azurecr.io/blog:v1, then push the image: docker push abblogregistry.azurecr.io/blog.

Azure Kubernetes deployment

First step is to set up Azure Kubernetes Services and attach our Container Registry to it - Microsoft has plenty of documentation to set these two things up, so I won’t be covering it here.

Now we will create a pod in the form of a deployment.yml file:

apiVersion : apps/v1beta1
kind: Deployment
metadata:
  name: blog 
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: blog 
    spec:
      containers:
        - name: blog 
          image: abblogregistry.azurecr.io/blog:v1
          ports:
          - containerPort: 80

deploy the pod using kubectl apply -f deployment.yml

Now you need to define a service. A Kubernetes service provides end to end networking between pods. It also defines how the access will work in a pod.

apiVersion: v1
kind: Service
metadata:
    name: blog
    namespace: kube-system
spec:
    type: ClusterIP
    ports:
    - port: 80 
    selector:
        app: blog

deploy to the pod with kubectl apply -f service.yml

Next, the ingress controller. This will provide load-balancing, and an external ip address we will use later to set up a domain name.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
 name: blog
 annotations:
   nginx.ingress.kubernetes.io/rewrite-target: /
   kubernetes.io/ingress.class: nginx
   certmanager.k8s.io/cluster-issuer: blog-letsencrypt
spec:
 tls:
 - hosts:
   - blog.aidanb.net
   secretName: blog
 rules:
 - host: www.aidanb.net
   http:
     paths:
     - path: /
       backend:
         serviceName: blog
         servicePort: 80
 - host: aidanb.net
   http:
     paths:
     - path: /
       backend:
         serviceName: blog
         servicePort: 80

kubectl deploy -f ingress.yml

Now to provide a certificate and ssl we will use a certificate-issuer.yml and a certificate.yml.

certificate-issuer.yml:

apiVersion: certmanager.k8s.io/v1alpha1
kind: ClusterIssuer
metadata:
 name: letsencrypt-prod
 namespace: kube-system
spec:
 acme:
   server: https://acme-v02.api.letsencrypt.org/directory
   email: [email protected]
   privateKeySecretRef:
     name: letsencrypt-prod
   http01: {}

certificate.yml:

apiVersion: certmanager.k8s.io/v1alpha1
kind: Certificate
metadata:
 name: letsencrypt-prod
 namespace: kube-system
spec:
 secretName: blog
 issuerRef:
   name: letsencrypt-prod
   kind: ClusterIssuer
 commonName: 'aidanb.net'
 dnsNames:
 - aidanb.net
 acme:
   config:
   - http01:
       ingressClass: nginx
     domains:
     - aidanb.net

Finally we need some persistant storage. For this we will need a volume.yml:

 apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: blog
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: default

Done with the Kubernetes cluster! You should now be able to browse to your site from the external ip of the service with kubectl get svc.

In part two we will move onto configuring a build/release pipeline with Azure Devops.

Categories: ,

Updated: