Skip to main content
This is a guide to setup OpenTaco Statesman with AWS Fargate and Microsoft Azure Authentication. The 3 files referenced are available in examples/aws-fargate-quickstart in the repo.

Prerequisites

  • Terraform >= 1.6.0
  • AWS CLI configured with credentials
  • AWS Bucket
  • Azure Account

Download the CLI

First you’ll want to download the CLI much like we do in the quickstart, this is not changed. We will have our server url later on though obviously, so don’t login yet.

Create Azure Native App

Then we’ll want to create a native app. Sign into azure, then navigate to Microsoft Entra ID. Search for Entra Once you’ve signed in, go to add and select “App Registration” Add App Registration From there you can name your app, select the platform as “Public client/native (mobile and desktop)” and for the redirect put “http://localhost:8585/callback”, we’ll need to add another oidc redirect later. App Registration

Set Up Terraform Files

Next, we’ll want to navigate to a new directory. We’ll create three files: main.tf, variables.tf, and dev.tfvars Lets start with our dev.tfvars:
region            = "us-west-2"
vpc_id            = "vpc-0123abcd"
public_subnet_ids = ["subnet-aaa", "subnet-bbb"]
container_image   = "ghcr.io/diggerhq/digger/taco-statesman:latest"

opentaco_s3_bucket   = "your-s3-bucket"
opentaco_s3_region   = "us-east-1"
opentaco_s3_prefix   = "your-prefix"



opentaco_auth_issuer    = "https://login.microsoftonline.com/your-tenant-id/v2.0" # no trailing slash! 
opentaco_auth_client_id = "your-application-client-id"
opentaco_auth_auth_url  = "https://login.microsoftonline.com/your-tenant-id/oauth2/v2.0/authorize"
opentaco_auth_token_url =  "https://login.microsoftonline.com/your-tenant-id/oauth2/v2.0/token"
# Keep this out of git; set via TF_VAR_... or a secrets manager:
opentaco_auth_client_secret = "your-client-secret"

opentaco_port         = 8080
opentaco_storage      = "s3"
opentaco_auth_disable = "false"
opentaco_public_base_url = "https://your-cloudfront-instance.cloudfront.net"
You may notice there are some values we don’t have handy, besides the base url which we’ll get once we apply.

Get AWS Resources

To get your vpc run:
VPC_ID=$(aws ec2 describe-vpcs \
  --filters Name=isDefault,Values=true \
  --query 'Vpcs[0].VpcId' --output text)
echo "$VPC_ID"
vpc-062393XXXXXXXX
To get your available subnets run this and pick 2 of them, I picked the first two in my table:
aws ec2 describe-subnets --filters Name=vpc-id,Values="$VPC_ID" \
  --query 'Subnets[].{id:SubnetId,az:AvailabilityZone,public:MapPublicIpOnLaunch}' \
  --output table
You can fill in your bucket details but then for the next few values we need to head back to Azure. In your application’s overview section you can define a secret, and copy it and the other values into our vars file. App Registration Now the only thing we don’t have is our base url but we’ll get that later. For now, lets add our main.tf:
#############################################
# main.tf — ECS (Fargate) + NLB + CloudFront
# Uses variables from variables.tf / *.tfvars
#############################################

terraform {
  required_version = ">= 1.6.0"
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.0" }
  }
}

provider "aws" {
  region = var.region
}

locals {
  name = var.name_prefix
}

############################
# Security (tasks SG)
############################
resource "aws_security_group" "tasks" {
  name   = "${local.name}-tasks-sg"
  vpc_id = var.vpc_id

  # Demo: open app port. Lock down in prod.
  ingress {
    protocol    = "tcp"
    from_port   = var.container_port
    to_port     = var.container_port
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    protocol    = "-1"
    from_port   = 0
    to_port     = 0
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = { Name = "${local.name}-tasks-sg" }
}

############################
# Network Load Balancer
############################
resource "aws_lb" "nlb" {
  name               = "${local.name}-nlb"
  load_balancer_type = "network"
  internal           = false
  subnets            = var.public_subnet_ids

  tags = { Name = "${local.name}-nlb" }
}

resource "aws_lb_target_group" "tg" {
  name        = "${local.name}-tg"
  port        = var.container_port
  protocol    = "TCP"
  vpc_id      = var.vpc_id
  target_type = "ip"

  health_check {
    protocol = "TCP"
    port     = "traffic-port"
  }

  tags = { Name = "${local.name}-tg" }
}

resource "aws_lb_listener" "tcp80" {
  load_balancer_arn = aws_lb.nlb.arn
  port              = 80
  protocol          = "TCP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.tg.arn
  }
}

