1. Introduction

Nowadays, AWS is one of the most common cloud providers. Thousands of applications have to interact with its services as part of their workflows. To simplify this task, AWS has released several official SDKs (Software Development Kits) for a number of languages, including, but not limited to Java, Python, JavaScript, Kotlin, and PHP.

In this tutorial, we’re going to explore AWScala, a Scala wrapper around the AWS Java SDK allowing Scala developers to interact with AWS services writing idiomatic Scala code. We’ll compare creating an S3 bucket implemented using the official Java SDK with creating it using AWScala.

2. What Is the Need for an SDK?

SDK libraries such as AWScala are great to write infrastructural code to provision AWS resources.

Writing code to create resources on a cloud provider is a practice known as Infrastructure as Code. In modern DevOps, it’s a very well-known and widely adopted practice, as it allows us to apply many software engineering principles to the code that creates and maintain our infrastructure. As a matter of fact, there are many libraries and frameworks to write infrastructural code. Another common solution is Terraform.

The main advantage of using an SDK (such as AWScala) is that we can pick our favorite programming language (provided that the SDK is available in that language), rather than having to use Terraform’s domain-specific language.

3. Using the Java AWS SDK in Scala

At the time of writing, AWS hasn’t released any official SDK for Scala. Thus, before AWScala, Scala developers had to resort to using the Java SDK. The latter heavily relies on the builder pattern. Consequently, the Scala code we can write using the AWS Java SDK does its job, but is not very idiomatic.

To understand the need for a Scala wrapper, let’s start by importing the AWS S3 Java SDK into our project:

libraryDependencies += "software.amazon.awssdk" % "s3" % "2.19.31"

Then, let’s define a function that simply creates an S3 bucket, returning true if it was created and false in case of error:

def createBucketWithJavaSDK(name: String): Boolean = {
  val s3 = S3Client
    .builder()
    .region(Region.US_WEST_1)
    .build()

  Try {
    s3.createBucket { req: CreateBucketRequest.Builder => req.bucket(name) }

    s3.headBucket(
      HeadBucketRequest.builder().bucket(name).build()
    )
  }.fold(_ => false, _ => true)
}

In the example above, we first built an S3Client. In order to do so, we had to use the builder pattern, specifying various information (only the region in our case). If we don’t specify any credentials, the client follows the default resolution order.

Secondly, we invoked the S3Client::createBucket() method. The latter comes with two overloaded variations, one taking a builder as a parameter, as before, and another accepting a Java Consumer. The Consumer way is a bit less verbose. Nonetheless, we still have to specify the type of the req parameter, which is a bit annoying.

Thirdly, we verified that a bucket existed by calling the S3Client::headBucket() method. In this case, we used the builder syntax, to emphasize how verbose it is, with respect to the Consumer one.

Furthermore, the SDK throws exceptions in case of errors. Hence, we had to wrap the SDK calls into a Try, and then transform the result into a Boolean. Again, idiomatic Scala code should handle errors with types, limiting the use of exceptions as much as possible.

4. AWScala

AWScala wraps the Java objects defined by the AWS Java SDK into Scala classes, to make the interaction with them more idiomatic. In this section, we’ll rewrite the example above using AWScala instead of the Java SDK.

At the time of writing, AWSScala only supports a handful of AWS services, including, but not limited to, AWS S3, IAM, and EC2.

First, let’s import AWScala into our project:

libraryDependencies += "com.github.seratch" %% "awscala" % "0.9.2"

Let’s start by writing a snippet of code creating an S3 bucket:

def createBucketWithAWScala(name: String): Boolean = {
  val s3 = S3.at(awscala.Region.NorthernCalifornia)
  Try {
    val bucket = s3.createBucket(name)
    s3.buckets contains bucket
  }.fold(_ => false, _ => true)
}

The code is visibly less verbose than before. First, AWScala lets us instantiate an S3 client without explicitly using the builder pattern. In the example above, we used the convenient S3::at() method, letting us specify only the target AWS region. When we use the S3::at() method, AWS credentials will be fetched from environment variables or from our AWS configuration files. The credentials resolution order is the same one used by the AWS Java SDK.

AWScala comes with a variety of apply methods allowing us to explicitly set the credentials, or customize other aspects of the S3 client.

Once we have an S3 client, we call S3::createBucket() to create a bucket. This is considerably simpler than how we’d do it using the AWS Java SDK. S3::createBucket() returns an instance of Bucket.

Lastly, checking for the existence of a bucket is as simple as listing all of our buckets, and looking for the one we just created. We can do so by calling S3::buckets.

Unfortunately, AWScala does nothing to handle errors with types. Hence, we are, once again, forced to wrap our code into Try to catch possible exceptions and turn them into Booleans.

5. Conclusion

In this article, we saw how to use AWScala to interact with AWS services. First, we picked a running example, creating an S3 bucket. Secondly, we saw how to do it using the AWS Java SDK, and noted how cumbersome the syntax was. Lastly, we rewrote the same example, in a much more idiomatic way, using AWScala.

As usual, the source code for the examples is available over on GitHub.