Deploying AWS Amplify with Terraform
AWS Amplify is a managed service for typical fullstack applications, allowing developers to connect their Git repository and manage automatic deployments of their app frontends, backend APIs, authentication, storage, serverless functions, etc. It’s AWS’ answer to Netlify, Vercel, Cloudflare kickstarter app deploys, and so abstracts away the underlying infrastructure and how it connects together for a simpler GUI and code-driven configuration.
Since AWS Amplify serves basic accelerated app deployments its Terraform support is in less demand which likely explains some long-standing bugs which I’ve circumvented and documented here.
To deploy with AWS Amplify via Terraform within a monorepo structure:
.
├── frontend
│ ├── package.json
│ └── ...
└── infra
├── main.tf
└── ...- From the AWS console, set up an IAM user with the policy
AdministratorAccess-Amplifyto manage our Terraform infrastructure. - Generate access keys for the IAM user, and then configure an AWS profile using these keys via the CLI:
aws configure --profile aws-amplify-demoThis will save the profile to ~/.aws/config and its secrets to ~/.aws/credentials.
- Set the AWS profile, and confirm it is the current profile:
export AWS_PROFILE=aws-amplify-demo
aws configure list # Show current configuration- Set up a GitHub personal access (fine-grained) token with the following (least-privilege) required permissions:
- Contents (Read-only) - access to the repository to build the app
- Webhooks (Read and write) - create a webhook on push to manage automatic deployments
- Configure Terraform for AWS Amplify. Take special note of the Terraform variables and how they’re used to ensure certain fields are consistent and present, especially
var.app_dirandvar.aws_region:
# variables.tf
variable "app_dir" {
type = string
description = "Root directory of the application within the monorepo"
}
variable "app_name" {
type = string
description = "Name of the AWS Amplify app, as it appears in the console"
}
variable "github_access_token" {
type = string
description = "Access Token for GitHub repository access and deployment webhooks"
sensitive = true
}
variable "github_repository_url" {
type = string
description = "URL of GitHub repository"
}
variable "aws_region" {
type = string
description = "AWS region"
}
variable "aws_profile" {
type = string
description = "Name of AWS profile to apply Terraform changes"
}
# versions.tf
terraform {
required_version = "1.14.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
profile = var.aws_profile
}
# main.tf
resource "aws_iam_role" "amplify_service_role" {
name = "amplify-service-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = [
"amplify.amazonaws.com",
"amplify.${var.aws_region}.amazonaws.com"
]
}
}
]
})
}
resource "aws_iam_role_policy_attachment" "amplify" {
role = aws_iam_role.amplify_service_role.name
policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess-Amplify"
}
resource "aws_amplify_app" "frontend" {
name = var.app_name
repository = var.github_repository_url
access_token = var.github_access_token
iam_service_role_arn = aws_iam_role.amplify_service_role.arn
environment_variables = {
AMPLIFY_MONOREPO_APP_ROOT = var.app_dir # Must match appRoot value in build_spec.applications.frontend
}
# The below is YAML - take care of indentation!
build_spec = <<-EOT
version: 1
applications:
- frontend:
phases:
preBuild:
commands:
- npm ci
build:
commands:
- npm run build
artifacts:
baseDirectory: dist
files:
- '**/*'
cache:
paths:
- node_modules/**/*
appRoot: ${var.app_dir}
EOT
}
resource "aws_amplify_branch" "main" {
app_id = aws_amplify_app.frontend.id
branch_name = "main"
}- We can now initialise, plan and apply Terraform:
terraform init
terraform plan # Check the plan looks OK
terraform apply # Check the plan looks OK, again- This will create the AWS Amplify instance, as visible in the AWS Console. The first deployment can be triggered by pushing to the
mainbranch, or manually triggering it within the AWS console. Alas, the deployment will fail with error:
[ERROR]: !!! Unable to assume specified IAM Role. Please ensure the selected IAM Role has sufficient permissions and the Trust Relationship is configured correctly.On the surface, this appears to be an issue with our IAM role not being able to be assumed when deploying our AWS Amplify instance—but this is a red herring! Since 2022, there has been an open GitHub issue reporting that the GitHub personal access token for AWS Amplify is insufficient, and this induces the misleading error.
To resolve this, one must navigate to AWS Amplify within the AWS console, then “App settings”, then “Branch settings”, and click “Reconnect Repository” to configure AWS Amplify for the GitHub repository.
If clicking “Reconnect Repository” fails with error:
There was an issue setting up your repository. Please try again later.({"message":"Not Found","documentation_url":"https://docs.github.com/rest/repos/webhooks#list-repository-webhooks","status":"404"})Another cryptic error that happens when an already installed AWS Amplify GitHub app does not have access to the repository. This is common when AWS Amplify has already been used and given access to a specific repository, and is unable to extend access to a new repository. We can overcome this by visiting https://github.com/settings/installations, and then configuring the AWS Amplify app to have access to our new repository. This effectively performs the “Reconnect Repository” step manually.
You may have noticed the installed AWS Amplify app has permissions which overlaps with our GitHub personal access token permissions:
- Write access to files located at amplify.yml
- Read access to code and metadata
- Read and write access to checks, pull requests, and repository hooks
So why do we need to configure both a GitHub personal access token and integrate with the AWS Amplify app? Seems like another bug…
- Deployment with AWS Amplify should now succeed!