Skip to content

Commit 2b35435

Browse files
feat: Implement Terraform module for Mediamtx Relay service with EC2 deployment and configuration
1 parent 80523e1 commit 2b35435

File tree

10 files changed

+460
-39
lines changed

10 files changed

+460
-39
lines changed

.github/workflows/deploy.yaml

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Deploy to Elastic Beanstalk
1+
name: Deploy Infrastructure and Services
22

33
on:
44
push:
@@ -21,18 +21,20 @@ jobs:
2121
path: control_broker
2222
application: "controll server"
2323
environment: "Controllserver-env"
24+
deployer: elasticbeanstalk
2425
- service: stream-cleaner
2526
path: stream_cleaner
2627
application: "Stream-cleaner"
2728
environment: "Stream-cleaner-env"
29+
deployer: elasticbeanstalk
2830
- service: visual-controller
2931
path: visual_controller
3032
application: "visual-controller"
3133
environment: "Visual-controller-env"
34+
deployer: elasticbeanstalk
3235
- service: media-relay
33-
path: media_relay
34-
application: "media-relay"
35-
environment: "Media-relay-env"
36+
path: infra/terraform/media_relay
37+
deployer: terraform
3638
steps:
3739
- name: Checkout repository
3840
uses: actions/checkout@v4
@@ -50,6 +52,12 @@ jobs:
5052
env:
5153
AWS_SHARED_CREDENTIALS_FILE: ~/.aws/credentials
5254

