query 객체에는 filter 메소드가 있다. SQL로 따지면 WHERE clause의 역할이다. filter 메소드에 expression을 전달하면, 새로운 query 객체를 반환한다. 아래 예제는 scoped session이 적용되어 있고, TblUsers라는 ORM 클래스가 적당히 존재한다고 가정한다.

조건 결합, 추가적인 관계 연산자

query 객체의 filter 메소드에 sqlalchemy.sql 하위 함수, 또는 ORM 클래스 필드의 SQL expression 메소드를 전달하여 조건식을 결합하거나, 일반적인 관계 연산자로 표현하기 힘든 IN, BETWEEN 등의 조건식을 표현할 수 있다.

SQLAlchemy의 query 객체가 메소드 형식으로 WHERE절을 빌드하는 컨셉만 알고 있도록 하자. 나중에 필요한 쿼리가 있을 때 구글링해서 찾아보기 더 편하다.

sqlalchemy.sql.text는 raw string 형태의 SQL을 Connection.execute나 그에 상응하는 함수(engine.execute, session.execute)에 전달하여 쿼리하기 위한 함수다.

placeholder 기능을 지원하며, :[name] 포맷을 사용하고 키워드 인자로 이들에 값을 채운다.

되도록이면 ORM을 쓰자. 개인적으로는 ORM만 충실히 써도 어플리케이션 코드와 데이터베이스 간의 결합도를 많이 낮출 수 있었던 것 같다.

boto3에서 거의 대부분의 기능은 AWS API를 사용하고, 자원에 대한 자격을 증명하기 위해 AWS IAM에서 얻어낼 수 있는 AWS access key IDAWS secret access key, 또는 임시 자격 증명을 위한 aws_session_token을 사용한다. 일반적으로 awscli 등의 도구를 통해 ~/.aws/credentials에 자격 증명 데이터를 설정하고, boto3는 여기에 접근해 자격 증명을 진행한다. 그러나 boto3를 사용하는 어플리케이션에서 여러 개의 자격 증명을 동시에 수행해야 하는 경우가 생길 수 있다. 데이터를 수집하여 원하는 곳으로 서빙해주는 B2B 서비스인 Segment의 Amazon S3 Integration 기능을 예로 들 수 있다.

다행히도, boto3는 자격 증명 데이터를 얻어낼 때까지 여기저기에 순서대로 접근한다.

client, resource 함수에서 자격 증명 데이터를 매개변수로 전달

client, resource, Session 등을 얻을 때 aws_access_key_id, aws_secret_access_key, aws_session_token이라는 문자열 타입의 선택 인자를 사용할 수 있다. 시스템 설정을 무시하고, 자격 증명 데이터를 직접 전달하고 싶을 때 대부분 이 방법을 사용한다.

환경 변수

client, resource, Session에 자격 증명 정보가 인자로 전달되지 않으면, 환경 변수를 확인한다.

AWS_ACCESS_KEY_ID

AWS 계정의 access key

AWS_SECRET_ACCESS_KEY

AWS 계정의 secret key

AWS_SESSION_TOKEN

AWS 계정의 세션 키

공유 자격 증명(Shared credentials) 파일

그 다음부턴 파일들에 접근하기 시작한다. 공유 자격 증명 파일의 기본 위치는 ~/.aws/credentials이고, AWS_SHARED_CREDENTIALS_FILE 환경 변수를 설정하여 위치를 변경할 수 있다. credentials는 .ini 형식의 파일이고, 각 섹션마다 자격 증명을 위한 세 가지의 변수를 지정할 수 있다.

섹션 각각을 profile이라고 부른다. session을 생성할 때 AWS_PROFILE 환경 변수를 설정하거나 profile_name 인자에 이름을 전달하여 어떤 profile을 사용할 지 명시할 수 있다. 여기서 session은 client나 resource를 생성할 수 있도록 자격 증명에 대한 '상태'를 저장하고 있는 객체다.

별도로 명시하지 않으면, [default] profile을 사용한다. 보통은 이렇게 credentials를 사용하는 방식을 쓰는 것 같다.

AWS Config 파일

기본 위치는 ~/.aws/config이고, AWS_CONFIG_FILE 환경 변수를 설정하여 위치를 변경할 수 있다. config 파일은 credentials 파일과 동일하게 작성된다. 유일한 차이점은, profile 섹션을 [profile [name]] 포맷으로 표현한다는 것이다.

AssumeRole을 호출시키기

AWS_CONFIG_FILE 환경 변수에 명시된 파일이나, 기본 경로인 ~/.aws/config 파일에서 role에 대한 정보를 명시하여, boto3가 자동으로 AWS STS에 AssumeRole을 호출하도록 할수도 있다. role_arn, source_profile, role_session_name 등의 파라미터가 필요하다.

boto2 config

그 후에는, boto2 config 파일에서 자격 증명을 로드한다. BOTO_CONFIG 환경 변수가 나타내는 경로의 파일을 먼저 검사하고, 그렇지 않으면 /etc/boto.cfg~/.boto 파일을 검사한다. [Credentials] 섹션만 사용한다.

IAM Role

Amazon EC2에서 실행 중이며 위의 방법들로 자격 증명을 찾지 못한 경우, boto3는 EC2 인스턴스의 메타 데이터 서비스에서 자격 증명을 로드한다. EC2 인스턴스를 시작할 때 사용할 IAM role만 지정하면, 별도의 명시적인 구성을 하지 않아도 boto3가 알아서 인스턴스의 자격 증명을 사용한다.

Best Practice

