Deploying Tines on AWS Fargate

Installation Steps 

Step 0. Before we start 

We recommend running Tines in a dedicated VPC. You can follow these instructions to create a new VPC with recommended configuration options. If you are running Tines in a VPC shared with other resources, you must ensure that there are two public and two private subnets that you can use for your Tines deployment.

There are some environment variables that are needed by multiple steps in this guide, so it's easiest to set them up in advance. To set up these variables, you would first need access to the AWS CLI:

# Replace this with the name of AWS region you're running Tines in:
export AWS_REGION="eu-west-1"
# Replace this with the name of AWS account you're running Tines in:
export AWS_ACCOUNT_ID="123456789012"
# Replace this with the ID of the VPC you're running Tines in:
export VPC_ID="vpc-xxxxxxxx"
# Replace these with the IDs of two private subnets from different AZs in your VPC:
# Ensure there is an outbound route configured within the private subnets route table with a Nat Gateway or similar
export PRIVATE_SUBNET_ID_1="subnet-xxxxxxxx"
export PRIVATE_SUBNET_ID_2="subnet-xxxxxxxx"
# Replace these with the IDs of two public subnets from different AZs in your VPC:
export PUBLIC_SUBNET_ID_1="subnet-xxxxxxxx"
export PUBLIC_SUBNET_ID_2="subnet-xxxxxxxx"
# Replace this with the name of the latest Tines image:
export IMAGE="tines-app:latest"
# Replace this with the name of the S3 bucket that will contain the environment files:
export ENV_FILE_S3_BUCKET="tines-test-env"

Step 1. Prepare an SSL certificate  

Following the instructions here, create a certificate for the domain that you ultimately want your Tines instance to be accessible at in your browser.

For this step, it's easiest to just use the AWS console and follow the instructions for validation. The remaining steps will use the CLI.

Step 2. Prepare the Tines Docker image  

For this step, you'll need access to the tines-app Docker Hub repository. If you don't have access then the Tines support team can provide it.

To make things a little easier later on, we'll create an AWS ECR repository and copy the image from Docker Hub into that repository:

aws ecr create-repository --repository-name tines-app

export REGISTRY="$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com"

# First you must login with your personal docker account that has access to the tines repo login
docker login

# Make sure and update the below region if necessary
aws ecr get-login-password --region eu-west-1 | \
  docker login --username AWS --password-stdin $REGISTRY

docker pull tines/$IMAGE
docker tag tines/$IMAGE $REGISTRY/$IMAGE
docker push $REGISTRY/$IMAGE

Step 3. Prepare some security groups  

To more clearly illustrate how the different components talk to each other, we'll create a security group for each one and open only the necessary ports. First we create the groups:

# Keep note of each GroupId value - we'll need them for later steps
aws ec2 create-security-group \
  --group-name tines-lb \
  --vpc-id $VPC_ID \
  --description "Load balancer security group for the Tines application"
aws ec2 create-security-group \
  --group-name tines-db \
  --vpc-id $VPC_ID \
  --description "Database security group for the Tines application"
aws ec2 create-security-group \
  --group-name tines-redis \
  --vpc-id $VPC_ID \
  --description "Redis security group for the Tines application"
aws ec2 create-security-group \
  --group-name tines-app \
  --vpc-id $VPC_ID \
  --description "tines-app container security group for the Tines application"
aws ec2 create-security-group \
  --group-name tines-sidekiq \
  --vpc-id $VPC_ID \
  --description "tines-sidekiq container security group for the Tines application"

Then we assign their IDs to environment variables that we can use throughout the rest of the process:

# Replace this with the ID of the tines-lb security group:
export LOAD_BALANCER_SECURITY_GROUP_ID="sg-xxxxxxxxxxxxxxxxx"
# Replace this with the ID of the tines-db security group:
export DATABASE_SECURITY_GROUP_ID="sg-xxxxxxxxxxxxxxxxx"
# Replace this with the ID of the tines-redis security group:
export REDIS_SECURITY_GROUP_ID="sg-xxxxxxxxxxxxxxxxx"
# Replace this with the ID of the tines-app security group:
export APP_SECURITY_GROUP_ID="sg-xxxxxxxxxxxxxxxxx"
# Replace this with the ID of the tines-sidekiq security group:
export SIDEKIQ_SECURITY_GROUP_ID="sg-xxxxxxxxxxxxxxxxx"

