Getting Started with bc_gitops (Elixir)
View SourceThis guide walks you through setting up bc_gitops to manage OTP applications from an Elixir project.
What is GitOps?
GitOps is an operational framework where:
- Git is the source of truth - The desired state of your system is stored in a Git repository
- Declarative configuration - You describe what you want, not how to achieve it
- Automatic reconciliation - The system continuously compares desired vs actual state and takes corrective actions
bc_gitops brings this pattern to the BEAM ecosystem, allowing you to manage OTP applications the same way Flux or ArgoCD manage Kubernetes workloads.
Prerequisites
- Elixir 1.14+ / Erlang/OTP 25+
- Git installed and accessible in PATH
- A Git repository for storing application specifications
Installation
Add to your mix.exs:
def deps do
[
{:bc_gitops, "~> 0.4.0"}
]
endAdd bc_gitops to your applications:
def application do
[
extra_applications: [:logger, :bc_gitops]
]
endStep 1: Create Your GitOps Repository
Create a new Git repository to store your application specifications:
mkdir my-gitops-repo
cd my-gitops-repo
git init
mkdir apps
Step 2: Define an Application
Create a specification file for each application you want to manage. Let's create one for a hypothetical my_web_app:
mkdir apps/my_web_app
bc_gitops supports three configuration formats. For Elixir projects, YAML is recommended for its clean syntax:
Option A: YAML (app.yaml) - Recommended for Elixir
Note: Requires
yamerldependency. Add{:yamerl, "~> 0.10.0"}to your deps.
Create apps/my_web_app/app.yaml:
name: my_web_app
version: "1.0.0"
source:
type: hex
# Or for git:
# type: git
# url: https://github.com/myorg/my_web_app.git
# ref: v1.0.0
env:
port: 8080
pool_size: 10
health:
type: http
port: 8080
path: /health
interval: 30000
timeout: 5000
depends_on: []Option B: JSON (app.json)
Note: Requires OTP 27+ for native JSON support.
Create apps/my_web_app/app.json:
{
"name": "my_web_app",
"version": "1.0.0",
"source": {
"type": "hex"
},
"env": {
"port": 8080,
"pool_size": 10
},
"health": {
"type": "http",
"port": 8080,
"path": "/health",
"interval": 30000,
"timeout": 5000
},
"depends_on": []
}Option C: Erlang Terms (app.config)
Create apps/my_web_app/app.config:
#{
name => my_web_app,
version => <<"1.0.0">>,
source => #{type => hex},
env => #{port => 8080, pool_size => 10},
health => #{
type => http,
port => 8080,
path => <<"/health">>,
interval => 30000,
timeout => 5000
},
depends_on => []
}.Config File Priority
bc_gitops looks for config files in this order:
app.config(Erlang terms)app.yaml/app.yml(YAML)app.json(JSON)config.yaml/config.yml/config.json/config
Commit and push:
git add .
git commit -m "Add my_web_app specification"
git remote add origin https://github.com/myorg/my-gitops-repo.git
git push -u origin main
Step 3: Configure bc_gitops
Add configuration to your config/config.exs:
config :bc_gitops,
repo_url: "https://github.com/myorg/my-gitops-repo.git",
local_path: "/var/lib/bc_gitops",
branch: "main",
reconcile_interval: 60_000,
apps_dir: "apps",
runtime_module: :bc_gitops_runtime_defaultFor development, you might want a shorter interval and local path:
# config/dev.exs
config :bc_gitops,
local_path: "_bc_gitops",
reconcile_interval: 10_000Step 4: Start the Application
bc_gitops starts automatically when your application starts. It will:
- Clone the repository (or pull if already cloned)
- Parse all application specifications in
apps/ - Compare desired state with current state
- Deploy/upgrade/remove applications as needed
- Repeat every
reconcile_intervalmilliseconds
Step 5: Monitor and Operate
Check Status
{:ok, status} = :bc_gitops.status()
# %{status: :synced, last_commit: "abc123...", app_count: 5, healthy_count: 5}Trigger Manual Reconciliation
:ok = :bc_gitops.reconcile()View States
# Desired state (from git)
{:ok, desired} = :bc_gitops.get_desired_state()
# Current state (running)
{:ok, current} = :bc_gitops.get_current_state()
# Specific app
{:ok, app_state} = :bc_gitops.get_app_status(:my_web_app)Understanding Upgrades
Why Restart on Upgrade?
Version upgrades restart the application rather than hot-reloading because several things in OTP cannot be updated at runtime:
Application metadata -
Application.get_key/2reads from a cache populated at app start. Hot reload doesn't refresh this cache, so:vsn,:description, and custom keys return stale values.Plug/Phoenix routes - Routes are compiled into the router module. New routes added in an upgrade won't be available without restarting.
Supervision trees - New child specs, changed restart strategies, or restructured supervisors require the supervisor to restart.
Application config - While
Application.put_env/3can update values, many applications read config only at startup (e.g., instart/2).
For same-version code changes (e.g., tracking a master branch during development), hot reload works well because you're only updating module bytecode, not structural changes. Use :bc_gitops_hot_reload directly for this:
# Reload changed modules only
{:ok, modules} = :bc_gitops_hot_reload.reload_changed_modules(:my_app)Deployment Workflow
Once bc_gitops is running, your deployment workflow becomes:
- Make changes to your application specifications in git
- Commit and push to the tracked branch
- Wait for bc_gitops to detect changes (or trigger manually)
- Verify the deployment via status API or telemetry
# Update version in apps/my_web_app/app.config
# Change: version => <<"1.0.0">> to version => <<"1.1.0">>
git add .
git commit -m "Upgrade my_web_app to 1.1.0"
git push
bc_gitops will automatically detect the change and upgrade the application.
Telemetry Integration
bc_gitops emits telemetry events that you can subscribe to. Add this to your application supervisor:
defmodule MyApp.GitOpsTelemetry do
require Logger
def setup do
:telemetry.attach_many(
"gitops-logger",
[
[:bc_gitops, :reconcile, :stop],
[:bc_gitops, :deploy, :stop],
[:bc_gitops, :upgrade, :stop],
[:bc_gitops, :remove, :stop]
],
&handle_event/4,
nil
)
end
def handle_event([:bc_gitops, action, :stop], measurements, metadata, _config) do
Logger.info("GitOps #{action}: #{inspect(metadata)} (#{measurements[:duration]}ms)")
end
endCall MyApp.GitOpsTelemetry.setup() in your application start.
Custom Runtime (Optional)
For most use cases, the default runtime works out of the box. If you need custom deployment logic, implement the bc_gitops_runtime behaviour:
defmodule MyApp.GitOpsRuntime do
@behaviour :bc_gitops_runtime
@impl true
def deploy(app_spec) do
# Custom deployment logic
:bc_gitops_runtime_default.deploy(app_spec)
end
@impl true
def remove(app_name) do
:bc_gitops_runtime_default.remove(app_name)
end
@impl true
def upgrade(app_spec, old_version) do
# Custom upgrade logic (e.g., blue-green)
:bc_gitops_runtime_default.upgrade(app_spec, old_version)
end
@impl true
def reconfigure(app_spec) do
:bc_gitops_runtime_default.reconfigure(app_spec)
end
@impl true
def get_current_state do
:bc_gitops_runtime_default.get_current_state()
end
endThen configure it:
config :bc_gitops, runtime_module: MyApp.GitOpsRuntimeNext Steps
- Read the API Reference for the full API
- Read the Runtime Implementation Guide for advanced deployment strategies
- Configure Git authentication for private repositories