Fullstack-приложение на Go c подключением к PostgreSQL

Чтобы развернуть основное приложение в Amvera, нужно выполнить следующие простые шаги:

  1. Открываем страницу https://cloud.amvera.ru/projects

  2. Нажимаем кнопку Создать и выбираем тип сервиса приложение

  3. Выгружаем все файлы (можно через git, а можно через интерфейс). Убедитесь, что вы выгрузили все нужные файлы. Для проекта из примера это:

  • go.mod (обязательно)

  • go.sum (обязательно)

  • main.go (обязательно)

  • Dockerfile (обязательно)

  • static/index.html (если используете)

  • static/script.js (если используете)

  • static/styles.css (если используете)

  • amvera.yml (необязательно)

  1. После этого начнется сборка и развертывание приложения. Дождитесь появления статуса «Успешно развернуто».

Рассмотрим процесс подробнее.

Видеопример деплоя Go-приложения

Создадим простое web приложение на языке программирования Go, где можно будет оставлять и читать цитаты. Для их хранения будем использовать СУБД PostgreSQL.

Директория приложения имеет следующую структуру:

└─ code/
├── static
│   ├── styles.css
│   ├── script.js
│   └── index.html
├── amvera.yml
├── main.go
├── go.sum
├── go.mod
└── Dockerfile 

Код статических файлов доступен в конце страницы.

Конфигурация

Amvera.yml

Написать yaml файл можно как самостоятельно, так и воспользоваться нашим генератором yaml, перейдя по ссылке, либо заполнить в разделе «Конфигурация» личного кабинета.

Пример файла amvera.yml при использовании только amvera.yml:

meta:
  environment: golang
  toolchain:
    name: go
    version: 1.22
build:
  image: golang:1.22
run:
  image: golang:1.22
  persistenceMount: /data
  containerPort: 80

Пример файла amvera.yml при использовании вместе с Dockerfile:

meta:
  environment: docker
  toolchain: docker
build:
  dockerfile: Dockerfile
  skip: false
run:
  persistenceMount: /data
  containerPort: "80"

Важно: сохранять изменяемые файлы (база данных и т.д.) именно в постоянное хранилище /data. Это позволит избежать их потери при пересборке. Папка Data в коде и постоянное хранилище /data - разные директории.

Рассмотрим альтернативный способ задания конфигурации - через Dockerfile.

Dockerfile

Замечание: если вы используете Dockerfile, то конфигурационный файл amvera.yml в большинстве случаев можно не добавлять.

Шаги:

  1. Создаем Dockerfile в директории с проектом.

  2. В Dockerfile указываем базовый образ с названием builder: FROM golang:1.21.1 AS builder

вместо 1.21.1 можете указать любую другую версию, которая вам нужна.

  1. Устанавливаем рабочую директорию: WORKDIR /app

  2. Копируем файлы main.go, go.mod и go.sum в текущую директорию рабочего каталога: COPY main.go go.mod go.sum ./

  3. Выполняем сборку проекта и создаем исполняемый файл server: RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o server

  4. Используем образ Alpine Linux (это уменьшит размер конечного образа и улучшит его производительность): FROM alpine:latest

  5. Копируем исполняемый файл server, собранный в предыдущем образе, в текущую директорию рабочего каталога: COPY --from=builder /app/server ./

  6. Копируем статические файлы внутрь контейнера: COPY static/ ./static/

  7. Открываем порт 80 для внешних подключений: EXPOSE 80

  8. Добавляем команду для запуска приложения: CMD ["./server", "--port", "80"]

Получившийся Dockerfile:

FROM golang:1.21.1 AS builder

WORKDIR /app

COPY main.go go.mod go.sum ./

RUN CGO_ENABLED=0 go build -a -installsuffix cgo -o server

FROM alpine:latest

COPY --from=builder /app/server ./

COPY static/ ./static/

EXPOSE 80

CMD ["./server", "--port", "80"]

Зависимости (go.mod и sum.go)

Инициализируем новый модуль для управления зависимостями. Для этого выполняем следующую команду, которая создаст файл go.mod:

$ go mod init main

Замечание: вместо main можно написать произвольную строку

Остаётся выполнить ещё одну команду, которая добавит по две записи на каждую зависимость и создаст файл go.sum:

go mod tidy

Развертывание СУБД (PostgreSQL)

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

Создание проекта в Amvera

Последний шаг - развернуть само приложение. В файле main.go содержится основной код и выполняется подключение к базе данных. Не забудьте поменять параметры для подключения к базе данных на те, которые вы использовали в прошлом шаге при создании базы данных на Amvera:

  • user = Имя пользователя

  • password = Пароль пользователя

  • dbname = Имя создаваемой БД

  • параметр host можно найти на странице Инфо вашего PostgreSQL проекта (например, amvera-username-cnpg-appname-rw)

main.go:

package main

import (
	"database/sql"
	"encoding/json"
	"log"
	"net/http"
	"strconv"

	_ "github.com/lib/pq"
)

// Укажите те значения, которые задавали при создании БД на Amvera Cloud
const (
	host     = "amvera-nskripko-cnpg-godb-rw"
	port     = 5432
	user     = "nick"
	password = "href239"
	dbname   = "godb"
)

