Процесс развёртывания программ обычно состоит из множества этапов. Некоторые из этих этапов могут представлять собой довольно-таки сложные последовательности действий. В наши дни имеется широкое разнообразие инструментов, позволяющих создавать описания процессов развёртывания программ, которые можно воспроизводить на разных системах.

А именно, в мире Linux уже довольно давно существуют менеджеры пакетов. Например — это RPM и YUM. Они упрощают установку, обновление и удаление программ в Linux-системах. Собственно говоря, в этой статье я хочу рассказать о том, как создать собственный простой RPM-пакет, хочу показать, что это совсем несложно.



Надо отметить, что во многих организациях менеджеры пакетов используются лишь для установки программ, предлагаемых разработчиком используемого этими организациями дистрибутива Linux. Для управления развёртываниями собственных программ менеджеры пакетов не применяются. Тому, кто попытается собрать свой первый RPM-пакет, может показаться, что это не так уж и просто. Но обычно тот, кто учится создавать такие пакеты, тратит время с пользой. Дело в том, что соответствующие знания способны помочь ему в деле оптимизации его рабочих процессов. Здесь мы рассмотрим процесс создания RPM-пакета, содержащего простую программу, написанную на Go.

Создание пакета


Во многих проектах для развёртывания ПО используют менеджеры конфигурации. Вот, например, как может выглядеть типичный плейбук Ansible:

tasks:
    - name: 'Copy the artifact'
      copy:
        src: 'my_app'
        dest: '/usr/bin/my_app'
      
    - name: 'Copy configuration files'
      template:
        src: config.json
        dest: /etc/my_app/config.json

Конечно, в плейбуках, применяющихся в реальных проектах, будет описано большее число этапов. Например, это может быть проверка ранее установленного ПО или управление сервисами. А почему бы не использовать нечто вроде следующей конструкции?

  tasks:
    - name: 'Install my_app'
      yum:
        name: 'my_app'

Теперь давайте посмотрим на наше Go-приложение. Это — простой сервер, поддерживающий работу веб-страницы. Вот код файла main.go:

package main

import (
    "encoding/json"
    "flag"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
)

type config struct {
    Text string `json:"string"`
}

func main() {

    var filename = flag.String("config", "config.json", "")
    flag.Parse()

    data, err := ioutil.ReadFile(*filename)
    if err != nil {
        log.Fatalln(err)
    }

    var config config
    err = json.Unmarshal(data, &config)

    if err != nil {
        log.Fatalln(err)
    }

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, config.Text)
    })

    log.Fatal(http.ListenAndServe(":8081", nil))

}

Вот — содержимое config.json:

{
    "string": "Hello world :)"
}

Если запустить эту программу, то, обратившись к ней, учитывая то, что сервер ожидает подключения на порту 8081, можно увидеть веб-страницу с текстом из config.json. Программа эта, конечно, далека от готовности к продакшну, но для наших экспериментов она вполне подойдёт.

Добавление сервисов


А как насчёт сервисов? Использование сервисов — это отличный способ унификации управления приложением. Поэтому создадим файл my_app.service:

[Unit]
Description=My App

[Service]
Type=simple
ExecStart=/usr/bin/my_app -config /etc/my_app/config.json

[Install]
WantedBy=multi-user.target

Каждый раз, когда мы соберёмся развернуть приложение, нужно будет выполнить следующие действия:

  1. Скомпилировать проект.
  2. Скопировать его в /usr/bin/my_app.
  3. Скопировать файл config.json в /etc/my_app/config.json.
  4. Скопировать my_app.service в /etc/systemd/system/.
  5. Запустить сервис.

Создание .spec-файла


RPM, что характерно и для Ansible, нуждается в файле определений, в котором описываются этапы установки программы, её зависимости, и другие действия, которые может понадобиться выполнить для установки программы на сервер:

$ sudo dnf install git
$ sudo dnf module install go-toolset
$ sudo dnf groupinstall "RPM Development Tools"

После того, как всё это установлено, мы готовы к тому, чтобы создать файл определений для пакета, известный ещё как .spec-файл:

$ rpmdev-newspec my_app.spec

Составить подобный файл может быть непросто. Нам, в деле создания этого файла, поможет утилита rpmdev-newspec. Вот его содержимое:

Name:           my_app
Version:        1.0
Release:        1%{?dist}
Summary:        A simple web app

License:        GPLv3
Source0:        %{name}-%{version}.tar.gz

BuildRequires:  golang
BuildRequires:  systemd-rpm-macros

Provides:       %{name} = %{version}

