상세 컨텐츠

본문 제목

[AWS 교육 요약] - Developing on AWS (1/3)

개발 이야기

by 리치윈드 - windFlex 2022. 7. 13. 09:58

본문

반응형

AWS CLI를 활용한 자원 관리

[관련글]

2022.07.11 - [IT 이야기/IT 상식] - AWS교육[요약]-Technical Essential : IAM/Role/EC2/VPC/ELB

2022.07.13 - [개발 이야기] - [AWS 교육 요약] - Developing on AWS (1/4)

2022.07.13 - [개발 이야기] - [AWS 교육 요약] Developing on AWS (2/4)


[ 교육 목표 ]

  • 개발환경을 지원하도록 IAM 권한 구성
  • AWS SDK를 사용한 Cloud Native Application 설계/구축/배포
  • AWS 리소스 모니터링 및 유지 관리

[ 목표 서비스 아키텍처 - MSA ] 

 

AWS 애플리케이션 서비스 아키텍처

AWS Web Console에서 AWS 서비스들을 제어 할 수 있지만, 서비스에 따라서는 프로그래밍 환경에서 제어가 될어야 할 필요성이 있다. AWS는 이러한 환경을 지원하기 위해, AWS CLI와 AWS SDK등과 같은 인터페이스를 제공하며, 이번 포스팅에서는 AWS CLI와 AWS SDK를 사용하여 환경구축/애플리케이션 개발하는 방법에 대하여 학습하는 과정을 요약 한다. 

AWS CLI와 SDK를 활용하여 AWS Service를 제어하는 예 (좌), AWS서비스를 제어하는 방법들 (우) AWS REST API를 활용하여 서비스 제어하며 REST API를 사용하는 인터페이스는 APP(1), AWS Console (2), AWS CLI (3), AWS SDK (4) 등을 지원하고 있다. 

보안을 위해, 대부분은 AWS Request는 Access Key( Access Key ID와 Secret Access Key로 구성)로 서명해야 한다. AWS의 HTTP 전송하는 Request header에 인증정보를 추가해야 한다. AWS는 HTTP의 인증정보로 서명 버전 (SigV4)을 지원한다.
REST API를 기본 인터페이스로 한다는 것을 고려하면, 여러가지 동작이 이해하기 쉬워진다.

 

AWS에서 개발하기 (SDK)

AWS Web Console을 사용하면 되지, 왜 SDK를 사용할까?

여러 가지 이유가 있겠지만, 1) 프로그래밍으로 자동화 해야 하는 경우 (Web Browser 엔진을 돌려서 클릭할 수 없지는 않은가...), 2) Web Console의 위치를 일일이 찾는 것이 더 귀찮은 경우, 3) 여러가지 서비스와 방식이 복합적으로 연동되서 돌아가야 하는 경우, SDK가 더 쉬울 수도 있음.

 

SDK 종류

  • 하위수준 API
  • 상위수준 API

하위 수준의 API의 예 : S3버킷의 리스트를 가져온다.

# Retrieve the list of existing buckets
s3 = boto3.client('s3')
response = s3.list_buckets_v2(Bucket='mybucket')

for content in response['Contents']:
    print(f'  {content["Key"]} : {content["LastModified"]}')

상위 수준 API의 예: S3 버킷 리스트를 가져온다. (위와 동일 기능)

추상화를 통하여 좀 더 간결하게 사용할 수 있으며, 직접적인 구조 (JSON)으로 접근하지 않고, Method/Member처럼 접근해서 사용할 수 있다. 

# Retrieve the list of existing buckets
s3 = boto3.resource('s3')
bucket = s3.Bucket('mybucket')

for obj in bucket.objects.all():
    print(f'  {obj.key} : {obj.last_modified')

 

상위수준과 하위수준 API간 차이가....뭐 큰 차이는 없어 보인다. 따옴표 사용하는 횟수가 줄어들었다는 것 정도??

 

AWS CLI (명령줄 인터페이스) 

AWS CLI (명령줄)을 사용하면 Linux Shell, Windows CMD, MacOS Ternmina 등에서 바로 실행할 수 있으며, SSH/PuTTy, Cloud9 등에서 바로 제어가 가능한다.

필자의 경우 CLI 명령어를 선호한다. 

 

AWS CLI 명령어의 기본 구조는 다음과 같다. 

