Terraform でEKS のGitOps パイプラインを構築する

# はじめに
本記事では、Terraform で[EKS](https://aws.amazon.com/jp/eks/)のGitOps パイプラインのデモ環境を構築する手順とデモした内容について、記載しています。

GitOps について学習する時の参考になれば幸いです。

### GitOps とは

https://www.weave.works/technologies/gitops/

引用:
>GitOpsは、次の2つに要約できます。
>1. Kubernetesおよびその他のクラウドネイティブテクノロジーのオペレーティングモデル。コンテナ化されたクラスターとアプリケーションのGitのデプロイ、管理、モニタリングを統合する一連のベストプラクティスを提供します。
>2. アプリケーションを管理するための開発者エクスペリエンスへの道。ここでは、エンドツーエンドのCICDパイプラインとGitワークフローが操作と開発の両方に適用されます。 

# Terraform で構築する全体構成図
**GitOps パイプライン**
![EKS_GitOps_Demo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/1a03510c-ae97-35eb-6fef-f1fe6a97b18b.png)

**EKS クラスター**
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/9d7be9be-fc17-9fa7-20e3-d7b03ba6d71e.png"　 width=70%>

# 構成の概要
GitOps は、下記のコンポーネントで構成しています。
|機能|コンポーネント名|説明|
|:---|:---|:---|
|ソースの管理|CodeCommit|アプリのソースとKubernetes のマニフェストを管理します。|
|イメージの管理|ECR|アプリのイメージを管理します。|
|CI|CodePipeline + CodeBuild|アプリのソースの変更をトリガーにして、イメージをビルドし、ECR にプッシュします。<br>プッシュ後、イメージの差し替えをマニフェストのリポジトリにプルリクエストします。|
|CD|[Argo CD](https://argo-cd.readthedocs.io/en/stable/)|マニフェストの変更を監視し、変更を検出すると、Kubernetes の実行環境に反映します。|

# Terraform のコードと構成
https://github.com/okubo-t/aws-tf-eks-gitops-demo

```terminal
$ tree aws-tf-eks-gitops-demo
aws-tf-eks-gitops-demo
├── eks-gitops-demo-app
│   ├── Dockerfile
│   └── index.html
├── eks-gitops-demo-k8s
│   ├── deployment.yaml
│   └── service.yaml
├── main.tf
├── modules
│   └── ci
│       ├── buildspec.yml
│       ├── cloudwatch_event.tf
│       ├── cloudwatch_logs.tf
│       ├── codebuild.tf
│       ├── codecommit.tf
│       ├── codepipeline.tf
│       ├── ecr.tf
│       ├── iam.tf
│       ├── iam_https_user.tf
│       ├── outputs.tf
│       ├── s3.tf
│       └── variables.tf
├── outputs.tf
├── provider.tf
└── versions.tf
```
# デモ環境を構築するメインのファイル
```terraform:main.tf
locals {
  aws_profile = "YOUR AWS ACCOUNT PROFILE NAME"
  aws_region  = "ap-northeast-1"
}

module "ci" {
  source = "./modules/ci"

  # PREFIX
  prefix = "eks-gitops"
  # ENVIRONMENT PREFIX
  env = "demo"

  # IMAGE CODECOMMIT REPOSITORY NAME
  codecommit_repository_for_image = "eks-gitops-demo-app"
  # BRANCH NAME
  branch_name = "master"
  # ECR REPOSITORY NAME
  ecr_name = "eks-gitops-demo-app"

  # K8S CODECOMMIT REPOSITORY NAME
  codecommit_repository_for_k8s = "eks-gitops-demo-k8s"
}

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "3.14.2"

  # VPC NAME
  name = "eks-gitops-demo-vpc"
  # VPC CIDR
  cidr = "10.0.0.0/16"
  # SUBNET
  azs             = ["ap-northeast-1a", "ap-northeast-1c"]
  public_subnets  = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnets = ["10.0.3.0/24", "10.0.4.0/24"]
  # NAT GATEWAY
  enable_nat_gateway = true
  single_nat_gateway = true
  # DNS
  enable_dns_hostnames = true
}

module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "18.26.0"

  # EKS CONTROL PLANE 
  cluster_name                    = "eks-gitops-demo"
  cluster_version                 = "1.22"
  cluster_endpoint_private_access = false
  cluster_endpoint_public_access  = true

  # EKS CLUSTER VPC AND SUBNETS
  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnets

  # EKS MANAGED NODE GROUPS
  eks_managed_node_groups = {
    gitops-demo = {
      desired_size   = 2
      instance_types = ["t3.small"]
    }
  }

  node_security_group_additional_rules = {
    admission_webhook = {
      description                   = "Admission Webhook"
      protocol                      = "tcp"
      from_port                     = 0
      to_port                       = 65535
      type                          = "ingress"
      source_cluster_security_group = true
    }

    ingress_node_communications = {
      description = "Ingress Node to node"
      protocol    = "tcp"
      from_port   = 0
      to_port     = 65535
      type        = "ingress"
      self        = true
    }

    egress_node_communications = {
      description = "Egress Node to node"
      protocol    = "tcp"
      from_port   = 0
      to_port     = 65535
      type        = "egress"
      self        = true
    }
  }
}

data "aws_eks_cluster" "cluster" {
  name = module.eks.cluster_id
}

data "aws_eks_cluster_auth" "cluster" {
  name = module.eks.cluster_id
}
```
# GitOps パイプラインのデモ環境の構築
## 前提条件
- Terraform がセットアップされていること
- kubectl がセットアップされていること
- AWS CLI がセットアップされていること
- Argo CDのCLI がセットアップされていること

## 動作確認環境
```
$ terraform version
Terraform v1.1.9

$ aws --version
aws-cli/2.7.12 Python/3.9.11 Darwin/19.6.0 exe/x86_64 prompt/off

$ kubectl version --client --short
Client Version: v1.21.2

$ argocd version
argocd: v2.4.3+471685f.dirty
```
## EKS クラスターのセットアップ
Terraform を実行します。
```terraform
$ terraform init
$ terraform plan
$ terraform apply
結果:
Apply complete! Resources: 72 added, 0 changed, 0 destroyed.
```

下記コマンドを実行し、Terraform のoutput を控えておきます。
```terraform
$ terraform output
結果:
CODECOMMIT_IMAGE_REPOSITORY_URL = "https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/eks-gitops-demo-app"
CODECOMMIT_INFRA_REPOSITORY_URL = "https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/eks-gitops-demo-k8s"
ECR_REPOSITORY_URL = "[AWSアカウントID].dkr.ecr.ap-northeast-1.amazonaws.com/eks-gitops-demo-app"
app_name = "eks-gitops-demo-app"
argocd_sync_password = <sensitive>
argocd_sync_username = <sensitive>
eks_cluster_name = "eks-gitops-demo"
```

構築したEKSクラスターに接続できることを確認します。
```terminal
$ aws eks update-kubeconfig \
  --region ap-northeast-1 \
  --name eks-gitops-demo \
  --profile [YOUR AWS ACCOUNT PROFILE NAME]
結果:
Added new context arn:aws:eks:ap-northeast-1:[AWSアカウントID]:cluster/eks-gitops-demo to /Users/*****/.kube/config
```

## マニフェストファイルをリポジトリにプッシュ
下記2つのマニフェストファイルを `terraform output`の`CODECOMMIT_INFRA_REPOSITORY_URL` 先（**マニフェストのリポジトリ**）にプッシュします。

※`[AWSアカウントId]`を自環境に変更します。
```yaml:deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: eks-gitops-demo-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: eks-gitops-demo-app
  template:
    metadata:
      labels:
        app: eks-gitops-demo-app
    spec:
      containers:
        - name: eks-gitops-demo-app
          image: [AWSアカウントId].dkr.ecr.ap-northeast-1.amazonaws.com/eks-gitops-demo-app:latest
          ports:
            - name: http
              containerPort: 80
```

```yaml:service.yaml
apiVersion: v1
kind: Service
metadata:
  name: eks-gitops-demo-app
spec:
  type: LoadBalancer
  selector:
    app: eks-gitops-demo-app
  ports:
    - port: 80
      targetPort: http
```
![INFRA_CodeCommit_REPO.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/7a216342-c682-dbb9-7eb0-2b08eb7abf98.png)


## Argo CDのセットアップ
kubectl コマンドを使って、Argo CDをデプロイします。
```
$ kubectl create namespace argocd
結果:
namespace/argocd created

$ kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
```

Argo CDのPod が稼働していることを確認します。
```
$ kubectl get pod -n argocd
結果:
NAME                                                READY   STATUS    RESTARTS   AGE
argocd-application-controller-0                     1/1     Running   0          25s
argocd-applicationset-controller-5f7d8fffb7-s2n6n   1/1     Running   0          26s
argocd-dex-server-c6dcf7fdb-6qqll                   1/1     Running   0          26s
argocd-notifications-controller-544ff7766d-pljxl    1/1     Running   0          26s
argocd-redis-75bdb869bf-8b2tv                       1/1     Running   0          26s
argocd-repo-server-649b4b66bd-4g22t                 0/1     Running   0          26s
argocd-server-5df946ff78-zq5tl                      0/1     Running   0          26s
```

下記コマンドで、Argo CDの管理者用のデフォルトのパスワードを取得します。
```terminal
$ kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d; echo
結果:
Argo CDの管理者用のデフォルトのパスワード
```

下記コマンドで、ローカルポートをArgo CDサーバーにポートフォワードします。
```
$ kubectl port-forward svc/argocd-server -n argocd 8080:443
結果:
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
```

下記コマンドで、Argo CDサーバーにログインします。
```terminal
$ argocd login localhost:8080 \
  --username admin \
  --password [Argo CD管理者用のデフォルトのパスワード] \
  --insecure \
  --port-forward-namespace argocd
結果:
'admin:login' logged in successfully
Context 'localhost:8080' updated
```

下記コマンドで、Argo CDからマニフェストのリポジトリにHTTPS 接続する時に使用するIAMの情報を参照します。
```terraform
$ terraform output argocd_sync_username
結果:
ユーザー名
$ terraform output argocd_sync_password
結果:
パスワード
```

下記コマンドで、マニフェストのリポジトリをArgo CDサーバーに追加します。
```
$ CODECOMMIT_INFRA_REPOSITORY_URL=https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/eks-gitops-demo-k8s

$ argocd repo add ${CODECOMMIT_INFRA_REPOSITORY_URL} \
  --username ユーザー名 \
  --password パスワード
結果:
Repository 'https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/eks-gitops-demo-k8s' added
```

Argo CDのアプリケーションとマニフェストのリポジトリを同期させる設定をします。
```
$ kubectl create namespace eks-gitops-demo-app
結果:
namespace/eks-gitops-demo-app created

$ argocd app create eks-gitops-demo-app \
  --repo ${CODECOMMIT_INFRA_REPOSITORY_URL} \
  --revision master \
  --path ./ \
  --dest-server https://kubernetes.default.svc \
  --dest-namespace eks-gitops-demo-app \
  --sync-policy automated \
  --auto-prune
結果:
application 'eks-gitops-demo-app' created
```

アプリケーションの同期設定が登録されていることを確認します。
```
$ argocd app list
NAME                 CLUSTER                         NAMESPACE            PROJECT  STATUS  HEALTH   SYNCPOLICY  CONDITIONS  REPO                                                                              PATH  TARGET
eks-gitops-demo-app  https://kubernetes.default.svc  eks-gitops-demo-app  default  Synced  Healthy  Auto-Prune  <none>      https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/eks-gitops-demo-k8s  ./    master
```

ブラウザで `https://localhost:8080` にアクセスして、Argo CDのGUIに接続します。
|項目|入力内容|
|:---|:---|
|username|admin|
|Password|Argo CDの管理者用のデフォルトのパスワード|

**ログイン画面**
![argocd_admin_login.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/b48af1a0-7db4-f510-e9ec-b1061691cbed.png)

GUI上でもアプリケーションが表示されていることを確認します。
![ArgoCD_App.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/61104968-aaf5-d4c6-e3c8-d70b741207a1.png)

これで、GitOps パイプラインのデモ環境の構築が完了です。

# GitOps パイプラインのデモ環境の実行
構築したデモ環境で、アプリケーションをデプロイします。
## アプリのソースをリポジトリにプッシュ
下記2つのファイルを `terraform output`の`CODECOMMIT_INFRA_REPOSITORY_URL` 先（**アプリのソースのリポジトリ**）にプッシュします。

```Dockerfile:Dockerfile
FROM nginx:latest 
WORKDIR /usr/share/nginx/html
COPY index.html index.html
```

```index.html
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="utf-8">
    <title>GitOps Demo</title>
</head>

<body>
    <h1>GitOps Demo App v1</h1>
</body>

</html>
```

## CodeBuild の実行
アプリのソースのリポジトリの変更をトリガーにCodePipeline が開始され、CodeBuild　が実行されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/49f466d8-032b-6690-b37f-2586322b4256.png"　 width=75%>

## イメージをECR をプッシュ
CodeBuild で、イメージがビルドされ、ECRにプッシュされます。
![ecr_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/3a11af92-5e08-32c8-871b-d80dabec38a4.png)

## マニフェストのリポジトリにプルリクエスト
CodeBuild で、ECRにプッシュされたイメージを差し替えするためのプルリクエストをマニフェストのリポジトリに送信します。
![Pull_Request_Send.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/77bab945-72b7-d3a4-37e7-f2931766bb1a.png)

## マージ
マニフェストのリポジトリへのプルリクエストの内容を確認して、マージします。
![Merge_Send.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/a4335073-c8e2-277e-a327-e770960b0267.png)

## デプロイ
Argo CDが、マニフェストのリポジトリの変更を検知すると、Kubernetes の実行環境に反映します。
```
$ kubectl get pod,svc,deploy -n eks-gitops-demo-app
NAME                                      READY   STATUS    RESTARTS   AGE
pod/eks-gitops-demo-app-7f54cfd78-cpcqm   1/1     Running   0          101s
pod/eks-gitops-demo-app-7f54cfd78-wmbcj   1/1     Running   0          98s

NAME                          TYPE           CLUSTER-IP      EXTERNAL-IP                                                                   PORT(S)        AGE
service/eks-gitops-demo-app   LoadBalancer   172.20.232.32   a3114a987111542a2affe40c67071c69-111111111.ap-northeast-1.elb.amazonaws.com   80:30146/TCP   17m

NAME                                  READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/eks-gitops-demo-app   2/2     2            2           17m
```

## 動作確認
下記コマンドで出力されたEXTERNAL-IP のLaoadBalancer のDNS名を確認します。
```
% kubectl get svc -n eks-gitops-demo-app 
NAME                  TYPE           CLUSTER-IP      EXTERNAL-IP                                                                   PORT(S)        AGE
eks-gitops-demo-app   LoadBalancer   172.20.232.32   a3114a987111542a2affe40c67071c69-111111111.ap-northeast-1.elb.amazonaws.com   80:30146/TCP   18m
```
ブラウザでLaoadBalancer のDNS名にHTTPアクセスして、アプリケーションがデプロイされたことを確認します。
![GitOps_Demo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/6b9e4e41-747d-3fbf-fc20-07969c15bfef.png)

Argo CDのGUI上でも確認できます。
**Node**
![ArgoCD_EKS_Node.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/962cde8a-270c-c2c2-d21c-5f41143cc5e4.png)

**NetWork**
![GitOps_Demo_Network.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/283246/23b81a27-12fd-b1fa-7c17-cda0653d15d8.png)
これで、GitOps パイプラインを使ったアプリケーションのデプロイが完了です。

# リソースのクリーンアップ
マニフェストファイルのリポジトリの`service.yaml` を削除して、Kubernetes の環境からELBを削除し、下記コマンドで確認します。
```
$ kubectl get svc -n eks-gitops-demo-app 
No resources found in eks-gitops-demo-app namespace.
```

下記のコマンドで、すべてのリソースを削除します。
```
$ terraform destroy
Destroy complete! Resources: 72 destroyed.
```

# さいごに
GitOps パイプラインの入門的な内容ですが、参考になれば幸いです。