Skip to content

About

spring cloud function with serverless framework and localstack

Resources

Stars

1 star

Watchers

1 watching

Forks

Repository files navigation

개요

AWS와 같은 Cloud 환경에서 이벤트 기반으로 개발할 때, AWS 계정의 권한 유무에 따라 개발자의 생산성에 영향을 미침. 이에 LocalStack 기반으로 로컬 환경에서 직접 E2E 테스트를 해보면서 개발하기 전에 필요한 리소스를 절약 가능.


필수사항

SpringCloudFunction Project

  • AWS의 경우, AWS Adapter 라이브러리 필수! Azure와 GCP의 경우도 별도의 Adapter를 직접 추가하면서 테스트 가능

  • 개별 비즈니스 로직은 Junit이나 SpringBootTest로 테스트 가능하나, 실제 환경과 비슷한 E2E 테스트가 가능하다.

  • build.gradle 내 jar 만드는 과정을 잘 설정해줘야 실제 프로젝트에서 실행 오류가 나지 않는다.

    LocalStack의 경우, AWS만 지원되므로 Azure는 Azurite, GCP의 경우 별도의 Mocking용 Cli 기능을 제공한다.

    build.gradle

        plugins {
            id 'java'
            id 'org.springframework.boot' version '3.4.4'
            id 'io.spring.dependency-management' version '1.1.7'
            id 'com.github.johnrengelman.shadow' version '8.1.1'
            id 'maven-publish'
            id 'org.springframework.boot.experimental.thin-launcher' version "1.0.31.RELEASE"
        }
        // https://github.com/spring-cloud/spring-cloud-function/blob/main/spring-cloud-function-samples/function-sample-aws/build.gradle
        group = 'dev.rumble'
        version = '0.0.1-SNAPSHOT'
        
        java {
            sourceCompatibility = '17'
        }
        
        repositories {
            mavenCentral()
            mavenLocal()
            maven { url 'https://repo.spring.io/milestone' }
        }
        
        ext {
            set('springCloudVersion', "2024.0.1")
        }
        
        assemble.dependsOn = [thinJar, shadowJar]
        
        publishing {
             publications {
                maven(MavenPublication) {
                    from components.java
                        versionMapping {
                            usage('java-api') {
                            fromResolutionOf('runtimeClasspath')
                        }
                        usage('java-runtime') {
                            fromResolutionResult()
                        }
                    }
                }
            }
        }
        
        shadowJar.mustRunAfter thinJar
        
        import com.github.jengelman.gradle.plugins.shadow.transformers.*
        
        shadowJar {
            archiveClassifier = 'aws'
            manifest {
                inheritFrom(project.tasks.thinJar.manifest)
            }
            // Required for Spring
            mergeServiceFiles()
            append 'META-INF/spring.handlers'
            append 'META-INF/spring.schemas'
            append 'META-INF/spring.tooling'
            append 'META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports'
            append 'META-INF/spring/org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration.imports'
            transform(PropertiesFileTransformer) {
            paths = ['META-INF/spring.factories']
            mergeStrategy = "append"
            }
        }
        
        dependencies {
            implementation 'org.springframework.boot:spring-boot-starter'
            implementation 'org.springframework.cloud:spring-cloud-function-adapter-aws'
            implementation 'software.amazon.awssdk:dynamodb:2.25.4' # aws sdk 2버전
            implementation 'software.amazon.awssdk:s3:2.25.4'
            implementation 'com.amazonaws:aws-lambda-java-core:1.2.1' # aws 내 람다에서 사용
            implementation 'com.amazonaws:aws-lambda-java-events:3.8.0'
            implementation 'org.springframework.cloud:spring-cloud-function-context'
            testImplementation 'org.springframework.boot:spring-boot-starter-test'
        }
        
        dependencyManagement {
            imports {
                mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
            }
        }
        
        tasks.named('test') {
            useJUnitPlatform()
        }  
    

