Among Us - Yellow Crewmate [Code Pipeline] EKS와 Code Pipeline를 사용하여 CI/CD 구축하기

DevOps/CI CD

[Code Pipeline] EKS와 Code Pipeline를 사용하여 CI/CD 구축하기

감쟈! 2021. 5. 7. 01:24

 

 

 

 

 

이번에 해볼 실습은 지난 글에서 구축했던 EKS 클러스터를 사용하여 AWS Code Pipeline로 CI/CD를 구축하고 이벤트가 발생할 때마다 Slack으로 사용자에게 알람 메시지가 수신되도록 해보는것이다.

 

아직 EKS 클러스터가 구성되어 있지 않다면 https://potato-yong.tistory.com/126?category=853010  글로 가서 

생성해주고 오도록 하자

 

 

위의 아키텍쳐를 순서대로 나열해보면

 

1. User가 Git Repository에 Push

 

2. Git Repository에 있는 소스 코드 빌드

 

3. Docker 이미지를 ECR에 업로드

 

4. ECR에 있는 Docker 이미지를 EKS 클러스터에 배포

 

5. Pipeline 이벤트 (Success/Fail) 발생 시, Cloud Watch Events로 이벤트 전송

 

6. Cloud Watch Events에서 Lambda 함수를 호출

 

7. Lambda 함수로 Slack에 알람 전송

 

8. Slack에서 User에서 알람 메시지 전송

 


실습 진행 순서는 다음과 같다

 

1. Git Repository 생성

 

2. AWS Code Build 생성

 

3. AWS Code Pipeline 생성

 

4. EKS에 도커 이미지 배포 

 

5. Slack Webhook 생성

 

6. AWS Lambda 함수 생성

 

7. AWS Cloud Watch Events 생성

 

8. Slack 알람 메시지 확인

 


1. Git Repository 생성

 

우선 본인의 GitHub에서 CodeBuild에 사용할 Repository를 생성해 주도록 하자.

 

 

 

 

2. AWS CodeBuild 생성

 

다음은 CodeBuild를 생성해 보도록 하자.

 

1. AWS 콘솔에서 Codebuild에 접속 후 Build projects > Create build project를 클릭하여 Codebuild를 생성해준다.

 

2. Codebuild 프로젝트의 이름을 적어준다.

 

 

3. Source provider를 GitHub를 지정하고 Github에 인증하여 이전에 생성해둔  Repository를 사용하도록 한다.

 

 

3. Environment 에서는 아래와 같이 설정해준다. CodeBuild에서 도커 빌드를 진행할 예정이기 때문에 Privileged를 체크해준다. 

 

4.  Environment에서 'Additional configuration'을 클릭해 다음과 같이 EKS에 사용했던 VPC와 subnet을 추가해준다.

 

 

5. Security groups은 default로 정하고 환경변수를 설정해준다. 이 환경변수는 Codebuild가 빌드시 사용되는 buildspec.yaml 에서 사용된다.

 

 

6.  Buildspec은 Codebuild가 빌드시 참조하는 스크립트 파일이다. buildspec.yaml 이라고 지정하고 codebuild 프로젝트를 생성한다.

 

 


3. AWS Code Pipeline 생성

 

이제 앞에서 생성했던 Git Repository와 Codebuild로 Pipeline을 생성해 보도록 하자.

 

 

1. AWS 콘솔에서 CodePipeline에 접속 후,  'Create pipeline'을 클릭하여 새 파이프라인을 생성해주자.

 

2. Pipeline의 이름과 새 역할을 선택한 후 다음으로 넘어가자

 

 

3. Source provide를 GitHub로 지정하고 Connected를 눌러 연결한다. 그리고 사용할 Repository를 선택해주자.

 

 

4. Build provide는 AWS CodeBuild로 지정해주고 앞에서 생성했던 Codebulid 프로젝트를 선택하여 pipeline을 생성한다.


4. EKS에 도커 이미지 배포

 

이번 실습에서 사용되는 파일은 다음과 같다.

 

1. Dockerfile

2. index.html

3. Deployment.yaml

4. Service.yaml

5. buildspec.yaml

 

 

각각 파일을 작성해주도록 하자.

 

 

 

1. Dockerfile

테스트를 진행하기 위해서 간단하게 Dockerfile로 아파치를 설치하여 실행할 예정이다.

Dockerfile은 git Repository의 루트 디렉터리에 생성한다.