Then, we set up the necessary rules:

aws ec2 authorize-security-group-ingress \
  --group-id $LOAD_BALANCER_SECURITY_GROUP_ID \
  --protocol tcp --port 443 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress \
  --group-id $APP_SECURITY_GROUP_ID \
  --source-group $LOAD_BALANCER_SECURITY_GROUP_ID \
  --protocol tcp --port 3000
aws ec2 authorize-security-group-ingress \
  --group-id $DATABASE_SECURITY_GROUP_ID \
  --source-group $APP_SECURITY_GROUP_ID \
  --protocol tcp --port 5432
aws ec2 authorize-security-group-ingress \
  --group-id $DATABASE_SECURITY_GROUP_ID \
  --source-group $SIDEKIQ_SECURITY_GROUP_ID \
  --protocol tcp --port 5432
aws ec2 authorize-security-group-ingress \
  --group-id $REDIS_SECURITY_GROUP_ID \
  --source-group $APP_SECURITY_GROUP_ID \
  --protocol tcp --port 6379
aws ec2 authorize-security-group-ingress \
  --group-id $REDIS_SECURITY_GROUP_ID \
  --source-group $SIDEKIQ_SECURITY_GROUP_ID \
  --protocol tcp --port 6379

💡Note

Step 4. Create a Postgres database  

First, we create a database subnet group:

aws rds create-db-subnet-group \
  --db-subnet-group-name tines-db \
  --db-subnet-group-description "Tines database" \
  --subnet-ids $PRIVATE_SUBNET_ID_1 $PRIVATE_SUBNET_ID_2

Then, we create an Aurora PostgreSQL cluster with a single instance:

# Enter a password at the prompt after running this command.
# A value that contains punctuation other than underscores and dashes may cause errors.
echo "Type in the password you'd like to use for your database, then press (Enter): "
read -rs DB_PASSWORD

# Keep note of the Endpoint of the created object - we'll need it later
aws rds create-db-cluster \
  --db-cluster-identifier tines \
  --engine aurora-postgresql \
  --engine-version 14.6 \
  --backup-retention-period 7 \
  --master-username tines \
  --master-user-password $DB_PASSWORD \
  --database-name tines \
  --db-subnet-group-name tines-db \
  --vpc-security-group-ids $DATABASE_SECURITY_GROUP_ID

aws rds create-db-instance \
  --db-instance-identifier tines-db-1 \
  --engine aurora-postgresql \
  --db-instance-class db.t4g.large \
  --db-cluster-identifier tines \
  --db-subnet-group-name tines-db

Later, additional instances can be added to the cluster to support failover for higher availability.

ℹ️Info

Step 5. Create a Redis cluster  

aws elasticache create-cache-subnet-group \
  --cache-subnet-group-name tines-redis \
  --cache-subnet-group-description "Tines Redis" \
  --subnet-ids $PRIVATE_SUBNET_ID_1 $PRIVATE_SUBNET_ID_2

aws elasticache create-cache-cluster \
  --cache-cluster-id tines \
  --engine redis \
  --engine-version 6.x \
  --cache-node-type cache.t4g.small \
  --num-cache-nodes 1 \
  --cache-subnet-group-name tines-redis \
  --security-group-ids $REDIS_SECURITY_GROUP_ID

ℹ️Info

Step 6. Create a load balancer  

# Keep note of the ARN of the created object - we'll need it later in this step
aws elbv2 create-load-balancer \
  --name tines \
  --subnets $PUBLIC_SUBNET_ID_1 $PUBLIC_SUBNET_ID_2 \
  --security-groups $LOAD_BALANCER_SECURITY_GROUP_ID

