# Elixir Part 4: Ecosystem, Production & Deployment ## Ash Framework — Declarative Resource Modeling For production/substantial projects, Ash provides a declarative, resource-oriented approach that eliminates boilerplate while remaining extensible. ```elixir # mix.exs deps {:ash, "~> 3.0"}, {:ash_postgres, "~> 2.0"}, # Ecto/Postgres data layer {:ash_phoenix, "~> 2.0"}, # Phoenix integration {:ash_graphql, "~> 1.0"}, # Auto-generated GraphQL API {:ash_json_api, "~> 1.0"}, # Auto-generated JSON:API ``` ### Resource Definition ```elixir defmodule MyApp.Blog.Post do use Ash.Resource, domain: MyApp.Blog, data_layer: AshPostgres.DataLayer postgres do table "posts" repo MyApp.Repo end attributes do uuid_primary_key :id attribute :title, :string, allow_nil?: false attribute :body, :string, allow_nil?: false attribute :status, :atom, constraints: [one_of: [:draft, :published, :archived]], default: :draft timestamps() end relationships do belongs_to :author, MyApp.Accounts.User has_many :comments, MyApp.Blog.Comment end actions do defaults [:read, :destroy] create :create do accept [:title, :body] change relate_actor(:author) end update :publish do change set_attribute(:status, :published) end end policies do policy action_type(:read) do authorize_if always() end policy action_type([:create, :update, :destroy]) do authorize_if actor_attribute_equals(:role, :admin) end end end ``` ### Domain Definition ```elixir defmodule MyApp.Blog do use Ash.Domain resources do resource MyApp.Blog.Post resource MyApp.Blog.Comment end end ``` ### Using Ash Resources ```elixir # Create MyApp.Blog.Post |> Ash.Changeset.for_create(:create, %{title: "Hello", body: "World"}, actor: current_user) |> Ash.create!() # Read with filters MyApp.Blog.Post |> Ash.Query.filter(status == :published) |> Ash.Query.sort(inserted_at: :desc) |> Ash.read!() # Custom action post |> Ash.Changeset.for_update(:publish) |> Ash.update!() ``` **When to use Ash vs plain Phoenix:** Ash excels when you need consistent authorization, multi-tenancy, auto-generated APIs, or complex domain logic. For simple CRUD apps or learning, plain Phoenix contexts are fine. --- ## Jido — Multi-Agent Orchestration Framework Jido (~> 2.1) is the premier Elixir framework for building and managing agents of all types — not just LLMs. Vision: 10,000 agents per user on the BEAM. ```elixir # mix.exs deps {:jido, "~> 2.1"}, {:jido_ai, "~> 0.x"}, # AI/LLM integration {:jido_action, "~> 0.x"}, # Composable actions {:jido_signal, "~> 0.x"}, # CloudEvents-based messaging ``` ### Core Concepts **Agents are pure functional structs** — immutable, no side effects in the agent itself. Side effects are described as Directives. ```elixir defmodule MyAgent do use Jido.Agent, name: "my_agent", description: "Does useful things", actions: [FetchData, ProcessData, NotifyUser], schema: [ model: [type: :string, default: "gpt-4"], temperature: [type: :float, default: 0.7] ] end # Create and command {:ok, agent} = MyAgent.new() {agent, directives} = MyAgent.cmd(agent, %Signal{type: "task.assigned", data: %{task: "analyze"}}) # directives are typed effects: Emit, Spawn, Schedule, Stop ``` ### Actions — Composable Units Actions are the building blocks. Each has a schema, validates input, and returns output. ```elixir defmodule FetchData do use Jido.Action, name: "fetch_data", description: "Fetches data from an API", schema: [ url: [type: :string, required: true], timeout: [type: :integer, default: 5000] ] @impl true def run(params, _context) do case Req.get(params.url, receive_timeout: params.timeout) do {:ok, %{status: 200, body: body}} -> {:ok, %{data: body}} {:ok, %{status: status}} -> {:error, "HTTP #{status}"} {:error, reason} -> {:error, reason} end end end # Actions expose themselves as AI tools FetchData.to_tool() # Returns tool spec for LLM function calling ``` ### Signals — CloudEvents v1.0.2 Messaging ```elixir signal = %Jido.Signal{ type: "task.completed", source: "agent:worker_1", data: %{result: "analysis complete"}, subject: "task:123" } ``` ### Directives — Typed Effect Descriptions Agents don't perform side effects directly. They return Directives describing what should happen: | Directive | Effect | |-----------|--------| | `Emit` | Send a signal to another agent/system | | `Spawn` | Create a new child agent | | `Schedule` | Schedule a future action | | `Stop` | Terminate the agent | ### AgentServer — Runtime Process `AgentServer` wraps an agent in a GenServer for real-time operation: ```elixir {:ok, pid} = Jido.AgentServer.start_link(agent: MyAgent, id: "worker-1") Jido.AgentServer.signal(pid, %Signal{type: "task.start", data: %{...}}) ``` ### When to Use Jido - Multi-agent systems where agents communicate via signals - LLM-powered agents that need tool calling (via `to_tool/0`) - Systems requiring parent-child agent hierarchies - Workflows with complex state machines (Jido includes FSM strategy) - Any scenario targeting high agent density (thousands per node) --- ## OTP Releases & Docker ### OTP Release ```elixir # mix.exs def project do [ app: :my_app, releases: [ my_app: [ include_executables_for: [:unix], steps: [:assemble, :tar] ] ] ] end ``` ### Docker Multistage Build ```dockerfile # Build stage FROM hexpm/elixir:1.19.5-erlang-27.3-debian-bookworm-20240904 AS build RUN apt-get update && apt-get install -y build-essential git WORKDIR /app ENV MIX_ENV=prod COPY mix.exs mix.lock ./ RUN mix deps.get --only $MIX_ENV && mix deps.compile COPY config/config.exs config/prod.exs config/ COPY lib lib COPY priv priv COPY assets assets RUN mix assets.deploy && mix compile && mix release # Runtime stage FROM debian:bookworm-slim RUN apt-get update && apt-get install -y libstdc++6 openssl libncurses5 locales RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen ENV LANG=en_US.UTF-8 WORKDIR /app COPY --from=build /app/_build/prod/rel/my_app ./ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD curl -f http://localhost:4000/health || exit 1 CMD ["bin/server"] ``` --- ## Distributed Erlang & Clustering ```elixir # libcluster config (config/runtime.exs) config :libcluster, topologies: [ local: [ strategy: Cluster.Strategy.Gossip ], # OR for Docker/K8s: k8s: [ strategy: Cluster.Strategy.Kubernetes, config: [ mode: :hostname, kubernetes_namespace: "default", kubernetes_selector: "app=my_app" ] ] ] # Distributed PubSub (built into Phoenix) Phoenix.PubSub.broadcast(MyApp.PubSub, "events", {:new_order, order}) Phoenix.PubSub.subscribe(MyApp.PubSub, "events") # Horde for distributed supervisor/registry {:horde, "~> 0.9"} ``` --- ## Monitoring & Observability ### PromEx — Prometheus Metrics for Elixir ```elixir # mix.exs {:prom_ex, "~> 1.9"} # lib/my_app/prom_ex.ex defmodule MyApp.PromEx do use PromEx, otp_app: :my_app @impl true def plugins do [ PromEx.Plugins.Application, PromEx.Plugins.Beam, {PromEx.Plugins.Phoenix, router: MyAppWeb.Router, endpoint: MyAppWeb.Endpoint}, {PromEx.Plugins.Ecto, repos: [MyApp.Repo]}, PromEx.Plugins.Oban ] end @impl true def dashboards do [ {:prom_ex, "application.json"}, {:prom_ex, "beam.json"}, {:prom_ex, "phoenix.json"}, {:prom_ex, "ecto.json"} ] end end ``` ### Full Observability Stack (BEAMOps) From "Engineering Elixir Applications": | Component | Role | |-----------|------| | **PromEx** | Expose BEAM/Phoenix/Ecto metrics as Prometheus endpoints | | **Prometheus** | Scrape and store time-series metrics | | **Grafana** | Dashboards and alerting | | **Loki** | Log aggregation (like Prometheus but for logs) | | **Promtail/Alloy** | Ship logs from containers to Loki | **Health checks + automatic rollback:** Docker `HEALTHCHECK` triggers rollback if the new container fails health checks within the start period. Use `docker system prune` automation to prevent disk bloat. --- ## CI/CD Pipeline (GitHub Actions) ```yaml name: Elixir CI on: [push, pull_request] jobs: test: runs-on: ubuntu-latest services: db: image: postgres:16 env: POSTGRES_PASSWORD: postgres ports: ['5432:5432'] steps: - uses: actions/checkout@v4 - uses: erlef/setup-beam@v1 with: elixir-version: '1.19.5' otp-version: '27.3' - run: mix deps.get - run: mix compile --warnings-as-errors - run: mix format --check-formatted - run: mix credo --strict - run: mix test - run: mix deps.unlock --check-unused ``` --- ## Interop — Elixir as Orchestrator ### Python via Ports/Erlport ```elixir # Using erlport for bidirectional Python calls {:ok, pid} = :python.start([{:python_path, ~c"./python_scripts"}]) result = :python.call(pid, :my_module, :my_function, [arg1, arg2]) :python.stop(pid) ``` ### Rust via Rustler NIFs ```elixir # mix.exs {:rustler, "~> 0.34"} # lib/my_nif.ex defmodule MyApp.NativeSort do use Rustler, otp_app: :my_app, crate: "native_sort" # NIF stubs — replaced at load time by Rust implementations def sort(_list), do: :erlang.nif_error(:nif_not_loaded) end ``` ### System Commands via Ports ```elixir # One-shot command {output, 0} = System.cmd("ffmpeg", ["-i", input, "-o", output]) # Long-running port port = Port.open({:spawn, "python3 worker.py"}, [:binary, :exit_status]) send(port, {self(), {:command, "process\n"}}) receive do {^port, {:data, data}} -> handle_response(data) end ``` --- ## Cortex Deployment Story ### Current State - Cortex runs Ubuntu 24.04 with Caddy as web server - Elixir is **not yet installed** — needs `asdf` or direct install - Docker is **planned but not installed** — needed for containerized Elixir apps - Symbiont (Python) is the current orchestrator — Elixir will gradually take over ### Installation Plan for Cortex ```bash # Option 1: asdf (recommended — manages multiple versions) git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.14.0 echo '. "$HOME/.asdf/asdf.sh"' >> ~/.bashrc asdf plugin add erlang asdf plugin add elixir asdf install erlang 27.3 asdf install elixir 1.19.5-otp-27 asdf global erlang 27.3 asdf global elixir 1.19.5-otp-27 # Option 2: Direct from Erlang Solutions wget https://packages.erlang-solutions.com/erlang-solutions_2.0_all.deb dpkg -i erlang-solutions_2.0_all.deb apt-get update && apt-get install esl-erlang elixir ``` ### BEAMOps Principles for Cortex From "Engineering Elixir Applications" — the deployment philosophy: 1. **Environment Integrity** — identical builds dev/staging/prod via Docker + releases 2. **Infrastructure as Code** — Caddy config, systemd units, backup scripts all version-controlled 3. **OTP Releases** — self-contained, no runtime deps, `bin/my_app start` 4. **Distributed Erlang** — nodes discover each other, share state via PubSub, global registry 5. **Instrumentation** — PromEx + Prometheus + Grafana + Loki for full observability 6. **Health Checks + Rollbacks** — Docker health checks trigger automatic rollback on failed deploys 7. **Zero-downtime deploys** — rolling updates via Docker Swarm or `mix release` hot upgrades --- ## Anti-Patterns to Avoid ### Process Anti-Patterns - **GenServer as code organization** — don't wrap pure functions in a GenServer. Use modules. - **Agent for complex state** — if you need more than get/update, use GenServer directly. - **Spawning unsupervised processes** — always use `Task.Supervisor` or link to a supervisor. ### Code Anti-Patterns - **Primitive obsession** — use structs, not bare maps, for domain concepts. - **Boolean parameters** — use atoms or keyword options: `format: :json` not `json: true`. - **Large modules** — split by concern, not by entity type. Domain logic, web layer, workers. - **String keys in internal maps** — use atoms internally, strings only at boundaries (JSON, forms). ### Design Anti-Patterns - **Monolithic contexts** — Phoenix contexts should be small, focused. Split `Accounts` from `Authentication`. - **God GenServer** — one process handling all state for the app. Distribute responsibility. - **Synchronous calls to slow services** — use `Task.async` + `Task.await` with timeouts. --- ## Quick Recipes ### HTTP Request with Req ```elixir Req.get!("https://api.example.com/data", headers: [{"authorization", "Bearer #{token}"}], receive_timeout: 15_000 ).body ``` ### JSON Encode/Decode ```elixir Jason.encode!(%{name: "Michael", role: :admin}) Jason.decode!(~s({"name": "Michael"}), keys: :atoms) ``` ### Background Job with Oban ```elixir defmodule MyApp.Workers.EmailWorker do use Oban.Worker, queue: :mailers, max_attempts: 3 @impl true def perform(%Oban.Job{args: %{"to" => to, "template" => template}}) do MyApp.Mailer.send(to, template) end end # Enqueue %{to: "user@example.com", template: "welcome"} |> MyApp.Workers.EmailWorker.new(schedule_in: 60) |> Oban.insert!() ``` --- ## Resources - [Elixir Official Docs](https://hexdocs.pm/elixir/) — always check 1.19.5 version - [Ash Framework Docs](https://hexdocs.pm/ash/) — resource-oriented patterns - [Phoenix HexDocs](https://hexdocs.pm/phoenix/) — web framework - [Jido Docs](https://hexdocs.pm/jido/) — multi-agent orchestration - [Elixir Forum](https://elixirforum.com/) — community Q&A - "Elixir in Action" by Saša Jurić — deep BEAM/OTP understanding (note: covers 1.15, check breaking changes) - "Engineering Elixir Applications" by Fairholm & D'Lacoste — BEAMOps deployment patterns