Building my Jekyll blog with Docker, K8s, and Azure-pipelines Pt 1
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.