Skip to content

Instantly share code, notes, and snippets.

@mfts
Last active April 4, 2024 13:38
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mfts/ea23b2b9e42d571aa77cf003862b93c3 to your computer and use it in GitHub Desktop.
Save mfts/ea23b2b9e42d571aa77cf003862b93c3 to your computer and use it in GitHub Desktop.
Setup Heroku Review App with Cloudflare DNS
{
"environments": {
"review": {
"scripts": {
"postdeploy": "bundle exec rake heroku:review_app_setup",
"pr-predestroy": "bundle exec rake heroku:review_app_predestroy"
}
}
}
}
namespace :heroku do
desc 'Setup Cloudflare DNS record for Heroku Review App'
task :review_app_setup do
require 'cloudflare'
require 'platform-api'
require 'octokit'
custom_domain = 'example.com'.freeze
# Heroku app names are default "<name>-pr-<pull request ID>"
heroku_app_name = ENV['HEROKU_APP_NAME']
email = ENV['CLOUDFLARE_EMAIL']
key = ENV['CLOUDFLARE_KEY']
pr_number = ENV['HEROKU_PR_NUMBER']
subdomain = "app-pr-#{pr_number}"
wild_subdomain = ["*", subdomain].join('.')
# Configure custom domain in Heroku
heroku_client = PlatformAPI.connect_oauth ENV['HEROKU_PLATFORM_TOKEN']
hostname = [subdomain, custom_domain].join('.')
heroku_client.domain.create(heroku_app_name, hostname: hostname, sni_endpoint: nil)
heroku_domain = heroku_client.domain.info(heroku_app_name, hostname)["cname"]
# Also add wildcard (*.) subdomain
wild_hostname = [wild_subdomain, custom_domain].join('.')
heroku_client.domain.create(heroku_app_name, hostname: wild_hostname, sni_endpoint: nil)
wild_heroku_domain = heroku_client.domain.info(heroku_app_name, wild_hostname)["cname"]
Cloudflare.connect(key: key, email: email) do |connection|
# Get a specific zone:
zone = connection.zones.find_by_id(ENV['CLOUDFLARE_ZONE_ID'])
# Search existing records
dns_record = zone.dns_records.find_by_name(subdomain)
wild_dns_record = zone.dns_records.find_by_name(wild_subdomain)
# Add a DNS record. Here we add an A record for `batman.example.com`:
if dns_record == nil
# Create DNS record in Cloudflare
zone.dns_records.create('CNAME', subdomain, heroku_domain, proxied: false)
end
if wild_dns_record == nil
zone.dns_records.create('CNAME', wild_subdomain, wild_heroku_domain, proxied: false)
end
end
# Add PR comment with deployment URL
github_client = Octokit::Client.new(:access_token => ENV['GITHUB_TOKEN'])
github_client.add_comment(ENV['GITHUB_REPO'], pr_number.to_i, "The review app has been deployed here: <a href='http://#{hostname}' target='_blank'>#{hostname}</a>")
end
desc 'Remove DNS Record from Cloudflare for Heroku Review App upon deletion'
task :review_app_predestroy do
require 'cloudflare'
# Cleanup subdomain DNS record for Heroku review app
custom_domain = 'example.com'.freeze
heroku_app_name = ENV['HEROKU_APP_NAME']
email = ENV['CLOUDFLARE_EMAIL']
key = ENV['CLOUDFLARE_KEY']
pr_number = ENV['HEROKU_PR_NUMBER']
subdomain = "app-pr-#{pr_number}"
wild_subdomain = ["*", subdomain].join('.')
Cloudflare.connect(key: key, email: email) do |connection|
# Get a specific zone:
zone = connection.zones.find_by_id(ENV['CLOUDFLARE_ZONE_ID'])
dns_record = zone.dns_records.find_by_name([subdomain, custom_domain].join('.'))
wild_dns_record = zone.dns_records.find_by_name([wild_subdomain, custom_domain].join('.'))
dns_record.delete if dns_record != nil
wild_dns_record.delete if wild_dns_record != nil
end
end
end
@mfts
Copy link
Author

mfts commented Jul 5, 2021

Thanks @gfu-clutter for the excellent starting point: https://medium.com/clutter-engineering/heroku-review-apps-with-custom-domains-8edfc0a2b153

I'm building this into a public GitHub action/workflow or even a simple online service for anyone to use --> https://twitter.com/mfts0/status/1412164682996228096?s=20

@starverdiyev
Copy link

@mfts Hello Mark! Is github action workflow available? I need to deploy custom domain with heroku-cloudflare in review app, and find your solution.

@starverdiyev
Copy link

starverdiyev commented Jul 29, 2022

@mfts hello) Do you find how to fix this bug? How I can set by default SNI param?

irb(main):011:1* begin
irb(main):012:1*   heroku_client.domain.create(heroku_app_name, hostname: "example.com")
irb(main):013:1* rescue Excon::Error::UnprocessableEntity => e
irb(main):014:1*   JSON.parse(e.response.data[:body])
irb(main):015:0> end
=> {"id"=>"invalid_params", "message"=>"Require params: sni_endpoint."}

@mfts
Copy link
Author

mfts commented Jul 29, 2022

@starverdiyev Hey Samir, did you add the wildcard domain to Heroku with a wildcard SSL certificate?

@starverdiyev
Copy link

@mfts No, didn't. I first must add on heroku: heroku domains:add *.example.com? Domain must be the same, like the Env custom_domain, right? But for which app I must add this domain? I will use it in review apps.

Thank you!

@starverdiyev
Copy link

@mfts I think this is way not working: https://devcenter.heroku.com/changelog-items/1938

I added:

heroku_client.domain.create(heroku_app_name, hostname: "example.com", sni_endpoint: "null"),

but get error: (Excon::Error::NotFound).

@mfts
Copy link
Author

mfts commented Jul 29, 2022

@starverdiyev I don't remember setting the sni_endpoint at all. And the last I used it was July 2021, so long after the changelog date.

@starverdiyev
Copy link

@mfts from Heroku: Beginning November 1, 2021, this new parameter will be required.

If your last using was on July 2021, this parameter was not released

@mfts
Copy link
Author

mfts commented Jul 29, 2022

@starverdiyev did you try adding the sni_endpoint without “”, just null

@starverdiyev
Copy link

starverdiyev commented Jul 29, 2022

@mfts with null yes, it's not working. But now i changed it to nil, and it begin works))

heroku_client.domain.create(heroku_app_name, hostname: "example.com", "sni_endpoint": nil)

In monday I will check dns records on cloudflare) Thank you!

@mfts
Copy link
Author

mfts commented Jul 29, 2022

@starverdiyev that’s good to hear. Keep me posted.

@starverdiyev
Copy link

starverdiyev commented Aug 2, 2022

@mfts I finished script. It's working, thanks. I just added ssl to domain: heroku_client.app.enable_acm(app_name), and removed wildcard, because heroku ssl didn't work with wildcard.

@mfts
Copy link
Author

mfts commented Aug 2, 2022

@starverdiyev happy you got it working. I will revisit this again and try to update it to comply with the new Heroku changes

@starverdiyev
Copy link

@mfts your script works after adding sni param. I just added acme, because we needed https. But without https, your script in working mode:)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment