Kamal: How to integrate with GitHub Actions using multiple destinations

eduardinni

Eduardo Lomeli

Posted on November 10, 2024

Kamal: How to integrate with GitHub Actions using multiple destinations

The recently introduced Kamal tool is very powerful and easy to use, you can achieve full deployment automation when it is integrated into the CI/CD pipelines. I'm going to walk you through how to run it automatically on GitHub Actions with multiple destinations (eg. staging and production).

Pipeline workflows:

Event Action
Push to main (PR merged) Desploy to Staging
Publish release (Tag created) Deploy to Production

Requirements

  • Setup your app secrets in your repo for both environments (staging and production) in this tutorial they are prefixed by STG_ and PROD_.
  • Make sure you have created SSH_PRIVATE_KEY, KAMAL_REGISTRY_USERNAME and KAMAL_REGISTRY_PASSWORD secrets.

GitHub Action files

.github/
├─ workflows/
   ├─ job-deploy-kamal.yml
   ├─ workflow-deploy-production.yml
   ├─ workflow-deploy-staging.yml
Enter fullscreen mode Exit fullscreen mode
  • job-deploy-kamal.yml is the shared job where Kamal runs
  • workflow-deploy-production.yml acts as a trigger for production
  • workflow-deploy-staging.yml acts as a trigger for staging

job-deploy-kamal.yml

name: Job - Deploy to Kamal

on:
  workflow_call:
    inputs:
      kamal-destination:
        required: true
        type: string
    secrets:
      KAMAL_REGISTRY_USERNAME:
        required: true
      KAMAL_REGISTRY_PASSWORD:
        required: true
      SSH_PRIVATE_KEY:
        required: true
      DATABASE_URL:
        required: true
      QUEUE_DATABASE_URL:
        required: true
      SECRET_KEY_BASE:
        required: true

env:
  DOCKER_BUILDKIT: 1
  KAMAL_REGISTRY_USERNAME: ${{ secrets.KAMAL_REGISTRY_USERNAME }}
  KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
  # App secrets
  DATABASE_URL: ${{ secrets.DATABASE_URL }}
  QUEUE_DATABASE_URL: ${{ secrets.QUEUE_DATABASE_URL }}
  SECRET_KEY_BASE: ${{ secrets.SECRET_KEY_BASE }}

jobs:
  deploy:
    name: kamal deploy
    timeout-minutes: 10
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true

      - uses: webfactory/ssh-agent@v0.9.0
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.KAMAL_REGISTRY_USERNAME }}
          password: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - run: gem install kamal

      - run: kamal lock release -d ${{ inputs.kamal-destination }}

      - run: kamal deploy -d ${{ inputs.kamal-destination }}
Enter fullscreen mode Exit fullscreen mode

workflow-deploy-staging.yml

name: Workflow - Deployment (staging)

on:
  push:
    branches:
      - main

concurrency:
  group: deployment-staging
  cancel-in-progress: false

jobs:
  deploy:
    uses: ./.github/workflows/job-deploy-kamal.yml
    with:
      kamal-destination: staging
    secrets:
      KAMAL_REGISTRY_USERNAME: ${{ secrets.KAMAL_REGISTRY_USERNAME }}
      KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
      SSH_PRIVATE_KEY: ${{ secrets.STG_SSH_PRIVATE_KEY }}
      DATABASE_URL: ${{ secrets.STG_DATABASE_URL }}
      QUEUE_DATABASE_URL: ${{ secrets.STG_QUEUE_DATABASE_URL }}
      SECRET_KEY_BASE: ${{ secrets.STG_SECRET_KEY_BASE }}
Enter fullscreen mode Exit fullscreen mode

workflow-deploy-production.yml

name: Workflow - Deployment (production)

on:
  release:
    types: [published]

concurrency:
  group: deployment-production
  cancel-in-progress: false

jobs:
  deploy:
    uses: ./.github/workflows/job-deploy-kamal.yml
    with:
      kamal-destination: production
    secrets:
      KAMAL_REGISTRY_USERNAME: ${{ secrets.KAMAL_REGISTRY_USERNAME }}
      KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
      SSH_PRIVATE_KEY: ${{ secrets.PROD_SSH_PRIVATE_KEY }}
      DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}
      QUEUE_DATABASE_URL: ${{ secrets.PROD_QUEUE_DATABASE_URL }}
      SECRET_KEY_BASE: ${{ secrets.PROD_SECRET_KEY_BASE }}
Enter fullscreen mode Exit fullscreen mode

Kamal files

.kamal/
├─ secrets-common
config/
├─ deploy.production.yml
├─ deploy.staging.yml
├─ deploy.yml
Enter fullscreen mode Exit fullscreen mode

secrets-common

KAMAL_REGISTRY_USERNAME=$KAMAL_REGISTRY_USERNAME
KAMAL_REGISTRY_PASSWORD=$KAMAL_REGISTRY_PASSWORD
DATABASE_URL=$DATABASE_URL
QUEUE_DATABASE_URL=$QUEUE_DATABASE_URL
SECRET_KEY_BASE=$SECRET_KEY_BASE
Enter fullscreen mode Exit fullscreen mode

deploy.yml

service: myapp
image: myorg/myapp

registry:
  username:
    - KAMAL_REGISTRY_USERNAME
  password:
    - KAMAL_REGISTRY_PASSWORD

proxy: 
  ssl: true
  app_port: 3000

builder:
  arch: amd64

volumes:
  - "app_storage:/app/storage"
Enter fullscreen mode Exit fullscreen mode

deploy.production.yml

servers:
  web:
    - 192.168.0.1
  job:
    hosts:
      - 192.168.0.1
    cmd: bin/jobs

proxy: 
  host: api.myapp.com

env:
  clear:
    RAILS_LOG_TO_STDOUT: 1
    WEB_CONCURRENCY: 2
    JOB_CONCURRENCY: 1
    SOLID_QUEUE_IN_PUMA: true
  secret:
    - DATABASE_URL
    - QUEUE_DATABASE_URL
    - SECRET_KEY_BASE
Enter fullscreen mode Exit fullscreen mode

deploy.staging.yml

servers:
  web:
    - 192.168.0.2
  job:
    hosts:
      - 192.168.0.2
    cmd: bin/jobs

proxy: 
  host: api-staging.myapp.com

env:
  clear:
    RAILS_LOG_TO_STDOUT: 1
    WEB_CONCURRENCY: 0
    JOB_CONCURRENCY: 1
    SOLID_QUEUE_IN_PUMA: true
  secret:
    - DATABASE_URL
    - QUEUE_DATABASE_URL
    - SECRET_KEY_BASE
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
eduardinni
Eduardo Lomeli

Posted on November 10, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related