問題の声明
チャレンジ
Kubernetesでコンテナ化されたアプリケーションを実行している組織は、次のために実行中のコンテナの状態をキャプチャおよび保存する必要があります:
- 災害復旧
- アプリケーションの移行
- デバッグ/トラブルシューティング
- 状態の保存
- 環境の再現
ただし、次のことに対して直接的で自動化された方法がありません:
- 必要に応じてコンテナのチェックポイントを作成する
- これらのチェックポイントを標準化された形式で保存する
- クラスタ全体で簡単にアクセス可能にする
- 標準インターフェースを介してチェックポイントをトリガーする
現在の制限事項
- 手動のチェックポイント作成には直接クラスタアクセスが必要
- チェックポイントの標準化された保存形式がない
- コンテナレジストリとの統合が限られている
- 自動化のためのプログラム的アクセスが不足している
- containerdとストレージシステムの複雑な調整が必要
ソリューション
次を行うKubernetesサイドカーサービス
- REST APIを介してチェックポイント機能を公開
- 自動的にOCI準拠のイメージにチェックポイントを変換
- ECRにイメージを保存して簡単に配布
- 既存のKubernetesインフラストラクチャと統合
- 自動化のための標準化されたインターフェースを提供
これにより次のコアの問題が解決されます:
- チェックポイントプロセスの自動化
- チェックポイントストレージの標準化
- チェックポイントをポータブルにする
- プログラムアクセスを可能にする
- 既存のワークフローとの統合を簡素化する
対象ユーザー:
- DevOps チーム
- プラットフォームエンジニア
- アプリケーション開発者
- サイト信頼性エンジニア(SRE)
フォレンジックコンテナのチェックポイントは、実行中のコンテナの状態を知らせずに状態のコピーを作成することを可能にする、Checkpoint/Restore In Userspace(CRIU)に基づいており、元のコンテナがそのことに気づかないまま、コンテナのコピーをサンドボックス環境で複数回分析および復元できます。フォレンジックコンテナのチェックポイントは、Kubernetes v1.25 でアルファ機能として導入されました。
本記事では、API を使用してコンテナのチェックポイントを取るために使用できるGolang コードを展開する方法について説明します。
このコードは、ポッド識別子を取得し、containerd からコンテナ ID を入力として使用し、その後、k8s.io
ネームスペースの特定のコンテナを ctr
コマンドを使用して containerd 内でチェックポイントします。
- Kubernetes クラスタ
ctrコマンドライン
ツールをインストールしてください。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は以下のことを行います:
- Goアプリケーションをコンパイルするためのビルドステージとして
golang:1.20
を使用します。 - 最終的なベースイメージとして
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: Kubernetes Deploymentの作成
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
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リソースの作成
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"}'
<EXTERNAL-IP>
を実際の外部IPアドレスに置き換えてください。
追加の考慮事項
- セキュリティ。
- TLS証明書の設定によるHTTPSの実装
- APIに認証を追加
- モニタリング。APIとチェックポイントプロセスのためにログ記録とモニタリングをセットアップしてください。
- リソース管理。サイドカーコンテナのリソース要求と制限を構成してください。
- エラー処理。Goアプリケーションで堅牢なエラー処理を実装してください。
- テスト。本番環境にデプロイする前に、非本番環境でセットアップを徹底的にテストしてください。
- ドキュメンテーション。チェックポイントAPIの使用方法について明確なドキュメントを維持してください。
結論
このセットアップでは、チェックポイントコンテナをKubernetesのサイドカーとしてデプロイし、クラスター外部からアクセス可能なAPIを介してその機能を公開します。これは、Kubernetes環境でコンテナのチェックポイントを管理する柔軟なソリューションを提供します。
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
ALBのDNS名であるADDRESSフィールドを探します。
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>
を実際の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