boto3는 자격 증명 정보를 얻기 위해, 결론적으로 아래의 순서를 따른다.

  1. client/resource/Session에 전달되는 자격 증명 정보
  2. 환경 변수
  3. AWS_SHARED_CREDENTIALS_FILE에 명시된 파일 또는 ~/.aws/credentials에 접근
  4. AWS_CONFIG_FILE에 명시된 파일 또는 ~/.aws/config에 접근
  5. AWS_CONFIG_FILE에 명시된 파일 또는 ~/.aws/config에 접근하여 role 데이터(arn 등)로 AWS STS에 AssumeRole
  6. BOTO_CONFIG에 명시된 파일에 접근
  7. /etc/boto.cfg에 접근
  8. ~/.boto에 접근
  9. EC2 인스턴스의 IAM role 설정에 접근

EC2 인스턴스에서 boto3를 사용하는 경우 IAM role을 사용하는 것이 좋고, 그렇지 않은 경우라면 공유 자격 증명 파일(~/.aws/credentials)을 쓰는 것이 좋다고 한다.

'Python 계열 > boto3' 카테고리의 다른 글

boto3.resource와 boto3.client의 차이  (1) 2019.02.12
S3에서 객체의 용량을 알아내기  (0) 2019.02.12

이번엔 서비스 운영을 위한 메인 데이터베이스를 결정하고, AWS 클라우드 위에서 해당 데이터베이스 엔진을 사용하는 인스턴스를 하나 띄워보자. 가상 컴퓨팅 환경 하나 단위를 AWS에서는 인스턴스라고 부른다.

도입 이유

데이터베이스는 엑셀을 떠올리면 된다. 데이터베이스의 가장 중요한 특성은 구조화된 데이터를 관리한다는 점이다. 'ID', '비밀번호'처럼 컬럼에 따라 데이터를 정리하고, 어떤 기준을 통해 데이터를 정렬하고, 필터링할 수 있다. 굳이 엑셀 안 쓰고 데이터베이스를 쓰는 이유는, 데이터베이스라는 것 자체가 데이터를 추가/변경/삭제/조회하는 작업이 더 구조화되어 있고 빠르기 때문이다. 잘 모르겠다면 이고잉님의 강의인 데이터베이스란?을 보고 오자.

이 세상엔 수많은 형태와 종류의 데이터베이스가 있다. 형태로 치자면 RDB(Relational Database)NoSQL(Not only SQL)로 나눌 수 있고, 종류로 치자면 빅데이터 쿼리를 분산 처리하기 위해 개발된 PrestoDB, key-value 형태의 구조로 캐싱 용도로 자주 쓰이는 redis나 memcached, 시계열성 데이터를 저장하는 데에 특화되어 있는 InfluxDB같은 특별한 제품들이나 MySQL, PostgreSQL 등 전형적인 RDBMS가 있을 수 있다.

위에서 이야기했듯 이번 의사결정은 서비스 운영을 위한 메인 데이터베이스를 결정하는 것이라, PrestoDB, druid, InfluxDB같이 특별한 목적을 위해 만들어진 데이터베이스를 사용하진 않을 것이다.

의사결정 - RDB vs NoSQL

배경과 요구사항

  • 모든 스키마가 고정되어 있다.
  • 엄청나게 빠른 속도를 요하진 않는다.
  • 되도록이면 AWS가 관리형으로 제공하는 인프라에서 사용할 수 있어야 한다. EC2에서 데이터베이스 서버를 직접 관리해야 하는 일이 없도록 만들고 싶다.

선택지

  • RDB
  • NoSQL

의사결정

RDB를 선택하겠다. 그 이유는,

  • 'NoSQL은 schemaless여서 더욱 유연한 형태로 데이터를 저장할 수 있다'고 하지만, 우리가 개발하려는 것을 포함해 대부분의 서비스는 스키마가 유동적인 경우가 거의 없다.
  • NoSQL은 'scale out(접속된 서버의 대수를 늘려 처리 능력을 향상시키는 것)이 가능해서 scale up만 가능한 RDB에 비해 단일 장애 지점도 없고 비용도 적고 클러스터에서 잘 동작하도록 만들어져 있다'라며 퍼포먼스와 장애 대응에 관한 메리트를 어필하곤 하는데, RDB에서도 master-slave 모델을 사용하면 이런 문제들을 충분히 커버할 수 있다. 이는 읽기 전용 인스턴스(slave)를 만들어 데이터를 복제시키는 것인데, 이정도만 해도 웬만한 수준의 서비스 트래픽을 다 감당할 수 있다고 판단했다. master가 죽은 경우 slave가 master로 승격하는 구조를 만들면 장애 복구도 잘 되고 말이다.
  • 평균적으로 NoSQL이 RDB보다 빠른 것이 맞고, 동일한 비용을 사용했을 때 퍼포먼스로 치면 웬만하면 NoSQL이 가성비가 좋다. 그러나 이건 우리가 어떤 RDB 제품을 사용하느냐에 따라 다르다.
  • NoSQL 데이터베이스 서버를 AWS에서 운영하려면, 대부분의 경우 EC2 위에서 직접 돌려줘야 한다. MongoDB의 경우 최근에 출시된 DocumentDB를 사용해볼 수 있는데, 아시아 쪽에선 아직 서비스가 오픈되지 않았다. 결국은 관리 포인트가 늘어날 것이며 이를 감당하기 어려운 상태다.
  • 나도 NoSQL을 좋아하지만, '많이 써봤으니 쓰기 편해서'라는 이유밖에 얘기하지 못 하겠다.

의사결정 - 데이터베이스 호스팅 위치

배경과 요구사항

  • 우리는 RDB를 사용하기로 했다.
  • EC2에서 직접 데이터베이스 서버를 관리하는 것은 설정, 패치, 백업과 같은 시간 소모적인 관리 작업이 많아지며 안정성을 보장하기 어렵다. 따라서 AWS에 인프라 관리를 양도하는 관리형 서비스가 있다면 이를 사용하고자 한다.

선택지

데이터베이스 서버 관리를 직접 하느냐, AWS에게 관리를 양도하느냐의 차이다.

  • EC2
  • RDS(Relational Database Service)

의사결정