############################
# IAM (exec + task roles)
############################
data "aws_iam_policy_document" "task_assume" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
  }
}

# Execution role: ECR pulls, CloudWatch Logs, etc.
resource "aws_iam_role" "exec" {
  name               = "${local.name}-exec"
  assume_role_policy = data.aws_iam_policy_document.task_assume.json
}

resource "aws_iam_role_policy_attachment" "exec_logs" {
  role       = aws_iam_role.exec.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

# Add Secrets Manager access for the execution role
resource "aws_iam_role_policy_attachment" "exec_secrets" {
  role       = aws_iam_role.exec.name
  policy_arn = "arn:aws:iam::aws:policy/SecretsManagerReadWrite"
}

# Task role: grant app S3 access (bucket + prefix)
resource "aws_iam_role" "task" {
  name               = "${local.name}-task"
  assume_role_policy = data.aws_iam_policy_document.task_assume.json
}

data "aws_iam_policy_document" "s3_policy" {
  statement {
    actions   = ["s3:ListBucket"]
    resources = ["arn:aws:s3:::${var.opentaco_s3_bucket}"]
    condition {
      test     = "StringLike"
      variable = "s3:prefix"
      values   = ["${var.opentaco_s3_prefix}/*"]
    }
  }
  statement {
    actions   = ["s3:GetObject","s3:PutObject","s3:DeleteObject"]
    resources = ["arn:aws:s3:::${var.opentaco_s3_bucket}/${var.opentaco_s3_prefix}/*"]
  }
}

resource "aws_iam_policy" "s3_policy" {
  name   = "${local.name}-s3"
  policy = data.aws_iam_policy_document.s3_policy.json
}

resource "aws_iam_role_policy_attachment" "task_s3" {
  role       = aws_iam_role.task.name
  policy_arn = aws_iam_policy.s3_policy.arn
}

############################
# Logs + Secrets
############################
resource "aws_cloudwatch_log_group" "lg" {
  name              = "/ecs/${local.name}"
  retention_in_days = 7
}

resource "aws_secretsmanager_secret" "auth0_client_secret" {
  name = "${local.name}/auth0_client_secret_v2"
}

resource "aws_secretsmanager_secret_version" "auth0_client_secret_v" {
  secret_id     = aws_secretsmanager_secret.auth0_client_secret.id
  secret_string = var.opentaco_auth_client_secret
}

############################
# ECS Cluster / Task / Service
############################
resource "aws_ecs_cluster" "cluster" {
  name = "${local.name}-cluster"
}

resource "aws_ecs_task_definition" "taskdef" {
  family                   = "${local.name}-task"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = "512"   # 0.5 vCPU
  memory                   = "1024"  # 1 GB
  execution_role_arn       = aws_iam_role.exec.arn
  task_role_arn            = aws_iam_role.task.arn

  container_definitions = jsonencode([
    {
      name        = "web"
      image       = var.container_image
      essential   = true
      portMappings = [{ containerPort = var.container_port, protocol = "tcp" }]

      environment = [
        { name = "OPENTACO_S3_BUCKET",  value = var.opentaco_s3_bucket },
        { name = "OPENTACO_S3_REGION",  value = var.opentaco_s3_region },
        { name = "OPENTACO_S3_PREFIX",  value = var.opentaco_s3_prefix },

        { name = "OPENTACO_AUTH_ISSUER",    value = var.opentaco_auth_issuer },
        { name = "OPENTACO_AUTH_CLIENT_ID", value = var.opentaco_auth_client_id },
        { name = "OPENTACO_AUTH_AUTH_URL",  value = var.opentaco_auth_auth_url },
        { name = "OPENTACO_AUTH_TOKEN_URL", value = var.opentaco_auth_token_url },

        { name = "OPENTACO_PORT",         value = tostring(var.opentaco_port) },
        { name = "OPENTACO_STORAGE",      value = var.opentaco_storage },
        { name = "OPENTACO_AUTH_DISABLE", value = var.opentaco_auth_disable },
        { name = "OPENTACO_PUBLIC_BASE_URL", value = var.opentaco_public_base_url }
      ]

      secrets = [
        { name = "OPENTACO_AUTH_CLIENT_SECRET", valueFrom = aws_secretsmanager_secret.auth0_client_secret.arn }
      ]

      logConfiguration = {
        logDriver = "awslogs",
        options = {
          awslogs-group         = aws_cloudwatch_log_group.lg.name,
          awslogs-region        = var.region,
          awslogs-stream-prefix = "ecs"
        }
      }
    }
  ])
}

resource "aws_ecs_service" "svc" {
  name            = "${local.name}-svc"
  cluster         = aws_ecs_cluster.cluster.id
  task_definition = aws_ecs_task_definition.taskdef.arn
  desired_count   = 1
  launch_type     = "FARGATE"
  platform_version = "LATEST"

  load_balancer {
    target_group_arn = aws_lb_target_group.tg.arn
    container_name   = "web"
    container_port   = var.container_port
  }

  network_configuration {
    subnets          = var.public_subnet_ids   # public subnets → no NAT required
    security_groups  = [aws_security_group.tasks.id]
    assign_public_ip = true
  }

  depends_on = [aws_lb_listener.tcp80]
}

############################
# CloudFront (free *.cloudfront.net HTTPS)
############################
resource "aws_cloudfront_distribution" "edge" {
  enabled     = true
  comment     = "${local.name} via CloudFront"
  price_class = "PriceClass_100"  # US/EU

  origin {
    domain_name = aws_lb.nlb.dns_name
    origin_id   = "nlb-origin"

    # NLB is a public HTTP origin for this demo
    custom_origin_config {
      http_port              = 80
      https_port             = 443
      origin_protocol_policy = "http-only"
      origin_ssl_protocols   = ["TLSv1.2"]
    }
  }

  default_cache_behavior {
    target_origin_id       = "nlb-origin"
    viewer_protocol_policy = "redirect-to-https"

    allowed_methods = ["GET","HEAD","OPTIONS","PUT","POST","PATCH","DELETE"]
    cached_methods  = ["GET","HEAD"]
    compress        = true

    # forward everything; disable caching for dynamic/API
    forwarded_values {
      query_string = true
      headers      = ["*"]
      cookies { forward = "all" }
    }
    min_ttl     = 0
    default_ttl = 0
    max_ttl     = 0
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }

  depends_on = [aws_lb_listener.tcp80]
}

############################
# Outputs
############################
output "cloudfront_domain" {
  description = "Public HTTPS domain for your app"
  value       = aws_cloudfront_distribution.edge.domain_name
}

output "nlb_dns_name" {
  description = "NLB DNS (HTTP origin behind CloudFront)"
  value       = aws_lb.nlb.dns_name
}

output "ecs_cluster" { value = aws_ecs_cluster.cluster.name }
output "ecs_service" { value = aws_ecs_service.svc.name }
and our variables.tf:


variable "region"            { type = string }
variable "name_prefix" {
  type    = string
  default = "statesman"
}

variable "vpc_id"            { type = string }
variable "public_subnet_ids" { type = list(string) }

variable "container_image"   { type = string }
variable "container_port" {
  type    = number
  default = 8080
}

# App config (non-secrets)
variable "opentaco_s3_bucket"  { type = string }
variable "opentaco_s3_region"  { type = string }
variable "opentaco_s3_prefix"  { type = string }

variable "opentaco_auth_issuer"    { type = string }
variable "opentaco_auth_client_id" { type = string }
variable "opentaco_auth_auth_url"  { type = string }
variable "opentaco_auth_token_url" { type = string }

variable "opentaco_port"         { type = number }
variable "opentaco_storage"      { type = string }
# Keep as string if your app expects "true"/"false"
variable "opentaco_auth_disable" { type = string }

variable "opentaco_public_base_url" { type = string }

# Secret
variable "opentaco_auth_client_secret" {
  type      = string
  sensitive = true
}

Deploy Infrastructure

Now from the root of this directory we can run the first apply, after this we’ll get the cloudfront domain and we can log in:
terraform apply -var-file=dev.tfvars -auto-approve
The apply takes a while for the first one, once it is done you can run the following for the cloudfront domain:
terraform output -raw cloudfront_domain

Update Configuration

Now we have to add this with https:// to our tfvars file as our OPENTACO_PUBLIC_BASE_URL, if you’ve been following along it should be the only value missing. We also need to add https://your-instance.cloudfront.net/oauth/oidc-callback to our redirect URIs in Azure, this can be found under “Manage” -> “Authentication” Your result should look like this: App Registration With those two set we can apply again:
terraform apply -var-file=dev.tfvars -auto-approve
We can check our backend is ready
echo "https://$(terraform output -raw cloudfront_domain)/readyz"

Login

Now with our service ready, we can run taco login and set our server to be the same value as our OPENTACO_PUBLIC_BASE_URL. For reference I used https://d2xr3at38awj4b.cloudfront.net/
I