Chapter 9: Argo Security and Permission Management
Deep dive into security configuration, RBAC permission management, SSO integration, and security best practices for the Argo ecosystem
作者
28min
Argo Security and Permission Management
Chapter 9: Securing the Argo Ecosystem
This chapter will detail how to secure the Argo ecosystem, including authentication, authorization, secret management, and security best practices.
9.1 Security Architecture Overview
9.1.1 Argo Security Layers
🔄 正在渲染 Mermaid 图表...
9.1.2 Security Component Relations
🔄 正在渲染 Mermaid 图表...
9.2 Argo CD Security Configuration
9.2.1 RBAC Configuration
# argocd-rbac-cm.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-rbac-cm
namespace: argo
data:
# Policy definition
policy.csv: |
# Role definition
# p, <role/user/group>, <resource>, <action>, <project>/<object>
# Admin role - full access
p, role:admin, applications, *, */*, allow
p, role:admin, clusters, *, *, allow
p, role:admin, repositories, *, *, allow
p, role:admin, projects, *, *, allow
p, role:admin, accounts, *, *, allow
p, role:admin, certificates, *, *, allow
p, role:admin, gpgkeys, *, *, allow
p, role:admin, logs, *, */*, allow
p, role:admin, exec, *, */*, allow
# Developer role - read + sync permission
p, role:developer, applications, get, */*, allow
p, role:developer, applications, sync, */*, allow
p, role:developer, applications, action/*, */*, allow
p, role:developer, logs, get, */*, allow
p, role:developer, repositories, get, *, allow
p, role:developer, projects, get, *, allow
# Read-only role
p, role:readonly, applications, get, */*, allow
p, role:readonly, projects, get, *, allow
p, role:readonly, repositories, get, *, allow
p, role:readonly, clusters, get, *, allow
p, role:readonly, logs, get, */*, allow
# Production admin - manage production project only
p, role:prod-admin, applications, *, production/*, allow
p, role:prod-admin, logs, get, production/*, allow
p, role:prod-admin, exec, create, production/*, allow
# Group mapping
g, admin-team, role:admin
g, dev-team, role:developer
g, ops-team, role:prod-admin
g, viewer-team, role:readonly
# Default policy
policy.default: role:readonly
# Match mode (glob or regex)
policy.matchMode: glob
# Scopes
scopes: '[groups, email]'
9.2.2 SSO/OIDC Configuration
# argocd-cm.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argo
data:
# OIDC configuration (using Keycloak as example)
oidc.config: |
name: Keycloak
issuer: https://keycloak.example.com/realms/argo
clientID: argocd
clientSecret: $oidc.keycloak.clientSecret
requestedScopes: ["openid", "profile", "email", "groups"]
requestedIDTokenClaims:
groups:
essential: true
# Dex configuration (multiple authentication sources)
dex.config: |
connectors:
# GitHub
- type: github
id: github
name: GitHub
config:
clientID: $dex.github.clientID
clientSecret: $dex.github.clientSecret
orgs:
- name: myorg
teams:
- admin-team
- dev-team
# LDAP
- type: ldap
id: ldap
name: LDAP
config:
host: ldap.example.com:636
insecureNoSSL: false
insecureSkipVerify: false
rootCAData: $dex.ldap.rootCA
bindDN: cn=admin,dc=example,dc=com
bindPW: $dex.ldap.bindPW
userSearch:
baseDN: ou=users,dc=example,dc=com
filter: "(objectClass=person)"
username: uid
idAttr: uid
emailAttr: mail
nameAttr: cn
groupSearch:
baseDN: ou=groups,dc=example,dc=com
filter: "(objectClass=groupOfNames)"
userMatchers:
- userAttr: DN
groupAttr: member
nameAttr: cn
# SAML
- type: saml
id: okta
name: Okta
config:
ssoURL: https://mycompany.okta.com/app/xxx/sso/saml
caData: $dex.okta.caData
redirectURI: https://argocd.example.com/api/dex/callback
usernameAttr: email
emailAttr: email
groupsAttr: groups
# URL configuration
url: https://argocd.example.com
# Admin user
admin.enabled: "false"
---
# Secret configuration
apiVersion: v1
kind: Secret
metadata:
name: argocd-secret
namespace: argo
type: Opaque
stringData:
# OIDC client secret
oidc.keycloak.clientSecret: "your-client-secret"
# Dex secrets
dex.github.clientID: "your-github-client-id"
dex.github.clientSecret: "your-github-client-secret"
dex.ldap.bindPW: "ldap-bind-password"
9.2.3 Project Permission Isolation
# argocd-project.yaml
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: production
namespace: argo
spec:
description: "Production environment project"
# Allowed source repositories
sourceRepos:
- https://github.com/myorg/gitops-config.git
- https://github.com/myorg/helm-charts.git
# Allowed destination clusters and namespaces
destinations:
- namespace: production
server: https://kubernetes.default.svc
- namespace: production-*
server: https://kubernetes.default.svc
# Allowed Kubernetes resource types
clusterResourceWhitelist:
- group: ''
kind: Namespace
- group: 'rbac.authorization.k8s.io'
kind: ClusterRole
- group: 'rbac.authorization.k8s.io'
kind: ClusterRoleBinding
# Denied namespace resources
namespaceResourceBlacklist:
- group: ''
kind: ResourceQuota
- group: ''
kind: LimitRange
- group: ''
kind: NetworkPolicy
# Allowed namespace resources
namespaceResourceWhitelist:
- group: ''
kind: '*'
- group: 'apps'
kind: '*'
- group: 'argoproj.io'
kind: '*'
# Role definitions
roles:
- name: prod-developer
description: "Production environment developer"
policies:
- p, proj:production:prod-developer, applications, get, production/*, allow
- p, proj:production:prod-developer, applications, sync, production/*, allow
- p, proj:production:prod-developer, logs, get, production/*, allow
groups:
- prod-dev-team
- name: prod-admin
description: "Production environment admin"
policies:
- p, proj:production:prod-admin, applications, *, production/*, allow
- p, proj:production:prod-admin, logs, get, production/*, allow
- p, proj:production:prod-admin, exec, create, production/*, allow
groups:
- prod-admin-team
# Sync windows (maintenance windows)
syncWindows:
- kind: allow
schedule: '0 9-18 * * 1-5' # Weekdays 9:00-18:00
duration: 9h
applications:
- '*'
manualSync: true
- kind: deny
schedule: '0 0 * * 0' # Sunday all day
duration: 24h
applications:
- '*'
# Signature keys (for verifying Git commit signatures)
signatureKeys:
- keyID: 1234567890ABCDEF
# Orphaned resources monitoring
orphanedResources:
warn: true
ignore:
- group: ''
kind: ConfigMap
name: kube-root-ca.crt
---
# Development environment project
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: development
namespace: argo
spec:
description: "Development environment project"
sourceRepos:
- '*' # Development environment allows all repositories
destinations:
- namespace: dev-*
server: https://kubernetes.default.svc
- namespace: development
server: https://kubernetes.default.svc
# Development environment restricts certain dangerous resources
namespaceResourceBlacklist:
- group: ''
kind: Secret
name: '*-production-*'
roles:
- name: developer
description: "Developer"
policies:
- p, proj:development:developer, applications, *, development/*, allow
groups:
- developers
9.2.4 TLS Configuration
# argocd-tls.yaml
apiVersion: v1
kind: Secret
metadata:
name: argocd-server-tls
namespace: argo
type: kubernetes.io/tls
data:
tls.crt: <base64-encoded-cert>
tls.key: <base64-encoded-key>
---
# Ingress with TLS
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: argocd-server
namespace: argo
annotations:
nginx.ingress.kubernetes.io/ssl-passthrough: "true"
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
spec:
tls:
- hosts:
- argocd.example.com
secretName: argocd-server-tls
rules:
- host: argocd.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: argocd-server
port:
number: 443
9.3 Argo Workflows Security Configuration
9.3.1 ServiceAccount Permissions
# workflow-rbac.yaml
# Minimal permission ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
name: workflow-minimal
namespace: argo
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: workflow-minimal-role
namespace: argo
rules:
# Only allow Pod operations
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "watch", "list"]
# Allow reading Pod logs
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get"]
# Allow reading ConfigMaps
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: workflow-minimal-binding
namespace: argo
subjects:
- kind: ServiceAccount
name: workflow-minimal
namespace: argo
roleRef:
kind: Role
name: workflow-minimal-role
apiGroup: rbac.authorization.k8s.io
---
# Builder-specific ServiceAccount (needs more permissions)
apiVersion: v1
kind: ServiceAccount
metadata:
name: workflow-builder
namespace: argo
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: workflow-builder-role
namespace: argo
rules:
- apiGroups: [""]
resources: ["pods", "pods/log", "pods/exec"]
verbs: ["*"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]
resourceNames: ["docker-credentials", "git-credentials"]
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "create", "update"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "create", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: workflow-builder-binding
namespace: argo
subjects:
- kind: ServiceAccount
name: workflow-builder
namespace: argo
roleRef:
kind: Role
name: workflow-builder-role
apiGroup: rbac.authorization.k8s.io
9.3.2 Workflow Security Policies
# workflow-controller-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: workflow-controller-configmap
namespace: argo
data:
config: |
# Executor configuration
containerRuntimeExecutor: emissary
# Security context
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
# Pod security context
podSecurityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
# Resource rate limit
resourceRateLimit:
limit: 100
burst: 200
# Parallelism limit
parallelism: 50
# Namespace parallelism
namespaceParallelism: 10
# Workflow defaults
workflowDefaults:
spec:
# Default ServiceAccount
serviceAccountName: workflow-minimal
# Timeout settings
activeDeadlineSeconds: 3600
# TTL strategy
ttlStrategy:
secondsAfterCompletion: 86400
secondsAfterSuccess: 3600
secondsAfterFailure: 172800
# Pod GC strategy
podGC:
strategy: OnPodCompletion
# Security context
securityContext:
runAsNonRoot: true
runAsUser: 1000
# Archive configuration
persistence:
archive: true
archiveTTL: 30d
# Metrics configuration
metricsConfig:
enabled: true
path: /metrics
port: 9090
9.3.3 SSO Configuration
# argo-server-sso.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: workflow-controller-configmap
namespace: argo
data:
sso: |
issuer: https://keycloak.example.com/realms/argo
clientId:
name: argo-workflows-sso
key: client-id
clientSecret:
name: argo-workflows-sso
key: client-secret
redirectUrl: https://argo-workflows.example.com/oauth2/callback
scopes:
- openid
- profile
- email
- groups
rbac:
enabled: true
insecureSkipVerify: false
---
apiVersion: v1
kind: Secret
metadata:
name: argo-workflows-sso
namespace: argo
type: Opaque
stringData:
client-id: "argo-workflows"
client-secret: "your-client-secret"
9.3.4 Network Policy
# workflow-network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: workflow-pods
namespace: argo
spec:
podSelector:
matchLabels:
workflows.argoproj.io/workflow: ""
policyTypes:
- Ingress
- Egress
ingress:
# Only allow traffic from workflow-controller
- from:
- podSelector:
matchLabels:
app: workflow-controller
egress:
# Allow DNS queries
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
# Allow access to internal services
- to:
- namespaceSelector:
matchLabels:
name: internal-services
# Allow access to specific external addresses
- to:
- ipBlock:
cidr: 10.0.0.0/8
ports:
- protocol: TCP
port: 443
9.4 Secret Management
9.4.1 Sealed Secrets
# sealed-secrets installation and usage
# Install Sealed Secrets Controller
# kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.0/controller.yaml
# Create SealedSecret
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: my-secret
namespace: argo
spec:
encryptedData:
password: AgBY2L... # Encrypted password
api-key: AgCX3M... # Encrypted API key
template:
metadata:
name: my-secret
namespace: argo
type: Opaque
# Using kubeseal to encrypt secrets
# Install kubeseal
brew install kubeseal
# Create regular Secret
kubectl create secret generic my-secret \
--dry-run=client \
--from-literal=password=mysecretpassword \
-o yaml > secret.yaml
# Encrypt Secret
kubeseal --format=yaml < secret.yaml > sealed-secret.yaml
# Apply SealedSecret
kubectl apply -f sealed-secret.yaml
9.4.2 External Secrets Operator
# external-secrets configuration
# Install External Secrets Operator
# helm install external-secrets external-secrets/external-secrets -n external-secrets --create-namespace
# SecretStore - HashiCorp Vault
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-backend
namespace: argo
spec:
provider:
vault:
server: "https://vault.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "argo-role"
serviceAccountRef:
name: "argo-vault-sa"
---
# SecretStore - AWS Secrets Manager
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secrets
namespace: argo
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
jwt:
serviceAccountRef:
name: aws-secrets-sa
---
# ExternalSecret
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: database-credentials
namespace: argo
spec:
refreshInterval: 1h
secretStoreRef:
kind: SecretStore
name: vault-backend
target:
name: database-credentials
creationPolicy: Owner
data:
- secretKey: username
remoteRef:
key: database/credentials
property: username
- secretKey: password
remoteRef:
key: database/credentials
property: password
9.4.3 Argo CD Secret Management
# argocd-secrets-plugin.yaml
# Using Argo CD Vault Plugin
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-cm
namespace: argo
data:
configManagementPlugins: |
- name: argocd-vault-plugin
generate:
command: ["argocd-vault-plugin"]
args: ["generate", "./"]
- name: argocd-vault-plugin-helm
init:
command: ["/bin/sh", "-c"]
args: ["helm dependency build"]
generate:
command: ["/bin/sh", "-c"]
args: ["helm template $ARGOCD_APP_NAME . | argocd-vault-plugin generate -"]
---
# Application using Vault Plugin
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: argo
spec:
source:
repoURL: https://github.com/myorg/my-app.git
path: k8s
plugin:
name: argocd-vault-plugin
destination:
server: https://kubernetes.default.svc
namespace: production
9.4.4 Secret Rotation
# secret-rotation-workflow.yaml
apiVersion: argoproj.io/v1alpha1
kind: CronWorkflow
metadata:
name: secret-rotation
namespace: argo
spec:
schedule: "0 0 1 * *" # Run on 1st of each month
workflowSpec:
entrypoint: rotate-secrets
serviceAccountName: secret-rotator
templates:
- name: rotate-secrets
dag:
tasks:
- name: rotate-db-password
template: rotate-password
arguments:
parameters:
- name: secret-name
value: database-credentials
- name: key
value: password
- name: rotate-api-keys
template: rotate-api-key
arguments:
parameters:
- name: secret-name
value: api-credentials
- name: rotate-password
inputs:
parameters:
- name: secret-name
- name: key
script:
image: bitnami/kubectl:latest
command: ["/bin/bash"]
source: |
# Generate new password
NEW_PASSWORD=$(openssl rand -base64 32)
# Update database password (actual database update logic needed here)
# mysql -u admin -p$OLD_PASSWORD -e "ALTER USER 'app'@'%' IDENTIFIED BY '$NEW_PASSWORD'"
# Update Kubernetes Secret
kubectl patch secret {{inputs.parameters.secret-name}} \
-p "{\"stringData\":{\"{{inputs.parameters.key}}\":\"$NEW_PASSWORD\"}}"
# Restart related application to get new secret
kubectl rollout restart deployment/myapp
- name: rotate-api-key
inputs:
parameters:
- name: secret-name
container:
image: curlimages/curl:latest
command: ["/bin/sh", "-c"]
args:
- |
# Call API to generate new key
NEW_KEY=$(curl -X POST https://api.example.com/rotate-key -H "Authorization: Bearer $OLD_KEY")
# Update Secret
kubectl patch secret {{inputs.parameters.secret-name}} \
-p "{\"stringData\":{\"api-key\":\"$NEW_KEY\"}}"
9.5 Audit and Compliance
9.5.1 Audit Log Configuration
# audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
# Record all Argo resource operations
- level: RequestResponse
resources:
- group: argoproj.io
resources: ["*"]
verbs: ["create", "update", "patch", "delete"]
# Record Secret access
- level: Metadata
resources:
- group: ""
resources: ["secrets"]
verbs: ["get", "list", "watch"]
# Record RBAC changes
- level: RequestResponse
resources:
- group: "rbac.authorization.k8s.io"
resources: ["*"]
verbs: ["*"]
# Ignore health checks
- level: None
users: ["system:kube-proxy"]
verbs: ["watch"]
resources:
- group: ""
resources: ["endpoints", "services", "services/status"]
9.5.2 Argo CD Audit
# argocd-notifications audit
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-notifications-cm
namespace: argo
data:
# Trigger - record all sync operations
trigger.on-sync-succeeded: |
- description: Application syncing has succeeded
send:
- audit-log
when: app.status.operationState.phase in ['Succeeded']
trigger.on-sync-failed: |
- description: Application syncing has failed
send:
- audit-log
when: app.status.operationState.phase in ['Error', 'Failed']
trigger.on-health-degraded: |
- description: Application has degraded
send:
- audit-log
when: app.status.health.status == 'Degraded'
# Template - audit log format
template.audit-log: |
webhook:
audit:
method: POST
path: /audit
body: |
{
"timestamp": "{{.app.status.operationState.finishedAt}}",
"application": "{{.app.metadata.name}}",
"project": "{{.app.spec.project}}",
"action": "sync",
"status": "{{.app.status.operationState.phase}}",
"revision": "{{.app.status.sync.revision}}",
"initiatedBy": "{{.app.status.operationState.operation.initiatedBy.username}}",
"message": "{{.app.status.operationState.message}}"
}
# Service - audit log collection
service.webhook.audit: |
url: https://audit.example.com
headers:
- name: Authorization
value: Bearer $audit-token
9.5.3 Compliance Check Workflow
# compliance-check-workflow.yaml
apiVersion: argoproj.io/v1alpha1
kind: CronWorkflow
metadata:
name: compliance-check
namespace: argo
spec:
schedule: "0 6 * * *" # Daily at 6 AM
workflowSpec:
entrypoint: compliance-pipeline
templates:
- name: compliance-pipeline
dag:
tasks:
- name: check-rbac
template: rbac-audit
- name: check-secrets
template: secret-audit
- name: check-network-policies
template: network-audit
- name: generate-report
template: generate-report
dependencies: [check-rbac, check-secrets, check-network-policies]
- name: rbac-audit
container:
image: bitnami/kubectl:latest
command: ["/bin/sh", "-c"]
args:
- |
echo "=== RBAC Audit ===" > /tmp/rbac-report.txt
# Check overly permissive ClusterRoleBindings
echo "Checking for overly permissive ClusterRoleBindings..."
kubectl get clusterrolebindings -o json | jq -r '
.items[] |
select(.roleRef.name == "cluster-admin") |
"WARNING: \(.metadata.name) has cluster-admin role"
' >> /tmp/rbac-report.txt
# Check ServiceAccount permissions
echo "Checking ServiceAccount permissions..."
kubectl get rolebindings,clusterrolebindings -A -o json | jq -r '
.items[] |
select(.subjects[]?.kind == "ServiceAccount") |
"\(.metadata.namespace)/\(.metadata.name): \(.roleRef.name)"
' >> /tmp/rbac-report.txt
cat /tmp/rbac-report.txt
outputs:
artifacts:
- name: rbac-report
path: /tmp/rbac-report.txt
- name: secret-audit
container:
image: bitnami/kubectl:latest
command: ["/bin/sh", "-c"]
args:
- |
echo "=== Secret Audit ===" > /tmp/secret-report.txt
# Check for unencrypted Secrets
echo "Checking for potential plaintext secrets..."
kubectl get secrets -A -o json | jq -r '
.items[] |
select(.type == "Opaque") |
select(.data | to_entries | any(.value | @base64d | test("password|secret|key|token"; "i"))) |
"WARNING: \(.metadata.namespace)/\(.metadata.name) may contain sensitive data"
' >> /tmp/secret-report.txt
# Check certificate expiration
echo "Checking certificate expiration..."
kubectl get secrets -A -o json | jq -r '
.items[] |
select(.type == "kubernetes.io/tls") |
"\(.metadata.namespace)/\(.metadata.name)"
' >> /tmp/secret-report.txt
cat /tmp/secret-report.txt
outputs:
artifacts:
- name: secret-report
path: /tmp/secret-report.txt
- name: network-audit
container:
image: bitnami/kubectl:latest
command: ["/bin/sh", "-c"]
args:
- |
echo "=== Network Policy Audit ===" > /tmp/network-report.txt
# Check namespaces without NetworkPolicy
echo "Namespaces without NetworkPolicy..."
for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
if [ $(kubectl get networkpolicies -n $ns 2>/dev/null | wc -l) -eq 0 ]; then
echo "WARNING: Namespace $ns has no NetworkPolicy" >> /tmp/network-report.txt
fi
done
cat /tmp/network-report.txt
outputs:
artifacts:
- name: network-report
path: /tmp/network-report.txt
- name: generate-report
inputs:
artifacts:
- name: rbac-report
path: /tmp/rbac-report.txt
- name: secret-report
path: /tmp/secret-report.txt
- name: network-report
path: /tmp/network-report.txt
container:
image: curlimages/curl:latest
command: ["/bin/sh", "-c"]
args:
- |
# Merge reports
cat /tmp/*.txt > /tmp/full-report.txt
# Send to Slack
curl -X POST $SLACK_WEBHOOK \
-H "Content-Type: application/json" \
-d "{\"text\": \"Daily Compliance Report\", \"attachments\": [{\"text\": \"$(cat /tmp/full-report.txt | head -100)\"}]}"
9.6 Chapter Summary
This chapter detailed security configuration for the Argo ecosystem:
🔄 正在渲染 Mermaid 图表...
Key Points:
- Authentication: Use SSO/OIDC for unified user authentication
- Authorization: Implement fine-grained permission control through RBAC and Projects
- Secret Management: Use Sealed Secrets or External Secrets for secure secret management
- Network Security: Configure TLS and NetworkPolicy to protect communication
- Audit & Compliance: Establish audit logging and compliance checking mechanisms
In the next chapter, we will learn about Argo production environment best practices.