$ aws S3 ls s3://mybucket --recursive
$ aws s3 cp myfile.txt s3://mybucket
$ aws help
$ aws s3 ls help
명령어의 유형 : `AWS <서비스명> <명령어> <타겟> <후속 파라미터/매개변수> ...`. 매개변수가 필요하지 않은 `help`와 같은 명령어는 뒷쪽 입력이 필요없는 경우도 존재
AWS CLI는 기본적으로 `Botocore`라는 Python 라이브러리를 사용한다. 즉, Python SDK와 가장 유사한 동작을 한다.

 

API의 동기/비동기 

* API 혹은 SDK 명령어는 대부분 비동기 방식 (Async.)로 동작한다. 만약, create db table 명령어를 요청 했다면, 반환된 값은 요청을 완료했다는 값이지, db table 생성이 완료되었다는 것을 의미하지는 않는다. 이 때문에, 비동기(async) 처리가 추가로 필요하다. AWS는 비동기 처리를 위해서는, 작업 완료까지 대기하는 waiter를 별도 지원한다. 

 

1) 비동기 명령어의 예

$ aws dynamodb describe-table --table-name Notes --query "Table.TableStatus"

2) 결과가 반환될 때까지 대기 (Async 처리)

aws dynamodb wait table-exists --table-name Notes

 

2) SDK 버전에서의 비동기 처리 (Python)

dynamodb = boto3.resource('dynamodb')

table = dynamodb.create_table(
	TableName=table_name,
    KeySchema=[
    	{'AttributeName': 'year', 'KeyType': 'HASH'},  # Partition key
        {'AttributeName': 'title', 'KeyType': 'RANGE'}  # Sort key
    ],
    AttributeDefinitions=[
    	{'AttributeName': 'year', 'AttributeType': 'N'},
        {'AttributeName': 'title', 'AttributeType': 'S'}
	],
	ProvisionedThroughput={'ReadCapacityUnits': 10, 'WriteCapacityUnits': 10})
table.wait_until_exists()
`table.wait_until_exists()`를 통해서 완료될 때까지 대기 한다. 만약, 딱히 대기가 필요한 프로시저가 아니라면 굳이 대기할 필요는 없다. 

3) Waiter의 또다른 예

dynamodb = boto3.resource('dynamodb')

table = dynamodb.create_table( TableName="Notes", ..... )

table.meta.client.get_waiter('table_exists').wait(TableName="Notes")

print(table.item_count)

 

참조할만한 소스 코드 : https://github.com/awsdocs/aws-doc-sdk-examples/tree/main/python/example_code

권한/인증 

권한/인증 부분은 조금은 어려운 부분이다. 그렇지만 우선 이런것이 있다...하고 이해하고 넘어가 보자. 

사실은, 조금 편하게 Cloud환경에서 개발하고 싶은데, 개발자가 인프라 담당자/보안담당자 처럼 이런 부분까지 Low Level로 설정해야 하나? 하는 생각이 들긴 한다. 

 

AWS 권한 / 정책

AWS에서 컨텍스트에 대한 접근 허용/차단을 위한 권한부여는 정책을 사용한다. 정책은 크개 두가지 유형 1) 자격 증명 기반 정책과 2) 리소스 기반 정책이 있다. 

  • 자격증명 기반 정책 : IAM 사용자 또는 그룹/역할에 연결된다. 
  • 리소스 기반 정책 : 리소스에 연결 된다. 예) S3버킷에 정책을 연결한다. 

AWS에서 권한 부여는 정책 (Policy)를 생성하고, 해당 권한에 Policy를 붙여서 권한을 부여하는 형태로 이루어 진다. 이 때 정책(Policy)는 JSON 형태로 표기한 문서를 의미한다. 

 

IAM 사용자 혹은 사용자 그룹이 , AWS CLI, Shell, SDK, Console 등을 사용하여 접근 자원에 접근하려 한다. 이 때, 자격증명 기반 정책은 해당 사용자에게 권한을 부여 한다. 

s3:::notes에 대해 ListBucket을 허용하는 권한 정책을 사용자/사용자그룹에 부여 (좌), 특정IP 대역을 제외한 모든 사용자는 S3:::notes에 접근불가(Deny) 정책을 리소스(S3)에 부여 (우)

 

역할(Role)

  • AWS 권한 분야에서 가장 중요하고 어렵기도 한 부분이다.
  • 역할의 중요 키워드는 "임시성"이다. 만료 기간이 정해진 임시 권한의 묶음이라고 할 수 있다. 
  • 가령, 외주개발자에세 1달동안 접근 권한을 주고자 할 때, 권한을 부여했다가 1개월 후에 권한을 제거하는 방식으로 처리가 된다면 실수/오류 발생 가능성이 높아진다. 따라서, 이러한 경우 1달 만료하도록 설정된 권한을 부여한 역할(Role)을 생성하고 외주개발자에게 Role을 부여하면 된다. 

 

 

