diff --git a/.github/workflows/azure-deploy-all.yml b/.github/workflows/azure-deploy-all.yml new file mode 100644 index 0000000..8436780 --- /dev/null +++ b/.github/workflows/azure-deploy-all.yml @@ -0,0 +1,16 @@ +name: Deploy everything to Azure + +on: workflow_dispatch + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Deploy everything to Azure + run: | + cd src/Grinder.Farmer + dotnet run --all diff --git a/src/Grinder.Farmer/.farmer/farmer-deploy.json b/src/Grinder.Farmer/.farmer/farmer-deploy.json new file mode 100644 index 0000000..897f3ab --- /dev/null +++ b/src/Grinder.Farmer/.farmer/farmer-deploy.json @@ -0,0 +1,118 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "outputs": {}, + "parameters": { + "docker-password-for-vahterregistry": { + "type": "securestring" + } + }, + "resources": [ + { + "apiVersion": "2020-03-01-preview", + "location": "northeurope", + "name": "vahter-log", + "properties": { + "publicNetworkAccessForIngestion": "Enabled", + "publicNetworkAccessForQuery": "Enabled", + "retentionInDays": 30, + "sku": { + "name": "PerGB2018" + } + }, + "tags": {}, + "type": "Microsoft.OperationalInsights/workspaces" + }, + { + "apiVersion": "2016-08-01", + "dependsOn": [ + "vahter-app-farm", + "vahter-log" + ], + "identity": { + "type": "None" + }, + "kind": "app,linux,container", + "location": "northeurope", + "name": "vahter-app", + "properties": { + "httpsOnly": false, + "serverFarmId": "vahter-app-farm", + "siteConfig": { + "alwaysOn": true, + "appCommandLine": "", + "appSettings": [ + { + "name": "DOCKER_ENABLE_CI", + "value": "true" + }, + { + "name": "DOCKER_REGISTRY_SERVER_PASSWORD", + "value": "[parameters('docker-password-for-vahterregistry')]" + }, + { + "name": "DOCKER_REGISTRY_SERVER_URL", + "value": "https://vahterregistry.azurecr.io" + }, + { + "name": "DOCKER_REGISTRY_SERVER_USERNAME", + "value": "vahterregistry" + }, + { + "name": "VAHTER_CONFIG", + "value": "{\r\n \"Bot\": {\r\n \"Token\": \"800514531:AAH12BMMpXJvuvtdj_6p4UHWszVs6101I5I\",\r\n \"ChannelId\": -1001492656505,\r\n \"AdminUserId\": 67509832,\r\n \"ChatsToMonitor\": [\r\n \"@fsharp_flood\",\r\n \"@dotnetby\",\r\n \"@cilchat\",\r\n \"@comput_math\",\r\n \"@higher_math\",\r\n \"@elasticsearch_ru\",\r\n \"@Avalonia\",\r\n \"@fsharp_jobs\",\r\n \"@pro_latex\",\r\n \"@fsharp_chat\",\r\n \"@pro_net\",\r\n \"@DotNetRuChat\",\r\n \"@dotnettalks\",\r\n \"@DotNetRuJobs\",\r\n \"@DotNetChat\",\r\n \"@powershell_pro\"\r\n ],\r\n \"AllowedUsers\": [\r\n \"Liminiens\",\r\n \"fvnever\",\r\n \"aensidhe\",\r\n \"etkee\",\r\n \"EgorBo\",\r\n \"neftedollar\",\r\n \"zawodskoj\",\r\n \"kekekeks\",\r\n \"ahydrax\",\r\n \"hacklex\",\r\n \"striped\",\r\n \"XaveScor\",\r\n \"omgszer\",\r\n \"grishace\",\r\n \"AnutaU\"\r\n ]\r\n }\r\n}" + } + ], + "connectionStrings": [], + "linuxFxVersion": "DOCKER|vahterregistry.azurecr.io/vahter/grinder:latest", + "metadata": [] + } + }, + "tags": {}, + "type": "Microsoft.Web/sites" + }, + { + "apiVersion": "2018-02-01", + "kind": "linux", + "location": "northeurope", + "name": "vahter-app-farm", + "properties": { + "name": "vahter-app-farm", + "perSiteScaling": false, + "reserved": true + }, + "sku": { + "capacity": 1, + "name": "B1", + "size": "0", + "tier": "Basic" + }, + "tags": {}, + "type": "Microsoft.Web/serverfarms" + }, + { + "type": "Microsoft.Web/sites/providers/diagnosticSettings", + "apiVersion": "2017-05-01-preview", + "name": "[concat('vahter-app', '/microsoft.insights/', 'vahter-log')]", + "dependsOn": [], + "properties": { + "workspaceId": "[resourceId('Microsoft.OperationalInsights/workspaces', 'vahter-log')]", + "metrics": [], + "logs": [ + { + "category": "AppServiceConsoleLogs", + "enabled": true + }, + { + "category": "AppServiceAppLogs", + "enabled": true + }, + { + "category": "AppServiceHTTPLogs", + "enabled": true + } + ] + } + } + ] +} \ No newline at end of file diff --git a/src/Grinder.Farmer/Grinder.Farmer.fsproj b/src/Grinder.Farmer/Grinder.Farmer.fsproj new file mode 100644 index 0000000..0015ab9 --- /dev/null +++ b/src/Grinder.Farmer/Grinder.Farmer.fsproj @@ -0,0 +1,22 @@ + + + + Exe + net5.0 + + + + + + PreserveNewest + + + + + + + + + + + diff --git a/src/Grinder.Farmer/Program.fs b/src/Grinder.Farmer/Program.fs new file mode 100644 index 0000000..1b6d5f0 --- /dev/null +++ b/src/Grinder.Farmer/Program.fs @@ -0,0 +1,185 @@ +open System +open System.IO +open Farmer +open Farmer.Builders +open Medallion.Shell + +let resourceGroup = "vahter-rg" +let acrName = "vahterregistry" +let logName = "vahter-log" +let appName = "vahter-app" + +let getEnv name = + match Environment.GetEnvironmentVariable name with + | null -> + failwith $"Provide required ENV variable: {name}" +// Console.WriteLine $"Please provide ENV variable: {name}" +// Console.ReadLine() + | value -> value + +let appId = getEnv "VAHTER_DEPLOY_APPID" +let pwd = getEnv "VAHTER_DEPLOY_PWD" +let tenant = getEnv "VAHTER_DEPLOY_TENANT" + +type Result.ResultBuilder with + member _.Bind(cmd: Command, next: unit -> Result<'a, string>): Result<'a, string> = + cmd.StandardOutput.PipeToAsync(Console.Out, true) |> ignore + cmd.StandardError.PipeToAsync(Console.Error, true) |> ignore + if cmd.Result.Success then + next() + else + Error cmd.Result.StandardError + +let botSettings = + let vahterConfig = Environment.GetEnvironmentVariable "VAHTER_CONFIG" + if File.Exists "./settings.json" then + File.ReadAllText "settings.json" + else if vahterConfig <> null then + vahterConfig + else + failwith "Please put bot settings either 1) in settings.json 2) or in env VAHTER_CONFIG" + +let logs = logAnalytics { + name logName + + retention_period 30 + enable_query + enable_ingestion +} + +let registry = containerRegistry { + name acrName + + sku ContainerRegistry.Basic + enable_admin_user +} + +let botApp = webApp { + name appName + + app_insights_off + always_on + operating_system Linux + sku WebApp.Sku.B1 + + setting "VAHTER_CONFIG" botSettings + + docker_ci + docker_use_azure_registry acrName + docker_image "vahter/grinder:latest" "" + + depends_on logs.Name +} + +let registryDeployment = arm { + location Location.NorthEurope + add_resource registry + output "host" registry.LoginServer + output "pwd" $"[listCredentials(resourceId('Microsoft.ContainerRegistry/registries','{acrName}'),'2017-10-01').passwords[0].value]" + output "login" $"['{acrName}']" +} + +let appDeployment = arm { + location Location.NorthEurope + add_resources [ + logs + botApp + ] + add_resource (Resource.ofJson $""" +{{ + "type": "Microsoft.Web/sites/providers/diagnosticSettings", + "apiVersion": "2017-05-01-preview", + "name": "[concat('{appName}', '/microsoft.insights/', '{logName}')]", + "dependsOn": [], + "properties": {{ + "workspaceId": "[resourceId('Microsoft.OperationalInsights/workspaces', '{logName}')]", + "metrics": [], + "logs": [ + {{ + "category": "AppServiceConsoleLogs", + "enabled": true + }}, + {{ + "category": "AppServiceAppLogs", + "enabled": true + }}, + {{ + "category": "AppServiceHTTPLogs", + "enabled": true + }} + ] + }} +}} +""") +} + +let getAcrCreds() = result { + let azShell = Shell(fun opts -> + opts.ThrowOnError(false) + |> ignore + ) + do! azShell.Run("az", "login", "--service-principal", "-u", appId, "-p", pwd, "--tenant", tenant) + let pwd = azShell.Run("az", "acr", "credential", "show", "--name", acrName, "--query", "passwords[0].value") + let pwdStringRaw = pwd.Result.StandardOutput + let pwdString = pwdStringRaw.Substring(1, pwdStringRaw.Length - 3) // truncating first and last chars + return $"{acrName}.azurecr.io", acrName, pwdString +} + +let pushDockerImage (host, user, pwd) = result { + let dockerShell = Shell(fun opts -> + opts.ThrowOnError(false) + .WorkingDirectory("../..") + |> ignore + ) + + do! dockerShell.Run("docker", "login", "-u", user, "-p", pwd, host) + do! dockerShell.Run("docker", "build", ".", "-t", "grinder") + do! dockerShell.Run("docker", "tag", "grinder", $"{host}/vahter/grinder") + do! dockerShell.Run("docker", "push", $"{host}/vahter/grinder") + return () +} + +let deployAll() = result { + // authenticate into Azure + let! authResult = Deploy.authenticate appId pwd tenant + printfn "%A" authResult + + // deploying container registry + let! registryDeploymentResult = + Deploy.tryExecute + resourceGroup + Deploy.NoParameters + registryDeployment + + let registryPwd = registryDeploymentResult.["pwd"] + let registryLogin = registryDeploymentResult.["login"] + let registryHost = registryDeploymentResult.["host"] + + // build&push image to registry + do! pushDockerImage(registryHost, registryLogin, registryPwd) + + // deploy webapp with bot + let! deploymentResult = + Deploy.tryExecute + resourceGroup + [ botApp.DockerAcrCredentials.Value.Password.Value, registryPwd ] + appDeployment + return printfn "%A" deploymentResult +} + +[] +let main argv = + match argv with + | null | [||] -> + // deploy only image + result { + let! host, usr, pwd = getAcrCreds() + do! pushDockerImage(host, usr, pwd) + } + | x when Array.contains "--all" x -> + // deploy everything = resources + image + deployAll() + | x -> + failwithf "Unknown arguments %A" x + |> Result.get + 0 \ No newline at end of file diff --git a/src/Grinder.Farmer/README.MD b/src/Grinder.Farmer/README.MD new file mode 100644 index 0000000..4e4a321 --- /dev/null +++ b/src/Grinder.Farmer/README.MD @@ -0,0 +1,28 @@ +#Description +This project will deploy Grinder bot to Azure +There are two scenarios +- Deploy Azure resources together with docker image +- Build and push docker image only into predeployed container registry + +To deploy everything pass `--all` argument + +To deploy just an image don't pass any argument + +#Prerequisites +- AZ CLI +- Docker +- Net5 SDK + +- Azure credentials (put them in env variables) + - `VAHTER_DEPLOY_APPID` + - `VAHTER_DEPLOY_PWD` + - `VAHTER_DEPLOY_TENANT` + +- Bot settings (pick one) + - `settings.json` file + - `VAHTER_CONFIG` variable + +#Run +`dotnet run [args]` (from project folder) + +or just run from IDE