{"id":114,"date":"2024-09-10T06:00:00","date_gmt":"2024-09-10T06:00:00","guid":{"rendered":"https:\/\/imcodinggenius.com\/?p=114"},"modified":"2024-09-10T06:00:00","modified_gmt":"2024-09-10T06:00:00","slug":"spring-boot-with-aws-s3-s3client-and-kotlin","status":"publish","type":"post","link":"https:\/\/imcodinggenius.com\/?p=114","title":{"rendered":"Spring Boot with AWS S3, S3Client and Kotlin"},"content":{"rendered":"<p>Hello and welcome to the first article in a series dedicated to integrating a Spring Boot Kotlin app with <strong>AWS S3 Object Storage<\/strong>, in which I will show you how to <strong>properly set up the connection<\/strong> and <strong>make use of the S3Client<\/strong>. <\/p>\n<p>What can you expect today? <\/p>\n<p>Well, at the end of this tutorial, you will know precisely how to connect to the S3 service, resolve the most common issues related to that, as well as how to perform basic operations on <strong>buckets <\/strong>and <strong>objects <\/strong>using the <strong>S3Client approach<\/strong>. <\/p>\n<p>In the future content, we will work a bit with an asynchronous client, learn when to use S3Template instead, and learn how to write tests properly, so do not hesitate to subscribe to my <a href=\"https:\/\/codersee.com\/newsletter\/\">newsletter<\/a> <\/p>\n<h2 class=\"wp-block-heading\">Project Setup <\/h2>\n<p>If you already have your Spring Boot project prepared, then feel free to skip this step. <\/p>\n<p>But if that is not the case, then let\u2019s navigate together to the <a href=\"https:\/\/start.spring.io\/\" target=\"_blank\" rel=\"noopener\">Spring Initializr<\/a> page and select the following settings: <\/p>\n<div class=\"wp-block-image\">\n<\/div>\n<p>As we can see, nothing related to the AWS S3 yet, but we imported the Spring Web dependency so that we could expose REST endpoints to test. <\/p>\n<p>As always, please generate the project, extract it on your local, and import it to your favorite IDE. <\/p>\n<h2 class=\"wp-block-heading\">AWS S3 Dependencies<\/h2>\n<p>It is worth mentioning that the <strong>S3Client<\/strong> is not a Spring Boot concept, but a class that comes from the AWS SDK. <\/p>\n<p><strong>However<\/strong>, to make our lives easier when working with Spring, we can make use of the Spring Cloud AWS, which simplifies using AWS-managed services in a Spring and Spring Boot applications.<\/p>\n<p>To do so, let\u2019s navigate to the build.gradle.kts and add the following: <\/p>\n<p>implementation(&#171;io.awspring.cloud:spring-cloud-aws-starter-s3:3.1.1&#187;)<\/p>\n<p>Why do I pick it over the AWS Java SDK? <\/p>\n<p>Because it auto-configures various S3 integration-related components out of the box. Additionally, we can quickly configure a bunch of things using the application.yaml. And lastly, we are all Spring boyzz here, we don\u2019t do things manually <\/p>\n<h2 class=\"wp-block-heading\">Create Test S3 Bucket<\/h2>\n<p>Following, let\u2019s navigate to the <a href=\"https:\/\/console.aws.amazon.com\/\" target=\"_blank\" rel=\"noopener\">AWS Console<\/a> to prepare a test bucket. <\/p>\n<p>Again, feel free to skip it if you are looking for Spring Boot details.<\/p>\n<p>Then, let\u2019s find the S3 (aka \u201cSimple Storage Service\u201d) in the search bar: <\/p>\n<div class=\"wp-block-image\">\n<\/div>\n<p>Nextly, let\u2019s hit the Create Bucket , provide a name for it, and leave the rest as is: <\/p>\n<div class=\"wp-block-image\">\n<\/div>\n<p>If everything succeeded, then we should see our bucket on the list: <\/p>\n<p>Excellent, at this point we can get back to our Spring Boot project :). <\/p>\n<h2 class=\"wp-block-heading\">Test S3Client Connection<\/h2>\n<p>With all of that done, let\u2019s figure out whether we can connect our local app with AWS using the <em>S3Client<\/em>. <\/p>\n<p>To do so, let\u2019s create the BucketController class and introduce the GET endpoint: <\/p>\n<p>@RestController<br \/>\n@RequestMapping(&#171;\/buckets&#187;)<br \/>\nclass BucketController(<br \/>\n  private val s3Client: S3Client<br \/>\n) {<\/p>\n<p>  @GetMapping<br \/>\n  fun listBuckets(): List&lt;String&gt; {<br \/>\n    val response = s3Client.listBuckets()<\/p>\n<p>    return response.buckets()<br \/>\n      .mapIndexed { index, bucket -&gt;<br \/>\n        &#171;Bucket #${index + 1}: ${bucket.name()}&#187;<br \/>\n      }<br \/>\n  }<br \/>\n}<\/p>\n<p>As we can see, the above logic will be responsible for exposing the GET \/buckets endpoint and returning a list of bucket names as \u201cBucket #N: some-name\u201d. <\/p>\n<p>Moreover, the starter we are using <strong>automatically configures and registers an S3Client bean in the Spring Boot context<\/strong>. So, we simply inject that without any previous configuration. <\/p>\n<p>Anyway, less talkie-talkie, and let\u2019s test the endpoint: <\/p>\n<p>curl &#8212;location &#8212;request GET &#8216;http:\/\/localhost:8080\/test&#8217; <\/p>\n<p>And depending on our local environment, we <strong>get the 200 OK with a bucket name: <\/strong><\/p>\n<p>[<br \/>\n    &#171;Bucket #1: your-awesome-name&#187;<br \/>\n]<\/p>\n<p>Or the <strong>error related to <em>AwsCredentialsProviderChain<\/em>:<\/strong> <\/p>\n<p>software.amazon.awssdk.core.exception.SdkClientException: Unable to load credentials from any of the providers in the chain AwsCredentialsProviderChain(credentialsProviders=[SystemPropertyCredentialsProvider(), EnvironmentVariableCredentialsProvider(), WebIdentityTokenCredentialsProvider(), ProfileCredentialsProvider(profileName=default, profileFile=ProfileFile(sections=[])), ContainerCredentialsProvider(), InstanceProfileCredentialsProvider()]) : [SystemPropertyCredentialsProvider(): Unable to load credentials from system settings. Access key must be specified either via environment variable (AWS_ACCESS_KEY_ID) or system property (aws.accessKeyId)., EnvironmentVariableCredentialsProvider(): Unable to load credentials from system settings. Access key must be specified either via environment variable (AWS_ACCESS_KEY_ID) or system property (aws.accessKeyId)., WebIdentityTokenCredentialsProvider(): Either the environment variable AWS_WEB_IDENTITY_TOKEN_FILE or the javaproperty aws.webIdentityTokenFile must be set., ProfileCredentialsProvider(profileName=default, profileFile=ProfileFile(sections=[])): Profile file contained no credentials for profile \u2018default\u2019: ProfileFile(sections=[]), ContainerCredentialsProvider(): Cannot fetch credentials from container \u2013 neither AWS_CONTAINER_CREDENTIALS_FULL_URI or AWS_CONTAINER_CREDENTIALS_RELATIVE_URI environment variables are set., InstanceProfileCredentialsProvider(): Failed to load credentials from IMDS.]<\/p>\n<p>Or even: <\/p>\n<p>Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [software.amazon.awssdk.services.s3.S3ClientBuilder]: Factory method \u2018s3ClientBuilder\u2019 threw exception with message: Unable to load region from any of the providers in the chain software.amazon.awssdk.regions.providers.DefaultAwsRegionProviderChain@15f35bc3: [software.amazon.awssdk.regions.providers.SystemSettingsRegionProvider@2bfb583b: Unable to load region from system settings. Region must be specified either via environment variable (AWS_REGION) or system property (aws.region)., software.amazon.awssdk.regions.providers.AwsProfileRegionProvider@7301eebe: No region provided in profile: default, software.amazon.awssdk.regions.providers.InstanceProfileRegionProvider@76a805b7: Unable to contact EC2 metadata service.]<\/p>\n<p>So, let\u2019s learn why it worked (or not).<\/p>\n<h2 class=\"wp-block-heading\">Configuring AWS Credentials <\/h2>\n<p>Long story short, the Spring Cloud AWS starter configures the DefaultCredentialsProvider that looks for credentials in the following order:<\/p>\n<p>Java System Properties \u2013 aws.accessKeyId and aws.secretAccessKey<\/p>\n<p>Environment Variables \u2013 AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY<\/p>\n<p>Web Identity Token credentials from system properties or environment variables<\/p>\n<p>Credential profiles file at the default location (~\/.aws\/ credentials) shared by all AWS SDKs and the AWS CLI<\/p>\n<p>Credentials delivered through the Amazon EC2 container service if AWS_CONTAINER_CREDENTIALS_RELATIVE_URI environment variable is set and the security manager has permission to access the variable,<\/p>\n<p>Instance profile credentials delivered through the Amazon EC2 metadata service<\/p>\n<p>And <strong>if you got 200 OK<\/strong>, but you don\u2019t remember specifying anything on your local machine, then I am pretty sure that the <strong>number 4<\/strong>\u00a0 is the answer here  <\/p>\n<p>The .aws folder inside the home directory is a default location for credentials, and you could have populated that unconsciously, for example <strong>when configuring AWS CLI with aws configure.<\/strong> And the DefaultCredentialsProvider was smart enough to use it without your knowledge. <\/p>\n<p>The autoconfiguration is a wonderful thing, but as we can see, it can backfire sometimes  <\/p>\n<p>On the other hand, if none of these 6 satisfies you, then <strong>Spring Cloud AWS allows us to use the access keys, too.<\/strong>  <\/p>\n<p>To do so, the only thing we need to do is add the following to the application.yaml: <\/p>\n<p>spring:<br \/>\n  cloud:<br \/>\n    aws:<br \/>\n      credentials:<br \/>\n        access-key: your-access-key<br \/>\n        secret-key: your-secret-key<\/p>\n<h2 class=\"wp-block-heading\">Configure Region <\/h2>\n<p>OK, so at this point the problem related to the credentials should be gone. However, if we got the second error related to the region, then let\u2019s see how it works internally, too. <\/p>\n<p>Well, when we take a look at the DefaultAwsRegionProviderChain docs, we will see that it looks for the region in this order:<\/p>\n<p>Check the aws.region system property for the region.<\/p>\n<p>Check the AWS_REGION environment variable for the region.<\/p>\n<p>Check the {user. home}\/.aws\/ credentials and {user. home}\/.aws\/config files for the region.<\/p>\n<p>If running in EC2, check the EC2 metadata service for the region.<\/p>\n<p>So, again, if we had the .aws credentials set, then this value came from there<\/p>\n<p>And as you might already have guessed, yes, we can update that in the properties, too:<\/p>\n<p>spring:<br \/>\n  cloud:<br \/>\n    aws:<br \/>\n      s3:<br \/>\n        region: us-east-1 #default is us-west-1<\/p>\n<p>And at this point, when we rerun our application, then everything should be working, as expected. <\/p>\n<h2 class=\"wp-block-heading\">AWS S3Client Operations<\/h2>\n<p>So, with all of that done, we can finally take a look at a few AWS S3Client capabilities. Of course, we will not cover all possible scenarios, so if you have a bit more specific use case, then I recommend checking the Spring Cloud AWS \/ AWS SDK documentation. <\/p>\n<h3 class=\"wp-block-heading\">List All Buckets <\/h3>\n<p>Firstly, let\u2019s get back to the listBuckets usage:<\/p>\n<p>@GetMapping<br \/>\nfun listBuckets(): List&lt;String&gt; {<br \/>\n  val response = s3Client.listBuckets()<\/p>\n<p>  return response.buckets()<br \/>\n    .mapIndexed { index, bucket -&gt;<br \/>\n      &#171;Bucket #${index + 1}: ${bucket.name()}&#187;<br \/>\n    }<br \/>\n}<\/p>\n<p>Long story short, this function returns ListBucketsResponse that contains a list of all buckets owned by us. It is worth mentioning that we must have the s3:ListAllMyBucket permission. <\/p>\n<p>Nevertheless, <strong>I wanted to emphasize one, important thing<\/strong>. <\/p>\n<p>AWS SDK methods throw exceptions quite heavily. For example, the above may result in SdkException, S3Exception, or SdkClientException to be thrown. In a production-ready code, we must keep that in mind, handle them according to our needs and (if necessary) translate to appropriate HTTP status codes.<\/p>\n<h3 class=\"wp-block-heading\">Create New S3 Bucket<\/h3>\n<p>Following, let\u2019s expose a POST \/buckets endpoint that will be used to create new buckets: <\/p>\n<p>@PostMapping<br \/>\nfun createBucket(@RequestBody request: BucketRequest) {<br \/>\n  val createBucketRequest = CreateBucketRequest.builder()<br \/>\n    .bucket(request.bucketName)<br \/>\n    .build()<\/p>\n<p>  s3Client.createBucket(createBucketRequest)<br \/>\n}<\/p>\n<p>data class BucketRequest(val bucketName: String)<\/p>\n<p>This time our function looks quite different- we must prepare the CreateBucketRequest that we pass to the createBucket function. <\/p>\n<p>And that is quite a common thing when dealing with AWS S3Client in Spring Boot. The SDK methods expect us to provide different objects of classes extending the S3Request, like CreateBucketRequest, DeleteObjectRequest, etc. <\/p>\n<p>What the createBucket does is pretty obvious, but we must be cautious about the bucket name, because the function may throw BucketAlreadyExistsException, or  BucketAlreadyOwnedByYouException. <\/p>\n<p>Of course, to test that, the only thing we need to do is to hit the endpoint: <\/p>\n<p>curl &#8212;location &#8212;request POST &#8216;http:\/\/localhost:8080\/buckets&#8217; <br \/>\n&#8212;header &#8216;Content-Type: application\/json&#8217; <br \/>\n&#8212;data-raw &#8216;{<br \/>\n    &#171;bucketName&#187;: &#171;your-awesome-name&#187;<br \/>\n}&#8217;<\/p>\n<p>And if everything is fine, a new bucket should be created. <\/p>\n<h3 class=\"wp-block-heading\">Create Object In The Bucket<\/h3>\n<p>So at this point, we know how to create buckets in AWS. Nevertheless, we use them to <strong>organise uploaded files<\/strong>. <\/p>\n<p>And it is the right time to learn how we can upload a file: <\/p>\n<p>\/\/ we must add the typealias to avoid name clash for the @RequestBody annotation \ud83d\ude42<br \/>\ntypealias PutObjectRequestBody = software.amazon.awssdk.core.sync.RequestBody<\/p>\n<p>@RestController<br \/>\n@RequestMapping(&#171;\/buckets&#187;)<br \/>\nclass BucketController(<br \/>\n  private val s3Client: S3Client<br \/>\n) {<\/p>\n<p>  @PostMapping(&#171;\/{bucketName}\/objects&#187;)<br \/>\n  fun createObject(@PathVariable bucketName: String, @RequestBody request: ObjectRequest) {<br \/>\n    val createObjectRequest = PutObjectRequest.builder()<br \/>\n      .bucket(bucketName)<br \/>\n      .key(request.objectName)<br \/>\n      .build()<\/p>\n<p>    val fileContent = PutObjectRequestBody.fromString(request.content)<\/p>\n<p>    s3Client.putObject(createObjectRequest, fileContent)<br \/>\n  }<\/p>\n<p>  data class ObjectRequest(val objectName: String, val content: String)<br \/>\n}<\/p>\n<p>As we can see, this time, we make use of the putObject and the PutObjectRequest (you see the pattern now  ). <\/p>\n<p>Moreover, when preparing the request we must specify both the bucket name and our object key. <\/p>\n<p>curl &#8212;location &#8212;request POST &#8216;http:\/\/localhost:8080\/buckets\/your-awesome-name\/objects&#8217; <br \/>\n&#8212;header &#8216;Content-Type: application\/json&#8217; <br \/>\n&#8212;data-raw &#8216;{<br \/>\n    &#171;objectName&#187;: &#171;file-example.txt&#187;,<br \/>\n    &#171;content&#187;: &#171;My file content&#187;<br \/>\n}&#8217;<\/p>\n<p>As a result, a new text file named \u201cfile-example\u201d with \u201cMy file content\u201d in it should be created in the \u201cyour-awesome-name\u201d bucket. <\/p>\n<p>Of course, this is not the only method of S3Client that allows us to upload files, and sometimes the multipart upload might be a better choice for our use case. <\/p>\n<h3 class=\"wp-block-heading\">List Objects From The Bucket<\/h3>\n<p>Nextly, let\u2019s take a look what is the content of our bucket:<\/p>\n<p>@GetMapping(&#171;\/{bucketName}\/objects&#187;)<br \/>\nfun listObjects(@PathVariable bucketName: String): List&lt;String&gt; {<br \/>\n  val listObjectsRequest = ListObjectsRequest.builder()<br \/>\n    .bucket(bucketName)<br \/>\n    .build()<\/p>\n<p>  val response = s3Client.listObjects(listObjectsRequest)<\/p>\n<p>  return response.contents()<br \/>\n    .map { s3Object -&gt; s3Object.key() }<br \/>\n}<\/p>\n<p>Similarly, we build the ListObjectsRequest instance, we perform the request using the listObjects and return an array with item names.<\/p>\n<p>And this time, when we check with the following query:<\/p>\n<p>curl &#8212;location &#8212;request GET &#8216;http:\/\/localhost:8080\/buckets\/your-awesome-name\/objects&#8217;<\/p>\n<p>We should get the 200 OK with the array: <\/p>\n<p>[<br \/>\n    &#171;file-example.txt&#187;<br \/>\n]<\/p>\n<h3 class=\"wp-block-heading\">Fetch The Object From S3 Bucket<\/h3>\n<p>And although listing objects might be sometimes useful, I am pretty sure you would be more often interested in getting the actual object with a key: <\/p>\n<p>@GetMapping(&#171;\/{bucketName}\/objects\/{objectName}&#187;)<br \/>\nfun getObject(@PathVariable bucketName: String, @PathVariable objectName: String): String {<br \/>\n  val getObjectRequest = GetObjectRequest.builder()<br \/>\n    .bucket(bucketName)<br \/>\n    .key(objectName)<br \/>\n    .build()<\/p>\n<p>  val response = s3Client.getObjectAsBytes(getObjectRequest)<\/p>\n<p>  return response.asString(UTF_8)<br \/>\n}<\/p>\n<p>Just like in the previous examples, we prepare the request with bucket name and object key, invoke the getObjectAsBytes and this time we print out the content to the output: <\/p>\n<p>curl &#8212;location &#8212;request GET &#8216;http:\/\/localhost:8080\/buckets\/your-awesome-name\/objects\/file-example.txt&#8217;<\/p>\n<p># Response:<br \/>\n&#171;My file content&#187;<\/p>\n<p>Of course, a friendly reminder that the AWS SDK throws the exceptions, and if the file does not exist, we will get the NoSuchKeyException. <\/p>\n<h3 class=\"wp-block-heading\">Delete S3 Bucket<\/h3>\n<p>As the last step, let\u2019s take a look at the logic necessary to delete a bucket: <\/p>\n<p>@DeleteMapping(&#171;\/{bucketName}&#187;)<br \/>\nfun deleteBucket(@PathVariable bucketName: String) {<br \/>\n  val listObjectsRequest = ListObjectsRequest.builder()<br \/>\n    .bucket(bucketName)<br \/>\n    .build()<\/p>\n<p>  val listObjectsResponse = s3Client.listObjects(listObjectsRequest)<\/p>\n<p>  val allObjectsIdentifiers = listObjectsResponse.contents()<br \/>\n    .map { s3Object -&gt;<br \/>\n      ObjectIdentifier.builder()<br \/>\n        .key(s3Object.key())<br \/>\n        .build()<br \/>\n    }<\/p>\n<p>  val del = Delete.builder()<br \/>\n    .objects(allObjectsIdentifiers)<br \/>\n    .build()<\/p>\n<p>  val deleteObjectsRequest = DeleteObjectsRequest.builder()<br \/>\n    .bucket(bucketName)<br \/>\n    .delete(del)<br \/>\n    .build()<\/p>\n<p>  s3Client.deleteObjects(deleteObjectsRequest)<\/p>\n<p>  val deleteBucketRequest = DeleteBucketRequest.builder()<br \/>\n    .bucket(bucketName)<br \/>\n    .build()<\/p>\n<p>  s3Client.deleteBucket(deleteBucketRequest)<br \/>\n}<\/p>\n<p>As we can see, this time, we must perform our actions in a few steps.<\/p>\n<p>Why? <\/p>\n<p>The reason is simple: <\/p>\n<p>Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: software.amazon.awssdk.services.s3.model.S3Exception: The bucket you tried to delete is not empty (Service: S3, Status Code: 409\u2026<\/p>\n<p>As we can see, the message above is pretty descriptive. Simply said- <strong>we cannot delete a bucket that is not empty.<\/strong> <\/p>\n<p>So, our function utilizes the listObjects, so that we can get keys to delete, the deleteObjects to actually delete them, and deleteBucket to get rid of the bucket. <\/p>\n<h2 class=\"wp-block-heading\">Summary <\/h2>\n<p>And that\u2019s all for this first tutorial on how to integrate your Spring Boot application with AWS and <strong>make use of the S3Client to work with AWS S3<\/strong>. In the upcoming articles in the series, we will expand our knowledge to work with S3 even more efficiently. <\/p>\n<p>I hope you enjoyed it and see you next time  <\/p>\n<p>For the source code, as always, please refer to <a href=\"https:\/\/github.com\/codersee-blog\/spring-boot-3-kotlin-s3Client\" target=\"_blank\" rel=\"noopener\">this GitHub repository<\/a>. <\/p>\n<p>The post <a href=\"https:\/\/codersee.com\/spring-boot-aws-s3-s3client-kotlin\/\">Spring Boot with AWS S3, S3Client and Kotlin<\/a> appeared first on <a href=\"https:\/\/codersee.com\/\">Codersee | Kotlin, Ktor, Spring<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>Hello and welcome to the first article in a series dedicated to integrating a Spring Boot Kotlin app with AWS S3 Object Storage, in which I will show you how to properly set up the connection and make use of the S3Client. What can you expect today? Well, at the &#8230; <\/p>\n<div><a class=\"more-link bs-book_btn\" href=\"https:\/\/imcodinggenius.com\/?p=114\">Read More<\/a><\/div>\n","protected":false},"author":0,"featured_media":115,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-114","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-news"],"_links":{"self":[{"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=\/wp\/v2\/posts\/114"}],"collection":[{"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"replies":[{"embeddable":true,"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=114"}],"version-history":[{"count":0,"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=\/wp\/v2\/posts\/114\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=\/wp\/v2\/media\/115"}],"wp:attachment":[{"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=114"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=114"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/imcodinggenius.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=114"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}