Deploying Ktor Web Service on AWS EC2 Instance
Tags: kotlin, ktor, aws, backend
Published: 2021-06-19 22:00:04

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m1fsps7i476wa4vkoxg1.png)

This article basically talks about how do we go about deploying a Ktor web service on an AWS EC2 instance running Ubuntu / any linux distro. 

At the time of writing this article, I am using Ktor version 1.5 to build the project and the EC2 instance we are going to be deploying on will be a T2 micro instance.

In order to learn more about creating a new Ktor web service you can follow this [series](https://himanshoe.com/kotlin-for-backend) it's extremely well written and at the time of writing this, it's still a WIP or there are tons of good articles along with the official docs that help you get started with Ktor on the Backend. 

I'll assume that your Ktor web app is up and running locally, and we will start with launching our instance and getting started with the deployment process.

Let's start by adding a security group to the instance if you don't already have kept it open for all IP address, this is done so that we are able to test our web service from the browser / mobile app.

Login to your AWS Dashboard -> EC2 -> Security Groups section and edit the inbound rules.

We will be adding two new rules here - 

First Rule 

* Type - `Custom TCP`
* Protocol - `TCP`
* Port Range - `8080` - we are using `8080` port number in this example this has to be the port number you are running our web service on.
* Source - `0.0.0.0/0`

Second Rule 

* Type - `Custom TCP`
* Protocol - `TCP`
* Port Range - `8080`
* Source - `::/0`

Now let's create a Jar file for our web service, we will be using the Gradle Shadow Plugin to create this, the reason for that is this plugin allows us to create a Fat Jar which is shadowed (obfuscated), a very good explanation for the same is [here](https://stackoverflow.com/a/59378158/1992823).

> If we don't create a fat jar, and start our deployment process when running it on the server it will throw an error - `no main manifest attribute, in projectname.jar` 

While you will see the Manifest file in the jar file if you extract it and check under the Manifest -> META-INF directory, you will notice that it doesn't have the `mainClass` attribute, that points to the Class in our project that has the main function, in our case it will be the ApplicationKt.class, we need to make some changes in our `build.gradle` file in order for it to be added in our Manifest and that we don't get the above error.

First, we need to make sure that in our `build.gradle.kts` file, we have the correct Class mentioned for `mainClassName` - 

```
application {
    mainClassName = "com.starter-project.ApplicationKt"
}
```

Next, we want Gradle to add the `mainClass` attribute in the Manifest file when building the jar file, for this we will create a task - 

```
tasks.withType<Jar> {
    manifest {
        attributes["Main-Class"] = application.mainClassName
    }
}

```

Now let's add the Gradle Shadow Plugin in our `build.gradle` file 

```
plugins {
    id("com.github.johnrengelman.shadow") version "6.1.0"
}
```

After this just run the Gradle build, this will create a Jar file in our project directory - `builds -> libs -> projectname-version-all.jar`

We are going to need to copy this file onto our web server, for this we will use the following command, make sure you to keep your `pem` file handy for this next step, since it's needed to access your instance via the terminal. 

```
scp -i "instance.pem" IdeaProjects/starter-project/build/libs/projectname-0.0.1.jar ubuntu@ec2-0.0.0.0.us-east-2.compute.amazonaws.com:/home/ubuntu
```

The above command is `Secure Copy` to securely transfer files from your machine to the server, further more you are giving it the directory of the jar file you want to transfer to your server along with the path on the server where you'd want to store it, you can also create a workspace directory on your server for storing this file, and give it that path instead. 

Now, let's SSH into our server for the next step -
```
ssh -i "instance.pem" ubuntu@ec2-0.0.0.0.us-east-2.compute.amazonaws.com
```

We need to check what's the version of Java installed on our server and make sure it aligns with the version we are running our web service on, we will install it if needed. 

For this we need to run the following command - 
`java -version`

This command will either return the version of Java installed on your server or an error that says `command not recognised` and a suggestion to install it, it will also include the commands that you can run to install the same, make sure you have the correct version installed, in our case it is `Java 1.8`

If all goes well, after installation you will see the below response on hitting the `java -version` command - 

```
openjdk version "1.8.0_292"
OpenJDK Runtime Environment (build 1.8.0_292-8u292-b10-0ubuntu1~20.04-b10)
OpenJDK 64-Bit Server VM (build 25.292-b10, mixed mode)
```

Next, let's run our web app on the server, make sure you're in the same directory where you had copied the jar file in the previous steps, run the below command to see if it's running on the port 8080.

Also make sure there are no other processes running on the port number we want our web service to run on - 

```
kill -9 $(lsof -t -i:8080)
```
This will kill all the processes that are running on the port number 8080. 

Now, let's run the jar file with the following command - 

```
java -jar projectname-0.0.1-all.jar
```

If all goes well, the above command should return the following response - 
```
2021-06-16 19:31:42.194 [main] INFO  Application - Autoreload is disabled because the development mode is off.
2021-06-16 19:31:42.711 [main] INFO  Application - Responding at http://0.0.0.0:8080
```

We can now hit our Public v4 DNS with the port number our web service is running on and see the response in the browser -

```
http://ec2-0-0-0-0.us-east-2.compute.amazonaws.com:8080/
```

But you will notice that as soon as we exit the shell, our web service stops working, in order for this to work even after existing the shell we need to create a service on our server instance that runs the web service even after exiting the shell or in case our server reboots.

Let's create a web service, by running the below command - 

```
sudo vim /etc/systemd/system/webapp.service
```

Press i -> to start inserting 
Paste the following the instructions to our service - 

```
[Unit]
 Description=Ktor REST Service
 [Service]
 User=ubuntu
 # Change this to your workspace 
WorkingDirectory=/home/ubuntu/
# Path to executable
ExecStart=usr/bin/java -jar /home/ubuntu/projectname-0.0.1-all.jar
SuccessExitStatus=143 
TimeoutStopSec=10
 Restart=on-failure
 RestartSec=5
 [Install]
 WantedBy=multi-user.target 
```

Press :wq to save the above script.

`ExecStart` is the instruction that tells our system to run the command mentioned in it, here we can also mention the path to our bash script which runs the web service instead, which is much easier to maintain. 

After this we need to run the following commands to tell our system to run the service - 

```
sudo systemctl daemon-reload 
sudo systemctl enable webapp.service
```
After enabling the service it will return a symlink to our webapp.service file 

```
sudo systemctl start webapp
 sudo systemctl status webapp
```

This should return the response that we had received earlier, when running the service - 

```
Autoreload is disabled because the development mode is off.
Application - Responding at http://0.0.0.0:8080
```

If you face any issues in this, make sure the paths mentioned in the `webapp.service` file under `ExecStart` instruction are valid, and accessible.

If all goes well you will be able to hit the Public V4 DNS URL / IP address / Domain with 8080 port number on the browser to see the response you were expecting. 

You can stop / restart the service by using the following commands - 

```
sudo systemctl stop webapp
sudo systemctl restart webapp
```

To get the service logs you can use the following commands, in case you want to further debug if there's an error - 

```
sudo journalctl -f -n 1000 -u webapp
```

I'm still very new to this, hence if I have failed to mention a way of doing this that was quite obvious, please ping me on [Twitter](https://twitter.com/hsm59) so that I can improve. Thanks!

Thanks to @trianton, who's article helped me completely understand the deployment process.

Know more about me from the links below - 
[Github](https://github.com/hsm59)
[Stack Overflow](https://stackoverflow.com/users/1992823/hsm59)