2장

2. 쿠버네티스 환경 구축과 애플리케이션 배포

2.1.2 예제 애플리케이션의 개요

장소 등록 서비스로 정보를 DB에 등록하는 배치 애플리케이션을 구성한다.

2.1.3 관련 서비스 및 용어

S3

설명

이 책에서는 FE 콘텐츠 배포 장소, 배치 애플리케이션에서 파일을 읽어오는 장소로 사용한다.

CloudFront

AWS가 제공하는 CDN으로 전 세계에 에지 로케이션을 가지고 있어 사용자가 접속한 가장 가까운 장소에서 콘텐츠를 제공할 수 있다.

RDS

설명

ECR

AWS가 제공하는 컨테이너 레지스트리 관리형 서비스로 DOCKER HUB와 비슷한 개념이다.

EC2

EC2의 컴퓨팅 서비스로

  • 온디맨드

  • EC2 Instance Savings Plans

    • 패밀리, 크기, AZ, 리전, OS, 테넌시와 상관없이 변경할 수 있는 절감모델

  • Compute Savings Plans

    • 해당 리전의 패밀리 내에서 인스턴스간의 사용량을 변경할 수 있는 유연성을 제공, 최대 77%

  • 스팟 인스턴스

등의 옵션으로 구매 가능하다.

ELB

AWS가 제공하는 로드밸런서로

ALB, NLB, GLB, CLB등이 제공됐지만 현재 CLB는 신규로 만들 수 없다.

  • ALB

    • L7 로드밸런서로 경로 기반 라우팅(URL), 호스트 기반 라우팅(HTTP Header) 등의 HTTP 고유 처리가 가능하며 CLB와 구분된다

  • NLB

    • L4 로드밸런서로 HTTP/HTTPS 프로토콜 외에도 사용 가능하다.

  • GWLB

    • L3 로드밸런서로 다른 회사의 가상 네트워킹 어플라이언스 배포, 확장, 실행을 돕고, 여러 회사의 어플라이언스에 대한 로드밸런싱 및 자동 조정 기능을 제공한다.

IAM

AWS 리소스에 접속하는 사용자나 권한을 관리하는 서비스

IAM에서 IAM 역할을 생성해 리소스에 설정함으로써 해당 리소스에 다른 리소스에 대한 접근 권한을 부여할 수있다.

2.1.4 EKS 구축에 사용하는 도구

EKS 클러스터 구축에는 두 가지 방법이 있다.

  • EKS 클러스터 구축 도구 eksctl을 이용하는 방법

  • AWS 콘솔 혹은 AWS CLI를 이용하는 방법들

eksctl

EKS 클러스터 구축 및 관리를 하기 위한 오픈소스 CLI 도구로

VPC, subnet, security group 등 EKS 클러스터를 구축하는데 필요한 리소스를 한 번에 구성할 수 있다.

VPC

CloudFormation

2.2 EKS 클러스터 구축

기본 리소스 생성

CloudFormation을 이용해 VPC 등의 기본 리소스를 생성한다.

base_resources_cfn
AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  ClusterBaseName:
    Type: String
    Default: eks-work

  TargetRegion:
    Type: String
    Default: ap-northeast-2

  AvailabilityZone1:
    Type: String
    Default: ap-northeast-2a

  AvailabilityZone2:
    Type: String
    Default: ap-northeast-2b

  AvailabilityZone3:
    Type: String
    Default: ap-northeast-2c

  VpcBlock:
    Type: String
    Default: 192.168.0.0/16

  WorkerSubnet1Block:
    Type: String
    Default: 192.168.0.0/24

  WorkerSubnet2Block:
    Type: String
    Default: 192.168.1.0/24

  WorkerSubnet3Block:
    Type: String
    Default: 192.168.2.0/24

Resources:
  EksWorkVPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcBlock
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub ${ClusterBaseName}-VPC

  WorkerSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AvailabilityZone1
      CidrBlock: !Ref WorkerSubnet1Block
      VpcId: !Ref EksWorkVPC
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${ClusterBaseName}-WorkerSubnet1

  WorkerSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AvailabilityZone2
      CidrBlock: !Ref WorkerSubnet2Block
      VpcId: !Ref EksWorkVPC
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${ClusterBaseName}-WorkerSubnet2

  WorkerSubnet3:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AvailabilityZone3
      CidrBlock: !Ref WorkerSubnet3Block
      VpcId: !Ref EksWorkVPC
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub ${ClusterBaseName}-WorkerSubnet3

  InternetGateway:
    Type: AWS::EC2::InternetGateway

  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref EksWorkVPC

  WorkerSubnetRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref EksWorkVPC
      Tags:
        - Key: Name
          Value: !Sub ${ClusterBaseName}-WorkerSubnetRouteTable

  WorkerSubnetRoute:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref WorkerSubnetRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  WorkerSubnet1RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref WorkerSubnet1
      RouteTableId: !Ref WorkerSubnetRouteTable

  WorkerSubnet2RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref WorkerSubnet2
      RouteTableId: !Ref WorkerSubnetRouteTable

  WorkerSubnet3RouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref WorkerSubnet3
      RouteTableId: !Ref WorkerSubnetRouteTable

Outputs:
  VPC:
    Value: !Ref EksWorkVPC

  WorkerSubnets:
    Value: !Join
      - ","
      - [!Ref WorkerSubnet1, !Ref WorkerSubnet2, !Ref WorkerSubnet3]

  RouteTable:
    Value: !Ref WorkerSubnetRouteTable

