Énoncé du problème
Défi
Les organisations qui exécutent des applications conteneurisées dans Kubernetes ont souvent besoin de capturer et de préserver l’état des conteneurs en cours d’exécution pour :
- La reprise après sinistre
- La migration d’application
- Le débogage/le dépannage
- La préservation de l’état
- La reproduction de l’environnement
Cependant, il n’existe pas de moyen direct et automatisé pour :
- Créer des points de contrôle de conteneurs à la demande
- Stocker ces points de contrôle dans un format normalisé
- Les rendre facilement accessibles à travers les clusters
- Déclencher la création de points de contrôle via une interface standard
Limitations actuelles
- La création manuelle de points de contrôle nécessite un accès direct au cluster
- Aucun format de stockage normalisé pour les points de contrôle
- Intégration limitée avec les registres de conteneurs
- Manque d’accès programmatique pour l’automatisation
- Coordination complexe entre containerd et les systèmes de stockage
Solution
Un service auxiliaire Kubernetes qui :
- Expose la fonctionnalité de points de contrôle via une API REST
- Convertit automatiquement les points de contrôle en images conformes à OCI
- Stocke les images dans ECR pour une distribution facile
- S’intègre à l’infrastructure Kubernetes existante
- Fournit une interface normalisée pour l’automatisation
Cela résout les problèmes essentiels en :
- Automatisant le processus de point de contrôle
- Normalisation du stockage des checkpoints
- Rendre les checkpoints portables
- Activation de l’accès programmatique
- Simplification de l’intégration avec les flux de travail existants
Utilisateurs cibles :
- Équipes DevOps
- Ingénieurs de plateforme
- Développeurs d’applications
- Ingénieurs de fiabilité des sites (SRE)
La prise de checkpoints des conteneurs forensiques est basée sur Checkpoint/Restore In Userspace (CRIU) et permet de créer des copies étatiques d’un conteneur en cours d’exécution sans que le conteneur ne sache qu’il est en cours de checkpoint. La copie du conteneur peut être analysée et restaurée plusieurs fois dans un environnement sandbox sans que le conteneur d’origine en soit conscient. La prise de checkpoints des conteneurs forensiques a été introduite en tant que fonctionnalité alpha dans Kubernetes v1.25.
Cet article vous guidera sur la manière de déployer du code Golang qui peut être utilisé pour prendre un checkpoint de conteneur en utilisant une API.
Le code prend un identifiant de pod, récupère l’ID du conteneur à partir de containerd en tant qu’entrée, puis utilise la commande ctr
pour prendre le checkpoint spécifique du conteneur dans l’espace de noms k8s.io
de containerd :
- Cluster Kubernetes
- Installez l’outil en ligne de commande
ctr
. Si vous pouvez exécuter des commandes ctr sur le kubelet ou le nœud worker; sinon, installez ou ajustez l’AMI pour contenir le ctr. kubectl
configuré pour communiquer avec votre cluster- Docker installé localement
- Accès à un registre de conteneurs (par exemple, Docker Hub, ECR)
- Helm (pour installer le contrôleur d’ingress Nginx)
Étape 0 : Code pour créer un point de contrôle de conteneur en utilisant GO
Créez un fichier nommé checkpoint_container.go
avec le contenu suivant :
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
}
Étape 1 : Initialiser le module Go
go mod init checkpoint_container
Modifiez le fichier 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
)
Exécutez la commande suivante :
go mod tidy
Étape 2 : Construire et publier l’image Docker
Créez un Dockerfile
dans le même répertoire :
# 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"]
Ce Dockerfile fait ce qui suit :
- Utilise
golang:1.20
comme étape de construction pour compiler votre application Go. - Utilise
amazonlinux:2
comme image de base finale. - Installe l’AWS CLI, Docker (qui inclut containerd) et skopeo en utilisant yum et amazon-linux-extras.
- Copie le binaire Go compilé depuis l’étape de construction.
docker build -t <your-docker-repo>/checkpoint-container:v1 .
docker push <your-docker-repo>/checkpoint-container:v1
Remplacez <votre-docker-repo>
par votre dépôt Docker réel.
Étape 3 : Appliquer les ressources RBAC
Créez un fichier nommé 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
Appliquez les ressources RBAC :
kubectl apply -f rbac.yaml
Étape 4 : Créer un déploiement Kubernetes
Créez un fichier nommé 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
Appliquez le déploiement:
kubectl apply -f deployment.yaml
Dans deployment.yaml
, mettez à jour ce qui suit:
image <your-docker-repo>/checkpoint-container v1
Étape 5 : Service Kubernetes
Créez un fichier nommé service.yaml
:
apiVersion v1
kind Service
metadata
name checkpoint-service
namespace default
spec
selector
app main-app
ports
protocol TCP
port80
targetPort8080
Appliquez le service:
kubectl apply -f service.yaml
Étape 6 : Installer le contrôleur Ngnix Ingress
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx
Étape 7 : Créer la ressource Ingress
Créez un fichier nommé 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
Appliquez l’Ingress:
kubectl apply -f ingress.yaml
Étape 8 : Tester l’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"}'
Remplacez <EXTERNAL-IP>
par l’adresse IP externe réelle.
Considérations supplémentaires
- Sécurité.
- Mettre en place HTTPS en configurant des certificats TLS
- Ajouter une authentification à l’API
- Surveillance. Mettre en place le suivi des journaux et de la surveillance pour l’API et le processus de point de contrôle.
- Gestion des ressources. Configurer les demandes de ressources et les limites pour le conteneur auxiliaire.
- Gestion des erreurs. Mettre en place une gestion d’erreurs robuste dans l’application Go.
- Tests. Tester rigoureusement la configuration dans un environnement non productif avant de la déployer en production.
- Documentation. Maintenir une documentation claire sur l’utilisation de l’API de point de contrôle.
Conclusion
Cette configuration déploie le conteneur de checkpoint en tant que sidecar dans Kubernetes et expose sa fonctionnalité via une API accessible depuis l’extérieur du cluster. Cela fournit une solution flexible pour gérer les checkpoints de conteneurs dans un environnement Kubernetes.
Spécifique à AWS/EKS
Étape 7 : Installer le contrôleur de répartiteur de charge AWS
Au lieu d’utiliser le contrôleur d’entrée Nginx, nous utiliserons le contrôleur de répartiteur de charge AWS. Ce contrôleur créera et gérera des ALB pour nos ressources d’entrée.
1. Ajouter le référentiel de chart EKS à Helm :
helm repo add eks https://aws.github.io/eks-charts
2. Installer le contrôleur de répartiteur de charge 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
Remplacez <votre-nom-de-cluster>
par le nom de votre cluster EKS.
Remarque : Assurez-vous d’avoir les autorisations IAM nécessaires configurées pour le contrôleur de répartiteur de charge AWS. Vous pouvez trouver la politique IAM détaillée dans la documentation AWS.
Étape 8 : Créer la ressource Ingress
Créez un fichier nommé 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
Appliquez l’Ingress :
kubectl apply -f ingress.yaml
Étape 9 : Tester l’API
1. Obtenez le nom DNS de l’ALB :
kubectl get ingress checkpoint-ingress
Recherchez le champ ADRESSE, qui sera le nom DNS de l’ALB.
2. Envoyez une requête de test :
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"}'
Remplacez <NOM-DNS-ALB>
par le nom DNS réel de votre ALB de l’étape 1.
Considérations supplémentaires pour AWS ALB
1. Groupes de sécurité. L’ALB aura un groupe de sécurité créé automatiquement. Assurez-vous qu’il autorise le trafic entrant sur le port 80 (et 443 si vous configurez HTTPS).
2. SSL/TLS: Pour activer HTTPS, vous pouvez ajouter les annotations suivantes à votre 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. Journaux d’accès. Activez les journaux d’accès pour votre ALB en ajoutant ceci:
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. Intégration WAF. Si vous souhaitez utiliser AWS WAF avec votre ALB, vous pouvez ajouter ceci:
alb.ingress.kubernetes.io/waf-acl-id your-waf-web-acl-id
5. Authentification. Vous pouvez configurer l’authentification en utilisant Amazon Cognito ou OIDC en utilisant les annotations appropriées du contrôleur ALB Ingress.
Ces changements configureront votre Ingress en utilisant un répartiteur de charge d’application AWS au lieu de Nginx. Le contrôleur ALB Ingress provisionnera et configurera automatiquement l’ALB en fonction de votre ressource Ingress.
Conclusion
N’oubliez pas de vous assurer que votre cluster EKS dispose des autorisations IAM nécessaires pour créer et gérer les ALB. Cela implique généralement de créer une stratégie IAM et un compte de service avec les autorisations appropriées.
Cette configuration utilisera désormais la solution de répartition de charges native d’AWS, qui s’intègre bien avec d’autres services AWS et peut être plus rentable dans un environnement AWS.
Source:
https://dzone.com/articles/container-checkpointing-kubernetes-api