55+
- name: Set up Terraform
56+
if: ${{ matrix.deployer == 'terraform' }}
57+
uses: hashicorp/setup-terraform@v3
58+
with:
59+
terraform_version: 1.7.5
60+
5361
- name: Build visual controller frontend
5462
if: ${{ matrix.service == 'visual-controller' }}
5563
shell: bash
@@ -68,25 +76,19 @@ jobs:
6876
id: package
6977
working-directory: ${{ matrix.path }}
7078
shell: bash
79+
if: ${{ matrix.deployer == 'elasticbeanstalk' }}
7180
run: |
7281
ZIP_NAME="${{ matrix.service }}-${{ steps.suffix.outputs.value }}.zip"
7382
shopt -s dotglob
7483
zip -r "../${ZIP_NAME}" . -x "*/__pycache__/*" -x "*.pyc"
7584
echo "zip-name=${ZIP_NAME}" >> "$GITHUB_OUTPUT"
7685
77-
- name: Validate media relay package contents
78-
if: ${{ matrix.service == 'media-relay' }}
79-
env:
80-
ZIP_NAME: ${{ steps.package.outputs.zip-name }}
81-
shell: bash
82-
run: |
83-
unzip -l "${ZIP_NAME}" | grep '.platform/elasticbeanstalk/docker/container-configuration.json'
84-
8586
- name: Upload package to S3
8687
id: upload
8788
env:
8889
ZIP_NAME: ${{ steps.package.outputs.zip-name }}
8990
shell: bash
91+
if: ${{ matrix.deployer == 'elasticbeanstalk' }}
9092
run: |
9193
S3_KEY="deployments/${{ matrix.service }}/${ZIP_NAME}"
9294
aws s3 cp "${ZIP_NAME}" "s3://${{ env.S3_BUCKET }}/${S3_KEY}"
@@ -97,6 +99,7 @@ jobs:
9799
env:
98100
VERSION_LABEL: ${{ matrix.service }}-${{ steps.suffix.outputs.value }}
99101
shell: bash
102+
if: ${{ matrix.deployer == 'elasticbeanstalk' }}
100103
run: |
101104
aws elasticbeanstalk create-application-version \
102105
--application-name "${{ matrix.application }}" \
@@ -107,7 +110,40 @@ jobs:
107110
108111
- name: Update Elastic Beanstalk environment
109112
shell: bash
113+
if: ${{ matrix.deployer == 'elasticbeanstalk' }}
110114
run: |
111115
aws elasticbeanstalk update-environment \
112116
--environment-name "${{ matrix.environment }}" \
113117
--version-label "${{ steps.version.outputs.version-label }}"
118+
119+
- name: Write terraform.tfvars from secret
120+
if: ${{ matrix.deployer == 'terraform' }}
121+
working-directory: ${{ matrix.path }}
122+
env:
123+
TFVARS_B64: ${{ secrets.MEDIA_RELAY_TFVARS_B64 }}
124+
shell: bash
125+
run: |
126+
if [[ -z "$TFVARS_B64" ]]; then
127+
echo "MEDIA_RELAY_TFVARS_B64 secret is not set" >&2
128+
exit 1
129+
fi
130+
echo "$TFVARS_B64" | base64 -d > terraform.tfvars
131+
132+
- name: Terraform init
133+
if: ${{ matrix.deployer == 'terraform' }}
134+
working-directory: ${{ matrix.path }}
135+
shell: bash
136+
env:
137+
TF_IN_AUTOMATION: 1
138+
run: |
139+
terraform init -input=false
140+
141+
- name: Terraform apply
142+
if: ${{ matrix.deployer == 'terraform' }}
143+
working-directory: ${{ matrix.path }}
144+
shell: bash
145+
env:
146+
TF_IN_AUTOMATION: 1
147+
run: |
148+
terraform apply -input=false -auto-approve
149+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.terraform/
2+
.terraform.lock.hcl
3+
terraform.tfstate
4+
terraform.tfstate.*
5+
terraform.tfvars
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Mediamtx Relay Terraform Stack
2+
3+
This module provisions everything required to host the relay on a single EC2 instance with an attached Elastic IP. The instance boots Amazon Linux 2023, installs Docker, writes the Mediamtx configuration, and launches the upstream `bluenviron/mediamtx` container with all required TCP and UDP ports published.
4+
5+
## What gets created
6+
7+
- Security group exposing TCP `8554`, `1935`, `8888`, `8889`, `9998`, `9999` and UDP `8200` to the CIDR list you supply.
8+
- EC2 instance (default `t3.small`) running in the chosen subnet/VPC.
9+
- User data script that installs Docker and runs Mediamtx with the configuration rendered from Terraform inputs.
10+
- Elastic IP associated with the instance for a stable ingress point.
11+
- If you supply `existing_eip_allocation_id`, the module reuses that Elastic IP instead of allocating a new one.
12+
13+
You can optionally set `vpc_id`, `subnet_id`, or `key_name` to place the instance in a specific network or enable SSH access.
14+
15+
Key variables:
16+
17+
| Name | Description | Default |
18+
| --- | --- | --- |
19+
| `region` | AWS region to deploy into | `us-east-1` |
20+
| `instance_type` | EC2 instance type | `t3.small` |
21+
| `publish_user` / `publish_pass` | Credentials the SBC uses to publish | **required** |
22+
| `viewer_user` / `viewer_pass` | Playback credentials (`viewer_user=any` allows anonymous access) | `any` / empty |
23+
| `allowed_cidrs` | List of CIDR blocks allowed to reach the relay | `["0.0.0.0/0"]` |
24+
| `existing_eip_allocation_id` | Allocation ID of an existing Elastic IP to reuse | `null` |
25+
| `mediamtx_version` | Container tag pulled from Docker Hub | `1.15.3` |
26+
| `tags` | Extra tags applied to every resource | `{}` |
27+
28+
## Scaling & updates
29+
30+
- **Scale vertically:** change `instance_type` and re-apply.
31+
- **Rotate credentials or config:** update the related variables and run `terraform apply`; the user data script re-renders the config and restarts the container on the next reboot. To force an immediate restart, use `terraform taint aws_instance.mediamtx` followed by `terraform apply`.
32+
- **Tear down:** run `terraform destroy` to remove the EC2 instance, security group, and Elastic IP.
33+
34+
## GitHub Actions automation
35+
36+
The `Deploy Infrastructure and Services` workflow expects a base64-encoded `terraform.tfvars` stored in the repository secret `MEDIA_RELAY_TFVARS_B64`. To generate it locally:
37+
38+
```bash
39+
cd infra/terraform/media_relay
40+
terraform fmt
41+
cat terraform.tfvars | base64 -w0 # on macOS: cat terraform.tfvars | base64 | tr -d '\n'
42+
```
43+
44+
Copy the output into the GitHub secret. The workflow decodes the file into `terraform.tfvars` before running `terraform init` and `terraform apply`.
45+
46+
## terraform.tfvars example
47+
48+
Create a `terraform.tfvars` (or copy `terraform.tfvars.example` if you create one) to store sensitive values locally:
49+
50+
```hcl
51+
region = "us-east-1"
52+
publish_user = "robot"
53+
publish_pass = "change-me"
54+
viewer_user = "any"
55+
viewer_pass = ""
56+
allowed_cidrs = ["203.0.113.0/24", "198.51.100.10/32"]
57+
#existing_eip_allocation_id = "eipalloc-0123456789abcdef0"
58+
```
59+
60+
Never commit files that contain secrets.
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
terraform {
2+
required_version = ">= 1.6.0"
3+
4+
required_providers {
5+
aws = {
6+
source = "hashicorp/aws"
7+
version = "~> 5.0"
8+
}
9+
}
10+
}
11+
12+
provider "aws" {
13+
region = var.region
14+
}
15+
16+
data "aws_vpc" "default" {
17+
count = var.vpc_id == null ? 1 : 0
18+
default = true
19+
}
20+
21+
locals {
22+
vpc_id = var.vpc_id != null ? var.vpc_id : data.aws_vpc.default[0].id
23+
}
24+
25+
data "aws_subnet_ids" "selected" {
26+
vpc_id = local.vpc_id
27+
}
28+
29+
data "aws_eip" "existing" {
30+
count = var.existing_eip_allocation_id != null ? 1 : 0
31+
id = var.existing_eip_allocation_id
32+
}
33+
34+
locals {
35+
subnet_id = var.subnet_id != null ? var.subnet_id : data.aws_subnet_ids.selected.ids[0]
36+
tcp_ports = [8554, 1935, 8888, 8889, 9998, 9999]
37+
udp_ports = [8200]
38+
port_rules = concat(
39+
[for p in local.tcp_ports : {
40+
port = p
41+
protocol = "tcp"
42+
}],
43+
[for p in local.udp_ports : {
44+
port = p
45+
protocol = "udp"
46+
}]
47+
)
48+
merged_tags = merge({
49+
Name = var.name
50+
}, var.tags)
51+
}
52+
53+
data "aws_ami" "al2023" {
54+
most_recent = true
55+
owners = ["amazon"]
56+
57+
filter {
58+
name = "name"
59+
values = ["al2023-ami-*-x86_64"]
60+
}
61+
62+
filter {
63+
name = "architecture"
64+
values = ["x86_64"]
65+
}
66+
}
67+
68+
locals {
69+
mediamtx_config = templatefile("${path.module}/templates/mediamtx.yaml.tpl", {
70+
log_level = var.log_level
71+
publish_user = var.publish_user
72+
publish_pass = var.publish_pass
73+
viewer_user = var.viewer_user
74+
viewer_pass = var.viewer_pass
75+
})
76+
77+
user_data = templatefile("${path.module}/templates/user_data.sh.tpl", {
78+
mediamtx_config = local.mediamtx_config
79+
mediamtx_version = var.mediamtx_version
80+
})
81+
}
82+
83+
resource "aws_security_group" "mediamtx" {
84+
name = "${var.name}-sg"
85+
description = "Network rules for the Mediamtx relay"
86+
vpc_id = local.vpc_id
87+
88+
dynamic "ingress" {
89+
for_each = local.port_rules
90+
content {
91+
description = "Allow ${ingress.value.protocol} port ${ingress.value.port}"
92+
from_port = ingress.value.port
93+
to_port = ingress.value.port
94+
protocol = ingress.value.protocol
95+
cidr_blocks = var.allowed_cidrs
96+
}
97+
}
98+
99+
egress {
100+
from_port = 0
101+
to_port = 0
102+
protocol = "-1"
103+
cidr_blocks = ["0.0.0.0/0"]
104+
ipv6_cidr_blocks = ["::/0"]
105+
}
106+
107+
tags = local.merged_tags
108+
}
109+
110+
resource "aws_instance" "mediamtx" {
111+
ami = data.aws_ami.al2023.id
112+
instance_type = var.instance_type
113+
subnet_id = local.subnet_id
114+
115+
associate_public_ip_address = true
116+
key_name = var.key_name
117+
vpc_security_group_ids = [aws_security_group.mediamtx.id]
118+
119+
user_data = local.user_data
120+
121+
tags = local.merged_tags
122+
}
123+
124+
resource "aws_eip" "mediamtx" {
125+
count = var.existing_eip_allocation_id == null ? 1 : 0
126+
domain = "vpc"
127+
instance = aws_instance.mediamtx.id
128+
tags = local.merged_tags
129+
}
130+
131+
resource "aws_eip_association" "mediamtx" {
132+
count = var.existing_eip_allocation_id != null ? 1 : 0
133+
allocation_id = var.existing_eip_allocation_id
134+
instance_id = aws_instance.mediamtx.id
135+
}
136+
137+
output "instance_id" {
138+
description = "ID of the EC2 instance running Mediamtx"
139+
value = aws_instance.mediamtx.id
140+
}
141+
142+
output "elastic_ip" {
143+
description = "Elastic IP address associated with the relay"
144+
value = var.existing_eip_allocation_id != null
145+
? data.aws_eip.existing[0].public_ip
146+
: aws_eip.mediamtx[0].public_ip
147+
}
148+
149+
output "public_dns" {
150+
description = "Public DNS name of the instance"
151+
value = aws_instance.mediamtx.public_dns
152+
}
153+
154+
output "security_group_id" {
155+
description = "Security group controlling relay ingress"
156+
value = aws_security_group.mediamtx.id
157+
}

0 commit comments

Comments
 (0)