RDS를 선택하겠다. 그 이유는,

  • 동일한 워크로드(하드웨어 요구량)를 기준으로 했을 때 EC2의 비용이 더 적을 수는 있겠으나, 데이터베이스 서버를 직접 운영하는 것은 정말 큰 관리 포인트다. 백업과 복구 정도만 생각해도 아득하다. 이런 건 그냥 돈 좀 더 내는 게 낫다고 생각한다(물론 여기선 어차피 free tier를 쓸테니 이런게 딱히 상관은 없겠지만, 의사결정의 이유니까 알아두고 넘어가자).
  • 관리형 서비스가 없는 데이터베이스를 선택한 경우 어쩔수 없이 EC2에 올려야겠지만, RDS는 대부분의 메이저한 RDB 엔진을 지원하며 그 외의 RDB 중에서는 굳이 EC2에서 직접 프로비저닝할 정도의 메리트가 있는 것도 딱히 없다.

의사결정 - 데이터베이스 엔진

배경과 요구사항

  • 우리는 Amazon RDS를 사용하기로 했으므로, RDS에서 사용 가능한 데이터베이스 엔진을 선택지로 두어야 한다.
  • 조직이 MySQL에 익숙한 상태다.
  • Python에서 주로 쓰는 ORM 라이브러리인 SQLAlchemy나 Peewee에서 지원하는 데이터베이스여야 한다.
  • 서비스 초기이므로, 한동안이라도 비용이 발생하지 않는 방향이면 더 좋다.

선택지

  • Aurora
  • MySQL
  • MariaDB
  • PostgreSQL
  • Oracle
  • SQL Server

의사결정

MySQL을 선택하고, 추후 Aurora로 마이그레이션하겠다. 그 이유는,

  • Aurora는 AWS가 개발한 RDBMS인데, 클라우드에서 잘 구동되도록 설계되어 비교적 빠르고 비용도 싸다. 하지만 다시 역으로 비용 문제가 있는데, Aurora를 RDS에 띄우는 경우 free tier가 지원되지 않는다.
  • 조직이 MySQL에 익숙하며, 메인 데이터베이스로 MySQL을 사용하는 것에 딱히 걸리는 점이 없다.
  • 어차피 AWS 가입 후 1년이 지나면 free tier가 지원되지 않으므로 비용 효율이 좋은 엔진으로 마이그레이션을 고려하게 될텐데, Aurora는 MySQL과 호환되며 MySQL보다 3~4배 정도 빠르므로 그 대상이 된다. 사실 Amazon 인프라에선 Aurora만한 RDB가 없는데 free tier 때문에 잠시 동안만 MySQL에 머물러 있는 것이라고 생각하면 된다.

작업 - 인스턴스 시작

Amazon RDS 자습서를 보고 따라하자.

SQLAlchemy에서 ORM을 사용하기 위해, 스키마를 class로 정의하는 방법이 있다.

sqlalchemy.declarative_base 함수를 통해 Base를 얻고, 이를 상속받는 형태로 모델을 구현한다. 클래스의 내부에는 __tablename__이라는 클래스 필드로 테이블 이름을 명시하며, Column 클래스의 생성자에 sqlalchemy.types 모듈 하위에 구현되어 있는, 타입을 나타내는 클래스를 넘겨 Column 타입의 객체들로 컬럼을 명시한다. 타입 정보를 생성자에 전달할 때, 클래스 자체를 넘겨도 되고 클래스의 인스턴스를 넘겨도 된다. Column(Integer)Column(Integer())가 동일하다는 것이다. sqlalchemy의 타입 클래스, SQL DDL에서의 타입, Python의 빌트인 타입 간 매핑은 다음과 같다. 자주 사용하는 타입 위주로 작성했다.

  • sqlalchemy.types.BigInteger - BIGINT - int
  • sqlalchemy.types.SmallInteger - SMALLINT - int
  • sqlalchemy.types.Integer - INT - int
  • sqlalchemy.types.Boolean - BOOLEAN or SMALLINT or TINYINT - bool
  • sqlalchemy.types.DateTime - TIME or TIMESTAMP - datetime.datetime
  • sqlalchemy.types.Float : FLOAT or REAL - float | asdecimal 인자를 True로 설정하면, decimal.Decimal 타입으로 다뤄진다.
  • sqlalchemy.types.PickleType - BLOB or TINYBLOB or MEDIUMBLOB or LONGBLOB - pickle로 직렬화 가능한 모든 객체 | list나 dict를 관리할 때 편리하게 사용할 수 있다.
  • sqlalchemy.types.String - CHAR or VARCHAR or TEXT - str
  • sqlalchemy.types.Text - CLOB or TEXT - str

BigInteger와 BIGINT, Boolean과 BOOLEAN, Text와 TEXT, ...

SQLAlchemy로 모델을 정의하다 보면, uppercase된 알파벳으로만 정의된 타입을 확인할 수 있다. SQLAlchemy 문서에서는 BigInteger, Boolean, Text 등을 Generic Type이라 부르고, BIGINT, BOOLEAN, TEXT 등을 SQL Standard and Multiple Vendor Type이라고 부른다.

Generic Type

BigInteger, Boolean, Text, DateTime, Float, Integer, ...

'Python 타입'의 데이터를 읽고, 쓰고, 저장할 수 있는 column을 명시한다. SQLAlchemy는 Generic Type의 컬럼에 대해 CREATE TABLE문을 만들어낼 때, 대상 데이터베이스에서 사용할 수 있는 최상의 타입을 알아서 선택한다. VARCHAR로 정의해 뒀는데 데이터베이스가 VARCHAR를 지원하지 않는다면, 그를 대체할 수 있는 타입으로 변경하여 DDL을 작성한다. 대충 써두면, 알아서 테이블을 생성해준다는 의미다. 코드 레벨에서 조금 더 편하다.

SQL Standard and Multiple Vendor Type

BIGINT, BOOLEAN, TEXT, BLOB, CHAR, CLOB, ...

