En Kinsta, tenemos proyectos de todos los tamaños para el alojamiento de aplicaciones, el alojamiento de bases de datos y el alojamiento administrado de WordPress.

Con las soluciones de alojamiento en la nube de Kinsta, puedes desplegar aplicaciones en varios lenguajes y frameworks, como NodeJS, PHP, Ruby, Go, Scala y Python. Con un Dockerfile, puedes desplegar cualquier aplicación. Puedes conectar tu repositorio Git (alojado en GitHub, GitLab o Bitbucket) para desplegar tu código directamente en Kinsta.

Puedes alojar bases de datos MariaDB, Redis, MySQL y PostgreSQL listas para usar, ahorrándote tiempo para centrarte en desarrollar tus aplicaciones en lugar de sufrir con las configuraciones de alojamiento.

Y si eliges nuestro Alojamiento WordPress Administrado, experimentarás la potencia de las máquinas Google Cloud C2 en su red de nivel Premium y la seguridad integrada de Cloudflare, haciendo que tus sitios web WordPress sean los más rápidos y seguros del mercado.

Superar el Reto de Desarrollar Aplicaciones Nativas de la Nube en un Equipo Distribuido

Uno de los mayores retos de desarrollar y mantener aplicaciones nativas de la nube a nivel empresarial es tener una experiencia coherente a lo largo de todo el ciclo de vida de desarrollo. Esto es aún más difícil para empresas remotas con equipos distribuidos que trabajan en plataformas diferentes, con configuraciones distintas y comunicación asíncrona. Necesitamos proporcionar una solución coherente, fiable y escalable que funcione para:

  • Desarrolladores y equipos de control de calidad, independientemente de sus sistemas operativos, crear una configuración sencilla y mínima para desarrollar y probar funciones.
  • Los equipos de DevOps, SysOps e Infra, para configurar y mantener los entornos de staging y producción.

En Kinsta, confiamos mucho en Docker para esta experiencia consistente en cada paso, desde el desarrollo hasta la producción. En este post, te guiaremos a través de:

  • Cómo aprovechar Docker Desktop para aumentar la productividad de los desarrolladores.
  • Cómo creamos imágenes Docker y las enviamos a Google Container Registry a través de canalizaciones CI con CircleCI y GitHub Actions.
  • Cómo utilizamos las canalizaciones de CI para promover cambios incrementales a producción utilizando imágenes Docker, Google Kubernetes Engine y Cloud Deploy.
  • Cómo el equipo de control de calidad utiliza sin problemas imágenes Docker precompiladas en distintos entornos.

Utilización de Docker Desktop para Mejorar la Experiencia de los Desarrolladores

Ejecutar una aplicación localmente requiere que los desarrolladores preparen meticulosamente el entorno, instalen todas las dependencias, configuren servidores y servicios, y se aseguren de que están bien configurados. Cuando ejecutas varias aplicaciones, esto puede resultar engorroso, especialmente cuando se trata de proyectos complejos con múltiples dependencias. Cuando introduces en esta variable múltiples colaboradores con múltiples sistemas operativos, se instala el caos. Para evitarlo, utilizamos Docker.

Con Docker, puedes declarar las configuraciones del entorno, instalar las dependencias y construir imágenes con todo donde debe estar. Cualquiera, en cualquier lugar, con cualquier SO puede utilizar las mismas imágenes y tener exactamente la misma experiencia que los demás.

Declara Tu Configuración con Docker Compose

Para empezar, crea un archivo Docker Compose, docker-compose.yml. Es un archivo de configuración declarativo escrito en formato YAML que indica a Docker cuál es el estado deseado de tu aplicación. Docker utiliza esta información para configurar el entorno de tu aplicación.

Los archivos Docker Compose resultan muy útiles cuando tienes más de un contenedor en ejecución y existen dependencias entre contenedores. Para crear tu archivo docker-compose.yml:

  1. Empieza eligiendo un image como base para nuestra aplicación. Busca en Docker Hub e intenta encontrar una imagen Docker que ya contenga las dependencias de tu aplicación. Asegúrate de utilizar una etiqueta de imagen específica para evitar errores. Utilizar la etiqueta latest puede causar errores imprevistos en tu aplicación. Puedes utilizar varias imágenes base para varias dependencias. Por ejemplo, una para PostgreSQL y otra para Redis.
  2. Utiliza volumes para persistir los datos en tu máquina anfitriona si lo necesitas. Persistir los datos en la máquina anfitriona te ayuda a evitar perderlos si se borran los contenedores Docker o si tienes que volver a crearlos.
  3. Utiliza networks para aislar tu configuración y evitar conflictos de red con el host y otros contenedores. También ayuda a tus contenedores a encontrarse y comunicarse fácilmente entre sí.

