Deploying Tines on AWS Fargate

Installation Steps 

Before we start 

There are some environment variables that are needed by multiple steps in this guide, so it's easiest to set them up in advance:

# 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:
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:fa7f7f1a_v14_0_3
# 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 and tines-nginx Docker Hub repositories. 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

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:

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  

# 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

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

aws rds create-db-instance \
  --db-instance-identifier tines \
  --db-instance-class db.t3.medium \
  --engine postgres \
  --engine-version 11.16 \
  --master-username tines \
  --master-user-password $DB_PASSWORD \
  --allocated-storage 80 \
  --db-name tines \
  --db-subnet-group-name tines-db \
  --vpc-security-group-ids $DATABASE_SECURITY_GROUP_ID

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

Step 6. Create a load balancer  

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:

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 \
  --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: Tenant Configuration #
#############################

# A human friendly identifier for this instance of Tines, e.g. your company name:
TENANT_NAME=company_name
# 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 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

# 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 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

# 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

########################
# 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

# Set worker count
SIDEKIQ_CONCURRENCY=6

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

# Replace this with the ARN of the role you created in step 9:
export EXECUTION_ROLE_ARN="arn:aws:iam::${AWS_ACCOUNT_ID}:role/tinesTaskExecutionRole"

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

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. The link in that email will work once we complete the next step.

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

Step 12. Start the services 

Once the tines-app and tines-sidekiq services are both up and running, you can accept the invite in the email sent during step 11 to get started:

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

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

aws ecs create-service \
  --cluster tines \
  --service-name tines-app \
  --task-definition tines-app:1 \
  --desired-count 1 \
  --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"
Was this helpful?