Project 4: Creating Serverless azure infra and deploying the application using Azure Devops.
We are going to create container apps using terraform.
Install the terraform, azurecli in your device and login to your azure account through your terminal/powershell.
I wrote the terraform to create new resources from scarch by creating child modules of each resources
terraform ├── main.tf ├── variables.tf ├── outputs.tf ├── modules │ ├── container_app │ │ ├── main.tf │ │ ├── variables.tf │ │ ├── outputs.tf │ ├── network │ │ ├── main.tf │ │ ├── variables.tf │ │ ├── outputs.tf │ ├── log_analytics │ │ ├── main.tf │ │ ├── variables.tf │ │ ├── outputs.tf │ ├── acr │ │ ├── main.tf │ │ ├── variables.tf │ │ ├── outputs.tf │ ├── identity │ │ ├── main.tf │ │ ├── variables.tf │ │ ├── outputs.tf │ ├── grafana │ ├── main.tf │ ├── variables.tf │ ├── outputs.tf
a) Log-analytics
# modules/LogAnalytics/main.tf resource "azurerm_log_analytics_workspace" "main" { name = var.name location = var.location resource_group_name = var.resource_group_name sku = "PerGB2018" retention_in_days = 30 tags = { "Created By" = var.tag_CreatedBy "Created Date" = var.tag_CreatedDate } } output "workspace_id" { value = azurerm_log_analytics_workspace.main.id } ---------------------------------------------------------------------- # modules/LogAnalytics/variable.tf variable "location" {} variable "resource_group_name" {} variable "name" {} variable "tag_CreatedBy" {} variable "tag_CreatedDate" {}
b) User Assigned Identity
# modules/user_assigned_identity/main.tf resource "azurerm_user_assigned_identity" "main" { name = "High5Identity" location = var.location resource_group_name = var.resource_group_name } output "id" { value = azurerm_user_assigned_identity.main.id } output "principal_id" { value = azurerm_user_assigned_identity.main.principal_id } ---------------------------------------------------------------------- # modules/user_assigned_identity/variables.tf variable "location" {} variable "resource_group_name" {}
c) ACR (Azure Container Registry)
# modules/acr/main.tf resource "azurerm_container_registry" "main" { name = "high5" location = var.location resource_group_name = var.resource_group_name sku = "Premium" admin_enabled = true identity { type = "UserAssigned" identity_ids = [var.identity_id] } tags = { "Created By" = var.tag_CreatedBy "Created Date" = var.tag_CreatedDate } lifecycle { prevent_destroy = true # Reference link https://developer.hashicorp.com/terraform/tutorials/state/resource-lifecycle#prevent-resource-deletion } } resource "azurerm_role_assignment" "acr_pull" { scope = azurerm_container_registry.main.id role_definition_name = "AcrPull" principal_id = var.principal_id } output "login_server" { value = azurerm_container_registry.main.login_server } ---------------------------------------------------------------------- # modules/acr/variables.tf variable "location" {} variable "resource_group_name" {} variable "identity_id" {} variable "principal_id" {} variable "tag_CreatedBy" {} variable "tag_CreatedDate" {}
d) Azure Container App Environment
# modules/container_environment/main.tf resource "azurerm_container_app_environment" "main" { name = var.env_name location = var.location resource_group_name = var.resource_group_name log_analytics_workspace_id = var.log_analytics_workspace_id infrastructure_subnet_id = azurerm_subnet.main.id zone_redundancy_enabled = true workload_profile { name = "Consumption" workload_profile_type = "Consumption" } tags = { "Created By" = var.tag_CreatedBy "Created Date" = var.tag_CreatedDate } } resource "azurerm_virtual_network" "main" { name = var.virtual_network_name location = var.location resource_group_name = var.resource_group_name address_space = ["10.0.0.0/16"] tags = { "Created By" = var.tag_CreatedBy "Created Date" = var.tag_CreatedDate } } resource "azurerm_subnet" "main" { name = "ContainerSubnet" resource_group_name = var.resource_group_name virtual_network_name = azurerm_virtual_network.main.name address_prefixes = ["10.0.0.0/23"] delegation { name = "appservices-delegation" service_delegation { name = "Microsoft.App/environments" actions = [ "Microsoft.Network/virtualNetworks/subnets/join/action", "Microsoft.Network/virtualNetworks/subnets/prepareNetworkPolicies/action", ] } } } output "id" { value = azurerm_container_app_environment.main.id } output "vnet_id" { value = azurerm_virtual_network.main.id } output "subnet_id" { value = azurerm_subnet.main.id } ---------------------------------------------------------------------- # modules/container_environment/variables.tf variable "location" {} variable "resource_group_name" {} variable "log_analytics_workspace_id" {} variable "env_name" {} variable "tag_CreatedBy" {} variable "tag_CreatedDate" {} variable "virtual_network_name" {}
e) Azure Container App
# modules/container_app/main.tf resource "azurerm_container_app" "main" { name = var.name resource_group_name = var.resource_group_name container_app_environment_id = var.environment_id revision_mode = "Single" identity { type = "UserAssigned" identity_ids = [var.identity_id] } ingress { external_enabled = true target_port = var.ingress_port traffic_weight { latest_revision = true percentage = 100 } } registry { server = var.acr_login_server identity = var.identity_id } template { # min_replicas = 2 # max_replicas = 3 container { name = var.name # image = "${var.acr_login_server}/${var.name}:latest" image = var.image cpu = 0.5 memory = "1Gi" env { name = "EXAMPLE_ENV_VAR" value = "examplevalue" } } } tags = { "Created By" = var.tag_CreatedBy "Created Date" = var.tag_CreatedDate } } ---------------------------------------------------------------------- # modules/container_app/main.tf variable "name" {} variable "resource_group_name" {} variable "environment_id" {} variable "identity_id" {} variable "acr_login_server" {} variable "ingress_port" {} variable "image" {} variable "tag_CreatedBy" {} variable "tag_CreatedDate" {}
f) Grafana
# modules/grafana/main.tf resource "azurerm_dashboard_grafana" "grafana" { name = "azure-grafana-High5" resource_group_name = var.resource_group_name location = var.location sku = "Standard" grafana_major_version = "10" zone_redundancy_enabled = true api_key_enabled = true deterministic_outbound_ip_enabled = true public_network_access_enabled = true identity { type = "SystemAssigned" } tags = { "Created By" = var.tag_CreatedBy "Created Date" = var.tag_CreatedDate } lifecycle { prevent_destroy = true # Reference link https://developer.hashicorp.com/terraform/tutorials/state/resource-lifecycle#prevent-resource-deletion } } data "azurerm_client_config" "current" {} resource "azurerm_role_assignment" "role_grafana_admin" { scope = azurerm_dashboard_grafana.grafana.id role_definition_name = "Grafana Admin" principal_id = data.azurerm_client_config.current.object_id } data "azurerm_subscription" "current" {} resource "azurerm_role_assignment" "role_monitoring_reader" { scope = data.azurerm_subscription.current.id role_definition_name = "Monitoring Reader" principal_id = azurerm_dashboard_grafana.grafana.identity.0.principal_id } ----------------------------------------------------------------------- # modules/grafana/variables.tf variable "location" {} variable "resource_group_name" {} variable "tag_CreatedBy" {} variable "tag_CreatedDate" {}
f) Root folder
# main.tf resource "azurerm_resource_group" "dev" { name = var.resource_group_name location = var.location tags = { Status = "Running application" } } module "log_analytics" { source = "./modules/LogAnalytics" name = var.log_analytics_name resource_group_name = azurerm_resource_group.dev.name location = azurerm_resource_group.dev.location tag_CreatedBy = var.tag_CreatedBy tag_CreatedDate = var.tag_CreatedDate depends_on = [azurerm_resource_group.dev] } module "user_assigned_identity" { source = "./modules/user_assigned_identity" resource_group_name = "Devops_Tools" location = azurerm_resource_group.dev.location depends_on = [azurerm_resource_group.dev] } module "acr" { source = "./modules/acr" resource_group_name = "Devops_Tools" location = "centralus" identity_id = module.user_assigned_identity.id principal_id = module.user_assigned_identity.principal_id tag_CreatedBy = var.tag_CreatedBy tag_CreatedDate = var.tag_CreatedDate depends_on = [module.user_assigned_identity] } module "environment" { source = "./modules/container_environment" env_name = var.environment_name resource_group_name = azurerm_resource_group.dev.name location = azurerm_resource_group.dev.location log_analytics_workspace_id = module.log_analytics.workspace_id virtual_network_name = var.vnet_name tag_CreatedBy = var.tag_CreatedBy tag_CreatedDate = var.tag_CreatedDate depends_on = [module.log_analytics, module.user_assigned_identity] # lifecycle { # prevent_destroy = true # Reference link https://developer.hashicorp.com/terraform/tutorials/state/resource-lifecycle#prevent-resource-deletion #} } module "container_app_1" { source = "./modules/container_app" name = var.app1_name resource_group_name = azurerm_resource_group.dev.name environment_id = module.environment.id acr_login_server = module.acr.login_server identity_id = module.user_assigned_identity.id image = "ghcr.io/houssemdellai/containerapps-album-frontend:v1" ingress_port = var.app1_ingress_port tag_CreatedBy = var.tag_CreatedBy tag_CreatedDate = var.tag_CreatedDate depends_on = [module.environment, module.acr] } module "container_app_2" { source = "./modules/container_app" name = var.app2_name resource_group_name = azurerm_resource_group.dev.name environment_id = module.environment.id acr_login_server = module.acr.login_server identity_id = module.user_assigned_identity.id image = "ghcr.io/houssemdellai/containerapps-album-backend:v1" ingress_port = var.app2_ingress_port tag_CreatedBy = var.tag_CreatedBy tag_CreatedDate = var.tag_CreatedDate depends_on = [module.environment, module.acr] } module "grafana" { source = "./modules/grafana" resource_group_name = "Devops_Tools" location = "eastus" } ---------------------------------------------------------------------- # variables.tf variable "location" { description = "The Azure region where the resources should be created." type = string } variable "resource_group_name" { description = "The name of the resource group." type = string } variable "environment_name" { description = "The name of the container app environment." type = string } variable "app1_name" { description = "The name of the first container app." type = string } variable "app2_name" { description = "The name of the second container app." type = string } variable "app1_ingress_port" { description = "The ingress port for the first container app." type = number } variable "app2_ingress_port" { description = "The ingress port for the second container app." type = number } variable "log_analytics_name" { description = "Log Analytics name." type = string } variable "tag_CreatedBy" { description = "Created By ___" type = string } variable "tag_CreatedDate" { description = "Enter Date of creation tag" } variable "vnet_name" { description = "Enter the name of the vnet" type = string } ---------------------------------------------------------------------- # terraform.tfvars location = "eastus" resource_group_name = "High-UA-RG" environment_name = "UAEnv" app1_name = "uafrontend" app2_name = "uabackend" log_analytics_name = "UAlogAnalyticsWorkspace" app1_ingress_port = 3000 app2_ingress_port = 5001 tag_CreatedBy = "Tara Prasad Sarangi" tag_CreatedDate = "08-08-2024" vnet_name = "UAEnvVNet" ---------------------------------------------------------------------- # provider.tf terraform { required_providers { azurerm = { source = "hashicorp/azurerm" version = "3.113.0" } } } provider "azurerm" { features {} }
But let's suppose you want to import some of the existing resources from the azure portal. You can use
terraform import
command by refering the terraform document provided for the perticular resources.Suppose we want to import existing MSSQL server and DB, Follow below steps
a) Go to the terraform document in azure provider azurerm and search for the resource. In our case it is mssql_server . Then copy the an example for the mssql_server to the VS Code. And change the name to the actual name that you have kept for the mssql_server in azure portal.
resource "azurerm_mssql_server" "example" { name = "high5uaserver" resource_group_name = azurerm_resource_group.example.name location = azurerm_resource_group.example.location version = "12.0" administrator_login = "missadministrator" administrator_login_password = "thisIsKat11" minimum_tls_version = "1.2" azuread_administrator { login_username = "AzureAD Admin" object_id = "00000000-0000-0000-0000-000000000000" } tags = { } }
b) Go to the terraform resource document same page from where you have copied the example code. Right ride you'll see import. Click on that.
It'll show something like below. Replace with your Subscription, resource group name and msserver name.terraform import <resource>.<terraform_resource_name> <Resource ID> # structure terraform import azurerm_mssql_server.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/my_aks_poc/providers/Microsoft.Sql/servers/high5stageserver
c) Or Let's suppose you want to import terraform resource which you have metioned in a module. We can do so by using below command.
terraform import module.<module_name>.<resource>.<terraform_resource_name> <Resource ID> # structure eg., terraform import module.mssql_server.azurerm_mssql_server.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/my_aks_poc/providers/Microsoft.Sql/servers/high5stageserver
d) Paste it in your terminal after replacing it with your values and hit enter. It will import your resource details.
e) Now type the below command to see view your state file and update the same with the information shown in your statefile.
terraform show terraform.tfstate
f) If you want to delete a perticular module from terraform state file, you can use below command.
terraform state rm <resource>.<terraform_resource_name> eg., terraform state rm azurerm_resource_group.dev
g) To prevent the changes or accidental deletion of the current imported resource include the below code in your terraform resource which you have updated from your imported resource terraform state file.
# Keep this to prevent destroying of any Resources where you don't want any deletion or replacement lifecycle { prevent_destroy = true }
resource "azurerm_mssql_server" "example" { name = "high5uaserver" resource_group_name = azurerm_resource_group.example.name location = azurerm_resource_group.example.location version = "12.0" administrator_login = "missadministrator" administrator_login_password = "thisIsKat11" minimum_tls_version = "1.2" azuread_administrator { login_username = "AzureAD Admin" object_id = "00000000-0000-0000-0000-000000000000" } tags = {} # Keep this to prevent destroying of any Resources where you don't want any deletion or replacement lifecycle { prevent_destroy = true } }
h) After replacing type the below command to check whether everything you have written write or wrong. What are the changes it is going to make if you apply.
terraform plan
i) Similarly follow the same for mssql_database.
Now we need to work on terraform backend / state file storage. If we don't create any tf file to store the backend file, then generally state file will be stored locally which is not advisable. Storing the Terraform state file locally risks data loss, concurrent modifications, and lack of collaboration. Using Azure Storage for the state file offers centralized management, versioning and secure access.
a) Creating
backend.tf
and Storing State in Azure Storage with Versioning EnabledStep 1: Create an Azure Storage Account and Blob Container
Create a Resource Group:
az group create --name myResourceGroup --location eastus
Create a Storage Account:
az storage account create --name mystorageaccount --resource-group myResourceGroup --location eastus --sku Standard_LRS
Create a Blob Container:
az storage container create --name terraformstate --account-name mystorageaccount
Step 2: Enable Versioning on the Storage Account
az storage account blob-service-properties update --account-name mystorageaccount --enable-change-feed true --enable-versioning true
Step 3: Get Access Key for the Storage Account
az storage account keys list --resource-group myResourceGroup --account-name mystorageaccount --query "[0].value" --output tsv
Note the output, as it will be the access key you use in your
backend.tf
configuration.Step 4: Create
backend.tf
Create a
backend.tf
file in your Terraform configuration directory with the following content:terraform { backend "azurerm" { resource_group_name = "myResourceGroup" storage_account_name = "mystorageaccount" container_name = "terraformstate" key = "terraform.tfstate" # This is the name of the state file } }
Step 5: Initialize Terraform with the Backend Configuration
Run the following command to initialize the backend configuration:
terraform init
Terraform will prompt you for the storage account access key. You can either provide it interactively or set it as an environment variable:
export ARM_ACCESS_KEY="your_access_key_here"
Then run
terraform init
again to initialize the backend configuration.Create a Dockerfile for your frontend and backend. Place dockerfile in the root frontend and backend folder separtly.
I have created multi-staging Dockerfile to save build space and has many other advantages.
a) Dockerfile for frontend
# Stage 1: Build the React app FROM node:16.15.0 AS build WORKDIR /frontend COPY package*.json ./ RUN npm install COPY . ./ RUN npm run build # Stage 2: Serve the app with Node.js FROM node:16.15.0-alpine WORKDIR /app COPY --from=build /frontend/build /app/build RUN npm install -g serve EXPOSE 3000 CMD ["serve", "-s", "build", "-l", "3000" ]
b) Dockerfile for backend
# Stage 1: Build stage FROM node:16.15.0 AS build WORKDIR /backend COPY package*.json ./ RUN npm install COPY . . # Stage 2: Runtime stage FROM node:16.15.0-alpine WORKDIR /backend COPY --from=build /backend /backend EXPOSE 5001 CMD [ "node", "Server.js" ]
Create CICD for Azure DevOps. I have taken YAML approch and save it as
azure-pipeline.yaml
.# Build and push an image to Azure Container Registry # https://docs.microsoft.com/azure/devops/pipelines/languages/docker trigger: - DevopsStage resources: - repo: self variables: # Container registry service connection established during pipeline creation ContainerApp_env: 'UAEnv' ContainerApp_frontend_name: 'uafrontend' ContainerApp_backend_name: 'uabackend' rg: 'High-UA-RG' frontendimageRepository: 'frontend' backendimageRepository: 'backend' containerRegistry: 'high5.azurecr.io' frontend_dockerfilePath: '$(Build.SourcesDirectory)/Frontend/dockerfile' backend_dockerfilePath: '$(Build.SourcesDirectory)/Backend/dockerfile' tag: '$(Build.BuildId)' # Agent VM image name vmImageName: 'ubuntu-latest' pool: vmImage: $(vmImageName) stages: - stage: Frontend displayName: Frontend jobs: - job: Build_Frontend displayName: Frontend steps: - script: npm install --force workingDirectory: Frontend displayName: Install Node.js - task: Npm@1 displayName: Jest Unit Testing inputs: command: 'custom' customCommand: 'run test' workingDir: Frontend - task: Docker@2 displayName: Image creation inputs: containerRegistry: 'high5' repository: '$(frontendimageRepository)' command: 'build' Dockerfile: '$(frontend_dockerfilePath)' tags: '$(tag)' - task: Docker@2 displayName: Image push inputs: containerRegistry: 'high5' repository: '$(frontendimageRepository)' command: 'push' tags: '$(tag)' - task: AzureContainerApps@1 displayName: Deploying to ContainerApp '$(ContainerApp_frontend_name)' inputs: azureSubscription: 'ARM' imageToDeploy: '$(containerRegistry)/$(frontendimageRepository):$(tag)' containerAppName: '$(ContainerApp_frontend_name)' resourceGroup: '$(rg)' containerAppEnvironment: '$(ContainerApp_env)' targetPort: '3000' ingress: 'external' - stage: Backend displayName: Backend jobs: - job: Build_Backend displayName: Backend steps: - task: NodeTool@0 displayName: Use Node Version inputs: versionSpec: 16.15.0 - script: npm install workingDirectory: Backend displayName: Install Node.js - task: Npm@1 displayName: Jest Unit Testing inputs: command: 'custom' customCommand: 'run test' workingDir: Backend - task: Docker@2 displayName: Image creation inputs: containerRegistry: 'high5' repository: '$(backendimageRepository)' command: 'build' Dockerfile: '$(backend_dockerfilePath)' tags: '$(tag)' - task: Docker@2 displayName: Image push inputs: containerRegistry: 'high5' repository: '$(backendimageRepository)' command: 'push' tags: '$(tag)' - task: AzureContainerApps@1 displayName: Deploying to ContainerApp '$(ContainerApp_backend_name)' inputs: azureSubscription: 'ARM' imageToDeploy: '$(containerRegistry)/$(backendimageRepository):$(tag)' containerAppName: '$(ContainerApp_backend_name)' resourceGroup: '$(rg)' containerAppEnvironment: '$(ContainerApp_env)' targetPort: '5001' ingress: 'external'
Make sure you have created the "service connections" in Azure devops for acr as
high5repo
as shown in CICD YAML and azure subscription named asARM
as shown in CICD YAML.Specify the name which you have given to your Container Registry and Azure Subscription in service connection.
You can Add Service connection by going to the location- Azure devops > Choose the organisation > Choose the project > Go to project setting > Go to service connections > New service connection.
To make sure pipeline works, go to the portal.azure.com after running terraform and creating the infra. Follow below steps to check infra and do some changes.
a) Check all resources are created successfully.
b) Go to Container registry which you created and navigate to Access keys and make sure Admin user is checked.
c) Now go to Container environment
devenv
in my case and choose and click on container appdevfrontend
.Navigate to Containers and click on
Edit and deploy
.Click on the container and edit it with correct repo and image with managed identity. Also edit the CPU and memory and save it.
Go to Scale and choose the desired scaling option based on traffic. Container app that I have chosen is serverless so it can scale down to 0 when not active. And we will be charged based on our usage.
Now go to Revisions and Replicas, You can see 3 options,
Active revisions - This will show all the active containers
Inactive revisions - This will show all the inactive containers which you can activate if you want to rollback.
Replicas - And this will show number of replicas with restart count
Now Click on Overview and click on Application URL to check if application works and also if you want you can create a custom domain placed under setting.
If you want to check logs you can click on logstream and also you can view matrics by following the below screenshot.
d) Follow the above steps for backend container app as well.