Juntándolo todo, tenemos un docker-compose.yml con este aspecto:

version: '3.8'services:
  db:
    image: postgres:14.7-alpine3.17
    hostname: mk_db
    restart: on-failure
    ports:
      - ${DB_PORT:-5432}:5432
    volumes:
      - db_data:/var/lib/postgresql/data
    environment:
      POSTGRES_USER: ${DB_USER:-user}
      POSTGRES_PASSWORD: ${DB_PASSWORD:-password}
      POSTGRES_DB: ${DB_NAME:-main}
    networks:
      - mk_network
  redis:
    image: redis:6.2.11-alpine3.17
    hostname: mk_redis
    restart: on-failure
    ports:
      - ${REDIS_PORT:-6379}:6379
    networks:
      - mk_network
      
volumes:
  db_data:

networks:
  mk_network:
    name: mk_network

Contenedorizar la Aplicación

Construye una imagen Docker para tu aplicación

En primer lugar, tenemos que crear una imagen Docker utilizando Dockerfile, y luego llamarla desde docker-compose.yml. Para crear tu archivo Dockerfile:

  1. Empieza por elegir una imagen como base. Utiliza la imagen base más pequeña que funcione para la aplicación. Normalmente, las imágenes alpines son muy mínimas, con casi ningún paquete extra instalado. Puedes empezar con una imagen alpine y construir sobre ella:
    FROM node:18.15.0-alpine3.17
    
  2. A veces necesitas utilizar una arquitectura de CPU específica para evitar conflictos. Por ejemplo, supongamos que utilizas un procesador arm64-based pero necesitas construir una imagen amd64. Puedes hacerlo especificando -- platform en Dockerfile:
    FROM --platform=amd64 node:18.15.0-alpine3.17
    
  3. Define el directorio de la aplicación e instala las dependencias y copia el resultado en tu directorio raíz:
    WORKDIR /opt/app 
    COPY package.json yarn.lock ./ 
    RUN yarn install 
    COPY . .
  4. Llama al Dockerfile desde docker-compose.yml:
    services:
      ...redis
      ...db
      
      app:
        build:
          context: .
          dockerfile: Dockerfile
        platforms:
          - "linux/amd64"
        command: yarn dev
        restart: on-failure
        ports:
          - ${PORT:-4000}:${PORT:-4000}
        networks:
          - mk_network
        depends_on:
          - redis
          - db
  5. Implementa la auto-recarga para que, cuando cambies algo en el código fuente, puedas previsualizar tus cambios inmediatamente sin tener que reconstruir la aplicación manualmente. Para ello, construye primero la imagen y luego ejecútala en un servicio independiente:
    services:
      ... redis
      ... db
      
      build-docker:
        image: myapp
        build:
          context: .
          dockerfile: Dockerfile
      app:
        image: myapp
        platforms:
          - "linux/amd64"
        command: yarn dev
        restart: on-failure
        ports:
          - ${PORT:-4000}:${PORT:-4000}
        volumes:
          - .:/opt/app
          - node_modules:/opt/app/node_modules
        networks:
          - mk_network
        depends_on:
          - redis
          - db
          - build-docker
          
    volumes:
      node_modules:

Consejo profesional: Ten en cuenta que node_modules también se monta explícitamente para evitar problemas específicos de la plataforma con los paquetes. Significa que en lugar de utilizar el node_modules en el host, el contenedor Docker utiliza el suyo propio pero lo mapea en el host en un volumen separado.

Construye Incrementalmente las Imágenes de Producción con Integración Continua

La mayoría de nuestras aplicaciones y servicios utilizan CI/CD para su despliegue. Docker desempeña un papel importante en el proceso. Cada cambio en la rama principal desencadena inmediatamente una cadena de construcción a través de Acciones de GitHub o CircleCI. El flujo de trabajo general es muy sencillo: instala las dependencias, ejecuta las pruebas, construye la imagen Docker y la envía a Google Container Registry (o Artifact Registry). La parte que trataremos en este artículo es el paso de construcción.

Construir las imágenes Docker

Utilizamos construcciones en varias etapas por razones de seguridad y rendimiento.

Etapa 1: Constructor

En esta etapa copiamos todo el código base con todo el código fuente y la configuración, instalamos todas las dependencias, incluidas las dependencias dev, y construimos la aplicación. Crea una carpeta dist/ y copia allí la versión construida del código. Pero esta imagen es demasiado grande, con un enorme conjunto de huellas, para utilizarla en producción. Además, como utilizamos registros NPM privados, también utilizamos nuestro NPM_TOKEN privado en esta fase. Así que, definitivamente, no queremos que esta etapa esté expuesta al mundo exterior. Lo único que necesitamos de esta etapa es la carpeta dist/.