%description
A simple web app

%global debug_package %{nil}

%prep
%autosetup

%build
go build -v -o %{name}

%install
install -Dpm 0755 %{name} %{buildroot}%{_bindir}/%{name}
install -Dpm 0755 config.json %{buildroot}%{_sysconfdir}/%{name}/config.json
install -Dpm 644 %{name}.service %{buildroot}%{_unitdir}/%{name}.service

%check
# go test should be here... :)

%post
%systemd_post %{name}.service

%preun
%systemd_preun %{name}.service

%files
%dir %{_sysconfdir}/%{name}
%{_bindir}/%{name}
%{_unitdir}/%{name}.service
%config(noreplace) %{_sysconfdir}/%{name}/config.json

%changelog
* Wed May 19 2021 John Doe - 1.0-1
- First release%changelog

Тут мне хотелось бы обратить ваше внимание на несколько моментов:

  • Запись Source0 может представлять собой ссылку на репозиторий с исходным кодом. Например, она может выглядеть так: https://github.com/user/my_app/archive/v%version.tar.gz.
  • Если в Source0 используется URL, то для загрузки исходного кода приложения можно воспользоваться командой spectool -g my_app.spec.
  • Git позволяет быстро, не создавая удалённый репозиторий, генерировать tar-архивы:

    $ git archive --format=tar.gz --prefix=my_app-1.0/ -o my_app-1.0.tar.gz HEAD
    
  • Содержимое tar-архива может выглядеть примерно так, как показано ниже:

    $tar tf my_app-1.0.tar.gz 
    my_app-1.0/
    my_app-1.0/config.json
    my_app-1.0/main.go
    my_app-1.0/my_app.service
    

Сборка RPM-пакета


Первым делом нам надо создать структуру директорий rpmbuild и поместить наш tar-архив в директорию SOURCES:

$ rpmdev-setuptree
$ mv my_app-1.0.tar.gz ~/rpmbuild/SOURCES

После этого соберём RPM-пакет для Red Hat Enterprise Linux 8:

$ rpmbuild -ba my_app.spec

Вот и всё.

Теперь у нас должна появиться возможность установить RPM-пакет и запустить наш сервис:

$ sudo dnf install ~/rpmbuild/RPMS/x86_64/my_app-1.0-1.el8.x86_64.rpm
$ sudo systemctl start my_app
$ curl -L http://localhost:8081

Если всё было сделано правильно, то, выполнив вышеописанную последовательность команд, вы должны увидеть содержимое файла config.json (который, кстати, находится в папке /etc/my_app).

А что если появится новая версия нашего приложения? Как создать новый пакет для её установки? Сделать это очень просто — достаточно увеличить номер версии программы в .spec-файле и снова собрать пакет. А DNF обнаружит, что появилось обновление нашей программы.

А если вы пользуетесь репозиторием пакетов — нужно лишь выполнить команду dnf update my_app.

Итоги


Если вы хотите лучше разобраться с тем, как интегрировать RPM-файлы в свои рабочие процессы, советую взглянуть на это и это руководства.

Кроме того, существует множество восхитительных инструментов, способных помочь в деле сборки RPM-пакетов. Есть и инструменты, умеющие создавать репозитории, которыми может воспользоваться разработчик. Это, например, mock, fedpkg, COPR и Koji. Эти инструменты могут пригодиться в проектах, где реализуются сложные сценарии развёртывания ПО. Например — там, где есть множество зависимостей, где в процессе развёртывания имеются сложные этапы, или там, где нужна поддержка нескольких архитектур.

Применяете ли вы RPM-пакеты, созданные самостоятельно?

Комментарии (3)


  1. hippoage
    05.09.2021 21:13
    +2

    Обычно использую https://github.com/jordansissel/fpm : и проще, и больше форматов поддерживает.


  1. Himura
    06.09.2021 10:28
    +2

    Спасибо за перевод, полезно. Оказывается, всё не так уж сложно. Очень хочется аналогичную статью про apt-пакеты, а то благодаря checkinstall, нет никакого повода изучить как оно там внутри (кстати, мне кажется, имеет смысл добавить примечание переводчика про checkinstall, я вот только в процессе написания этого комента узнал что он с rpm тоже работает)

    Не хватает аналогичной статьи про apt-пакеты, а то благодаря checkinstall, нет никакого повода изучить как оно там внутри

    Потом ещё про snap vs flatpak


  1. POPSuL
    21.09.2021 03:37

    Так есть же goreleaser, и deb, и rpm, и даже apk умеет.