LocalStack

실제 AWS 와 유사한 시뮬레이션 환경 제공.

AWS에서 실제 사용되는 Request, Response 값을 기준으로 초기 검증에 도움

예시 코드

  version: '3.8'

  services:
  localstack:
  image: localstack/localstack:latest
  container_name: localstack
  ports:
  - "4566:4566"   # Edge port -
  environment:
    - SERVICES=s3,lambda,dynamodb,sqs,sns,ssm,cloudformation,iam,logs,apigateway # 예제로 사용할 Service 목록
    - DEBUG=1
    - DOCKER_HOST=unix:///var/run/docker.sock
    - LAMBDA_EXECUTOR=docker  # lambda
    - LAMBDA_RUNTIME_ENVIRONMENT_TIMEOUT=60 # runtime init timeout default = 10
    volumes:
    - ./localstack:/var/lib/localstack
    - /var/run/docker.sock:/var/run/docker.sock

실시간 로그 확인

> docker logs -f localstack

실시간 로그 예시

2025-04-11T13:17:33.726  INFO --- [et.reactor-3] localstack.request.http    : POST /_localstack_lambda/69783bc1bdf84380ad219ae7e33f97f1/status/69783bc1bdf84380ad219ae7e33f97f1/ready => 202
2025-04-11T13:17:33.728 DEBUG --- [er:$LATEST_0] l.s.l.i.execution_environm : Start of execution environment 69783bc1bdf84380ad219ae7e33f97f1 for function arn:aws:lambda:us-east-1:000000000000:function:spring-cloud-aws-local-dev-s3Handler:$LATEST took 43141.54ms
2025-04-11T13:17:33.731 DEBUG --- [er:$LATEST_0] l.s.l.i.docker_runtime_exe : Sending invoke-payload '{"invoke-id": "3fc434ce-bd56-4621-9724-8540339a92f3", "invoked-function-arn": "arn:aws:lambda:us-east-1:000000000000:function:spring-cloud-aws-local-dev-s3Handler", "payload": "{\"Records\": [{\"eventVersion\": \"2.1\", \"eventSource\": \"aws:s3\", \"awsRegion\": \"us-east-1\", \"eventTime\": \"2025-04-11T13:16:52.987Z\", \"eventName\": \"ObjectCreated:Put\", \"userIdentity\": {\"principalId\": \"AIDAJDPLRKLG7UEXAMPLE\"}, \"requestParameters\": {\"sourceIPAddress\": \"127.0.0.1\"}, \"responseElements\": {\"x-amz-request-id\": \"b14a66e1\", \"x-amz-id-2\": \"eftixk72aD6Ap51TnqcoF8eFidJG9Z/2\"}, \"s3\": {\"s3SchemaVersion\": \"1.0\", \"configurationId\": \"84b27787\", \"bucket\": {\"name\": \"my-bucket\", \"ownerIdentity\": {\"principalId\": \"A3NL1KOZZKExample\"}, \"arn\": \"arn:aws:s3:::my-bucket\"}, \"object\": {\"key\": \"aws\", \"sequencer\": \"0055AED6DCD90281E5\", \"eTag\": \"098f6bcd4621d373cade4e832627b4f6\", \"size\": 4}}}]}", "trace-id": "Root=1-67f91645-fce3a2f1bc3ccadcadf2d564;Parent=fd5bcd2ce33ef878;Sampled=1"}' to executor '69783bc1bdf84380ad219ae7e33f97f1'
2025-04-11T13:17:33.981  INFO --- [et.reactor-2] localstack.request.http    : POST /_localstack_lambda/69783bc1bdf84380ad219ae7e33f97f1/invocations/3fc434ce-bd56-4621-9724-8540339a92f3/logs => 202


Serverless

CloudFormation과 같은 IaC 작업을 YML 기반으로 쉽게 작성 가능한 프레임워크

Serverless Framework

사전작업