Fase 2: Producción

La mayoría de la gente utiliza esta etapa para el tiempo de ejecución, ya que está muy cerca de lo que necesitamos para ejecutar la aplicación. Sin embargo, todavía tenemos que instalar las dependencias de producción y eso significa que dejamos huellas y necesitamos la carpeta NPM_TOKEN. Así que esta etapa todavía no está lista para ser expuesta. Además, presta atención a yarn cache clean en la línea 19. Ese minúsculo comando reduce el tamaño de nuestra imagen hasta en un 60%.

Etapa 3: Tiempo de ejecución

La última etapa tiene que ser lo más delgada posible, con huellas mínimas. Así que simplemente copiamos la aplicación totalmente horneada de producción y seguimos adelante. Dejamos atrás todas esas cosas de Yarn y NPM_TOKEN y sólo ejecutamos la aplicación.

Este es el Dockerfile.production final:

# Stage 1: build the source code 
FROM node:18.15.0-alpine3.17 as builder 
WORKDIR /opt/app 
COPY package.json yarn.lock ./ 
RUN yarn install 
COPY . . 
RUN yarn build 

# Stage 2: copy the built version and build the production dependencies FROM node:18.15.0-alpine3.17 as production 
WORKDIR /opt/app 
COPY package.json yarn.lock ./ 
RUN yarn install --production && yarn cache clean 
COPY --from=builder /opt/app/dist/ ./dist/ 

# Stage 3: copy the production ready app to runtime 
FROM node:18.15.0-alpine3.17 as runtime 
WORKDIR /opt/app 
COPY --from=production /opt/app/ . 
CMD ["yarn", "start"]

Observa que, para todas las etapas, empezamos copiando primero los archivos package.json y yarn.lock, instalando las dependencias, y luego copiando el resto de la base de código. La razón es que Docker construye cada comando como una capa sobre la anterior. Y cada compilación podría utilizar las capas anteriores si están disponibles y sólo construir las capas nuevas por motivos de rendimiento.

Supongamos que has cambiado algo en src/services/service1.ts sin tocar los paquetes. Significa que las cuatro primeras capas de la etapa constructora están intactas y podrían reutilizarse. Esto hace que el proceso de construcción sea increíblemente más rápido.

Empujar la aplicación al Registro de Contenedores de Google a través de Pipelines CircleCI

Existen varias formas de construir una imagen Docker en las canalizaciones CircleCI. En nuestro caso, elegimos utilizar circleci/gcp-gcr orbs:

executors:
  docker-executor:
    docker:
      - image: cimg/base:2023.03
orbs:
  gcp-gcr: circleci/[email protected]
jobs:
  ...
  deploy:
    description: Build & push image to Google Artifact Registry
    executor: docker-executor
    steps:
      ...
      - gcp-gcr/build-image:
          image: my-app
          dockerfile: Dockerfile.production
          tag: ${CIRCLE_SHA1:0:7},latest
      - gcp-gcr/push-image:
          image: my-app
          tag: ${CIRCLE_SHA1:0:7},latest

Gracias a Docker, se necesita una configuración mínima para crear y enviar nuestra aplicación.

Enviar la aplicación al Registro de Contenedores de Google mediante acciones de GitHub

Como alternativa a CircleCI, podemos utilizar GitHub Actions para desplegar la aplicación de forma continua. Configuramos gcloud y creamos y enviamos la imagen Docker a gcr.io:

jobs:
  setup-build:
    name: Setup, Build
    runs-on: ubuntu-latest

    steps:
    - name: Checkout
      uses: actions/checkout@v3

    - name: Get Image Tag
      run: |
        echo "TAG=$(git rev-parse --short HEAD)" >> $GITHUB_ENV

    - uses: google-github-actions/setup-gcloud@master
      with:
        service_account_key: ${{ secrets.GCP_SA_KEY }}
        project_id: ${{ secrets.GCP_PROJECT_ID }}

    - run: |-
        gcloud --quiet auth configure-docker

    - name: Build
      run: |-
        docker build 
          --tag "gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:$TAG" 
          --tag "gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:latest" 
          .

    - name: Push
      run: |-
        docker push "gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:$TAG"
        docker push "gcr.io/${{ secrets.GCP_PROJECT_ID }}/my-app:latest"

Con cada pequeño cambio empujado a la rama principal, construimos y enviamos una nueva imagen Docker al registro.

Desplegar Cambios en el Google Kubernetes Engine Utilizando Google Delivery Pipelines

Disponer de imágenes Docker listas para usar para todos y cada uno de los cambios también facilita el despliegue en producción o el retroceso en caso de que algo vaya mal. Utilizamos Google Kubernetes Engine para gestionar y servir nuestras aplicaciones, y utilizamos Google Cloud Deploy y Delivery Pipelines para nuestro proceso de despliegue continuo.

