AWS App Runner for Node.js with Docker and Terraform

Bjorn Krols

Bjorn Krols

Published on
04 June 2021

Application code

npm install koa
// index.js
const Koa = require("koa");
const application = new Koa();

application.use(async (ctx) => {
  ctx.body = "Hello, World!";
});

application.listen(8000);

Dockerfile

FROM node:14-alpine
WORKDIR /usr/src/app
COPY package*.json ./it
RUN npm ci --production
COPY . .
EXPOSE 8080
CMD [ "node", "index.js" ]

Terraform

  1. Run terraform apply -target aws_ecr_repository.ecr_repository
  2. Deploy an initial version of your image (see script below)
  3. Run terraform apply

If the provisioning of the AWS App Runner service takes more than 5-10 minutes, something is probably wrong.

locals {
  ecr_repository_name = "hello-world"
  service_name        = "hello-world"
  service_port        = 8000
  service_release_tag = "latest"
}

data "aws_caller_identity" "current" {}

data "aws_region" "current" {}

resource "aws_ecr_repository" "ecr_repository" {
  name = local.ecr_repository_name
  image_scanning_configuration {
    scan_on_push = true
  }
}

resource "aws_ecr_lifecycle_policy" "ecr_lifecycle_policy" {
  repository = aws_ecr_repository.ecr_repository.name
  policy     = jsonencode({
    "rules" : [
      {
        "rulePriority" : 1,
        "description" : "Expire untagged images older than 14 days",
        "selection" : {
          "tagStatus" : "untagged",
          "countType" : "sinceImagePushed",
          "countUnit" : "days",
          "countNumber" : 14
        },
        "action" : {
          "type" : "expire"
        }
      }
    ]
  })
}

resource "aws_iam_role" "runner_role" {
  name               = "${local.service_name}-role"
  assume_role_policy = jsonencode({
    Version   = "2012-10-17"
    Statement = [
      {
        Action    = "sts:AssumeRole"
        Effect    = "Allow"
        Principal = {
          Service = "build.apprunner.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "runner_role_policy_attachment" {
  role       = aws_iam_role.runner_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSAppRunnerServicePolicyForECRAccess"
}

resource "aws_apprunner_service" "runner_service" {
  service_name = local.service_name
  source_configuration {
    authentication_configuration {
      access_role_arn = aws_iam_role.runner_role.arn
    }
    image_repository {
      image_identifier      = "${aws_ecr_repository.ecr_repository.repository_url}:${local.service_release_tag}"
      image_repository_type = "ECR"
      image_configuration {
        port = local.service_port
      }
    }
  }
}

output "service_url" {
  value = aws_apprunner_service.runner_service.service_url
}

Custom domain

  1. Run terraform apply -target aws_apprunner_custom_domain_association.runner_custom_domain
  2. Run terraform apply

I have seen the DNS validation take up to 20 minutes.

locals {
  custom_domain = "test.com"
}

resource "aws_apprunner_custom_domain_association" "runner_custom_domain" {
  domain_name = local.custom_domain
  service_arn = aws_apprunner_service.runner_service.arn
}

resource "aws_route53_record" "runner_custom_domain_record" {
  allow_overwrite = true
  name            = local.custom_domain
  records         = [
    aws_apprunner_custom_domain_association.runner_custom_domain.dns_target
  ]
  ttl             = 60
  type            = "CNAME"
  zone_id         = aws_route53_zone.hosted_zone.zone_id
}

resource "aws_route53_record" "runner_custom_domain_validation_record" {
  for_each        = {for r in aws_apprunner_custom_domain_association.runner_custom_domain.certificate_validation_records :  r.name => r}
  allow_overwrite = true
  name            = each.value.name
  records         = [
    each.value.value
  ]
  ttl             = 60
  type            = each.value.type
  zone_id         = aws_route53_zone.hosted_zone.zone_id
}

Deploy script

#!/bin/bash

set -e

ACCOUNT_ID="your-account-id"
REGION="your-region"
REPOSITORY="hello-world"
RELEASE_TAG="latest"
IMAGE_URI="$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/$REPOSITORY:$RELEASE_TAG"

docker build -t "$REPOSITORY:$RELEASE_TAG" .
docker tag "$REPOSITORY:$RELEASE_TAG" "$IMAGE_URI"
aws ecr get-login-password --region "$REGION" | docker login --username AWS --password-stdin "$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com"
docker push "$IMAGE_URI"

Subscribe to our newsletter

The latest news, articles, and resources, sent to your inbox weekly.

More like this