Then create a target group:

# Keep note of the ARN of the created object - we'll need it later in this step
aws elbv2 create-target-group \
  --name tines-app \
  --protocol HTTP \
  --port 3000 \
  --target-type ip \
  --health-check-path '/is_up' \
  --vpc-id $VPC_ID

Then create a listener:

# Replace this with the ARN of the load balancer created by the previous command:
export LOAD_BALANCER_ARN="arn:aws:elasticloadbalancing:eu-west-1:123456789012:loadbalancer/app/tines/ad446b3207c26fe7"

# Replace this with the ARN of the target group created by the previous command:
export TARGET_GROUP_ARN="arn:aws:elasticloadbalancing:eu-west-1:123456789012:targetgroup/tines-app/66559e249e21308d"

# Replace this with the ARN of a certificate that you created at step 1
export CERTIFICATE_ARN="arn:aws:acm:eu-west-1:123456789012:certificate/85b79526-e45f-4e76-8e3a-1d407142a62a"

aws elbv2 create-listener \
  --load-balancer-arn $LOAD_BALANCER_ARN  \
  --protocol HTTPS \
  --port 443 \
  --certificates CertificateArn=$CERTIFICATE_ARN \
  --ssl-policy ELBSecurityPolicy-2016-08 \
  --default-actions '[{"Type": "forward", "TargetGroupArn": "'$TARGET_GROUP_ARN'"}]'

Step 7. Create a .env file 

Make sure to make any necessary changes to the values in the .env file before moving on to step 8.

cat << EOF > tines.env

#############################
# Required: Initial Tenant Configuration #
#
# Note: the values  in this section will only be read on the first deployment of
# the Tines instance. If you need to change these values after the first run, you will
# need to update the app or database directly. All other values in this file will be
# picked up by the Tines app on every deployment or server restart.
#############################

# A human friendly identifier for this instance of Tines, e.g. your company name:
TENANT_NAME=company_name

# The domain that you ultimately want your Tines installation to be accessible at in your browser.
# This should match the SSL certificate from step 1.
DOMAIN=tines.example.com

# This will be the first user to be created and get invited to this Tines instance:
SEED_EMAIL=alice@example.com
SEED_FIRST_NAME=Alice
SEED_LAST_NAME=Smith

# If SEED_EMAIL_PASSWORD is set, this will bypass the email invite process for the first user and allow the SEED_EMAIL to login without SMTP configured using SEED_EMAIL:SEED_EMAIL_PASSWORD 
# SEED_EMAIL_PASSWORD is *superceded* by either of the following 2 conditions:
# 1. If SMTP is configured correctly
# 2. If SSO is configured
SEED_EMAIL_PASSWORD=123456

#############################
# Required: Server Configuration #
#############################

# Company name and stack name (eg. tines_prod). This is used to identify your tenant's telemetry data,
# if you have enabled that feature.
TELEMETRY_ID=company_name_prod

# This should match the port that you use to access the Tines UI.
# Unless you have chosen a custom port, you should use 443 as typical for HTTPS.
PORT=443

# This should be set to a random 128 character string to ensure security for your installation.
# Changing this value may force users to log in again.
# You can generate a value for this by running: openssl rand -hex 64
APP_SECRET_TOKEN=__SET_YOUR_SECRET_TOKEN__

#############################
# Required: Email Configuration #
#############################
# Outgoing email settings. This must be configured correctly in order for the invite email
# to be sent to the first user.

SMTP_DOMAIN=mail.example.com
SMTP_USER_NAME=AKIAXXXXXXXXXXXXXXXX
SMTP_PASSWORD=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
SMTP_SERVER=email-smtp.eu-west-1.amazonaws.com
SMTP_PORT=587
SMTP_AUTHENTICATION=login
SMTP_ENABLE_STARTTLS_AUTO=true

# This address will be the sender for all emails from this Tines instance:
EMAIL_FROM_ADDRESS=Example Support <tines@example.com>

############################
# Required: Database connections
############################

