|
1 | 1 | # Castle |
2 | 2 |
|
3 | | -Runtime support for hot-code upgrades. |
| 3 | +Castle adds hot-code upgrade support to Elixir releases. It manages upgrades on |
| 4 | +the running node and resolves the target release's runtime configuration before |
| 5 | +OTP installs it. |
4 | 6 |
|
5 | | -`Castle` provides runtime support for hot-code upgrades. In particular, it generates a |
6 | | -valid `sys.config` from `runtime.exs` and/or other [Config Providers](https://hexdocs.pm/elixir/main/Config.Provider.html) |
7 | | -prior to both boot and hot-code upgrade. |
| 7 | +[Forecastle](https://hexdocs.pm/forecastle) handles the build-time work. Castle |
| 8 | +includes it as a build-time dependency. |
8 | 9 |
|
9 | | -It relies on [Forecastle](https://hexdocs.pm/forecastle/readme.html) for build-time release generation |
10 | | -and brings it in as a build-time dependency. |
| 10 | +## Requirements |
| 11 | + |
| 12 | +- Elixir 1.18 or later. |
| 13 | +- A Unix release built with its own ERTS. Castle refuses releases built with |
| 14 | + `include_erts: false`. |
| 15 | +- An external supervisor such as systemd, Docker, Kubernetes or runit if an |
| 16 | + upgrade restarts the emulator. |
| 17 | + |
| 18 | +Use Castle and Forecastle from the same release series. Castle 1.x expects the |
| 19 | +release layout produced by Forecastle 1.x. |
11 | 20 |
|
12 | 21 | ## Installation |
13 | 22 |
|
14 | | -The package can be installed by adding `castle` to your list of dependencies in |
15 | | -`mix.exs`. For projects that don't define a release, but use the `appup` compiler, |
16 | | -it's sufficient to bring `Castle` in as a build-time dependency: |
| 23 | +Add Castle to applications that build a release: |
17 | 24 |
|
18 | 25 | ```elixir |
19 | 26 | def deps do |
20 | 27 | [ |
21 | | - {:castle, "~> 0.3.0", runtime: false} |
| 28 | + {:castle, "~> 1.0"} |
22 | 29 | ] |
23 | 30 | end |
24 | 31 | ``` |
25 | 32 |
|
26 | | -For projects that _do_ define one or more releases, `Castle` should be brought in |
27 | | -as a runtime dependency: |
| 33 | +An application that only uses Forecastle's appup compiler can keep Castle out |
| 34 | +of its runtime release: |
28 | 35 |
|
29 | 36 | ```elixir |
30 | 37 | def deps do |
31 | 38 | [ |
32 | | - {:castle, "~> 0.3.0"} |
| 39 | + {:castle, "~> 1.0", runtime: false} |
33 | 40 | ] |
34 | 41 | end |
35 | 42 | ``` |
36 | 43 |
|
37 | | -`Castle` brings in `Forecastle` as a build-time dependency. |
| 44 | +## Project setup |
38 | 45 |
|
39 | | -## Integration |
| 46 | +Point the project at its appup file and add the appup compiler: |
| 47 | + |
| 48 | +```elixir |
| 49 | +def project do |
| 50 | + [ |
| 51 | + appup: "appup.exs", |
| 52 | + compilers: Mix.compilers() ++ [:appup] |
| 53 | + ] |
| 54 | +end |
| 55 | +``` |
40 | 56 |
|
41 | | -Build-time integration is done via `Forecastle` and more details can be found in its |
42 | | -documentation but, in summary, it will integrate into your release process via the |
43 | | -release assembly process. In particular, it requires that that the `Forecastle.pre_assemble/1` |
44 | | -and `Forecastle.post_assemble/1` functions are placed around the `:assemble` step, e.g.: |
| 57 | +Define each release lazily and pass its options to `Castle.customize/1`: |
45 | 58 |
|
46 | 59 | ```elixir |
47 | 60 | defp releases do |
48 | 61 | [ |
49 | | - myapp: [ |
50 | | - include_executables_for: [:unix], |
51 | | - steps: [&Forecastle.pre_assemble/1, :assemble, &Forecastle.post_assemble/1, :tar] |
52 | | - ] |
| 62 | + my_app: fn -> |
| 63 | + [include_executables_for: [:unix]] |
| 64 | + |> Castle.customize() |
| 65 | + end |
53 | 66 | ] |
54 | 67 | end |
55 | 68 | ``` |
56 | 69 |
|
57 | | -## Release Management |
58 | | - |
59 | | -The script in the `bin` folder supports some extra commands to manage upgrades. |
60 | | -Releases, in their tarred-gzipped form, should first be copied to the `releases` |
61 | | -subfolder on the target system. The following commands can be used to manage |
62 | | -them: |
63 | | - |
64 | | - - `releases` - Lists the releases on the system and their status. Status can |
65 | | - be one of the following: |
66 | | - - permanent - the release the system will boot into on next restart. |
67 | | - - current - if it exists, represents the current running release. Will be |
68 | | - different from the permanent version if a new release has been installed |
69 | | - but not yet committed. If no version is listed as current, the permanent |
70 | | - version is the currently running version. |
71 | | - - old - if it exists, a previously installed version. |
72 | | - - unpacked - an unpacked version, but not yet installed. |
73 | | - - `unpack <vsn>` - Unpacks the release called `<name>-<vsn>.tar.gz`. |
74 | | - - `install <vsn>` - Installs the new release. This makes the release the |
75 | | - current one, but not yet the permanent one. Prior to running the relup, |
76 | | - `Castle` generates the version specific `sys.config` for the new version. |
77 | | - - `commit <vsn>` - Makes the specified release the one the permanent one. |
78 | | - - `remove <vsn>` - Remove an old version from the filesystem. Any files |
79 | | - shared with remaining releases are left untouched. |
80 | | - |
81 | | -## The Appup Compiler |
82 | | - |
83 | | -You are responsible for writing the [appup](https://www.erlang.org/doc/man/appup.html) |
84 | | -scripts for your application, but `Castle` will copy the appup into the `ebin` folder |
85 | | -for you. The steps are as follows: |
86 | | - |
87 | | -1. Write a file, in _Elixir form_, describing the application upgrade. e.g.: |
88 | | - ```elixir |
89 | | - # You can call the file what you like, e.g. appup.ex, |
90 | | - # but you should # keep it away from the compiler paths. |
91 | | - { |
92 | | - '0.1.1', |
93 | | - [ |
94 | | - {'0.1.0', [ |
95 | | - {:update, MyApp.Server, {:advanced, []}} |
96 | | - ]} |
97 | | - ], |
98 | | - [ |
99 | | - {'0.1.0', [ |
100 | | - {:update, MyApp.Server, {:advanced, []}} |
101 | | - ]} |
102 | | - ] |
103 | | - } |
104 | | - ``` |
105 | | - This file will typically be checked in to SCM. |
106 | | -2. Add the appup file to the Mix project definition in mix.exs and add the |
107 | | - `:appup` compiler. |
108 | | - ```elixir |
109 | | - # Mix.exs |
110 | | - def project do |
111 | | - [ |
112 | | - appup: "appup.ex", # Relative to the project root. |
113 | | - compilers: Mix.compilers() ++ [:appup] |
114 | | - ] |
115 | | - end |
116 | | - ``` |
117 | | - |
118 | | -## Relup Generation |
119 | | - |
120 | | -Castle contains a mix task, `castle.relup`, that simplifies the generation of |
121 | | -the relup file. Assuming you have two _unpacked_ releases e.g. `0.1.0` and `0.1.1` |
122 | | -and you wish to generate a relup between them: |
| 70 | +The function wrapper is required. Mix loads `mix.exs` before dependencies have |
| 71 | +been compiled during commands such as `mix deps.get`. It evaluates the release |
| 72 | +function later, when Castle is available. |
| 73 | + |
| 74 | +`Castle.customize/1` adds Forecastle's assembly steps around `:assemble`. When |
| 75 | +`:steps` is omitted, it uses `[:assemble, :tar]`. An explicit steps list is |
| 76 | +preserved; Castle warns if it has no `:tar` step. |
| 77 | + |
| 78 | +You can also provide `rel/env.sh.eex`. Forecastle keeps its contents and appends |
| 79 | +the launcher setup Castle needs. |
| 80 | + |
| 81 | +## Appups and relups |
| 82 | + |
| 83 | +Write an appup for each application whose code changes during a hot upgrade. |
| 84 | +The appup file uses Erlang terms written in Elixir syntax: |
| 85 | + |
| 86 | +```elixir |
| 87 | +{ |
| 88 | + ~c"1.1.0", |
| 89 | + [ |
| 90 | + {~c"1.0.0", [{:update, MyApp.Server, {:advanced, []}}]} |
| 91 | + ], |
| 92 | + [ |
| 93 | + {~c"1.0.0", [{:update, MyApp.Server, {:advanced, []}}]} |
| 94 | + ] |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +Generate a relup between assembled releases: |
| 99 | + |
| 100 | +```shell |
| 101 | +mix forecastle.relup \ |
| 102 | + --target _build/prod/rel/my_app/releases/1.1.0/my_app \ |
| 103 | + --fromto _build/prod/rel/my_app/releases/1.0.0/my_app |
| 104 | +``` |
| 105 | + |
| 106 | +The task writes `relup` to the project root by default. Leave it there for the |
| 107 | +next release build to package. Use `--hot` to require a hot transition or |
| 108 | +`--restart` to force a one-stage emulator restart. |
| 109 | + |
| 110 | +## Managing releases |
| 111 | + |
| 112 | +Build the new release, then copy `<name>-<vsn>.tar.gz` into the running |
| 113 | +deployment's `releases` directory. Manage it with `bin/castle`: |
123 | 114 |
|
124 | 115 | ```shell |
125 | | -> mix castle.relup --target myapp/releases/0.1.1/myapp --fromto myapp/releases/0.1.0/myapp |
| 116 | +# Show known releases and their status. |
| 117 | +my_app/bin/castle releases |
| 118 | + |
| 119 | +# Check whether this node can be upgraded. Success prints nothing. |
| 120 | +my_app/bin/castle upgradable |
| 121 | + |
| 122 | +# Stage, install and make version 1.1.0 permanent. |
| 123 | +my_app/bin/castle unpack 1.1.0 |
| 124 | +my_app/bin/castle install 1.1.0 |
| 125 | +my_app/bin/castle commit |
| 126 | + |
| 127 | +# Remove a version that is no longer needed. |
| 128 | +my_app/bin/castle remove 1.0.0 |
126 | 129 | ``` |
127 | 130 |
|
128 | | -If the generated file is in the project root, it will be copied during |
129 | | -post-assembly to the release. |
| 131 | +Release statuses are: |
| 132 | + |
| 133 | +- `permanent`: the version used on the next ordinary restart. |
| 134 | +- `current`: the running version, installed but not committed. |
| 135 | +- `old`: a superseded version that can be removed. |
| 136 | +- `unpacked`: a staged version, or one returned to that state after a failed or |
| 137 | + rolled-back install. |
| 138 | + |
| 139 | +`install` resolves the target version's config providers in a temporary VM |
| 140 | +running that version's code. It then asks OTP to install the release. The |
| 141 | +version remains provisional until `commit` writes it as permanent. If a |
| 142 | +provisional hot upgrade fails or the service restarts, the previous permanent |
| 143 | +version boots again. |
| 144 | + |
| 145 | +For a relup containing `restart_emulator`, `install` waits across the restart |
| 146 | +until the target version has finished booting. The external supervisor must |
| 147 | +restart the process. Castle does not support OTP's two-stage |
| 148 | +`restart_new_emulator` transition. |
| 149 | + |
| 150 | +## Limitations |
| 151 | + |
| 152 | +- Windows launchers are not supported. |
| 153 | +- `RELDIR` and the SASL `releases_dir` option are not supported yet. Castle and |
| 154 | + `:release_handler` must use the same release directory. See |
| 155 | + [issue #23](https://github.com/ausimian/castle/issues/23). |
| 156 | +- Castle serialises installs within one Erlang node. Do not run Castle from a |
| 157 | + second VM against the same deployment. |
0 commit comments