CloudFormation을 통해 기본 리소스를 생성한다.

다음과 같이 리소스가 구성된 것을 볼 수 있다.

2.2.2 EKS 클러스터 구축

eksctl 명령어로 EKS 클러스터를 구축한다.

eksctl create cluster \
--vpc-public-subnets subnet-0e8042321293c7ae2,subnet-0009414b42f02c0c9,subnet-095d6a47ee9e2cdd5 \
--name eks-work-cluster \
--region ap-northeast-2 \
--version 1.19 \
--nodegroup-name eks-work-nodegroup \
--node-type t2.small
--nodes 2 \
--nodes-min 2 \
--nodes-max 5

해당 명령어 동작 중에 에러가 발생했다.

2024-06-04 20:49:09 [✖]  AWS::IAM::Role/ServiceRole: CREATE_FAILED – "Resource handler returned message: \"Encountered a permissions error performing a tagging operation, please add required tag permissions. See https://repost.aws/knowledge-center/cloudformation-tagging-permission-error for how to resolve. Resource handler returned message: \"User: arn:aws:iam::566620743708:user/hojin is not authorized to perform: iam:CreateRole on resource: arn:aws:iam::566620743708:role/eksctl-eks-work-cluster-cluster-ServiceRole-o9PL6esmhBCb because no identity-based policy allows the iam:CreateRole action (Service: Iam, Status Code: 403, Request ID: 3218d612-0915-459c-a593-168f0254df9e)\"\" (RequestToken: 3073b698-9f51-d355-9de0-cdd413cc0b44, HandlerErrorCode: UnauthorizedTaggingOperation)"
2024-06-04 20:49:09 [!]  1 error(s) occurred and cluster hasn't been created properly, you may wish to check CloudFormation console

현재 로컬에서 root 계정이 아닌 hojin 계정을 생성해 사용중인데 해당 계정에 CreateRole이 없는 모양이었다.

따라서 aws configure 명령어를 통해 user를 Administrator로 전환하고 진행했다.

그 다음에 문제는 creating CloudFormation stack "eksctl-eks-work-cluster-cluster": operation error CloudFormation: CreateStack, https response error StatusCode: 400, RequestID: bdaf29da-cce6-43f1-a9b5-3cf8565efe00, AlreadyExistsException: Stack [eksctl-eks-work-cluster-cluster] already exists

따라서 해당 stack을 제거하고 진행했다.

  • EKS 클러스터 구축

  • 워커 노드 구축 이 완료됐다.

kubeconfig 설정

eksctl은 EKS 클러스터 구축 중에 kubeconfig파일을 자동으로 업데이트하는데 kubeconfig파일은 쿠버네티스 클라이언트인 kubectl이 이용할 설정 파일로 접속 대상 쿠버네티스 클러스터의 접속 정보(컨트롤 플레인 URL, 인증 정보, 쿠버네티스 네임스페이스)를 저장하고 있다.

AWS CLI를 호출해 인증하기 위한 설정을 kubeconfig에 포함할 수 있다

kubectl: 쿠버네티스 클러스터에 명령을 내리는 CLI로 쿠버네티스는 오브젝트 관리를 위해 쿠버네티스 API를 사용하는데 kubectl CLI를 사용하면 해당 명령어가 쿠버네티스 API를 호출해 관련 동작을 수행한다.

kubectl config get-contexts

출력 결과에서 컨텍스트가 활성화되어 있음을 알 수 있고, 노드 상태를 확인할 수 있다.

클러스터의 동작을 확인해보면

kubectl apply -f 02_nginx_k8s.yaml
pod/nginx-pod created
cat 02_nginx_k8s.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx-pod
  labels:
    app: nginx-app
spec:
  containers:
  - name: nginx-container
    image: nginx
    ports:
    - containerPort: 80
kubectl get pods
NAME        READY   STATUS    RESTARTS   AGE
nginx-pod   1/1     Running   0          96s

정상적으로 pod가 생성되었음을 알 수 있다.

kubectl port-forward nginx-pod 8080:80
Forwarding from 127.0.0.1:8080 -> 80
Forwarding from [::1]:8080 -> 80

2.3 데이터베이스 설정

예제 애플리케이션 DB 생성, DDL 적용 등 애플리케이션에서 사용하기 위한 설정을 수행한다.

  • DB와 Bastion host 구축

  • Session Manager를 이용한 Bastion host 구축

  • bastion host에 필요한 도구 설치

  • bastion host 상에 깃 리포지터리 클론

  • DB 접속과 DDL 및 예제 데이터 불러오기

RDS의 PostgreSQL을 사용하고, 다중화 없이 단일 가용 영역으로 구축한다.

기존 리소스 생성과 마찬가지로 CloudFormation에 미리 준비된 템플릿을 업로드해 생성한다.

하지만 기존의 템플릿이 에러로 실행되지 않아

Resource handler returned message: "RDS does not support creating a DB instance with the following combination: DBInstanceClass=db.t2.micro, Engine=postgres, EngineVersion=11.22, LicenseModel=postgresql-license. For supported combinations of instance class and database engine version, see the documentation. (Service: Rds, Status Code: 400, Request ID: ac86e4f7-6daf-45c9-aea4-3fd0f53e7f6c)" (RequestToken: 77f0f6dc-85cc-f9de-1ede-6aa49cb7e339, HandlerErrorCode: InvalidRequest)

Instance class를 바꿔서 실행한다.

해당 링크를 참고하자

10_rds_ope_cfn.yaml
AWSTemplateFormatVersion: "2010-09-09"