# Your PostgreSQL database server settings:
# Populate this value using the host name for the database created in step 4:
DATABASE_HOST=tines.xxxxxxxxxxxx.eu-west-1.rds.amazonaws.com
DATABASE_NAME=tines
DATABASE_POOL=20
DATABASE_USERNAME=tines
# Populate this value using the password you set in step 4:
DATABASE_PASSWORD=xxxxxxxxx
DATABASE_PORT=5432

# Opt in to allow Tines app to forward to certain queries to the read only endpoint for performance efficiency
#DATABASE_READONLY_ENDPOINT=""

# Populate this value using the endpoint for the Redis cluster created in step 5:
REDIS_URL=redis://tines.xxxxxx.0001.euw1.cache.amazonaws.com:6379/1

########################
# Optional feature configuration #
########################

# Enables a periodic job to update public template date from template-data.tines.com
SYNC_TEMPLATES=true

# Set worker count
SIDEKIQ_CONCURRENCY=8

# Uncomment the line below to output Audit Logs to stdout
# AUDIT_LOGS_TO_STDOUT=true

# Seconds before a web request times out
# RACK_SERVICE_TIMEOUT_SECONDS=35

# Maximum fraction of sidekiq workers that can be used for slow action runs
# ACTION_RUNS_MAX_CONCURRENCY = 0.4 # 40%

########################
# Core configuration
#
# These values should not be changed.
########################

# Ensure system logs are included in Docker container logs.
RAILS_LOG_TO_STDOUT=true

# Configure Rails environment. This should always be set to 'production'.
RAILS_ENV=production

# Force all requests to use SSL.
FORCE_SSL=true

# Set the installation's timezone.
TIMEZONE=UTC

EOF

Step 8. Upload the .env file to S3 

Both the tines-app and tines-sidekiq containers need to run with the exact same environment variables. To make this a bit easier, we have them both fetch the same .env file from S3.

aws s3api create-bucket \
  --bucket $ENV_FILE_S3_BUCKET \
  --region $AWS_REGION \
  --create-bucket-configuration "LocationConstraint=$AWS_REGION"

aws s3api put-public-access-block \
  --bucket $ENV_FILE_S3_BUCKET \
  --public-access-block-configuration "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

aws s3 cp tines.env s3://$ENV_FILE_S3_BUCKET/tines.env

Step 9. Create the IAM roles for running the containers 

# This only needs to be create once for an AWS account - if you're already using ECS, you can skip this command:
aws iam create-service-linked-role \
  --aws-service-name ecs.amazonaws.com

aws iam create-role \
  --role-name tinesTaskExecutionRole \
  --assume-role-policy-document '{ "Version": "2012-10-17", "Statement": [{ "Sid": "", "Effect": "Allow", "Principal": { "Service": "ecs-tasks.amazonaws.com" }, "Action": "sts:AssumeRole" }]}'

aws iam attach-role-policy \
  --role-name tinesTaskExecutionRole \
  --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy

aws iam put-role-policy \
  --role-name tinesTaskExecutionRole \
  --policy-name TinesEnvAccess \
  --policy-document '{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Action": ["s3:GetObject"], "Resource": ["arn:aws:s3:::'$ENV_FILE_S3_BUCKET'/tines.env"]}, { "Effect": "Allow", "Action": ["s3:GetBucketLocation"], "Resource": ["arn:aws:s3:::'$ENV_FILE_S3_BUCKET'"]}]}'

Step 10. Create the ECS task definitions and cluster 

aws logs create-log-group --log-group-name tines

export EXECUTION_ROLE_ARN="arn:aws:iam::${AWS_ACCOUNT_ID}:role/tinesTaskExecutionRole"

aws ecs register-task-definition \
  --family "tines-app" \
  --memory 3072 \
  --network-mode awsvpc \
  --cpu 1024 \
  --execution-role-arn $EXECUTION_ROLE_ARN \
  --container-definitions '[{"name": "tines-app", "command": ["start-tines-app"], "image": "'$REGISTRY'/'$IMAGE'", "environmentFiles": [{"value": "arn:aws:s3:::'$ENV_FILE_S3_BUCKET'/tines.env", "type": "s3"}], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "tines", "awslogs-region": "'$AWS_REGION'", "awslogs-stream-prefix": "tines" }}, "portMappings": [{"containerPort": 3000}]}]'