권한/정책에서 권한은 최소 권한 사용을 원칙으로 한다. (좌), 권한에 대한 중복/충돌이 발생할 수 있는데, 권한을 평가하는 우선순위는 "거부/차단"정책이 있는가를 우선 적용한다. 즉, 차단 정책과 허용 정책이 있다면 "차단"이 적용된다. 그리고 특별히 거론하지 않는 경우 거부 (Deny All)이 적용된다.

정책 평가 로직 참조 : https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_evaluation-logic.html

 

 

AWS Cloud 9 - AWS IDE 

AWS IDE-Cloud9의 특징 (좌) AWS IDE (Cloud9) 초기 모습 (우)

AWS cloud 9 

AWS에서 제공하는 Cloud 형 IDE이다. AWS EC2, S3, Lambda 등과 연계된 작업을 한다면 최적의 환경을 제공한다. 겉으로 보기에는 VS Code와 유사한 환경을 제공하며, AWS 관련 CLI / SDK 등 환경이 기본 세팅되어 있다. 

AWS cloud9을 실행 후 좀 더 상세하게 보면, 해당 환경의 EC2 Instance를 살펴 보면 다음과 같이 Cloud9은 Cloud9용 Instance가 이미 구동되어 있다. EC2의 Security 탭에 가보면, IAM Role이 이미 부여되어 있는 것을 알 수 있다. 부여된 Role의 이름은 위에서 사용한 "notes-appolication-role"임을 알 수 있다.

Cloud9을 위한 EC2 인스턴스가 생성되어 있다.

 

 

개발 환경 구축 (Practice -1)

Cloud9의 개발 환경중 하단의 터미널 창에서 "aws configure" 명령어를 통해 기본 개발환경에 대한 설정을 할 수 있다. 
aws configure

위와 같이 aws cli 명령어를 통하여 기본 설정을 할 수 있다. AWS Access Key ID, Access Key, Region, Format등을 설정해 준다. 별도 Role이 설정 되어 있다면 어차피 개인 증명은 동작하지 않기 때문에, Acces Key ID, Access Key는 그냥 엔터를 치고 넘어가면 된다. 

aws configure 명령어를 통하여 설정하는 화면

  • 현재 Role을 확인 하는 방법 : `aws sts get-caller-identity`

현재 부여된 Role을 확인 <- `aws sts get-caller-identity`

  • S3 버킷을 리스트 하는 명령어 : `aws s3 ls`
  • S3 버킷을 삭제 `rb` 명령어 : `aws s3 rb s3:// <버킷이름> `
  • IAM 권한 중 특정 Policy를 해당 Role "notes-application-role"에 부여  :
    `aws iam attach-role-policy --policy-arn <정책이름> --role-name notes-application-role`
권한 할당을 위해서는 정책이름은 자원명 (arn)으로 입력해야 한다. 그런데, S3 Delete Bucket 정책의 ARN은 어떻게 확보할 수 있을까?  "S3-Delete-Bucket-Policy" 정책의 ARN은 다음과 같은 명령어를 통하여 변수로 만들 수 있다. 
`policyArn=$(aws iam list-policies --output text --query 'Policies[?PolicyName == `S3-Delete-Bucket-Policy`].Arn')`

 

IAM Role : "notes-application-role"에 부여된 권한(permission)은 ReadOnlyAccess와 SSM(?)에 대한 권한만 존재함을 알 수 있다. 

 

 

 

 

 

SDK 활용 S3버킷 생성/삭제/파일업로드 (Practice-2) 

 

 

 

 

CreateBucket (Boto3)

import boto3, botocore, configparser

def main(s3Client):
    print('\nStart of create bucket script\n')

    print('Reading configuration file for bucket name...')
    config = readConfig()
    bucket_name = config['bucket_name']

    print('Verifying that the bucket name is valid...')
    #### Verify that the bucket exists. The script with exit 
    #### if the name is not valid for a new bucket.
    verifyBucketName(s3Client, bucket_name)
    print(bucket_name)

    #### Create the notes-bucket-
    createBucket(s3Client, bucket_name)

    ##Pause until the the bucket is in the account
    print('\nConfirm that the bucket exists...')
    verifyBucket(s3Client, bucket_name)

    print('\nEnd of create bucket script\n')