func main() {
	portStr := strconv.Itoa(port)
	dbinfo := "host=" + host + " port=" + portStr + " user=" + user + " password=" + password + " dbname=" + dbname + " sslmode=disable"

	db, err := sql.Open("postgres", dbinfo)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	_, err = db.Exec(`CREATE TABLE IF NOT EXISTS quotes (
                        id SERIAL PRIMARY KEY,
                        quote TEXT NOT NULL
                    )`)
	if err != nil {
		log.Fatal(err)
	}
	
	http.HandleFunc("/quotes", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodGet {
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
			return
		}

		rows, err := db.Query("SELECT quote FROM quotes")
		if err != nil {
			log.Println("Error querying database:", err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}
		defer rows.Close()

		var quotes []string
		for rows.Next() {
			var quote string
			if err := rows.Scan(&quote); err != nil {
				log.Println("Error scanning rows:", err)
				continue
			}
			quotes = append(quotes, quote)
		}

		json.NewEncoder(w).Encode(quotes)
	})

	http.HandleFunc("/addquote", func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
			return
		}

		var data struct {
			Quote string `json:"quote"`
		}
		if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
			http.Error(w, "Bad request", http.StatusBadRequest)
			return
		}

		_, err := db.Exec("INSERT INTO quotes (quote) VALUES ($1)", data.Quote)
		if err != nil {
			log.Println("Error inserting quote into database:", err)
			http.Error(w, "Internal server error", http.StatusInternalServerError)
			return
		}

		w.WriteHeader(http.StatusCreated)
	})

	fs := http.FileServer(http.Dir("static"))
	http.Handle("/", fs)

	log.Fatal(http.ListenAndServe(":80", nil))
}

:warning: Код является демонстрационным примером и мы настоятельно не рекомендуем указывать логин и пароль для подключения к базе данных в коде. Используйте переменные окружения (секреты)!

Проверка работоспособности

  1. Переходим в настройки проекта и активируем доменное имя.

  2. Теперь можно перейти по данному URL и откроется наше приложение.

Если что-то не работает, рекомендуем ознакомиться с логами Сборки и Приложения.

Поздравляем, вы успешно создали свое первое приложение в Amvera!

Код статических файлов из примера

static/index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Quote Board</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <div class="container">
        <h1>Quote Board</h1>
        <form id="quoteForm">
            <input type="text" id="quoteInput" placeholder="Enter your quote" required>
            <button type="submit">Submit</button>
        </form>
        <div id="quoteList"></div>
    </div>
    <script src="script.js"></script>
</body>
</html>

static/styles.css:

body {
    font-family: Arial, sans-serif;
}

.container {
    max-width: 600px;
    margin: 50px auto;
    padding: 0 20px;
}

input[type="text"] {
    width: calc(100% - 80px);
    padding: 10px;
    margin-right: 10px;
}

button {
    padding: 10px 20px;
    background-color: #007bff;
    color: #fff;
    border: none;
    cursor: pointer;
}

button:hover {
    background-color: #0056b3;
}

#quoteList {
    margin-top: 20px;
}

static/script.js:

document.addEventListener('DOMContentLoaded', () => {
    const quoteForm = document.getElementById('quoteForm');
    const quoteInput = document.getElementById('quoteInput');
    const quoteList = document.getElementById('quoteList');

    // Function to fetch quotes from the server and display them
    const fetchQuotes = async () => {
        try {
            const response = await fetch('/quotes');
            const quotes = await response.json();

            // Clear previous quotes
            quoteList.innerHTML = '';

            // Append new quotes to the list
            quotes.forEach(quote => {
                const quoteItem = document.createElement('div');
                quoteItem.textContent = quote;
                quoteList.appendChild(quoteItem);
            });
        } catch (error) {
            console.error('Error fetching quotes:', error);
        }
    };

    // Fetch initial quotes when the page loads
    fetchQuotes();

    // Submit quote form
    quoteForm.addEventListener('submit', async event => {
        event.preventDefault();
        const newQuote = quoteInput.value.trim();

        if (newQuote === '') {
            alert('Please enter a quote.');
            return;
        }

        try {
            // Send the new quote to the server
            await fetch('/addquote', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ quote: newQuote })
            });

            // Clear the input field
            quoteInput.value = '';

            // Fetch and display updated quotes
            fetchQuotes();
        } catch (error) {
            console.error('Error adding quote:', error);
        }
    });
});

Важно

Сохраняйте файлы БД и иные изменяемые данные в постоянное хранилище, чтобы избежать их потери при обновлении проекта, когда производится «откат» папки код до состояния обновления репозитория. Папка data в корне проекта и директория /data, это разные директории.

Проверить, что сохранение идет в /data, можно зайдя в папку «data» на странице «Репозиторий».

Важно

Чтобы избежать ошибки 502, измените в вашем коде host 127.0.0.1 (или подобный localhost) на 0.0.0.0, и пропишите в конфигурации порт, который слушает ваше приложение (пример - 8080).

Если у вас не получается развернуть проект

Напишите наблюдаемую вами симптоматику на support@amvera.ru с указанием вашего имени пользователя и названия проекта, мы постараемся вам помочь.