SQLAlchemy에서 여기에 속하는 타입의 컬럼을 통해 CREATE TABLE문을 만들 때는, 데이터베이스 엔진에 상관 없이 컬럼의 타입을 항상 동일하게 만들어낸다. SQLAlchemy가 원하는 타입으로 정확한 DDL을 작성하도록 만들 때 유용할 수 있으나, Generic type과 달리 이들은 모든 데이터베이스에서 잘 작동한다는 보장이 없다. 따라서, 대충 'VARCHAR'라고 했다가 데이터베이스가 VARCHAR를 지원하지 않으면 DDL에 실패한다.

Vendor-Specific Type

mysql.BIGINT, mysql.BINARY, postgresql.REAL, oracle.VARCHAR2, ...

'벤더별로 명시되어 있는 타입'을 의미하는데, 여기서 벤더는 MySQL, PostgreSQL 등을 떠올리면 된다. sqlalchemy.dialects 패키지 하위에 데이터베이스별로 패키지가 존재하고, 여기에 명시되어 있는 타입을 사용하는 방식이다.


'Python 계열 > SQLAlchemy' 카테고리의 다른 글

Query 객체의 WHERE절 작성  (0) 2019.02.12
text()  (0) 2019.02.12
Column.like, Column.ilike, not_, ~expr  (0) 2019.02.12
특정 컬럼만 SELECT  (0) 2019.02.12
aliasing과 함수  (0) 2019.02.12

SQLAlchemy에서 query 객체의 filter 메소드에 전달하는 관계 연산자는 Python에 내장된 관계 연산 기호(<, >, == 등)나, Column 객체에서 지원하는 메소드(Column.between, Column.in_ 등)를 사용할 수 있었다. SQLAlchemy의 query 객체의 filter 메소드를 통해 LIKEILIKE(case sensitive LIKE) 쿼리를 표현하려면, 각각 Column.likeColumn.ilike 메소드를 사용할 수 있다.

not

NOT을 표현하는 방법은, and와 or이 각각 sqlalchemy.and_, sqlalchemy.or_로 지원되었던 것을 생각하면 된다. sqlalchemy.not_ 함수를 사용한다.

또는, 단항 비트 반전을 위해 한번쯤 사용했던 unary operator(~)를 사용할 수 있다.

'Python 계열 > SQLAlchemy' 카테고리의 다른 글

text()  (0) 2019.02.12
모델 정의와 Generic Type vs SQL Standard/Multiple Vendor Type vs Vendor-Specific Type  (0) 2019.02.12
특정 컬럼만 SELECT  (0) 2019.02.12
aliasing과 함수  (0) 2019.02.12
limit  (0) 2019.02.12

GROUP BY [expr](, expr...) 쿼리는 SELECT 문의 질의 결과를 expr을 기준으로 묶어 새로운 row들을 반환하는 역할을 한다. 이는 보통 집계 함수와 함께 쓰이는데, 예를 들면 아래와 같다.

tbl_2nd_grade라는 테이블이 2학년 학생들의 정보를 관리하고, 여기에는 '반''키', '몸무게'가 각각 class, height, weight라는 이름의 컬럼에 들어가 있다고 가정한다. 위 쿼리는 GROUP BY class로 인해 row들이 class를 기준으로 묶여 class마다 '반별 학생들의 키와 몸무게 평균'을 결과로 반환하게 된다. GROUP BY 없이 어플리케이션 레벨에서 이러한 요구사항을 만족시키려면, class를 key로 갖는 dictionary에 학생 수와 함께 키, 몸무게를 누적시킨 후 평균을 계산하는 로직을 작성해야할 것이다.

GROUP BY가 포함된 SELECT 문은 그룹마다 하나의 행을 반환한다. 예를 들어 class가 1, 2, 3만 있다면 3개의 행이 반환될 것이고, 1부터 10까지 있다면 10개의 행이 반환된다. GROUP BY가 포함된 쿼리를 작성할 때 인지하면 좋은 것은, GROUP BY 절에 명시한 expression이 아니라면 SELECT 절에 metric을 명시할 때 집계 함수 등을 통해 row가 하나만 등장하도록 만들어야 한다는 것이다. 예를 들면,

위의 예제는 유효한 쿼리지만, 아래의 예제는 유효하지 않다. grade를 기준으로 GROUP BY 되었기에 height와 weight는 AVG 함수에 전달되어 알아서 집계되어 group마다 하나의 값만을 가질텐데, student_number는 group에 따라 여러 개의 값을 가질 수 있기 때문이다. 논리적으로 맞지 않는 것이다.

GROUP BY 써먹기

사실 웬만하면 GROUP BY는 유용하다. 내 경우에는 수만~수십만 row가 넘는 데이터를 특별한 기준으로 나누어 집계하는 쿼리를 최적화할 때 GROUP BY의 유용함을 느낄 수 있었다. 내가 처했던 상황을 조금 간단한 예로 표현한다면, 10만 개가 넘는 학생별 시험 성적 데이터가 row마다 학번, 수학 점수, 과학 점수, ... 형태로 이루어져 있다고 칠 때, 과목별로 5점 단위마다 학생 수를 count해야 하는 일이었다. '수학 0~4점에 몇명, 5~9점에 몇명, ..., 95~99점에 몇명, 100점에 몇명' 하는 식의 데이터를 만들어야 하는 것이다. 처음 했던 것은, 과목을 기준으로 GROUP BY하고 score에 대해 CASE WHEN을 사용하는 것이었다.

결과는 적당히 잘 나오지만, 쿼리 시간은 SELECT절에 포함시키는 metric의 수에 비례하여 늘어난다는 것을 생각하면 확실히 좋기만 한 쿼리는 아니다. 만점이 더 높다면 그만큼 쿼리 시간이 더 늘어날 것이고, 피겨 스케이팅처럼 만점이 따로 없는 경우를 생각한다면 그렇게 유연한 쿼리도 아니다. GROUP BY에 단지 column name이 아니라 expression을 사용할 수 있다는 것을 응용하면 더 나은 쿼리를 작성할 수 있다.

