Introduction
In the domain management industry, technological advancement has often been a slow and cautious process, lagging behind the rapid innovations seen in other tech sectors. This measured pace is understandable given the critical role domain infrastructure plays in the global internet ecosystem. However, as we stand on the cusp of a new era in web technology, it is becoming increasingly clear that modernization should be a priority. This blog post embarks on a journey to demystify one of the most critical yet often misunderstood components of the industry: the Extensible Provisioning Protocol (EPP).
Throughout this blog, we will dive deep into the intricacies of EPP, exploring its structure, commands and how it fits into the broader domain management system. We will walk through the process of building a robust EPP client using Kotlin and Spring Boot. Then, we will take our solutions to the next level by containerizing with Docker and deploying it to Azure Container Apps, showcasing how modern cloud technologies can improve the reliability and scalability of your domain management system. We will also set up a continuous integration and deployment (CI/CD) pipeline, ensuring that your EPP implementation remains up-to-date and easily maintainable.
By the end of this blog, you will be able to provision domains programatically via an endpoint, and have the code foundation ready to create dozens of other domain management commands (e.g. updating nameservers, updating contact info, renewing and transferring domains).
Who it is for
What you will need: EPP credentials
Understanding EPP
EPP is short for Extensible Provisioning Protocol. It is a protocol designed to streamline and standardise communication between domain name registries and registrars. Developed to replace older, less efficient protocols, EPP has become the industry standard for domain registration and management operations.
- Stateful connections: EPP maintains persistent connections between registrars and registries, reducing overhead and improving performance.
- Extensibility: As the name suggests, EPP is designed to be extensible. Registries can add custom extensions to support unique features or requirements.
- Standardization: EPP provides a uniform interface across different registries, simplifying integration for registrars and reducing development costs.
Kotlin
Spring
Azure Container Apps ('ACA')
The architecture
- Registrant (end user) requests to purchase a domain
- Website backend sends instruction to EPP API (what we are making in this blog)
- EPP API sends command to the EPP server provided by the registry
- Response provided by registry and received by registrant (end user) on website
Setting up the development environment
Prerequisites
For this blog, I will be using the following technologies:
- Visual Studio Code (VS Code) as the IDE (integrated development environment). I will be installing some extensions and changing some settings to make it work for our technology. Download at Download Visual Studio Code - Mac, Linux, Windows
- Docker CLI for containerization and local testing. Download at Get Started | Docker
- Azure CLI for deployment to Azure Container Registry & Azure Container Apps (you can use the portal if more comfortable). Download at How to install the Azure CLI | Microsoft Learn
- Git for version control and pushing to GitHub to setup CI/CD pipeline. Download at Git - Downloads (git-scm.com)
VS Code Extensions
Extensions
and install the following:- Kotlin
- Spring Initialzr Java Support
Implementing EPP with Kotlin & Spring
Creating the project
First up, let us create a blank Spring project. We will do this with the Spring Initializr plugin we just installed:
- Press
CTRL + SHIFT + P
to open the command palette - Select
Spring Initialzr: Create a Gradle project...
- Select version (I recommend
3.3.4
) - Select
Kotlin
as project language - Type Group Id (I am using
com.stephen
) - Type Artifact ID (I am using
eppapi
) - Select
jar
as packaging type - Select any Java version (The version choice is yours)
- Add
Spring Web
as a dependency - Choose a folder
- Open project
Your project should look like this:
We are using the Gradle build tool for this project. Gradle is a powerful, flexible build automation tool that supports multi-language development and offers convenient integration with both Kotlin & Spring. Gradle will handle our dependency management, allowing us to focus on our EPP implementation rather than build configuration intricacies.
Adding the EPP dependency
- It handles the low-level details of EPP communication, allowing us to focus on business logic.
- It is a Java-based implementation, which integrates seamlessly with our Kotlin and Spring setup.
- It supports all basic EPP commands out of the box, such as domain checks, registrations and transfers.
build.gradle
in the dependencies
section:implementation 'io.github.mschout:epp-rtk-java:0.9.11'
2.7.18
. This version is most compatible with the APIs we are using, and I have tried and tested it. To do this, in the plugins
block, change the dependency to this:id 'org.springframework.boot' version '2.7.18'
Modifying the build settings
build.gradle
to support the proper Java version. The version is entirely up to you, though I would personally recommend latest due to staying up to date with security patches. Copy/replace the following into the build.gradle
:java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
kotlin {
jvmToolchain(21)
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile) {
kotlinOptions {
jvmTarget = "21"
freeCompilerArgs = ["-Xjsr305=strict"]
}
}
tasks.named('test') {
enabled = false
}
Tasks > build > build
, or run this command in the terminal:.\gradlew clean build
BUILD SUCCESSFUL
.The structure
- Rename the main class to
EPPAPI.kt
(Spring auto generation did not do it justice). - Split the code into two folders:
epp
andapi
, with our main class remaining at the root. - Create a class inside the
epp
folder namedEPP.kt
- this is where we will connect to and manage theEPPClient
soon. - Create a class inside the
api
folder namedAPI.kt
- this is where we will configure and run the Spring API.
EPPAPI.kt
api
└── API.kt
epp
└── EPP.kt
.env
and populate with the following structure. I have prefilled with the host and port for the registry I am using to show the expected format:HOST=ote.channelisles.net
PORT=700
USERNAME=X
PASSWORD=X
The code
EPP
class. The goal with this class is to provide access to an EPPClient
which we can use to connect to the EPP server and authenticate with our details. The class will extend the EPPClient
provided by the EPP-RTK API and implement a singleton pattern through its companion object. The class uses the environment variables we set earlier for configuration. The create()
function serves as a factory method, handling the process of establishing a secure SSL connection, logging in and initializing the client. It employs Kotlin's apply
function for a concise and readable initialization block. The implementation also includes error handling and logging which will help us debug if anything goes wrong. The setupSSLContext()
function configures a trust-all certificate strategy, which, while not recommended for production, is useful in development or specific controlled environments. This design will allow for easy extension through Kotlin's extension functions on the companion object.import com.tucows.oxrs.epprtk.rtk.EPPClient
import java.net.Socket
import java.security.KeyStore
import java.security.cert.X509Certificate
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
import javax.net.ssl.X509TrustManager
class EPP private constructor(
host: String,
port: Int,
username: String,
password: String,
) : EPPClient(host, port, username, password) {
companion object {
private val HOST = System.getenv("HOST")
private val PORT = System.getenv("PORT").toInt()
private val USERNAME = System.getenv("USERNAME")
private val PASSWORD = System.getenv("PASSWORD")
lateinit var client: EPP
fun create(): EPP {
println("Creating client with HOST: $HOST, PORT: $PORT, USERNAME: $USERNAME")
return EPP(HOST, PORT, USERNAME, PASSWORD).apply {
try {
println("Creating SSL socket...")
val socket = createSSLSocket()
println("SSL socket created. Setting socket to EPP server...")
setSocketToEPPServer(socket)
println("Socket set. Getting greeting...")
val greeting = greeting
println("Greeting received: $greeting")
println("Connecting...")
connect()
println("Connected. Logging in...")
login(PASSWORD)
println("Login successful.")
client = this
} catch (e: Exception) {
println("Error during client creation: ${e.message}")
e.printStackTrace()
throw e
}
}
}
private fun createSSLSocket(): Socket {
val sslContext = setupSSLContext()
return sslContext.socketFactory.createSocket(HOST, PORT) as Socket
}
private fun setupSSLContext(): SSLContext {
val trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {
override fun getAcceptedIssuers(): Array<X509Certificate>? = null
override fun checkClientTrusted(certs: Array<X509Certificate>, authType: String) {}
override fun checkServerTrusted(certs: Array<X509Certificate>, authType: String) {}
})
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()).apply {
load(null, null)
}
val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()).apply {
init(keyStore, "".toCharArray())
}
return SSLContext.getInstance("TLS").apply {
init(kmf.keyManagers, trustAllCerts, java.security.SecureRandom())
}
}
}
}
API.kt
class shortly. The main class should now look like this:fun main() {
EPP.create()
}
domains
, contacts
and hosts
.- Domains: These are the web addresses that users type into their browsers. In EPP, a domain object represents the registration of a domain name.
- Contacts: These are individuals or entities associated with a domain. There are typically four types of contact: Registrant, Admin, Tech & Billing. ICANN (Internet Corporation for Assigned Names and Numbers) mandates that every provisioned domain must have a valid contact attached to it.
- Hosts: Also known as nameservers, these are the servers that translate domain names into IP addresses. In EPP, host objects can either be internal (subordinate to a domain in the registry) or external.
epp
folder, named domain
, contact
and host
. And the first EPP command we will make is the simplest: a domain check. Because this relates to domain objects, create a class inside the domain
folder named CheckDomain.kt
. Your project structure should now look like this:EPPAPI.kt
api
└── API.kt
epp
├── contact
├── domain
│ └── CheckDomain.kt
├── host
└── EPP.kt
CheckDomain.kt
class called checkDomain
which can be used on our EPP class. Here's the code:import com.tucows.oxrs.epprtk.rtk.example.DomainUtils.checkDomains
import epp.EPP
import com.tucows.oxrs.epprtk.rtk.xml.EPPDomainCheck
import org.openrtk.idl.epprtk.domain.epp_DomainCheckReq
import org.openrtk.idl.epprtk.domain.epp_DomainCheckRsp
import org.openrtk.idl.epprtk.epp_Command
fun EPP.Companion.checkDomain(
domainName: String,
): Boolean {
val check = EPPDomainCheck().apply {
setRequestData(
epp_DomainCheckReq(
epp_Command(),
arrayOf(domainName)
)
)
}
val response = processAction(check) as EPPDomainCheck
val domainCheck = response.responseData as epp_DomainCheckRsp
return domainCheck.results[0].avail
}
- We create an
EPPDomainCheck
object, which represents an EPP domain check command. - We set the request data using
epp_DomainCheckReq
. This takes anepp_command
(a generic EPP command) and an array of domain names to check. In this case, we are only checking one domain. - We process the action using our EPP client's
processAction
function, which sends the request to the EPP server. - We cast the response to
EPPDomainCheck
and extract theresponseData
. - Finally, we return whether the domain is available or not from the first (and only result) by checking the
avail
value.
example.gg
, returns the following:org.openrtk.idl.epprtk.domain.epp_DomainCheckRsp: { m_rsp [org.openrtk.idl.epprtk.epp_Response: { m_results [[org.openrtk.idl.epprtk.epp_Result: { m_code [1000] m_values [null] m_ext_values [null] m_msg [Command completed successfully] m_lang [] }]] m_message_queue [org.openrtk.idl.epprtk.epp_MessageQueue: { m_count [4] m_queue_date [null] m_msg [null] m_id [916211] }] m_extension_strings [null] m_trans_id [org.openrtk.idl.epprtk.epp_TransID: { m_client_trid [null] m_server_trid [1728106430577] }] }] m_results [[org.openrtk.idl.epprtk.epp_CheckResult: { m_value [example.gg] m_avail [false] m_reason [(00) The domain exists] m_lang [] }]] }
EPP
class, we can call it super easily. Let us add it to our main class as a test:fun main() {
EPP.create()
println(EPP.checkDomain("example.gg"))
}
true
or false
, in this case false
. This pattern of creating extension functions for various EPP operations allows us to build a clean, intuitive API for interacting with the EPP server, while keeping our core EPP
class focused on connection and authentication.CreateContact.kt
class under my /epp/contact
folder. Here is is how it looks:import com.tucows.oxrs.epprtk.rtk.xml.EPPContactCreate
import epp.EPP
import org.openrtk.idl.epprtk.contact.*
import org.openrtk.idl.epprtk.epp_AuthInfo
import org.openrtk.idl.epprtk.epp_AuthInfoType
import org.openrtk.idl.epprtk.epp_Command
fun EPP.Companion.createContact(
contactId: String,
name: String,
organization: String? = null,
street: String,
street2: String? = null,
street3: String? = null,
city: String,
state: String? = null,
zip: String? = null,
country: String,
phone: String,
fax: String? = null,
email: String
): Boolean {
val create = EPPContactCreate().apply {
setRequestData(
epp_ContactCreateReq(
epp_Command(),
contactId,
arrayOf(
epp_ContactNameAddress(
epp_ContactPostalInfoType.INT,
name,
organization,
epp_ContactAddress(street, street2, street3, city, state, zip, country)
)
),
phone.let { epp_ContactPhone(null, it) },
fax?.let { epp_ContactPhone(null, it) },
email,
epp_AuthInfo(epp_AuthInfoType.PW, null, "pass")
)
)
}
val response = client.processAction(create) as EPPContactCreate
val contactCreate = response.responseData as epp_ContactCreateRsp
return contactCreate.rsp.results[0].m_code.toInt() == 1000
}
EPPContactCreate
class which we populate from the data we took in from the constructor. Some of that data is optional, and I have given default null values to all that are optional according to the EPP specification. I am then checking for the m_code
which is, for all intents and purposes, a code that indicates the result of the operation. According to the EPP specification, a result code of 1000
indicates a successful operation.CreateHost.kt
class in my /epp/host
folder with the following code:import com.tucows.oxrs.epprtk.rtk.xml.EPPHostCreate
import epp.EPP
import org.openrtk.idl.epprtk.epp_Command
import org.openrtk.idl.epprtk.host.epp_HostAddress
import org.openrtk.idl.epprtk.host.epp_HostAddressType
import org.openrtk.idl.epprtk.host.epp_HostCreateReq
import org.openrtk.idl.epprtk.host.epp_HostCreateRsp
fun EPP.Companion.createHost(
hostName: String,
ipAddresses: Array<String>?
): Boolean {
val create = EPPHostCreate().apply {
setRequestData(
epp_HostCreateReq(
epp_Command(),
hostName,
ipAddresses?.map { epp_HostAddress(epp_HostAddressType.IPV4, it) }?.toTypedArray()
)
)
}
val response = client.processAction(create) as EPPHostCreate
val hostCreate = response.responseData as epp_HostCreateRsp
return hostCreate.rsp.results[0].code.toInt() == 1000
}
true
if the code is 1000
, and false
otherwise. The parameters are particularly important here and can lead to confusion for those not too familiar with how DNS works. The hostName
parameter is the fully qualified domain name (FQDN) of the host we are creating. For example, ns1.example.com
. The other ask is an array of IP addresses associated with the host. This is more crucial for internal nameservers, and for external nameservers (probably your use case) this can often be left null
.CreateDomain.kt
in my /epp/domain
folder:import epp.EPP
import com.tucows.oxrs.epprtk.rtk.xml.EPPDomainCreate
import org.openrtk.idl.epprtk.domain.*
import org.openrtk.idl.epprtk.epp_AuthInfo
import org.openrtk.idl.epprtk.epp_AuthInfoType
import org.openrtk.idl.epprtk.epp_Command
fun EPP.Companion.createDomain(
domainName: String,
registrantId: String,
adminContactId: String,
techContactId: String,
billingContactId: String,
nameservers: Array<String>,
password: String,
period: Short = 1
): Boolean {
val create = EPPDomainCreate().apply {
setRequestData(
epp_DomainCreateReq(
epp_Command(),
domainName,
epp_DomainPeriod(epp_DomainPeriodUnitType.YEAR, period),
nameservers,
registrantId,
arrayOf(
epp_DomainContact(epp_DomainContactType.ADMIN, adminContactId),
epp_DomainContact(epp_DomainContactType.TECH, techContactId),
epp_DomainContact(epp_DomainContactType.BILLING, billingContactId)
),
epp_AuthInfo(epp_AuthInfoType.PW, null, password)
)
)
}
val response = client.processAction(create) as EPPDomainCreate
val domainCreate = response.responseData as epp_DomainCreateRsp
return domainCreate.rsp.results[0].code.toInt() == 1000
}
createDomain
function encapsulates the EPP command for provisioning a new domain. The function brings together all the pieces we have prepared: contacts, hosts and domain-specific information. As before, it constructs an EPP domain create request, associating the domain with its contacts and nameservers. It then processes this request and checks the result code to determine if the request was successful. By returning a Boolean, we can easily pass the response upstream and, if connected to a user interface such as a web application, can inform the end user. import epp.EPP
import epp.contact.createContact
import epp.domain.createDomain
fun main() {
EPP.create()
val contactResponse = EPP.createContact(
contactId = "12345",
name = "Stephen",
organization = "Test",
street = "Test Street",
street2 = "Test Street 2",
street3 = "Test Street 3",
city = "Test City",
state = "Test State",
zip = "Test Zip",
country = "GB",
phone = "1234567890",
fax = "1234567890",
email = "test@gg.com"
)
if (contactResponse) {
println("Contact created")
} else {
println("Contact creation failed")
return
}
val domainResponse = EPP.createDomain(
domainName = "randomavailabletestdomain.gg",
registrantId = "123",
adminContactId = "123",
techContactId = "123",
billingContactId = "123",
nameservers = arrayOf("ernest.ns.cloudflare.com", "adaline.ns.cloudflare.com"),
password = "XYZXYZ",
period = 1
)
if (domainResponse) {
println("Domain created")
} else {
println("Domain creation failed")
}
}
createContact
extension function. I have passed through every single parameter, required or optional, to show how it would look. Then, once confirming the contact has created, I am creating a domain with our createDomain
extension function. I am giving it the required parameters, such domain name and the nameservers, but also providing the ID of the contact created just above in the four contact fields. It is required that the contact ID which is provided is a valid contact that has first been created in the system. Therefore, this merger of a couple functions that we have made should provision a domain.Contact created
Domain created
org.openrtk.idl.epprtk.contact.epp_ContactCreateRsp: { m_rsp [org.openrtk.idl.epprtk.epp_Response: { m_results [[org.openrtk.idl.epprtk.epp_Result: { m_code [1000] m_values [null] m_ext_values [null] m_msg [Command completed successfully] m_lang [] }]] m_message_queue [org.openrtk.idl.epprtk.epp_MessageQueue: { m_count [4] m_queue_date [null] m_msg [null] m_id [916211] }] m_extension_strings [null] m_trans_id [org.openrtk.idl.epprtk.epp_TransID: { m_client_trid [null] m_server_trid [1728110331411] }] }] m_id [123456] m_creation_date [2024-10-05T06:38:51.408Z] }
org.openrtk.idl.epprtk.domain.epp_DomainCreateRsp: { m_rsp [org.openrtk.idl.epprtk.epp_Response: { m_results [[org.openrtk.idl.epprtk.epp_Result: { m_code [1000] m_values [null] m_ext_values [null] m_msg [Command completed successfully] m_lang [] }]] m_message_queue [org.openrtk.idl.epprtk.epp_MessageQueue: { m_count [4] m_queue_date [null] m_msg [null] m_id [916211] }] m_extension_strings [null] m_trans_id [org.openrtk.idl.epprtk.epp_TransID: { m_client_trid [null] m_server_trid [1728110331467] }] }] m_name [randomavailabletestdomain2.gg] m_creation_date [2024-10-05T06:38:51.464Z] m_expiration_date [2025-10-05T06:38:51.493Z] }
Both of those objects were created using our extension functions on top of the EPP-RTK which is in contact with my target EPP server. If your registry has a user interface, you should see that these objects have now been created and are usable going forward. For example, one contact can be used for multiple domains. For my case study, you can see that both objects were successfully created on the Channel Isles side through our EPP communication:
- Domain check
- Domain info
- Domain create
- Domain update
- Domain delete
- Domain transfer
- Contact check
- Contact info
- Contact create
- Contact update
- Contact delete
- Contact transfer
- Host check
- Host info
- Host create
- Host update
- Host delete
API.kt
by putting them in their own controller
folder. I am going to name my controllers HostController.kt
, ContactController.kt
and DomainController.kt
. At this point, the file structure should look like this:EPPAPI.kt
api
├── controller
│ └── ContactController.kt
│ └── DomainController.kt
│ └── HostController.kt
└── API.kt
epp
├── contact
├── domain
│ └── CheckDomain.kt
├── host
└── EPP.kt
The job of controllers in Spring is to handle incoming HTTP requests, process them and return appropriate responses. In the context of our EPP API, controllers will act as the bridge between the client interface and our EPP functionality. Therefore, it makes logical sense to split up the three major sections into multiple classes so that the code does not become unmaintainable.
CheckDomain.kt
class. Now let us make it so that a user can trigger it via an endpoint. Because it is domain related, I will add the new code into the DomainController.kt
class. @RestController
. And then a mapping is created as below:import epp.EPP
import epp.domain.checkDomain
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
@RestController
class DomainController {
@GetMapping("/domain-check")
fun helloWorld(@RequestParam name: String): ResponseEntity<Map<String, Any>> {
val check = EPP.checkDomain(name)
return ResponseEntity.ok(
mapOf(
"available" to check
)
)
}
}
GetMapping("domain-check")
: This annotation maps the HTTPGET
requests to thedomain-check
route. When aGET
request is made to this URL, Spring will call this function to handle it.fun helloWorld(@RequestParam name: String)
: This is the function that will handle the request. The@RequestParam
annotation tells Spring to extract thename
parameter from the query string of the URL. For example, a request to/domain-check?=name=example.gg
would setname
toexample.gg
. This allows us to then process the EPP command with their requested domain name.ResponseEntity<Map<String, Any>>
: This is the return type of the function.ResponseEntity
allows us to have full control over the HTTP response, including status code, headers and body.val check = EPP.checkDomain(name)
: This line calls our EPP function to check if the domain is available (remember, it returnstrue
if available andfalse
if not).return ResponseEntity.ok(mapOf("available" to check))
: This creates a response with HTTP status200 (OK)
and a body containing the JSON object with a single keyavailable
whose value is the result of the domain check.
GET
request to /domain-check
with a domain name as a parameter, Spring routes that request to this method, which then uses our EPP implementation to check the domain's availability and returns the result. This setup allows external applications to easily check domain availability by making a simple HTTP GET request, without needing to know anything about the underlying EPP protocol or implementation. It is a great example of how we are using Spring to create a user-friendly API on top of our more complex EPP operations.POST
request, updating domain information could use PUT
, and deleting a domain would naturally fit with the DELETE
HTTP method. For domain creation, we could use @PostMapping("/domain")
and accept a request body with all necessary information. Domain updates could use @PutMapping("/domain/{domainName}")
, where the domain name is part of the path and the updated information is in the request body. For domain deletion, @DeleteMapping("/domain/{domainName}")
would be appropriate. Similar patterns can be applied to contact and host operations. By mapping our EPP commands to these standard HTTP methods, we create an intuitive API that follows RESTful conventions. Each of these endpoints would call the corresponding EPP function we have already implemented, process the result, and return an appropriate HTTP response. This approach provides a clean separation between the HTTP interface and the underlying EPP operations, making our system more modular and easier to maintain or extend in the future.API.kt
class, I am going to put the following:import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class API {
companion object {
fun start() {
runApplication<API>()
}
}
}
API.kt
class serves as the entry point for the Spring application. Inside this class, we have defined a companion object with a start()
function. This function calls runApplication<API>()
to bootstrap the application, which is a Kotlin-specific way to launch a Spring application. Behind the scenes, Spring's recognition of controllers happens automatically through a process called component scanning. When the application starts, because we have registered it here, Spring examines the codebase, starting from the package containing the main class and searching through all subpackages. It looks for classes annotated with specific markers, such as the @RestController
that we put at the top of our controllers. Spring then inspects these classes, looking for any functions that may be annotated as mappings (e.g. @GetMapping
like above), and then uses that information to build a map of URL paths to controller functions. This means that when a request comes in, Spring knows exactly which function in which class should process the result. It would be fair to say that Spring has an unconventional approach to application structure and dependency management. Spring embraces the philosophy of "convention over configuration" and heavily leverages annotations. However, this has helped us to significantly reduce boilerplate code, making it cleaner and more maintainable for future travelers.start()
function we just created in our APP.kt
:import api.API
import epp.EPP
fun main() {
EPP.create()
API.start()
}
Creating client with HOST: ote.channelisles.net, PORT: 700, USERNAME: [Redacted]
Creating SSL socket...
SSL socket created. Setting socket to EPP server...
Socket set. Getting greeting...
Greeting received: org.openrtk.idl.epprtk.epp_Greeting: { m_server_id [OTE] m_server_date [2024-10-06T05:47:08.628Z] m_svc_menu [org.openrtk.idl.epprtk.epp_ServiceMenu: { m_versions [[1.0]] m_langs [[en]] m_services [[urn:ietf:params:xml:ns:contact-1.0, urn:ietf:params:xml:ns:domain-1.0, urn:ietf:params:xml:ns:host-1.0]] m_extensions [[urn:ietf:params:xml:ns:rgp-1.0, urn:ietf:params:xml:ns:auxcontact-0.1, urn:ietf:params:xml:ns:secDNS-1.1, urn:ietf:params:xml:ns:epp:fee-1.0]] }] m_dcp [org.openrtk.idl.epprtk.epp_DataCollectionPolicy: { m_access [all] m_statements [[org.openrtk.idl.epprtk.epp_dcpStatement: { m_purposes [[admin, prov]] m_recipients [[org.openrtk.idl.epprtk.epp_dcpRecipient: { m_type [ours] m_rec_desc [null] }, org.openrtk.idl.epprtk.epp_dcpRecipient: { m_type [public] m_rec_desc [null] }]] m_retention [stated] }]] m_expiry [null] }] }
Connecting...
Connected. Logging in...
Login successful.
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.7.18)
2024-10-06 06:47:09.531 INFO 43872 --- [ main] com.stephen.eppapi.EPPAPIKt : Starting EPPAPIKt using Java 1.8.0_382 on STEPHEN with PID 43872 (D:\IntelliJ Projects\epp-api\build\classes\kotlin\main started by [Redacted] in D:\IntelliJ Projects\epp-api)
2024-10-06 06:47:09.534 INFO 43872 --- [ main] com.stephen.eppapi.EPPAPIKt : No active profile set, falling back to 1 default profile: "default"
2024-10-06 06:47:10.403 INFO 43872 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2024-10-06 06:47:10.414 INFO 43872 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2024-10-06 06:47:10.414 INFO 43872 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.83]
2024-10-06 06:47:10.511 INFO 43872 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2024-10-06 06:47:10.511 INFO 43872 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 928 ms
2024-10-06 06:47:11.220 INFO 43872 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2024-10-06 06:47:11.229 INFO 43872 --- [ main] com.stephen.eppapi.EPPAPIKt : Started EPPAPIKt in 2.087 seconds (JVM running for 3.574)
EPPClient
. Then the native Spring output which shows that the local server has been started on port 8080
.localhost:8080
in the browser should resolve, but throw a fallback error page, because we have not set anything to show at that route. We have, however, created a GET
route at /domain-check
. If you head to just /domain-check
you will be met with a 400 (BAD REQUEST)
error. This is because you will need to specify the name
parameter as enforced in our function. So, let us try this out with a couple domains.../domain-check?name=test.gg
-{"available":false}
/domain-check?name=thisshouldprobablybeavailable.gg
-{"available":true}
POST
request to /domain
, taking contact details, nameservers, and other required information in the request body. Domain information retrieval could be a GET
request to /domain/{domainName}
, fetching comprehensive information about a specific domain. Updates to domain information, such as changing contacts or nameservers, could be managed through a PUT
request to /domain/{domainName}
. The domain deletion process could be initiated with a DELETE
request to /domain/{domainName}
. Domain transfer operations, including initiating, approving, or rejecting transfers, could also be incorporated into our API. Each of these operations would follow the same pattern we have established: a Spring controller method that takes in the necessary parameters, calls the appropriate EPP function, and returns the result in a user-friendly format.Deploying to Azure Container Apps
Now that we have our EPP API functioning locally, it is time to think about productionizing our application. Our goal is to run the API as an Azure Container App (ACA), which is a fully managed environment perfect for easy deployment and scaling of our Spring application. However, before deploying to ACA, we will need to containerise our application. This is where Azure Container Registry (ACR) comes into play. ACR will serve as the private Docker registry to store and manage our container images. It provides a centralised repository for our Docker images and integrates seamlessly with ACA, streamlining our CI/CD pipeline.
Dockerfile
. This step is required to run both locally and in Azure Container Registry. A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image. It serves as a blueprint for building a Docker container. In our case, our Dockerfile will set up the environment and instructions needed to containerise our Spring application. Dockerfile
in the root of your project with the following content:# Use OpenJDK 21 as the base image (use your JDK version)
FROM openjdk:21-jdk-alpine
# Set the working directory in the container
WORKDIR /app
# Copy the JAR file into the container
COPY build/libs/*.jar app.jar
# Expose the port your application runs on
EXPOSE 8080
# Command to run the application
CMD ["java", "-jar", "app.jar"]
./gradlew build
- build our application and package into a JAR file found under/build/libs/X.jar
.docker build -t epp-api .
- tells Docker to create an image namedepp-api
based on the instructions in our Dockerfile.docker run -p 8080:8080 --env-file .env epp-api
- start a container from the image, mapping port8080
of the container to port8080
on the host machine. We use this port because this is the default port on which Spring exposes endpoints. The-p
flag ensures that the application can be accessed throughlocalhost:8080
on your machine. We also specify the.env
file we created earlier so that Docker is aware of our EPP login details.
az login
- if not already authenticated, be sure to log in through the CLI.az group create --name registrar --location uksouth
- create a resource group if you have not already. I have named mineregistrar
and chosen the location asuksouth
because that is closest to me.az acr create --resource-group registrar --name registrarcontainers --sku Basic
- create an Azure Container Registry resource within ourregistrar
resource group, with the name ofregistrarcontainers
(note that this has to be globally unique) and SKU Basic.az acr login --name registrarcontainers
- login to the Azure Container Registry.docker tag epp-api myacr.azurecr.io/epp-api:v1
- tag the local Docker image with the ACR login server name.docker push myacr.azurecr.io/epp-api:v1
- push the image to the container registry!
The push refers to repository [registrarcontainers.azurecr.io/epp-api]
2111bc7193f6: Pushed
1b04c1ea1955: Pushed
ceaf9e1ebef5: Pushed
9b9b7f3d56a0: Pushed
f1b5933fe4b5: Pushed
v1: digest: sha256:07eba5b555f78502121691b10cd09365be927eff7b2e9db1eb75c072d4bd75d6 size: 1365
az containerapp env create --resource-group registrar --name containers --location uksouth
- create the Container App environment within our resource group with namecontainers
and locationuksouth
.az acr update -n registrarcontainers --admin-enabled true
- ensure ACR allows admin access.az containerapp create --name epp-api --resource-group registrar --environment containers --image registrarcontainers.azurecr.io/epp-api:v1 --target-port 8080 --ingress external --registry-server registrarcontainers.azurecr.io --env-vars "HOST=your_host" "PORT=your_port" "USERNAME=your_username" "PASSWORD=your_password"
- creates a new Container App named
epp-api
within our resource group and thecontainers
environment. It uses the Docker image stored in the ACR. The application inside the container is configured to listen on port8080
which is where our Spring endpoints will be accessible. The-ingress external
flag makes it accessible from the internet. You must also set your environment variables or the app will crash.
Container app created. Access your app at https://epp-api.purpledune-772f2e5a.uksouth.azurecontainerapps.io/
/domain-check?name=test.gg
as we did when locally testing, we are met with: {"available":false}
Setting up GitHub CI/CD
epp-api
. Be sure to copy/paste or remember the URL for this repository as we will need it to link Git in a moment. git init
- Initialise a new Git repository in your current directory. This creates a hidden.git
directory that stores the repository's metadata.git add .
- Stages all of the files in the current directory and its subdirectories for commit. This means that these files will be included in the next commit.git commit -m "Initial commit"
- Creates a new commit with the staged files and a common initial commit message.git remote add origin <URL>
- Adds a remote repository namedorigin
to your local repository, connecting it to our remote Git repository hosted on GitHub.git push origin master
- Uploads the local repository's content to the remote repository namedorigin
, specifically to themaster
branch.
- Head to your Container App
- On the sidebar, hit
Settings
- Hit
Deployment
You should find yourself in the Continuous deployment
section. There are two headings, let us start with GitHub settings
:
- Authenticate into GitHub and provide permissions to repository (if published to a GH organization, give permissions also)
- Select organization, or your GitHub name if published on personal account
- Select the repository you just created (for me,
epp-api
) - Select the main branch (likely either
master
ormain
)
Then, under Registry settings
:
- Ensure
Azure Container Registry
is selected forRepository source
- Select the Container Registry you created earlier (for me,
registrarcontainers
) - Select the image you created earlier (for me,
epp-api
)
It should look something like this:
Start continuous deployment
..github/workflows
with the commit message Create an auto-deploy file
. Based on the content of the workflow, we can see that the trigger is on push
to master
. This means that, moving forward, every change you commit and push to this repository will trigger this workflow, which will in-turn trigger a build and push the new container image to the registry.Checkout to the branch
and before the Azure Login
job:- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Set up JDK 21
uses: actions/setup-java@v2
with:
java-version: '21'
distribution: 'adopt'
- name: Build with Gradle
run: ./gradlew build
Grant execute permission to gradlew
- gradlew is a wrapper script that helps manage Gradle installations. This step grants execute permission to the gradlew file which allows this build process to execute Gradle commands, needed for the next steps.Set up JDK
- This sets up the JDK as the Java envrionment for the build process. Make sure this matches the Java version you have chosen to use for this tutorial.Build with Gradle
- This executes the Gradle build process which will compile our Java code and package it into a JAR file which will then be used by the last job to push to the Container Registry.
The final workflow file should look like this:
name: Trigger auto deployment
# When this action will be executed
on:
# Automatically trigger it when detected changes in repo
push:
branches:
[ master ]
paths:
- '**'
- '.github/workflows/AutoDeployTrigger-aec369b2-f21b-47f6-8915-0d087617a092.yml'
# Allow manual trigger
workflow_dispatch:
jobs:
build-and-deploy:
runs-on: ubuntu-latest
permissions:
id-token: write #This is required for requesting the OIDC JWT Token
contents: read #Required when GH token is used to authenticate with private repo
steps:
- name: Checkout to the branch
uses: actions/checkout@v2
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Set up JDK 21
uses: actions/setup-java@v2
with:
java-version: '21'
distribution: 'adopt'
- name: Build with Gradle
run: ./gradlew build
- name: Azure Login
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Build and push container image to registry
uses: azure/container-apps-deploy-action@v2
with:
appSourcePath: ${{ github.workspace }}
_dockerfilePathKey_: _dockerfilePath_
registryUrl: fdcontainers.azurecr.io
registryUsername: ${{ secrets.REGISTRY_USERNAME }}
registryPassword: ${{ secrets.REGISTRY_PASSWORD }}
containerAppName: epp-api
resourceGroup: registrar
imageToBuild: registrarcontainers.azurecr.io/fdspring:${{ github.sha }}
_buildArgumentsKey_: |
_buildArgumentsValues_
Actions
tab and see the result of all builds, and if any build fails you can explore in detail on which job the error occured.Conclusion
That is it! You have successfully built a robust EPP API using Kotlin and Spring Boot and now containerised it with Docker and deployed it to Azure Container Apps. This journey took us from understanding the intricacies of EPP and domain registration, through implementing core EPP operations, to creating a user-friendly RESTful API. We then containerised our application, ensuring consistency across different environments. Finally, we leveraged Azure's powerful cloud service services - Azure Container Registry for storing our Docker image, and Azure Container Apps for deploying and running our application in a scalable, managed environment. The result is a fully functional, cloud-hosted API that can handle domain checks, registrations and other EPP operations. This accomplishment not only showcases the technical implementation but also opens up possibilities for creating sophisticated domain management tools and services, such as by starting a public registrar or managing a domain portfolio internally.
I hope this blog was useful, and I am happy to answer any questions in the replies. Well done on bringing this complex system to life!