Heesung Yang

[Python] AWS EC2 정보를 Google Spreadsheets 문서에 정리하기

서론

사용 중인 AWS EC2 리소스에 대해 엑셀 문서로 정리해서 보안팀에 제출해야 하는 일이 생겼다. 1년에 한번씩 보안 심사를 받는데, 그 때마다 같은 일을 계속해야 하는 귀찮음을 해결하고자 삽질한 기록을 남긴다.

  • 사용 언어 : Python 3.6
  • 사용한 라이브러리
    • AWS
      • boto3 : 1.18.21
    • Google Sheets (Link to Official Document)
      • google-api-python-client : 2.15.0
      • google-auth-httplib2 : 0.1.0
      • google-auth-oauthlib : 0.4.5

AWS

boto3는 AWS에서 공식지원하는 파이썬 라이브러리다. (공식 홈페이지) 해당 라이브러리 사용의 큰 흐름은 아래와 같다.

  1. boto3 session 생성 (profile, region)
  2. session 으로부터 client 생성
  3. 생성한 client 로 필요한 리소스 조회/생성/삭제/업데이트

엑세스 키 생성

우선 자신의 AWS 계정에서 사용할 Key를 생성하자. 아래 두 정보가 필요하다. 이미 사용 중인 key가 있다면 아래 내용은 넘어가자.

  • ACCESS_KEY
  • SECRET_ACCESS_KEY
  1. AWS Console 페이지 접속 후 IAM 서비스 선택한다. aws iam service selection screenshot

  2. 왼쪽 사이드바 메뉴에서 사용자 선택 후 자신의 계정명을 클릭한다. aws iam service selection screenshot

  3. 보안 자격 증명 탭을 클릭한다. aws iam service selection screenshot

  4. 화면을 아래로 스크롤한 후 엑세스 키 만들기 버튼을 클릭한다. aws iam service selection screenshot

  5. 자동으로 생성된 엑세스 키 ID비밀 엑세스 키(표시 버튼을 눌러서 확인)를 적어둔다. 주의할 점은 비밀 엑세스 키 값은 해당 창을 닫으면 이 후 확인할 수 있는 방법이 없다.! 잊어버릴 경우 재생성 해야한다. aws iam service selection screenshot

  6. 아래와 같이 엑세스 키가 생성됨을 확인할 수 있다. 해당 키는 언제든지 비활성화/삭제가 가능하므로 혹시나 외부에 노출된 경우 바로 삭제하자. (가장 오른쪽의 x 버튼을 누르면 삭제할 수 있다.) aws iam service selection screenshot

boto3 라이브러리

우선 라이브러리 설치부터 하자. 필자는 파이썬 3.6을 사용하였다. 아래 명령어를 터미널 창에 입력한다.

pip install boto3

그 후 아래 코드를 입력하여 ec2 인스턴스 정보를 받아와보자.

#!/usr/bin/env python3

import boto3
from botocore.config import Config

# 아래처럼 코드 내에 키를 삽입하는건 굉장히 위험하다 !!!
# 코드가 github public repository에 올라가는 순간...
# 좀 더 나은 방법은 이 포스팅 하단부에 나오니 해당 부분을 참고하자.
AWS_ACCESS_KEY='YOUR_ACCESS_KEY'
AWS_SECRET_KEY='YOUR_SECRET_ACCESS_KEY'

client = boto3.client('ec2',
                      aws_access_key_id=AWS_ACCESS_KEY,
                      aws_secret_access_key=AWS_SECRET_KEY,
                      config=Config(region_name='ap-northeast-2'),
                     )

# running 상태인 ec2 인스턴스 목록 얻기
response = client.describe_instances(
    Filters=[
            {'Name': 'instance-state-name', 'Values': ['running']}
        ]
    )

# `response`를 출력하면 굉장히 많은 정보가 출력됨을 알 수 있는데
# 내용이 너무 길어서 여기에서는 필자가 필요했던 내용만 추려서 출력해보았다.
# print(response)

for obj in response['Reservations']:
    for tag in obj['Instances'][0]['Tags']:
        if tag['Key'] == 'Name':
            name = tag['Value']

        print("{} {} {} {} {} {} {}".format(
                obj['Instances'][0]['InstanceId'],
                name,
                obj['Instances'][0]['InstanceType'],
                obj['Instances'][0].get('PrivateIpAddress'),
                obj['Instances'][0].get('PublicIpAddress'),
                obj['Instances'][0]['State']['Name'],
                obj['Instances'][0]['Placement']['AvailabilityZone'],
        ))

