AWS Controllers for Kubernetes (ACK): để Helm chart của ứng dụng tự sở hữu tài nguyên AWS
ACK giúp Helm chart của mỗi ứng dụng tự sở hữu bucket, queue, database — và kro đưa những giá trị như DATABASE_URL vào Deployment một cách tự động, không copy-paste.

Đây là câu chuyện mà team platform nào cũng từng gặp. Một developer cần một S3 bucket và một IAM role cho service mới. Ứng dụng nằm trong Helm chart, deploy bằng Argo CD. Bucket thì nằm trong repo Terraform của một team khác. Thế là phải mở ticket, tạo pull request thứ hai, chạy terraform apply, rồi ai đó copy tay tên bucket và role ARN vào values-prod.yaml. Sáu tháng sau, không ai nhớ bucket nào thuộc service nào, và xoá service xong thì hạ tầng mồ côi vẫn nằm đó.
AWS Controllers for Kubernetes (ACK) lấp khoảng trống này. ACK cho phép khai báo tài nguyên AWS dưới dạng object Kubernetes, nên Helm chart của mỗi ứng dụng có thể tự sở hữu tài nguyên AWS mà ứng dụng đó cần. Chúng đi cùng chart, cùng pull request và cùng lần sync của Argo CD với đoạn code dùng chúng. Cấu hình cũng tự kết nối: tạo database trong chart, và DATABASE_URL tự xuất hiện trong biến môi trường của Deployment mà không ai phải copy endpoint. Thêm kro ở tầng trên, team platform có thể biến các tài nguyên thô đó thành một API nhỏ gọn, an toàn cho developer.
ACK là gì
ACK là dự án mã nguồn mở của AWS, gồm các Kubernetes controller, mỗi controller cho một dịch vụ AWS (s3-controller, iam-controller, rds-controller, eks-controller…), hỗ trợ hơn 50 dịch vụ. Mỗi controller được sinh tự động từ API model của AWS, nên custom resource của nó gần như khớp 1-1 với API thật.
status.apiVersion: s3.services.k8s.aws/v1alpha1
kind: Bucket
metadata:
name: orders-api-data
spec:
name: orders-prod-orders-api-data
versioning:
status: Enabled
publicAccessBlock:
blockPublicACLs: true
blockPublicPolicy: true
ignorePublicACLs: true
restrictPublicBuckets: true
Sau kubectl apply, controller tạo bucket và điền status:
status:
ackResourceMetadata:
arn: arn:aws:s3:::orders-prod-orders-api-data
ownerAccountID: "111122223333"
region: eu-west-1
conditions:
- type: ACK.ResourceSynced
status: "True"
Ba đặc điểm quan trọng khi dùng thực tế:
- Reconcile liên tục. Đây không phải
applymột lần là xong. Mặc định mỗi controller kiểm tra lại tài nguyên mỗi 10 giờ và sửa drift, ví dụ ai đó tắt versioning trên console. Chu kỳ này chỉnh được cho cả controller (reconcile.defaultResyncPeriod) hoặc cho từng loại resource (reconcile.resourceResyncPeriods) trong Helm values. - Status chính là API. ARN, endpoint, ID đều nằm trong
.status, để công cụ khác đọc được. Các conditionACK.ResourceSynced,ACK.Recoverable,ACK.Terminalcho biết tài nguyên có khoẻ hay không. - Dùng được RBAC và audit của Kubernetes. Ai được tạo
Buckettrong namespace nào giờ là chuyện của RBAC, và mọi thay đổi đều có trong audit log của API server.
Cài đặt
Controller được phát hành dưới dạng OCI Helm chart trên Amazon ECR Public:
export SERVICE=s3
export RELEASE_VERSION=$(curl -sL https://api.github.com/repos/aws-controllers-k8s/${SERVICE}-controller/releases/latest | jq -r '.tag_name | ltrimstr("v")')
helm install --create-namespace -n ack-system ack-$SERVICE-controller \
oci://public.ecr.aws/aws-controllers-k8s/$SERVICE-chart \
--version=$RELEASE_VERSION --set=aws.region=eu-west-1
Mỗi controller cần IAM permission riêng. Hãy cấp role cho nó qua EKS Pod Identity hoặc IRSA, và chỉ giới hạn quyền trong đúng dịch vụ đó.
Nếu không muốn tự vận hành controller, EKS Capabilities cung cấp ACK, kro và Argo CD dưới dạng tính năng do AWS quản lý. Chúng chạy trong EKS chứ không chiếm worker node của bạn, và AWS lo việc nâng cấp và scale.
Pattern 1: Helm chart của ứng dụng sở hữu tài nguyên AWS của chính nó
Đây là ý tưởng cốt lõi của bài. Helm chart của một ứng dụng nên mô tả mọi thứ ứng dụng đó cần: image và các object Kubernetes, và cả tài nguyên AWS của riêng nó, như bucket nó ghi vào, queue nó đọc, database nó sở hữu, IAM role nó dùng để chạy. Phần nền móng dùng chung vẫn thuộc về team platform.
| Nằm trong chart của ứng dụng | Thuộc nền móng của platform |
|---|---|
| S3 bucket, SQS queue, SNS topic, DynamoDB table | VPC, subnet, NAT, Transit Gateway |
| Database RDS / Aurora riêng của service, ElastiCache | EKS cluster và node pool |
| IAM role + Pod Identity association của app | ACK controller và IAM role của chúng |
| CloudWatch alarm của app | IAM cấp tổ chức, SCP, DNS zone |
charts/orders-api/
├── Chart.yaml
├── values.yaml
├── values-dev.yaml
├── values-prod.yaml
└── templates/
├── _helpers.tpl
├── deployment.yaml
├── service.yaml
├── serviceaccount.yaml
└── aws/
├── bucket.yaml
├── queue.yaml
├── database.yaml
├── role.yaml
└── podidentityassociation.yaml
# templates/aws/bucket.yaml
{{- if .Values.aws.bucket.enabled }}
apiVersion: s3.services.k8s.aws/v1alpha1
kind: Bucket
metadata:
name: {{ include "orders-api.fullname" . }}-data
annotations:
services.k8s.aws/deletion-policy: {{ .Values.aws.deletionPolicy }}
spec:
name: {{ include "orders-api.bucketName" . }}
versioning:
status: Enabled
{{- end }}
# templates/aws/podidentityassociation.yaml
apiVersion: eks.services.k8s.aws/v1alpha1
kind: PodIdentityAssociation
metadata:
name: {{ include "orders-api.fullname" . }}
spec:
clusterName: {{ .Values.aws.clusterName }}
namespace: {{ .Release.Namespace }}
serviceAccount: {{ include "orders-api.serviceAccountName" . }}
roleRef:
from:
name: {{ include "orders-api.fullname" . }} # ACK Role trong cùng namespace
# values-prod.yaml
aws:
region: eu-west-1
accountId: "444444444444"
clusterName: prod-eu-1
deletionPolicy: retain # không bao giờ xoá dữ liệu prod theo release
bucket:
enabled: true
Lợi ích:
- Một pull request, một version. Thay đổi code, bucket mới và IAM permission được review và release cùng nhau, rollback cũng cùng nhau.
- Vòng đời đi theo ứng dụng. Môi trường preview được tạo bằng
helm installvà xoá bằnghelm uninstall, kèm luôn bucket và queue của nó (deletionPolicy: deleteở dev). Ở prod,retaingiữ lại dữ liệu kể cả khi release bị xoá. - Values theo môi trường có sẵn. Dev dùng
db.t4g.micro, không Multi-AZ. Prod dùng instance lớn hơn, có Multi-AZ, từ cùng một bộ template. - Ownership rõ ràng. Mỗi tài nguyên AWS thuộc về một Helm release trong một namespace. Chạy
kubectl get buckets,queues,dbinstances -n orders-prodlà biết service này sở hữu những gì. - Cấu hình tự kết nối, là phần ngay dưới đây.
Đưa output của AWS vào Deployment
Ứng dụng không quan tâm ARN. Nó cần giá trị trong biến môi trường: BUCKET_NAME, QUEUE_URL, DATABASE_URL. Các giá trị này chia làm hai loại:
| Giá trị ứng dụng cần | Ví dụ | Biết được lúc Helm render? | Cách đưa vào |
|---|---|---|---|
| Tên S3 bucket | orders-prod-orders-api-data |
✅ do bạn đặt | template |
| URL của SQS queue | https://sqs.eu-west-1.amazonaws.com/444444444444/orders-prod-events |
✅ region + account + tên | template |
| Tên DynamoDB table | orders-prod-orders |
✅ do bạn đặt | template |
| ARN của IAM role | arn:aws:iam::444444444444:role/orders-prod-orders-api |
✅ account + tên | template hoặc ACK roleRef |
| Endpoint RDS | orders-prod-orders-api.c9xyz….rds.amazonaws.com |
❌ AWS tự sinh | kro → ConfigMap |
| Endpoint ElastiCache | ….cache.amazonaws.com |
❌ AWS tự sinh | kro → ConfigMap |
Giá trị do bạn đặt: định nghĩa tên một lần, dùng hai nơi. Đặt tên trong _helpers.tpl rồi dùng cho cả resource ACK lẫn env của Deployment. Nhờ vậy resource và cấu hình không bao giờ lệch nhau:
# templates/_helpers.tpl
{{- define "orders-api.bucketName" -}}
{{ .Release.Namespace }}-{{ include "orders-api.fullname" . }}-data
{{- end }}
{{- define "orders-api.queueName" -}}
{{ .Release.Namespace }}-{{ include "orders-api.fullname" . }}-events
{{- end }}
# templates/deployment.yaml (env của container)
env:
- name: BUCKET_NAME
value: {{ include "orders-api.bucketName" . }}
- name: QUEUE_URL
value: https://sqs.{{ .Values.aws.region }}.amazonaws.com/{{ .Values.aws.accountId }}/{{ include "orders-api.queueName" . }}
Giá trị do AWS sinh ra cần một chút “keo” lúc chạy. Endpoint RDS chưa tồn tại cho tới khi AWS tạo xong instance, tức là rất lâu sau khi Helm render xong. ACK từng có resource FieldExport cho việc này nhưng đã bị deprecate. “Keo” được khuyến nghị là kro, được trình bày chi tiết ở phần sau. Ví dụ dưới đây cho thấy toàn bộ luồng.
Ví dụ: tạo database và có DATABASE_URL trong Deployment
Mục tiêu: developer thêm một database vào chart của ứng dụng, và pod nhận được DATABASE_URL dùng được ngay. Không ai phải copy endpoint, chạy script hay restart gì bằng tay.
DATABASE_URL khi pod khởi động.Bước 1, làm một lần, do team platform: tạo API PostgresDatabase bằng kro. API này tạo DBInstance của ACK, và khi RDS đã có endpoint thì tạo một ConfigMap chứa thông tin kết nối:
apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
name: postgresdatabase.kro.run
spec:
schema:
apiVersion: v1alpha1
kind: PostgresDatabase
spec:
dbName: string
username: string | default="app"
passwordSecretName: string
instanceClass: string | default="db.t4g.micro"
allocatedStorage: integer | default=20
engineVersion: string | default="16.10"
multiAZ: boolean | default=false
subnetGroupName: string
securityGroupIDs: "[]string"
status:
host: ${dbinstance.status.endpoint.address}
connectionConfigMap: ${connection.metadata.name}
resources:
- id: dbinstance
readyWhen:
- ${dbinstance.status.dbInstanceStatus == "available"}
- ${has(dbinstance.status.endpoint)}
template:
apiVersion: rds.services.k8s.aws/v1alpha1
kind: DBInstance
metadata:
name: ${schema.metadata.name}
annotations:
services.k8s.aws/deletion-policy: retain
spec:
dbInstanceIdentifier: ${schema.metadata.namespace}-${schema.metadata.name}
engine: postgres
engineVersion: ${schema.spec.engineVersion}
dbInstanceClass: ${schema.spec.instanceClass}
allocatedStorage: ${schema.spec.allocatedStorage}
multiAZ: ${schema.spec.multiAZ}
storageEncrypted: true
backupRetentionPeriod: 7
dbName: ${schema.spec.dbName}
masterUsername: ${schema.spec.username}
masterUserPassword:
namespace: ${schema.metadata.namespace}
name: ${schema.spec.passwordSecretName}
key: password
dbSubnetGroupName: ${schema.spec.subnetGroupName}
vpcSecurityGroupIDs: ${schema.spec.securityGroupIDs}
- id: connection
template:
apiVersion: v1
kind: ConfigMap
metadata:
name: ${schema.metadata.name}-connection
data:
DB_HOST: ${dbinstance.status.endpoint.address}
DB_PORT: "5432"
DB_NAME: ${schema.spec.dbName}
DB_USER: ${schema.spec.username}
Bước 2, trong chart của ứng dụng: khai báo database ngay cạnh Deployment dùng nó:
# templates/aws/database.yaml
apiVersion: kro.run/v1alpha1
kind: PostgresDatabase
metadata:
name: {{ include "orders-api.fullname" . }}-db
spec:
dbName: orders
passwordSecretName: {{ .Values.db.passwordSecretName }}
instanceClass: {{ .Values.db.instanceClass }}
multiAZ: {{ .Values.db.multiAZ }}
subnetGroupName: {{ .Values.db.subnetGroupName }}
securityGroupIDs: {{ toJson .Values.db.securityGroupIDs }}
# templates/deployment.yaml (container)
envFrom:
- configMapRef:
name: {{ include "orders-api.fullname" . }}-db-connection # do kro ghi
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Values.db.passwordSecretName }}
key: password
- name: DATABASE_URL
value: "postgres://$(DB_USER):$(DB_PASSWORD)@$(DB_HOST):$(DB_PORT)/$(DB_NAME)?sslmode=require"
# values-dev.yaml # values-prod.yaml
db: # db:
instanceClass: db.t4g.micro # instanceClass: db.r6g.large
multiAZ: false # multiAZ: true
passwordSecretName: orders-api-db-password
subnetGroupName: dev-private # subnetGroupName: prod-private
securityGroupIDs: [sg-0dev…] # securityGroupIDs: [sg-0prod…]
Cú pháp $(DB_USER) là tính năng có sẵn của Kubernetes: một biến môi trường có thể tham chiếu tới biến đã khai báo trước nó trong cùng container, kể cả các biến nạp bằng envFrom. URL do kubelet ghép, không cần script entrypoint hay init container.
Bước 3: mật khẩu. Không để mật khẩu trong Git hay values.yaml. Tạo Secret bằng External Secrets Operator (lấy từ AWS Secrets Manager) hoặc Sealed Secrets. Để thử nhanh:
kubectl create secret generic orders-api-db-password -n orders-prod \
--from-literal=password="$(openssl rand -hex 24)" # hex = an toàn khi đặt trong URL
Nếu muốn RDS tự sinh và tự xoay vòng mật khẩu, ACK cũng hỗ trợ manageMasterUserPassword: true. Khi đó mật khẩu nằm trong AWS Secrets Manager (status.masterUserSecret.secretARN), và bạn đồng bộ nó vào cluster bằng External Secrets Operator.
Chuyện gì xảy ra ở lần deploy đầu tiên:
- Argo CD sync chart.
PostgresDatabase, Deployment và Service được tạo cùng lúc. - Pod ở trạng thái
CreateContainerConfigErrorvì ConfigMap chưa có. Đây là điều bình thường, không phải lỗi cần sửa. - ACK tạo RDS instance. Sau vài phút, RDS báo
availablekèm endpoint. - kro ghi
orders-api-db-connection, kubelet thử lại, và pod khởi động vớiDATABASE_URLđầy đủ. Không cần restart, không có bước thủ công nào.
Vận hành về sau cũng đơn giản như vậy. Đổi instanceClass trong values-prod.yaml, merge pull request, và ACK nâng cấp instance tại chỗ. Endpoint giữ nguyên nên ứng dụng không bị ảnh hưởng.
Helm thuần bắt đầu thấy giới hạn ở đâu
Ví dụ trên đã dùng kro cho đúng một việc mà Helm không làm được. Nói chung, Helm render template rồi apply tất cả cùng lúc, và không biết các resource phụ thuộc vào nhau:
- Thứ tự tạo. Pod của Deployment có thể được tạo trước khi
PodIdentityAssociationtồn tại. Pod Identity webhook chỉ inject credentials lúc tạo pod, nên các pod đó chạy mà không có AWS credentials cho tới khi được restart. - Output lúc render. Mọi thứ AWS tự sinh ra (endpoint, ID) đều chưa biết lúc Helm render. Bạn cần “keo” lúc chạy như
PostgresDatabaseở trên. helm --waitkhông hiểu ACK. Nó không biếtACK.ResourceSynced=Truenghĩa là đã sẵn sàng.- Chuẩn chung bị copy-paste. Mỗi team có một bản
bucket.yaml,role.yamlhơi khác nhau. Mã hoá, tag, chặn public access lệch dần giữa các team.
Pattern 2: compose bằng kro
kro (Kube Resource Orchestrator) là dự án thuộc Kubernetes SIG, được AWS, Google và Microsoft cùng phát triển. kro cho phép team platform định nghĩa một API mới, ví dụ WebService, bằng một ResourceGraphDefinition (RGD): gồm schema và tập template tài nguyên. Sau đó kro sẽ:
- tạo một CRD cho kind mới,
- đọc các tham chiếu CEL
${...}giữa các template để dựng đồ thị phụ thuộc (DAG), - tạo tài nguyên theo đúng thứ tự phụ thuộc, chờ điều kiện
readyWhen, và truyền giá trị (ARN, tên, endpoint) từ resource này sang resource khác, - đưa các trường được chọn ra
statuscủa instance.
apiVersion: kro.run/v1alpha1
kind: ResourceGraphDefinition
metadata:
name: webservice.kro.run
spec:
schema:
apiVersion: v1alpha1
kind: WebService
spec:
image: string
replicas: integer | default=2
port: integer | default=8080
clusterName: string
status:
bucketARN: ${bucket.status.ackResourceMetadata.arn}
roleARN: ${role.status.ackResourceMetadata.arn}
availableReplicas: ${deployment.status.availableReplicas}
resources:
- id: bucket
readyWhen:
- ${has(bucket.status.ackResourceMetadata.arn)}
template:
apiVersion: s3.services.k8s.aws/v1alpha1
kind: Bucket
metadata:
name: ${schema.metadata.name}-data
annotations:
services.k8s.aws/deletion-policy: retain
spec:
name: ${schema.metadata.namespace}-${schema.metadata.name}-data
versioning:
status: Enabled
publicAccessBlock:
blockPublicACLs: true
blockPublicPolicy: true
ignorePublicACLs: true
restrictPublicBuckets: true
- id: role
template:
apiVersion: iam.services.k8s.aws/v1alpha1
kind: Role
metadata:
name: ${schema.metadata.name}
spec:
name: ${schema.metadata.namespace}-${schema.metadata.name}
assumeRolePolicyDocument: |
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "pods.eks.amazonaws.com" },
"Action": ["sts:AssumeRole", "sts:TagSession"]
}]
}
inlinePolicies:
bucket-access: |
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "${bucket.status.ackResourceMetadata.arn}/*"
}]
}
- id: serviceaccount
template:
apiVersion: v1
kind: ServiceAccount
metadata:
name: ${schema.metadata.name}
- id: podidentityassociation
template:
apiVersion: eks.services.k8s.aws/v1alpha1
kind: PodIdentityAssociation
metadata:
name: ${schema.metadata.name}
spec:
clusterName: ${schema.spec.clusterName}
namespace: ${schema.metadata.namespace}
serviceAccount: ${serviceaccount.metadata.name}
roleARN: ${role.status.ackResourceMetadata.arn}
- id: deployment
readyWhen:
- ${deployment.status.availableReplicas == schema.spec.replicas}
template:
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${schema.metadata.name}
spec:
replicas: ${schema.spec.replicas}
selector:
matchLabels:
app: ${schema.metadata.name}
template:
metadata:
labels:
app: ${schema.metadata.name}
annotations:
# tham chiếu tới association để biến nó thành dependency:
# pod chỉ được tạo khi Pod Identity đã sẵn sàng
example.com/pod-identity: ${podidentityassociation.status.ackResourceMetadata.arn}
spec:
serviceAccountName: ${serviceaccount.metadata.name}
containers:
- name: app
image: ${schema.spec.image}
ports:
- containerPort: ${schema.spec.port}
env:
- name: BUCKET_NAME
value: ${bucket.spec.name}
${...} và tự suy ra thứ tự. Không ai phải viết sync wave hay init container.Giờ developer chỉ cần viết đúng thế này:
apiVersion: kro.run/v1alpha1
kind: WebService
metadata:
name: orders-api
namespace: orders
spec:
image: 111122223333.dkr.ecr.eu-west-1.amazonaws.com/orders-api:1.4.2
replicas: 3
clusterName: prod-eu-1
kubectl get webservice orders-api -n orders -o jsonpath='{.status.bucketARN}'
# arn:aws:s3:::orders-orders-api-data
Những gì thay đổi:
- Thứ tự được giải quyết bằng dữ liệu, không phải bằng quy ước. Role chờ bucket ARN, association chờ role ARN, pod chờ association.
- Chuẩn chung được viết một lần. Mã hoá, chặn public access, deletion policy, quy tắc đặt tên, tag đều nằm trong RGD. Mọi
WebServiceđều tuân theo, và sửa RGD một lần là cả tổ chức được cập nhật. - API nhỏ hơn và an toàn hơn. Developer chỉ khai báo
imagevàreplicas, không bao giờ phải đụng vào IAM JSON. - Least privilege bằng RBAC. Developer chỉ cần quyền tạo object
WebService. kro tạoRole,Bucket… bên dưới bằng service account của chính nó, nên team ứng dụng không bao giờ có quyền tạo IAM role tuỳ ý. Có quyền đó chính là một đường leo thang đặc quyền.
Pattern 3: Helm + kro + ACK cùng nhau
kro không thay thế Helm, vì hai công cụ giải quyết hai bài toán khác nhau. Helm lo đóng gói: chart có version, values theo môi trường, và vòng đời release mà Argo CD hiểu được. kro lo việc compose lúc chạy: phụ thuộc và output. ACK là cầu nối tới AWS API.
Trên thực tế, chart của ứng dụng chỉ còn phần riêng của app cộng thêm một WebService:
# charts/orders-api/templates/webservice.yaml
apiVersion: kro.run/v1alpha1
kind: WebService
metadata:
name: {{ include "orders-api.fullname" . }}
spec:
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
replicas: {{ .Values.replicas }}
clusterName: {{ .Values.clusterName }}
| Terraform + Helm (tách rời) | Helm + ACK | Helm + kro + ACK | |
|---|---|---|---|
| Số pull request mỗi tính năng | 2+ (thường 2 team) | 1 | 1 |
| Đưa output hạ tầng vào app | Copy tay | Chỉ qua ACK ref | Mọi trường, qua CEL |
| Thứ tự tạo | Thủ công / pipeline | Không có (apply một lượt) | DAG tự động |
| Sửa drift | Lần terraform plan sau |
Liên tục (ACK) | Liên tục (ACK + kro) |
| Chuẩn của tổ chức | Module | Template copy-paste | Viết một lần trong RGD |
| Developer cần quyền IAM | Thường có | Có (tạo Role) |
Không (chỉ WebService) |
| Dọn dẹp khi xoá app | Destroy riêng | Theo release | Theo release |
Pattern 4: một control plane cho nhiều AWS account
Rất ít tổ chức chỉ dùng một AWS account. Phần lớn chạy AWS Organization với account riêng cho từng môi trường, team hay mảng kinh doanh. Cài ACK vào từng workload cluster nghĩa là N bản controller, N bộ quyền và N chỗ phải debug. Mô hình gọn hơn là hub-and-spoke: một EKS cluster làm control plane riêng, đặt trong account quản lý (platform account), chạy Argo CD, kro và các ACK controller, rồi từ đó quản lý tài nguyên AWS ở mọi account khác.
IAMRoleSelector quyết định ACK sẽ assume role nào cho mọi resource trong namespace đó.Các thành phần:
- ACK controller chạy với một danh tính duy nhất trong management account:
role/ack-controller, được cấp qua Pod Identity. - Mỗi account đích có một
role/ack-targettinack-controller, và chỉ có những quyền mà account đó cần. - Mỗi namespace trong control plane đại diện cho một đích: label cho biết account nào, annotation cho biết region nào.
- Một
IAMRoleSelectoránh xạ label đó tới role đích. Với mọi resource trong namespace, ACK assume role này và gọi AWS API ngay trong account đích.
Trước đây ACK làm việc này bằng CARM (ConfigMap
ack-role-account-mapcộng annotationservices.k8s.aws/owner-account-idtrên namespace). CARM đang bị deprecate. Hệ thống mới nên dùngIAMRoleSelector, và khi bật tính năng này thì CARM tự động bị tắt.
Bước 1: tạo target role ở mọi account
Mỗi account đích tin role của controller trong management account:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111111111111:role/ack-controller" },
"Action": "sts:AssumeRole"
}]
}
Chỉ gắn quyền cho những dịch vụ mà ACK quản lý ở account đó, và để prod chặt hơn dev. Triển khai role này bằng CloudFormation StackSets hoặc Terraform trên cả organizational unit, để account mới nào cũng tự có. Đây là một phần của lớp nền móng nhỏ nằm ngoài ACK.
Bước 2: cho controller quyền assume các role đó
Role của controller trong management account chỉ được assume đúng những role này, không hơn:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": [
"arn:aws:iam::222222222222:role/ack-target",
"arn:aws:iam::333333333333:role/ack-target",
"arn:aws:iam::444444444444:role/ack-target"
]
}]
}
Bước 3: bật feature gate trên mọi controller
for SERVICE in s3 iam rds eks sqs; do
helm upgrade --install -n ack-system ack-$SERVICE-controller \
oci://public.ecr.aws/aws-controllers-k8s/$SERVICE-chart \
--version "<version đã ghim cho controller này>" \
--set aws.region=eu-west-1 \
--set featureGates.IAMRoleSelector=true
done
Bước 4: mỗi đích một namespace và một selector
apiVersion: v1
kind: Namespace
metadata:
name: orders-prod
labels:
aws-account: prod
annotations:
services.k8s.aws/region: ap-southeast-1 # region mặc định cho resource ACK ở đây
s3.services.k8s.aws/deletion-policy: retain
---
apiVersion: services.k8s.aws/v1alpha1
kind: IAMRoleSelector
metadata:
name: account-prod
spec:
arn: arn:aws:iam::444444444444:role/ack-target
namespaceSelector:
names: [] # bắt buộc phải có, kể cả khi chọn theo label
labelSelector:
matchLabels:
aws-account: prod
Từ giờ, mọi resource ACK tạo trong orders-prod, kể cả do một WebService của kro tạo ra, đều nằm ở account 444444444444, region ap-southeast-1. Resource cũng cho biết nó đã dùng selector nào:
status:
conditions:
- type: ACK.IAMRoleSelected
status: "True"
message: "roleARN: arn:aws:iam::444444444444:role/ack-target, selectorName: account-prod, selectorResourceVersion: 89424719"
ACK.Recoverable và không được reconcile.Hai chi tiết dễ dính bẫy:
- Chồng lấn là xung đột, không phải ưu tiên. Selector cụ thể hơn không thắng selector rộng hơn. Ví dụ, một selector dùng
resourceTypeSelectorchoBucketvà một selector khác cho cả namespace đều khớp với bucket, và đó là xung đột. Dùng một khoá label duy nhất với đúng một giá trị cho mỗi namespace (aws-account=…) thì không thể chồng lấn ngay từ thiết kế. - Không khớp sẽ rơi về management account. Namespace bị quên gắn label sẽ âm thầm tạo resource ngay trong account của control plane. Hãy chặn bằng policy Kyverno hoặc Gatekeeper từ chối resource ACK trong namespace không có label
aws-account, và để role của chính controller gần như không có quyền gì.
Bước 5: để Argo CD nhân bản ra các môi trường
Khi đường ống đã xong, mỗi môi trường chỉ còn là một namespace. Một ApplicationSet của Argo CD render cùng một chart cho từng account:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: orders-infra
namespace: argocd
spec:
generators:
- list:
elements:
- env: dev
- env: staging
- env: prod
template:
metadata:
name: 'orders-infra-{{env}}'
spec:
project: default
source:
repoURL: https://github.com/example/platform.git
targetRevision: main
path: charts/orders-infra
helm:
valueFiles:
- 'values-{{env}}.yaml'
destination:
server: https://kubernetes.default.svc # chính cluster control plane
namespace: 'orders-{{env}}'
syncPolicy:
automated: {}
Trong mô hình hub-and-spoke, hãy tách hai đích deploy:
- Tài nguyên AWS (
Bucket,Role,PodIdentityAssociation, RDS…) vào namespace trên control plane. - Workload (
Deployment,Service) vào workload cluster ở account đích.
RGD kro cho mô hình này chính là WebService ở trên nhưng bỏ phần Deployment. PodIdentityAssociation.clusterName trỏ tới workload cluster trong cùng account, nên ACK EKS controller sẽ tạo association ở đó. Workload cluster vì thế rất gọn: chỉ chạy ứng dụng, không chạy controller hạ tầng.
Vận hành control plane
- Coi nó là tier 0. Nó tạo được tài nguyên ở mọi account. Đặt nó trong account riêng, dùng private API endpoint, chỉ cho người dùng quyền RBAC read-only, và để Argo CD là bên duy nhất được ghi.
- Sự cố được thiết kế để “nhàm chán”. Khi control plane sập, tài nguyên AWS hiện có vẫn chạy bình thường và workload cluster không bị ảnh hưởng. Chỉ có việc thay đổi và sửa drift tạm dừng cho tới khi nó hoạt động lại.
- Tách hub nếu cần cô lập. Nhiều team chạy một control plane cho non-prod và một cái riêng cho prod, để lỗi trong công cụ dev không bao giờ chạm được tới credentials của production.
- Audit ở account đích. CloudTrail trong từng account cho thấy lệnh
AssumeRoletừack-controller, tiếp theo là các API call thực hiện bằng session đó. - Thứ tự debug: kiểm tra condition
ACK.IAMRoleSelected, rồi trust policy của target role, rồi quyềnsts:AssumeRolecủa role controller, cuối cùng là log của controller.
Checklist cho production
- Bảo vệ tài nguyên có dữ liệu.
services.k8s.aws/deletion-policy: retaingiữ lại tài nguyên AWS khi object Kubernetes bị xoá. Mặc định làdelete. Có thể đặt cho từng resource, từng namespace, hoặc bằng cờ--deletion-policycủa controller. Kết hợp thêmhelm.sh/resource-policy: keepcho các object mà Helm không được xoá. - Adopt thay vì tạo lại. Với bucket hay database đã có sẵn, dùng annotation
services.k8s.aws/adoption-policy(adopthoặcadopt-or-create) cùngservices.k8s.aws/adoption-fields. Tính năng này cần bật feature gate ResourceAdoption. CRDAdoptedResourcekiểu cũ không còn được khuyến nghị. - Multi-account và multi-region. Dùng
IAMRoleSelector(Pattern 4) cho nhiều account và annotationservices.k8s.aws/regioncho nhiều region. Đừng bắt đầu hệ thống mới bằng CARM, vì nó đã bị deprecate. - Dạy Argo CD hiểu trạng thái health. Thêm custom health check để resource ACK báo
Progressingcho tới khiACK.ResourceSyncedlàTrue, và báoDegradedkhi gặpACK.Terminal. Nếu không, Argo sẽ báo “Healthy” trong khi AWS vẫn đang tạo tài nguyên. - Guardrail. Dùng RBAC để chặn namespace ứng dụng tạo trực tiếp object
iam.services.k8s.aws, và thêm policy Kyverno hoặc Gatekeeper để bắt buộc có tag và mã hoá. IAM là chỗ quan trọng nhất, vì tạo đượcRolenghĩa là tự cấp được quyền. - Bootstrap vẫn cần IaC. Cluster, VPC và IAM role của chính các controller phải có trước thì ACK mới chạy được. Giữ phần nền móng nhỏ này trong Terraform (hoặc trong một management cluster), còn mọi thứ ứng dụng cần thì để ACK và kro lo.
- Ghim version. API của kro vẫn là
v1alpha1, nên hãy ghim version và đọc release notes trước khi nâng cấp. Ghim cả version chart của ACK controller, hoặc dùng EKS Capabilities để AWS lo việc nâng cấp.
Khi nào ACK không phải lựa chọn đúng
- Tổ chức multi-cloud và muốn một lớp trừu tượng chung cho mọi cloud. Crossplane, hoặc Terraform kết hợp GitOps controller, có thể hợp hơn.
- Tài nguyên nền móng và dùng chung (VPC, transit gateway, IAM cấp tổ chức). Chúng ít thay đổi, sống lâu hơn bất kỳ ứng dụng nào, và nên nằm trong một pipeline riêng, chậm hơn.
- Bạn không dùng Kubernetes làm control plane. Toàn bộ giá trị của ACK đến từ việc dùng Kubernetes API làm giao diện.
Kết luận
ACK biến tài nguyên AWS thành object Kubernetes, nghĩa là chúng có thể được đóng gói, đánh version, review và xoá cùng với ứng dụng cần chúng. Helm mang lại việc đóng gói đó và cấu hình theo từng môi trường. kro bổ sung những gì Helm còn thiếu: thứ tự phụ thuộc, truyền output giữa các tài nguyên, và một API nhỏ gọn mang sẵn chuẩn của platform. Kết hợp lại, “tôi cần một database” chỉ còn là vài dòng trong chính chart của ứng dụng, và DATABASE_URL tự có mặt trong biến môi trường của pod.
Nguồn
- Tổng quan ACK: https://aws-controllers-k8s.github.io/community/docs/community/overview/
- ACK quản lý resource khác account: https://aws-controllers-k8s.github.io/community/docs/user-docs/cross-account-resource-management/
- Hướng dẫn ACK: cross-account (IAMRoleSelector): https://aws-controllers-k8s.github.io/docs/guides/cross-account
- ACK FieldExport: https://aws-controllers-k8s.github.io/community/docs/user-docs/field-export/
- Repo cộng đồng ACK: https://github.com/aws-controllers-k8s/community
- Tổng quan kro: https://kro.run/docs/overview
- Repo kro: https://github.com/kubernetes-sigs/kro



Bình luận