def verifyBucketName(s3Client, bucket):
    try:
        ## Start TODO 2: enter a command that will check if a bucket already exists in AWS
        ## with the name built from your ini file input.
        
        # s3Client.list_buckets(Bucket=bucket)
        s3Client.head_bucket(Bucket=bucket)

        ## End TODO 2

        # If the previous command is successful, the bucket is already in your account.
        raise SystemExit('This bucket has already been created in your account, exiting because there is nothing further to do!')
    except botocore.exceptions.ClientError as e:
        error_code = int(e.response['Error']['Code'])
        if error_code == 404:
          ## If you receive a 404 error code, a bucket with that name
          ##  does not exist anywhere in AWS.
          print('Existing Bucket Not Found, please proceed')
        if error_code == 403:
          ## If you receive a 403 error code, a bucket exists with that
          ## in another AWS account.
          raise SystemExit('This bucket has already owned by another AWS Account, change the suffix and try a new name!')

def createBucket(s3Client, name):
    session = boto3.session.Session()

    # Obtain the region from the boto3 session
    current_region = session.region_name
    print('\nCreating ' + name + ' in ' + current_region)

    # Start TODO 3: Create a new bucket in the users current region 
    # and return the response in a response variable.
    if current_region == 'us-east-1':
        response = s3Client.create_bucket(Bucket=name)
    else:
        response = s3Client.create_bucket(
          Bucket=name,
          CreateBucketConfiguration={
              'LocationConstraint': current_region
          })
    
    
    # End TODO 3:

    print('Success!')

def verifyBucket(s3Client, bucket):
    ## Start TODO 4: Complete the function so that it will 
    ## pause and only proceed after the bucket exists.
    waiter = s3Client.get_waiter('bucket_exists')
    waiter.wait(Bucket=bucket)  

    ## End TODO 4
    print('The bucket:' + bucket + ' is now available.')

## Utility methods
def readConfig():
    config = configparser.ConfigParser()
    config.read('./labRepo/config.ini')
    
    return config['S3']

## TODO 1: Create an S3 client to interact with the service and pass 
## it to the main function that will create the buckets

client = boto3.client('s3')

## End TODO 1

try:
    main(client)
except botocore.exceptions.ClientError as err:
    print(err.response['Error']['Message'])
except botocore.exceptions.ParamValidationError as error:
    print(error)

 

awsstudent:~/environment $ python labRepo/create-bucket.py 

Start of create bucket script

Reading configuration file for bucket name...
Verifying that the bucket name is valid...
Existing Bucket Not Found, please proceed
notes-bucket-sjlee-0010701

Creating notes-bucket-sjlee-0010701 in us-west-2
Success!

Confirm that the bucket exists...
The bucket:notes-bucket-sjlee-0010701 is now available.

End of create bucket script

 

 

 

 

 

 

import boto3, botocore, configparser

def main(s3Client):
    print('\nStart of create object script\n')
    ## Initialize variables for object creation

    print('Reading configuration file for bucket name...')
    config = readConfig()
    bucket_name = config['bucket_name']
    source_file_name = config["object_name"] + config['source_file_extension']
    key_name = config['key_name']+ config['source_file_extension']
    contentType = config['source_content_type']
    metaData_key = config['metaData_key']
    metaData_value = config['metaData_value']

    #### Create object in the s3 bucket
    print('Creating Object...')
    print(uploadObject(s3Client, bucket_name, source_file_name, key_name, contentType, {metaData_key: metaData_value}))
    
    print('\nEnd of create object script\n')

def uploadObject(s3Client, bucket, name, key, contentType, metadata={}):

    ## Start TODO 5: create a object by transferring the file to the S3 bucket, 
    ## set the contentType of the file and add any metadata passed to this function.
    
    response = s3Client.upload_file(
        Bucket=bucket, 
        Key=key,
        Filename=name,
        ExtraArgs={
            'ContentType': contentType,
            'Metadata': metadata
            }
    )
    
    
    ## End TODO 5
    return "Finished creating object\n"
    
def readConfig():
    config = configparser.ConfigParser()
    config.read('./labRepo/config.ini')
    
    return config['S3']

# Create an S3 client to interact with the service and pass 
# it to the main function that will create the buckets
client = boto3.client('s3')
try:
    main(client)
except botocore.exceptions.ClientError as err:
    print(err.response['Error']['Message'])
except botocore.exceptions.ParamValidationError as error:
    print(error)

 

 

 

데이터 변환 후 업로드 (CSV -> JSON)