aws ecs register-task-definition \
  --family "tines-sidekiq" \
  --memory 3072 \
  --network-mode awsvpc \
  --cpu 1024 \
  --execution-role-arn $EXECUTION_ROLE_ARN \
  --container-definitions '[{"name": "tines-sidekiq", "command": ["start-tines-sidekiq"], "image": "'$REGISTRY'/'$IMAGE'", "environmentFiles": [{"value": "arn:aws:s3:::'$ENV_FILE_S3_BUCKET'/tines.env", "type": "s3"}], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "tines", "awslogs-region": "'$AWS_REGION'", "awslogs-stream-prefix": "tines" }}}]'

aws ecs create-cluster --cluster-name tines

Step 11. Seed the database 

We run a one-off task, with a one-off command to prepare the database. This command will also trigger an invite email to the seed email address you specified in step 7, once the services in step 12 have been started. The invite link in this email is how you will sign into Tines for the first time.

aws ecs run-task \
  --cluster tines \
  --task-definition tines-app \
  --launch-type "FARGATE" \
  --network-configuration "awsvpcConfiguration={subnets=[$PRIVATE_SUBNET_ID_1,$PRIVATE_SUBNET_ID_2],securityGroups=[$SIDEKIQ_SECURITY_GROUP_ID],assignPublicIp=DISABLED}" \
  --overrides '{ "containerOverrides": [{ "name": "tines-app", "command": ["prepare-database"]}] }'

If you run into any issues during this initial setup, you can delete and recreate the database by running the following command. Please note that this will delete all data in your Tines instance, so it should only be used if the initial setup fails. After this command is run, you can repeat the command above to re-seed the database. If you have created the services from step 12 already, you should update them to set their Desired tasks to 0 while you run these commands.

# WARNING - this command will delete any data in the database.

aws ecs run-task \
  --cluster tines \
  --task-definition tines-app \
  --launch-type "FARGATE" \
  --network-configuration "awsvpcConfiguration={subnets=[$PRIVATE_SUBNET_ID_1,$PRIVATE_SUBNET_ID_2],securityGroups=[$SIDEKIQ_SECURITY_GROUP_ID],assignPublicIp=DISABLED}" \
  --overrides '{ "containerOverrides": [{ "name": "tines-app", "command": ["bundle", "exec", "rake", "db:drop", "db:create"], "environment": [{"name": "DISABLE_DATABASE_ENVIRONMENT_CHECK", "value": "1"}] }]}'

Step 12. Start the services 

aws ecs create-service \
  --cluster tines \
  --service-name tines-sidekiq \
  --task-definition tines-sidekiq \
  --desired-count 2 \
  --launch-type "FARGATE" \
  --network-configuration "awsvpcConfiguration={subnets=[$PRIVATE_SUBNET_ID_1,$PRIVATE_SUBNET_ID_2],securityGroups=[$SIDEKIQ_SECURITY_GROUP_ID],assignPublicIp=ENABLED}"

aws ecs create-service \
  --cluster tines \
  --service-name tines-app \
  --task-definition tines-app \
  --desired-count 2 \
  --launch-type "FARGATE" \
  --network-configuration "awsvpcConfiguration={subnets=[$PRIVATE_SUBNET_ID_1,$PRIVATE_SUBNET_ID_2],securityGroups=[$APP_SECURITY_GROUP_ID],assignPublicIp=ENABLED}" \
  --load-balancers "targetGroupArn=$TARGET_GROUP_ARN,containerName=tines-app,containerPort=3000"

ℹ️Info

Once the tines-app and tines-sidekiq services are both up and running, it will send the email that was set up during step 11. You can then accept the invite to get started.

Was this helpful?