Docker & CI/CD

Container deployment

Build the image:

# Dockerfile
FROM python:3.12-slim
RUN pip install metapod-probes[all]
WORKDIR /app
ENTRYPOINT ["metapod"]
docker build -t metapod .

Run with mounted config and output:

docker run --rm \
  -v $(pwd)/config.yaml:/app/config.yaml \
  -v $(pwd)/output:/app/output \
  metapod run "oracle.metadata.*" --config /app/config.yaml

Docker Compose

# docker-compose.yml
version: "3.8"
services:
  metapod:
    build: .
    volumes:
      - ./config.yaml:/app/config.yaml:ro
      - ./output:/app/output
    environment:
      - ORACLE_PASSWORD=${ORACLE_PASSWORD}
    command: run "oracle.metadata.*" --config /app/config.yaml --delta

  # Optional: daemon mode
  metapod-daemon:
    build: .
    ports:
      - "9090:9090"
    volumes:
      - ./config.yaml:/app/config.yaml:ro
    command: serve --config /app/config.yaml --port 9090

GitHub Actions

# .github/workflows/metadata.yml
name: Metadata Extraction
on:
  schedule:
    - cron: "0 6 * * 1-5"   # Weekdays at 6 AM
  workflow_dispatch:          # Manual trigger

jobs:
  extract:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install metapod
        run: pip install metapod-probes[oracle,azure]

      - name: Extract metadata
        env:
          ORACLE_PASSWORD: ${{ secrets.ORACLE_PASSWORD }}
        run: metapod run "oracle.metadata.*" --config config.yaml --delta

      - name: Upload artifacts
        uses: actions/upload-artifact@v4
        with:
          name: metadata-${{ github.run_number }}
          path: output/
          retention-days: 30

      # Optional: push to catalog
      - name: Push to Dataflix
        if: always()
        env:
          DATAFLIX_API_KEY: ${{ secrets.DATAFLIX_API_KEY }}
        run: |
          metapod push \
            --target https://dataflix.example.com \
            --project production \
            --api-key $DATAFLIX_API_KEY

GitLab CI

# .gitlab-ci.yml
metadata-extraction:
  image: python:3.12-slim
  stage: extract
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"
  script:
    - pip install metapod-probes[oracle]
    - metapod run "oracle.metadata.*" --config config.yaml --delta
  artifacts:
    paths:
      - output/
    expire_in: 30 days
  variables:
    ORACLE_PASSWORD: $ORACLE_PASSWORD

Kubernetes CronJob

# k8s/cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: metapod-extraction
spec:
  schedule: "0 6 * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: metapod
              image: metapod:latest
              command:
                - metapod
                - run
                - "oracle.metadata.*"
                - --config
                - /config/config.yaml
                - --delta
                - --output
                - /output
              volumeMounts:
                - name: config
                  mountPath: /config
                - name: output
                  mountPath: /output
              envFrom:
                - secretRef:
                    name: metapod-secrets
          volumes:
            - name: config
              configMap:
                name: metapod-config
            - name: output
              persistentVolumeClaim:
                claimName: metapod-output
          restartPolicy: OnFailure

Credential management

Never put credentials in config files. Use:

  • Environment variables: ORACLE_PASSWORD, AZURE_CLIENT_SECRET

  • CI/CD secrets: GitHub Secrets, GitLab CI Variables, Azure Key Vault

  • .env file (local dev only, add to .gitignore)

  • Kubernetes secrets: mounted as env vars or files

Example .env:

# .env (add to .gitignore!)
ORACLE_PASSWORD=my-secret
AZURE_TENANT_ID=xxxx
AZURE_CLIENT_ID=xxxx
AZURE_CLIENT_SECRET=xxxx

metapod loads .env automatically via python-dotenv.