# ./Dockerfile

FROM ubuntu:18.04
RUN apt-get update
RUN apt-get install apache2 -y
COPY index.html /var/www/html
EXPOSE 80
CMD apachectl -DFOREGROUND

 

 

2. index.html

index.hmtl 파일은 Dockerfile로 생성한 아파치로 띄울  테스트 웹페이지

git repository 루트 디렉터리에 생성한다.

# ./index.html

<h1>Pratice ~~~~~</h1>
hello
potato
hihihihihih

 

2. Deployment.yaml

Deployment는 git repository에서 EKS 폴더안에 생성하였다. Dockerfile로 이미지를 만들어 ECR에 업로드하면, 그 이미지를 EKS 클러스터에 배포하도록 되어있다.

# ./EKS/Deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: eks-deploy
spec:
  replicas: 1
  selector:
    matchLabels:
      app: eks
  template:
    metadata:
      labels:
        app: eks
    spec:
      containers:
        - name: eks
          image: AWS_ECR_URI
          ports:
            - containerPort: 80
          imagePullPolicy: Always
          env:
            - name: DATE
              value: 'DATE_STRING'

 

 

3. Service.yaml

Service.yaml 파일도 git repository에서 EKS 폴더안에 생성하였다. Service는 쿠버네티스 클러스터가 외부와 통신할 수 있도록 LoadBalancer로 작성한다. 이때 아파치에 접속할 때, 포트 80을 사용하여 접속하게 된다

# ./EKS/Service.yaml

apiVersion: v1
kind: Service
metadata:
  name: eks-svc
spec:
  ports:
    - name: "80"
      port: 80
      targetPort: 80
  selector:
    app: eks
  type: LoadBalancer

 

 

 

4. buildspec.yaml

buildspec.yaml은 git repository 루트 디렉터리에 생성한다. 이 파일은 codebuild 생성할때 정의했던 파일로 Codebuild가 빌드할 때 이 파일을 참조하게 된다.

 

install :  런타임으로 도커를 설치하고, EKS 클러스터에 도커이미지를 배포하기 위해서 kubectl을 설치하고 kubeconfig를 생성하여 kubectl 명령어를 사용할 수 있게 한다.

pre_build : ECR에 로그인 한다

build :  Dockerfile을 빌드하고 ECR에 Push 한다

post_build : ECR의 URI를 AWS_ECR_URI로 치환하여 사용한다. 그리고 deployment.yaml 파일과 service.yaml 파일을 배포한다.

# ./buildspec.yaml

version: 0.2
phases:
  install:
    runtime-versions:
      docker: 18
    commands:
      - curl -o kubectl https://amazon-eks.s3.us-west-2.amazonaws.com/1.19.6/2021-01-05/bin/linux/amd64/kubectl
      - chmod +x ./kubectl
      - mv ./kubectl /usr/local/bin/kubectl
      - mkdir ~/.kube
      - aws eks --region ap-northeast-2 update-kubeconfig --name eks
      - kubectl get po -n kube-system
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/
  build:
    commands:
      - echo Building the Docker image
      - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG .
      - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
      - docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG

  post_build:
    commands:
      - AWS_ECR_URI=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
      - DATE='date'
      - echo Build completed on $DATE
      - sed -i.bak 's#AWS_ECR_URI#'"$AWS_ECR_URI"'#' ./EKS/deploy.yaml
      - sed -i.bak 's#DATE_STRING#'"$DATE"'#' ./EKS/deploy.yaml
      - kubectl apply -f ./EKS/deploy.yaml
      - kubectl apply -f ./EKS/svc.yaml

 

 

 

이제 위의 파일들을 Git Repository에 Push를 진행할 때마다 Pipeline이 작동한다.

 

 

 

하지만 , 이대로 Pipeline이 실행되면 위의 사진과는 다르게  Git Push는 성공하지만 Codebuild는 실패할 것이다.

Log를 확인하고 Log에 나오는 에러메시지를 통해 해결할 수 있다.

 

나는 이 과정에서 에러가 너무 많이 나오고 헤맸었다..

대충 내가 겪었었던 에러 해결법들은 정리해본다.

 

1. pre_build 단계에서 ECR 로그인이 안되는 에러.......

