action-branch-deploy/README.md

173 lines
7.5 KiB
Markdown

---
gitea: none
include_toc: true
---
# Action for Docker Compose deployment to branch.dsv.su.se
This action takes the current branch (that triggered the workflow) and clones it to the branch.dsv.su.se server and runs `docker compose -f ${compose-file} up` and gives you back a URL pointing to the deployment.
> [!IMPORTANT]
> Please read the section [about the Compose file](#compose-file-1)
## Quickstart
```
[...]
- uses: https://gitea.dsv.su.se/ansv7779/action-branch-deploy@v1
with:
gitea-token: ${{ secrets.GITEA_TOKEN }}
ssh-key: ${{ secrets.BRANCH_DEPLOY_KEY }}
compose-file: compose.yaml
```
## TLDR
* Create your own network for inter-service communication
* Join Traefik network on exposed services
* Add Traefik routing labels to exposed services
* Use environment variable `COMPOSE_PROJECT_NAME` for unique values
* Environment variable `VHOST` is the fully qualified hostname for your deployment
## Inputs
### gitea-token
The token used to authenticate with the Gitea API.
Defaults to `${{ secrets.GITEA_TOKEN }}` which is populated by Gitea automatically.
### ssh-key
The SSH key used to access the deploy script on branch.dsv.su.se.
Defaults to `${{ secrets.BRANCH_DEPLOY_KEY }}` and is populated for you in the DMC organisation.
### compose-file
The Compose file to use when starting the services on branch.dsv.su.se.
Defaults to `compose.yaml`
There are many specifics that you have to adhere to in the Compose file used that will be reacted to by branch.dsv.su.se.
These specifics are related to how traffic gets routed to your services and how to isolate your services from other
deployments on the same server. This is explained in the [Compose file section](#compose-file-1) below.
## Outputs
### url
The complete URL where the system can be accessed.
## Compose file
### Isolation
Since there are multiple deployments on the same server, it is important to isolate your services from others.
There are two primary things that need to be isolated, container names and networks.
Container names are dealt with by *not* specifying a `container_name` for the services in the Compose file. This will
make Docker Compose generate a name for each container based on the project name and service name and since this action
takes care to set a unique project name for each deployment, the container names will be unique.
> [!IMPORTANT]
> Do *not* specify a `container_name` for a service in the Compose file.
When containers talk directly to each other they need to be on the same network. They are referenced using the service
name as the hostname. Since it is impossible to know what every service will be named you must define your own network
(separate from the Traefik network) for inter-service communication. Networks need to have unique names so generate a
name based on `${COMPOSE_PROJECT_NAME}`.
> [!IMPORTANT]
> Define your own network for inter-service communication.
### Traefik
On branch.dsv.su.se there is a [Traefik proxy](https://traefik.io/traefik/) running in the Docker environment that takes care of routing traffic to your containers based on the HTTP host used. As such, your containers should *not* have host port bindings.
> [!IMPORTANT]
> Your containers should *not* have host port bindings.
To get Traefik to send traffic to your container you need to inform Traefik what host you are interested in, this is done using labels.
```
labels:
- "traefik.enable=true"
- "traefik.http.routers.<router name>.rule=Host(`<host>`)"
- "traefik.http.routers.<router name>.tls.certresolver=letsencrypt"
```
> [!NOTE]
> &lt;router name&gt; and &lt;host&gt; are placeholders that should be replaced with your own values,
> they are both explained below in their respective sections.
> [!NOTE]
> The example shows a `Host` rule which is the most common but there are many others, see the
> [Traefik documentation](https://doc.traefik.io/traefik/routing/routers/) for more information.
What do all these labels mean? First one tells Traefik that this container should have traffic routed to it. The `rule` says which traffic.
`tls.certresolver` tells Traefik how to generate HTTPS certificate, the only valid value is `letsencrypt`. Unfortunately this has to be specified on each exposed service since there is no way to set a default.
There is one final thing to do which is to have your Traefik-enabled services join the `traefik` network. This is done using the [top-level element `networks`](https://docs.docker.com/reference/compose-file/networks/). This is an existing network that your services should join, not create, so `external: true` is specified.
```
networks:
traefik:
name: traefik
external: true
```
Not all your services should join this network, but you still want them to be able to communicate with each other. For that you should define a second network used by those services that need to communicate.
#### &lt;router name&gt; in the Traefik labels
This is a unique name that is used to identify the router in Traefik. The name has to be *globally* unique among all
deployed systems, for all repositories and all branches. Fortunately there's an environment variable that is set up for
you named `${COMPOSE_PROJECT_NAME}` that is guaranteed to be unique.
This is used in the [example](#example-compose-file) below. There is rarely, if ever, a need to deviate from this.
`${COMPOSE_PROJECT_NAME}` can be used for other must be unique values as well, see usage below in the example.
#### &lt;host&gt; in the Traefik labels
If a [`Host` rule](https://doc.traefik.io/traefik/routing/routers/#host-and-hostregexp) is used, the hostname can be
accessed using the environment variable `${VHOST}`. This is a fully qualified hostname that is unique for each
deployment and can be prefixed if there's a need for multiple hosts. Do *not* use `.` in the hostname.
### Example Compose file
The below Compose file consists of three services: a `frontend` that runs in the browser, an `api` that is used by the
frontend and a `db` that is used by the `api`. The `frontend` service is exposed to the outside world, as is the `api`
service as it is meant to be accessed by the application running in the browser. However, it uses a different host than
what is used by the `frontend` and that hostname is passed as an environment variable to the `frontend` so it knows
where it is. The `db` lives in an internal network that it shares with `api` and is not exposed to the outside world.
```yaml
services:
frontend:
build:
context: ./frontend/
depends_on:
- api
networks:
- traefik
environment:
- API_URL=api-${VHOST} # will be something like api-repository-branch.branch.dsv.su.se
labels:
- "traefik.enable=true"
- "traefik.http.routers.${COMPOSE_PROJECT_NAME}.rule=Host(`${VHOST}`)"
- "traefik.http.routers.${COMPOSE_PROJECT_NAME}.tls.certresolver=letsencrypt"
api:
build:
context: ./api/
depends_on:
- db
networks:
- internal
- traefik
environment:
- DATABASE_HOSTNAME=db # can use service name as hostname since they share the internal network
- FRONTEND_URL=${VHOST} # if needed to configure CORS for example
labels:
- "traefik.enable=true"
- "traefik.http.routers.api-${COMPOSE_PROJECT_NAME}.rule=Host(`api-${VHOST}`)"
- "traefik.http.routers.api-${COMPOSE_PROJECT_NAME}.tls.certresolver=letsencrypt"
db:
image: mariadb:latest
networks:
- internal
networks:
internal:
name: ${COMPOSE_PROJECT_NAME}_internal
traefik:
name: traefik
external: true
```