diff --git a/.github/workflows/publish-ghcr.yaml b/.github/workflows/publish-ghcr.yaml new file mode 100644 index 0000000..48083e9 --- /dev/null +++ b/.github/workflows/publish-ghcr.yaml @@ -0,0 +1,34 @@ +name: Docker Build and Publish + +on: + push: + branches: + - main + - master + paths: + - 'Dockerfile' + - 'projeto-fsharp/**' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Build Docker Image + run: | + docker build -t docker-image . + + - name: Login to Registry + run: | + docker login ${{ vars.REGISTRY }} \ + -u ${{ secrets.REGISTRY_USERNAME }} \ + --password ${{ secrets.REGISTRY_PASSWORD }} + + - name: Push to Registry + run: | + IMAGE_NAME="${{ vars.REGISTRY }}/${{ secrets.REGISTRY_USERNAME }}/${{ vars.PROJECT_NAME }}" + docker tag docker-image:latest $IMAGE_NAME:latest + docker push $IMAGE_NAME:latest diff --git a/.github/workflows/tf-plan.yaml b/.github/workflows/tf-plan.yaml new file mode 100644 index 0000000..d21b0eb --- /dev/null +++ b/.github/workflows/tf-plan.yaml @@ -0,0 +1,33 @@ +name: Terraform Plan + +on: + pull_request: + paths: + - terraform/** + +jobs: + plan-dev: + name: 'Terraform plan DEV' + runs-on: 'ubuntu-latest' + + 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 }} + + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v1 + with: + terraform_version: 1.5.0 + + - name: Terraform Plan + run: | + pwd + terraform init + terraform plan + working-directory: terraform diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a6dd9c0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +# Build layer + +# Official .NET SDK image as a base image +FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build + +# Build directory +WORKDIR /build + +# Copy the F# API app source code to the container +COPY projeto-fsharp . + +# Run the restore script +RUN ./restore.sh + +# Run the build command to build the app +RUN dotnet fake run build.fsx -t "Build" + +# Run layer + +# Official .NET runtime image +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS runtime + +# Working app directory +WORKDIR /app + +# Copy the built app from the build layer +COPY --from=build /build/src/Server/out /app + +# Expose the port that the app will run on +EXPOSE 8085 + +# Command to run the app when the container starts +CMD ["dotnet", "Server.dll"] + +# test \ No newline at end of file diff --git a/descricao.md b/descricao.md new file mode 100644 index 0000000..34299cc --- /dev/null +++ b/descricao.md @@ -0,0 +1,71 @@ +# Máquina Virtual + +Na pasta `terraform` estão os arquivos para criação da máquina virtual. + +- Os recursos foram nomeados usando um prefixo `datapi`. O nome do workspace também faz parte do nome, o que facilita o uso destes mesmos recursos para diferentes ambientes. +- Um ip público foi adicionado ao recurso de `network interface` para tornar a máquina acessivel externamente. +- Uma chave ssh pública foi adicionada `vm.pub` para permitir o acesso via ssh. +- A VM foi criada com linux, rodando `Ubuntu 22.04`. +- A senha de admin foi gerada de forma aleatória, e é somente exibida durante a aplicação do plano como `output`. +- O arquivo `providers` está configurado para salvar o `tfstate` em uma `storage` na Azure. O importante aqui é criar uma `key` unica para cada projeto, para evitar conflitos entre arquivos terraform. + +# Docker + +A construção do `Dockerfile` está dividido em duas etapas: `build` e `runtime`. + +## Build + +- Usa a imagem .NET SDK `6.0.x` +- Copia os arquivos do repositório +- Restaura as dependencias +- Chama o `fake build` + +## Runtime + +- Usa a imagem .NET Runtime `6.0.x` +- Copia os arquivos gerados pelo build +- Exporta a porta usada pelo app `8085` +- Define o `entrypoint` chamando a aplicação + +A divisão dessas etapas ajuda a diminuir o tamanho da imagem final, e também mantém apenas os arquivos necessários para executar a aplicação. + +# CI/CD + +Foram criados dois workflows: + +## publish-ghcr + +Realiza o build da imagem Docker da aplicação e faz o envio para o registry do Github. + +Requer algumas variaveis e segredos: +- var `REGISTRY`: a URL do registry desejado (`ghcr.io`) +- var `PROJECT_NAME`: com o nome do projeto, que vai ser usado como nome da imagem +- secret `REGISTRY_USERNAME`: o `username` do dono do repositório +- secret `REGISTRY_PASSWORD`: o token pessoal com acesso ao registry do github + +A execução desse workflow foi restrito para apenas ocorrer quando houver mudanças nos arquivos da aplicação (pasta `projeto-fsharp`), ou no arquivo `Dockerfile`. + +## tf-plan + +Executa o `plan` do Terraform para garantir que as mudanças estão corretas. + +Requer alguns segredos para autenticar com a Azure: +- secret `ARM_CLIENT_ID`: O ID da aplicação +- secret `ARM_CLIENT_SECRET`: O segredo criado para a aplicação +- secret `ARM_SUBSCRIPTION_ID`: O ID da `subscription` da conta +- secret `ARM_TENANT_ID`: O ID do `tenant` da aplicação + +A execução desse workflow foi restrito para ser executado apenas em `pull requests` se arquivos da pasta `terraform` foram modificados. + +# Kubernetes + +Os arquivos YAML para deploy da aplicação em um cluster Kubernetes. + +Todo o deploy foi criado usando um arquivo de `kustomization`, que lista os demais arquivos: +- `namespace.yaml`: cria um namespace para aplicação +- `deployment.yaml`: configura o deployment do app +- `secret.yaml`: cria um segredo com a autenticação com o GHCR.io + +O arquivo de `kustomization` também inclui labels comuns para todos os serviços (app, env, version), que faciliam o controle para diferentes apps e ambientes. + +Utilizar secrets como feito nesse exemplo não é recomendado pois pode expor dados sensíveis, como o token de auth para o GHCR. Nesses casos é melhor usar ferramentas como `Sealed Secrets` ou `External Secret`, que são aplicações que rodam no cluster Kubernetes, e são usados para encriptar segredos, tornando mais seguro comitar esses arquivos em repositórios git. diff --git a/kubernetes/deployment.yaml b/kubernetes/deployment.yaml new file mode 100644 index 0000000..d941616 --- /dev/null +++ b/kubernetes/deployment.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: datapi-deploy +spec: + replicas: 1 + selector: + matchLabels: + app: datapi + template: + metadata: + labels: + app: datapi + spec: + containers: + - name: datapi-container + image: ghcr.io/rhrlima/datapi:latest + ports: + - containerPort: 8085 + imagePullSecrets: + - name: registry-secret diff --git a/kubernetes/kustomization.yaml b/kubernetes/kustomization.yaml new file mode 100644 index 0000000..91e84db --- /dev/null +++ b/kubernetes/kustomization.yaml @@ -0,0 +1,14 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +namespace: datapi + +resources: + - namespace.yaml + - deployment.yaml + - secret.yaml + +commonLabels: + app: datapi + env: dev + version: v1.0 diff --git a/kubernetes/namespace.yaml b/kubernetes/namespace.yaml new file mode 100644 index 0000000..a897ff2 --- /dev/null +++ b/kubernetes/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: datapi diff --git a/kubernetes/secret.yaml b/kubernetes/secret.yaml new file mode 100644 index 0000000..9b6c043 --- /dev/null +++ b/kubernetes/secret.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + name: registry-secret + namespace: datapi +type: kubernetes.io/dockerconfigjson +data: + .dockerconfigjson: diff --git a/resources/vm.pub b/resources/vm.pub new file mode 100644 index 0000000..d19db5e --- /dev/null +++ b/resources/vm.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCxIUbdI166bxR4FYr9a3BwBayGQqNl+N7iDbnD0vkG/C4t5OFll2bUysU/7m4durfcDjWUhAENTvJTdNllP88lgGgn9HkzQZq4/X0kjfEcUHj0yyf6B4ffmKgCYteNv3RS1W4nAWvTQOmBmCVLWDuw3ZEBbUnjxqf48KZjVNHq/J+erZ2F0JG4bJLc0qyi34ljbzLe8jqTetxCNqO5oH7264JOZnqJMPSD0q0JpI3GYzlbB5qh+yPwtku7pjbVr9uLTSfryzyNvwqtjbTxXXGquJQLGRtECBUAIkErjprXtKbo6uhyLEGuubF0yqZzvR7rAuz2IlSOlaxJ+E/bqryPuXi7eAgvAqhpAr8LyjqBDHaXDgZLPEWjJhQEfdWkFftGVSUMoom0ZFARbVN7j3jHigShnbOgijL1su/3iieVzq3R/LZXuVjarwES1lB22NMTwnVX/uBYfeNms0LU12n6UrX/vvsvhGHFR1/mlNCs53usodG6VYMLnYT3QeSK/3k= ricardo@SIVIR.local diff --git a/terraform/main.tf b/terraform/main.tf new file mode 100644 index 0000000..0b73494 --- /dev/null +++ b/terraform/main.tf @@ -0,0 +1,75 @@ +resource "azurerm_resource_group" "vm_rg" { + name = "${var.prefix}-${terraform.workspace}-resource-group" + location = var.resource_group_location +} + +resource "azurerm_virtual_network" "vm_vnet" { + name = "${var.prefix}-${terraform.workspace}-network" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.vm_rg.location + resource_group_name = azurerm_resource_group.vm_rg.name +} + +resource "azurerm_subnet" "vm_subnet" { + name = "${var.prefix}-${terraform.workspace}-subnet" + resource_group_name = azurerm_resource_group.vm_rg.name + virtual_network_name = azurerm_virtual_network.vm_vnet.name + address_prefixes = ["10.0.1.0/24"] +} + +resource "azurerm_network_interface" "vm_nic" { + name = "${var.prefix}-${terraform.workspace}-nic" + location = azurerm_resource_group.vm_rg.location + resource_group_name = azurerm_resource_group.vm_rg.name + + ip_configuration { + name = "internal" + subnet_id = azurerm_subnet.vm_subnet.id + private_ip_address_allocation = "Dynamic" + public_ip_address_id = azurerm_public_ip.vm_publicip.id + } +} + +resource "azurerm_linux_virtual_machine" "vm" { + name = "${var.prefix}-${terraform.workspace}-machine" + resource_group_name = azurerm_resource_group.vm_rg.name + location = azurerm_resource_group.vm_rg.location + size = "Standard_F2" + admin_username = "adminuser" + network_interface_ids = [ + azurerm_network_interface.vm_nic.id, + ] + + admin_ssh_key { + username = "adminuser" + public_key = file("../resources/vm.pub") + } + + os_disk { + caching = "ReadWrite" + storage_account_type = "Standard_LRS" + } + + source_image_reference { + publisher = "Canonical" + offer = "0001-com-ubuntu-server-jammy" + sku = "22_04-lts-gen2" + version = "latest" + } +} + +resource "random_password" "password" { + length = 20 + min_lower = 1 + min_upper = 1 + min_numeric = 1 + min_special = 1 + special = true +} + +resource "azurerm_public_ip" "vm_publicip" { + name = "${var.prefix}-${terraform.workspace}-publicip" + resource_group_name = azurerm_resource_group.vm_rg.name + location = azurerm_resource_group.vm_rg.location + allocation_method = "Static" +} diff --git a/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 0000000..c9eb51d --- /dev/null +++ b/terraform/outputs.tf @@ -0,0 +1,12 @@ +output "resource_group_name" { + value = azurerm_resource_group.vm_rg.name +} + +output "public_ip_address" { + value = azurerm_linux_virtual_machine.vm.public_ip_address +} + +output "admin_password" { + sensitive = true + value = azurerm_linux_virtual_machine.vm.admin_password +} \ No newline at end of file diff --git a/terraform/providers.tf b/terraform/providers.tf new file mode 100644 index 0000000..bc7767a --- /dev/null +++ b/terraform/providers.tf @@ -0,0 +1,24 @@ +terraform { + required_version = ">=1.5" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~>3.0" + } + random = { + source = "hashicorp/random" + version = "~>3.0" + } + } + backend "azurerm" { + resource_group_name = "tfstate" + storage_account_name = "tfstate24650" + container_name = "tfstate" + key = "datapi/terraform.tfstate" + } +} + +provider "azurerm" { + features {} +} \ No newline at end of file diff --git a/terraform/variables.tf b/terraform/variables.tf new file mode 100644 index 0000000..113374a --- /dev/null +++ b/terraform/variables.tf @@ -0,0 +1,11 @@ +variable "resource_group_location" { + type = string + default = "East US" + description = "Location of the resource group." +} + +variable "prefix" { + type = string + default = "datapi" + description = "Prefix name for resources." +} \ No newline at end of file