GitHub Actions doesn't have a first-party Alibaba Cloud integration the way it does for AWS or Azure, but that turns out not to matter — the aliyun CLI and kubectl cover everything you need, and the pipeline ends up just as clean. Here's the version I run for shipping a containerized service to Container Service for Kubernetes (ACK).

The RAM permissions, scoped correctly

Before touching the pipeline, create a RAM user dedicated to CI — never reuse your own credentials or a broad admin key:

aliyun ram CreateUser --UserName ci-deploy

aliyun ram AttachPolicyToUser \
  --PolicyType System \
  --PolicyName AliyunContainerRegistryFullAccess \
  --UserName ci-deploy

aliyun ram AttachPolicyToUser \
  --PolicyType System \
  --PolicyName AliyunCSFullAccess \
  --UserName ci-deploy

AliyunCSFullAccess is broader than most pipelines need — if you're running this in production, write a custom policy scoped to the specific ACK cluster ID rather than every cluster in the account.

The pipeline

name: Deploy to Alibaba Cloud

on:
  push:
    branches: [main]

env:
  REGISTRY: registry.ap-southeast-1.aliyuncs.com
  NAMESPACE: my-app-namespace
  IMAGE: my-app

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Log in to Container Registry
        run: |
          echo "${{ secrets.ACR_PASSWORD }}" | docker login \
            -u "${{ secrets.ACR_USERNAME }}" \
            --password-stdin ${{ env.REGISTRY }}

      - name: Build and push image
        run: |
          IMAGE_TAG=${{ env.REGISTRY }}/${{ env.NAMESPACE }}/${{ env.IMAGE }}:${{ github.sha }}
          docker build -t $IMAGE_TAG .
          docker push $IMAGE_TAG
          echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV

      - name: Set up kubectl for ACK
        uses: aliyun/ack-set-context-action@v1
        with:
          access-key-id: ${{ secrets.ALICLOUD_ACCESS_KEY }}
          access-key-secret: ${{ secrets.ALICLOUD_SECRET_KEY }}
          cluster-id: ${{ secrets.ACK_CLUSTER_ID }}

      - name: Roll out the new image
        run: |
          kubectl set image deployment/my-app \
            my-app=${{ env.IMAGE_TAG }} \
            -n production
          kubectl rollout status deployment/my-app -n production --timeout=120s

A few things doing real work in that last step:

  • kubectl rollout status blocks the job until the rollout actually succeeds or times out — without it, a broken image ships "successfully" and fails silently in production five minutes later.
  • Tagging the image with github.sha rather than latest means every deploy is traceable back to an exact commit, which matters the first time you need to answer "what changed" during an incident.

Handling secrets correctly

Store ACR_USERNAME, ACR_PASSWORD, ALICLOUD_ACCESS_KEY, ALICLOUD_SECRET_KEY, and ACK_CLUSTER_ID as encrypted repository or environment secrets in GitHub — never as plaintext in the workflow file, and never echoed to logs. If your repo supports GitHub Environments, gate the production environment behind a required reviewer so a merge to main doesn't deploy unattended.

Adding a staging gate

For anything beyond a side project, split the workflow so main deploys to staging automatically and production requires a manual promotion:

  deploy-production:
    needs: build-and-deploy
    runs-on: ubuntu-latest
    environment: production # requires manual approval if configured
    steps:
      - name: Promote staging image to production
        run: |
          kubectl set image deployment/my-app \
            my-app=${{ needs.build-and-deploy.outputs.image_tag }} \
            -n production

What this buys you over doing it by hand

The whole point of wiring this up is that a deploy becomes a git push instead of a person SSHing in and running commands from memory. Once this pipeline exists, the next useful step is layering in the cost and scaling practices I cover in the Kubernetes on Alibaba Cloud article — the deployment mechanics here stay the same regardless of how the cluster underneath is provisioned.