Parameters:
  EksWorkVPC:
    Type: AWS::EC2::VPC::Id

  WorkerSubnet1Block:
    Type: String
    Default: 192.168.0.0/24

  WorkerSubnet2Block:
    Type: String
    Default: 192.168.1.0/24

  WorkerSubnet3Block:
    Type: String
    Default: 192.168.2.0/24

  OpeSubnetBlock:
    Type: String
    Default: 192.168.5.0/24

  OpeServerRouteTable:
    Type: String

  ClusterBaseName:
    Type: String
    Default: eks-work

  TargetRegion:
    Type: String
    Default: ap-northeast-2

  AvailabilityZone1:
    Type: String
    Default: ap-northeast-2a

  AvailabilityZone2:
    Type: String
    Default: ap-northeast-2b

  AvailabilityZone3:
    Type: String
    Default: ap-northeast-2c

  RdsSubnet1Block:
    Type: String
    Default: 192.168.3.0/24

  RdsSubnet2Block:
    Type: String
    Default: 192.168.4.0/24

  OpeServerInstanceType:
    Type: String
    Default: t3.micro

  OpeServerImageId:
    Type: String
    Default: ami-004ca97dc8c3b41f8  # ap-northeast-2, GPU, Amazon Linux 2
    # aws ssm get-parameter --name /aws/service/eks/optimized-ami/1.19/amazon-linux-2-gpu/recommended/image_id --region ap-northeast-2 --query "Parameter.Value" --output text

  OpeServerVolumeSize:
    Type: Number
    Default: 8

Resources:
  RdsSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AvailabilityZone1
      CidrBlock: !Ref RdsSubnet1Block
      VpcId: !Ref EksWorkVPC

  RdsSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AvailabilityZone2
      CidrBlock: !Ref RdsSubnet2Block
      VpcId: !Ref EksWorkVPC

  RdsSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security Group for RDS
      VpcId: !Ref EksWorkVPC

  RdsIngressPostgreSQLWorker1:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref RdsSecurityGroup
      CidrIp: !Ref WorkerSubnet1Block
      IpProtocol: tcp
      FromPort: 5432 # mysql 3306 mariadb 3396 postgresql 5432
      ToPort: 5432

  RdsIngressPostgreSQLWorker2:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref RdsSecurityGroup
      CidrIp: !Ref WorkerSubnet2Block
      IpProtocol: tcp
      FromPort: 5432
      ToPort: 5432

  RdsIngressPostgreSQLWorker3:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref RdsSecurityGroup
      CidrIp: !Ref WorkerSubnet3Block
      IpProtocol: tcp
      FromPort: 5432
      ToPort: 5432

  RdsIngressPostgreSQLOpe:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref RdsSecurityGroup
      CidrIp: !Ref OpeSubnetBlock
      IpProtocol: tcp
      FromPort: 5432
      ToPort: 5432

  EksWorkDB:
    Type: "AWS::RDS::DBInstance"
    Properties:
      DBInstanceIdentifier: eks-work-db
      Engine: postgres  # https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-rds-database-instance.html#aws-properties-rds-database-instance-properties
      EngineVersion: 11  # https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_MySQL.html#MySQL.Concepts.VersionMgmt
      DBInstanceClass: db.t3.micro
      AllocatedStorage: 30
      StorageType: gp2
      DBName: eksworkdb
      MasterUsername: !Join ['', ['{{resolve:secretsmanager:', !Ref RdsMasterSecret, ':SecretString:username}}' ]]
      MasterUserPassword: !Join ['', ['{{resolve:secretsmanager:', !Ref RdsMasterSecret, ':SecretString:password}}' ]]
      DBSubnetGroupName: !Ref EksWorkDBSubnetGroup
      PubliclyAccessible: false
      MultiAZ: false
      PreferredBackupWindow: 18:00-18:30
      PreferredMaintenanceWindow: sat:19:00-sat:19:30
      AutoMinorVersionUpgrade: false
      DBParameterGroupName: !Ref EksWorkDBParameterGroup
      VPCSecurityGroups:
        - !Ref RdsSecurityGroup
      CopyTagsToSnapshot: true
      BackupRetentionPeriod: 7
      DeletionProtection: false

  RdsMasterSecret:
    Type: AWS::SecretsManager::Secret
    Properties:
      GenerateSecretString:
        SecretStringTemplate: '{"username": "eksdbadmin"}'
        GenerateStringKey: 'password'
        PasswordLength: 16
        ExcludeCharacters: '"@/\'''
      Name: RdsMasterSecret

  RdsUserSecret:
    Type: AWS::SecretsManager::Secret
    Properties:
      GenerateSecretString:
        SecretStringTemplate: '{"username": "mywork"}'
        GenerateStringKey: 'password'
        PasswordLength: 16
        ExcludeCharacters: '"@/\''{}#%&*<>[]^`|'
      Name: RdsUserSecret

  RdsSecretAttachment:
    Type: AWS::SecretsManager::SecretTargetAttachment
    Properties:
      SecretId: !Ref RdsMasterSecret
      TargetId: !Ref EksWorkDB
      TargetType: AWS::RDS::DBInstance

  EksWorkDBSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupName: subnet-group-eks-work-db
      DBSubnetGroupDescription: "DB Subnet Group"
      SubnetIds:
        - !Ref RdsSubnet1
        - !Ref RdsSubnet2

  EksWorkDBParameterGroup:
    Type: AWS::RDS::DBParameterGroup
    Properties:
      Family: postgres11 # aws rds describe-db-engine-versions --query "DBEngineVersions[].DBParameterGroupFamily"
      Description: Parameter Group for PostgreSQL 11

  OpeServerSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AvailabilityZone1
      CidrBlock: !Ref OpeSubnetBlock
      VpcId: !Ref EksWorkVPC

  OpeServerSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref OpeServerSubnet
      RouteTableId: !Ref OpeServerRouteTable

  OpeServerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security Group for Ope
      VpcId: !Ref EksWorkVPC

  RdsIngressPostgreSQLFromOpe:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !Ref RdsSecurityGroup
      SourceSecurityGroupId: !Ref OpeServerSecurityGroup
      IpProtocol: tcp
      FromPort: 5432
      ToPort: 5432

  OpeServerRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${ClusterBaseName}-OpeServerRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM
      Path: /
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ec2.amazonaws.com
            Action:
              - sts:AssumeRole

  OpeServerInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: /
      Roles:
        - !Ref OpeServerRole

  OpeServerEIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc

  OpeServerEIPAssociation:
    Type: AWS::EC2::EIPAssociation
    Properties:
      AllocationId: !GetAtt OpeServerEIP.AllocationId
      InstanceId: !Ref OpeServerInstance

  OpeServerInstance:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref OpeServerInstanceType
      ImageId: !Ref OpeServerImageId
      BlockDeviceMappings:
        - DeviceName: /dev/xvda
          Ebs:
            VolumeType: gp2
            VolumeSize: 20  # 원래는 8GB였음. 권고 사항이 20GB라고 함
            DeleteOnTermination: true
      NetworkInterfaces:
        - SubnetId: !Ref OpeServerSubnet
          AssociatePublicIpAddress: false
          GroupSet:
            - !Ref OpeServerSecurityGroup
          DeviceIndex: 0
          DeleteOnTermination: true
      DisableApiTermination: false
      IamInstanceProfile: !Ref OpeServerInstanceProfile
      UserData:
        Fn::Base64:
          !Sub |
          #!/bin/bash
          set -o xtrace
          yum install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm
          /opt/aws/bin/cfn-signal --exit-code $? \
                   --stack  ${AWS::StackName} \
                   --resource NodeGroup  \
                   --region ${AWS::Region}

