Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
name: Deploy

on:
push:
branches: [main]
paths:
- 'frontend/**'
- 'backend/**'
- 'infrastructure/**'
- '.github/workflows/deploy.yaml'

env:
IMAGE_NAME: 'ghcr.io/${{ github.repository }}'

jobs:
build:
name: 'Build ${{ matrix.app }}'
runs-on: ubuntu-latest
strategy:
matrix:
app: [frontend, backend]
fail-fast: false
permissions:
actions: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: 'ghcr.io'
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: 'Build ${{ matrix.app }} with cache and push to registry'
uses: docker/build-push-action@v5
with:
push: 'true'
tags: '${{ env.IMAGE_NAME }}/${{ matrix.app }}:latest,${{ env.IMAGE_NAME}}/${{ matrix.app }}:${{ github.sha }}'
cache-from: 'type=registry,ref=${{ env.IMAGE_NAME }}:latest'
cache-to: 'type=inline'
context: '${{ matrix.app }}'

deploy:
name: 'Deploy using Terraform'
runs-on: ubuntu-latest
needs: [build]
env:
ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
TF_VAR_revision_suffix: ${{ github.sha }}
TF_VAR_repository_owner: ${{ github.repository_owner }}
TF_VAR_api_key: ${{ secrets.API_KEY }}
TF_VAR_connection_string: ${{ secrets.SUPABASE_CONNECTION_STRING }}
defaults:
run:
working-directory: 'infrastructure'
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Terraform
uses: hashicorp/setup-terraform@v3

- name: Init Terraform
run: terraform init

- name: Run Terraform plan
run: terraform plan

- name: Run Terraform apply
run: terraform apply -auto-approve
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.terraform/
.terraform.lock.hcl
terraform.tfvars
70 changes: 70 additions & 0 deletions infrastructure/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Infrastructure

## Kom i gang

Last ned:

- WSL
- Git
- Azure CLI

### 1. Fork repoet

Fork dette repoet til din egen GitHub-konto (samme hvilken).

Eventuelt opprett en GitHub-konto hvis du ikke har en fra før.

### 2. Lag en Azure-konto

Registrer deg for en gratis Azure-konto [her](https://azure.microsoft.com/free).

### 3. Autentiser deg med Azure CLI

```bash
az login
```

Følg flyten i nettleseren.

### 4. Lag Terraform-state

```bash
./create-terraform-state.sh
```

### 5. Lag en Azure-konto for Terraform

```bash
./create-azure-service-principal.sh
```

### 5.1. Legg til secrets i GitHub for Terraform

Du vil få ut fire verdier:

```bash
ARM_CLIENT_ID=...
ARM_CLIENT_SECRET=...
ARM_SUBSCRIPTION_ID=...
ARM_TENANT_ID=...
```

Lagre disse inn som secrets i GitHub-repoet ditt.
Gå til "Settings" -> "Secrets and variables" -> "Actions" -> "New repository secret".

### 5.2. Legg til secrets i GitHub for backend

Du har tidligere laget en connection string for Supabase og en API-nøkkel for frontend/backend.
Lagre disse som secrets i GitHub-repoet ditt med navnene `SUPABASE_CONNECTION_STRING` og `API_KEY`.

### 6. Push lokale endringer til GitHub

```bash
git add .
git commit -m "Lage Terraform state og Azure Service Principal"
git push
```

### 7. Følg med på GitHub Actions

Du vil til slutt få ut en URL for frontend og backend.
18 changes: 18 additions & 0 deletions infrastructure/create-azure-service-principal.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/bash

main() {
# Get subscription ID
local subscription_id
subscription_id=$(az account show --query id -o tsv)

# Create a service principal with Contributor role
local service_principal
service_principal=$(az ad sp create-for-rbac --role="Contributor" --scopes="/subscriptions/$subscription_id" --name "terraform")

echo "ARM_CLIENT_ID=$(echo "$service_principal" | jq -r '.appId')"
echo "ARM_CLIENT_SECRET=$(echo "$service_principal" | jq -r '.password')"
echo "ARM_SUBSCRIPTION_ID=$subscription_id"
echo "ARM_TENANT_ID=$(echo "$service_principal" | jq -r '.tenant')"
}

main "$@"
106 changes: 106 additions & 0 deletions infrastructure/create-terraform-state.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/bin/bash

set -eoux pipefail

create() {
local resource_group_name="$1"
local storage_account_name="$2"
local container_name="$3"
local location="$4"

# Create resource group
az group create \
--name "$resource_group_name" \
--location "$location"

# Enable resource providers
az provider register \
--namespace Microsoft.Storage \

az provider register \
--namespace Microsoft.App \

sleep 30s

# Create storage account
az storage account create \
--resource-group "$resource_group_name" \
--name "$storage_account_name" \
--sku Standard_LRS \
--encryption-services blob

# Create blob container
az storage container create \
--name "$container_name" \
--account-name "$storage_account_name"

# Generate providers.tf file
printf "Generating providers.tf file...\n"
cat << EOF > providers.tf
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}

backend "azurerm" {
resource_group_name = "$resource_group_name"
storage_account_name = "$storage_account_name"
container_name = "$container_name"
key = "terraform.tfstate"
}
}

provider "azurerm" {
features {}
}
EOF
printf "Successfully generated providers.tf file.\n"

}

delete() {
local resource_group_name="$1"
local storage_account_name="$2"
local container_name="$3"

# Remove the generated providers.tf file
rm -f providers.tf

# Delete blob container
az storage container delete \
--name "$container_name" \
--account-name "$storage_account_name"

# Delete storage account
az storage account delete \
--name "$storage_account_name" \
--resource-group "$resource_group_name"

# Delete resource group
az group delete \
--name "$resource_group_name" \
--yes
}

main() {
local resource_group_name='tfstate'
local container_name='tfstate'
local location='westeurope'

local storage_account_name
storage_account_name="tfstate$RANDOM"

if [[ "$1" == "create" ]]; then
create "$resource_group_name" "$storage_account_name" "$container_name" "$location"
elif [[ "$1" == "delete" ]]; then
delete "$resource_group_name" "$storage_account_name" "$container_name"
else
echo "Usage: $0 [create|delete]"
exit 1
fi
}

main "$@"
117 changes: 117 additions & 0 deletions infrastructure/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
locals {
location = "westeurope"
}

resource "azurerm_resource_group" "cv" {
name = "cv-rg"
location = local.location
}

resource "azurerm_container_app_environment" "cv" {
name = "${azurerm_resource_group.cv.name}-env"
location = local.location
resource_group_name = azurerm_resource_group.cv.name
}

resource "azurerm_container_app" "cv-frontend" {
name = "cv-frontend"
container_app_environment_id = azurerm_container_app_environment.cv.id
resource_group_name = azurerm_resource_group.cv.name
revision_mode = "Single"

template {
container {
name = "frontend"
image = "ghcr.io/${var.repository_owner}/cv-workshop/frontend:latest"
cpu = "0.25"
memory = "0.5Gi"

env {
name = "BACKEND_URL"
value = "https://${azurerm_container_app.cv-backend.ingress.0.fqdn}"
}

env {
name = "BACKEND_API_KEY"
value = "backend-api-key"
}
}

min_replicas = 1
max_replicas = 1
revision_suffix = substr(var.revision_suffix, 0, 10)
}

secret {
name = "backend-api-key"
value = var.api_key
}

ingress {
target_port = 3000
external_enabled = true

traffic_weight {
percentage = 100
latest_revision = true
}
}
}

resource "azurerm_container_app" "cv-backend" {
name = "cv-backend"
container_app_environment_id = azurerm_container_app_environment.cv.id
resource_group_name = azurerm_resource_group.cv.name
revision_mode = "Single"

template {
container {
name = "backend"
image = "ghcr.io/${var.repository_owner}/cv-workshop/backend:latest"
cpu = "0.25"
memory = "0.5Gi"

env {
name = "AppSettings__FrontendApiKey"
value = "frontend-api-key"
}

env {
name = "ConnectionStrings__DefaultConnection"
value = "connection-string"
}
}

min_replicas = 1
max_replicas = 1
revision_suffix = substr(var.revision_suffix, 0, 10)
}

secret {
name = "fontend-api-key"
value = var.api_key
}

secret {
name = "connection-string"
value = var.connection_string
}

ingress {
target_port = 8080
external_enabled = true

traffic_weight {
percentage = 100
latest_revision = true
}
}
}

output "frontend_url" {
value = "https://${azurerm_container_app.cv-frontend.ingress.0.fqdn}"
}

output "backend_url" {
value = "https://${azurerm_container_app.cv-backend.ingress.0.fqdn}"
}
Loading