Closed gcm1001 closed 4 years ago
Dado que existen varias formas de implementar la integración continua (CI) y el despliegue continuo (CD), voy a proponer el siguiente método.
Aprovechando que estamos utilizando Docker
para el despliegue de la aplicación, creo que es una buena idea implementar el CD
haciendo uso de Web-Hooks
.
Un WebHook es un método que permite, a través de llamadas HTTP, comunicar en tiempo real dos aplicaciones o servicios web. Para que esta comunicación se efectúe, debe acontecer un evento que desencadene la acción.
Su funcionamiento es bastante sencillo, por una parte tendríamos al proveedor de webhook, que es la aplicación o servicio web cuyo cometido es enviar una señal cuando acontece un evento determinado. Por otra parte tenemos al listener, que es quien se encarga de recibir los webhooks y ejecutar las acciones asociadas a cada uno de ellos.
Para llevar a cabo este método de despliegue continuo hay que cumplir con una serie de requisitos:
omeka_cenieh
), necesitamos contar con un repositorio en DockerHub que almacene las imágenes custom que vamos a utilizar en el despliegue.Como hemos comentado anteriormente, necesitaremos, para el despliegue, tener instalado Docker en nuestro servidor.
sudo apt-get update
sudo apt-get install docker
sudo systemctl enable docker
sudo systemctl start docker
A continuación, debemos instalar la herramienta webhook
para poder establecer la comunicación entre Github Actions y nuestro servidor.
sudo apt-get install webhook
Una vez instalado, necesitamos configurar el webhook que se encargará de atender la llamada asociada al despliegue. Para ello, en cualquier directorio del servidor, necesitamos crear un archivo al que llamaremos hooks.json
.
[
{
"id": "accioncd",
"execute-command": "/directorio/scripts/accioncd.sh",
"command-working-directory": "/directorio/de/trabajo",
"response-message": "Despliegue continuo."
}
]
Los elementos configurados han sido:
id
: identificador asociado al hook. execute-command
: señala el comando/fichero de comandos que se ejecutará tras una llamada.command-working-directory
: directorio desde donde se ejecutarán los comandos indicados.response-message
: mensaje de respuesta que se envía al proveedor.Como podemos observar, en el campo execute-command
he indicado un fichero denominado accioncd.sh
. Por ello, la siguiente tarea será crear, en el directorio indicado, dicho fichero. Este debe contener todos los comandos relativos al despliegue de la aplicación.
#!/bin/bash
docker stack deploy -c docker-compose.yml omeka_cenieh
docker system prune -f
La secuencia de comandos es la siguiente:
Para que esta secuencia funcione perfectamente deberíamos tener configurados previamente los secrets
indicados en el fichero docker-compose.yml
. Además, como es lógico, tanto el fichero docker-compose.yml
como el fichero secreto db.ini
deben estar almacenados en el directorio donde se ejecutará la acción, es decir, la indicada en el campo command-working-directory
del fichero hooks.json
.
cd /directorio/de/trabajo
wget https://raw.githubusercontent.com/gcm1001/TFG-CeniehAriadne/master/docker-compose.yml
wget https://raw.githubusercontent.com/gcm1001/TFG-CeniehAriadne/master/db.ini.modificar
cp db.ini.modificar db.ini
Por último, activamos el endpoint.
webhook -hooks /directorio/del/hook/hooks.json -verbose
El siguiente paso es actualizar el workflow utilizado por Github Actions
. Este proceso se explicará en el próximo comentario.
Actualmente, nuestro workflow crea la imagen Docker asociada a la aplicación, la publica en DockerHub y "despliega" la infraestructura sobre el contenedor de Github Actions
. Esta última acción es inútil ya que no se puede acceder a ninguno de los contenedores desplegados. Por ello, la primera modificación será eliminar esta parte.
name: Docker CI/CD
on:
push:
branches:
- master
paths-ignore:
- 'docs/**'
jobs:
docker:
name: Publicación - DockerHub
runs-on: ubuntu-latest
env:
REPO: ${{ secrets.DOCKER_REPO }}
steps:
- uses: actions/checkout@v1
- name: Login
run: docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }}
- name: Build
run: docker build -t omeka_cenieh .
- name: Tags
run: |
docker tag omeka_cenieh $REPO:${{ github.sha }}
docker tag omeka_cenieh $REPO:latest
- name: Push
run: |
docker push $REPO:${{ github.sha }}
docker push $REPO:latest
Como se puede observar, no solo he eliminado la parte de despliegue, también he añadido una variable de entorno, REPO
, que almacenará el nombre del repositorio de nuestra imagen, oculto mediante un nuevo secret
.
Para añadir la parte de despliegue, configuraré un nuevo job
al que llamaré deploy
. En su interior haré uso del WebHook Action, acción que nos permite llevar a cabo la comunicación con nuestro servidor.
...
deploy:
name: Despliegue - Llamada WebHook
runs-on: ubuntu-latest
needs: [docker]
steps:
- name: Despliegue Continuo
uses: joelwmale/webhook-action@master
env:
WEBHOOK_URL: ${{ secrets.DEPLOY_WEBHOOK_URL }}
data: "Deploying from github actions!"
Los campos de configuración relevantes son:
WEBHOOK_URL
: indica la dirección url asociada al webhook sobre el que queremos realizar la llamada. Para su asignación he utilizado un nuevo secret
.data
: se pueden añadir datos adicionales en la llamada. En nuestro caso, se podría dejar vacío ya que, de momento, no requerimos ninguna información adicional por parte de Github Actions.Con este último paso, ya tendríamos configurado el workflow. A partir de este momento, cada cambio cometido sobre la rama master
provocará la publicación de la imagen sobre DockerHub y el posterior despliegue de la aplicación, utilizando esta nueva imagen, sobre el servidor del CENIEH.
En este momento lo ideal sería crear una nueva rama de desarrollo, develop
, sobre la que pudiera realizar modificaciones sin que estas sean inmediatamente cometidas sobre el servidor.
A su vez, podría crear un nuevo workflow destinado a desplegar la infraestructura cada vez que cometiera una modificación sobre la rama de desarrollo, pero esta vez sobre el servidor de desarrollo, excluyendo por tanto al servidor en producción.
Tras comentar la primera propuesta al administrador de sistemas del CENIEH, este me confirmó que no era viable ya que no iba a poder administrarme una IP para el servidor de desarrollo y, por lo tanto, no existía la posibilidad de realizar conexiones entrantes al servidor, imposibilitando cualquier tipo de conexión del servidor con el exterior.
Como solución a este problema, se me ha ocurrido una segunda propuesta, que consistirá en montar mi propio servidor de desarrollo en la nube, donde podré llevar a cabo los procesos propios de la integración continua y, además, será accesible al público.
Para montar el servidor en la nube he utilizado la plataforma Google Kubernetes Engine (GKE) de Google Cloud.
Container Registry
y Kubernetes Engine APIs
.Haz clic aquí para activarlos automáticamenteKubernetes Engine Developer
- nos permitirá desplegar aplicaciones en la plataforma GKEStorage Admin
- nos permitirá publicar contenedores Docker en la plataforma Container RegistryComo hemos venido haciendo anteriormente, para implementar las técnicas CI/CD a través de Github Actions hay que configurar un workflow. En este caso, me he basado en la configuración por defecto que Github te propone al comenzar el proceso de configuración.
name: Build and Deploy to GKE
on:
push:
branches:
- master
paths-ignore:
- 'docs/**'
env:
PROJECT_ID: ${{ secrets.GKE_PROJECT }}
GKE_CLUSTER: cluster-cenieh-project
GKE_ZONE: europe-west2-a
IMAGE: gke-omeka
jobs:
setup-build-publish-deploy:
name: Setup, Build, Publish, and Deploy
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
# Setup gcloud CLI
- uses: GoogleCloudPlatform/github-actions/setup-gcloud@master
with:
version: '290.0.1'
service_account_key: ${{ secrets.GKE_SA_KEY }}
project_id: ${{ secrets.GKE_PROJECT }}
# Configure Docker to use the gcloud command-line tool as a credential
# helper for authentication
- run: |-
gcloud --quiet auth configure-docker
# Get the GKE credentials so we can deploy to the cluster
- run: |-
gcloud container clusters get-credentials "$GKE_CLUSTER" --zone "$GKE_ZONE"
# Build the Docker image
- name: Build
run: |-
docker build \
--tag "gcr.io/$PROJECT_ID/$IMAGE:$GITHUB_SHA" \
--build-arg GITHUB_SHA="$GITHUB_SHA" \
--build-arg GITHUB_REF="$GITHUB_REF" \
.
docker build \
--tag "gcr.io/$PROJECT_ID/$IMAGE:latest" \
.
# Push the Docker image to Google Container Registry
- name: Publish
run: |-
docker push "gcr.io/$PROJECT_ID/$IMAGE:$GITHUB_SHA"
docker push "gcr.io/$PROJECT_ID/$IMAGE:latest"
# Set up kustomize
- name: Set up Kustomize
run: |-
curl -sfLo kustomize https://github.com/kubernetes-sigs/kustomize/releases/download/v3.1.0/kustomize_3.1.0_linux_amd64
chmod u+x ./kustomize
# Deploy the Docker images to the GKE cluster
- name: Deploy
run: |-
./kustomize build . | kubectl apply -f -
kubectl rollout status deployment/$IMAGE
kubectl get services -o wide
Los procesos que se llevan a cabo son los siguientes:
Checkout
: recogemos el contenido del repositorio.setup-gcloud
: preparamos el entorno para tener acceso a todas las herramientas existentes en la plataforma Google Cloud..yaml
..yaml
, actualizamos el servidor, y comprobamos que se han creado todos los servicios correspondientes..yaml
para KustomizeEsta etapa ha sido la más complicada de todas ya que la sintaxis utilizada en este tipo de ficheros es distinta a la utilizada en los ficheros docker-compose.yml
.
Dado que nuestra infraestructura utiliza dos contenedores, uno para la base de datos y otro para la aplicación web, he tenido que crear los ficheros correspondientes a estas dos bases, MySQL
y Omeka
, junto al fichero kustomization.yaml
para "unir" ambas bases.
Además, para montar el volumen e indicar las variables de entorno de la base omeka
, he creado un fichero adicional denominiado patch.yml
.
kustomization.yaml
resources:
deployment.yaml
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
name: omeka
labels:
app: omeka
spec:
selector:
matchLabels:
app: omeka
strategy:
type: Recreate
template:
metadata:
labels:
app: omeka
spec:
containers:
- image: gcr.io/cenieh-project/gke-omeka
name: omeka
ports:
- containerPort: 80
name: omeka
resources:
requests:
cpu: 100m
limits:
cpu: 100m
service.yaml
apiVersion: v1
kind: Service
metadata:
name: omeka
labels:
app: omeka
spec:
ports:
- port: 80
selector:
app: omeka
type: LoadBalancer
loadBalancerIP: "35.246.115.96"
kustomization.yaml
resources:
deployment.yaml
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2
kind: Deployment
metadata:
name: mysql
labels:
app: mysql
spec:
selector:
matchLabels:
app: mysql
strategy:
type: Recreate
template:
metadata:
labels:
app: mysql
spec:
containers:
- image: mysql:5.6
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: omeka-db
key: user-password
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: omeka-db
key: root-password
- name: MYSQL_DATABASE
valueFrom:
secretKeyRef:
name: omeka-db
key: database
- name: MYSQL_USER
valueFrom:
secretKeyRef:
name: omeka-db
key: username
ports:
- containerPort: 3306
name: mysql
volumeMounts:
- name: mysql-persistent-storage
mountPath: /var/lib/mysql
volumes:
- name: mysql-persistent-storage
emptyDir: {}
service.yaml
apiVersion: v1
kind: Service
metadata:
name: mysql
labels:
app: mysql
spec:
ports:
- port: 3306
selector:
app: mysql
kustomization.yaml
resources:
vars:
patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: omeka
spec:
template:
spec:
containers:
- name: omeka
env:
- name: DB_HOST
value: $(MYSQL_SERVICE)
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: omeka-db
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: omeka-db
key: user-password
- name: DB_DATABASE
valueFrom:
secretKeyRef:
name: omeka-db
key: database
volumeMounts:
- name: db-config
mountPath: /var/www/html/db.ini
subPath: db.ini
restartPolicy: Always
volumes:
- name: db-config
configMap:
name: db-config
Los secretos utilizados por los ficheros yaml
de la etapa anterior tienen que estar presentes en el servidor. Para crearlos, hay que ejecutar los siguientes comandos en el servidor:
omeka-db
kubectl create secret omeka-db \
--from-literal=user-password=$DB_PASSWORD \
--from-literal=root-password=$DB_ROOT_PASSWORD \
--from-literal=username=$DB_USERNAME \
--from-literal=database=$DB_DATABASE
db-config
kubectl create configmap db-config \
--from-file ./configFiles/db.ini
Como es lógico, deben estar configuradas las variables de entorno utilizadas en el primer comando.
La última etapa consistiría en ejecutar un commit
sobre la rama master
(siempre que el directorio afectado no sea /docs
), comprobando así que se activa correctamente la acción recién creada y que finaliza de forma exitosa.
En aproximadamente 48h ya se podrá acceder a la aplicación a través de este enlace.