subject와 score를 5로 나눠 소수점을 버리는 floor(score / 5)를 기준으로 group이 만들어질테니, 수학 0~4점은 math, 0, 13, 수학 5~9점은 math, 1, 3, ..., 수학 100점은 math, 100, 17 이런 식으로 각 row가 구성될 것이다. 이러한 쿼리가 빠른 이유는, row들을 iteration하는 for문 안에 수많은 if 절이 있는 것dictionary에 값을 누적하는 것 중 무엇이 빠른지를 생각하면 된다.

추가적으로, GROUP BY는 결과를 '분산'시키는 것에도 유용하다. 예를 들어 SELECT AVG(score) FROM tbl_score_2018_02_mid;모든 학생의 전체 과목에 대한 평균 점수를 집계할 수 있을텐데, 이걸 학생 단위로 결과를 분산시키기 위해 SELECT AVG(score) FROM tbl_score_2018_02_mid GROUP BY student_number;처럼 표현할수도 있다는 것이다.

'데이터베이스 > SQL' 카테고리의 다른 글

Literal SELECT  (0) 2019.02.12
집계 함수와 조건식을 함께 사용하기(SELECT FROM SELECT)  (0) 2019.02.12

이번 주제는 배포 자동화다. 원래 CI(Continuous Integration)CD(Continuous Deployment)같은 것들을 이야기해보려 했으나, 배포 자동화라는 용어가 덜 추상적이고 더 명시적이라 용어 선택을 선회했다.

도입 이유

배포라는 과정은 불필요한 반복 작업이다. 우리가 만들고 있는 웹 어플리케이션의 경우, 기능 추가나 버그 패치 등 어떤 단일 작업 하나를 완료한 후 → 테스트를 돌리고 → 문제가 없다면 배포한다. 우리의 상황에 맞추자면,

  1. 게시글 작성 API를 작업하고
  2. 이에 대응되는 테스트를 작성한 후
  3. 테스트를 돌리고
  4. 테스트가 모두 성공하면 zappa update 커맨드를 실행해 새로운 버전의 코드를 배포한다.

1, 2번은 분명히 개발자의 작업이 필요한 게 확실한데, 3, 4번에 해당하는 배포 과정은 자동화시킬 여지가 있다. 그 이유는,

  • '작업의 완료''master 브랜치에 대한 push'로 판단할 수 있다. 어떤 방법으로든 push 이벤트를 다른 곳에 알려주는 기능을 GitHub이 지원하기만 한다면, 코딩 후의 작업을 시스템적으로 처리할 수 있게 된다.
  • 테스트를 돌리고, 문제가 없는지를 판단하고, 배포하는 것은 결국 명령어의 집합일 뿐이다. 명령어를 대신 실행해주는 뭔가가 있다면, 필요한 명령어를 정리해 두기만 하면 된다.

그럼 명령어를 대신 실행해주는 뭔가가 있다고 치고 push가 일어나면 알아서 명령어를 실행하도록 설정해 두었다고 하면, 작업-배포 흐름이 아래처럼 바뀔 것이다.

  1. 게시글 작성 API를 작업하고
  2. 이에 대응되는 테스트를 작성한 후
  3. master 브랜치에 push하면 테스트랑 배포를 누군가가 대신 해준다.

이에 대한 흐름을 시스템적으로 조금 더 이야기하자면,

  • master 브랜치에 대한 push같이, GitHub 내에서 특정 이벤트가 일어났을 때 특정 URL로 HTTP 요청을 하게 만드는 webhook이라는 기능이 있다. 예를 들면, push가 일어났을 때 GitHub가 자동으로 http://mingyu.io/hook에 POST 요청을 하도록 설정할 수 있다.
  • 배포 자동화 서비스에게 이런 hook URL을 받아서 GitHub 저장소에 push에 대한 webhook을 추가하면, push가 발생할 때마다 대상 hook URL로 HTTP 요청을 보낼 것이다.(서비스에 따라 webhook을 자동으로 추가해주는 경우도 있다.)
  • webhook에 의해 HTTP 요청을 받은 배포 자동화 서비스는 'push가 일어났구나' 하며 저장소를 clone받는다.
  • 배포 자동화 서비스는 우리가 준비해 둔 빌드 스크립트(.travis.yml, circle.yml, buildspec.yml 등)에 따라 명령어들을 실행한다. 빌드 스크립트는 대부분 yml 파일로 작성하고, 동일한 동작을 명시하더라도 그 내용은 서비스마다 조금씩 다르다. 아래는 Travis-CI를 기준으로 작성한 빌드 스크립트인 .travis.yml의 예다.

따라서 이번엔 배포 자동화를 위한 서비스를 선택해 세팅하고, zappa update배포 자동화 시스템에 의해 실행되도록 만들어 보자.

의사결정 - 배포 자동화 서비스

배경과 요구사항

  • private repository에 대해서 사용하는 데에 비용적인 문제가 크게 없어야 한다.
  • Python 런타임이 제공되어야 한다.
  • GitHub과 잘 연동되어야 한다.
  • AWS와 잘 접합되면 좋다.
  • 개발 조직이 Travis-CI를 써본 경험이 많이 있다.

선택지

  • Travis-CI
  • CircleCI
  • Amazon CodeBuild와 CodeDeploy를 함께 사용
  • Amazon CodeBuild만 사용

의사결정

