Platform
Kubernetes Application Delivery and GitOps Operations Model
A deployment record connecting Kubernetes workloads, Services, Helm, Operators, Argo CD, GitOps, and operational boundaries.
At first glance, Kubernetes appears to be a container deployment tool that uses a lot of YAML. However, if you follow it a little further, the core of Kubernetes is closer to “declaring what state the application should run in and continuously adjusting that state” rather than “running the container.”
If you think about operating a backend service, what you need is not only container execution. You need to decide what image to deploy, how many instances to maintain, what name the traffic will come under, where to place settings and sensitive information, what health checks are needed to avoid sending requests to unprepared pods, and how to rollback when deployment fails.
Rather than memorizing Kubernetes, AKS, Helm, Operator, ArgoCD, and GitOps separately, this article is a record of the flow of an application from a container image to the operating state of a cluster.
Transition point from container to orchestration
Containers allow applications and execution environments to be bundled and deployed as images. Unlike VMs, startup is fast and resource usage is low because each instance does not contain an entire OS but shares the host OS kernel. It is also advantageous in reducing the difference between the development environment and the operating environment.
However, running one container well and operating dozens of services are different problems. As the number of services increases, the following questions arise.
- If a container dies, who relaunches it?
- When there are multiple servers, which node should they be placed on?
- Which pod will the traffic be sent to?
- How to gradually deploy new versions
- How to separate settings and secrets
- Where to collect logs and metrics
Kubernetes is a container orchestration platform that addresses this problem. The container execution itself is handled by a runtime such as containerd, but Kubernetes looks at the overall state of the cluster and deploys pods, recreates failed pods, and maintains service endpoints.
Separate view of Control Plane and Worker Node
Kubernetes clusters are largely divided into Control Plane and Worker Nodes. The Control Plane manages the state of the cluster, and Worker Nodes run the actual application pods.
Control Plane
- kube-apiserver: API entry point for all cluster requests
- scheduler: decides which Node should run a new Pod
- controller-manager: control loops that reconcile desired and actual state
- etcd: cluster state store
Worker Node
- kubelet: manages Pod state on each Node
- kube-proxy: handles Service network routing
- container runtime: runs containers through containerd or a similar runtimeIn managed Kubernetes such as AKS or EKS, much of the control plane is managed by the cloud provider. Users mainly deal with Node Pool, network, storage, application resources, access rights, and observability configuration. Being a managed service does not mean that Kubernetes operations disappear. Although the burden of operating API Server and etcd is reduced, incorrect deployment, excessive resource requests, poor probes, open ingress, and uncleaned secrets still lead to service problems.
Deployment is not a deployment file, but an operating contract
The most frequently encountered resources in Kubernetes are Pod, Deployment, Service, Ingress, ConfigMap, and Secret. These resources are not simple YAML files, but rather express “what the service should be like.”
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: ghcr.io/example/api:1.0.0
ports:
- containerPort: 8080
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"Here, replicas: 3 is a declaration to maintain three pods, and readinessProbe is a device for not sending traffic to pods that are not yet ready. livenessProbe serves as a basis for restarting the application when it enters an unrecoverable state. resources.requests is used by the scheduler to make placement decisions, and limits limits one container from taking excessive node resources.
At first, it is easy to check that it is deployed with kubectl apply -f deployment.yaml. However, from an operational perspective, the following is more important:
- Don't readiness and liveness see the same endpoint?
- Is the application startup time longer than the probe settings?
- Is the CPU limit too low and causing throttling?
- Is OOMKilled not repeated due to exceeding the memory limit?
- Is the minimum number of available pods maintained during rolling updates?
Kubernetes leaves many hints when a problem occurs.
kubectl get pods -o wide
kubectl describe pod api-7c9d8d9f8b-x2k7s
kubectl logs deploy/api --tail=100
kubectl rollout status deployment/api
kubectl rollout history deployment/api
kubectl rollout undo deployment/apiService and Ingress hide pod replacement
Pods can be created and destroyed at any time. Therefore, operation is difficult if you rely directly on the Pod IP. A Service provides a reliable access point to a set of Pods based on a label selector.
apiVersion: v1
kind: Service
metadata:
name: api
spec:
type: ClusterIP
selector:
app: api
ports:
- port: 80
targetPort: 8080
`
ClusterIPis for internal access to the cluster, andNodePortis exposed to the outside through the node port. In a cloud environment, using theLoadBalancer` type connects to an external load balancer such as Azure Load Balancer or AWS Load Balancer. HTTP layer routing, TLS, and host/path-based routing are usually handled by the Ingress Controller.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api
spec:
ingressClassName: nginx
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api
port:
number: 80In AKS, there are options such as Azure CNI, Azure Load Balancer, and Application Gateway Ingress Controller, and in EKS, you can connect to ALB/NLB with AWS Load Balancer Controller. Ultimately, the Kubernetes network is not just a matter of resources within the cluster, but also connects to VNet/VPC, Subnet, NSG/Security Group, DNS, and TLS certificates.
ConfigMap and Secret make the deployment unit lightweight
If all settings are entered when creating an image, the image must be recreated for each environment. In Kubernetes, settings can be separated into ConfigMap and Secret.
apiVersion: v1
kind: ConfigMap
metadata:
name: api-config
data:
SPRING_PROFILES_ACTIVE: prod
LOG_LEVEL: infoapiVersion: v1
kind: Secret
metadata:
name: api-secret
type: Opaque
stringData:
DATABASE_URL: postgresql://user:password@postgres:5432/appSecret has a good name and purpose, but that doesn't mean that the default Kubernetes Secret is a robust secret management system. etcd encryption, RBAC, External Secrets, cloud secret manager integration, and Secret rotation must be seen together to create an operational configuration. In particular, when applying GitOps, methods such as SOPS, Sealed Secrets, and External Secrets Operator should be reviewed to avoid uploading plaintext secrets to Git.
Helm reduces YAML duplication, but also creates abstraction costs
As Kubernetes resources increase, YAML management for each environment becomes complicated. Helm bundles Kubernetes resources into a package called Chart and injects environment-specific values with values.yaml. Like apt, yum, and pip, it is close to a package manager that installs and upgrades Kubernetes applications.
my-api-chart/
Chart.yaml
values.yaml
templates/
deployment.yaml
service.yaml
ingress.yamlFor example, the development environment and the operating environment can use the same template, but only have different replicas, image tags, resources, and ingress hosts.
# values-prod.yaml
image:
repository: ghcr.io/example/api
tag: "1.0.0"
replicaCount: 3
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
ingress:
enabled: true
host: api.example.comDistribution is performed as follows.
helm lint ./charts/api
helm template api ./charts/api -f values-prod.yaml
helm upgrade --install api ./charts/api -n production -f values-prod.yamlHelm is convenient, but if the template becomes too complicated, it becomes a deployment system that is difficult to read. A good chart is not a chart that abstracts everything, but a chart that separates values that the team frequently changes from structures that should not be changed. It is also important to make a habit of checking the YAML that is actually created with the helm template.
Kustomize is a different direction from Helm. Combine YAML using base and overlay rather than a template language. Helm is strong in packaging and parameterization, and Kustomize is good at keeping the existing YAML relatively intact and adding environment-specific differences. One is always better than the other; you should choose whichever is easier for your team to read and review changes.
Operator is a method of transferring operating knowledge to the controller.
Operator is a pattern that extends Kubernetes. It goes beyond simply installing Deployment and Service, and puts the operating procedures of a specific system into the controller. The core is CRD (Custom Resource Definition), CR (Custom Resource), and Custom Controller.
CRD: defines a custom resource type named Prometheus
CR: requests an actual Prometheus instance
Controller: watches the CR, creates the required StatefulSet, Service, and Config, and reconciles stateTaking Prometheus Operator as an example, users do not directly create complex StatefulSets and ConfigMap to configure the Prometheus server. Create Custom Resources such as Prometheus, ServiceMonitor, and PodMonitor, and the Operator looks at them and configures the necessary sub-resources.
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: api
spec:
selector:
matchLabels:
app: api
endpoints:
- port: http
path: /metrics
interval: 30sThe Operator becomes a controller who is comfortable but has strong authority within the cluster. You need to check which CRDs are installed, which namespaces are monitored, and whether RBAC permissions are excessive. Operator Lifecycle Manager (OLM) or Operator Hub assists with installation and lifecycle management, but does not eliminate operational responsibility.
GitOps is more about change control than deployment automation
GitOps is an operating method that sets the Git repository as the desired state of the cluster and synchronizes it with the actual cluster state. ArgoCD is a representative tool that implements this model in Kubernetes.
The general flow is as follows.
1. Application code changes
2. CI runs tests and builds the image
3. The image is pushed to the registry
4. Helm values or manifest image tags change in the deployment repository
5. A Pull Request is reviewed
6. Argo CD detects the change after merge
7. Cluster state synchronizes with the desired state in GitThe important thing about this structure is that it reduces the need to manually fix the cluster. If the operator makes any modifications with kubectl edit deployment, ArgoCD detects the difference between Git and the actual state as drift. Depending on the Sync Policy, it can be automatically reverted or synchronized after manual approval.
Installing ArgoCD is relatively simple.
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yamlInitially, you can replace the argocd-server service with LoadBalancer for external access. For practice or local verification, port-forward is safer and simpler.
kubectl patch svc argocd-server -n argocd -p '{"spec": {"type": "LoadBalancer"}}'
kubectl port-forward svc/argocd-server -n argocd 8080:443Check the initial password in Secret or look it up using CLI.
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath="{.data.password}" | base64 -d
argocd admin initial-password -n argocd
argocd login <ARGOCD_SERVER>
argocd account update-passwordYou can check the basic operation of ArgoCD by deploying the Guestbook example.
kubectl config set-context --current --namespace=argocd
argocd app create guestbook \
--repo https://github.com/argoproj/argocd-example-apps.git \
--path guestbook \
--dest-server https://kubernetes.default.svc \
--dest-namespace default
argocd app get guestbook
argocd app sync guestbookIn a production environment, the storage structure is more important than the examples. You need to decide whether to separate the application source repository, deployment configuration repository, and platform add-on repository.
app-source-repo
- application code
- Dockerfile
- tests
app-deploy-repo
- Helm chart or manifests
- values-dev.yaml
- values-staging.yaml
- values-prod.yaml
platform-addons-repo
- ingress controller
- cert-manager
- external-secrets
- prometheus stack
- logging stackSplitting up repositories makes it easier to separate change responsibilities, but also increases flow. Putting them all in one repository is simple, but it can blur the boundaries between permissions and reviews. What is more important in GitOps than the tool name is the structure in which changes remain as Pull Requests, only approved settings are reflected in the cluster, and rollbacks are linked to Git history.
ArgoCD Application can also be declared according to this structure.
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: api-prod
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/example/deployments.git
targetRevision: main
path: apps/api/chart
helm:
valueFiles:
- values-prod.yaml
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
`
```prune: true` means that resources that have disappeared from Git will be removed from the cluster. Although it is convenient, it is also a dangerous setting. If the structure requires resource deletion, you must check not only “newly added resources” but also “disappearing resources” in the PR review. `selfHeal: true` returns settings manually changed in the cluster based on Git. Since the content that the operator hastily modified with `kubectl edit` can be restored, there must be a procedure to eventually reflect emergency changes in Git.
## Terraform and Ansible manage cluster transfer steps
Kubernetes resources can be managed with GitOps, but the Kubernetes cluster itself has a separate infrastructure lifecycle. Terraform is best left to creating AKS or EKS, configuring node pools, connecting networks and permissions, and issuing kubeconfig.
```hcl
resource "azurerm_resource_group" "platform" {
name = "rg-platform-prod"
location = "koreacentral"
}
resource "azurerm_kubernetes_cluster" "main" {
name = "aks-platform-prod"
location = azurerm_resource_group.platform.location
resource_group_name = azurerm_resource_group.platform.name
dns_prefix = "platform-prod"
default_node_pool {
name = "system"
node_count = 3
vm_size = "Standard_D2s_v5"
}
identity {
type = "SystemAssigned"
}
}When using Terraform, the important thing is not “the cluster has been created” but whether it can be created again with the same definition. Bundling resource groups, VNet/Subnet, AKS, ACR, Key Vault, Log Analytics Workspace, DNS, and permission assignments in code improves environment reproducibility. Conversely, as the number of settings manually changed in the portal increases, the Terraform state and actual resources begin to diverge.
Ansible is in a slightly different position. In managed Kubernetes, Ansible's role is reduced, but in configurations such as VM-based clusters or Kubespray, it is useful for node preparation tasks. For example, disabling swap, kernel parameters, package installation, SSH access, and container runtime configuration are between declarative infrastructure and operational scripts.
- name: Prepare Kubernetes nodes
hosts: kube_nodes
become: true
tasks:
- name: Disable swap
command: swapoff -a
- name: Enable bridge netfilter
modprobe:
name: br_netfilter
state: present
- name: Configure sysctl for Kubernetes networking
copy:
dest: /etc/sysctl.d/k8s.conf
content: |
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1From an operational perspective, it is important not to mix the responsibilities of Terraform, Ansible, and ArgoCD.
Terraform
- cloud resources
- Kubernetes cluster
- network, IAM, registry, logging workspace
Ansible
- VM node bootstrap
- OS package and kernel settings
- self-managed cluster preparation
ArgoCD / GitOps
- Kubernetes workloads
- Helm values and manifests
- platform add-onsSetting these boundaries will make your questions clear even when obstacles arise. If there is no node or the network is incorrect, it is the Terraform area. If the node OS settings are incorrect, it is the Ansible area. If Deployment or Ingress is different from Git, it is the ArgoCD area.
AKS provisioning involves leaving operational input rather than creating a cluster.
When creating AKS with Terraform, looking at just one azurerm_kubernetes_cluster resource is not enough. Actual operational inputs are determined together with network, node pool, ACR connection, log workspace, Key Vault access, upgrade policy, and permission model. What is more important than creating a cluster is leaving code and variables so that the same judgment can be repeated in the next environment.
variable "environment" {
type = string
default = "prod"
}
variable "location" {
type = string
default = "koreacentral"
}
resource "azurerm_log_analytics_workspace" "platform" {
name = "log-platform-${var.environment}"
location = var.location
resource_group_name = azurerm_resource_group.platform.name
sku = "PerGB2018"
retention_in_days = 30
}
resource "azurerm_kubernetes_cluster" "main" {
name = "aks-platform-${var.environment}"
location = var.location
resource_group_name = azurerm_resource_group.platform.name
dns_prefix = "platform-${var.environment}"
default_node_pool {
name = "system"
vm_size = "Standard_D2s_v5"
node_count = 3
vnet_subnet_id = azurerm_subnet.aks.id
type = "VirtualMachineScaleSets"
orchestrator_version = null
}
identity {
type = "SystemAssigned"
}
oms_agent {
log_analytics_workspace_id = azurerm_log_analytics_workspace.platform.id
}
network_profile {
network_plugin = "azure"
load_balancer_sku = "standard"
outbound_type = "loadBalancer"
}
}In this example, the key is the surrounding dependencies rather than AKS itself. If Log Analytics is omitted, failure analysis becomes difficult, and if the subnet is not clearly specified, the network policy and IP plan become unstable later. From the moment you attach ACR, Key Vault, and DNS Zone, the cluster becomes part of the platform rather than an independent resource.
Terraform execution flow should also be left as an operating procedure.
terraform init -upgrade
terraform fmt -recursive
terraform validate
terraform plan -out main.tfplan
terraform apply main.tfplan
az aks get-credentials \
--resource-group rg-platform-prod \
--name aks-platform-prod \
--overwrite-existing
kubectl get nodes -o wide
kubectl get pods -AWhat you need to look at in the terraform plan results is not only the number of creations. You should look for changes that could lead to failures, such as re-creating a node pool, changing a subnet, replacing an identity, or deleting a role assignment. In particular, AKS can lead to replacement rather than simple modification if the node pool and network settings change. So in PR, it is better to treat Terraform diff as part of the code review.
State management cannot be left out either. Local state is convenient for individual practice, but there is a risk of conflict and loss in a collaborative environment.
terraform {
backend "azurerm" {
resource_group_name = "rg-tfstate-prod"
storage_account_name = "sttfstateprod001"
container_name = "tfstate"
key = "platform/aks-prod.tfstate"
}
}Using remote state makes it easier to track who applied what infrastructure changes and when. However, because the state file may contain resource identifiers and some sensitive values, Storage Account access rights should also be minimized.
Kubespray asks different questions than managed Kubernetes
In managed Kubernetes such as AKS/EKS, the burden of operating the control plane is taken over by the cloud. On the other hand, configuring a cluster on top of a VM with Kubespray requires more direct understanding of the control plane, etcd, worker nodes, container runtime, certificates, and upgrade procedures. This experience is helpful even if you use a managed service. This is because when a failure occurs, you can more quickly narrow down which layer the problem is, rather than saying “Kubernetes is not working.”
self-managed cluster preparation
1. prepare Linux nodes and SSH access
2. disable swap and configure kernel modules
3. configure container runtime
4. define inventory for control plane and workers
5. run Kubespray playbook
6. verify kubeconfig and cluster components
7. apply CNI, storage class, ingress, monitoringThe advantage of Kubespray is that repeatable cluster installation can be left with Ansible inventory and playbooks. However, operational responsibility also increases. You must manually check etcd backups, certificate expiration, control plane upgrades, node OS patches, and even CNI compatibility.
[kube_control_plane]
cp-01 ansible_host=10.10.1.10
cp-02 ansible_host=10.10.1.11
cp-03 ansible_host=10.10.1.12
[kube_node]
worker-01 ansible_host=10.10.2.10
worker-02 ansible_host=10.10.2.11
[etcd:children]
kube_control_plane
[k8s_cluster:children]
kube_control_plane
kube_nodeWhether you choose managed Kubernetes or build your own is a matter of operating model, not technology preference. If a small team needs to quickly operate a product, AKS/EKS is realistic, and if the environment requires strong control of the network and OS layers, self-construction or private PaaS should be considered. In either case, it is safer to separate the scope of GitOps and infrastructure provisioning.
Deployment strategy is about determining the scope of failure, not YAML options
Kubernetes Deployment supports RollingUpdate by default. Creates a new ReplicaSet and reduces the existing pods when the new pods are ready.
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
`
```maxUnavailable: 0` means that the existing available pods will not be reduced even during deployment, and `maxSurge: 1` means that one more pod can be temporarily launched. This setting requires resource availability, but is stable for services with traffic.
If you need Blue/Green or Canary, you can look into tools like Argo Rollouts. Canary is a method of sending only a portion of traffic to a new version and gradually increasing it by looking at the metrics. At this time, it can be automatically stopped based on error rate or latency by linking with Prometheus. The important thing is not to “use a canary,” but to decide which indicators to stop deployment when they get worse.
```txt
Canary decision signals
- 5xx error rate
- p95 / p99 latency
- pod restart count
- CPU throttling
- business transaction failureObservability should go into resource design, not after deployment
When looking at problems in Kubernetes, Pod status alone is not enough. Deployment rollout, Service endpoint, Ingress routing, Node resource, application log, metrics, and trace must be viewed together.
kubectl get deploy,rs,pod,svc,ingress
kubectl get endpoints api
kubectl top pod
kubectl top node
kubectl describe ingress api
kubectl logs deploy/api -fPrometheus/Grafana reports metrics, Loki or OpenSearch help with log searches, and Tempo or Jaeger provide distributed tracing. By attaching OpenTelemetry to your application, you can follow HTTP requests, DB queries, and external API calls as a single trace.
Istio, a service mesh, provides mTLS, traffic policy, retry, timeout, circuit breaking, and telemetry. However, adding a mesh from the beginning increases operational complexity. It is better to first review the Service, Ingress, Probe, Resource, and logs/metrics when they are stably organized and the need for traffic control becomes clear.
Cleanup
Kubernetes starts out as a tool to launch containers, but in reality, it is more of a platform for declaring and coordinating the operating state of an application. Deployment defines rollout and recovery units, Service and Ingress hide pod replacement, and ConfigMap and Secret separate configuration from the image. Helm manages differences in each environment, and Operator moves repetitive operating procedures to the controller.
GitOps and ArgoCD are not a method of creating one more deployment button, but an operating model that matches change history and cluster status based on Git. For this model to work properly, we need to keep asking more basic questions than just using a lot of YAML. Which state will be the desired state, who will approve changes, revert when drift occurs, and stop deployment when some indicator gets worse.
Ultimately, Kubernetes operations do not end with “successful deployment.” Requests come in, pods are ready, services can deliver traffic, deployments can progress incrementally, and when problems arise, logs and metrics should be able to narrow down the cause. When the entire flow is managed with Git and declarative settings, Kubernetes becomes a platform beyond a simple execution environment.
Unauthorized copying, redistribution, and automated scraping are not permitted. Please cite the original URL when referencing this article.