Cuando la imagen Docker se construye después de cada pequeño cambio (con la tubería CI mostrada arriba) damos un paso más y desplegamos el cambio en nuestro clúster de desarrollo utilizando gcloud. Echemos un vistazo a ese paso en la tubería CircleCI:

- run:
    name: Create new release
    command: gcloud deploy releases create release-${CIRCLE_SHA1:0:7} --delivery-pipeline my-del-pipeline --region $REGION --annotations commitId=$CIRCLE_SHA1 --images my-app=gcr.io/${PROJECT_ID}/my-app:${CIRCLE_SHA1:0:7}

Esto desencadena un proceso de lanzamiento para desplegar los cambios en nuestro clúster Kubernetes dev. Después de probar y obtener las aprobaciones, promovemos el cambio a staging y luego a producción. Todo esto es posible porque tenemos una imagen Docker aislada para cada cambio que tiene casi todo lo que necesita. Sólo tenemos que indicar al despliegue qué etiqueta debe utilizar.

Cómo Se Beneficia de este Proceso el Equipo de Control de Calidad

El equipo de control de calidad necesita sobre todo una versión en la nube previa a la producción de las aplicaciones que va a probar. Sin embargo, a veces necesitan ejecutar localmente una aplicación preconstruida (con todas las dependencias) para probar una determinada función. En estos casos, no quieren o no necesitan pasar por todo el dolor de clonar todo el proyecto, instalar paquetes npm, construir la app, enfrentarse a errores de desarrollador y repasar todo el proceso de desarrollo para poner la app en marcha. Ahora que todo está ya disponible como imagen Docker en Google Container Registry, todo lo que necesitan es un servicio en el archivo Docker compose:

services:
  ...redis
  ...db
  
  app:
    image: gcr.io/${PROJECT_ID}/my-app:latest
    restart: on-failure
    ports:
      - ${PORT:-4000}:${PORT:-4000}
    environment:
      - NODE_ENV=production
      - REDIS_URL=redis://redis:6379
      - DATABASE_URL=postgresql://${DB_USER:-user}:${DB_PASSWORD:-password}@db:5432/main
    networks:
      - mk_network
    depends_on:
      - redis
      - db

Con este servicio, pueden poner en marcha la aplicación en sus máquinas locales utilizando contenedores Docker ejecutando:

docker compose up

Este es un gran paso hacia la simplificación de los procesos de prueba. Incluso si el control de calidad decide probar una etiqueta específica de la aplicación, puede cambiar fácilmente la etiqueta de la imagen en la línea 6 y volver a ejecutar el comando Docker compose. Incluso si deciden comparar diferentes versiones de la aplicación simultáneamente, pueden conseguirlo fácilmente con unos pocos ajustes. La mayor ventaja es mantener a nuestro equipo de control de calidad alejado de los retos de los desarrolladores.

Ventajas del Uso de Docker

  • Casi cero huellas de dependencias: Si alguna vez decides actualizar la versión de Redis o Postgres, sólo tienes que cambiar 1 línea y volver a ejecutar la aplicación. No necesitas cambiar nada en tu sistema. Además, si tienes dos aplicaciones que necesitan Redis (quizá incluso con versiones diferentes), puedes hacer que ambas se ejecuten en su propio entorno aislado sin que haya conflictos entre ellas.
  • Múltiples instancias de la aplicación: Hay muchos casos en los que necesitamos ejecutar la misma app con un comando diferente. Como inicializar la BD, ejecutar pruebas, ver los cambios en la BD o escuchar mensajes. En cada uno de estos casos, como ya tenemos lista la imagen construida, sólo tenemos que añadir otro servicio al archivo Docker compose con un comando diferente, y ya está.
  • Entorno de pruebas más fácil: La mayoría de las veces, sólo necesitas ejecutar la aplicación. No necesitas el código, los paquetes ni ninguna conexión a bases de datos locales. Sólo quieres asegurarte de que la aplicación funciona correctamente o necesitas una instancia en ejecución como servicio backend mientras trabajas en tu propio proyecto. También podría ser el caso de QA, revisores de Pull Request, o incluso gente de UX que quiere asegurarse de que su diseño se ha implementado correctamente. Nuestra configuración Docker hace que sea muy fácil para todos ellos poner las cosas en marcha sin tener que lidiar con demasiadas cuestiones técnicas.

Este artículo se publicó originalmente en Docker.

Amin Choroomi

Desarrollador de software en Kinsta. Apasionado de Docker y Kubernetes, está especializado en desarrollo de aplicaciones y prácticas DevOps.