Outputs:
  RDSEndpoint:
    Value: !GetAtt EksWorkDB.Endpoint.Address

2.2.3 세션 관리자를 통한 베스천 호스트 접속

ec2 설정에서도 접속 가능하다.

해당 bastion host에 git, postgreSQL 클라이언트를 설치해준다.

sudo yum install -y git
sudo amazon-linux-extras install -y postgresql11
git clone https://github.com/dybooksIT/k8s-aws-book.git

2.3.6 데이터베이스 엔드포인트 주소와 관리자 비밀번호 확인

DB 접속 전에 DB 접속정보가 필요하다. CloudFormation을 통해 다음 세 가지 정보를 확인한다.

  • 데이터베이스 엔드포인트 주소

  • 데이터베스 관리자 비밀번호

  • 애플리케이션용 데이터베스 사용자 비밀번호

데이터베이스 엔드포인트 주소 확인

엔드포인트 주소는 CloudFormation - Stacks - eks-work-rds - Outputs에서 확인 가능하다.

cloudformation 마지막 부분

Outputs:
  RDSEndpoint:
    Value: !GetAtt EksWorkDB.Endpoint.Address

덕분인 것 같다.

eks-work-db.cvsiy1n9oexu.ap-northeast-2.rds.amazonaws.com

데이터베이스 관리자 비밀번호 확인

CloudFormation으로 구축한 DB는 AWS Secrets Manager가 RDS 관리자 비밀번호를 생성해 AWS Secrets Manager에서 확인 가능하다.