[AWS CLI를 사용하여 ECR에 로그인하는 방법이 CLI 1 버전과 CLI 2버전이 다르다, 자신의 버전을 확인해보자.. 그 후에 Codebuild가 ECR에 접근할 수 있도록 권한을 부여해주어야 한다. Codebuild에 사용했던 Role에 AmazonEC2contianerRegistryPowerUser 정책을 추가 해주었더니 에러가 나오지않고 다음으로 넘어간다.]

 

2. An error occurred (AccessDeniedException) when calling the DescribeCluster operation 에러......

[마찬가지로 권한문제 인것같다. Codebuild에 사용했던 Role에 eks:DescribeCluster 권한을 추가한다.]

 

3. error: unable to recognize "./k8s/deployment.yaml": Unauthorized 에러.....

[kubectl 명령어가 안먹힌다.. 이것도 무슨 권한문제라고 뜬다... 여기서도 Codebuild에 EKS 클러스터에 접근할 수 있는 권한이 없기 때문이다. 아래와 같은 명령어를 입력하여 configmaps을 aws-auth.yaml 파일로 추출해내서 수정해주자.]

$ kubectl get configmaps aws-auth -n kube-system -o yaml > aws-auth.yaml

aws-auth.yaml 파일을 열어서 아래의 내용을 추가해주자

    - rolearn: arn:aws:iam::123456712345:role/[CodeBuild Role 이름]
      username: [CodeBuild Role 이름]
      groups:
        - system:masters

 

여러번의 에러 끝에 code build하는것 까지 성공했다.

 

 

Codebuild의 로그도 확인해보면 무사히 deployment와 Service가 배포가 된다.

 

 

아래의 명령어로 codebuild를 통해서 제대로 EKS 클러스터에 배포가 되었는지 확인해보자

kubectl get deploy

kubectl get svc

$ kubectl get deploy
$ kubectl get svc

 

 

Service를 LoadBalancer 유형으로 만들어서 EXTERNAL-IP가 출력된다.

EXTERNAL-IP와 80포트로 접속을 했을때, 아래와 같이 index.html 파일로 만든 텍스트가 웹페이지에 출력되면 제대로 배포가 완료된 것이다.

5. Slack Webhook 생성

 

사용자에게 Pipeline 성공/실패 이벤트 알람을 보내기 위해서 Slack Webhook을 생성해야 한다.

Slack 으로 알림 메시지를 받을 워크스페이스와 채널이 생성되어 있어야 한다. 채널이 없는 분들은 채널부터 만들고 오자...

 

 

 

이제 다음과 같이 실습을 순서대로 진행해보자

 

1. Slack Webhook을 생성하기 위해서는 api.slack.com/ 링크로 접속해서 검색창에 Webhook을 검색하여 'Sending messages using Incoming Webhooks' 을 선택한다.

 

 

2. app creation page를 눌러서 Webhook 생성 페이지로 넘어가자

 

 

 

3. App Name을 입력하고 Slack 알람을 보낼 워크스페이스를 선택한다

 

 

4. Incoming Webhooks 을 클릭하여 Webhook을 생성하자

 

 

 

5. Webhooks 을 활성화 시키고,  하단의 'Add New Webhook to Workspace'을 클릭해 알람을 받을 채널을 선택한다.

 

6. 그러면 해당 채널에 Webhooks URL이 생성되는데 이 URL은 나중에 Lambda 함수를 생성할 때, Lambda 함수에서 Slack으로 알람을 보내는데에 사용된다. 잘 적어두도록 하자.

 

6. AWS Lambda 함수 생성

 

이제 Slack에 알람을 보낼 Lambda 함수를 생성해 줄 차례다.

 

 

1. AWS 콘솔에서 AWS Lambda로 접속하여 'Create function'을 클릭하여 Lambda 함수를 생성해주자.

 

 

2. Fuction name을 입력해주고 Lambda에 사용할 런타임 언어를 정해주자. 

 

 

 

 

3. Lambda 환경 변수에 Slack 알람을 보낼 채널과 아까 생성했던 Webhooks URL 의 /services 부터 적어준다

 

 

 

4. Lambda 함수로 다음과 같이 Slack Webhooks을 호출하는 코드를 작성해보자.

 

(하지만, 코딩 찐따인 나는.... node.js 예제를 사용해주자...코딩 공부도 하고 싶다 ㅠ)

 

var services = process.env.SERVICES;  
var channel = process.env.CHANNEL;  

var https = require('https');
var util = require('util');