Amazon CodeBuild만 사용하는 것을 선택하겠다. 그 이유는,

  • 개발 조직이 Travis-CI를 많이 써 봤지만, CircleCI나 Travis-CI나 private repository에 붙이려면 유료 플랜을 결제해야 하며 가격이 만만치 않다.
  • 관리 포인트가 여기저기에 분산되어 있으면 관리하기 불편하다. 되도록 AWS 내에서 모두 해결할 수 있도록 하기 위해서다.
  • CodeBuild는 private repository도 무료로 붙일 수 있게 되어 있다. 단지 빌드를 실행한 시간만큼만 지불하면 되는데, 월 100분까지는 무료로 제공되며 이를 넘더라도 과금이 그렇게 많지 않다. 메모리 3GB, 2개의 vCPU를 지원하는 build.general1.small 인스턴스 기준으로 하면 빌드 분당 0.005달러만 지불하면 된다.
  • CodeDeploy를 함께 사용하지 않는 이유는 바로 아래에서 더 설명한다.

CodeDeploy를 사용하지 않는 이유

zappa update라는 건 사실 우리의 웹 어플리케이션을 정말로 lambda에 새로 업데이트하는 배포의 과정이라, '코드 배포 자동화'라는 주제를 가진 CodeDeploy라는 서비스가 더 어울릴 것이라 생각할 수도 있다. 필자도 그렇게 생각했어서 검토를 위해 조금 찾아봤더니, CodeDeploy는 정말 '배포 자체'에 대해 많은 지원을 하고 있었다. 무슨 뜻이냐면,

  • 우리가 그냥 zappa update 커맨드 하나로 패키징부터 업데이트까지 다 끝내니까 배포라는 게 정말 쉬워 보일 수 있겠지만, 배포 과정을 손수 정의하게 된다면 단일 작업이 많이 쪼개져 있어서 그리 쉬운 일이 아니다. 패키징도 해야 하고, 배포가 끝나면 어플리케이션 reload(서버 껐다 켜기)도 해줘야 하고, 서버가 다중화되어 있다면 그들도 함께 배포를 진행해야 한다. CodeDeploy는 일차적으로 '배포를 정의'하는 일을 많이 도와준다.
  • 어플리케이션을 배포하는 동안 가동 중지 시간을 최소화시키는 것도 배포에 있어서 매우 중요한 일이다. 그래서 구 버전과 새로운 버전의 코드를 함께 올려둔 채 트래픽을 나눠 전송하고, 새 버전의 어플리케이션으로 보내는 트래픽의 비율을 점진적으로 증가시키는 Canary 배포나, 구 버전과 새로운 버전의 코드를 함께 올려둔 채 트래픽을 이동시키고 구 버전의 코드를 제거하는 Blue/Green 배포같은 걸 쓰곤 한다. 에러가 생기면 롤백도 하고. 이러한 배포 프로세스들도 손수 정의하기 어려운 일이라 CodeDeploy가 도와준다.

엄청 좋아 보이지만 우리가 CodeDeploy를 사용하지 않는 이유는, CodeDeploy를 써서 얻을 수 있는 메리트가 그리 많지 않을 것이라고 생각했기 때문이다. 위에서도 말했듯 그냥 zappa update 커맨드가 패키징부터 배포까지 알아서 다 해주니 배포 정의에 대해 도움을 받을 이유가 딱히 없다. 게다가 가동 중지 시간 최소화는 lambda 자체에서 알아서 잘 챙겨준다. 현재로선 그냥 명령어를 대신 실행해 줄 주체가 필요할 뿐이다.

작업 - CodeBuild 세팅

새로운 소스 코드로 배포가 이루어지기 전엔 빌드라는 과정을 거친다. 그런데 사실 파이썬은 컴파일 언어도 아니고, 뭐 따로 패키징하고 그런 작업도 없기 때문에 빌드라는 단어는 별로 어울리지 않는 것 같다고 생각했는데, 그냥 '배포 전처리 과정'을 빌드라고 관례적으로 이야기하는 것 같다.

빌드 프로젝트 생성

AWS 콘솔에서 CodeBuild에 들어가, 우측 상단의 '빌드 프로젝트 생성' 버튼을 눌러 내용을 채우자. 총 5개의 메뉴 - 프로젝트 구성 / 소스 / 환경 / Buildspec / 아티팩트에 대해 뭔가 설정하게 되어 있는데, 아래 것들만 채워주면 된다.

  • 프로젝트 구성 - 프로젝트 이름
  • 소스 - '소스 공급자'를 'GitHub'으로
  • 소스 - '리포지토리'를 '내 GitHub 계정의 리포지토리'로 선택하고, GitHub 계정을 연결
  • 소스 - 'GitHub 리포지토리'에서 만들어 두었던 저장소를 선택
  • 환경 - '운영 체제'를 'Ubuntu'로
  • 환경 - '런타임'을 'Python'으로
  • 환경 - '실행 시간 버전'을 'aws/codebuild/python:3.6.5'로

모두 완료했다면, '빌드 프로젝트 생성' 버튼을 눌러 생성을 완료하자.

buildspec.yml 추가

프로젝트 화면에서 '빌드 시작'을 누르고 다른거 건들 거 없이 아래로 내려가 '빌드 시작'을 한 번 더 눌러 보자. 빌드 화면에서 '단계 세부정보'를 보면, DOWNLOAD_SOURCE에서 실패가 일어난 것을 확인할 수 있다.

CodeBuild가 우리 대신 명령어를 실행해 줄텐데, 이게 정리된 파일인 buildspec.yml 파일이 없어서 생긴 문제다. 이 파일을 추가한 뒤, 다시 빌드를 시작하면 에러가 생기지 않을 것이다. 'Build Started'를 콘솔에 찍는 커맨드 하나만 들어 있는 buildspec.yml을 추가했다.

buildspec.yml 추가 직후 스냅샷

빌드를 다시 시작하면, 문제 없이 잘 실행되는 것을 볼 수 있다.

webhook 추가