RdsMasterSecret: ]v[|mEh+<t~DTG7F

애플리케이션용 데이터베이스 사용자 비밀번호 확인

RdsUserSecret: J4(yv2fH~x,(LyaZ

2.3.7 데이터베이스 작업

DB에서 할 작업목록은 다음과 같다.

  • 애플리케이션용 DB 사용자 생성

  • 애플리케이션용 DB 생성

  • DDL 실행

  • 예제 데이터 불러오기

애플리케이션용 DB 사용자 생성

createuser -d -U eksdbadmin -P -h eks-work-db.cvsiy1n9oexu.ap-northeast-2.rds.amazonaws.com mywork

DB 사용자를 생성한다.

애플리케이션용 DB 생성

createdb -U mywork -h eks-work-db.cvsiy1n9oexu.ap-northeast-2.rds.amazonaws.com -E UTF8 myworkdb

DDL 실행 및 예제 데이터 불러오기

\i k8s-aws-book/backend-app/scripts/10_ddl.sql
\i k8s-aws-book/backend-app/scripts/20_insert_sample_data.sql

두 명령어를 통해 postgreSQL 접속 상태에서 스크립트를 실행한다

2.4 API 애플리케이션 빌드와 배포

이 절에서는 API 애플리케이션을 빌드하고 컨테이너 이미지를 생성해 2.2절에서 생성한 EKS 클러스터에서 동작시킨다.

작업목록은 다음과 같다.

  • API 애플리케이션 소스 코드 빌드

  • 컨테이너 이미지를 저장할 컨테이너 레지스트리 준비

  • API 애플리케이션 컨테이너 이미지를 생성하고 컨테이너 레지스트리에 푸시

  • DB 접속용 비밀 정보를 보관하는 시크릿 생성

  • API 애플리케이션 배포

2.4.2 소스 코드 빌드와 컨테이너 이미지 생성

애플리케이션 빌드는 로컬 환경의 셸에서 진행한다.

sudo chmod 755 ./gradlew
./gradlew clean build
  • 라이브러리 다운로드

  • 컴파일

  • 테스트 컴파일

  • 테스트 실행

  • JAR 생성

이제 컨테이너 이미지를 생성한다.

sudo docker build -t k8sbook/backend-app:1.0.0 --build-arg JAR_FILE=build/libs/backend-app-1.0.0.jar .

실행 과정에서 다음과 같은 에러가 떴다.

ERROR: failed to solve: amazoncorretto:11: failed to resolve source metadata for docker.io/library/amazoncorretto:11: error getting credentials - err: exit status 1, out: ``

다음 링크를 보고 credsStore -> credStore로 변경해 해결하긴 했지만 원인은 파악해봐야 하겠다.

2.4.3 컨테이너 레지스트리 준비

2.4.4 컨테이너 이미지 푸시

ECR 로그인

ECR 로그인을 위해 다음 명령어를 입력한다

aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin 566620743708.dkr.ecr.ap-northeast-2.amazonaws.com

컨테이너 이미지 태그 설정과 푸시

컨테이너 이미지 푸시는 두 가지 순서로 진행한다.

  • docker tag 명령으로 컨테이너 이미지에 태그 설정

  • 해당 태그에 대해 docker push 명령 실행

API 애플리케이션 컨테이너 이미지(k8s/backendapp:1.0.0)에 ECR 주소를 포함한 형태의 태그를 설정한다.

docker tag k8sbook/backend-app:1.0.0 566620743708.dkr.ecr.ap-northeast-2.amazonaws.com/k8sbook/backend-app:1.0.0

해당 태그에 대해 docker push 명령어를 실행한다.

docker push 566620743708.dkr.ecr.ap-northeast-2.amazonaws.com/k8sbook/backend-app:1.0.0

2.4.5 EKS 클러스터에 API 애플리케이션 배포

API 애플리케이션을 EKS 클러스터에 배포하기 위해 다음과 같은 단계를 진행한다.

  • 네임스페이스 생성

  • kubeconfig에 네임스페이스 반영

  • 데이터베이스 접속용 시크릿 등록

  • API 애플리케이션 배포

  • API 애플리케이션 외부 공개

네임스페이스 생성

쿠버네티스에서는 클러스터 하나를 네임스페이스라는 논리적 구획으로 구분해 관리할 수 있다. 여기서는 eks-work라는 네임스페이스를 생성하고 해당 네임스페이스에 API 애플리케이션을 배포한다.

네임스페이스는 쿠버네티스 오브젝트 설정 파일(매니페스트)라고 하며, kubectl apply 명령으로 적용해 생성한다.

쿠버네티스 오브젝트는 kubectl create으로도 생성 가능하지만(kubectl create namespace myns) 보통 매니페스트 파일을 이용해 생성하며, 파일 버전 관리와 같은 환경 구축에 사용한다.

kubectl apply -f 20_create_namespace_k8s.yaml

apiVersion: v1
kind: Namespace
metadata:
  name: eks-work

kubeconfig에 네임스페이스 반영

kubeconfig파일에는 컨텍스트마다 네임스페이스를 지정할 수 있다. 따라서 2.2.2에서 업데이트한 kubeconfig에 네임스페이스를 반영해야 한다

위에서 봤듯이 AWS CLI로 생성한 컨텍스트에는 네임스페이스가 지정되지 않았다.

여기서는 eks-work라는 새 네임스페이스를 이용하기 위해 eks-work가 설정된 새로운 컨텍스트를 생성하고 활성화한다.

kubectl config set-context eks-work --cluster eks-work-cluster.ap-northeast-2.eksctl.io --user Administrator@eks-work-cluster.ap-northeast-2.eksctl.io --namespace eks-work

Context "eks-work" created.

kubectl config use-context eks-work
Switched to context "eks-work".
kubectl config get-contexts
CURRENT   NAME                                                      CLUSTER                                     AUTHINFO                                                  NAMESPACE
          Administrator@eks-work-cluster.ap-northeast-2.eksctl.io   eks-work-cluster.ap-northeast-2.eksctl.io   Administrator@eks-work-cluster.ap-northeast-2.eksctl.io
*         eks-work                                                  eks-work-cluster.ap-northeast-2.eksctl.io   Administrator@eks-work-cluster.ap-northeast-2.eksctl.io   eks-work
          minikube                                                  minikube                                    minikube                                                  default

이제 eks-work가 지정된 컨텍스트가 생성대 활성화된 컨텍스트로 변경됐다는 것을 확인할 수 있다.

데이터베이스 접속용 시크릿 등록

API 애플리케이션이 DB에 접속하기 위한 비밀번호 등을 저장하는 시크릿을 생성한다 시크릿은 쿠버네티스 클러스터 안에 비밀번호 등의 비밀 정보를 보관하기 위한 도구다.

DB_URL=jdbc:postgresql:eks-work-db.cvsiy1n9oexu.ap-northeast-2.rds.amazonaws.com/myworkdb DB_PASSWORD='J4(yv2fH~x,(LyaZ' envsubst < 21_db_config_k8s.yaml.template | kubectl apply -f -
secret/db-config created
21_db_config_k8s
apiVersion: v1
kind: Secret
type: Opaque
metadata:
  name: db-config
stringData:
  db-url: ${DB_URL}
  db-username: mywork
  db-password: ${DB_PASSWORD}

API 애플리케이션 배포

API 애플리케이션을 배포한다.

ECR_HOST=566620743708.dkr.ecr.ap-northeast-2.amazonaws.com envsubst < 22_deployment_backend-app_k8s.yaml.template | kubectl apply -f -
deployment.apps/backend-app created
22_deployment_backend
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend-app
  labels:
    app: backend-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: backend-app
  template:
    metadata:
      labels:
        app: backend-app
    spec:
      containers:
      - name: backend-app
        image: ${ECR_HOST}/k8sbook/backend-app:1.0.0
        imagePullPolicy: Always
        ports:
        - containerPort: 8080
        env:
        - name: DB_URL
          valueFrom:
            secretKeyRef:
              key: db-url
              name: db-config
        - name: DB_USERNAME
          valueFrom:
            secretKeyRef:
              key: db-username
              name: db-config
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              key: db-password
              name: db-config
        readinessProbe:
          httpGet:
            port: 8080
            path: /health
          initialDelaySeconds: 15
          periodSeconds: 30
        livenessProbe:
          httpGet:
            port: 8080
            path: /health
          initialDelaySeconds: 30
          periodSeconds: 30
        resources:
          requests:
            cpu: 100m
            memory: 512Mi
          limits:
            cpu: 250m
            memory: 768Mi
        lifecycle:
          preStop:
            exec:
              command: ["/bin/sh", "-c", "sleep 2"]%

kubectl apply 명령으로 적용한 매니페스트 파일은 디플로이먼트라는 종류의 오브젝트를 생성하기 위한 것으로

디플로이먼트는 컨테이너를 배포, 디프로이먼트 뒤에 레플리카셋이 생성, 레플리카셋 뒤에는 파드가 생성된다. 아래에 결과가 있다.

kubectl get all
NAME                               READY   STATUS    RESTARTS   AGE
pod/backend-app-59b66ccd45-fmz8k   1/1     Running   0          9m59s
pod/backend-app-59b66ccd45-q8pxk   1/1     Running   0          9m59s

NAME                          READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/backend-app   2/2     2            2           9m59s

NAME                                     DESIRED   CURRENT   READY   AGE
replicaset.apps/backend-app-59b66ccd45   2         2         2       9m59s

API 애플리케이션 외부 공개

API 애플리케이션 배포가 끝났지만 아직 외부에서는 호출이 불가능하다. 쿠버네티스는 배포된 파드를 외부에 공개하기 위해 서비스라는 리소스가 준비되어 있다. 여기서는 로드밸런서를 이용한다.

매니페스트 파일을 이용해 배포해보자

kubectl apply -f 23_service_backend-app_k8s.yaml
service/backend-app-service created
23_service_backend-app_k8s
apiVersion: v1
kind: Service
metadata:
  name: backend-app-service
spec:
  type: LoadBalancer
  selector:
    app: backend-app
  ports:
  - protocol: TCP
    port: 8080
    targetPort: 8080

EXTERNAL-IP에 주소가 공개된 것을 확인할 수 있다.

kubectl get all
NAME                               READY   STATUS    RESTARTS   AGE
pod/backend-app-59b66ccd45-fmz8k   1/1     Running   0          16m
pod/backend-app-59b66ccd45-q8pxk   1/1     Running   0          16m

NAME                          TYPE           CLUSTER-IP       EXTERNAL-IP                                                                   PORT(S)          AGE
service/backend-app-service   LoadBalancer   10.100.118.202   ac415c77bb14c4c45a1ad43a51570d97-482146193.ap-northeast-2.elb.amazonaws.com   8080:31069/TCP   3m2s

NAME                          READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/backend-app   2/2     2            2           16m

NAME                                     DESIRED   CURRENT   READY   AGE
replicaset.apps/backend-app-59b66ccd45   2         2         2       16m

정상 작동 확인이 가능하다.

curl -s http://ac415c77bb14c4c45a1ad43a51570d97-482146193.ap-northeast-2.elb.amazonaws.com:8080/health
{"status":"OK"}%

2.5 프런트엔드 애플리케이션 빌드와 배포

수행할 작업은 다음과 같다.

  • 프런트엔드 애플리케이션 빌드

  • 프런트엔드 콘텐츠를 전송할 S3 버킷과 CloudFront 배포 생성

  • 프런트엔드 콘텐츠를 S3에 저장

  • CloudFront를 통해 접속해 애플리케이션 접속 확인

Nuxt.js로 구축된 SPA를 S3 + CloudFront 구성으로 구축하며, 빌드에는 Node.js를 사용한다

2.5.2 프런트엔드 애플리케이션 빌드

npm install

BASE_URL=http://ac415c77bb14c4c45a1ad43a51570d97-482146193.ap-northeast-2.elb.amazonaws.com:8080 npm run build

패키지 다운로드, 통신하기 위한 ELB url을 매개변수로 입력해서 빌드하고, S3 버킷과 CloudFront 배포를 생성한다.

30_s3_cloudfront_cfn.yaml
Parameters:
  TargetRegion:
    Type: String
    Default: ap-northeast-2

  BucketBaseName:
    Type: String
    Default: eks-work-frontend-

  BucketSuffix:
    Type: String

Resources:
  ContentsBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Join ["", [!Ref BucketBaseName, !Ref BucketSuffix]]
      AccessControl: Private
      PublicAccessBlockConfiguration:
        BlockPublicAcls: false
        BlockPublicPolicy: false
        IgnorePublicAcls: false
        RestrictPublicBuckets: false

  ContentsDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Origins:
        - Id: S3Origin
          DomainName: !GetAtt ContentsBucket.DomainName
          CustomOriginConfig:
            OriginProtocolPolicy: match-viewer
        Enabled: true
        DefaultRootObject: index.html
        Comment: !Ref AWS::StackName
        DefaultCacheBehavior:
          TargetOriginId: S3Origin
          ForwardedValues:
            QueryString: false
          ViewerProtocolPolicy: allow-all

Outputs:
  URL:
    Value: !Join ["", ["http://", !GetAtt [ContentsDistribution, DomainName] ]]

  DistributionID:
    Value: !Ref ContentsDistribution

이제 콘텐츠를 업로드한다.

aws s3 sync dist s3://eks-work-frontend-hj-bk-1 --delete --include "*" --acl public-read
upload: dist/.nojekyll to s3://eks-work-frontend-hj-bk-1/.nojekyll
upload: dist/index.html to s3://eks-work-frontend-hj-bk-1/index.html
upload: dist/_nuxt/870f68b.js to s3://eks-work-frontend-hj-bk-1/_nuxt/870f68b.js
upload: dist/_nuxt/41092ad.js to s3://eks-work-frontend-hj-bk-1/_nuxt/41092ad.js
upload: dist/200.html to s3://eks-work-frontend-hj-bk-1/200.html
upload: dist/_nuxt/LICENSES to s3://eks-work-frontend-hj-bk-1/_nuxt/LICENSES
upload: dist/favicon.ico to s3://eks-work-frontend-hj-bk-1/favicon.ico
upload: dist/_nuxt/d3f4395.js to s3://eks-work-frontend-hj-bk-1/_nuxt/d3f4395.js
upload: dist/v.png to s3://eks-work-frontend-hj-bk-1/v.png
upload: dist/_nuxt/b171587.js to s3://eks-work-frontend-hj-bk-1/_nuxt/b171587.js
upload: dist/_nuxt/0567ccc.js to s3://eks-work-frontend-hj-bk-1/_nuxt/0567ccc.js
upload: dist/regionDetail/index.html to s3://eks-work-frontend-hj-bk-1/regionDetail/index.html
upload: dist/_nuxt/9c2283f.js to s3://eks-work-frontend-hj-bk-1/_nuxt/9c2283f.js

왜인지 모르겠지만 cloudfront url로 접속했을때는 에러가 떴다.

2.6 배치 애플리케이션 빌드와 배포

수행할 작업은 다음과 같다.

  • 배치 애플리케이션 빌드

  • 배치 애플리케이션용 ECR 레포지터리 생성

  • 배치 애플리케이션 컨테이너 이미지 푸시

  • 배치 애플리케이션 파일 다운로드를 위한 S3 버킷 생성

  • 배치 애플리케이션용 컨피그맵 생성

  • 배치 애플리케이션용 시크릿 생성

  • 배치 애플리케이션 입력 파일을 S3에 업로드

  • 스케줄 실행을 위한 크론잡 등록

배치 애플리케이션은 자바와 스프링 부트로 구축한 애플리케이션으로 동작시 환경 변수로 설정된 S3 버킷의 파일을 다운롣하고 파일 내용을 DB에 등록하고 쿠버네티스의 크론잡을 이용해 5분에 1번 동작시키도록 설정한다.

애플리케이션 빌드 및 컨테이너 이미지 생성

소스 코드 빌드

batch-app 디렉토리로 이동해 gradle build를 실행해주고 Dockerfile을 이용해 컨테이너 이미지를 생성한다.

./gradlew clean build
sudo docker build -t k8sbook/batch-app:1.0.0 --build-arg JAR_FILE=build/libs/batch-app-1.0.0.jar .

ECR 리포지터리 생성

ECR 로그인

aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin 566620743708.dkr.ecr.ap-northeast-2.amazonaws.com

컨테이너 이미지 태그 생성과 푸시

태깅을 하고 레포지터리에 푸시한다.

docker tag k8sbook/batch-app:1.0.0 566620743708.dkr.ecr.ap-northeast-2.amazonaws.com/k8sbook/batch-app:1.0.0
docker push 566620743708.dkr.ecr.ap-northeast-2.amazonaws.com/k8sbook/batch-app:1.0.0
40_s3_batch_cfn.yaml
Resources:
  BatchBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Join ["", [!Ref BucketBaseName, !Ref BucketSuffix]]
      AccessControl: Private
      PublicAccessBlockConfiguration:
        BlockPublicAcls: false
        BlockPublicPolicy: false
        IgnorePublicAcls: false
        RestrictPublicBuckets: false

  BatchUser:
    Type: AWS::IAM::User
    Properties:
      Path: /
      UserName: eks-work-batch-user

  BatchUserAccessKey:
    Type: AWS::IAM::AccessKey
    Properties:
      UserName: !Ref BatchUser

  BatchBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref BatchBucket
      PolicyDocument:
        Statement:
        - Action:
          - s3:GetObject
          - s3:DeleteObject
          - s3:PutObject
          Effect: Allow
          Principal:
            AWS: !GetAtt BatchUser.Arn
          Resource: !Join ["", [!GetAtt BatchBucket.Arn, "/*"]]
        - Action:
          - s3:ListBucket
          Effect: Allow
          Principal:
            AWS: !GetAtt BatchUser.Arn
          Resource: !GetAtt BatchBucket.Arn

  BatchUserAccessKeyParameter:
    Type: AWS::SSM::Parameter
    Properties:
      Name: BatchUserAccessKey
      Type: String
      Value: !Ref BatchUserAccessKey

  BatchUserSecretAccessKeyParameter:
    Type: AWS::SSM::Parameter
    Properties:
      Name: BatchUserSecretAccessKey
      Type: String
      Value: !GetAtt BatchUserAccessKey.SecretAccessKey

2.6.6 컨피그맵 생성

배치 애플리케이션에서 설정값을 저장하는 저장소로 컨피그맵을 사용하는데 쿠버네티스에서 설정값을 보관하기 위한 구조다. 배치 애플리케이션에서 사용할 S3 버킷 이름과 디렉토리 이름을 보관할 컨피그맵을 등록한다.

kubectl 명령어로도 설정할 수 있지만 버킷 이름의 접미사를 설정해야 하므로 로컬 작업 환경의 셸에서 envsubst를 사용한 명령을 사용한다

BUCKET_SUFFIX=hojin envsubst < 41_config_map_batch_k8s.yaml.template | kubectl apply -f -
configmap/batch-app-config created

2.6.7 S3 접속용 액세스 키 확인과 시크릿 생성

S3 버킷은 허가된 특정 사용자(배치 어플리케이션)만 접속할 수 있도록 설정해야 하므로 S3 인증 정보를 배치 애플리케이션에 전달해야 한다.

API와 마찬가지로 비밀 정보는 쿠버네티스 시크릿을 사용한다.

액세스 키 확인

envsubst < 42_batch_secrets_k8s.yaml.template | kubectl apply -f -
secret/batch-secret-config created

해당 명령어를 사용해 시크릿을 생성한다.

2.6.8 입력 파일 업로드

AWS CLI를 이용해 배치 어플리케이션이 사용할 입력 파일을 버킷에 업로드한다. CS는 batch-app/sample_dat/normal에 포함되어 있다.

aws s3 sync ../batch-app/sample_data/normal s3://eks-work-batch-hojin/locationData --delete --include "*" --acl public-read
upload: ../batch-app/sample_data/normal/sample_location2.csv to s3://eks-work-batch-hojin/locationData/sample_location2.csv
upload: ../batch-app/sample_data/normal/sample_location1.csv to s3://eks-work-batch-hojin/locationData/sample_location1.csv

2.6.9 배치 애플리케이션 배포

배치 애플리케이션을 배포하기 전에 DB 내용과 S3 버킷을 확인한다.

데이터베이스 내용 확인

bastion host에 Systems Manager를 통해 접속한 후 데이터를 확인한다.

데이터베이스를 체크하고

S3 파일을 확인한다

배치 애플리케이션 배포

크론잡을 이용해 5의 배수의 시점에 동작하도록 설정한다.

ECR_HOST=566620743708.dkr.ecr.ap-northeast-2.amazonaws.com envsubst < 43_cronjob_k8s.yaml.template | kubectl apply -f -
cronjob.batch/batch-app created
kubectl get all
NAME                               READY   STATUS    RESTARTS   AGE
pod/backend-app-59b66ccd45-fmz8k   1/1     Running   0          7h9m
pod/backend-app-59b66ccd45-q8pxk   1/1     Running   0          7h9m

NAME                          TYPE           CLUSTER-IP       EXTERNAL-IP                                                                   PORT(S)          AGE
service/backend-app-service   LoadBalancer   10.100.118.202   ac415c77bb14c4c45a1ad43a51570d97-482146193.ap-northeast-2.elb.amazonaws.com   8080:31069/TCP   6h55m

NAME                          READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/backend-app   2/2     2            2           7h9m

NAME                                     DESIRED   CURRENT   READY   AGE
replicaset.apps/backend-app-59b66ccd45   2         2         2       7h9m

NAME                      SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
cronjob.batch/batch-app   */5 * * * *   False     0        <none>          101s

cron job이 실행중임을 알 수 있다.

kubectl get all
NAME                               READY   STATUS             RESTARTS      AGE
pod/backend-app-59b66ccd45-fmz8k   1/1     Running            0             7h10m
pod/backend-app-59b66ccd45-q8pxk   1/1     Running            0             7h10m
pod/batch-app-28626500-lmcdm       0/1     CrashLoopBackOff   1 (14s ago)   30s

NAME                          TYPE           CLUSTER-IP       EXTERNAL-IP                                                                   PORT(S)          AGE
service/backend-app-service   LoadBalancer   10.100.118.202   ac415c77bb14c4c45a1ad43a51570d97-482146193.ap-northeast-2.elb.amazonaws.com   8080:31069/TCP   6h57m

NAME                          READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/backend-app   2/2     2            2           7h10m

NAME                                     DESIRED   CURRENT   READY   AGE
replicaset.apps/backend-app-59b66ccd45   2         2         2       7h10m

NAME                      SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
cronjob.batch/batch-app   */5 * * * *   False     1        30s             3m7s

NAME                           COMPLETIONS   DURATION   AGE
job.batch/batch-app-28626500   0/1           30s        30s

5분 후 배치 어플리케이션이 실행됐음을 알 수 있다.

2.7 예제 애플리케이션 환경 삭제

리소스 삭제는 구축의 반대이며 쿠버네티스상의 리소스는 한 번에 삭제한다.

  • 쿠버네티스상의 애플리케이션과 서비스 삭제

  • EKS 클러스터 삭제

  • 배치 애플리케이션용 S3 버킷 비우기

  • 프런트엔드 애플리케이션용 S3 버킷 비우기

  • 배치 애플리케이션용 S3 버킷 삭제

  • 프런트엔드 애플리케이션용 S3 버킷과 CloudFront 배포 삭제

  • 데이터베이스와 배스천 호스트 삭제

  • 기본 리소스 삭제

Last updated