아래는 출력 결과다. 이제 이 내용을 구글 시트로 생성해보자.

i-0d6d659b8e7745462 test-ec2-02 t3a.nano 172.31.8.172 3.34.95.108 running ap-northeast-2a
i-0735e4c70bd06ed42 test-ec2-01 t3a.nano 172.31.7.145 3.38.96.11 running ap-northeast-2a

구글 시트 API

API 이용을 위해 우선 아래 과정을 통해 API 이용 권한을 획득하자.

구글 프로젝트 생성

  1. 구글 Console 페이지에 접속하여 새로운 프로젝트를 생성한다. google console main page google console main page google console main page

  2. 구글 API Library 페이지에 접속 후 google sheets를 검색한다. 이 때 위에서 생성한 프로젝트가 선택된 상태인지 체크한다.

    google api library main page google api library main page google api library main page

  3. API 사용을 위한 Credentials을 생성한다.

    google api credential creation screen google api credential creation screen google api credential creation screen google api credential creation screen google api credential creation screen google api credential creation screen google api credential creation screen

    다운로드한 파일은 잠시 후 사용해야 하니 잘 보관할 것.

    google api credential creation screen

  4. 현재 이 API는 Testing 단계이다. PUBLISH APP 버튼을 눌러 Production 단계로 변경할 수 있는데 절차가 복잡하고 시간도 2~3일 더 필요하다. (구글에서 인증을 해줘야 한다.) google api status google api publish

  5. 이 App은 나만 사용할 것이므로 Testing 단계로도 충분하다. 단, Testing 단계에서는 Test users 목록에 등록되어 있는 구글 계정들만 해당 API를 사용할 수 있으므로 본인의 구글 계정을 Test users 목록에 추가하자.

    google api add test user

자. 이제 API를 사용할 준비를 마쳤다. 이제 본격적으로 코드를 작성해보자. (API 코드 작성보다 API 사용 준비하는 과정이 더 복잡해 보이는 이유는 단지 느낌인 걸까…)

구글 시트 API 사용 전 알아둘 내용

코드 작성 전에 구글 시트에서 사용하는 용어와 URL 규칙에 대해 알고 가면 더 좋다.

  • Spreadsheet : 구글 시트 문서를 의미한다. 엑셀로 비유하면 엑셀파일이라고 보면 된다.
  • Sheet : 구글 시트 하단에 표기되는 탭 각각을 의미한다.
  • Cell : 각 칸을 의미한다.
  • A1 notation
    • 구글 시트의 범위를 나타내는 표현식이다.
    • 예)
      • Sheet1!A1:B2 : 이름이 Sheet1인 시트의 A1,A2,B1,B2 Cell
      • Sheet1!A:A : 이름이 Sheet1인 시트의 A열 전체
      • Sheet1!1:2 : 이름이 Sheet1인 시트의 1,2 번째 행 전체

URL은 아래와 같이 생겼는데 Spreadsheet IDSheet ID가 포함되어 있다. 이 ID값은 파이썬 코드상에서 사용된다.

https://docs.google.com/spreadsheets/d/spreadsheetId/edit#gid=sheetId

예를 들어, 아래와 같은 URL이 있다고 할때 SpreadsheetIdSheetId는 각각 다음과 같다.

https://docs.google.com/spreadsheets/d/1xuAfx7X9g2Nd5DikD-sLM4iA_lk45H0AKYJ0BS0sqfA/edit#gid=0
  • SpreadsheetId : 1xuAfx7X9g2Nd5DikD-sLM4iA_lk45H0AKYJ0BS0sqfA
  • SheetId : 0
    • Spreadsheet를 생성할 때 기본으로 생성되는 sheet의 ID는 항상 0이다.
    • 추가 생성되는 sheet의 ID는 꽤 큰 숫자값이다. 예) 1784376386

