问题陈述
挑战
在 Kubernetes 中运行容器化应用程序的组织通常需要捕获和保留运行容器状态,以用于:
- 灾难恢复
- 应用程序迁移
- 调试/故障排除
- 状态保留
- 环境再现
然而,目前没有直接、自动化的方法来:
- 根据需要创建容器检查点
- 将这些检查点存储在标准化格式中
- 使其在集群间轻松访问
- 通过标准接口触发检查点
目前的限制
- 手动创建检查点需要直接访问集群
- 检查点没有标准化存储格式
- 与容器注册表集成有限
- 缺乏用于自动化的程序化访问
- 在 containerd 和存储系统之间复杂协调
解决方案
一个 Kubernetes 旁路服务,它:
- 通过 REST API 公开检查点功能
- 自动将检查点转换为符合 OCI 标准的镜像
- 将镜像存储在 ECR 中以便分发
- 与现有 Kubernetes 基础设施集成
- 为自动化提供标准化接口
通过:
- 自动化检查点过程解决了核心问题
- 标准化检查点存储
- 使检查点可移植
- 启用编程访问
- 简化与现有工作流程集成
目标用户:
- DevOps团队
- 平台工程师
- 应用程序开发人员
- 站点可靠性工程师(SREs)
取证容器检查点基于用户态检查点/恢复(CRIU),允许在不让容器知道正在被检查点的情况下创建运行容器的有状态副本。容器的副本可以在沙盒环境中进行多次分析和恢复,原始容器并不知晓。取证容器检查点功能在Kubernetes v1.25中作为α特性引入。
本文将指导您如何部署 Golang 代码,用于使用 API 进行容器检查点。
该代码接受一个 pod 标识符,从 containerd 检索容器 ID 作为输入,然后使用 ctr
命令在 containerd 的 k8s.io
命名空间中检查点特定容器:
- Kubernetes 集群
- 安装
ctr命令行
工具。如果您能在kubelet或工作节点上运行ctr命令; 如果不能,请安装或调整AMI以包含ctr。 kubectl
配置用于与您的集群通信- 本地安装Docker
- 访问容器注册表(例如Docker Hub,ECR)
- Helm(用于安装Nginx Ingress Controller)
步骤0:使用GO创建容器检查点的代码
创建名为checkpoint_container.go
的文件,并包含以下内容:
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
}
步骤1:初始化go模块
go mod init checkpoint_container
修改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
)
运行以下命令:
go mod tidy
步骤2:构建和发布Docker镜像
在相同目录中创建一个Dockerfile
:
# 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"]
此Dockerfile执行以下操作:
- 使用
golang:1.20
作为构建阶段来编译您的Go应用程序。 - 使用
amazonlinux:2
作为最终基础镜像。 - 使用yum和amazon-linux-extras安装AWS CLI,Docker(包括containerd)和skopeo。
- 从构建阶段复制编译的Go二进制文件。
docker build -t <your-docker-repo>/checkpoint-container:v1 .
docker push <your-docker-repo>/checkpoint-container:v1
将<your-docker-repo>
替换为您实际的Docker存储库。
步骤3:应用RBAC资源
创建名为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
应用RBAC资源:
kubectl apply -f rbac.yaml
第4步:创建Kubernetes部署
创建名为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
应用部署:
kubectl apply -f deployment.yaml
在deployment.yaml
中更新以下内容:
image <your-docker-repo>/checkpoint-container v1
第5步:Kubernetes服务
创建名为service.yaml
的文件:
apiVersion v1
kind Service
metadata
name checkpoint-service
namespace default
spec
selector
app main-app
ports
protocol TCP
port80
targetPort8080
应用服务:
kubectl apply -f service.yaml
第6步:安装Ngnix Ingress控制器
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx
第7步:创建Ingress资源
创建名为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
应用Ingress:
kubectl apply -f ingress.yaml
第8步:测试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"}'
用实际外部IP替换<EXTERNAL-IP>
:
其他考虑
- 安全。
- 通过设置TLS证书实现HTTPS
- 为API添加身份验证
- 监控。为API和检查点进程设置日志记录和监控。
- 资源管理。为辅助容器配置资源请求和限制。
- 错误处理。在Go应用程序中实现健壮的错误处理。
- 测试。在非生产环境中彻底测试设置,然后再部署到生产环境。
- 文档。保持关于如何使用检查点API的清晰文档。
结束
这个设置在Kubernetes中部署检查点容器作为一个旁路容器,并通过可以从集群外部访问的API暴露其功能。它为在Kubernetes环境中管理容器检查点提供了灵活的解决方案。
AWS/EKS特定
第7步:安装AWS负载均衡器控制器
我们将使用AWS负载均衡器控制器,而不是使用Nginx Ingress控制器。这个控制器将为我们的Ingress资源创建和管理ALB。
1. 将EKS chart仓库添加到Helm中:
helm repo add eks https://aws.github.io/eks-charts
2. 安装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
用你的EKS集群名称替换<your-cluster-name>
。
注意:确保你已经为AWS负载均衡器控制器设置了必要的IAM权限。你可以在AWS文档中找到详细的IAM策略。
第8步:创建Ingress资源
创建一个名为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
应用Ingress:
kubectl apply -f ingress.yaml
第9步:测试API
1. 获取ALB的DNS名称:
kubectl get ingress checkpoint-ingress
查找ADDRESS字段,这将是ALB的DNS名称。
2. 发送一个测试请求:
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"}'
用第1步中的ALB的实际DNS名称替换<ALB-DNS-NAME>
。
AWS ALB的额外考虑事项
1. 安全组。ALB 将自动创建一个安全组。确保它允许端口 80 的入站流量(如果设置了 HTTPS,则还需允许端口 443 的流量)。
2. SSL/TLS:要启用 HTTPS,请向您的 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. 访问日志。通过添加以下内容启用 ALB 的访问日志:
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. WAF 集成。如果要在 ALB 中使用 AWS WAF,可以添加:
alb.ingress.kubernetes.io/waf-acl-id your-waf-web-acl-id
5. 身份验证。您可以使用 Amazon Cognito 或 OIDC 设置身份验证,通过使用相应的 ALB Ingress Controller 注释。
这些更改将使用 AWS 应用负载均衡器来设置您的 Ingress,而不是使用 Nginx。ALB Ingress Controller 将根据您的 Ingress 资源自动配置和配置 ALB。
结论
请确保您的 EKS 集群具有创建和管理 ALB 所需的 IAM 权限。通常需要创建一个 IAM 策略和一个带有适当权限的服务账户。
现在,这个设置将使用 AWS 的原生负载均衡解决方案,与其他 AWS 服务集成良好,并且在 AWS 环境中可能更具成本效益。
Source:
https://dzone.com/articles/container-checkpointing-kubernetes-api