require 'colorize'
namespace :terra_boi do
desc """ Generate terraform (AWS) infrastructure code for your rails application. Creates infrastructure code for RDS DBs, ALBs, ECRs, ECS clusters, services, and tasks, and various security groups and other resources for each deployment environment (e.g. staging and prod) Usage without ENVS arg (creates staging and prod envs by default): rake \"terra_boi:generate_infra\" Usage with ENVS arg: rake \"terra_boi:generate_infra[ENV1 ENV2 ENVn]\" e.g. rake \"terra_boi:generate_infra[dev staging prod]\" Takes one optional arg: arg 1: envs (default value: staging prod) """ task :generate_infra, [:envs] => [:environment] do |_, args| ENVS = get_envs(args) create_boilerplate_files apply_terraform_state apply_terraform_cert apply_ecr apply_data run_wheneverize push_container_to_ecr apply_web_app_and_worker puts_urls_for_alb puts_how_to_connect_domain_and_load_balancer puts_twitter_plug end desc """ Destroy terraform (AWS) infrastructure code for your rails application. Usage without ENVS arg (creates staging and prod envs by default): rake \"terra_boi:destroy_infra\" Usage with ENVS arg: rake \"terra_boi:destroy_infra[ENV1 ENV2 ENVn]\" e.g. rake \"terra_boi:destroy_infra[dev staging prod]\" Takes one optional arg: arg 1: envs (default value: staging prod) """ task :destroy_infra, [:envs] => [:environment] do |_, args| ENVS = get_envs(args) ENVS.each do |env| puts "\nDestroying application infrastructure for #{env}...\n".cyan.bold directories = [:head_worker, :web_app, :ecs_cluster, :data] directories.each do |dir_name| sh "cd terraform/#{env}/#{dir_name} && terraform destroy" end end sh "cd terraform/ecr && terraform destroy" sh "cd terraform/cert && terraform destroy" sh "cd terraform/state && terraform destroy" end
end
desc “”“ Deploy your rails application.
By default, deploys to staging and prod.
Usage without ENVS arg (deploys to staging and prod envs by default): rake "terra_boi:deploy"
Usage with ENVS arg: rake "terra_boi:deploy[ENV1 ENV2 ENVn]" e.g. rake "terra_boi:deploy[staging prod]"
Takes one arg: arg 1: envs (default value: staging prod) “”“ task :deploy, [:envs] => [:environment] do |task, args|
ENVS = get_envs(args) puts "\nDeploying rails application to #{ENVS.to_sentence} infrastructure\n".cyan.bold conditional_push_container_to_ecr ENVS.each do |env| ecs_tasks = [:web_app, :head_worker] ecs_tasks.each do |task_name| puts "\nDeploying #{env} #{task_name} task\n".cyan.bold sh "./terraform/lib/scripts/update_service_pull_from_ecr.sh #{env} #{task_name}" end end
end
# ——————————— # Helper methods # ———————————
def get_envs(args)
if args[:envs] args[:envs].split(' ') else [:staging, :prod] end
end
def run_wheneverize
sh "bundle exec wheneverize ."
end
def create_boilerplate_files
config = {} puts "\nTERRA_BOI | Generating boilerplate infrastructure as code with terra_boi for your rails project...\n".cyan.bold sleep 1 config[:ruby_version] = get_ruby_docker_base_image sleep 1 config[:domain_name] = get_domain_name sleep 1 puts "\nTERRA_BOI | Generating infrastructure code using the configuration you provided...\n".cyan.bold sleep 1 sh "rails g terra_boi:boilerplate -d #{config[:domain_name]} -r #{config[:ruby_version]}" sleep 1 puts "\nTERRA_BOI | Marking deployment scripts executable...\n".bold sh "chmod +x ./terraform/lib/scripts/*"
end
def apply_terraform_state
puts "\nTERRA_BOI | Creating terraform state...\n".cyan.bold sh "cd terraform/state && terraform init && terraform apply -input=false -auto-approve"
end
def apply_terraform_cert
puts "\nTERRA_BOI | Creating HTTPS certificate in AWS Certificate Manager...\n".cyan.bold sh "cd terraform/cert && terraform init" print_certificate_validation_instructions sleep 2 sh "cd terraform/cert && terraform apply -input=false -auto-approve" confirm_certificate_successfully_validated
end
def apply_data
ENVS.each do |env| puts "\nTERRA_BOI | Creating RDS DB instance and S3 bucket for #{env}...\n".cyan.bold sh "cd terraform/#{env}/data && terraform init && terraform apply -input=false -auto-approve" end
end
def apply_ecr
puts "\nTERRA_BOI | Creating AWS ECR (Elastic Container Registry) for your application's docker images...\n".cyan.bold sh "cd terraform/ecr && terraform init && terraform apply -input=false -auto-approve"
end
def push_container_to_ecr
puts "\nTERRA_BOI | Building application docker container then pushing to ECR...\n".cyan.bold sh "./terraform/lib/scripts/push_to_ecr.sh"
end
def apply_web_app_and_worker
directories = [:ecs_cluster, :web_app, :head_worker] ENVS.each do |env| puts "\nTERRA_BOI | Building web app and worker ECS infrastructure for #{env}...\n".cyan.bold directories.each do |dir_name| sh "cd terraform/#{env}/#{dir_name} && terraform init && terraform apply -input=false -auto-approve" end end
end
def puts_urls_for_alb
ENVS.each do |env| url = `cd terraform/#{env}/web_app && terraform output alb_dns` puts "\nTERRA_BOI | Public application load balancer URL for #{env}:".cyan.bold puts "#{env} alb_dns: #{url}" end
end
def puts_how_to_connect_domain_and_load_balancer
puts "\nTERRA_BOI | GIDDY UP! TERRA_BOI HAS FINISHED CREATING INFRASTRUCTURE FOR YOUR RAILS APPLICATION!".cyan.bold puts "\nTERRA_BOI | To connect your domain name to your application's new AWS infrastructure:".red puts "1) Go to your domain register (e.g. Namecheap)" puts "2) Add the alb_dns output value (i.e. the public URL for your load balancer) to your domain's DNS records. alb_dns public URL values are output above" puts """E.g. to redirect staging.YOUR_DOMAIN_NAME to the load balancer you just deployed, add the following record to your DNS records: TYPE: ALIAS HOST: staging VALUE: alb_dns output value (something like app-staging-725123955.us-east-2.elb.amazonaws.com)""" puts "\nNote: you can have multiple ALIAS records for different subdomains\n"
end
def puts_twitter_plug
puts "\nTERRA_BOI | Let me know what you think about terra_boi on Twitter @charlieinthe6!".cyan.bold
end
def conditional_push_container_to_ecr
print "TERRA_BOI | Question: ".red puts "Build and push container updates to ECR first (y/n)?" puts "Answer y if you haven't built and pushed most recent updates to ECR yet." puts "...and answer y if you aren't sure." print "==> ".red answer = STDIN.gets.downcase.gsub(/[^yn]/, "") push_container_to_ecr if answer == 'y'
end
# ——————————— # Helper^2 methods # ———————————
def print_certificate_validation_instructions
puts "\nTERRA_BOI | Certificate validation instructions:".red puts "1) Log into AWS Console and go to AWS Certificate Manager (default region is us-east-2)" puts "2) Expand issued certificate for your domain, and find CNAME record for domain validation" puts "3) Add CNAME record listed for your domain to your domain's DNS records (i.e. log into where you purchased your domain and add CNAME record to it)\n\n" puts "NOTE: the Host field for your CNAME record will be something like _123f2cc99f15298ff717ac26dd6993. The Value field for your CNAME record will be something like _bf123a01234a134123412341324.dasfjkhasd.acm-validation.aws.\n\n".bold
end
def confirm_certificate_successfully_validated
answer = '' until answer == 'y' print "TERRA_BOI | Question 3: ".red puts "Has your HTTPS / SSL certificate successfully validated in AWS Console - AWS Certificate Manager (y/n)?" print "==> ".red answer = STDIN.gets.downcase.gsub(/[^yn]/, "") if answer == 'y' next else print_certificate_validation_instructions sleep 5 end end
end
def get_ruby_docker_base_image
print "TERRA_BOI | Question 1: ".red puts "Do you want to use the default ruby Docker base image 2.7.1 (y/n)?" print "==> ".red answer = STDIN.gets.downcase.gsub(/[^yn]/, "") if answer == 'y' ruby_docker_base_image = "2.7.1" else ruby_docker_base_image = "" print "\nTERRA_BOI | Question 1 follow-up: ".red puts "Which ruby Docker base image would you like to use (https://hub.docker.com/_/ruby/)?" puts "Recommended answer: 2.7.1" until ruby_docker_base_image.length > 0 print "==> ".red ruby_docker_base_image = STDIN.gets.gsub(/[^0-9\.]/, "") end end puts "\nGreat! Using #{ruby_docker_base_image} as the base Docker image for your infrastructure.".bold return ruby_docker_base_image
end
def get_domain_name
print "\nTERRA_BOI | Question 2: ".red puts "What domain name will you be using for your project?" puts "E.g. example.com (do not include a subdomain or 'www')" domain_name = "" until domain_name.length > 2 print "==> ".red domain_name = STDIN.gets.chomp end puts "\nRad! Setting #{domain_name} as your domain_name.".bold return domain_name
end