구글 시트 API 활용한 파이썬 코드

  1. 우선 늘 그렇듯 설치부터 하자. 아래 명령어를 터미널에서 입력한다.

    pip install --upgrade google-api-python-client \
                        google-auth-httplib2 \
                        google-auth-oauthlib
    
  2. 이전 과정에서 다운로드 받은 client_secret_xxxxxxxxxxxx.json 파일의 이름을 credentials.json으로 변경한다.

  3. 아래 코드를 붙여넣는다.

    import os
    
    from googleapiclient.discovery import build
    from google_auth_oauthlib.flow import InstalledAppFlow
    from google.auth.transport.requests import Request
    from google.oauth2.credentials import Credentials
    
    SCOPES = ['https://www.googleapis.com/auth/spreadsheets']
    
    # 아래 코드는 구글 공식 페이지에서 가져왔다.
    # https://developers.google.com/sheets/api/quickstart/python
    def init_token():
        creds = None
        if os.path.exists('token.json'):
            creds = Credentials.from_authorized_user_file('token.json', SCOPES)
    
        if not creds or not creds.valid:
            if creds and creds.expired and creds.refresh_token:
                creds.refresh(Request())
            else:
                flow = InstalledAppFlow.from_client_secrets_file(
                    'credentials.json', SCOPES)
                creds = flow.run_local_server(port=0)
    
            with open('token.json', 'w') as token:
                token.write(creds.to_json())
        return creds
    
    
    service = build('sheets', 'v4', credentials=init_token())
    
    # 구글 시트에 입력할 값들
    # 리스트 안에 리스트 형태로 표현하며, 내부 리스트가 각 행을 의미한다.
    values = [
        ['ID', 'Name', 'Type', 'PrivateIP', 'PublicIP', 'State', 'AZ'],
    ]
    
    # 새로운 Spreadsheet 생성 방법
    # spreadsheet = {
    #     'properties': {
    #         'title': '새 시트'
    #     }
    # }
    # sheet = service.spreadsheets().create(body=spreadsheet).execute()
    # SPREADSHEET_ID = sheet.get('spreadsheetId')
    
    # 이 예제에서는 이미 생성되어 있는 문서의 spreadsheetId 값을 이용하였다.
    # 기존에 생성되어 있던 문서의 spreadsheetId를 확인하는 방법은 위에 이미 설명했다.
    SPREADSHEET_ID = '1xuAfx7X9g2Nd5DikD-sLM4iA_lk45H0AKYJ0BS0sqfA'
    
    # Sheet1 이라는 이름의 sheet에 값을 업데이트 하였다.
    service.spreadsheets().values().update(
                    spreadsheetId=SPREADSHEET_ID,
                    range="Sheet1!A1:Z{}".format(len(values)),
                    valueInputOption='RAW',
                    body={'values': values}
    ).execute()
    
  4. 위 코드를 최초 실행 시 아래와 같이 브라우저가 자동으로 실행되며 어디서 많이 본 화면이 뜬다. (구글계정으로 로그인하기를 해본적이 있다면, 해당 절차와 동일하다고 생각하면 된다.) 이 절차는 로컬에서만 가능하기 때문에 원격으로 접속한 리눅스 서버에서 이 코드를 실행시키면 에러가 발생한다. 원격에서 실행하고 싶으면 로컬에서 우선 한번 실행한 후, 그 뒤에 생성된 token.json 파일을 원격 서버로 복사한 후 실행하자.

    google api credential creation screen google api credential creation screen google api credential creation screen google api credential creation screen

  5. 성공이다. 이제 AWS 코드와 구글 시트 코드를 하나로 합쳐보자.

    google sheet test screen

AWS + 구글 시트 API

일단 코드부터 보자.

import os

import boto3
from botocore.config import Config

from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials

AWS_ACCESS_KEY='AKIAYAMQOPDIXEGGW5PB'
AWS_SECRET_KEY='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'

client = boto3.client('ec2',
                      aws_access_key_id=AWS_ACCESS_KEY,
                      aws_secret_access_key=AWS_SECRET_KEY,
                      config=Config(region_name='ap-northeast-2'),
                     )

# running 상태인 ec2 인스턴스 목록 얻기
response = client.describe_instances(
    Filters=[
            {'Name': 'instance-state-name', 'Values': ['running']}
        ]
    )

ec2_list = []
for obj in response['Reservations']:
    for tag in obj['Instances'][0]['Tags']:
        if tag['Key'] == 'Name':
            name = tag['Value']

        ec2_list.append([
            obj['Instances'][0]['InstanceId'],
            name,
            obj['Instances'][0]['InstanceType'],
            obj['Instances'][0].get('PrivateIpAddress'),
            obj['Instances'][0].get('PublicIpAddress'),
            obj['Instances'][0]['State']['Name'],
            obj['Instances'][0]['Placement']['AvailabilityZone'],
        ])

SCOPES = ['https://www.googleapis.com/auth/spreadsheets']

def init_token():
    creds = None
    if os.path.exists('token.json'):
        creds = Credentials.from_authorized_user_file('token.json', SCOPES)

    if not creds or not creds.valid:
        if creds and creds.expired and creds.refresh_token:
            creds.refresh(Request())
        else:
            flow = InstalledAppFlow.from_client_secrets_file(
                'credentials.json', SCOPES)
            creds = flow.run_local_server(port=0)

        with open('token.json', 'w') as token:
            token.write(creds.to_json())
    return creds


service = build('sheets', 'v4', credentials=init_token())
values = [
    ['ID', 'Name', 'Type', 'PrivateIP', 'PublicIP', 'State', 'AZ'],
]

values.extend(ec2_list)

SPREADSHEET_ID = '1HLpvoAc2Cntxy1bMBu0ZaxTbmhz5Z7Ek_uH4K9RTiRE'

service.spreadsheets().values().update(
                spreadsheetId=SPREADSHEET_ID,
                range="Sheet1!A1:G{}".format(len(values)),
                valueInputOption='RAW',
                body={'values': values}
).execute()

결과는… 두둥! 성공이다.

aws ec2 to google sheet

추가 기능들

위 예제는 아주 기본적인 내용만을 다루고 있는데, 필자의 경우 아래와 같은 상황이라 조금 더 내용을 추가하였다.

aws 계정별/지역별 접속 방법

사용 중인 AWS Root 계정이 n개이고 각 계정의 모든 region에 대한 전수 조사가 필요하다. 이럴 때는 아래와 같이 가능하다. 사용자 홈 폴더/.aws/ 폴더 하위에 config, credentials 파일을 만들고 아래와 같이 파일 내용을 적어둔다. (사실 awscli를 설치 후 config 설정을 하면 아래 파일들이 자동으로 생성된다. 자세한 내용은 이 글을 참고하자.)

  • ~/.aws/config

    [default]
    region = ap-northeast-2
    output = json
    
    [profile dev]
    region = ap-northeast-2
    output = json
    
    [profile prod]
    region = ap-northeast-2
    output = json
    
  • ~/.aws/credentials : 위 config 파일에 작성한 profile 이름과 해당 profile의 access key,secret access key 정보를 적는다.

    [default]
    aws_access_key_id = AAAAAAAAAPDIV4INFU65
    aws_secret_access_key = 9x3Yvt88+OmSSSSSSSSSSSSSSSSS
    
    [dev]
    aws_access_key_id = AAAAAAAAAL4246XEJPVU
    aws_secret_access_key = zpY22349347SSSSSSSSSSSSSSSSS
    
    [prod]
    aws_access_key_id = AAAAAAAAA2323YKSSPP
    aws_secret_access_key = 135-asgasrSSSSSSSSSSSSSSSSSS
    

그리고 코드에서 아래와 같이 사용한다.

REGION = [
    "us-east-2",
    "us-east-1",
    "us-west-1",
    "us-west-2",
    "sa-east-1",
    "ap-northeast-2",
    "ap-south-1",
    "ap-northeast-3",
    "ap-southeast-1",
    "ap-southeast-2",
    "ap-northeast-1",
    "ca-central-1",
    "eu-central-1",
    "eu-west-1",
    "eu-west-2",
    "eu-west-3",
    "eu-north-1",
]
profile_list = ['dev', 'prod']

for profile in profile_list:
    for region in REGION:
        session = boto3.session.Session(
            profile_name=profile,
            region_name=region
            )
        client = session.client('ec2')
        response = client.describe_instances()

sheet 추가 및 이름 변경

구글 시트 생성 시 default sheet 이름은 Sheet1 이다. 이 이름을 변경하는 방법과 sheet를 추가하는 방법은 아래 코드를 참조하자.

service.spreadsheets().batchUpdate(spreadsheetId=SPREADSHEET_ID, body={
    'requests': [
        {
            'updateSheetProperties': {
                'properties': {
                    'title': 'EC2'
                },
                'fields': 'title'
            }
        },
        {
            'addSheet': {
                'properties': {
                    'title': 'RDS'
                }
            }
        }
    ]
}).execute()

마치며

코드는 얼마 안되는데, 코드를 사용하기 위한 준비과정이 훨씬 길었다… 구글 API 등록은 이제 눈감고도 할 것 같…