Declaración del Problema
Desafío
Las organizaciones que ejecutan aplicaciones en contenedores en Kubernetes a menudo necesitan capturar y preservar el estado de los contenedores en ejecución para:
- Recuperación ante desastres
- Migración de aplicaciones
- Depuración/resolución de problemas
- Preservación del estado
- Reproducción del entorno
Sin embargo, no hay una forma sencilla y automatizada de:
- Crear puntos de control de contenedores a pedido
- Almacenar estos puntos de control en un formato estandarizado
- Hacerlos fácilmente accesibles en todos los clústeres
- Disparar la creación de puntos de control a través de una interfaz estándar
Limitaciones Actuales
- La creación manual de puntos de control requiere acceso directo al clúster
- No hay un formato de almacenamiento estandarizado para los puntos de control
- Integración limitada con registros de contenedores
- Falta de acceso programático para la automatización
- Coordinación compleja entre containerd y sistemas de almacenamiento
Solución
Un servicio auxiliar de Kubernetes que:
- Expone la funcionalidad de puntos de control a través de una API REST
- Convierte automáticamente los puntos de control en imágenes compatibles con OCI
- Almacena las imágenes en ECR para una distribución fácil
- Se integra con la infraestructura de Kubernetes existente
- Proporciona una interfaz estandarizada para la automatización
Esto resuelve los problemas principales mediante:
- Automatizar el proceso de puntos de control
- Estándarización del almacenamiento de checkpoints
- Haciendo checkpoints portables
- Facilitando el acceso programático
- Simplificando la integración con flujos de trabajo existentes
Usuarios objetivo:
- Equipos de DevOps
- Ingenieros de plataforma
- Desarrolladores de aplicaciones
- Ingenieros de Confiabilidad del Sitio (SREs)
El checkpointing forense de contenedores se basa en Checkpoint/Restore In Userspace (CRIU) y permite la creación de copias con estado de un contenedor en ejecución sin que el contenedor se dé cuenta de que está siendo checkpointeado. La copia del contenedor puede ser analizada y restaurada en un entorno de sandbox múltiples veces sin que el contenedor original lo sepa. El checkpointing forense de contenedores se introdujo como una característica alfa en Kubernetes v1.25.
Este artículo te guiará sobre cómo implementar código en Golang que se puede utilizar para realizar un checkpoint de contenedor utilizando una API.
El código toma un identificador de pod, recupera el ID del contenedor de containerd como entrada, y luego utiliza el comando ctr
para realizar el checkpoint del contenedor específico en el espacio de nombres k8s.io
de containerd:
- Cluster de Kubernetes
- Instale la herramienta de línea de comandos
ctr
. si puede ejecutar comandos ctr en el kubelet o en el nodo worker; si no, instale o ajuste la AMI para contener el ctr. kubectl
configurado para comunicarse con su clúster- Docker instalado localmente
- Acceso a un registro de contenedores (por ejemplo, Docker Hub, ECR)
- Helm (para instalar el controlador de Ingress de Nginx)
Paso 0: Código para crear un punto de control de contenedor usando GO
Cree un archivo llamado checkpoint_container.go
con el siguiente contenido:
package main
import (
"context"
"fmt"
"log"
"os"
"os/exec"
"strings"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ecr"
"github.com/containerd/containerd"
"github.com/containerd/containerd/namespaces"
)
func init() {
log.SetOutput(os.Stdout)
log.SetFlags(log.Ldate | log.Ltime | log.Lmicroseconds | log.Lshortfile)
}
func main() {
if len(os.Args) < 4 {
log.Fatal("Usage: checkpoint_container <pod_identifier> <ecr_repo> <aws_region>")
}
podID := os.Args[1]
ecrRepo := os.Args[2]
awsRegion := os.Args[3]
log.Printf("Starting checkpoint process for pod %s", podID)
containerID, err := getContainerIDFromPod(podID)
if err != nil {
log.Fatalf("Error getting container ID: %v", err)
}
err = processContainerCheckpoint(containerID, ecrRepo, awsRegion)
if err != nil {
log.Fatalf("Error processing container checkpoint: %v", err)
}
log.Printf("Successfully checkpointed container %s and pushed to ECR", containerID)
}
func getContainerIDFromPod(podID string) (string, error) {
log.Printf("Searching for container ID for pod %s", podID)
client, err := containerd.New("/run/containerd/containerd.sock")
if err != nil {
return "", fmt.Errorf("failed to connect to containerd: %v", err)
}
defer client.Close()
ctx := namespaces.WithNamespace(context.Background(), "k8s.io")
containers, err := client.Containers(ctx)
if err != nil {
return "", fmt.Errorf("failed to list containers: %v", err)
}
for _, container := range containers {
info, err := container.Info(ctx)
if err != nil {
continue
}
if strings.Contains(info.Labels["io.kubernetes.pod.uid"], podID) {
log.Printf("Found container ID %s for pod %s", container.ID(), podID)
return container.ID(), nil
}
}
return "", fmt.Errorf("container not found for pod %s", podID)
}
func processContainerCheckpoint(containerID, ecrRepo, region string) error {
log.Printf("Processing checkpoint for container %s", containerID)
checkpointPath, err := createCheckpoint(containerID)
if err != nil {
return err
}
defer os.RemoveAll(checkpointPath)
imageName, err := convertCheckpointToImage(checkpointPath, ecrRepo, containerID)
if err != nil {
return err
}
err = pushImageToECR(imageName, region)
if err != nil {
return err
}
return nil
}
func createCheckpoint(containerID string) (string, error) {
log.Printf("Creating checkpoint for container %s", containerID)
checkpointPath := "/tmp/checkpoint-" + containerID
cmd := exec.Command("ctr", "-n", "k8s.io", "tasks", "checkpoint", containerID, "--checkpoint-path", checkpointPath)
output, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("checkpoint command failed: %v, output: %s", err, output)
}
log.Printf("Checkpoint created at: %s", checkpointPath)
return checkpointPath, nil
}
func convertCheckpointToImage(checkpointPath, ecrRepo, containerID string) (string, error) {
log.Printf("Converting checkpoint to image for container %s", containerID)
imageName := ecrRepo + ":checkpoint-" + containerID
cmd := exec.Command("buildah", "from", "scratch")
containerId, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to create container: %v", err)
}
cmd = exec.Command("buildah", "copy", string(containerId), checkpointPath, "/")
err = cmd.Run()
if err != nil {
return "", fmt.Errorf("failed to copy checkpoint: %v", err)
}
cmd = exec.Command("buildah", "commit", string(containerId), imageName)
err = cmd.Run()
if err != nil {
return "", fmt.Errorf("failed to commit image: %v", err)
}
log.Printf("Created image: %s", imageName)
return imageName, nil
}
func pushImageToECR(imageName, region string) error {
log.Printf("Pushing image %s to ECR in region %s", imageName, region)
sess, err := session.NewSession(&aws.Config{
Region: aws.String(region),
})
if err != nil {
return fmt.Errorf("failed to create AWS session: %v", err)
}
svc := ecr.New(sess)
authToken, registryURL, err := getECRAuthorizationToken(svc)
if err != nil {
return err
}
err = loginToECR(authToken, registryURL)
if err != nil {
return err
}
cmd := exec.Command("podman", "push", imageName)
err = cmd.Run()
if err != nil {
return fmt.Errorf("failed to push image to ECR: %v", err)
}
log.Printf("Successfully pushed checkpoint image to ECR: %s", imageName)
return nil
}
func getECRAuthorizationToken(svc *ecr.ECR) (string, string, error) {
log.Print("Getting ECR authorization token")
output, err := svc.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{})
if err != nil {
return "", "", fmt.Errorf("failed to get ECR authorization token: %v", err)
}
authData := output.AuthorizationData[0]
log.Print("Successfully retrieved ECR authorization token")
return *authData.AuthorizationToken, *authData.ProxyEndpoint, nil
}
func loginToECR(authToken, registryURL string) error {
log.Printf("Logging in to ECR at %s", registryURL)
cmd := exec.Command("podman", "login", "--username", "AWS", "--password", authToken, registryURL)
err := cmd.Run()
if err != nil {
return fmt.Errorf("failed to login to ECR: %v", err)
}
log.Print("Successfully logged in to ECR")
return nil
}
Paso 1: Inicialice el módulo de go
go mod init checkpoint_container
Modifique el archivo go.mod
:
module checkpoint_container
go 1.23
require (
github.com/aws/aws-sdk-go v1.44.298
github.com/containerd/containerd v1.7.2
)
require (
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect
github.com/pkg/errors v0.9.1 // indirect
google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
google.golang.org/grpc v1.53.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
)
Ejecute el siguiente comando:
go mod tidy
Paso 2: Construir y Publicar la Imagen de Docker
Cree un Dockerfile
en el mismo directorio:
# Build stage
FROM golang:1.20 as builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o checkpoint_container
# Final stage
FROM amazonlinux:2
# Install necessary tools
RUN yum update -y && \
amazon-linux-extras install -y docker && \
yum install -y awscli containerd skopeo && \
yum clean all
# Copy the built Go binary
COPY --from=builder /app/checkpoint_container /usr/local/bin/checkpoint_container
EXPOSE 8080
ENTRYPOINT ["checkpoint_container"]
Este Dockerfile hace lo siguiente:
- Utiliza
golang:1.20
como etapa de compilación para compilar su aplicación Go. - Utiliza
amazonlinux:2
como imagen base final. - Instala AWS CLI, Docker (que incluye containerd), y skopeo usando yum y amazon-linux-extras.
- Copia el binario Go compilado de la etapa de compilación.
docker build -t <your-docker-repo>/checkpoint-container:v1 .
docker push <your-docker-repo>/checkpoint-container:v1
Reemplace <su-repositorio-docker>
con su repositorio Docker real.
Paso 3: Aplicar los recursos RBAC
Cree un archivo llamado rbac.yaml
:
apiVersion v1
kind ServiceAccount
metadata
name checkpoint-sa
namespace default
---
apiVersion rbac.authorization.k8s.io/v1
kind Role
metadata
name checkpoint-role
namespace default
rules
apiGroups""
resources"pods"
verbs"get" "list"
---
apiVersion rbac.authorization.k8s.io/v1
kind RoleBinding
metadata
name checkpoint-rolebinding
namespace default
subjects
kind ServiceAccount
name checkpoint-sa
namespace default
roleRef
kind Role
name checkpoint-role
apiGroup rbac.authorization.k8s.io
Aplique los recursos RBAC:
kubectl apply -f rbac.yaml
Paso 4: Crear un despliegue de Kubernetes
Crea un archivo llamado deployment.yaml
:
apiVersion apps/v1
kind Deployment
metadata
name main-app
namespace default
spec
replicas1
selector
matchLabels
app main-app
template
metadata
labels
app main-app
spec
serviceAccountName checkpoint-sa
containers
name main-app
image nginx latest # Replace with your main application image
name checkpoint-sidecar
image <your-docker-repo>/checkpoint-container v1
ports
containerPort8080
securityContext
privilegedtrue
volumeMounts
name containerd-socket
mountPath /run/containerd/containerd.sock
volumes
name containerd-socket
hostPath
path /run/containerd/containerd.sock
type Socket
Aplica el despliegue:
kubectl apply -f deployment.yaml
En deployment.yaml
, actualiza lo siguiente:
image <your-docker-repo>/checkpoint-container v1
Paso 5: Servicio de Kubernetes
Crea un archivo llamado service.yaml
:
apiVersion v1
kind Service
metadata
name checkpoint-service
namespace default
spec
selector
app main-app
ports
protocol TCP
port80
targetPort8080
Aplica el servicio:
kubectl apply -f service.yaml
Paso 6: Instalar el controlador de Ingress de Ngnix
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx
Paso 7: Crear un recurso de Ingress
Crea un archivo llamado ingress.yaml
:
apiVersion networking.k8s.io/v1
kind Ingress
metadata
name checkpoint-ingress
annotations
kubernetes.io/ingress.class nginx
nginx.ingress.kubernetes.io/ssl-redirect"false"
spec
rules
http
paths
path /checkpoint
pathType Prefix
backend
service
name checkpoint-service
port
number80
Aplica el Ingress:
kubectl apply -f ingress.yaml
Paso 8: Probar la API
kubectl get services ingress-ngnix-contoller -n ingress-ngnix
curl -X POST http://<EXTERNAL-IP>/checkpoint \
-H "Content-Type: application/json" \
-d '{"podId": "your-pod-id", "ecrRepo": "your-ecr-repo", "awsRegion": "your-aws-region"}'
Reemplaza <EXTERNAL-IP>
con la IP externa real.
Consideraciones adicionales
- Seguridad.
- Implementar HTTPS configurando certificados TLS
- Agregar autenticación a la API
- Monitoreo. Configurar el registro y monitoreo para la API y el proceso de punto de control.
- Administración de recursos. Configurar solicitudes y límites de recursos para el contenedor auxiliar.
- Manejo de errores. Implementar un manejo de errores sólido en la aplicación Go.
- Pruebas. Probar minuciosamente la configuración en un entorno no productivo antes de implementarla en producción.
- Documentación. Mantener una documentación clara sobre cómo utilizar la API de punto de control.
Conclusión
Esta configuración despliega el contenedor de checkpoint como un sidecar en Kubernetes y expone su funcionalidad a través de una API accesible desde fuera del clúster. Proporciona una solución flexible para gestionar checkpoints de contenedores en un entorno de Kubernetes.
Específico de AWS/EKS
Paso 7: Instalar el Controlador del Balanceador de Carga de AWS
En lugar de utilizar el Controlador de Ingress de Nginx, utilizaremos el Controlador del Balanceador de Carga de AWS. Este controlador creará y gestionará ALBs para nuestros recursos de Ingress.
1. Agregar el repositorio del gráfico de EKS a Helm:
helm repo add eks https://aws.github.io/eks-charts
2. Instalar el Controlador del Balanceador de Carga de AWS:
helm install aws-load-balancer-controller eks/aws-load-balancer-controller \
-n kube-system \
--set clusterName=<your-cluster-name> \
--set serviceAccount.create=false \
--set serviceAccount.name=aws-load-balancer-controller
Sustituir <nombre-de-tu-clúster>
con el nombre de tu clúster de EKS.
Nota: Asegúrate de tener configurados los permisos IAM necesarios para el Controlador del Balanceador de Carga de AWS. Puedes encontrar la política IAM detallada en la documentación de AWS.
Paso 8: Crear Recurso de Ingress
Crear un archivo llamado ingress.yaml
:
apiVersion networking.k8s.io/v1
kind Ingress
metadata
name checkpoint-ingress
annotations
kubernetes.io/ingress.class alb
alb.ingress.kubernetes.io/scheme internet-facing
alb.ingress.kubernetes.io/target-type ip
spec
rules
http
paths
path /checkpoint
pathType Prefix
backend
service
name checkpoint-service
port
number80
Aplicar el Ingress:
kubectl apply -f ingress.yaml
Paso 9: Probar la API
1. Obtener el nombre DNS del ALB:
kubectl get ingress checkpoint-ingress
Buscar el campo DIRECCIÓN, que será el nombre DNS del ALB.
2. Enviar una solicitud de prueba:
curl -X POST http://<ALB-DNS-NAME>/checkpoint \
-H "Content-Type: application/json" \
-d '{"podId": "your-pod-id", "ecrRepo": "your-ecr-repo", "awsRegion": "your-aws-region"}'
Sustituir <NOMBRE-DNS-ALB>
con el nombre DNS real de tu ALB del paso 1.
Consideraciones adicionales para AWS ALB
1. Grupos de seguridad. El ALB tendrá un grupo de seguridad creado automáticamente. Asegúrate de que permita tráfico entrante en el puerto 80 (y 443 si configuraste HTTPS).
2. SSL/TLS: Para habilitar HTTPS, puedes agregar las siguientes anotaciones a tu Ingress:
alb.ingress.kubernetes.io/listen-ports'[{"HTTP": 80}, {"HTTPS":443}]'
alb.ingress.kubernetes.io/certificate-arn arn aws acm region account-id certificate/certificate-id
3. Registros de acceso. Habilita los registros de acceso para tu ALB agregando lo siguiente:
alb.ingress.kubernetes.io/load-balancer-attributes access_logs.s3.enabled=true,access_logs.s3.bucket=your-log-bucket,access_logs.s3.prefix=your-log-prefix
4. Integración de WAF. Si deseas utilizar AWS WAF con tu ALB, puedes agregar:
alb.ingress.kubernetes.io/waf-acl-id your-waf-web-acl-id
5. Autenticación. Puedes configurar la autenticación utilizando Amazon Cognito u OIDC mediante las anotaciones apropiadas del controlador de Ingress de ALB.
Estos cambios configurarán tu Ingress utilizando un Balanceador de Carga de Aplicaciones de AWS en lugar de Nginx. El Controlador de Ingress de ALB provisionará y configurará automáticamente el ALB en función de tu recurso de Ingress.
Conclusión
Recuerda asegurarte de que tu clúster de EKS tenga los permisos IAM necesarios para crear y gestionar ALBs. Esto generalmente implica crear una política IAM y una cuenta de servicio con los permisos adecuados.
Esta configuración ahora utilizará la solución de equilibrio de carga nativa de AWS, que se integra bien con otros servicios de AWS y puede ser más rentable en un entorno de AWS.
Source:
https://dzone.com/articles/container-checkpointing-kubernetes-api