function toYyyymmddhhmmss(date) {

    if(!date){
        return '';
    }

    function utcToKst(utcDate) {
        return new Date(utcDate.getTime() + 32400000);
    }

    function pad2(n) { return n < 10 ? '0' + n : n }

    var kstDate = utcToKst(date);
    return kstDate.getFullYear().toString()
        + '-'+ pad2(kstDate.getMonth() + 1)
        + '-'+ pad2(kstDate.getDate())
        + ' '+ pad2(kstDate.getHours())
        + ':'+ pad2(kstDate.getMinutes())
        + ':'+ pad2(kstDate.getSeconds());
}

var formatFields = function(event) {
    var fields  = [];

    // Make sure we have a valid response
    if (event) {
        fields = [
            {
                "title" : "type",
                "value" : event['detail-type'],
                "short" : true
            },
            {
                "title" : "time",
                "value" : toYyyymmddhhmmss(new Date(event.time)),
                "short" : true
            },
            {
                "title" : "region",
                "value" : event.region,
                "short" : true
            },
            {
                "title" : "link",
                "value" : "https://"+event.region+".console.aws.amazon.com/codesuite/codepipeline/pipelines/"+event.detail.pipeline+"/executions/"+event.detail['execution-id']+"/timeline?region="+event.region,
                "short" : true
            },
            {
                "title" : "pipeline",
                "value" : event.detail.pipeline,
                "short" : true
            },
            {
                "title" : "execution_id",
                "value" : event.detail['execution-id'],
                "short" : true
            },
            {
                "title" : "state",
                "value" : event.detail.state,
                "short" : true
            }
        ];

    }

    return fields;
};

exports.handler = function(event, context) {
    var postData = {
        "channel": channel, 
        "text": "*" + event.detail.pipeline + " Notify" + "*"   
    };

    var fields = formatFields(event);

    postData.attachments = [
        {
            "color": event.detail.state == "SUCCESS" ? "good" : (event.detail.state == "STARTED" ? "good" : "danger"),
            "fields": fields
        }
    ];

    var options = {
        method: 'POST',
        hostname: 'hooks.slack.com',
        port: 443,
        path: services
    };

    var req = https.request(options, function(res) {
        res.setEncoding('utf8');
        res.on('data', function (chunk) {
            context.done(null);
        });
    });

    req.on('error', function(e) {
        console.log('problem with request: ' + e.message);
    });

    req.write(util.format("%j", postData));
    req.end();
};

7. AWS CloudWatch Events 생성

 

CloudWatch로 Pipleline의 상태를 감지하다가 Pipeline의 성공/실패 이벤트가 발생하면 Lambda 함수를 호출 한다.

 

Lambda 함수의 트리거가 되는 CloudWatch Events를 생성해보자. 

 

 

1. AWS 콘솔에서 CloudWatch에 접속한 뒤, Events > Rules > Create rule를 클릭하여 이벤트 규칙을 생성해주자. 

 

 

2. Event Pattern 을 체크하여 ,특정 이벤트 발생할 때 트리거가 발생하도록 한다.

Service Name 으로는 CodePipeline을 선택하고 

Event Type에서 CodePipeline의 상태가 변경되면 트리거가 발생하도록 한다.

 

 

 

3. 트리거를 발생시킬 타켓을 설정해주자.

 

Target은 Lambda function

Function은 이전에 만들어둔 Slack 알람 전송 Lambda 함수.

설정값은 기본값으로 해주었다.

 

 

4. 그 후 이벤트 규칙의 이름을 정해주고 생성해주면 끝

 

 

 

5. CloudWatch Events 가 생성이 되었으면 다시 Lambda로 돌아가서 Lambda와 CloudWatch Event가 연결되어 있는지 확인해주자.


8. Slack 알람 메시지 확인

 

이제 마지막으로 Pipeline이 실행될 때, Slack으로 알림 메시지가 오는지 확인해주자.

 

 

 

1. Git push를 통해서 CodePipeline을 시작시켜주자.

 

2. CodePipe의 Source 단계가 시작되는것을 확인해주자

 

 

3. 그리고 Slack을 확인해보면 다음과 같이 Slack 채널에 메시지가 오는것을 확인할 수 있다.

 

 

4. 다음으로 Build 단계에서 CodeBuild가 실행되는 것을 확인해주자.

 

 

 

5. CodeBuild가 성공하면 SUCCEEDED 라는 메시지가 오게 되면 성공..