빌드가 필요할 때마다 손으로 '빌드 시작' 버튼을 클릭하는 것보단, push가 일어났을 때 CodeBuild가 자동으로 가져가서 buildspec.yml에 따라 빌드를 실행하게 만드는 게 더 이상적이다. 그러려면 webhook을 추가해야 하는데, 필자는 CLI를 정말 싫어해서 AWS Console에서 webhook을 설정하는 곳을 찾아보려 했으나 아무리 찾아도 보이지 않았다. 따라서 어쩔 수 없이 CLI를 써야 할텐데, 이건 AWS CodeBuild의 사용 설명서 중 AWS CodeBuild에서 빌드 실행 - 빌드 실행 자동 시작(AWS CLI) 부분을 따라하면 된다. 여기서도 간단히 설명하겠다.

Hello World 서버를 배포했던 9챕터에서, 액세스 권한 유형이 Programmatic access이며 AdministratorAccess 권한을 가진 IAM 사용자를 만들어서 이미 configure를 완료했을 것이므로 바로 본론으로 들어가자. webhook을 추가하려면 aws codebuild create-webhook 명령을 사용하면 된다. 내 CodeBuild 프로젝트의 이름은 Sampleapp_for_blog이므로, aws codebuild create-webhook --project-name Sampleapp_for_blog를 입력했다. url, payloadUrl 같은 데이터가 반환되면 성공이다.

GitHub 저장소에 들어가, Settings - Webhooks 메뉴에 들어가면 webhook이 자동으로 추가된 것을 볼 수 있다.

작업 - zappa update 커맨드 추가하기

이제 push가 일어나면 CodeBuild가 코드를 가져가buildspec.yml에 따라 빌드를 실행하게 만들었다. 이제 그 buildspec.yml에 아래 3개의 커맨드를 추가하면 된다.

  • pipenv install
  • pipenv shell
  • zappa update

이들은 yml 파일의 build → commands 하위에 밀어넣는 것보다, 각각을 적절한 phase에 나누어 정의해두는 게 좋다.

phase

위에서 빌드를 실행한 후 '단계 세부정보'에서 보았듯, CodeBuild는 빌드를 여러 단계로 나누어서 실행한다. submitted - queued - provisioning - ... - completed 순서로 이루어지는데, 이는 빌드 커맨드를 '설치 단계', '빌드 직전 단계', '빌드 단계', '빌드 후 단계' 등으로 체계화시키기 위함도 있고, 빌드 단계별로 접근 가능한 데이터들을 몇가지 붙여주기 위함도 있다. post_build에서 '빌드 성공 여부'(정확히는 build phase의 exit code)를 판단하기 위해 접근할 수 있는 환경변수 CODEBUILD_BUILD_SUCCEEDING을 예로 들 수 있다.

적절한 phase에 나누어 buildspec.yml에 명령어 추가하기

pipenv installinstall phase에, pipenv shellpre_build phase에, zappa updatebuild phase에 추가했다.

또한 배포가 잘 이루어지는지 알아보기 위해 GET /에서 내려주던 'Hello World'라는 문자열을 'This is version 2'로 변경했다. 코드 스냅샷

push 후 삽질

hook을 걸어 두었으니, push만 하고 기다렸을 때 CodeBuild가 hook에 의해 코드를 알아서 가져가서 잘 빌드하는지 지켜보자.

첫번째 실패

pipenv shell 명령어를 실행하다가 'Inappropriate ioctl for device'라는 에러 메시지와 함께 빌드에 실패했다. 구글링 해보니 Trouble running pipenv in CI라는 제목의 이슈를 찾을 수 있었고, 애초에 그냥 pipenv shell을 실행하지 말고, pipenv가 필요한 커맨드의 앞에 pipenv run을 붙이면 될 것 같았다. buildspec.yml을 아래와 같이 수정했고, 다시 push했다.

pipenv shell을 제거한 직후의 스냅샷

두번째 실패

pipenv 관련된 이슈가 해결되어 zappa update 명령어도 실행 시작까지 잘 됐지만, CodeBuild 머신에선 aws configure에 의해 'default' profile이 설정된 적 없어서 또 빌드에 실패했다.

zappa가 AWS 관련 작업을 할 때 boto3라는 라이브러리를 사용하는데, 이 친구는 자격 증명을 위해 환경 변수도 확인한다. 더 많은 정보는 boto3 User Guide - Credentials를 확인하자. 아무튼 우린 환경 변수에 AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY를 설정해 주면 되고, 이는 CodeBuild에서 빌드 프로젝트를 선택한 후 '편집' 드롭다운을 열고 '환경'에 들어가 '추가 구성'을 열면 설정할 수 있다.

'환경 업데이트' 버튼을 누르고, zappa가 config를 무시하고 바로 환경 변수를 참조할 수 있도록, 아래와 같이 zappa_settings.json에서 profile_name 부분을 지운 후 push하여 빌드를 유발하자.

zappa_settings.json에서 profile_name을 제거한 직후의 스냅샷

세번째 실패

그러나 이번에도 빌드는 실패한다.

virtual environment가 활성화되어 있지 않다고 한다. 근데 사실 zappa가 virtual environment의 활성화 상태를 확인하는 건 환경 변수라서(코드), pre_build에서 VIRTUAL_ENV 환경변수를 설정해주면 될 것이다. 그 값은 virtual environment의 경로여야 하는데, 이는 pipenv --venv로 알아낼 수 있다.

pre_build에 export 커맨드를 추가한 직후의 스냅샷

다섯번째 실패

빌드는 또 실패한다.

export 명령어가 안 먹히는 것 같아서 구글링을 좀 해봤더니, Environment variables not being set on AWS Codebuild라는 스택오버플로우 질문이 있었고, buildspec 버전을 0.2로 변경하면 잘 될 거라는 답변이 있어서 buildspec.yml을 한 번 더 변경해 봤다.

buildspec 버전을 0.2로 변경한 직후의 스냅샷

성공!

드디어 성공했다. URL에 접속해 보면, 'This is version 2'라는 문자열이 반환되는 걸 볼 수 있다.

