deploy app via GitHub pipeline
Summary
Fairly simple, almost complete copy & paste from ClaudeAI but some things required tweaking: specifically how image versions were identified (generated pipeline had issues with using branch name which would disappear on PR) as well as lowercasing image name on the right place.
This assumes that the docker image is built and pushed into ghcr.io
repository and it is named ramblings
(see sample build file here: https://github.com/dkambur/Ramblings/blob/main/.github/workflows/build.yaml - stages prior to deploy
)
Preparation steps - GitHub source code repo
Secure main
branch
This is questionable as it is my own repo with public visibility. Done so just in case.
ClaudeAI instructions For securing
Originally from ClaudeAI - slightly modified
Require approval for fork PRs:
Go to Settings → Actions → General
Under Fork pull request workflows
, select Require approval for first-time contributors
Was done already.
Protect your main branch:
Settings → Branches → Add rule for main
Enable Require pull request reviews before merging
Different name: Require a pull request before merging
1 Approver
Require review from Code Owners
Limit who can approve workflow runs:
Only trusted collaborators should be able to approve Actions. Set accordingly.
Preparation steps - kubernetes
Create Service Account
Taken from ClaudeAI
# Create a namespace for your app (optional but recommended)
kubectl create namespace ramblings
# Create a service account for GitHub Actions
kubectl create serviceaccount github-deployer -n ramblings
# Create a secret for the service account token
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
name: github-deployer-token
namespace: ramblings
annotations:
kubernetes.io/service-account.name: github-deployer
type: kubernetes.io/service-account-token
EOF
Create deployer role and authorise to deploy
Taken from ClaudeAI
Copy the text below into rbac-deployment.yaml
:
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: deployer-role
namespace: ramblings
rules:
# Deployments
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# ReplicaSets (needed for deployments)
- apiGroups: ["apps"]
resources: ["replicasets"]
verbs: ["get", "list", "watch"]
# Pods (for viewing status)
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
# Services
- apiGroups: [""]
resources: ["services"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# ConfigMaps and Secrets (if your app needs them)
- apiGroups: [""]
resources: ["configmaps", "secrets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: deployer-rolebinding
namespace: ramblings
subjects:
- kind: ServiceAccount
name: github-deployer
namespace: ramblings
roleRef:
kind: Role
name: deployer-role
apiGroup: rbac.authorization.k8s.io
Apply
kubectl apply -f rbac-deployment.yaml
Get token for GitHub
# Get the token
TOKEN=$(kubectl get secret github-deployer-token -n ramblings -o jsonpath='{.data.token}' | base64 -d)
# Get the cluster CA certificate
CA_CERT=$(kubectl get secret github-deployer-token -n ramblings -o jsonpath='{.data.ca\.crt}')
# Get your cluster server URL
CLUSTER_URL=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')
# Create a kubeconfig for GitHub Actions
cat <<EOF > github-kubeconfig.yaml
apiVersion: v1
kind: Config
clusters:
- name: github-cluster
cluster:
certificate-authority-data: ${CA_CERT}
server: ${CLUSTER_URL}
contexts:
- name: github-context
context:
cluster: github-cluster
namespace: ramblings
user: github-deployer
current-context: github-context
users:
- name: github-deployer
user:
token: ${TOKEN}
EOF
cat github-kubeconfig.yaml
Note: Generate a separate certificate for this but for development environment, this is ok.
Store the token in GitHub
Go to your repo: /settings/secrets/actions
Click New repository secret
Add this secret:
Name: KUBE_CONFIG
Value: The entire contents of github-kubeconfig.yaml
Add deployment stage to your pipeline
deploy:
needs: build-and-push
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up kubectl
uses: azure/setup-kubectl@v4
with:
version: 'latest'
- name: Configure kubectl
run: |
mkdir -p $HOME/.kube
echo "${{ secrets.KUBE_CONFIG }}" > $HOME/.kube/config
chmod 600 $HOME/.kube/config
- name: Verify kubectl access
run: |
kubectl version --client
kubectl get pods -n ramblings || echo "No pods yet"
- name: Deploy to Kubernetes
run: |
export IMAGE_TAG IMAGE_NAME
envsubst < kubernetes/k8s-deployment.yaml | kubectl apply -f -
- name: Wait for deployment rollout
run: |
kubectl rollout status deployment/ramblings-app -n ramblings --timeout=5m
- name: Get deployment status
run: |
echo "Deployment status:"
kubectl get deployment ramblings-app -n ramblings
echo ""
echo "Pods:"
kubectl get pods -n ramblings -l app=ramblings-app
And here’s corresponding k8s-deployment.yaml
file:
---
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: ramblings-app
namespace: ramblings
labels:
app: ramblings-app
spec:
replicas: 1
selector:
matchLabels:
app: ramblings-app
template:
metadata:
labels:
app: ramblings-app
spec:
containers:
- name: ramblings-app
image: ghcr.io/$IMAGE_NAME:$IMAGE_TAG
ports:
- containerPort: 8080
name: http
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "512Mi"
livenessProbe:
httpGet:
path: /deck-api/health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /deck-api/health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
Replace ramblings
with your application name.
See deployment pipeline for how IMAGE_NAME
and IMAGE_TAG
are generated. It should be done differently.