Deploy the TripIt → Reclaim timezone sync as a scheduled ECS Fargate task that runs daily at 3 AM UTC. The container runs, syncs, and exits — you only pay for the few seconds of execution (~$0.01/month).
- AWS CLI installed and configured (
aws configure) - Docker installed
- Your TripIt iCal feed URL and Reclaim.ai API token
EventBridge Rule (cron: daily 3 AM UTC)
→ ECS RunTask on Fargate (256 CPU / 512 MiB)
→ Pulls image from ECR
→ Injects secrets from SSM Parameter Store
→ Runs: node sync.mjs sync
→ Logs to CloudWatch
→ (Optional) Notifies via Telegram and/or SNS email
→ Container exits (no ongoing cost)
Set these variables for use throughout the guide:
export AWS_REGION=us-west-1 # change to your preferred region
export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)aws ecr create-repository \
--repository-name reclaim-tripit-sync \
--region $AWS_REGION \
--image-scanning-configuration scanOnPush=true# Authenticate Docker to ECR
aws ecr get-login-password --region $AWS_REGION | \
docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com
# Build the image
docker build -t reclaim-tripit-sync .
# Tag and push
docker tag reclaim-tripit-sync:latest \
$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/reclaim-tripit-sync:latest
docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/reclaim-tripit-sync:latestTip: If your internet upload is slow, launch a temporary EC2 instance in the same region, clone the repo there, build and push from within AWS where the network to ECR is fast and free. Terminate the instance when done.
aws ssm put-parameter \
--name /reclaim-tripit-sync/TRIPIT_ICAL_URL \
--type SecureString \
--value 'YOUR_TRIPIT_ICAL_URL' \
--region $AWS_REGION
aws ssm put-parameter \
--name /reclaim-tripit-sync/RECLAIM_API_TOKEN \
--type SecureString \
--value 'YOUR_RECLAIM_API_TOKEN' \
--region $AWS_REGION
# Optional: Google Calendar credentials for OOO blocks (see README)
aws ssm put-parameter \
--name /reclaim-tripit-sync/GOOGLE_CLIENT_ID \
--type SecureString \
--value 'YOUR_GOOGLE_CLIENT_ID' \
--region $AWS_REGION
aws ssm put-parameter \
--name /reclaim-tripit-sync/GOOGLE_CLIENT_SECRET \
--type SecureString \
--value 'YOUR_GOOGLE_CLIENT_SECRET' \
--region $AWS_REGION
aws ssm put-parameter \
--name /reclaim-tripit-sync/GOOGLE_REFRESH_TOKEN \
--type SecureString \
--value 'YOUR_GOOGLE_REFRESH_TOKEN' \
--region $AWS_REGIONNote: If you are re-running these commands to update credentials, add the
--overwriteflag to eachput-parametercall. Without it, AWS will reject the request because the parameter already exists.
aws logs create-log-group \
--log-group-name /ecs/reclaim-tripit-sync \
--region $AWS_REGION
aws logs put-retention-policy \
--log-group-name /ecs/reclaim-tripit-sync \
--retention-in-days 30 \
--region $AWS_REGIONTask execution role (used by ECS to pull images, fetch secrets, write logs):
aws iam create-role \
--role-name reclaim-tripit-sync-execution-role \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "ecs-tasks.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}'
aws iam attach-role-policy \
--role-name reclaim-tripit-sync-execution-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
aws iam put-role-policy \
--role-name reclaim-tripit-sync-execution-role \
--policy-name SSMParameterAccess \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["ssm:GetParameters", "ssm:GetParameter"],
"Resource": ["arn:aws:ssm:'$AWS_REGION':'$AWS_ACCOUNT_ID':parameter/reclaim-tripit-sync/*"]
}]
}'Task role (used by the container at runtime — needs SNS publish if using SNS notifications):
aws iam create-role \
--role-name reclaim-tripit-sync-task-role \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "ecs-tasks.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}'
# Optional: Allow SNS publish for email notifications (skip if not using SNS)
aws iam put-role-policy \
--role-name reclaim-tripit-sync-task-role \
--policy-name SNSPublishAccess \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "sns:Publish",
"Resource": "arn:aws:sns:'$AWS_REGION':'$AWS_ACCOUNT_ID':reclaim-tripit-sync"
}]
}'EventBridge role (allows EventBridge to trigger ECS tasks):
aws iam create-role \
--role-name reclaim-tripit-sync-eventbridge-role \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "events.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}'
aws iam put-role-policy \
--role-name reclaim-tripit-sync-eventbridge-role \
--policy-name ECSRunTask \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "ecs:RunTask",
"Resource": "arn:aws:ecs:'$AWS_REGION':'$AWS_ACCOUNT_ID':task-definition/reclaim-tripit-sync:*",
"Condition": {
"ArnEquals": {
"ecs:cluster": "arn:aws:ecs:'$AWS_REGION':'$AWS_ACCOUNT_ID':cluster/reclaim-tripit-sync"
}
}
},
{
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": [
"arn:aws:iam::'$AWS_ACCOUNT_ID':role/reclaim-tripit-sync-execution-role",
"arn:aws:iam::'$AWS_ACCOUNT_ID':role/reclaim-tripit-sync-task-role"
]
}]
}'aws ecs create-cluster \
--cluster-name reclaim-tripit-sync \
--region $AWS_REGIONIf you want email notifications when the sync makes changes (in addition to or instead of Telegram):
# Create SNS topic
aws sns create-topic \
--name reclaim-tripit-sync \
--region $AWS_REGION
# Subscribe your email
aws sns subscribe \
--topic-arn arn:aws:sns:$AWS_REGION:$AWS_ACCOUNT_ID:reclaim-tripit-sync \
--protocol email \
--notification-endpoint your-email@example.com \
--region $AWS_REGIONImportant: Check your inbox and confirm the subscription before testing.
The command override bypasses the Dockerfile's built-in cron entrypoint, running a single sync and exiting:
aws ecs register-task-definition \
--region $AWS_REGION \
--family reclaim-tripit-sync \
--requires-compatibilities FARGATE \
--network-mode awsvpc \
--cpu 256 \
--memory 512 \
--execution-role-arn arn:aws:iam::$AWS_ACCOUNT_ID:role/reclaim-tripit-sync-execution-role \
--task-role-arn arn:aws:iam::$AWS_ACCOUNT_ID:role/reclaim-tripit-sync-task-role \
--container-definitions '[
{
"name": "reclaim-tripit-sync",
"image": "'$AWS_ACCOUNT_ID'.dkr.ecr.'$AWS_REGION'.amazonaws.com/reclaim-tripit-sync:latest",
"command": ["node", "sync.mjs", "sync"],
"essential": true,
"environment": [
{
"name": "SNS_TOPIC_ARN",
"value": "arn:aws:sns:'$AWS_REGION':'$AWS_ACCOUNT_ID':reclaim-tripit-sync"
}
],
"secrets": [
{
"name": "TRIPIT_ICAL_URL",
"valueFrom": "arn:aws:ssm:'$AWS_REGION':'$AWS_ACCOUNT_ID':parameter/reclaim-tripit-sync/TRIPIT_ICAL_URL"
},
{
"name": "RECLAIM_API_TOKEN",
"valueFrom": "arn:aws:ssm:'$AWS_REGION':'$AWS_ACCOUNT_ID':parameter/reclaim-tripit-sync/RECLAIM_API_TOKEN"
},
{
"name": "GOOGLE_CLIENT_ID",
"valueFrom": "arn:aws:ssm:'$AWS_REGION':'$AWS_ACCOUNT_ID':parameter/reclaim-tripit-sync/GOOGLE_CLIENT_ID"
},
{
"name": "GOOGLE_CLIENT_SECRET",
"valueFrom": "arn:aws:ssm:'$AWS_REGION':'$AWS_ACCOUNT_ID':parameter/reclaim-tripit-sync/GOOGLE_CLIENT_SECRET"
},
{
"name": "GOOGLE_REFRESH_TOKEN",
"valueFrom": "arn:aws:ssm:'$AWS_REGION':'$AWS_ACCOUNT_ID':parameter/reclaim-tripit-sync/GOOGLE_REFRESH_TOKEN"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/reclaim-tripit-sync",
"awslogs-region": "'$AWS_REGION'",
"awslogs-stream-prefix": "ecs"
}
}
}
]'Identify your default VPC, a subnet, and the default security group:
VPC_ID=$(aws ec2 describe-vpcs --filters Name=isDefault,Values=true \
--query 'Vpcs[0].VpcId' --output text --region $AWS_REGION)
SUBNET_ID=$(aws ec2 describe-subnets --filters Name=vpc-id,Values=$VPC_ID \
--query 'Subnets[0].SubnetId' --output text --region $AWS_REGION)
SG_ID=$(aws ec2 describe-security-groups --filters Name=vpc-id,Values=$VPC_ID Name=group-name,Values=default \
--query 'SecurityGroups[0].GroupId' --output text --region $AWS_REGION)
echo "VPC=$VPC_ID SUBNET=$SUBNET_ID SG=$SG_ID"aws events put-rule \
--name reclaim-tripit-sync-daily \
--schedule-expression 'cron(0 3 * * ? *)' \
--state ENABLED \
--region $AWS_REGION
aws events put-targets \
--rule reclaim-tripit-sync-daily \
--region $AWS_REGION \
--targets '[{
"Id": "reclaim-tripit-sync-target",
"Arn": "arn:aws:ecs:'$AWS_REGION':'$AWS_ACCOUNT_ID':cluster/reclaim-tripit-sync",
"RoleArn": "arn:aws:iam::'$AWS_ACCOUNT_ID':role/reclaim-tripit-sync-eventbridge-role",
"EcsParameters": {
"TaskDefinitionArn": "arn:aws:ecs:'$AWS_REGION':'$AWS_ACCOUNT_ID':task-definition/reclaim-tripit-sync",
"TaskCount": 1,
"LaunchType": "FARGATE",
"PlatformVersion": "LATEST",
"NetworkConfiguration": {
"awsvpcConfiguration": {
"Subnets": ["'$SUBNET_ID'"],
"SecurityGroups": ["'$SG_ID'"],
"AssignPublicIp": "ENABLED"
}
}
}
}]'Run a one-off task to verify everything works:
aws ecs run-task \
--cluster reclaim-tripit-sync \
--task-definition reclaim-tripit-sync \
--launch-type FARGATE \
--network-configuration '{
"awsvpcConfiguration": {
"subnets": ["'$SUBNET_ID'"],
"securityGroups": ["'$SG_ID'"],
"assignPublicIp": "ENABLED"
}
}' \
--region $AWS_REGIONCheck the logs:
aws logs tail /ecs/reclaim-tripit-sync --follow --region $AWS_REGIONYou should see output like:
=== TripIt → Reclaim Travel Timezone Sync ===
Mode: sync
Fetching TripIt iCal feed...
Found 77 VEVENT(s)
Identified 4 trip-level event(s)
Found 15 flight arrival(s)
Building timezone segments...
Built 22 segment(s)
4 future segment(s) > 1 day
2 after deduplication
...
Sync complete!
- Fargate: ~$0.01/month (256 CPU / 512 MiB running for ~30 seconds daily)
- SSM Parameter Store: Free (standard parameters)
- CloudWatch Logs: Negligible with 30-day retention
- ECR: Free tier covers 500 MB/month
- SNS: Free tier covers 1,000 email notifications/month
When you update the application code, rebuild and push the new image:
docker build -t reclaim-tripit-sync .
docker tag reclaim-tripit-sync:latest \
$AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/reclaim-tripit-sync:latest
docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/reclaim-tripit-sync:latestNo ECS changes are needed — the task definition references the :latest tag, so ECS will pull the new image on the next scheduled run.
Create an alarm that notifies you when a scheduled sync fails:
# Create an SNS topic for notifications
aws sns create-topic --name reclaim-tripit-sync-failures --region $AWS_REGION
aws sns subscribe \
--topic-arn arn:aws:sns:$AWS_REGION:$AWS_ACCOUNT_ID:reclaim-tripit-sync-failures \
--protocol email \
--notification-endpoint 'YOUR_EMAIL@example.com' \
--region $AWS_REGION
# Create a CloudWatch alarm on the ECS "task failed" metric
aws cloudwatch put-metric-alarm \
--alarm-name reclaim-tripit-sync-task-failure \
--namespace AWS/ECS \
--metric-name TaskFailure \
--dimensions Name=ClusterName,Value=reclaim-tripit-sync \
--statistic Sum \
--period 86400 \
--threshold 1 \
--comparison-operator GreaterThanOrEqualToThreshold \
--evaluation-periods 1 \
--alarm-actions arn:aws:sns:$AWS_REGION:$AWS_ACCOUNT_ID:reclaim-tripit-sync-failures \
--treat-missing-data notBreaching \
--region $AWS_REGIONConfirm the subscription by clicking the link in the email you receive.
Your AWS user/role needs the following permissions to deploy:
ecr:*— create repository, push imagesecs:*— create cluster, register task definitions, run tasksiam:*— create roles and policiesssm:PutParameter— store secretslogs:*— create log groupsevents:*— create scheduled rulesec2:Describe*— look up VPC/subnet/security group info
Create a scoped IAM policy containing only the permissions listed above and attach it to your deployment user/role. Avoid using AdministratorAccess — even temporarily — as it grants far more access than needed and risks accidental changes to unrelated resources.
To remove all AWS resources:
# Delete EventBridge rule and target
aws events remove-targets --rule reclaim-tripit-sync-daily --ids reclaim-tripit-sync-target --region $AWS_REGION
aws events delete-rule --name reclaim-tripit-sync-daily --region $AWS_REGION
# Deregister all task definition revisions
# List all revisions, then deregister each one:
for arn in $(aws ecs list-task-definitions --family-prefix reclaim-tripit-sync --query 'taskDefinitionArns[]' --output text --region $AWS_REGION); do
aws ecs deregister-task-definition --task-definition "$arn" --region $AWS_REGION
done
# Delete ECS cluster
aws ecs delete-cluster --cluster reclaim-tripit-sync --region $AWS_REGION
# Delete ECR repository
aws ecr delete-repository --repository-name reclaim-tripit-sync --force --region $AWS_REGION
# Delete SSM parameters
aws ssm delete-parameter --name /reclaim-tripit-sync/TRIPIT_ICAL_URL --region $AWS_REGION
aws ssm delete-parameter --name /reclaim-tripit-sync/RECLAIM_API_TOKEN --region $AWS_REGION
# Delete CloudWatch log group
aws logs delete-log-group --log-group-name /ecs/reclaim-tripit-sync --region $AWS_REGION
# Delete SNS topic and subscriptions (if configured)
TOPIC_ARN=arn:aws:sns:$AWS_REGION:$AWS_ACCOUNT_ID:reclaim-tripit-sync
aws sns list-subscriptions-by-topic --topic-arn $TOPIC_ARN --region $AWS_REGION \
--query 'Subscriptions[].SubscriptionArn' --output text | \
xargs -n1 aws sns unsubscribe --subscription-arn
aws sns delete-topic --topic-arn $TOPIC_ARN --region $AWS_REGION
# Delete IAM roles and policies
aws iam delete-role-policy --role-name reclaim-tripit-sync-task-role --policy-name SNSPublishAccess 2>/dev/null || true
aws iam detach-role-policy --role-name reclaim-tripit-sync-execution-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
aws iam delete-role-policy --role-name reclaim-tripit-sync-execution-role --policy-name SSMParameterAccess
aws iam delete-role --role-name reclaim-tripit-sync-execution-role
aws iam delete-role --role-name reclaim-tripit-sync-task-role
aws iam delete-role-policy --role-name reclaim-tripit-sync-eventbridge-role --policy-name ECSRunTask
aws iam delete-role --role-name reclaim-tripit-sync-eventbridge-role