>  brew install node 
>  npm install -g serverless
>  npm install --save-dev serverless-localstack

Serverless 기반으로 배포 예시

>  serverless deploy --stage dev

Serverless 기반 CloudFormation Json 확인

>  serverless info 

참고자료

명령어 설명
serverless deploy / sls deploy 프로젝트 배포 (로컬 or AWS)
sls invoke -f 함수명 함수 수동 호출
sls remove 리소스 삭제
sls info 배포된 정보 확인
sls logs -f 함수명 Lambda 로그 보기
sls print Cloudformation json 보기

테스트 방법

소스 변경 사항이 있는 경우는 1번부터 수행.

인프라만 변경 사항이 있는 경우는 2번부터 수행.

  1. Gradle Build 수행

     ./gradlew clean build
    
  2. Serverless Deploy 수행

    샘플의 경우, 테스트할 stage를 dev로 설정했지만, local로도 변경하여 사용 가능하다.

     serverless deploy --stage dev
    

    배포 완료시, API Gateway가 매핑된 람다의 경우, Gateway ResourceId가 생성된다. 이를 기반으로 호출하면 된다.

    특히, ResourceId는 LocalStack을 다시 띄우면 바뀌므로 주의해야한다.

    sls print 시, get-file이라는 POST 메서드가 생성된다는 것을 볼 수 있다.

      getFile:
        handler: org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest
        environment:
          SPRING_CLOUD_FUNCTION_DEFINITION: getS3Object
          MAIN_CLASS: dev.rumble.serverless.ServerlessApplication
        events:
          - http:
            path: get-file
            method: post

    위의 CloudFormation이 정상 배포시 아래와 같이 표시된다.

     C:\Users\root\Desktop\Study\localstack\serverless>sls deploy --stage dev
    
     Deploying "spring-cloud-aws-local" to stage "dev" (us-east-1)
     Using serverless-localstack
     serverless-localstack: Reconfigured endpoints
     
     Skipping template validation: Unsupported in Localstack
     
     ✔ Service deployed to stack spring-cloud-aws-local-dev (98s)
     
     endpoint: http://localhost:4566/restapis/7nnetdx0fv/dev/_user_request_
     functions:
       s3Handler: spring-cloud-aws-local-dev-s3Handler (35 MB)
       getFile: spring-cloud-aws-local-dev-getFile (35 MB)
       snsHandler: spring-cloud-aws-local-dev-snsHandler (35 MB)
       sqsHandler: spring-cloud-aws-local-dev-sqsHandler (35 MB)
    
    
  3. Aws Cli 명령어 호출 혹은 API Gateway의 API Path 호출

    • API Gateway와 연결된 람다의 경우, 29초가 timeout 시간으로 설정.

      위의 예시의 경우, 아래와 같이 샘플 API를 호출하면 된다.

      > C:\Users\root\Desktop>curl -X POST -H "Content-Type: application/json" http://localhost:4566/restapis/7nnetdx0fv/dev/_user_request_/get-file -d "myfile.txt"
        "File content:\ntest"
      
    • Event Trigger를 기반으로 테스트를 한다면, Aws Cli로 호출해야한다.

          > C:\Users\root\Desktop>aws --endpoint-url=http://localhost:4566 sns publish --topic-arn arn:aws:sns:us-east-1:000000000000:MyTopic --message "Hello from SNS 2"
          {
              "MessageId": "4ab06c8e-8108-4b83-a78d-f9d9ec5dc37b"
          }
             
             
          > C:\Users\root\Desktop>aws --endpoint-url=http://localhost:4566 dynamodb scan --table-name MyTable
          {
              "Items": [
                  {
                      "message": {
                          "S": "{\"Records\": [{\"messageId\": \"2dd7046c-9869-46e1-87b6-eebe5a82a50a\", \"receiptHandle\": \"YTU1NzA0OTgtNzlkZC00NDI2LTk0NWYtOWRkOWE4OWFlZWZiIGFybjphd3M6c3FzOnVzLWVhc3QtMTowMDAwMDAwMDAwMDA6TXlRdWV1ZSAyZGQ3MDQ2Yy05ODY5LTQ2ZTEtODdiNi1lZWJlNWE4MmE1MGEgMTc0NDM3MTEwMy40NzUzODQ1\", \"body\": \"{\\\"Type\\\": \\\"Notification\\\", \\\"MessageId\\\": \\\"4ab06c8e-8108-4b83-a78d-f9d9ec5dc37b\\\", \\\"TopicArn\\\": \\\"arn:aws:sns:us-east-1:000000000000:MyTopic\\\", \\\"Message\\\": \\\"Hello from SNS 2\\\", \\\"Timestamp\\\": \\\"2025-04-11T11:31:31.386Z\\\", \\\"UnsubscribeURL\\\": \\\"http://localhost.localstack.cloud:4566/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:us-east-1:000000000000:MyTopic:e60ba03d-a23d-470b-a8de-926e950bc763\\\", \\\"SignatureVersion\\\": \\\"1\\\", \\\"Signature\\\": \\\"So/mISrgBNYej5/+ymY6kjYAwV3xtUhuofTNUtBHlA0qJtf4pdGCpxJVVio7N+6yLXYPsaH83Ho5eXpuBHy8ljg94BLfX0caI1Ygcj5ii+deqEdi5EsOYHqowGBb7n0FoUhL/DMg3b2PuS7tpTWfzu6/6E6EyIaiXVKFoPt3k7+uGBbP9c/X5qpGzPf34f9xyhDIRFqxNl0zSS1wPubxgxkNAEmbCYZhS2i+lbGbjF+R7JH1e5ts4qgXuwzpDuREcwF4UUhfhd+ibrNaF5UhV2lA/nNIL0AHgI3GhYFgk+27JkgbhdJwlkUKxTTXfirbgPuXPtATx7uyT5gr4XyV5Q==\\\", \\\"SigningCertURL\\\": \\\"http://localhost.localstack.cloud:4566/_aws/sns/SimpleNotificationService-6c6f63616c737461636b69736e696365.pem\\\"}\", \"attributes\": {\"SenderId\": \"000000000000\", \"SentTimestamp\": \"1744371091553\", \"ApproximateReceiveCount\": \"1\", \"ApproximateFirstReceiveTimestamp\": \"1744371103475\"}, \"messageAttributes\": {}, \"md5OfBody\": \"c67f1b908db7450961f534d3e44b2ccb\", \"eventSourceARN\": \"arn:aws:sqs:us-east-1:000000000000:MyQueue\", \"eventSource\": \"aws:sqs\", \"awsRegion\": \"us-east-1\"}]}"
                      },
                      "id": {
                          "S": "a092b949-e06f-437d-a7d0-d7ae0b99faad"
                      }
                  }
              ],
              "Count": 1,
              "ScannedCount": 1,
              "ConsumedCapacity": null
          }
      

참고사항

현재 프로젝트에서 localstack은 wsl의 docker 환경에서 띄우고 있다. wsl의 docker 환경과 windows를 연결시키려면 아래와 같은 방식으로 연결 가능하다.

  1. WSL에서 dev eth0 proto kernel scope link IP 주소를 찾는다.

    $ ip route 
    
  2. PowerShell을 관리자 모드로 켜고, 원하는 포트와 ip를 허용해 준다.

    > netsh interface portproxy add v4tov4 listenport=4566 listenaddress=0.0.0.0 connectport=4566 connectaddress=위에서 찾은 ip
    
  3. 포트가 정상적으로 등록됬는지 확인 한다.

    (proxy 형태로 허용하는 형태여서, windows에서 해당 port를 쓸일이 있으면 지워줘야된다.)

    > netsh interface portproxy show all
    

About

spring cloud function with serverless framework and localstack

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages