Pulumi(golang)でEKS環境を構築する

# はじめに
AWS環境構築をコード化するためのツールとしてAWS CDK、Terraform、Ansible、Pulumiなどがあります。
今回は記述言語をgolangとしてPulumiでEKS環境を構築します。

# Pulumiとは
「Modern Infrastructure as Code」を掲げた、クラウドインフラを構成するためのOSSです。
対応言語はJavaScript、TypeScript、Python、golangなどが挙げられます。
GCP、AWS、Azureに対応しており、Kubernetesリソースをデプロイすることも可能です。

まだ新しいこともあり公式ドキュメントや公式サンプルコードはTypeScript中心となっています。
普段バックエンドを触っている人にとってはgolangの方が嬉しいですね。

# 環境情報
macOS Mojave 10.14.1
pulumi: 1.7.0
golang: 1.12.4

# 実装
golangを使用して実装します。
事前にPulumiやgolangはインストールされていることは前提とします。

`$GOPATH/src/pulumi-eks`配下に以下のようにファイルを配置します。

```console
├── Pulumi.yaml
├── main.go
├── iam.go
├── network.go
└── eks.go
```

## `Pulumi.yaml`
プロジェクトのメタデータを`Pulumi.yaml`に記述します。

```yaml:Pulumi.yaml
name: pulumi-eks
runtime: go
description: pulumi EKS
```

## `main.go`
EKSリソースを作成するための関数呼び出しを`main.go`に記述します。

```go:main.go
package main

import (
	"github.com/pulumi/pulumi/sdk/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		err := createEks(ctx)
		if err != nil {
			return err
		}
		return nil
	})
	
}
```

## `iam.go`
EKSクラスタとEKSワーカーに付与するIAM Roleを`iam.go`に記述します。
このIAM Roleを関数の返り値とし、EKSクラスタ作成時に使用します。

```go:iam.go
package main

import (
	"encoding/json"
	"github.com/pulumi/pulumi-aws/sdk/go/aws/iam"
	"github.com/pulumi/pulumi/sdk/go/pulumi"
)

func createEksClusterRole(ctx *pulumi.Context, roleName string) (*iam.Role, error) {
	assumeRolePolicy, err  := getEksAssumeRolePolicy()
	if err != nil {
		return nil, err
	}
	role, err := iam.NewRole(ctx, roleName, &iam.RoleArgs{
		Name: roleName,
		AssumeRolePolicy: assumeRolePolicy,
	})
	if err != nil {
		return nil, err
	}

	_, err = iam.NewRolePolicyAttachment(ctx, "eks-cluster-policy", &iam.RolePolicyAttachmentArgs{
		PolicyArn: "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy",
		Role: role.Name(),
	})
	if err != nil {
		return nil, err
	}

	_, err = iam.NewRolePolicyAttachment(ctx, "eks-service-policy", &iam.RolePolicyAttachmentArgs{
		PolicyArn: "arn:aws:iam::aws:policy/AmazonEKSServicePolicy",
		Role: role.Name(),
	})
	if err != nil {
		return nil, err
	}

	return role, nil
}

func getEksAssumeRolePolicy() (interface{}, error) {
	policy, _ := json.Marshal(map[string]interface{}{
		"Version": "2012-10-17",
		"Statement": []map[string]interface{}{
			{
				"Effect": "Allow",
				"Principal": map[string]interface{}{
					"Service": "eks.amazonaws.com",
				},
				"Action": "sts:AssumeRole",
			},
		},
	})
	return string(policy), nil
}

func createEksWorkerRole(ctx *pulumi.Context, roleName string) (*iam.Role, error) {
	assumeRolePolicy, err  := getEc2AssumeRolePolicy()
	if err != nil {
		return nil, err
	}
	role, err := iam.NewRole(ctx, roleName, &iam.RoleArgs{
		Name: roleName,
		AssumeRolePolicy: assumeRolePolicy,
	})
	if err != nil {
		return nil, err
	}

	_, err = iam.NewRolePolicyAttachment(ctx, "eks-worker-policy", &iam.RolePolicyAttachmentArgs{
		PolicyArn: "arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy",
		Role: role.Name(),
	})
	if err != nil {
		return nil, err
	}

	_, err = iam.NewRolePolicyAttachment(ctx, "eks-cni-policy", &iam.RolePolicyAttachmentArgs{
		PolicyArn: "arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy",
		Role: role.Name(),
	})
	if err != nil {
		return nil, err
	}

	_, err = iam.NewRolePolicyAttachment(ctx, "ecr-policy", &iam.RolePolicyAttachmentArgs{
		PolicyArn: "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly",
		Role: role.Name(),
	})
	if err != nil {
		return nil, err
	}

	return role, nil
}

func getEc2AssumeRolePolicy() (interface{}, error) {
	policy, _ := json.Marshal(map[string]interface{}{
		"Version": "2012-10-17",
		"Statement": []map[string]interface{}{
			{
				"Effect": "Allow",
				"Principal": map[string]interface{}{
					"Service": "ec2.amazonaws.com",
				},
				"Action": "sts:AssumeRole",
			},
		},
	})
	return string(policy), nil
}
```

## `network.go`
VPCやサブネットを`network.go`で作成します。
AZは事前に設定するリージョンから検出されます。
サブネット情報などをまとめて返り値としています。

```go:network.go
package main

import (
	"fmt"
	"github.com/pulumi/pulumi-aws/sdk/go/aws"
	"github.com/pulumi/pulumi-aws/sdk/go/aws/ec2"
	"github.com/pulumi/pulumi/sdk/go/pulumi"
)

type Network struct {
	AvailabilityZones interface{}
	VpcId interface{}
	SubnetIds interface{}
}

func createNetwork(ctx *pulumi.Context) (*Network, error){
	azs, err := aws.LookupAvailabilityZones(ctx, &aws.GetAvailabilityZonesArgs{
		State: "available",
	})
	if err != nil {
		return nil, err
	}
	vpc, err := ec2.NewVpc(ctx, "eks-vpc", &ec2.VpcArgs{
		CidrBlock: "10.0.0.0/16",
	})
	if err != nil {
		return nil, err
	}

	ig, err := ec2.NewInternetGateway(ctx, "eks-ig", &ec2.InternetGatewayArgs{
		VpcId: vpc.ID(),
	})
	if err != nil {
		return nil, err
	}
	
	rt, err := ec2.NewRouteTable(ctx, "eks-rt", &ec2.RouteTableArgs{
		VpcId: vpc.ID(),
		Routes: []map[string]interface{}{{
			"cidrBlock": "0.0.0.0/0",
			"gatewayId": ig.ID(),
		},},
	})
	if err != nil {
		return nil, err
	}

	var subnetIds []pulumi.IDOutput
	usePrivateSubnets := false
	for i, az := range azs.Names.([]interface{}) {
		subnetName := fmt.Sprintf("eks-sn-%d", i)
		cidrBlock := fmt.Sprintf("10.0.%d.0/24", i)
		subnet, err := ec2.NewSubnet(ctx, subnetName, &ec2.SubnetArgs{
			VpcId: vpc.ID(),
			CidrBlock: cidrBlock,
			AvailabilityZone: az,
			MapPublicIpOnLaunch: !usePrivateSubnets,
		})
		if err != nil {
			return nil, err
		}

		rtaName := fmt.Sprintf("eks-rta-%d", i)
		_, err = ec2.NewRouteTableAssociation(ctx, rtaName, &ec2.RouteTableAssociationArgs{
			SubnetId: subnet.ID(),
			RouteTableId: rt.ID(),
		})
		if err != nil {
			return nil, err
		}
		subnetIds = append(subnetIds, subnet.ID())
	}

	network := new(Network)
	network.AvailabilityZones = azs
	network.VpcId = vpc.ID()
	network.SubnetIds = subnetIds

	return network, nil
}
```

## `eks.go`
`eks.go`でEKSクラスタとEKSワーカーを作成します。
その前段としてNetworkとIAMの関数を呼び出しています。
今回はManaged Node Groupsでワーカーを立てるので事前にSecurity Groupを指定する必要がありません。

