Modernising Registrar Technology: Implementing EPP with Kotlin, Spring & Azure Container Apps
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
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.
Modifying the build settings
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
}
The structure
Rename the main class to EPPAPI.kt (Spring auto generation did not do it justice).
Split the code into two folders: epp and api, with our main class remaining at the root.
Create a class inside the epp folder named EPP.kt – this is where we will connect to and manage the EPPClient soon.
Create a class inside the api folder named API.kt – this is where we will configure and run the Spring API.
api
└── API.kt
epp
└── EPP.kt
PORT=700
USERNAME=X
PASSWORD=X
The code
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())
}
}
}
}
EPP.create()
}
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.
api
└── API.kt
epp
├── contact
├── domain
│ └── CheckDomain.kt
├── host
└── EPP.kt
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 an epp_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 the responseData.
Finally, we return whether the domain is available or not from the first (and only result) by checking the avail value.
EPP.create()
println(EPP.checkDomain(“example.gg”))
}
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
}
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
}
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
}
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”)
}
}
Domain created
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
├── 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.
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 HTTP GETrequests to the domain-check route. When a GET 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 the name parameter from the query string of the URL. For example, a request to /domain-check?=name=example.gg would set name to example.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 returns true if available and false if not).
return ResponseEntity.ok(mapOf(“available” to check)): This creates a response with HTTP status 200 (OK) and a body containing the JSON object with a single key available whose value is the result of the domain check.
import org.springframework.boot.runApplication
@SpringBootApplication
class API {
companion object {
fun start() {
runApplication<API>()
}
}
}
import epp.EPP
fun main() {
EPP.create()
API.start()
}
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 Projectsepp-apibuildclasseskotlinmain started by [Redacted] in D:IntelliJ Projectsepp-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)
/domain-check?name=test.gg – {“available”:false}
/domain-check?name=thisshouldprobablybeavailable.gg – {“available”:true}
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.
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 named epp-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 port 8080 of the container to port 8080 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 through localhost: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 mine registrar and chosen the location as uksouth because that is closest to me.
az acr create –resource-group registrar –name registrarcontainers —sku Basic – create an Azure Container Registry resource within our registrar resource group, with the name of registrarcontainers (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!
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 name containers and location uksouth.
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 the containers environment. It uses the Docker image stored in the ACR. The application inside the container is configured to listen on port 8080 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.
Setting up GitHub CI/CD
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 named origin 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 named origin, specifically to the master 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 or main)
Then, under Registry settings:
Ensure Azure Container Registry is selected for Repository 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:
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_
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!
Microsoft Tech Community – Latest Blogs –Read More