diff --git a/.gitea/workflows/felsokning.yml b/.gitea/workflows/felsokning.yml index deb9a76..0afef76 100644 --- a/.gitea/workflows/felsokning.yml +++ b/.gitea/workflows/felsokning.yml @@ -9,9 +9,15 @@ name: Guidad Felsökning on: push: - paths: ["felsokning/**", ".gitea/workflows/felsokning.yml"] + paths: ['felsokning/**', '.gitea/workflows/felsokning.yml'] pull_request: - paths: ["felsokning/**"] + paths: ['felsokning/**'] + workflow_dispatch: + inputs: + bildtagg: + description: 'Bildtagg att driftsätta (commit-SHA från publiceringen)' + required: true + type: string jobs: test-och-bygg: @@ -59,3 +65,61 @@ jobs: - run: terraform fmt -check -recursive - run: terraform init -backend=false -input=false - run: terraform validate + + # Bygger och publicerar till vårt eget ECR. Bara på main och bara när + # kontrollerna ovan gått igenom. + publicera: + needs: [test-och-bygg, tjanster, terraform] + if: gitea.ref == 'refs/heads/main' + runs-on: felsokning + permissions: + id-token: write + steps: + - uses: actions/checkout@v4 + - uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ vars.BYGG_ROLL_ARN }} + aws-region: ${{ vars.AWS_REGION }} + - uses: aws-actions/amazon-ecr-login@v2 + id: ecr + + - name: Bygg och publicera + env: + REGISTER: ${{ steps.ecr.outputs.registry }} + TAGG: ${{ gitea.sha }} + run: | + # Klientens API-adress bakas in vid bygget (Vite), så bilden är + # miljöbunden. + docker build -t "$REGISTER/guidad-felsokning-web:$TAGG" \ + --build-arg VITE_PLATTFORM_URL="https://${{ vars.DOMAN }}" \ + --build-arg VITE_AI_ORKESTER_URL="https://${{ vars.DOMAN }}" \ + felsokning/app + docker build -t "$REGISTER/guidad-felsokning-plattform:$TAGG" felsokning/services/plattform + docker build -t "$REGISTER/guidad-felsokning-ai-orkester:$TAGG" felsokning/services/ai-orkester + for bild in web plattform ai-orkester; do + docker push "$REGISTER/guidad-felsokning-$bild:$TAGG" + done + + # Driftsättning är ett eget, manuellt steg: en bild i registret är inte + # samma sak som en bild som kör. Rollback = kör igen med tidigare tagg. + driftsatt: + if: gitea.event_name == 'workflow_dispatch' + runs-on: felsokning + environment: produktion + permissions: + id-token: write + defaults: + run: + working-directory: felsokning/infra/terraform + steps: + - uses: actions/checkout@v4 + - uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: ${{ vars.DRIFT_ROLL_ARN }} + aws-region: ${{ vars.AWS_REGION }} + - run: aws eks update-kubeconfig --name ${{ vars.KLUSTER }} + - uses: hashicorp/setup-terraform@v3 + with: { terraform_version: 1.9.8 } + - run: terraform init -input=false + - run: terraform apply -input=false -auto-approve -var bildtagg=${{ inputs.bildtagg }} + - run: terraform output karta diff --git a/.github/workflows/driftsatt.yml b/.github/workflows/driftsatt.yml deleted file mode 100644 index b9e9ed8..0000000 --- a/.github/workflows/driftsatt.yml +++ /dev/null @@ -1,103 +0,0 @@ -# Driftsättning: terraform apply mot klustret. -# -# Startas för hand med en bildtagg från publiceringsflödet. Det är -# avsiktligt — driftsättning är ett beslut, och miljön `produktion` kan -# kräva godkännande i GitHub innan jobbet får köra. -# -# Rollback = kör flödet igen med en tidigare tagg. -# -# Krävs på miljön `produktion`: -# secrets KUBE_CONFIG (base64), TF_API_TOKEN eller backend-uppgifter, -# ANTHROPIC_API_NYCKEL, DATABAS_URL -# vars DOMAN, REGISTER, DATABAS_LAGE -name: Driftsätt - -on: - workflow_dispatch: - inputs: - bildtagg: - description: "Bildtagg att driftsätta (git-SHA från Publicera)" - required: true - type: string - bara_plan: - description: "Visa bara planen, applicera inte" - required: false - default: false - type: boolean - -permissions: - contents: read - -jobs: - driftsatt: - runs-on: ubuntu-latest - environment: produktion - defaults: - run: - working-directory: infra/terraform - - steps: - - uses: actions/checkout@v4 - - - uses: hashicorp/setup-terraform@v3 - with: - terraform_version: 1.9.8 - - - name: Kubeconfig - run: | - mkdir -p "$HOME/.kube" - echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > "$HOME/.kube/config" - chmod 600 "$HOME/.kube/config" - - - name: Formatering - run: terraform fmt -check -recursive - - - name: Init - run: terraform init -input=false - - - name: Validering - run: terraform validate - - - name: Plan - run: terraform plan -input=false -out=plan.tfplan - env: - TF_VAR_bildtagg: ${{ inputs.bildtagg }} - TF_VAR_doman: ${{ vars.DOMAN }} - TF_VAR_register: ${{ vars.REGISTER }} - TF_VAR_databas_lage: ${{ vars.DATABAS_LAGE }} - TF_VAR_databas_url: ${{ secrets.DATABAS_URL }} - TF_VAR_anthropic_api_nyckel: ${{ secrets.ANTHROPIC_API_NYCKEL }} - TF_VAR_jwt_hemlighet: ${{ secrets.JWT_HEMLIGHET }} - TF_VAR_postgres_losenord: ${{ secrets.POSTGRES_LOSENORD }} - TF_VAR_integration_nyckel: ${{ secrets.INTEGRATION_NYCKEL }} - - - name: Applicera - if: ${{ !inputs.bara_plan }} - run: terraform apply -input=false plan.tfplan - - - name: Kartan efter driftsättning - if: ${{ !inputs.bara_plan }} - run: | - { - echo "### Driftsatt \`${{ inputs.bildtagg }}\`" - echo "" - echo '```' - terraform output -no-color karta - echo '```' - } >> "$GITHUB_STEP_SUMMARY" - - # Rök: klienten, API-specen och hälsokontrollen ska svara. - - name: Rökkontroll - if: ${{ !inputs.bara_plan }} - run: | - for i in 1 2 3 4 5 6 7 8 9 10; do - if curl -fsS "https://${{ vars.DOMAN }}/halsa" > /dev/null; then - echo "hälsokontrollen svarar" - curl -fsS "https://${{ vars.DOMAN }}/api/openapi.yaml" | head -1 - exit 0 - fi - echo "väntar på att tjänsten ska svara ($i/10)" - sleep 15 - done - echo "tjänsten svarade inte inom 150 s" - exit 1 diff --git a/.github/workflows/publicera.yml b/.github/workflows/publicera.yml deleted file mode 100644 index 1581881..0000000 --- a/.github/workflows/publicera.yml +++ /dev/null @@ -1,59 +0,0 @@ -# Bygger och publicerar de tre bilderna till GHCR, taggade med git-SHA:t. -# Driftsättningen sker i ett eget flöde (driftsatt.yml) som kör -# terraform apply mot en miljö med godkännandekrav — en bild som ligger i -# registret är alltså inte samma sak som en bild som kör. -name: Publicera - -on: - push: - branches: [main] - -permissions: - contents: read - packages: write - -env: - REGISTER: ghcr.io/${{ github.repository_owner }} - -jobs: - publicera: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - # Klientens API-adress bakas in vid bygget (Vite), så bilden är - # miljöbunden — en bild byggd mot en domän pekar på fel API i en - # annan miljö. - - name: Bygg och publicera web - run: | - docker build -t "$REGISTER/guidad-felsokning-web:${{ github.sha }}" \ - --build-arg VITE_PLATTFORM_URL="${{ vars.PLATTFORM_URL }}" \ - --build-arg VITE_AI_ORKESTER_URL="${{ vars.PLATTFORM_URL }}" . - docker push "$REGISTER/guidad-felsokning-web:${{ github.sha }}" - - - name: Bygg och publicera ai-orkester - run: | - docker build -t "$REGISTER/guidad-felsokning-ai-orkester:${{ github.sha }}" services/ai-orkester - docker push "$REGISTER/guidad-felsokning-ai-orkester:${{ github.sha }}" - - - name: Bygg och publicera plattform - run: | - docker build -t "$REGISTER/guidad-felsokning-plattform:${{ github.sha }}" services/plattform - docker push "$REGISTER/guidad-felsokning-plattform:${{ github.sha }}" - - - name: Sammanfatta - run: | - { - echo "### Publicerat" - echo "" - echo '```' - echo "bildtagg: ${{ github.sha }}" - echo '```' - echo "" - echo "Driftsätt med flödet **Driftsätt** och den taggen." - } >> "$GITHUB_STEP_SUMMARY" diff --git a/.prettierignore b/.prettierignore index d35fdc1..440cbe5 100644 --- a/.prettierignore +++ b/.prettierignore @@ -5,3 +5,6 @@ build/ cdk.out/ coverage/ package-lock.json + +# Egen produkt med egen verktygskedja — se felsokning/app. +felsokning/ diff --git a/eslint.config.js b/eslint.config.js index 28ff861..b276dc9 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,7 +3,17 @@ import tseslint from 'typescript-eslint'; export default tseslint.config( { - ignores: ['**/node_modules/', '**/dist/', '**/build/', '**/.expo/', '**/cdk.out/'], + // felsokning/ är en egen produkt med egen verktygskedja: egen + // eslint-konfiguration, egna prettier-regler och egen CI i + // .gitea/workflows/. Den här konfigurationen gäller Semantika. + ignores: [ + '**/node_modules/', + '**/dist/', + '**/build/', + '**/.expo/', + '**/cdk.out/', + 'felsokning/', + ], }, js.configs.recommended, ...tseslint.configs.recommended, diff --git a/felsokning/infra/aws/variables.tf b/felsokning/infra/aws/variables.tf new file mode 100644 index 0000000..f36ce3c --- /dev/null +++ b/felsokning/infra/aws/variables.tf @@ -0,0 +1,142 @@ +variable "region" { + description = "AWS-region. eu-north-1 (Stockholm) håller fordonsdata inom EU." + type = string + default = "eu-north-1" +} + +variable "miljo" { + description = "Miljöns namn — ingår i alla resursnamn och taggar." + type = string + default = "produktion" +} + +variable "doman" { + description = "Domänen tjänsten nås på, t.ex. felsokning.exempel.se." + type = string +} + +variable "zon_id" { + description = <<-TEXT + Befintlig Route 53-zon att skapa posten i. Lämnas den tom skapas en + ny publik zon för domänens rot — då måste namnservrarna pekas om hos + registraren innan certifikatet kan utfärdas. + TEXT + type = string + default = "" +} + +# ---- Nät --------------------------------------------------------------- + +variable "vpc_cidr" { + description = "Adressrymd för VPC:n." + type = string + default = "10.42.0.0/16" + + validation { + condition = can(cidrsubnet(var.vpc_cidr, 4, 0)) + error_message = "vpc_cidr måste vara ett giltigt CIDR med plats för /20-subnät." + } +} + +variable "antal_zoner" { + description = "Antal tillgänglighetszoner. Tre ger failover som överlever att en zon försvinner." + type = number + default = 3 + + validation { + condition = var.antal_zoner >= 2 && var.antal_zoner <= 4 + error_message = "antal_zoner måste vara mellan 2 och 4." + } +} + +variable "nat_per_zon" { + description = <<-TEXT + En NAT-gateway per zon i stället för en delad. Rätt i produktion — + med en delad blir den zonen en enskild felkälla för all utgående + trafik. Kostar mer, så av i test. + TEXT + type = bool + default = true +} + +# ---- Kluster ----------------------------------------------------------- + +variable "kubernetes_version" { + description = "EKS-version." + type = string + default = "1.31" +} + +variable "nod_typer" { + description = "Instanstyper för nodgruppen. Flera ökar chansen att kapacitet finns." + type = list(string) + default = ["m7g.large", "m6g.large"] +} + +variable "noder" { + description = "Nodgruppens storlek." + type = object({ + minsta = number + onskad = number + hogsta = number + }) + default = { + minsta = 3 + onskad = 3 + hogsta = 10 + } +} + +variable "tillatna_api_cidr" { + description = <<-TEXT + Varifrån EKS API-server får nås. Standard är öppet eftersom det + annars inte går att komma åt klustret alls från en ny miljö — + begränsa till kontorets och byggrunnernas adresser i produktion. + TEXT + type = list(string) + default = ["0.0.0.0/0"] +} + +# ---- Databas ----------------------------------------------------------- + +variable "databas" { + description = <<-TEXT + Aurora PostgreSQL. Serverless v2 skalar efter last; min_kapacitet + 0.5 ACU räcker för en liten verkstad, max sätter taket. + backup_dagar styr både automatisk säkerhetskopiering och hur långt + tillbaka PITR går. + TEXT + type = object({ + version = optional(string, "16.6") + min_kapacitet = optional(number, 0.5) + max_kapacitet = optional(number, 8) + instanser = optional(number, 2) + backup_dagar = optional(number, 30) + raderingsskydd = optional(bool, true) + }) + default = {} +} + +# ---- Larm -------------------------------------------------------------- + +variable "larm_epost" { + description = "Adress som larm skickas till. Tom = inget SNS-abonnemang skapas." + type = string + default = "" +} + +# ---- Självhostad git --------------------------------------------------- + +variable "gitea" { + description = <<-TEXT + Självhostad git med egna byggrunners, så varken källkod, bygge eller + register ligger hos någon annan. Slås av om ni tills vidare kör git + någon annanstans. + TEXT + type = object({ + aktiv = optional(bool, true) + doman = optional(string, "") + runners = optional(number, 2) + }) + default = {} +} diff --git a/felsokning/infra/aws/versions.tf b/felsokning/infra/aws/versions.tf new file mode 100644 index 0000000..ec57894 --- /dev/null +++ b/felsokning/infra/aws/versions.tf @@ -0,0 +1,79 @@ +# Guidad Felsökning — AWS-basen. +# +# Två lager, medvetet åtskilda: +# +# felsokning/infra/aws/ det här — VPC, EKS, Aurora, S3, ECR, +# Secrets Manager, Route 53/ACM, CloudWatch +# felsokning/infra/terraform/ arbetslasten i klustret +# +# Anledningen är inte smak: en enda apply som både skapar ett EKS-kluster +# och schemalägger in i det är en känd fälla — kubernetes-leverantören +# måste konfigureras med uppgifter som inte finns förrän klustret är +# skapat, och planen blir därför opålitlig. Kör basen först, arbetslasten +# sedan. Arbetslasten läser basens utdata. +# +# Läsordning: +# variables.tf alla rattar +# 10-natverk.tf VPC, subnät, NAT, VPC-endpoints +# 20-eks.tf klustret, noderna, IRSA +# 30-databas.tf Aurora PostgreSQL +# 40-lagring.tf S3 för bilagor, ECR för bilder +# 50-hemligheter.tf Secrets Manager + rollerna som får läsa dem +# 60-doman.tf Route 53 och certifikat +# 70-observation.tf CloudWatch: loggar, mätvärden, larm +# 80-gitea.tf självhostad git + byggrunners +# outputs.tf det arbetslastlagret behöver + +terraform { + required_version = ">= 1.6" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.80" + } + random = { + source = "hashicorp/random" + version = "~> 3.6" + } + tls = { + source = "hashicorp/tls" + version = "~> 4.0" + } + } + + # Tillståndet innehåller databasens slutpunkt och rollnamn. Lägg det i + # S3 med versionshantering och lås — aldrig lokalt i git. + # + # backend "s3" { + # bucket = "felsokning-tfstate" + # key = "aws/terraform.tfstate" + # region = "eu-north-1" + # encrypt = true + # use_lockfile = true + # } +} + +provider "aws" { + region = var.region + + default_tags { + tags = local.taggar + } +} + +# ACM-certifikat för CloudFront måste ligga i us-east-1. Vi använder inte +# CloudFront i dag, men leverantören står redo så tillägget inte kräver +# en omstrukturering. +provider "aws" { + alias = "us_east_1" + region = "us-east-1" + + default_tags { + tags = local.taggar + } +} + +data "aws_caller_identity" "denna" {} + +data "aws_partition" "denna" {}