문제 설명
도전 과제
Kubernetes에서 컨테이너화된 애플리케이션을 운영하는 조직은 종종 다음을 위해 실행 중인 컨테이너의 상태를 캡처하고 보존해야 합니다:
- 재해 복구
- 애플리케이션 마이그레이션
- 디버깅/문제 해결
- 상태 보존
- 환경 재현
그러나 다음을 수행할 수 있는 간단하고 자동화된 방법은 없습니다:
- 요청 시 컨테이너 체크포인트 생성
- 이 체크포인트를 표준화된 형식으로 저장
- 클러스터 간에 쉽게 접근 가능하게 만들기
- 표준 인터페이스를 통해 체크포인트 트리거
현재의 한계
- 수동 체크포인트 생성은 클러스터에 직접 접근해야 함
- 체크포인트에 대한 표준화된 저장 형식이 없음
- 컨테이너 레지스트리와의 제한된 통합
- 자동화를 위한 프로그래밍 접근 부족
- containerd와 저장 시스템 간의 복잡한 조정
해결책
Kubernetes 사이드카 서비스가:
- REST API를 통해 체크포인트 기능을 노출
- 체크포인트를 OCI 호환 이미지로 자동 변환
- 이미지를 ECR에 저장하여 쉽게 배포
- 기존 Kubernetes 인프라와 통합
- 자동화를 위한 표준화된 인터페이스 제공
이것은 다음을 통해 핵심 문제를 해결합니다:
- 체크포인트 프로세스 자동화
- 체크포인트 저장소 표준화
- 체크포인트의 이동성 확보
- 프로그램적 접근 가능성 활성화
- 기존 워크플로와의 통합 간소화
대상 사용자:
- DevOps 팀
- 플랫폼 엔지니어
- 애플리케이션 개발자
- 사이트 신뢰성 엔지니어(SRE)
포렌식 컨테이너 체크포인팅은 사용자 공간에서 체크포인트/복원 (CRIU)를 기반으로 하며, 컨테이너가 체크포인트가 생성되고 있다는 사실을 인식하지 못한 채 실행 중인 컨테이너의 상태 저장 복사본을 생성할 수 있도록 합니다. 컨테이너의 복사본은 원본 컨테이너가 인식하지 못하는 상태에서 샌드박스 환경에서 여러 번 분석하고 복원할 수 있습니다. 포렌식 컨테이너 체크포인팅은 Kubernetes v1.25에서 알파 기능으로 도입되었습니다.
이 문서에서는 API를 사용하여 컨테이너 체크포인트를 생성하는 데 사용할 수 있는 Golang 코드 배포 방법을 안내합니다.
코드는 포드 식별자를 가져오고, 입력으로부터 containerd에서 컨테이너 ID를 검색한 후, ctr
명령을 사용하여 containerd의 k8s.io
네임스페이스에서 특정 컨테이너의 체크포인트를 생성합니다:
- Kubernetes 클러스터
ctr commandline
도구를 설치하십시오. kubelet 또는 워커 노드에서 ctr 명령을 실행할 수 있는 경우; 그렇지 않은 경우 AMI를 설치하거나 조정하여 ctr을 포함하십시오.kubectl
이 클러스터와 통신하도록 구성됨- 로컬에 Docker가 설치되어 있어야 함
- 컨테이너 레지스트리에 액세스할 수 있어야 함(Docker Hub, ECR 등)
- Nginx Ingress Controller를 설치하기 위해 Helm이 필요함
단계 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를 사용함
- AWS CLI, Docker(포함하여 containerd), skopeo를 yum과 amazon-linux-extras를 사용하여 설치함
- 빌드 단계에서 컴파일된 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: 쿠버네티스 배포 생성
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: 쿠버네티스 서비스
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 Controller 설치
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx
단계 7: 인그레스 리소스 생성
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
인그레스를 적용합니다:
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"}'
<EXTERNAL-IP>
를 실제 외부 IP로 대체합니다.
추가 고려 사항
- 보안.
- TLS 인증서 설정을 통한 HTTPS 구현
- API에 인증 추가
- 모니터링. API 및 체크포인트 프로세스에 대한 로깅 및 모니터링 설정
- 리소스 관리. 사이드카 컨테이너에 대한 리소스 요청과 제한 구성
- 에러 처리. Go 애플리케이션에서 견고한 에러 처리 구현
- 테스트. 프로덕션 환경에 배포하기 전에 비 프로덕션 환경에서 설정을 철저히 테스트
- 문서화. 체크포인트 API 사용 방법에 대한 명확한 문서 유지
결론
이 설정은 체크포인트 컨테이너를 쿠버네티스의 사이드카로 배포하고 클러스터 외부에서 접근 가능한 API를 통해 그 기능을 노출합니다. 이는 쿠버네티스 환경에서 컨테이너 체크포인트를 관리하기 위한 유연한 솔루션을 제공합니다.
AWS/EKS 특정
7단계: AWS 로드 밸런서 컨트롤러 설치
우리는 Nginx Ingress Controller 대신 AWS 로드 밸런서 컨트롤러를 사용할 것입니다. 이 컨트롤러는 우리의 Ingress 리소스를 위한 ALB를 생성하고 관리합니다.
1. EKS 차트 저장소를 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
<your-cluster-name>
을 EKS 클러스터 이름으로 대체합니다.
참고: AWS 로드 밸런서 컨트롤러에 필요한 IAM 권한이 설정되어 있는지 확인하세요. 자세한 IAM 정책은 AWS 문서에서 확인할 수 있습니다.
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"}'
<ALB-DNS-NAME>
을 1단계에서 가져온 ALB의 실제 DNS 이름으로 대체합니다.
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. 인증. 적절한 ALB Ingress Controller 주석을 사용하여 Amazon Cognito 또는 OIDC를 사용하여 인증을 설정할 수 있습니다.
이러한 변경 사항은 Nginx 대신 AWS 애플리케이션 로드 밸런서를 사용하여 Ingress를 설정합니다. ALB Ingress Controller는 Ingress 리소스를 기반으로 ALB를 자동으로 프로비저닝하고 구성합니다.
결론
EKS 클러스터가 ALB를 생성하고 관리할 수 있는 필요한 IAM 권한을 보장해야 합니다. 일반적으로 적절한 권한을 가진 IAM 정책과 서비스 계정을 생성하는 것이 포함됩니다.
이 설정은 이제 AWS의 네이티브 로드 밸런싱 솔루션을 사용하며 다른 AWS 서비스와 통합이 잘되어 AWS 환경에서 더 효율적 일 수 있습니다.
Source:
https://dzone.com/articles/container-checkpointing-kubernetes-api