```go:eks.go
package main

import (
	"github.com/pulumi/pulumi-aws/sdk/go/aws/eks"
	"github.com/pulumi/pulumi/sdk/go/pulumi"
)

func createEks(ctx *pulumi.Context) error {
	network, err := createNetwork(ctx)
	if err != nil {
		return err
	}

	eksClusterRole, err := createEksClusterRole(ctx, "eks-cluster-role")
	if err != nil {
		return err
	}

	eksWorkerRole, err := createEksWorkerRole(ctx, "eks-worker-role")
	if err != nil {
		return err
	}

	eksClusterName := "eks-cluster"
	eksCluster, err := eks.NewCluster(ctx, eksClusterName, &eks.ClusterArgs{
		Name: eksClusterName,
		RoleArn: eksClusterRole.Arn(),
		VpcConfig: map[string]interface{}{
			"subnetIds": network.SubnetIds,
		},
	})
	if err != nil {
		return err
	}

	eksWorkerName := "eks-worker"
	_, err = eks.NewNodeGroup(ctx, eksWorkerName, &eks.NodeGroupArgs{
		ClusterName: eksCluster.Name(),
		NodeGroupName: eksWorkerName,
		NodeRoleArn: eksWorkerRole.Arn(),
		ScalingConfig: map[string]int{
			"DesiredSize": 2,
			"MaxSize": 3,
			"MinSize": 1,
		},
		SubnetIds: network.SubnetIds,
	})
	if err != nil {
		return err
	}

	return nil
}
```

# 実行
作成したプラグラムを実行します。

## 1. スタックの作成

```console
$ pulumi stack init pulumi-eks
```

## 2. Pulumi AWSプラグインのインストール

```console
$ pulumi plugin install resource aws 1.17.0
```

## 3. AWSリージョンの設定

```console
$ pulumi config set aws:region ap-northeast-1
```

## 4. golangプログラムのコンパイル

```console
$ go get .
$ go install .
```

## 5. デプロイ

```console
$ pulumi up
Updating (eks):

     Type                              Name                Status      
 +   pulumi:pulumi:Stack               pulumi-eks-eks      created     
 +   ├─ aws:ec2:Vpc                    eks-vpc             created     
 +   ├─ aws:iam:Role                   eks-cluster-role    created     
 +   ├─ aws:iam:Role                   eks-worker-role     created     
 +   ├─ aws:iam:RolePolicyAttachment   eks-cluster-policy  created     
 +   ├─ aws:iam:RolePolicyAttachment   eks-service-policy  created     
 +   ├─ aws:iam:RolePolicyAttachment   eks-worker-policy   created     
 +   ├─ aws:iam:RolePolicyAttachment   eks-cni-policy      created     
 +   ├─ aws:iam:RolePolicyAttachment   ecr-policy          created     
 +   ├─ aws:ec2:Subnet                 eks-sn-1            created     
 +   ├─ aws:ec2:Subnet                 eks-sn-2            created     
 +   ├─ aws:ec2:InternetGateway        eks-ig              created     
 +   ├─ aws:ec2:Subnet                 eks-sn-0            created     
 +   ├─ aws:ec2:RouteTable             eks-rt              created     
 +   ├─ aws:eks:Cluster                eks-cluster         created     
 +   ├─ aws:ec2:RouteTableAssociation  eks-rta-1           created     
 +   ├─ aws:ec2:RouteTableAssociation  eks-rta-0           created     
 +   ├─ aws:ec2:RouteTableAssociation  eks-rta-2           created     
 +   └─ aws:eks:NodeGroup              eks-worker          created     
 
Resources:
    + 19 created
```

## 6. 確認
EKSクラスタが作成されるのを確認できます。
<img width="1435" alt="スクリーンショット 2019-12-15 23.56.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307853/149f4d47-2f54-dcb7-6f00-9e2505666643.png">

コマンドラインでも確認できます。

```console
$ aws eks update-kubeconfig --name eks-cluster

$ kubectl get node
kubectl get node
NAME                                           STATUS   ROLES    AGE   VERSION
ip-10-0-0-35.ap-northeast-1.compute.internal   Ready    <none>   19m   v1.14.7-eks-1861c5
ip-10-0-2-93.ap-northeast-1.compute.internal   Ready    <none>   19m   v1.14.7-eks-1861c5
```


# おわりに
golangを記述言語としてPulumiを使用してEKSクラスタが構築できることを確認しました。
golangを使用した場合のドキュメントがもっと増えることを期待します。