Configurar Microk8s para usar repositorios de AWS ECR

Continuando con un post anterior de cómo probar Ansible AWX con Microk8s (en AWS EC2). Bueno, pues resulta que me creé una imagen personalizada para el contener awx_task para instalar una serie de librerías y comandos que necesitaba para lanzar unos playbooks; el fichero Dockerfile es similar a éste:

FROM ansible/awx_task:9.1.1

# Switch user to become root
USER 0

# Additional software
RUN cd && \
    set -x && \
    dnf install -y nmap-ncat htop && \
    dnf clean all

# Ansible venv additional dependencies
RUN cd && \
    source /var/lib/awx/venv/ansible/bin/activate && \
    umask 0022 && \
    pip install --upgrade pypsrp pysocks && \
    deactivate

# Restore the original user
# https://github.com/ansible/awx/blob/devel/installer/roles/image_build/templates/Dockerfile.task.j2
USER 1000

Y me creé un repositorio en AWS ECR. Después generé la imagen y la subí al repositorio (siendo xxxxxxxxxxx el ID de la cuenta de AWS):

$(aws ecr get-login --no-include-email)
docker build --force-rm --pull --no-cache -t xxxxxxxxxxx.dkr.ecr.eu-west-1.amazonaws.com/ansible/awx_task:9.1.1 .
docker push xxxxxxxxxxx.dkr.ecr.eu-west-1.amazonaws.com/ansible/awx_task:9.1.1

Tras esto modifiqué el fichero de inventario que usa el instalador de AWX para hacer referencia a la imagen que acabo de subir y crear.

kubernetes_task_image=xxxxxxxxxxx.dkr.ecr.eu-west-1.amazonaws.com/ansible/awx_task

Pero cuando el cluster de Kubernetes intenta obtener la imagen para crear el pod, se queda en estado ErrImagePull con el mensaje:

  Normal   Pulling            2s (x3 over 46s)   kubelet, pcjuan    Pulling image "xxxxxxxxxxx.dkr.ecr.eu-west-1.amazonaws.com/ansible/awx_task:9.1.1"
  Warning  Failed             2s (x3 over 45s)   kubelet, pcjuan    Failed to pull image "xxxxxxxxxxx.dkr.ecr.eu-west-1.amazonaws.com/ansible/awx_task:9.1.1": rpc error: code = Unknown desc = failed to resolve image "xxxxxxxxxxx.dkr.ecr.eu-west-1.amazonaws.com/ansible/awx_task:9.1.1": no available registry endpoint: unexpected status code https://xxxxxxxxxxx.dkr.ecr.eu-west-1.amazonaws.com/v2/ansible/awx_task/manifests/9.1.1: 401 Unauthorized
  Warning  Failed             2s (x3 over 45s)   kubelet, pcjuan    Error: ErrImagePull

Esto se debe a que Kubernetes no tiene las credenciales necesarias para acceder al repositorio. Pero después de investigar, es fácil solucionarlo. Lo primero que tenemos que hacer es crear un cronjob en Kubernetes (lo haremos con un crojob porque realmente lo que usa Docker es un token, que tiene caducidad, y hay que renovarlo cada cierto tiempo), para que haga login en el repositorio, y cree una credencial para obtener de forma correcta la imagen; para esto, crearemos un fichero llamado ecr-cred-refresh.yml con el siguiente contenido:

apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: ecr-cred-helper
spec:
  concurrencyPolicy: Allow
  schedule: 0 */6 * * *
  failedJobsHistoryLimit: 1
  successfulJobsHistoryLimit: 3
  suspend: false
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - command:
            - /bin/sh
            - -c
            - |-
              NAMESPACE=awx
              SERVICE_ACCOUNT=awx
              ACCOUNT=$(aws sts get-caller-identity --query 'Account' --output text)
              REGION=$(curl -s http://169.254.169.254/latest/dynamic/instance-identity/document | python -c "import json,sys; print(json.loads(sys.stdin.read())['region'])")
              SECRET_NAME=${REGION}-ecr-registry
              EMAIL=anymail.doesnt.matter@email.com
              TOKEN=$(aws ecr get-login --region ${REGION} --registry-ids ${ACCOUNT} | cut -d' ' -f6)
              echo "ENV variables setup done."
              kubectl -n ${NAMESPACE} delete secret --ignore-not-found $SECRET_NAME
              kubectl -n ${NAMESPACE} create secret docker-registry $SECRET_NAME \
                  --docker-server=https://${ACCOUNT}.dkr.ecr.${REGION}.amazonaws.com \
                  --docker-username=AWS \
                  --docker-password="${TOKEN}" \
                  --docker-email="${EMAIL}"
              echo "Secret created by name $SECRET_NAME"
              kubectl -n ${NAMESPACE} patch serviceaccount ${SERVICE_ACCOUNT} -p '{"imagePullSecrets":[{"name":"'$SECRET_NAME'"}]}'
              echo "All done."
            image: odaniait/aws-kubectl:latest
            imagePullPolicy: IfNotPresent
            name: ecr-cred-helper
            resources: {}
            securityContext:
              capabilities: {}
            terminationMessagePath: /dev/termination-log
            terminationMessagePolicy: File
          dnsPolicy: Default
          hostNetwork: true
          restartPolicy: Never
          schedulerName: default-scheduler
          securityContext: {}
          terminationGracePeriodSeconds: 30

En el fichero anterior, dependiendo de nuestra configuración particular, podremos cambiar el valor de las variables NAMESPACE y SERVICE_ACCOUNT, y también especificar manualmente las variables ACCOUNT y REGION si no queremos que el script las auto-detecte porque usamos otras en concreto.

Básicamente, lo que hace esto, es crear un trabajo de cron, que lanza un contenedor y ejecuta el script en Bash definido en la especificación del pod. En resumen:

  • Obtiene las credenciales de acceso a ECR utilizando la cli de AWS
  • Elimina, si existe, el secreto llamado ${REGION}-ecr-registry
  • Lo crea de nuevo, con el token obtenido de ECR
  • Actualiza la service account de AWX indicándole que para obtener las imágenes (imagePullSecrets) tiene usar las credenciales del secreto recién creado

Tras esto, importamos la definición del job en Kubernetes:

kubectl -n awx apply -f ecr-cred-refresh.yml

Este job se ejecuta cada 6 horas; si queremos forzar la ejecución, podemos hacerlo con los siguientes comandos:

JOB_NAME="manual-$(date --utc +%Y%m%d-%H%M%S)"
kubectl -n awx create job --from=cronjob/ecr-cred-helper ${JOB_NAME}
kubectl -n awx wait --for=condition=complete job.batch/${JOB_NAME}
kubectl -n awx logs job.batch/${JOB_NAME}

Pero esto por sí solo no nos vale… porque ¿dónde le decimos las crendenciales para acceder a AWS (es decir, para que desde dentro del cronjob se pueda hacer aws ecr get-login)? Es decir, el access key y el secret. Para esto, no le pasaremos una key y un secret, sino que crearemos un rol y se lo asignaremos a la instancia EC2 de AWS donde estemos ejecutando Microk8s. El rol debe tener una policy que permita a la instancia acceder al repositorio; podemos usar la policy predefinida AmazonEC2ContainerRegistryReadOnly o crear una manualmente.

Tras crear el rol y la policy, y asignar el rol a la instancia, podemos ejecutar manualmente el cronjob, y ejecutar de nuevo el instalador de AWX, que ya debería obtener la imagen de Docker sin problemas.

Comandos útiles:

# Ver información de la service account de awx
kubectl -n awx describe serviceaccounts awx

# Ver información de los secretos (cuándo se actualizó/obtuvo el token por última vez)
kubectl -n awx get secrets

Probar manualmente el script:

kubectl run -i --tty --rm debug --image=odaniait/aws-kubectl:latest --restart=Never -- sh
kubectl run --generator=run-pod/v1 -n awx --rm -i --tty compass-tmp --image=odaniait/aws-kubectl:latest -- sh

Referencias:

comments powered by Disqus

Relacionado