Initial Things™, should be good enough to collaborate on

This commit is contained in:
Jan Koppe 2024-01-25 19:13:35 +01:00
parent 3c8f8f365d
commit e11e4b138f
Signed by: thunfisch
GPG Key ID: BE935B0735A2129B
40 changed files with 1836 additions and 74 deletions

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# DO NOT COMMIT UNENCRYPTED SECRETS TO GIT YOU DUMMY
secrets.yaml
aws_key
.terraform

1
.tool-versions Normal file
View File

@ -0,0 +1 @@
terraform 1.5.5

131
README.md
View File

@ -1,93 +1,76 @@
# tf
# infrastruktur/tf
Terraform'ed Infrastructure for Chaos-West TV.
## Not included
## Getting started
This repository just sets up all of our services on Docker Swarm. It does not take care of setting up the underlying bare-metal server, or configuring Docker Swarm itself. This is done by hand. The server is running [Arch Linux](https://archlinux.org/) and was ordered, installed and configured initially by hand.
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
Maintenance (updates, SSH key management) is still done by hand.
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
## Structure
## Add your files
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
```
cd existing_repo
git remote add origin https://gitlab.montage2.de/infrastruktur/tf.git
git branch -M main
git push -uf origin main
```text
.
├── README.md # You are here
├── modules # Terraform modules
│   ├── swarm # Swarm Stacks
│   │   ├── traefik # Traefik Ingress
├── stacks # Terraform stacks - combinations of modules
│   ├── ax41-1 # Stack for ax41-1, big 'ol bare-metal server at Hetzner
```
## Integrate with your tools
The Terraform code is split up into modules so that they are small and easy to understand. The modules are then combined into stacks, which are the actual Terraform configurations that are applied. Currently, there is only one stack, `ax41-1`, which is the stack for the big 'ol bare-metal server at Hetzner.
- [ ] [Set up project integrations](https://gitlab.montage2.de/infrastruktur/tf/-/settings/integrations)
## Collaborate with your team
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
## Test and Deploy
Use the built-in continuous integration in GitLab.
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
***
# Editing this README
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
The Terraform state is stored in a versioned S3 bucket. The bucket is located in thunfisch's private AWS account. A dedicated AWS credential pair with minimal permissions is saved in the `aws_key.enc` file. This file is encrypted using sops and age.
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
### Prerequisites
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
- Terraform 1.5.5 (not newer because Hashicorp changed to non-free licensing)
- age (for encrypting & decrypting secrets)
- sops (for encrypting & decrypting secrets)
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
### Deploying
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
```bash
export SOPS_AGE_KEY_FILE=/path/to/your/private/key
eval $(sops -d aws_key.enc) # Sets the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment variables
cd stacks/ax41-1
terraform init
terraform apply
```
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
Check, double-check and triple-check the changes that Terraform wants to apply. If everything looks good, type `yes` and hit enter. Terraform will then apply the changes.
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
### Secrets
## License
For open source projects, say how it is licensed.
Secrets are encrypted using [sops](https://github.com/getsops/sops) and [age](https://github.com/FiloSottile/age). The public keys for the age encryption are stored in the repository, so that anyone can encrypt secrets for the repository. Your private key is stored in your password manager and is only available to you.
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
If you want to roll out changes to the actual infrastructure, you need to be able to decrypt the secrets. To do so, your private key needs to be used as a recipient for the age encryption by someone that previously had access to the secrets. If you don't have access to the secrets, ask someone who does. If you don't know who that is, you probably shouldn't be rolling out changes to the infrastructure.
#### Decrypting Secrets
To be able to decrypt secrets, sops needs to know where your private key is stored. This is done by setting the `SOPS_AGE_KEY_FILE` environment variable to the path of your private key or passing it directly by setting the `SOPS_AGE_KEY` environment variable.
You can manually decrypt a file using sops:
```bash
sops --decrypt stuff.enc.yaml > stuff.yaml
```
Terraform is using the [carlpett/sops](https://registry.terraform.io/providers/carlpett/sops/latest/docs) provider to decrypt secrets. This provider is configured to use these environment variables, so you don't need to do anything else.
#### Encrypting Secrets
To encrypt secrets, you need to have the public keys of the recipients. These are stored in the repository, so you can just use them. The public keys are stored in the `sops-age-recipients.txt` file. To encrypt a secret, load the public keys from this file to your `SOPS_AGE_RECIPIENTS` environment variable and then use sops to encrypt the secret.
```bash
export SOPS_AGE_RECIPIENTS="$(cat sops-age-recipients.txt)"
sops --encrypt stuff.yaml > stuff.enc.yaml
```
#### Modifying Secrets
To modify secrets, run `sops stuff.enc.yaml` and edit the file with your default `$EDITOR`. When you save the file, sops will automatically decrypt and re-encrypt the file. Alternatively, you can also use `sops --decrypt stuff.enc.yaml > stuff.yaml` to decrypt the file and then edit it. When you're done, use `sops --encrypt stuff.yaml > stuff.enc.yaml` to re-encrypt the file. Make sure to remove the unencrypted file afterwards.

20
aws_key.enc Normal file
View File

@ -0,0 +1,20 @@
{
"data": "ENC[AES256_GCM,data:JEWQ5oDpbR+mIJpTdrMBoacpUxnUA8J1EAU+0H/XIJurHGD6lveZ5FJusjAHcFHCKuG4zblKUi2OQ1gsF9Nnabfugvr7s1KnuQgqOIWqrhKzhdlj6llbNSIs2eM1pN2esZ5b8G1aBueobfHQXNuaU76kavG547nE1Q==,iv:mz0kG3Mfv2nWZIr+pfj1q9QZdda1IoL2HXE75HVQQss=,tag:oIISoRuNbil3/smqAw8otQ==,type:str]",
"sops": {
"kms": null,
"gcp_kms": null,
"azure_kv": null,
"hc_vault": null,
"age": [
{
"recipient": "age1zwv4tl8ws6ke8wseenq4lrwcck3el2wandlgztefz9v4qdlnwu7saw7g8z",
"enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzOEp1ZDZmSEhDN3hMZDRi\ndXZIVStVbXJVL0hzWmRaNWNoTmVhMURWYkhJCjFXQm13NVY5Rkt5K0FsRHk2cmdx\nVUhQOVc0YXhVQm94Rmt5bnhjSjBTKzQKLS0tIEVsRXk4UTJoYVpCYlRFVFhIVW1l\nMklzSC9kS3BwMGU2SWgxM2ltMWh6RDgKFItY+3CFsLHEjUtmANyoQ+lLA7zfESWy\nkU+z8YNEM5rwECJAdZiqp8/nJalSRfeYYtdzw/7dNxsGGelNgibBKA==\n-----END AGE ENCRYPTED FILE-----\n"
}
],
"lastmodified": "2024-01-25T17:29:23Z",
"mac": "ENC[AES256_GCM,data:/zhb5hGJLQvfn/1Cqu0CW/Fplq50ohWqif9hIqE9vvhAUp/r7nOSE108dEYA2/mo20yagsf5re9K92zGywPIoPYwKF1Uzm3vWuZnXST5erW3kKuSTVVzt9j3265mWx/uZi+5h+pY4/g1i10qKQV2FF8h1uwOnChSQglLbnlEE2k=,iv:Nqc3G7Ze5dAv/HEbrE1TxJsrtjwxs8A8+okceUy7gC0=,tag:i+mvDXkudii/fGaeISYkmA==,type:str]",
"pgp": null,
"unencrypted_suffix": "_unencrypted",
"version": "3.8.1"
}
}

View File

@ -0,0 +1,29 @@
data "docker_registry_image" "deckchores" {
name = "funkyfuture/deck-chores:1"
}
resource "docker_service" "deckchores" {
name = "deckchores"
task_spec {
container_spec {
image = "${data.docker_registry_image.deckchores.name}@${data.docker_registry_image.deckchores.sha256_digest}"
env = {
TIMEZONE = "Europe/Berlin"
DEFAULT_FLAGES = "noservice"
}
labels {
label = "shepherd.auto-update"
value = "true"
}
mounts {
target = "/var/run/docker.sock"
source = "/var/run/docker.sock"
type = "bind"
}
}
}
}

View File

@ -0,0 +1,13 @@
terraform {
required_version = "1.5.5"
required_providers {
hetznerdns = {
source = "timohirt/hetznerdns"
version = "~>2.2"
}
docker = {
source = "kreuzwerker/docker"
version = "~>3.0"
}
}
}

View File

@ -0,0 +1,715 @@
##################### Grafana Configuration Example #####################
#
# Everything has defaults so you only need to uncomment things you want to
# change
# possible values : production, development
;app_mode = production
# instance name, defaults to HOSTNAME environment variable value or hostname if HOSTNAME var is empty
;instance_name = ${HOSTNAME}
#################################### Paths ####################################
[paths]
# Path to where grafana can store temp files, sessions, and the sqlite3 db (if that is used)
;data = /var/lib/grafana
# Temporary files in `data` directory older than given duration will be removed
;temp_data_lifetime = 24h
# Directory where grafana can store logs
;logs = /var/log/grafana
# Directory where grafana will automatically scan and look for plugins
;plugins = /var/lib/grafana/plugins
# folder that contains provisioning config files that grafana will apply on startup and while running.
;provisioning = conf/provisioning
#################################### Server ####################################
[server]
# Protocol (http, https, h2, socket)
protocol = http
# The ip address to bind to, empty will bind to all interfaces
http_addr = 0.0.0.0
# The http port to use
http_port = 3000
# The public facing domain name used to access grafana from a browser
domain = "grafana.montage2.de"
# Redirect to correct domain if host header does not match domain
# Prevents DNS rebinding attacks
;enforce_domain = false
# The full public facing url you use in browser, used for redirects and emails
# If you use reverse proxy and sub path specify full url (with sub path)
;root_url = %(protocol)s://%(domain)s:%(http_port)s/
root_url = "https://grafana.montage2.de/"
# Serve Grafana from subpath specified in `root_url` setting. By default it is set to `false` for compatibility reasons.
;serve_from_sub_path = false
# Log web requests
;router_logging = false
# the path relative working path
;static_root_path = public
# enable gzip
;enable_gzip = false
# https certs & key file
;cert_file =
;cert_key =
# Unix socket path
;socket =
#################################### Database ####################################
[database]
# You can configure the database connection by specifying type, host, name, user and password
# as separate properties or as on string using the url properties.
# Either "mysql", "postgres" or "sqlite3", it's your choice
type = sqlite3
;host = 127.0.0.1:3306
;name = grafana
;user = root
# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
;password =
# Use either URL or the previous fields to configure the database
# Example: mysql://user:secret@host:port/database
;url =
# For "postgres" only, either "disable", "require" or "verify-full"
;ssl_mode = disable
;ca_cert_path =
;client_key_path =
;client_cert_path =
;server_cert_name =
# For "sqlite3" only, path relative to data_path setting
;path = grafana.db
# Max idle conn setting default is 2
;max_idle_conn = 2
# Max conn setting default is 0 (mean not set)
;max_open_conn =
# Connection Max Lifetime default is 14400 (means 14400 seconds or 4 hours)
;conn_max_lifetime = 14400
# Set to true to log the sql calls and execution times.
;log_queries =
# For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared)
;cache_mode = private
#################################### Cache server #############################
[remote_cache]
# Either "redis", "memcached" or "database" default is "database"
;type = database
# cache connectionstring options
# database: will use Grafana primary database.
# redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=0,ssl=false`. Only addr is required. ssl may be 'true', 'false', or 'insecure'.
# memcache: 127.0.0.1:11211
;connstr =
#################################### Data proxy ###########################
[dataproxy]
# This enables data proxy logging, default is false
;logging = false
# How long the data proxy should wait before timing out default is 30 (seconds)
;timeout = 30
# If enabled and user is not anonymous, data proxy will add X-Grafana-User header with username into the request, default is false.
;send_user_header = false
#################################### Analytics ####################################
[analytics]
# Server reporting, sends usage counters to stats.grafana.org every 24 hours.
# No ip addresses are being tracked, only simple counters to track
# running instances, dashboard and error counts. It is very helpful to us.
# Change this option to false to disable reporting.
reporting_enabled = false
# Set to false to disable all checks to https://grafana.net
# for new vesions (grafana itself and plugins), check is used
# in some UI views to notify that grafana or plugin update exists
# This option does not cause any auto updates, nor send any information
# only a GET request to http://grafana.com to get latest versions
;check_for_updates = true
# Google Analytics universal tracking code, only enabled if you specify an id here
;google_analytics_ua_id =
# Google Tag Manager ID, only enabled if you specify an id here
;google_tag_manager_id =
#################################### Security ####################################
[security]
# disable creation of admin user on first start of grafana
;disable_initial_admin_creation = false
# default admin user, created on startup
admin_user = admin
# default admin password, can be changed before first start of grafana, or in profile settings
#admin_password = hunter2
# used for signing
#secret_key = hunter42
# disable gravatar profile images
;disable_gravatar = false
# data source proxy whitelist (ip_or_domain:port separated by spaces)
;data_source_proxy_whitelist =
# disable protection against brute force login attempts
;disable_brute_force_login_protection = false
# set to true if you host Grafana behind HTTPS. default is false.
cookie_secure = true
# set cookie SameSite attribute. defaults to `lax`. can be set to "lax", "strict", "none" and "disabled"
;cookie_samesite = lax
# set to true if you want to allow browsers to render Grafana in a <frame>, <iframe>, <embed> or <object>. default is false.
;allow_embedding = false
# Set to true if you want to enable http strict transport security (HSTS) response header.
# This is only sent when HTTPS is enabled in this configuration.
# HSTS tells browsers that the site should only be accessed using HTTPS.
# The default version will change to true in the next minor release, 6.3.
;strict_transport_security = false
# Sets how long a browser should cache HSTS. Only applied if strict_transport_security is enabled.
;strict_transport_security_max_age_seconds = 86400
# Set to true if to enable HSTS preloading option. Only applied if strict_transport_security is enabled.
;strict_transport_security_preload = false
# Set to true if to enable the HSTS includeSubDomains option. Only applied if strict_transport_security is enabled.
;strict_transport_security_subdomains = false
# Set to true to enable the X-Content-Type-Options response header.
# The X-Content-Type-Options response HTTP header is a marker used by the server to indicate that the MIME types advertised
# in the Content-Type headers should not be changed and be followed. The default will change to true in the next minor release, 6.3.
;x_content_type_options = false
# Set to true to enable the X-XSS-Protection header, which tells browsers to stop pages from loading
# when they detect reflected cross-site scripting (XSS) attacks. The default will change to true in the next minor release, 6.3.
;x_xss_protection = false
#################################### Snapshots ###########################
[snapshots]
# snapshot sharing options
;external_enabled = true
;external_snapshot_url = https://snapshots-origin.raintank.io
;external_snapshot_name = Publish to snapshot.raintank.io
# Set to true to enable this Grafana instance act as an external snapshot server and allow unauthenticated requests for
# creating and deleting snapshots.
;public_mode = false
# remove expired snapshot
;snapshot_remove_expired = true
#################################### Dashboards History ##################
[dashboards]
# Number dashboard versions to keep (per dashboard). Default: 20, Minimum: 1
;versions_to_keep = 20
# Minimum dashboard refresh interval. When set, this will restrict users to set the refresh interval of a dashboard lower than given interval. Per default this is not set/unrestricted.
# The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m.
;min_refresh_interval =
#################################### Users ###############################
[users]
# disable user signup / registration
allow_sign_up = false
# Allow non admin users to create organizations
allow_org_create = false
# Set to true to automatically assign new users to the default organization (id 1)
auto_assign_org = true
# Set this value to automatically add new users to the provided organization (if auto_assign_org above is set to true)
auto_assign_org_id = 1
# Default role new users will be automatically assigned (if disabled above is set to true)
auto_assign_org_role = Viewer
# Require email validation before sign up completes
verify_email_enabled = false
# Background text for the user field on the login page
;login_hint = email or username
;password_hint = password
# Default UI theme ("dark" or "light")
;default_theme = dark
# External user management, these options affect the organization users view
;external_manage_link_url =
;external_manage_link_name =
;external_manage_info =
# Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard.
;viewers_can_edit = false
# Editors can administrate dashboard, folders and teams they create
;editors_can_admin = false
[auth]
# Login cookie name
;login_cookie_name = grafana_session
# The lifetime (days) an authenticated user can be inactive before being required to login at next visit. Default is 7 days,
;login_maximum_inactive_lifetime_days = 7
# The maximum lifetime (days) an authenticated user can be logged in since login time before being required to login. Default is 30 days.
;login_maximum_lifetime_days = 30
# How often should auth tokens be rotated for authenticated users when being active. The default is each 10 minutes.
;token_rotation_interval_minutes = 10
# Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false
;disable_login_form = false
# Set to true to disable the signout link in the side menu. useful if you use auth.proxy, defaults to false
;disable_signout_menu = false
# URL to redirect the user to after sign out
;signout_redirect_url =
# Set to true to attempt login with OAuth automatically, skipping the login screen.
# This setting is ignored if multiple OAuth providers are configured.
;oauth_auto_login = false
# limit of api_key seconds to live before expiration
;api_key_max_seconds_to_live = -1
#################################### Anonymous Auth ######################
[auth.anonymous]
# enable anonymous access
enabled = true
# specify organization name that should be used for unauthenticated users
org_name = Montage2
# specify role for unauthenticated users
org_role = Viewer
#################################### Github Auth ##########################
#[auth.github]
;enabled = true
;allow_sign_up = true
;client_id = some_client_id
;client_secret = some_client_secret
;scopes = user:email,read:org
;auth_url = https://github.com/login/oauth/authorize
;token_url = https://github.com/login/oauth/access_token
;api_url = https://api.github.com/user
;allowed_domains =
;team_ids =
;allowed_organizations = chaoswest-tv
#################################### GitLab Auth #########################
[auth.gitlab]
enabled = true
allow_sign_up = true
;client_id = some_id
;client_secret = some_secret
scopes = read_user
auth_url = https://gitlab.montage2.de/oauth/authorize
token_url = https://gitlab.montage2.de/oauth/token
api_url = https://gitlab.montage2.de/api/v4
;allowed_domains =
;allowed_groups =
#################################### Google Auth ##########################
[auth.google]
;enabled = false
;allow_sign_up = true
;client_id = some_client_id
;client_secret = some_client_secret
;scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email
;auth_url = https://accounts.google.com/o/oauth2/auth
;token_url = https://accounts.google.com/o/oauth2/token
;api_url = https://www.googleapis.com/oauth2/v1/userinfo
;allowed_domains =
;hosted_domain =
#################################### Grafana.com Auth ####################
[auth.grafana_com]
;enabled = false
;allow_sign_up = true
;client_id = some_id
;client_secret = some_secret
;scopes = user:email
;allowed_organizations =
#################################### Azure AD OAuth #######################
[auth.azuread]
;name = Azure AD
;enabled = false
;allow_sign_up = true
;client_id = some_client_id
;client_secret = some_client_secret
;scopes = openid email profile
;auth_url = https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/authorize
;token_url = https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token
;allowed_domains =
;allowed_groups =
#################################### Generic OAuth ##########################
[auth.generic_oauth]
enabled = true
name = authentik
allow_sign_up = true
; client_id = some_client_id
; client_secret = some_client_secret
scopes = openid,profile,email
;email_attribute_name = email
;email_attribute_path =
auth_url = https://authentik.montage2.de/application/o/authorize/
token_url = https://authentik.montage2.de/application/o/token/
api_url = https://authentik.montage2.de/application/o/userinfo/
;allowed_domains =
;team_ids =
;allowed_organizations =
role_attribute_path = contains(groups[*], 'Montage2 Admin') && 'Admin' || contains(groups[*], 'Montage2 Member') && 'Editor' || 'Viewer'
;tls_skip_verify_insecure = false
;tls_client_cert =
;tls_client_key =
;tls_client_ca =
#################################### SAML Auth ###########################
[auth.saml] # Enterprise only
# Defaults to false. If true, the feature is enabled.
;enabled = false
# Base64-encoded public X.509 certificate. Used to sign requests to the IdP
;certificate =
# Path to the public X.509 certificate. Used to sign requests to the IdP
;certificate_path =
# Base64-encoded private key. Used to decrypt assertions from the IdP
;private_key =
;# Path to the private key. Used to decrypt assertions from the IdP
;private_key_path =
# Base64-encoded IdP SAML metadata XML. Used to verify and obtain binding locations from the IdP
;idp_metadata =
# Path to the SAML metadata XML. Used to verify and obtain binding locations from the IdP
;idp_metadata_path =
# URL to fetch SAML IdP metadata. Used to verify and obtain binding locations from the IdP
;idp_metadata_url =
# Duration, since the IdP issued a response and the SP is allowed to process it. Defaults to 90 seconds.
;max_issue_delay = 90s
# Duration, for how long the SP's metadata should be valid. Defaults to 48 hours.
;metadata_valid_duration = 48h
# Friendly name or name of the attribute within the SAML assertion to use as the user's name
;assertion_attribute_name = displayName
# Friendly name or name of the attribute within the SAML assertion to use as the user's login handle
;assertion_attribute_login = mail
# Friendly name or name of the attribute within the SAML assertion to use as the user's email
;assertion_attribute_email = mail
#################################### Basic Auth ##########################
[auth.basic]
;enabled = true
#################################### Auth Proxy ##########################
[auth.proxy]
;enabled = false
;header_name = X-WEBAUTH-USER
;header_property = username
;auto_sign_up = true
;sync_ttl = 60
;whitelist = 192.168.1.1, 192.168.2.1
;headers = Email:X-User-Email, Name:X-User-Name
# Read the auth proxy docs for details on what the setting below enables
;enable_login_token = false
#################################### Auth LDAP ##########################
[auth.ldap]
;enabled = false
;config_file = /etc/grafana/ldap.toml
;allow_sign_up = true
# LDAP backround sync (Enterprise only)
# At 1 am every day
;sync_cron = "0 0 1 * * *"
;active_sync_enabled = true
#################################### SMTP / Emailing ##########################
[smtp]
;enabled = false
;host = localhost:25
;user =
# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;"""
;password =
;cert_file =
;key_file =
;skip_verify = false
;from_address = admin@grafana.localhost
;from_name = Grafana
# EHLO identity in SMTP dialog (defaults to instance_name)
;ehlo_identity = dashboard.example.com
[emails]
;welcome_email_on_sign_up = false
;templates_pattern = emails/*.html
#################################### Logging ##########################
[log]
# Either "console", "file", "syslog". Default is console and file
# Use space to separate multiple modes, e.g. "console file"
;mode = console file
# Either "debug", "info", "warn", "error", "critical", default is "info"
;level = info
# optional settings to set different levels for specific loggers. Ex filters = sqlstore:debug
;filters =
# For "console" mode only
[log.console]
;level =
# log line format, valid options are text, console and json
;format = console
# For "file" mode only
[log.file]
;level =
# log line format, valid options are text, console and json
;format = text
# This enables automated log rotate(switch of following options), default is true
;log_rotate = true
# Max line number of single file, default is 1000000
;max_lines = 1000000
# Max size shift of single file, default is 28 means 1 << 28, 256MB
;max_size_shift = 28
# Segment log daily, default is true
;daily_rotate = true
# Expired days of log file(delete after max days), default is 7
;max_days = 7
[log.syslog]
;level =
# log line format, valid options are text, console and json
;format = text
# Syslog network type and address. This can be udp, tcp, or unix. If left blank, the default unix endpoints will be used.
;network =
;address =
# Syslog facility. user, daemon and local0 through local7 are valid.
;facility =
# Syslog tag. By default, the process' argv[0] is used.
;tag =
#################################### Usage Quotas ########################
[quota]
; enabled = false
#### set quotas to -1 to make unlimited. ####
# limit number of users per Org.
; org_user = 10
# limit number of dashboards per Org.
; org_dashboard = 100
# limit number of data_sources per Org.
; org_data_source = 10
# limit number of api_keys per Org.
; org_api_key = 10
# limit number of orgs a user can create.
; user_org = 10
# Global limit of users.
; global_user = -1
# global limit of orgs.
; global_org = -1
# global limit of dashboards
; global_dashboard = -1
# global limit of api_keys
; global_api_key = -1
# global limit on number of logged in users.
; global_session = -1
#################################### Alerting ############################
[alerting]
# Disable alerting engine & UI features
;enabled = true
# Makes it possible to turn off alert rule execution but alerting UI is visible
;execute_alerts = true
# Default setting for new alert rules. Defaults to categorize error and timeouts as alerting. (alerting, keep_state)
;error_or_timeout = alerting
# Default setting for how Grafana handles nodata or null values in alerting. (alerting, no_data, keep_state, ok)
;nodata_or_nullvalues = no_data
# Alert notifications can include images, but rendering many images at the same time can overload the server
# This limit will protect the server from render overloading and make sure notifications are sent out quickly
;concurrent_render_limit = 5
# Default setting for alert calculation timeout. Default value is 30
;evaluation_timeout_seconds = 30
# Default setting for alert notification timeout. Default value is 30
;notification_timeout_seconds = 30
# Default setting for max attempts to sending alert notifications. Default value is 3
;max_attempts = 3
# Makes it possible to enforce a minimal interval between evaluations, to reduce load on the backend
;min_interval_seconds = 1
#################################### Explore #############################
[explore]
# Enable the Explore section
;enabled = true
#################################### Internal Grafana Metrics ##########################
# Metrics available at HTTP API Url /metrics
[metrics]
# Disable / Enable internal metrics
enabled = false
# Graphite Publish interval
;interval_seconds = 10
# Disable total stats (stat_totals_*) metrics to be generated
;disable_total_stats = false
#If both are set, basic auth will be required for the metrics endpoint.
; basic_auth_username =
; basic_auth_password =
# Send internal metrics to Graphite
[metrics.graphite]
# Enable by setting the address setting (ex localhost:2003)
;address =
;prefix = prod.grafana.%(instance_name)s.
#################################### Grafana.com integration ##########################
# Url used to import dashboards directly from Grafana.com
[grafana_com]
;url = https://grafana.com
#################################### Distributed tracing ############
[tracing.jaeger]
# Enable by setting the address sending traces to jaeger (ex localhost:6831)
;address = localhost:6831
# Tag that will always be included in when creating new spans. ex (tag1:value1,tag2:value2)
;always_included_tag = tag1:value1
# Type specifies the type of the sampler: const, probabilistic, rateLimiting, or remote
;sampler_type = const
# jaeger samplerconfig param
# for "const" sampler, 0 or 1 for always false/true respectively
# for "probabilistic" sampler, a probability between 0 and 1
# for "rateLimiting" sampler, the number of spans per second
# for "remote" sampler, param is the same as for "probabilistic"
# and indicates the initial sampling rate before the actual one
# is received from the mothership
;sampler_param = 1
# Whether or not to use Zipkin propagation (x-b3- HTTP headers).
;zipkin_propagation = false
# Setting this to true disables shared RPC spans.
# Not disabling is the most common setting when using Zipkin elsewhere in your infrastructure.
;disable_shared_zipkin_spans = false
#################################### External image storage ##########################
[external_image_storage]
# Used for uploading images to public servers so they can be included in slack/email messages.
# you can choose between (s3, webdav, gcs, azure_blob, local)
;provider =
[external_image_storage.s3]
;endpoint =
;path_style_access =
;bucket =
;region =
;path =
;access_key =
;secret_key =
[external_image_storage.webdav]
;url =
;public_url =
;username =
;password =
[external_image_storage.gcs]
;key_file =
;bucket =
;path =
[external_image_storage.azure_blob]
;account_name =
;account_key =
;container_name =
[external_image_storage.local]
# does not require any configuration
[rendering]
# Options to configure a remote HTTP image rendering service, e.g. using https://github.com/grafana/grafana-image-renderer.
# URL to a remote HTTP image renderer service, e.g. http://localhost:8081/render, will enable Grafana to render panels and dashboards to PNG-images using HTTP requests to an external service.
;server_url =
# If the remote HTTP image renderer service runs on a different server than the Grafana server you may have to configure this to a URL where Grafana is reachable, e.g. http://grafana.domain/.
;callback_url =
[panels]
# If set to true Grafana will allow script tags in text panels. Not recommended as it enable XSS vulnerabilities.
;disable_sanitize_html = false
[plugins]
;enable_alpha = false
;app_tls_skip_verify_insecure = false
[enterprise]
# Path to a valid Grafana Enterprise license.jwt file
;license_path =
[feature_toggles]
# enable features, separated by spaces
;ena

View File

@ -0,0 +1,8 @@
resource "docker_config" "grafana" {
name = "grafana-ini-${replace(timestamp(), ":", ".")}"
data = base64encode(file("${path.module}/cfg/grafana.ini"))
lifecycle {
ignore_changes = [name]
create_before_destroy = true
}
}

View File

@ -0,0 +1,74 @@
data "docker_registry_image" "grafana" {
name = "grafana/grafana:latest"
}
data "docker_network" "traefik" {
name = "traefik"
}
locals {
labels = {
"shepherd.auto-update" = "true",
"traefik.enable" = "true"
"traefik.http.services.grafana.loadbalancer.server.port" = "3000",
"traefik.http.routers.grafana.rule" = "Host(`grafana.montage2.de`)||Host(`grafana.chaoswest.tv`)",
"traefik.http.routers.grafana.tls" = "true",
"traefik.http.routers.grafana.tls.certresolver" = "default",
"traefik.http.routers.grafana.middlewares" = "grafana-redirect",
"traefik.http.middlewares.grafana-redirect.redirectregex.regex" = "^https://grafana.chaoswest.tv/(.*)",
"traefik.http.middlewares.grafana-redirect.redirectregex.replacement" = "https://grafana.montage2.de/$$${1}", # double escaping is necessary here
}
}
resource "docker_service" "grafana" {
name = "grafana"
dynamic "labels" {
for_each = local.labels
content {
label = labels.key
value = labels.value
}
}
task_spec {
networks_advanced {
name = data.docker_network.traefik.id
}
container_spec {
image = "${data.docker_registry_image.grafana.name}@${data.docker_registry_image.grafana.sha256_digest}"
env = {
for k, v in var.secrets : "${upper(k)}__FILE" => "/run/secrets/${k}"
}
dynamic "secrets" {
for_each = nonsensitive(var.secrets)
content {
secret_id = docker_secret.secrets[secrets.key].id
secret_name = docker_secret.secrets[secrets.key].name
file_name = "/run/secrets/${secrets.key}"
file_uid = "472"
file_gid = "472"
file_mode = "0400"
}
}
mounts {
target = "/var/lib/grafana/"
source = "/mnt/data/grafana/"
type = "bind"
}
configs {
config_id = docker_config.grafana.id
config_name = docker_config.grafana.name
file_name = "/etc/grafana/grafana.ini"
file_uid = "472"
file_gid = "472"
file_mode = "0400"
}
}
}
}

View File

@ -0,0 +1,13 @@
resource "docker_secret" "secrets" {
for_each = nonsensitive(var.secrets) # tf complains about sensitive values, but keys are not sensitive
# Because secrets names can only be 64 characters long, we're hashing the key to shorten it & make it unique.
# This makes the key name harder to read, but it's not like we're going to look at it often anyway.
name = "grafana_${md5("${each.key}-${replace(timestamp(), ":", ".")}")}"
data = base64encode(each.value)
lifecycle {
ignore_changes = [name]
create_before_destroy = true
}
}

View File

@ -0,0 +1,4 @@
variable "secrets" {
description = "map of secrets to be used by grafana"
type = map(string)
}

View File

@ -0,0 +1,13 @@
terraform {
required_version = "1.5.5"
required_providers {
hetznerdns = {
source = "timohirt/hetznerdns"
version = "~>2.2"
}
docker = {
source = "kreuzwerker/docker"
version = "~>3.0"
}
}
}

View File

@ -0,0 +1,122 @@
data "docker_registry_image" "hedgedoc" {
name = "quay.io/hedgedoc/hedgedoc:1.9.9"
}
data "docker_registry_image" "hedgedoc_mysql" {
name = "mysql:8"
}
data "docker_network" "traefik" {
name = "traefik"
}
resource "docker_network" "hedgedoc" {
name = "hedgedoc"
attachable = true
driver = "overlay"
lifecycle {
ignore_changes = [labels]
}
}
locals {
labels = {
"shepherd.auto-update" = "true",
"traefik.enable" = "true"
"traefik.http.services.hedgedoc.loadbalancer.server.port" = "3000",
"traefik.http.routers.hedgedoc.rule" = "Host(`pad.montage2.de`)||Host(`pad.chaoswest.tv`)",
"traefik.http.routers.hedgedoc.tls" = "true",
"traefik.http.routers.hedgedoc.tls.certresolver" = "default",
"traefik.http.routers.hedgedoc.middlewares" = "hedgedoc-redirect",
"traefik.http.middlewares.hedgedoc-redirect.redirectregex.regex" = "^https://pad.chaoswest.tv/(.*)",
"traefik.http.middlewares.hedgedoc-redirect.redirectregex.replacement" = "https://pad.montage2.de/$$${1}", # double escaping is necessary here
}
}
resource "docker_service" "hedgedoc_mysql" {
name = "hedgedoc-mysql"
task_spec {
networks_advanced {
name = docker_network.hedgedoc.id
}
container_spec {
image = "${data.docker_registry_image.hedgedoc_mysql.name}@${data.docker_registry_image.hedgedoc_mysql.sha256_digest}"
args = [
"--character-set-server=utf8mb4",
"--collation-server=utf8mb4_unicode_ci"
]
env = {
MYSQL_RANDOM_ROOT_PASSWORD = "1",
MYSQL_DATABASE = "hedgedoc",
MYSQL_USER = "hedgedoc",
MYSQL_PASSWORD = "hedgedoc",
}
mounts {
target = "/var/lib/mysql/"
source = "/mnt/data/pad/mysql/"
type = "bind"
}
}
}
}
resource "docker_service" "hedgedoc" {
name = "hedgedoc"
dynamic "labels" {
for_each = local.labels
content {
label = labels.key
value = labels.value
}
}
task_spec {
networks_advanced {
name = data.docker_network.traefik.id
}
networks_advanced {
name = docker_network.hedgedoc.id
}
container_spec {
image = "${data.docker_registry_image.hedgedoc.name}@${data.docker_registry_image.hedgedoc.sha256_digest}"
env = merge({
for k, v in var.secrets : k => v
}, {
CMD_DB_URL = "mysql://hedgedoc:hedgedoc@hedgedoc-mysql:3306/hedgedoc",
CMD_DOMAIN = "pad.montage2.de",
CMD_URL_ADDPORT = "false",
CMD_PROTOCOL_USESSL = "true",
CMD_EMAIL = "false",
CMD_ALLOW_EMAIL_REGISTER = "false",
CMD_ALLOW_FREEURL = "true",
CMD_GITLAB_BASEURL = "https://gitlab.montage2.de",
CMD_OAUTH2_PROVIDERNAME = "authentik",
CMD_OAUTH2_SCOPE = "openid email profile",
CMD_OAUTH2_USER_PROFILE_URL = "https://authentik.montage2.de/application/o/userinfo/",
CMD_OAUTH2_TOKEN_URL = "https://authentik.montage2.de/application/o/token/",
CMD_OAUTH2_AUTHORIZATION_URL = "https://authentik.montage2.de/application/o/authorize/",
CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR = "preferred_username",
CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR = "name",
CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR = "email",
})
mounts {
target = "/hedgedoc/public/uploads"
source = "/mnt/data/pad/uploads"
type = "bind"
}
}
}
}

View File

@ -0,0 +1,4 @@
variable "secrets" {
description = "map of secrets to be used by hedgedoc"
type = map(string)
}

View File

@ -0,0 +1,13 @@
terraform {
required_version = "1.5.5"
required_providers {
hetznerdns = {
source = "timohirt/hetznerdns"
version = "~>2.2"
}
docker = {
source = "kreuzwerker/docker"
version = "~>3.0"
}
}
}

View File

@ -0,0 +1,37 @@
data "docker_registry_image" "jitsi_jicofo" {
name = "jitsi/jicofo:stable"
}
resource "docker_service" "jitsi_jicofo" {
name = "jitsi_jicofo"
labels {
label = "shepherd.auto-update"
value = "true"
}
task_spec {
networks_advanced {
name = docker_network.jitsi.id
}
container_spec {
image = "${data.docker_registry_image.jitsi_jicofo.name}@${data.docker_registry_image.jitsi_jicofo.sha256_digest}"
env = {
TZ = "Europe/Berlin"
JICOFO_AUTH_USER = "focus"
JICOFO_AUTH_PASSWORD = nonsensitive(var.secrets.jicofo_auth_password)
JIBRI_BREWERY_MUC = "jibribrewery"
JIGASI_BREWERY_MUC = "jigasibrewery"
JVB_BREWERY_MUC = "jvbbrewery"
XMPP_DOMAIN = "meet.jitsi"
XMPP_AUTH_DOMAIN = "auth.meet.jitsi"
XMPP_INTERNAL_MUC_DOMAIN = "internal-muc.meet.jitsi"
XMPP_MUC_DOMAIN = "muc.meet.jitsi"
XMPP_RECORDER_DOMAIN = "recorder.meet.jitsi"
XMPP_SERVER = "jitsi_prosody"
}
}
}
}

View File

@ -0,0 +1,55 @@
data "docker_registry_image" "jitsi_jvb" {
name = "jitsi/jvb:stable"
}
resource "docker_service" "jitsi_jvb" {
name = "jitsi_jvb"
labels {
label = "shepherd.auto-update"
value = "true"
}
endpoint_spec {
ports {
target_port = 10000
published_port = 10000
protocol = "udp"
publish_mode = "host"
}
ports {
target_port = 4443
published_port = 4443
protocol = "tcp"
publish_mode = "host"
}
}
task_spec {
networks_advanced {
name = docker_network.jitsi.id
}
container_spec {
image = "${data.docker_registry_image.jitsi_jvb.name}@${data.docker_registry_image.jitsi_jvb.sha256_digest}"
env = {
DOCKER_HOST_ADDRESS = "talk.chaoswest.tv"
JVB_AUTH_USER = "jvb"
JVB_AUTH_PASSWORD = nonsensitive(var.secrets.jvb_auth_password)
JVB_BREWERY_MUC = "jvbbrewery"
JVB_PORT = "10000"
JVB_TCP_HARVESTER_DISABLED = "true"
JVB_TCP_PORT = "4443"
JVB_TCP_MAPPED_PORT = "4443"
JVB_STUN_SERVERS = "meet-jit-si-turnrelay.jitsi.net:443"
PUBLIC_URL = "https://talk.chaoswest.tv"
TZ = "Europe/Berlin"
XMPP_AUTH_DOMAIN = "auth.meet.jitsi"
XMPP_INTERNAL_MUC_DOMAIN = "internal-muc.meet.jitsi"
XMPP_SERVER = "jitsi_prosody"
}
}
}
}

View File

@ -0,0 +1,62 @@
data "docker_registry_image" "jitsi_prosody" {
name = "jitsi/prosody:stable"
}
locals {
labels_prosody = {
"shepherd.auto-update" = "true",
"traefik.enable" = "true"
"traefik.http.services.jitsi-prosody.loadbalancer.server.port" = "5280",
"traefik.http.routers.jitsi-prosody.rule" = "Host(`talk.chaoswest.tv`)&&Path(`/room-census`)",
"traefik.http.routers.jitsi-prosody.tls" = "true",
"traefik.http.routers.jitsi-prosody.tls.certresolver" = "default",
}
}
resource "docker_service" "jitsi_prosody" {
name = "jitsi_prosody"
dynamic "labels" {
for_each = local.labels_prosody
content {
label = labels.key
value = labels.value
}
}
task_spec {
networks_advanced {
name = data.docker_network.traefik.id
}
networks_advanced {
name = docker_network.jitsi.id
}
container_spec {
image = "${data.docker_registry_image.jitsi_prosody.name}@${data.docker_registry_image.jitsi_prosody.sha256_digest}"
env = {
JIBRI_RECORDER_USER = "recorder",
JIBRI_RECORDER_PASSWORD = nonsensitive(var.secrets.jibri_recorder_password),
JIBRI_XMPP_USER = "jibri",
JIBRI_XMPP_PASSWORD = nonsensitive(var.secrets.jibri_xmpp_password),
JIGASI_XMPP_USER = "jigasi",
JIGASI_XMPP_PASSWORD = nonsensitive(var.secrets.jigasi_xmpp_password),
JVB_AUTH_USER = "jvb",
JVB_AUTH_PASSWORD = nonsensitive(var.secrets.jvb_auth_password),
JICOFO_AUTH_USER = "focus",
JICOFO_AUTH_PASSWORD = nonsensitive(var.secrets.jicofo_auth_password),
GLOBAL_MODULES = "muc_census",
PUBLIC_URL = "https://talk.chaoswest.tv",
TZ = "Europe/Berlin",
XMPP_DOMAIN = "meet.jitsi",
XMPP_AUTH_DOMAIN = "auth.meet.jitsi",
XMPP_INTERNAL_MUC_DOMAIN = "internal-muc.meet.jitsi",
XMPP_GUEST_DOMAIN = "guest.meet.jitsi",
XMPP_RECORDER_DOMAIN = "recorder.meet.jitsi",
XMPP_MUC_DOMAIN = "muc.meet.jitsi",
}
}
}
}

View File

@ -0,0 +1,51 @@
data "docker_registry_image" "jitsi_web" {
name = "jitsi/web:stable"
}
locals {
labels_web = {
"shepherd.auto-update" = "true",
"traefik.enable" = "true"
"traefik.http.services.jitsi-web.loadbalancer.server.port" = "80",
"traefik.http.routers.jitsi-web.rule" = "Host(`talk.chaoswest.tv`)&&!Path(`/room-census`)",
"traefik.http.routers.jitsi-web.tls" = "true",
"traefik.http.routers.jitsi-web.tls.certresolver" = "default",
}
}
resource "docker_service" "jitsi_web" {
name = "jitsi_web"
dynamic "labels" {
for_each = local.labels_web
content {
label = labels.key
value = labels.value
}
}
task_spec {
networks_advanced {
name = data.docker_network.traefik.id
}
networks_advanced {
name = docker_network.jitsi.id
}
container_spec {
image = "${data.docker_registry_image.jitsi_web.name}@${data.docker_registry_image.jitsi_web.sha256_digest}"
env = {
PUBLIC_URL = "https://talk.chaoswest.tv",
TZ = "Europe/Berlin",
XMPP_AUTH_DOMAIN = "auth.meet.jitsi",
XMPP_BOSH_URL_BASE = "http://jitsi_prosody:5280",
XMPP_DOMAIN = "meet.jitsi",
XMPP_GUEST_DOMAIN = "guest.meet.jitsi",
XMPP_MUC_DOMAIN = "muc.meet.jitsi",
XMPP_RECORDER_DOMAIN = "recorder.meet.jitsi",
}
}
}
}

View File

@ -0,0 +1,14 @@
data "docker_network" "traefik" {
name = "traefik"
}
resource "docker_network" "jitsi" {
name = "jitsi"
attachable = true
driver = "overlay"
lifecycle {
ignore_changes = [labels]
}
}

View File

@ -0,0 +1,4 @@
variable "secrets" {
description = "map of secrets to be used by jitsi"
type = map(string)
}

View File

@ -0,0 +1,13 @@
terraform {
required_version = "1.5.5"
required_providers {
hetznerdns = {
source = "timohirt/hetznerdns"
version = "~>2.2"
}
docker = {
source = "kreuzwerker/docker"
version = "~>3.0"
}
}
}

View File

@ -0,0 +1,7 @@
# shepherd
This service is responsible for automatically updating the container images for Docker services.
It connects to the Docker socket, and looks for services with the label `shepherd.auto-update=true`. It then checks the Docker registry for a newer version of the image, and if one is found, it updates the service.
The checks are performed once per day.

View File

@ -0,0 +1,35 @@
data "docker_registry_image" "shepherd" {
name = "containrrr/shepherd:latest"
}
resource "docker_service" "shepherd" {
name = "shepherd"
task_spec {
container_spec {
image = "${data.docker_registry_image.shepherd.name}@${data.docker_registry_image.shepherd.sha256_digest}"
env = {
FILTER_SERVICES = "label=shepherd.auto-update=true"
SLEEP_TIME = "1d"
IMAGE_AUTOCLEAN_LIMIT = "2"
}
mounts {
target = "/var/run/docker.sock"
source = "/var/run/docker.sock"
type = "bind"
}
}
placement {
constraints = ["node.role == manager"]
}
}
lifecycle {
ignore_changes = [
task_spec[0].placement[0].platforms
]
}
}

View File

@ -0,0 +1,13 @@
terraform {
required_version = "1.5.5"
required_providers {
hetznerdns = {
source = "timohirt/hetznerdns"
version = "~>2.2"
}
docker = {
source = "kreuzwerker/docker"
version = "~>3.0"
}
}
}

View File

@ -0,0 +1,46 @@
data "docker_registry_image" "shit" {
name = "nginx:latest"
}
data "docker_network" "traefik" {
name = "traefik"
}
locals {
labels = {
"shepherd.auto-update" = "true",
"traefik.enable" = "true"
"traefik.http.services.shit.loadbalancer.server.port" = "80",
"traefik.http.routers.shit.rule" = "Host(`shit.montage2.de`)",
"traefik.http.routers.shit.tls" = "true",
"traefik.http.routers.shit.tls.certresolver" = "default",
}
}
resource "docker_service" "shit" {
name = "shit"
dynamic "labels" {
for_each = local.labels
content {
label = labels.key
value = labels.value
}
}
task_spec {
networks_advanced {
name = data.docker_network.traefik.id
}
container_spec {
image = "${data.docker_registry_image.shit.name}@${data.docker_registry_image.shit.sha256_digest}"
mounts {
target = "/usr/share/nginx/html/"
source = "/mnt/data/shit/html/"
type = "bind"
}
}
}
}

View File

@ -0,0 +1,24 @@
terraform {
required_version = "1.5.5"
required_providers {
hetznerdns = {
source = "timohirt/hetznerdns"
version = "~>2.2"
}
docker = {
source = "kreuzwerker/docker"
version = "~>3.0"
}
}
}
data "hetznerdns_zone" "primary" {
name = "montage2.de"
}
resource "hetznerdns_record" "primary" {
zone_id = data.hetznerdns_zone.primary.id
name = "shit"
value = "ax41-1.fsn.mon2.de."
type = "CNAME"
}

View File

@ -0,0 +1,16 @@
http:
routers:
dashboard-secure:
rule: Host(`traefik.montage2.de`)
service: api@internal
tls:
certResolver: default
middlewares:
- auth-hausmeister
entryPoints:
- https
middlewares:
auth-hausmeister:
basicAuth:
users:
- "hausmeister:$2y$10$.ewz0qQlm.mT/LRzuSwRYOmytRj7K3ojcxFsvkgrMKFicbA5EtKV."

View File

@ -0,0 +1,40 @@
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
swarmMode: true
network: "traefik"
file:
directory: "/etc/traefik/dynamic"
watch: true
entryPoints:
http:
address: ":80"
http:
redirections:
entryPoint:
to: https
scheme: https
https:
address: ":443"
metrics:
address: ":8081"
mqtt:
address: ":8883"
api:
dashboard: true
certificatesResolvers:
default:
acme:
email: post@chaoswest.tv
storage: /acme/acme.json
dnsChallenge:
provider: hetzner
delayBeforeCheck: "60"
metrics:
prometheus:
entryPoint: metrics

View File

@ -0,0 +1,17 @@
resource "docker_config" "traefik" {
name = "traefik-cfg-traefik-${replace(timestamp(), ":", ".")}"
data = base64encode(file("${path.module}/cfg/traefik.yaml"))
lifecycle {
ignore_changes = [name]
create_before_destroy = true
}
}
resource "docker_config" "traefik_dynamic" {
name = "traefik-cfg-dynamic-${replace(timestamp(), ":", ".")}"
data = base64encode(file("${path.module}/cfg/dynamic/dynamic.yaml"))
lifecycle {
ignore_changes = [name]
create_before_destroy = true
}
}

View File

@ -0,0 +1,126 @@
data "docker_registry_image" "traefik" {
name = "traefik:v2.9"
}
resource "docker_secret" "hetzner_dns_api_token" {
name = "traefik_hetzner_dns_api_token-${replace(timestamp(), ":", ".")}"
data = base64encode(var.hetzner_dns_api_token)
lifecycle {
ignore_changes = [name]
create_before_destroy = true
}
}
resource "docker_network" "traefik" {
name = "traefik"
attachable = true
driver = "overlay"
lifecycle {
ignore_changes = [labels]
}
}
resource "docker_volume" "traefik_acme" {
name = "traefik_acme"
lifecycle {
prevent_destroy = true
}
}
resource "docker_service" "traefik" {
name = "traefik"
mode {
global = true
}
endpoint_spec {
ports {
target_port = 80
published_port = 80
protocol = "tcp"
publish_mode = "host"
}
ports {
target_port = 443
published_port = 443
protocol = "tcp"
publish_mode = "host"
}
ports {
target_port = 443
published_port = 443
protocol = "udp"
publish_mode = "host"
}
ports {
target_port = 8883
published_port = 8883
protocol = "tcp"
publish_mode = "host"
}
}
task_spec {
networks_advanced {
name = docker_network.traefik.id
}
container_spec {
image = "${data.docker_registry_image.traefik.name}@${data.docker_registry_image.traefik.sha256_digest}"
env = {
HETZNER_API_KEY_FILE = "/hetznerdns-token"
}
secrets {
secret_id = docker_secret.hetzner_dns_api_token.id
secret_name = docker_secret.hetzner_dns_api_token.name
file_name = "/hetznerdns-token"
file_uid = "0"
file_gid = "0"
file_mode = "0400"
}
labels {
label = "shepherd.auto-update"
value = "true"
}
mounts {
target = "/var/run/docker.sock"
source = "/var/run/docker.sock"
type = "bind"
}
mounts {
target = "/acme"
source = docker_volume.traefik_acme.name
type = "volume"
}
configs {
config_id = docker_config.traefik.id
config_name = docker_config.traefik.name
file_name = "/etc/traefik/traefik.yaml"
file_uid = "0"
file_gid = "0"
file_mode = "0400"
}
configs {
config_id = docker_config.traefik_dynamic.id
config_name = docker_config.traefik_dynamic.name
file_name = "/etc/traefik/dynamic/dynamic.yaml"
file_uid = "0"
file_gid = "0"
file_mode = "0400"
}
}
}
}

View File

@ -0,0 +1,4 @@
variable "hetzner_dns_api_token" {
description = "Hetzner DNS API token used to solve ACME DNS-01 challenges"
type = string
}

View File

@ -0,0 +1,13 @@
terraform {
required_version = "1.5.5"
required_providers {
hetznerdns = {
source = "timohirt/hetznerdns"
version = "~>2.2"
}
docker = {
source = "kreuzwerker/docker"
version = "~>3.0"
}
}
}

1
sops-age-recipients.txt Normal file
View File

@ -0,0 +1 @@
age1zwv4tl8ws6ke8wseenq4lrwcck3el2wandlgztefz9v4qdlnwu7saw7g8z

View File

@ -0,0 +1,55 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/carlpett/sops" {
version = "1.0.0"
constraints = "~> 1.0"
hashes = [
"h1:tnN2Mgl0NUF3cg7a0HtGmtOhHcG+tkaT6ncOPRuA9l8=",
"zh:064e63ea800cd1a8e575064097bc7de6fd5faa8ad50dbb3f2f9d8a3ebc9d7b97",
"zh:0663900085949d2faf24c170c7cdfbf76e545797915cc331da8304144c02bf27",
"zh:2ff26c7e5ee356c30791a12dd8e114c6237bd873d09e52805cb30dd5d758ed23",
"zh:44211fa474112ad0c9fcdae03f13ec7c75cdefd3ab29979b99cb834208055593",
"zh:6c3ab441c12b9679ad1dcac580d1ee7782f0d94efe6da6e983435ed39335cd3f",
"zh:8924cc939b52382ef042dc38bde93cdf438ff0aeab5e1801fbd198f05b80cd47",
"zh:ebc189ce22c23b903399f71e33d465001a79d7de7f7bf115c7763fcf794f4b58",
]
}
provider "registry.terraform.io/kreuzwerker/docker" {
version = "3.0.2"
constraints = "~> 3.0"
hashes = [
"h1:tryCE8s9BiT6VyfnGgU1mUt9s0HcCKlRERdLd2fr010=",
"zh:15b0a2b2b563d8d40f62f83057d91acb02cd0096f207488d8b4298a59203d64f",
"zh:23d919de139f7cd5ebfd2ff1b94e6d9913f0977fcfc2ca02e1573be53e269f95",
"zh:38081b3fe317c7e9555b2aaad325ad3fa516a886d2dfa8605ae6a809c1072138",
"zh:4a9c5065b178082f79ad8160243369c185214d874ff5048556d48d3edd03c4da",
"zh:5438ef6afe057945f28bce43d76c4401254073de01a774760169ac1058830ac2",
"zh:60b7fadc287166e5c9873dfe53a7976d98244979e0ab66428ea0dea1ebf33e06",
"zh:61c5ec1cb94e4c4a4fb1e4a24576d5f39a955f09afb17dab982de62b70a9bdd1",
"zh:a38fe9016ace5f911ab00c88e64b156ebbbbfb72a51a44da3c13d442cd214710",
"zh:c2c4d2b1fd9ebb291c57f524b3bf9d0994ff3e815c0cd9c9bcb87166dc687005",
"zh:d567bb8ce483ab2cf0602e07eae57027a1a53994aba470fa76095912a505533d",
"zh:e83bf05ab6a19dd8c43547ce9a8a511f8c331a124d11ac64687c764ab9d5a792",
"zh:e90c934b5cd65516fbcc454c89a150bfa726e7cf1fe749790c7480bbeb19d387",
"zh:f05f167d2eaf913045d8e7b88c13757e3cf595dd5cd333057fdafc7c4b7fed62",
"zh:fcc9c1cea5ce85e8bcb593862e699a881bd36dffd29e2e367f82d15368659c3d",
]
}
provider "registry.terraform.io/timohirt/hetznerdns" {
version = "2.2.0"
constraints = "~> 2.2"
hashes = [
"h1:HyskQAglrOueur79gSCBgx9MNDOs0tz39aNYQiFgxz8=",
"zh:5bb0ab9f62be3ed92070235e507f3c290491d51391ef4edcc70df53b65a83019",
"zh:5ccdfac7284f5515ac3cff748336b77f21c64760e429e811a1eeefa8ebb86e12",
"zh:687c35665139ae37c291e99085be2e38071f6b355c4e1e8957c5a6a3bcdf9caf",
"zh:6de27f0d0d1513b3a4b7e81923b4a8506c52759bd466e2b4f8156997b0478931",
"zh:85770a9199a4c2d16ca41538d7a0f7a7bfc060678104a1faac19213e6f0a800c",
"zh:a5ff723774a9ccfb27d5766c5e6713537f74dd94496048c89c5d64dba597e59e",
"zh:bf9ab76fd37cb8aebb6868d73cbe8c08cee36fc25224cc1ef5949efa3c34b06c",
"zh:db998fe3bdcd4902e99fa470bb3f355883170cf4c711c8da0b5f1f4510f1be41",
]
}

35
stacks/ax41-1/main.tf Normal file
View File

@ -0,0 +1,35 @@
data "hetznerdns_zone" "chaoswest_tv" {
name = "chaoswest.tv"
}
module "shepherd" {
source = "../../modules/swarm/shepherd"
}
module "deckchores" {
source = "../../modules/swarm/deckchores"
}
module "traefik" {
source = "../../modules/swarm/traefik"
hetzner_dns_api_token = data.sops_file.secrets.data["hetzner_dns_api_token"]
}
module "grafana" {
source = "../../modules/swarm/grafana"
secrets = yamldecode(data.sops_file.secrets.raw).swarm.grafana
}
module "jitsi" {
source = "../../modules/swarm/jitsi"
secrets = yamldecode(data.sops_file.secrets.raw).swarm.jitsi
}
module "hedgedoc" {
source = "../../modules/swarm/hedgedoc"
secrets = yamldecode(data.sops_file.secrets.raw).swarm.hedgedoc
}
module "shit" {
source = "../../modules/swarm/shit"
}

10
stacks/ax41-1/provider.tf Normal file
View File

@ -0,0 +1,10 @@
provider "docker" {
host = "ssh://hausmeister@ax41-1.fsn.mon2.de"
ssh_opts = ["-o", "StrictHostKeyChecking=no", "-o", "UserKnownHostsFile=/dev/null"]
}
provider "sops" {}
provider "hetznerdns" {
apitoken = data.sops_file.secrets.data["hetzner_dns_api_token"]
}

View File

@ -0,0 +1,39 @@
hetzner_dns_api_token: ENC[AES256_GCM,data:6m0svnZBgwLeMu12tSz1oUHHaA69dU/dHi7oWgvbrNE=,iv:Iwre9h80vPZRZ3qu43gohDOaE88Aea5T2vc+3hVmM5I=,tag:9aad0ekU962w9acNXpe94w==,type:str]
swarm:
grafana:
gf_security_secret_key: ENC[AES256_GCM,data:o6iR/KCnM1Ru56i0ANylmZFqiERuNfwx8sHeM4wE/k7aHI8l0Lg2oifrTx38m5puCBw79U4qqFQqe9QJjZMROhRblBK/RWi+3vSv+rwYv2W8wcvMeqcjJgZV0erOMyFkuZpapbfTnbDvidWg4kKbS0k2OmdRHVYc89IETU7hdow=,iv:B5EC8gwEK0g8T2I/EgoLFGCzR0Uhtaot01xeoWU0mp0=,tag:vlYNnn+fI5m6Dw+IpTPfuw==,type:str]
gf_auth_gitlab_client_id: ENC[AES256_GCM,data:xZfnyduTnDVpenAMqqcETqQiaphfVdKXJ7lsloynL1ka1V6FTz5i85MK8mgrRPY4zLiPCgmp8gzFiV2PDxDLOQ==,iv:egOxWveuFg9hyQAhO9V5ILwNSjmm4QLSYZQhknZPjvU=,tag:GRLP3QERCZ0hhJ9GpSmWxg==,type:str]
gf_auth_gitlab_client_secret: ENC[AES256_GCM,data:SgNbtzg8o0U2NJi2CN3Dmlox7F3v7boURNBZytxuOgnFi6N30xSUW0gHT1UivSEz9V+lo1km4tMI2+HqrukzPQ==,iv:Qbo/T5Er+8oY6rYfbPNHRKFuWLpAnRMAlQQ0Fp/FEKg=,tag:sUNsXhywIkt3Vcq+rIABoA==,type:str]
gf_auth_generic_oauth_client_id: ENC[AES256_GCM,data:XLp880bI5ANkbk1t49839gs8EwI1LlfZwJgtlVPd21jmhXkpRSuKLw==,iv:u+wkKImGyNo0t7nXg7MdbRBBDEC08tWfE1sMo5fbIcY=,tag:CXl/6P9U6kANNFrmDlLvnA==,type:str]
gf_auth_generic_oauth_client_secret: ENC[AES256_GCM,data:AHWpqQbHVyL6rliQeOsf+hVEwDI0mFOuOtllz3Aqfm/FWQTai4sgdQ9F+AkuiqCsKtdvOyCYdsBNv6BolR1Ef8vNBECKpkt1N97hPvlEuStBzbt0Hcu9uHxo0h1+L10SaM7VzUfVcYJ2gNY/HdU01C7av1NUHsYBMlIb8vE+Eww=,iv:ax6nnqrvYrxa5m08Hby6hCkawJZoJYBVA4JiPur10AU=,tag:ilSrpwsp3GC/AT9CgrfsGw==,type:str]
jitsi:
jibri_recorder_password: ENC[AES256_GCM,data:RS64hUpIZCF5a8XMhoSk5X+ICe3+1YXzfu5xY13VpIQ=,iv:lwWwxDHZLbQ0ITuBUFAyybYdlvbM9T5b/cPqBnW5oa8=,tag:now385b+72CSU595I4m96Q==,type:str]
jibri_xmpp_password: ENC[AES256_GCM,data:XxOmZuUCyNxfIgk3FAp4mbBDtwOxus6x2Qe/kPSHFQo=,iv:xHMZxNyZo+eJfh8DqDxoRDzYOP0Xc918KEXujwDaqfQ=,tag:m/fRIDcU/Y9ifu/82YoIuQ==,type:str]
jigasi_xmpp_password: ENC[AES256_GCM,data:Bvhm2/alyR1K9BxTOibgioUKLjJ83C6m/humfxjMw9g=,iv:qXNpDYW5E19g9da9eHyGJGxw6wqLZvGHZyo1cvRXxFs=,tag:Css7lGS2WuzcRk3TQGO8ZA==,type:str]
jicofo_auth_password: ENC[AES256_GCM,data:g2o4JBlKePPFSI6JKCQnVEtKv9XgKINc0rOHJ9qzguE=,iv:+L3gNJji/nOiImQ1cFLfT7KeGOdk1AcK6hXQdGCDkV4=,tag:LXZmoXTPxLq80xNTsDWQUA==,type:str]
jvb_auth_password: ENC[AES256_GCM,data:w0snMcVCm8wSzxzSTPNO8IuPhS6LvrLAv2t7s3fzUHI=,iv:R0unYAzJXAwDTO2mE3nD/bjJy9aGlHlOR2122gc/HFg=,tag:kE8WDsrpGFwMqPFYTRCz4A==,type:str]
hedgedoc:
CMD_GITLAB_CLIENTID: ENC[AES256_GCM,data:kZvl37vR+/+fHFGCgq3Lj29N1UY6GHfjUeC9jl5kwqqVrKcW2U4Rpocp4oR7FmcByUe1KmmDje0ebrq7rq6kkQ==,iv:037nPII9Tz/VXbemgONHtw6rf7YhywwkucpQP0I+vHA=,tag:qeNYqDBbBgyEIbtz69sZmg==,type:str]
CMD_GITLAB_CLIENTSECRET: ENC[AES256_GCM,data:htsR1iVbrGzvvy895ovDxGUemWLxbhHSkJEGcsXk5nwtOoLt1I4N6XNLlPSQ94EzZLJ65bF8Ay3l8nK11B/waw==,iv:aOyhuicriQB2gQJSzKzFvlEK6ZI6zeF5gz4Jjy3anDE=,tag:ldyjLIswF+0Uh8STV0Jmpw==,type:str]
CMD_OAUTH2_CLIENT_ID: ENC[AES256_GCM,data:rwt7C+jxcXZRgAHq5D0iIsPCjBjDiBoUb8bYVQtA6qejqDmfXGHAzg==,iv:HL6N9f6gysDJwUYJKV18fdqy+2zAUOiL9HK+k5DvieU=,tag:CqZb/U5LSDBo4K1X79rOTQ==,type:str]
CMD_OAUTH2_CLIENT_SECRET: ENC[AES256_GCM,data:F3lebEE0hnwopmKEAD3ySrMEnnjMBy0hgaKcLkZwelIR+j5kR8fUtD2ITndKgQLFhPQXvC5PAkTW93I3tu9Z91/TseIcgZsivxXmRdScMucY0Wmrt/8+yAHKPZxhDsaZbvRnVhwL6v22/eqnmTNDbn62r9I6QfZUqwrogV2wY1I=,iv:vibJn1HyaQ/xgsQvSp7e+cgGhZmsDrsUz/b78XG63oU=,tag:7rKcELyRIkKu2SByXwSTNQ==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age:
- recipient: age1zwv4tl8ws6ke8wseenq4lrwcck3el2wandlgztefz9v4qdlnwu7saw7g8z
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnK0FEdDBxN0JjVXRyUFZO
Qk1KQVlWMlF5RFlpeHNtL2VjRFpmcU8vc0VNCmtkcDBVTFBiaW5PUVhrTExWVjFa
SU1UdlhrN21IUlZVejljcW5kZEx5b2sKLS0tIHhWdjdpTitrbUhNRzZucGpzeDZz
WmlRUnowa2lMNWpDT0xEU0htV0w3U00K1f/SO/FBvC9lIBzveBEwhopj5ryMVCmD
jw8AdxvmMwsCSfIROKkzMqiUs2zsj6FOMlYFI1Rb07mItSO2Yd7TsA==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2024-01-25T17:33:50Z"
mac: ENC[AES256_GCM,data:VUSGKproAmU286+mf9nM/IzddWcT18/6tk73guFUB6C3Turfv9DXDW1wSXu6vTxGlOinChxlcBCnsGfk7gfFisjAEZHJq9IJ0P2myYp+lKbybolm0fbTZ4jda7DUnvN3n4I0EqFoUa/vPN/DSkdt0hKj1Ayz5AdFIAvOtkphMPA=,iv:dHhMBT3T3bWPSUadDgo+h2KSf2qC32q+nM26eK9ivDo=,tag:30dNpRvB6vet5UoWy/DZtg==,type:str]
pgp: []
unencrypted_suffix: _unencrypted
version: 3.8.1

4
stacks/ax41-1/sops.tf Normal file
View File

@ -0,0 +1,4 @@
data "sops_file" "secrets" {
source_file = "secrets.enc.yaml"
input_type = "yaml"
}

24
stacks/ax41-1/version.tf Normal file
View File

@ -0,0 +1,24 @@
terraform {
required_version = "1.5.5"
backend "s3" {
bucket = "cwtv-tf"
key = "infrastructure/ax41-1/terraform.tfstate"
region = "eu-central-1"
}
required_providers {
hetznerdns = {
source = "timohirt/hetznerdns"
version = "~>2.2"
}
docker = {
source = "kreuzwerker/docker"
version = "~>3.0"
}
sops = {
source = "carlpett/sops"
version = "~>1.0"
}
}
}