import boto3, botocore, json, csv, io, configparser

def main(s3Client):
    print('\nStart of convert object script\n')

    ## Initialize variables for object creation
    print('Reading configuration file for bucket name...')
    config = readConfig()
    bucket_name = config['bucket_name']
    source_file_name = config['object_name'] + config['source_file_extension']
    key_name = config['key_name']+ config['source_file_extension']
    processed_file_name = config['key_name'] + config['processed_file_extension']
    contentType = config['processed_content_type']
    metaData_key = config['metaData_key']
    metaData_value = config['metaData_value']

    #### Get the object from S3
    print('\nGetting the CSV object from S3 bucket')
    csvStr = getCSVFile(s3Client, bucket_name, key_name)
    
    ## Convert the object to the new format
    print('\nConverting CSV string to JSON...')
    jsonStr = convertToJSON(csvStr)
    
    ## Uploaded the converted object to S3
    print('Creating the new JSON object on S3')
    print(createObject(s3Client, bucket_name, processed_file_name, jsonStr, contentType, {metaData_key: metaData_value}))

    print('\nEnd of convert object script\n')

def getCSVFile(s3Client, bucket, key):
    bytes_buffer = io.BytesIO()
    
    ## Start TODO 6: Download the file contents to the 
    ## bytes_buffer object so that it can be decoded to a string.
    s3Client.download_fileobj(
        Bucket=bucket, 
        Key=key, 
        Fileobj=bytes_buffer)
    

    ## End TODO 6

    byte_value = bytes_buffer.getvalue()
    return byte_value.decode('utf-8')

def createObject(s3Client, bucket, key, data, contentType, metadata={}):
    ## Start TODO 7: Create an S3 object with the converted data
    s3Client.put_object(
        Bucket=bucket, 
        Key=key,
        Body=data,
        ContentType=contentType,
        Metadata=metadata
    )
    

    ## End TODO 7
    
    return 'Successfully Created Object\n'

def convertToJSON(input):
    jsonList = []
    keys = []
    
    csvReader = csv.reader(input.split('\n'), delimiter=",")

    for i, row in enumerate(csvReader):
        if i == 0:
            keys = row
        else:
            obj = {}
            for x, val in enumerate(keys):
                obj[val] = row[x]
            jsonList.append(obj)
    return json.dumps(jsonList, indent=4)

def readConfig():
    config = configparser.ConfigParser()
    config.read('./labRepo/config.ini')
    
    return config['S3']

# Create an S3 client to interact with the service and pass 
# it to the main function that will create the buckets
client = boto3.client('s3')
try:
    main(client)
except botocore.exceptions.ClientError as err:
    print(err.response['Error']['Message'])
except botocore.exceptions.ParamValidationError as error:
    print(error)
파일 처리가 아니라, 메모리에서 처리하고 메모리로 부터 업로드 하는 것에 유의 한다. 따라서, 이전에 Bucket에 object 를 업로드 할 때와 달리, `s3Client.download_fileobject()`와 `s3Client.put_object()`를 사용한다.

 

 

 

notes.json 파일이 추가로 업로드 되어 있는 것을 확인 할 수 있다.

 

 

 

S3 정적 웹호스팅 설정 - 

 

버킷 이름이 표함된 버킷 이름을 변수명으로 저장

  • 고정된 버킷 이름 보다는 동적으로 찾는 형태로 변화수화 하기 (저수준의 api 인 "s3api" 사용)
$> mybucket=$(aws s3api list-buckets --output text --query 'Buckets[?contains(Name, `notes-bucket`) == `true`].Name')

 

파일 동기화 (html/아래 파일과 s3 bucket의 파일을 동기화)

aws s3 sync ~/environment/labRepo/html/. s3://$mybucket/

파일 동기화 aws cli 명령어 실행 결과 (좌), 실형 결과를 AWS Console(web)에서 확인한 결과 (우)

 

 

웹 호스팅 활성화

$> aws s3api put-bucket-website --bucket $mybucket --website-configuration file://~/environment/labRepo/website.json

 

 

 

 

권한 정책 (policy) 부여

aws s3api put-bucket-policy --bucket $mybucket --policy file://~/environment/labRepo/policy.json

 

정적 웹사이트 경로

printf "\nYou can now access the website at:\nhttp://$mybucket.s3-website-$region.amazonaws.com\n\n"
You can now access the website at: http://notes-bucket-xxxxx-xxxxxx.s3-website-us-west-2.amazonaws.com
반응형

관련글 더보기

댓글 영역