저번 포스팅에서는 고루틴을 이용하여 19900개의 URL을 날려보고, 검색 결과를 goquery를 통해 가져와 저장하는 것까지 알아보았다. (링크: https://kyumcoding.tistory.com/62)
이번 시간에는 스크래핑 결과를 S3에 업로드 하기 위해 구현한 코드를 설명한다. 최신 자료도 한국어 자료도 많지는 않아서 구현하는데 고생 좀 했다; 특히 그냥 S3에 업로드 하는 코드는 공식 문서 등에서 볼 수 있었는데, 아예 Credential Config하는 코드가 같이 있는 경우는 많지 않았어서 더 고생한듯
크게 1) AWS 접속을 위해 Credential 설정하는 부분과 2) S3에 업로드 하는 부분 두 부분으로 나뉘어 있다.
*참고사항 Lambda와 S3가 같은 계정일 경우 Credential 설정은 필요없지만, 이번의 경우 Lambda 계정과 S3 계정이 달랐음 |
소스 코드 전체
type BucketBasics struct {
S3Client *s3.Client
}
// AWS S3 사용을 위한 credential 설정 & client 생성
func AWSConfigure() BucketBasics {
staticProvider := credentials.NewStaticCredentialsProvider(
os.Getenv("AWS_BUCKET_ACCESS_KEY"),
os.Getenv("AWS_BUCKET_SECRET_KEY"),
"")
sdkConfig, err := config.LoadDefaultConfig(
context.Background(),
config.WithCredentialsProvider(staticProvider),
config.WithRegion(os.Getenv("AWS_REGION")),
)
checkErr(err)
s3Client := s3.NewFromConfig(sdkConfig)
bucketBasics := BucketBasics{s3Client}
return bucketBasics
}
// S3 업로드를 위한 Uploader 구현
func S3Uploader(INFO map[string][]map[string]interface{}, basics BucketBasics, fileName string) error {
// data가 struct 형태일때는 이상하게 marshal이 되더니, map으로 바꾸니까 한방에 marshal이 잘 됨. 이유가 뭘까?
content, err := json.MarshalIndent(INFO, "", " ")
if err != nil {
log.Fatalln("JSON marshaling failed: %s", err)
}
// json 바이트 스트림을 S3에 업로드
_, err = basics.S3Client.PutObject(context.TODO(), &s3.PutObjectInput{
Bucket: aws.String(os.Getenv("AWS_BUCKET_NAME")),
Key: aws.String(fileName),
Body: bytes.NewReader(content),
})
if err != nil {
return fmt.Errorf("failed to upload, %v", err)
}
fmt.Println(fileName + "file successfully uploaded in S3")
return nil
}
// 위 두 함수를 HandleRequest에서 아래와 같이 사용
func HandleRequest(ctx context.Context) {
// s3에 업로드
bucktBasics := AWSConfigure()
S3Uploader(INFO, bucktBasics, fileName)
}
Credential Config 설정하는 코드
Configuration을 로딩하는 방법은 정말 여러가지이지만 (그래서 내가 더 고통 받았지만) 기본적으로 config.LoadDefaultConfig를 사용하여 aws.config 인스턴스를 초기화한다. (AWS SDK for Go V2 공식문서 참고)
그리고 config.LoadDefaultConfig를 사용 기본 패턴은 아래와 같다
import (
"context"
"log"
"github.com/aws/aws-sdk-go-v2/config"
)
// ...
cfg, err := config.LoadDefaultConfig(context.TODO(),
config.WithRegion("ap-northeast-2") // region 설정
// 요기에 credential 정보들을 입력, 불러온다
)
if err != nil {
log.Fatalf("failed to load configuration, %v", err)
}
config.LoadDefaultConfig를 사용하려 할 때 SDK는 credentail chain을 활용하여 아래 4가지 정보들을 통해 credential 정보를 얻는다.
- 환경변수
- Static Credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN)
- Web Identity Token (AWS_WEB_IDENTITY_TOKEN_FILE)
- config 파일
- .aws 폴더 아래에 credentials 파일
- .aws 폴더 아래에 config 파일
- ECS task definition이나 RunTask API operation, IAM role을 사용하는지
- EC2 인스턴스를 사용한다면 EC2의 IAM role
우선 내가 사용한 방법은 Static Credential들을 하드 코딩하는 방법을 사용했다. (이제와서 좋은 방법은 아니겠다싶긴 하지만, 나 같은 초보에게 명시적인 것 혹은 하드코딩만큼 직관적인 것도 없긴하다) 다만 나는 os.Getenv를 통해 민감한 정보를 .env 파일로 빼서 관리하는 방법을 택했다.
ACCESS KEY, SECRET KEY를 NewStaticCredentialsProvider로 staticProvider에 담은 이후, LoadDefaultConfig에 넘겨주는 방식이다.
공식 문서에 나온 Static Credential 하드코딩 패턴
cfg, err := config.LoadDefaultConfig(context.TODO(),
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(
"AKID",
"SECRET_KEY",
"TOKEN")),
)
실제 내가 구현한 것
func AWSConfigure() BucketBasics {
staticProvider := credentials.NewStaticCredentialsProvider(
os.Getenv("AWS_BUCKET_ACCESS_KEY"),
os.Getenv("AWS_BUCKET_SECRET_KEY"),
"")
sdkConfig, err := config.LoadDefaultConfig(
context.Background(),
config.WithCredentialsProvider(staticProvider),
config.WithRegion(os.Getenv("AWS_REGION")),
)
checkErr(err)
s3Client := s3.NewFromConfig(sdkConfig)
bucketBasics := BucketBasics{s3Client}
return bucketBasics
}
ACCESS KEY, SECRET KEY를 NewStaticCredentialsProvider로 staticProvider에 담은 이후, LoadDefaultConfig에 넘겨주는 방식이다.
만약 .aws/credentials나 .aws/config 파일을 만들어서 관리하고 싶다면 공식 문서 참고. 영어지만... 차근차근 읽으니 읽을만하다
S3 Upload를 위한 BucketBasics, Uploader 구현
아 이 부분도 쉽지 않았다. 열심히 공식문서 뒤져보며 나랑 비슷한 시나리오의 코드를 찾아서 구현해보았다. 참고한 코드는 Go V2용 SDK를 사용하는 아마존 S3 예제 페이지에서 '버킷에 객체 업로드' 와 시나리오 - 버킷 및 객체 시작하기'에 있다.
특이한 점은 BucketBasics라는 type을 하나 만들어서 사용한다는 점인데, 문서에는 BucketBasics encapsulates S3 actions used in the examples. It contains S3Client that is used to perform bucket and object actions 라고 쓰여있다.
공식 문서 내용 일부 편집
type BucketBasics struct {
S3Client *s3.Client
}
// UploadFile reads from a file and puts the data into an object in a bucket.
func (basics BucketBasics) UploadFile(bucketName string, objectKey string, fileName string) error {
file, err := os.Open(fileName)
if err != nil {
log.Printf("Couldn't open file %v to upload. Here's why: %v\n", fileName, err)
} else {
defer file.Close()
_, err := basics.S3Client.PutObject(context.TODO(), &s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(objectKey),
Body: file,
})
if err != nil {
log.Printf("Couldn't upload file %v to %v:%v. Here's why: %v\n",
fileName, bucketName, objectKey, err)
}
}
return err
}
func RunGetStartedScenario(sdkConfig aws.Config, questioner demotools.IQuestioner) {
defer func() {
if r := recover(); r != nil {
fmt.Println("Something went wrong with the demo.\n", r)
}
}()
log.Println(strings.Repeat("-", 88))
log.Println("Welcome to the Amazon S3 getting started demo.")
log.Println(strings.Repeat("-", 88))
s3Client := s3.NewFromConfig(sdkConfig)
bucketBasics := actions.BucketBasics{S3Client: s3Client}
실제 내가 구현한 것
// S3 업로드를 위한 Uploader 구현
func S3Uploader(INFO map[string][]map[string]interface{}, basics BucketBasics, fileName string) error {
content, err := json.MarshalIndent(INFO, "", " ")
if err != nil {
log.Fatalln("JSON marshaling failed: %s", err)
}
// json 바이트 스트림을 S3에 업로드
_, err = basics.S3Client.PutObject(context.TODO(), &s3.PutObjectInput{
Bucket: aws.String(os.Getenv("AWS_BUCKET_NAME")),
Key: aws.String(fileName),
Body: bytes.NewReader(content),
})
if err != nil {
return fmt.Errorf("failed to upload, %v", err)
}
fmt.Println(fileName + "file successfully uploaded in S3")
return nil
}
// 위 두 함수를 HandleRequest에서 아래와 같이 사용
func HandleRequest(ctx context.Context) {
// s3에 업로드
bucktBasics := AWSConfigure()
S3Uploader(INFO, bucktBasics, fileName)
}