수많은 실패 끝에 드디어 배포 자동화 세팅을 성공시켰다. 사실 독자 입장에서는 여기까지 오는 데 별로 긴 시간이 걸리지 않았겠지만, 필자는 CodeBuild를 처음 써보는 입장이었어서 빌드 실패만 16번 만들고 4시간동안 계속 구글링하며 삽질했다. 그러나 배포 자동화는 세팅 삽질로 하루를 보내더라도 아깝지 않을 정도로 우리의 생산성에 많은 도움을 줄테니, 좋은 경험이라고 생각하고 넘어가고자 한다.

이제부턴 push가 일어날 때마다 빌드가 실행될 것이고, 빌드 한 번에 1분 30초 정도가 소요된다. 우리가 build.general1.small 인스턴스 타입을 쓰고 있으니, free tier로 제공되는 100분을 넘긴 후부턴 AWS CodeBuild 요금에 따라 push 한 번당 0.0075$(약 8~9원) 정도의 요금이 발생할 것이다.

SQLAlchemy를 통해 쿼리하는 경우, 테이블 모델 자체를 넘기는 것이 가장 일반적이다.

SQL로 치면, SELECT * FROM ... 꼴이 될 것이다. SELECT id FROM ...처럼 특정 컬럼만 조회하고 싶다면, 해당 모델의 컬럼 객체들을 가변 인자 형태로 전달해주면 된다.

위 예제는 TblUsers에서 id, name, email을 SELECT한다. TblUsers의 테이블 이름이 tbl_users라면, SELECT id, name, email FROM tbl_users로 표현할 수 있을 것이다. 이 경우 쿼리의 결과 객체에서는 id, name, email만 접근할 수 있다. 다른 필드에 접근하려고 하면, TblUsers에 명시되어 있는 컬럼이더라도 에러가 발생한다.

직렬화(serialization)은 어떠한 데이터 구조를 송/수신하거나 저장하기 위해 전송에 적합한 포맷으로 변환하는 과정이다. 대부분의 서비스에는 통신이 필요하고, 어떤 프로토콜을 쓰던 간에 컴포넌트 간 데이터 통신(안드로이드 앱 → Python 서버 등)을 위해서는 프로그래밍 언어나 플랫폼에 독립적인, 표준화된 포맷이 필요하다.(Java의 객체를 이런저런 방식으로 바이너리로 변환해 봤자, 다른 언어에는 이에 대한 표준이 없어서 완벽하게 읽을 수 없기 때문에) 따라서 직렬화는 컴퓨터 과학에서 매우 중요하다. 여기서는 데이터를 송/수신하기 위한 직렬화에 대해 이야기해 보겠다.

웬만한 서비스에서 대부분의 데이터는 'ID는 planb고, 나이는 19세다'처럼 a는 b같은 map 개념과 '학생 목록'처럼 학생1, 학생2, 학생3, ...같은 list 개념만 있으면 대충 다 표현할 수 있다. 예를 들어, '학생들의 주소록'은 '이름', '주소', '전화번호' 등이 담긴 각각의 요소를 list로 묶어주면 된다. 결론적으로 map과 list를 어떤 모습으로 표현할 것인지, 요소들에는 어떤 타입을 허용할지에 따라 수많은 형태의 직렬화 양식이 존재한다.

위부터 차례대로 json, xml, yaml이다. 이들 중 데이터의 송/수신에서 가장 많이 쓰이는 것은 (아마도)JSON이다.

JSON

JSON은 JavaScript Object Notation의 약자이며, 따라서 JavaScript의 객체를 표현하기 위한 포맷에서 파생되었다. key-value 쌍으로 이루어진 object 타입과, 값들의 순서화된 열거인 array 타입으로 나뉜다.

Object

Object는 Python의 Dictionary, Java의 HashMap과 대응할 수 있으며, 비순서화된 key-value 쌍으로 이루어진다. 한 object에서 key는 항상 유일성이 보장되며, 따라서 각각의 요소들을 key로 식별한다. 중괄호로 감싸고, key와 value의 구분자로 colon(:)을 사용하고, comma(,)로 각 쌍을 구분한다.

Array

Array는 Python의 List, Java의 ArrayList와 대응할 수 있으며, 값(value)의 순서화된 열거를 표현한다. 대괄호로 감싼다.

value

사실상 특정 언어에서 JSON이라는 데이터를 직렬화/역직렬화하기 위한 패키지를 개발하려면, 위에서 이야기한 format에 대한 정의와 함께 value의 정의도 필요하다. 문자열(string), 숫자(number), JSON object, JSON array, boolean 타입 또는 null이 올 수 있다.

결론적으로,

  • A와 B가 통신하려면 특정 프로그래밍 언어에 의존하지 않는 데이터 포맷이 아무튼 필요하다.
  • 이를 위해 많은 사람들이 직렬화 포맷들을 이것저것 제안했으나 결국은 JSON이 가장 많이 쓰인다.
  • 콤마로 구분된 key-value 매핑을 중괄호로 감싸고, 이를 'object'라고 부르며
  • 요소의 열거를 표현하기 위해 콤마로 구분된 값들을 대괄호로 감싸고, 이를 'array'라고 부른다.
  • 이 둘을 통해 열심히 데이터를 만들어서 송/수신하면, 대부분의 프로그래밍 언어가 JSON 파싱을 지원하기 때문에 알아서 잘 파싱해서 쓸 수 있다.
  • 따라서 클라이언트가 서버로 데이터를 쏠 때나, 서버가 클라이언트로 데이터를 내려줄 때나 웬만하면 JSON을 사용하는 것 같다.


'배경지식' 카테고리의 다른 글

ProxySQL  (1) 2019.02.12
ORM  (0) 2019.02.12
HTTP 메소드  (0) 2019.02.12
StatsD  (0) 2019.02.11
HTTP 헤더  (0) 2018.11.02

+ Recent posts