閉域 de EKS

※1 閉域:インターネット接続なし、と読み替えてください。英語だとoff-lineだそう（ググる時捗ります）
※2 kubernetes v1.13 を利用

ドコモではRedshiftを中核にした分析基盤の前段に、こんな形の前処理システムの運用をしています。

<img width="600" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320408/0c8dfb96-1c20-3f59-ef29-0bc81f9e90ab.png">

オンプレミスで構築された各システムからのデータをDirectConnect経由で受け取り、整形し、DWHへ格納する、よくあるNW構成のシステムです。

このようなシステムの常として、主にセキュリティの観点から、

- **インターネット接続不可、もしくはプロキシ経由でのみ可能**

という制約が付いて回ります( **閉域** と私たちは良く呼びます)。

このような制限下でのシステム開発、ダミーデータや検証環境をしっかり用意できれば良いのですが、なんだかんだ理想通りにはいかず……

1. 検証環境下で開発、ビルド
2. インターネット疎通経路のない本番環境へと転送、デプロイ
3. エラーなどログの調査、改善点の洗い出し

といったサイクルを回しながら進めることになるものの、

- ネットワーク的に断絶されているがゆえ、1サイクルを回すのに時間かかる
- 本番環境内での作業が増え、検証環境との解離が起きがち

と、**開発自体が複雑/高負荷**になりがちでした。また当初は予定通りの動作であったとしても、その後の変更への対応の負担が大きく、**対向システムの変化の速さ** に追従しきれないことも…

そのため現在、このようなアーキテクチャの前処理システムの開発・運用を一部で進めています。

<img width="600" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320408/a9ffa092-f749-4201-f186-730d49997ab8.png">

EKSを中核に[^1]

- **コンテナの可搬性を活かし、制限環境下でも迅速な開発を可能にする**
- **Kubernetesにより、水平スケーリングをシンプルに行えるようにする**

のが狙いのアーキテクチャ、です。色々大変な想いもしましたが、上記のメリットはそれを補いあまりあるものを得られているな、と感じています。

せっかくのアドベントカレンダーの機会をいただけたので、この **閉域環境でのEKSの利用** について少し内部に踏み込みつつ、まとめていこうと思います。


## 模擬環境の構築
### 要点
- EKSのプライベートアクセスオプションをによりControlPlaneへの経路作成、VPC内からのアクセスを可能に。
- VPC Endpointを利用し経路確保。必要なのはECR、S3、EC2。
- AMI起動時に明示的に認証パラメータを注入し、ControlPlane側にノードとして認識させる。

勘所を押さえれば、全てAWSが提供する手段で完結するので、さした手間なしに作成できます。

### Kubernetesの構成要素とEKSの職掌範囲
勘所を押さえるのに簡単に内部構造から。

> <img width="600" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320408/6f6d9ed7-5c6e-3c63-af3e-09bcd585341a.png">
> [kubernetes blog](https://kubernetes.io/blog/2018/07/18/11-ways-not-to-get-hacked/)より引用

Kubernetesは上図のようにいくつかのコンポーネントから成り立っており、ユーザとのインターフェースや全体管理を行うMaster (Cotrol Plane、CPlaneとも)と、実際にアプリケーションコンテナが動作するNodeの部分へと大別されます。

EKSはこれらのうち、主にMasterの部分を提供するサービスです。[^2]

> <img width="600" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320408/adc32776-2345-ab0c-80d5-31786910c6ef.png">
> [Running Kubernetes at Amazon scale using Amazon EKS](https://d1.awsstatic.com/events/reinvent/2019/REPEAT_1_Running_Kubernetes_at_Amazon_scale_using_Amazon_EKS_CON212-R1.pdf) re:Invent 2019セッション資料より引用

Nodeの部分はベースとなるAMIからEC2を起動し、Masterへと登録/認識させる、という割と下回りを意識する流れになります（CloudFormationが提供されているので操作自体は簡素ではあります）。
この操作を行う際、**Node内の構成要素(kubelet, コンテナランタイム、ネットワーキング)について、全て動作可能な状態**となるように必要なコンポーネントのダウンロードや認証等の通信が発生します。
これらについて、なにかしらの経路で通信できるようにするか、事前に入れ込んでおけば、閉域内でもクラスタ構築が可能になる、と言ったかたちとなります。

### EKS-Optimized AMI
それではどのようにこれら要素を整えれば良いのか、NodeのもとになるAMI（EKS-Optimized AMI）について踏み込んでみましょう。AWSが配布しているAMIの、ビルドに使用されるスクリプトが[GitHub](https://github.com/awslabs/amazon-eks-ami)で公開されています。
ビルドには[Packer](https://www.packer.io/)が利用されています。利用したことがないと最初は??となるかもしれませんが、EC2上でシェルスクリプト走らせたり、設定ファイルを配置したりして後AMIを作り上げているものだと思っておけば大丈夫です。

- [`eks-worker-al2.json`](https://github.com/awslabs/amazon-eks-ami/blob/master/eks-worker-al2.json)で各種パラメータ定義
  - amzn2-ami-minimal-hvm(最低限のライブラリのみが入ったAmazon Linux 2のAMI)をベースAMIとして指定
- [`files`](https://github.com/awslabs/amazon-eks-ami/tree/master/files)`下の設定ファイルを転送
- [`install-worker.sh`](https://github.com/awslabs/amazon-eks-ami/blob/master/install-worker.sh)起動
といった流れとなっています（実際に自分でAMIを焼きたければ、`Makefile`があるので`make`を使うのが簡便です）。

このうち`install-worker.sh`に前述の必要なコンポーネントの導入が記述されています。

#### コンテナランタイム
[こちら](https://github.com/awslabs/amazon-eks-ami/blob/master/install-worker.sh#L102)の通り、Dockerがランタイムとしてインストールされます。device-mapperやlvmもこちらでインストールされています。

#### kubelet
[こちら](https://github.com/awslabs/amazon-eks-ami/blob/master/install-worker.sh#L171)で、AWSの用意したバイナリをS3から取得、設定がされるようになっています。`aws s3 ls amazon-eks/1.14.7/2019-09-27/bin/linux/amd64/`を打つとバイナリの配置されているS3が見られます（バージョンや日付、OSなどは適宜読み替えてください）。この`amazon-eks`バケットに、kubelet以外のEKS動作に必要なファイル類（IAMと連携するための`aws-iam-authenticator`など）が収められています。

#### ネットワーキング
ここまでは予めAMIの内部に収められるのですが、ネットワーキングについては少々異なります。
EKS（※v1.11以降）では

- `kube-proxy`: Podの通信先を制御。EKSだとデフォルトでNodeのiptablesを制御するモードで動作
- `coredns`: Service(Kubernetesの外部への公開点の概念)にDNSを付与
- `aws-nodes`: kubeletと連携してPodに対しVPC内でWorkするPrivateIPアドレスを付与
 
の3つがコンテナとして起動され、動作するのですが、これらに関してはAMIの中には収めらない状態です。
つまり起動した際、AWSのコンテナレジストリサービス（ECR）からPullしてくることで、NodeとしてWorkするような仕組みとなっています（EKSでKubernetes 1.13が公開される前は、この辺をユーザーがゴニョゴニョすることで無理矢理off-lineで動作させるしんどい状況だったりしました）。

これらについては、ECRとS3のVPCエンドポイントを付与することで、v1.13より解決が可能です。ポイントとしては

- ECRのVPCエンドポイントは2種類。dkr.ecr...(コンテナ本体のPull元)だけでなくapi.ecr...もつける（認証に必要なため）
- S3のエンドポイントも必要（ECRのストレージがS3のため）。最小の権限は[こちら](https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/ecr-minimum-s3-perms.html)。

### EKSからのNodeの認識
Nodeを起動した際、EKS側から認識させる必要があります。通常CloudFormationからNodeを起動すると、自動でEKS側の情報をEKSのAPIを利用し取得してくれるのですが、EKSそのものについてはPrivateLinkが現在存在しないため、そのままでは認識がされません。
そのため、AMIの起動時にパラメータとして予め渡してあげることで、EKSへのAPIコールを回避しNodeとして認識させることが可能です。
先ほど触れたAMI内部に転送されているファイルのなかにある、[bootstrap.sh](https://github.com/awslabs/amazon-eks-ami/blob/master/files/bootstrap.sh)が起動時に実行されるスクリプトになります。これに

- EKSのKubernetesのMasterとしてのエンドポイント（ややこしいですが...`kube-apiserver`へのエンドポイントと読み替え他方がわかりやすいかもしれません。)
- その認証情報

の2点を、CloudFormationから渡すと、Nodeとして認識がされます。

### EKSのPrivateAccess
このオプションをEnableにすると、Masterエンドポイントに対するPrivate IPが払い出され、EKSが対象としたVPC内部からはDNSでの名前解決も可能となります。

これをEnableの状態で、PublicAccessの方をDisableにすると、対象のVPC内からのみkubectlを用いてKubernetesの操作が可能な状態となります（※ 正確には、[Enabling DNS resolution for Amazon EKS cluster endpoints \| AWS Compute Blog](https://aws.amazon.com/blogs/compute/enabling-dns-resolution-for-amazon-eks-cluster-endpoints/)工夫すればピアリングした隣のVPCからもエンドポイントへの名前解決が可能です）。

### EKSとkubectlとIAMの関係
EKSでは、kubectlが打たれた際、クライアントのIAMを認証として利用します。
Masterを作成した時点では、その作成者がAdminとされるため、閉域内からkubectlを叩いて利用するためには作成者のIAMキーを渡すか、作成後に他のIAMユーザーかロールをAdminとして追加する必要がでます。


### 環境構築
長くなってしまいましたが、それでは閉域を模擬した環境で、EKSを使用してみます。
（CloudFormationや構築スクリプトを付しておきますが、参考程度に。）

#### 1 NW+関連サービス

<img width="600" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320408/2f381cf6-b0cc-80cf-83bb-201445f385d9.png">

Direct Connectを用意はできないので、まず[VPC Peeringを越えてインターネットへは出ていけない](https://docs.aws.amazon.com/vpc/latest/peering/invalid-peering-configurations.html#edge-to-edge-vgw)ことを利用して閉域を模擬した環境を作ります。（今だったらSSM Session Managerを利用するのが良いかもしれません）
EKS自身が2以上のサブネットを必要とするのでそれについても作成（今回利用はしません）。
また、EKS自身が利用するためのIAMロールや、kubectlを打つためにEC2へ付与するロールも、ここで作成しています。

(VPCFlowLogsつけたり色々してますが無視してください、開発環境でもなるべくつけるようにしようという弊社ポリシーです)

<details><summary>`1-infrastructure.yaml`</summary><div>

```yaml:1-infrastructure.yaml
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  EnvName:
    Type: String
    Default: yoda-close-nw
    Description: Component application identifier used by this sequential deployment
  AZ1:
    Type: String
    Default: ap-northeast-1a
    Description: Main AZ
  AZ2:
    Type: String
    Default: ap-northeast-1c
    Description: Second AZ (Just for EKS K8s endpoints)
  BastionVPCCIDR:
    Type: String
    Default: 172.24.0.0/24
    Description: Bastion VPC CIDR block
  BastionPublicSubnetCIDR:
    Type: String
    Default: 172.24.0.0/25
    Description: Public Subnet in Bastion VPC CIDR block (For bastion, natgw)
  K8sVPCCIDR:
    Type: String
    Default: 172.25.0.0/16
    Description: K8s VPC CIDR block
  K8sWorkerSubnet1CIDR:
    Type: String
    Default: 172.25.0.0/17
    Description: K8s Worker Subnet in AZ1 CIDR block
  K8sWorkerSubnet2CIDR:
    Type: String
    Default: 172.25.128.0/18
    Description: K8s Worker Subnet in AZ2 CIDR block
  K8sOperatorSubnetCIDR:
    Type: String
    Default: 172.25.192.0/18
    Description: kubectl instance located subnet CIDER
  YourComputerIPAddress:
    Type: String
    Default: 121.118.77.48
    Description: Access point IP address
  InstanceType:
    Description: Server EC2 instance type
    Type: String
    Default: t3.nano
    ConstraintDescription: must be a valid EC2 instance type.
  RootVolumeSize:
    Default: 8
    Type: String
  RootVolumePath:
    Description: Amazon Linux=>/dev/xvda, Ubuntu=>/dev/sda1
    Default: /dev/xvda
    Type: String
  KeyName:
    Description: Name of an existing EC2 KeyPair to enable SSH access to the instance
    Type: AWS::EC2::KeyPair::KeyName
    ConstraintDescription: must be the name of an existing EC2 KeyPair.
  AmiId:
    Description: kubectl installed ami
    Type: String
Resources:  
  ################################################################################
  ### VPC ########################################################################
  ################################################################################
  BastionVPC:
    Type: 'AWS::EC2::VPC'
    Properties:
      CidrBlock: !Sub ${BastionVPCCIDR}
      EnableDnsSupport: 'true'
      EnableDnsHostnames: 'true'
      Tags:
      - Key: Name
        Value: !Sub ${EnvName}-bastion-vpc
  K8sVPC:
    Type: 'AWS::EC2::VPC'
    Properties:
      CidrBlock: !Sub ${K8sVPCCIDR}
      EnableDnsSupport: 'true'
      EnableDnsHostnames: 'true'
      Tags:
      - Key: Name
        Value: !Sub ${EnvName}-k8s-vpc
  ### VPC Peering ###
  VPCPeering:
    Type: AWS::EC2::VPCPeeringConnection
    Properties: 
      PeerVpcId: !Ref BastionVPC  # Accepter
      Tags:
      - Key: Name
        Value: !Sub ${EnvName}-vpcpeering
      VpcId: !Ref K8sVPC  # Requester

  ################################################################################
  ### Subnet #####################################################################
  ################################################################################

  BastionPublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AZ1
      CidrBlock: !Ref BastionPublicSubnetCIDR
      VpcId: !Ref BastionVPC
      Tags:
      - Key: Name
        Value: !Sub ${EnvName}-bastion-public-subnet
  K8sOperatorSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AZ1
      CidrBlock: !Sub ${K8sOperatorSubnetCIDR}
      VpcId: !Ref K8sVPC
      Tags:
      - Key: Name
        Value: !Sub ${EnvName}-k8s-operator-subnet
  K8sWorkerSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AZ1
      CidrBlock: !Sub ${K8sWorkerSubnet1CIDR}
      VpcId: !Ref K8sVPC
      Tags:
      - Key: Name
        Value: !Sub ${EnvName}-k8s-worker-subnet-1
  K8sWorkerSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: !Ref AZ2
      CidrBlock: !Sub ${K8sWorkerSubnet2CIDR}
      VpcId: !Ref K8sVPC
      Tags:
      - Key: Name
        Value: !Sub ${EnvName}-k8s-worker-subnet-2

  ################################################################################
  ### Internet Gateway ###########################################################
  ################################################################################

  BastionInternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
      - Key: Name
        Value: !Sub ${EnvName}
  AttachBastionInternetGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref BastionVPC
      InternetGatewayId:
        Ref: BastionInternetGateway

  ################################################################################
  ### Route Table & Rule #########################################################
  ################################################################################

  # Route Table
  BastionPublicSubnetRouteTable:  # RTB for ops public subet
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref BastionVPC
      Tags:
      - Key: Name
        Value: !Sub ${EnvName}-bastion-public-subnet-rtb
  K8sWorkerRouteTable:  # For master & worker subet
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref K8sVPC
      Tags:
      - Key: Name
        Value: !Sub ${EnvName}-k8s-worker-subnet-rtb
  K8sOperatorRouteTable:  # For master & worker subet
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref K8sVPC
      Tags:
      - Key: Name
        Value: !Sub ${EnvName}-k8s-operator-subnet-rtb
  # Association
  BastionPublicRtbAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref BastionPublicSubnet
      RouteTableId: !Ref BastionPublicSubnetRouteTable
  K8sOperatorRtbAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref K8sOperatorSubnet
      RouteTableId: !Ref K8sOperatorRouteTable
  K8sWorkerAZ1RtbAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref K8sWorkerSubnet1
      RouteTableId: !Ref K8sWorkerRouteTable
  K8sWorkerAZ2RtbAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref K8sWorkerSubnet2
      RouteTableId: !Ref K8sWorkerRouteTable
  # Route
  BastionPublicToIGWRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: '0.0.0.0/0'
      RouteTableId: !Ref BastionPublicSubnetRouteTable
      GatewayId:
        !Ref BastionInternetGateway
    DependsOn: AttachBastionInternetGateway
  BastionPublicToK8sOperatorRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: !Ref K8sOperatorSubnetCIDR
      RouteTableId: !Ref BastionPublicSubnetRouteTable
      VpcPeeringConnectionId: !Ref VPCPeering
  K8sOperatorToBastionRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: !Ref BastionVPCCIDR
      RouteTableId: !Ref K8sOperatorRouteTable
      VpcPeeringConnectionId: !Ref VPCPeering
  
  ################################################################################
  ### Security Group & Rule ######################################################
  ################################################################################

  ##### Bastion Public SG #####
  BastionPublicSecurityGroup: 
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupName: !Sub ${EnvName}-bastion-public-sg
      GroupDescription: SG for public subnet instance
      Tags:
      - Key: Name
        Value:  !Sub ${EnvName}-bastion-public-sg
      VpcId: !Ref BastionVPC
  BastionPublicIngressFromYourComputerSSH:
    Type: AWS::EC2::SecurityGroupIngress
    Properties: 
      Description: From Your Computer to Public
      GroupId: !GetAtt BastionPublicSecurityGroup.GroupId
      FromPort: 22
      ToPort: 22
      IpProtocol: tcp
      CidrIp: !Sub ${YourComputerIPAddress}/32
    DependsOn: BastionPublicSecurityGroup
  BastionPublicIngressFromYourComputerRDP:
    Type: AWS::EC2::SecurityGroupIngress
    Properties: 
      Description: From Your Computer to Public
      GroupId: !GetAtt BastionPublicSecurityGroup.GroupId
      FromPort: 3389
      ToPort: 3389
      IpProtocol: tcp
      CidrIp: !Sub ${YourComputerIPAddress}/32
    DependsOn: BastionPublicSecurityGroup
  BastionPublicIngressFromK8sOperator:
    Type: AWS::EC2::SecurityGroupIngress
    Properties: 
      Description: From K8s Operator to Bastion Public
      GroupId: !GetAtt BastionPublicSecurityGroup.GroupId
      FromPort: -1  # All port
      ToPort: -1
      IpProtocol: -1  # All traffic
      CidrIp: !Ref K8sOperatorSubnetCIDR
    DependsOn: BastionPublicSecurityGroup

  #### K8s Operator SG #####
  K8sOperatorSecurityGroup: 
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupName: !Sub ${EnvName}-k8s-operator-sg
      GroupDescription: SG for K8s operator
      Tags:
      - Key: Name
        Value: !Sub ${EnvName}-k8s-operator-sg
      VpcId: !Ref K8sVPC
  K8sOperatorIngressFromK8s:
    Type: AWS::EC2::SecurityGroupIngress
    Properties: 
      Description: From K8sVPC to Control
      GroupId: !GetAtt K8sOperatorSecurityGroup.GroupId
      FromPort: -1  # All port
      ToPort: -1
      IpProtocol: -1  # All traffic
      CidrIp: !Ref K8sVPCCIDR
    DependsOn: K8sOperatorSecurityGroup
  K8sOperatorIngressFromBastion:
    Type: AWS::EC2::SecurityGroupIngress
    Properties: 
      Description: From Closed to Worker
      GroupId: !GetAtt K8sOperatorSecurityGroup.GroupId
      FromPort: -1  # All port
      ToPort: -1
      IpProtocol: -1  # All traffic
      CidrIp: !Ref BastionVPCCIDR
    DependsOn: K8sOperatorSecurityGroup

  ################################################################################
  ### VPC Endpoint ###############################################################
  ################################################################################

  # VPC Endpoints Security Group for K8s VPCs
  VPCEndpointSG:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupName: !Sub ${EnvName}-vpce-sg
      GroupDescription: SG for VPC endpoints
      Tags:
        - Key: Name
          Value:  !Sub ${EnvName}-vpce-sg
      VpcId: !Ref K8sVPC
  VPCEndpointSGRules:
    Type: AWS::EC2::SecurityGroupIngress
    Properties: 
      Description: From k8s vpc to private links
      GroupId: !GetAtt VPCEndpointSG.GroupId
      IpProtocol: tcp
      FromPort: 443
      ToPort: 443
      CidrIp: !Ref K8sVPCCIDR
    DependsOn: VPCEndpointSG
  S3Endpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ServiceName: !Sub com.amazonaws.${AWS::Region}.s3
      VpcEndpointType: Gateway
      VpcId: !Ref K8sVPC
      RouteTableIds:
      - !Ref K8sWorkerRouteTable
      PolicyDocument:
        Version: 2012-10-17
        Statement:
        - Sid: AmazonLinuxYumUpdatePolicy
          Effect: Allow
          Principal: '*'
          Action: 
          - s3:Get*
          Resource: 
          - !Sub arn:aws:s3:::packages.${AWS::Region}.amazonaws.com/*
          - !Sub arn:aws:s3:::repo.${AWS::Region}.amazonaws.com/*
          - !Sub arn:aws:s3:::amazonlinux.${AWS::Region}.amazonaws.com/*
        - Sid: AllowPullFromECRPolicy
          Effect: Allow
          Principal: '*'
          Action: 
          - s3:Get*
          Resource:
          - !Sub arn:aws:s3:::prod-${AWS::Region}-starport-layer-bucket/*
  EC2Endpoint:  # Kubelet with aws cloud provider requires ec2 endpoint access
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ec2
      VpcEndpointType: Interface  # Private Link
      VpcId: !Ref K8sVPC
      SubnetIds: 
      - !Ref K8sWorkerSubnet1
      SecurityGroupIds:
      - !Ref VPCEndpointSG
      PrivateDnsEnabled: True
  ECRDKREndpoint:  # WorkerNodes requires pull containers from ECR
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ecr.dkr  # Image Endpoisnt
      VpcEndpointType: Interface  # Private Link
      VpcId: !Ref K8sVPC
      SubnetIds: 
      - !Ref K8sWorkerSubnet1
      SecurityGroupIds:
      - !Ref VPCEndpointSG
      PrivateDnsEnabled: True
  ECRAPIEndpoint:  # WorkerNodes requires pull containers from ECR
    Type: AWS::EC2::VPCEndpoint
    Properties:
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ecr.api  # Control Endpoint
      VpcEndpointType: Interface  # Private Link
      VpcId: !Ref K8sVPC
      SubnetIds: 
      - !Ref K8sWorkerSubnet1
      SecurityGroupIds:
      - !Ref VPCEndpointSG
      PrivateDnsEnabled: True

  ################################################################################
  ### IAM ########################################################################
  ################################################################################
  
  # Role for EKS
  # This Role should be created for each cluster,
  # however aws doesn't allow custom policy for eks role.
  # so, to simplify, create 1 role managing multiple clustors
  AWSServiceRoleForAmazonEKS:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${EnvName}-eks-service-role
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - eks.amazonaws.com
          Action:
          - sts:AssumeRole
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/AmazonEKSServicePolicy  # EKS requires aws managed policy...
      - arn:aws:iam::aws:policy/AmazonEKSClusterPolicy  # EKS requires aws managed policy...
  # K8s operator role
  K8sAdminRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${EnvName}-admin-role
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - ec2.amazonaws.com
          Action:
          - sts:AssumeRole
  K8sAdminInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties: 
      Path: /Minaden/DataProcessing/
      Roles: 
      - Ref: K8sAdminRole
      InstanceProfileName: !Sub ${EnvName}-k8s-admin-instance-profile  # Limitation in length

  ################################################################################
  ### EC2 Instance ###############################################################
  ################################################################################

  BastionEIP:
    Type: AWS::EC2::EIP
    Properties:
      Domain: vpc
  BastionEIPAssociate:
    Type: AWS::EC2::EIPAssociation
    Properties: 
      AllocationId: !GetAtt BastionEIP.AllocationId
      InstanceId: !Ref Bastion
  Bastion:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref AmiId
      InstanceType: !Ref InstanceType
      BlockDeviceMappings:
      - DeviceName: !Ref RootVolumePath
        Ebs:
          VolumeSize: !Ref RootVolumeSize
          Encrypted: true
          DeleteOnTermination: true
      KeyName: !Ref KeyName
      SecurityGroupIds: 
      - !GetAtt BastionPublicSecurityGroup.GroupId
      Tags:
      - Key: Name
        Value: !Sub ${EnvName}-bastion
      SubnetId: !Ref BastionPublicSubnet
      CreditSpecification: 
        CPUCredits: standard  # Disable T2/3 Unlimited
      IamInstanceProfile: !Ref K8sAdminInstanceProfile
  Operator:
    Type: AWS::EC2::Instance
    Properties:
      ImageId: !Ref AmiId
      InstanceType: !Ref InstanceType
      BlockDeviceMappings:
      - DeviceName: !Ref RootVolumePath
        Ebs:
          VolumeSize: !Ref RootVolumeSize
          Encrypted: true
          DeleteOnTermination: true
      KeyName: !Ref KeyName
      SecurityGroupIds: 
      - !GetAtt K8sOperatorSecurityGroup.GroupId
      Tags:
      - Key: Name
        Value: !Sub ${EnvName}-operator
      SubnetId: !Ref K8sOperatorSubnet
      CreditSpecification: 
        CPUCredits: standard  # Disable T2/3 Unlimited
      IamInstanceProfile: !Ref K8sAdminInstanceProfile

Outputs:
  K8sVPCID:
    Description: The ID of K8s VPC
    Value: !Ref K8sVPC
  K8sWorkerSubnet1:
    Description: The ID of K8s worker subnet for main use
    Value: !Ref K8sWorkerSubnet1
  K8sWorkerSubnet2:
    Description: The ID of K8s worker subnet for sub use
    Value: !Ref K8sWorkerSubnet2
  K8sOperatorSubnet:
    Description: The ID of K8s operator subnet
    Value: !Ref K8sOperatorSubnet
  K8sOperatorSecurityGroup:
    Description: SG for K8s Operator
    Value: !Ref K8sOperatorSecurityGroup 
  RoleArn:
    Description: The role that EKS will use to create AWS resources for Kubernetes clusters
    Value: !GetAtt AWSServiceRoleForAmazonEKS.Arn
```
</div></details>


#### 2-1 Control Plane SG

<img width="600" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320408/36119ae6-f0c0-f420-0abb-a3f8d9ff849e.png">

1に統合してもいいのですが、[クラスターセキュリティグループの考慮事項 - Amazon EKS](https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/sec-group-reqs.html)によるとクラスタごとにSGは分けた方がいいと記述されています。今回は1つのみですが、複数クラスタを作成することを考慮し分けています。
（実際のうちの環境では、コンポーネントごとにEKSクラスタを分離しています。）

<details><summary>`2-1-controlplane-sg.yaml`</summary><div>
```yaml:2-1-controlplane-sg.yaml

AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  EnvName:
    Type: String
    Default: yoda-close-nw
    Description: Component application identifier used by this sequential deployment
  ClusterName:
    Type: String
    Default: yoda-closed-cluster-1
    Description: K8s cluster name
  K8sVpcId:
    Type: String
    Description: K8s cluster VPCID
  OperatorSgId:
    Type: String
    Description: Operator security group id
Resources:
  ##### K8s Cluster ControlPlane SG #####
  ClusterControlPlaneSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupName: !Sub ${EnvName}-${ClusterName}-controlplane-sg
      GroupDescription: SG for control plane (k8s endpoint)
      Tags:
      - Key: Name
        Value: !Sub ${EnvName}-${ClusterName}-controlplane-sg
      VpcId: !Ref K8sVpcId
  ClusterControlPlaneSecurityGroupIngressFromOperator:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      Description: Allow operator to communicate with the cluster API Server
      GroupId: !Ref ClusterControlPlaneSecurityGroup
      SourceSecurityGroupId: !Ref OperatorSgId
      IpProtocol: tcp
      ToPort: 443
      FromPort: 443
```
</div></details>

#### 2-2 EKS

<img width="600" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320408/41f558b8-c627-f74c-f026-f2b57656ada8.png">

CloudFormationでは現在、Private AccessをEnableにできないのでCLIで作成します。 [^3]


#### 2-3 Worker Nodes

<img width="600" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320408/c770a9ca-8048-0827-6edf-67a1369c2b40.png">

AWS提供のテンプレートをいくつか改修してあります（PublicIPを削除する、EBS暗号化入れるなど）
UserDataのセクションで`bootstrap.sh`を起動しているのが見えるかと思います。

<details><summary>`2-3-workers.yaml`</summary><div>

```yaml:2-3-workers.yaml

# Based on https://github.com/awslabs/amazon-eks-ami/blob/master/amazon-eks-nodegroup.yaml
# Add
# - Default Value for NodeImageId
# - Parameter for EnvName
# - Name tag for worker sg
# - Name for worker IAM role
# - Parameter for operator sg
# - SG rule from operator
# - EBS encryption enable
# Change
# - Default Value for AutoScalingCapacities (to be single nodes)
# - Disable Public IP for workers

---
AWSTemplateFormatVersion: 2010-09-09
Description: Amazon EKS - Node Group

Parameters:

  KeyName:
    Description: The EC2 Key Pair to allow SSH access to the instances
    Type: AWS::EC2::KeyPair::KeyName

  NodeImageId:
    Description: AMI id for the node instances.
    Type: AWS::EC2::Image::Id
    Default: ami-04c0f02f5e148c80a

  NodeInstanceType:
    Description: EC2 instance type for the node instances
    Type: String
    Default: t3.medium
    ConstraintDescription: Must be a valid EC2 instance type
    AllowedValues:
      - a1.medium
      - a1.large
      - a1.xlarge
      - a1.2xlarge
      - a1.4xlarge
      - c1.medium
      - c1.xlarge
      - c3.large
      - c3.xlarge
      - c3.2xlarge
      - c3.4xlarge
      - c3.8xlarge
      - c4.large
      - c4.xlarge
      - c4.2xlarge
      - c4.4xlarge
      - c4.8xlarge
      - c5.large
      - c5.xlarge
      - c5.2xlarge
      - c5.4xlarge
      - c5.9xlarge
      - c5.18xlarge
      - c5d.large
      - c5d.xlarge
      - c5d.2xlarge
      - c5d.4xlarge
      - c5d.9xlarge
      - c5d.18xlarge
      - c5n.large
      - c5n.xlarge
      - c5n.2xlarge
      - c5n.4xlarge
      - c5n.9xlarge
      - c5n.18xlarge
      - cc2.8xlarge
      - cr1.8xlarge
      - d2.xlarge
      - d2.2xlarge
      - d2.4xlarge
      - d2.8xlarge
      - f1.2xlarge
      - f1.4xlarge
      - f1.16xlarge
      - g2.2xlarge
      - g2.8xlarge
      - g3s.xlarge
      - g3.4xlarge
      - g3.8xlarge
      - g3.16xlarge
      - h1.2xlarge
      - h1.4xlarge
      - h1.8xlarge
      - h1.16xlarge
      - hs1.8xlarge
      - i2.xlarge
      - i2.2xlarge
      - i2.4xlarge
      - i2.8xlarge
      - i3.large
      - i3.xlarge
      - i3.2xlarge
      - i3.4xlarge
      - i3.8xlarge
      - i3.16xlarge
      - i3.metal
      - i3en.large
      - i3en.xlarge
      - i3en.2xlarge
      - i3en.3xlarge
      - i3en.6xlarge
      - i3en.12xlarge
      - i3en.24xlarge
      - m1.small
      - m1.medium
      - m1.large
      - m1.xlarge
      - m2.xlarge
      - m2.2xlarge
      - m2.4xlarge
      - m3.medium
      - m3.large
      - m3.xlarge
      - m3.2xlarge
      - m4.large
      - m4.xlarge
      - m4.2xlarge
      - m4.4xlarge
      - m4.10xlarge
      - m4.16xlarge
      - m5.large
      - m5.xlarge
      - m5.2xlarge
      - m5.4xlarge
      - m5.12xlarge
      - m5.24xlarge
      - m5a.large
      - m5a.xlarge
      - m5a.2xlarge
      - m5a.4xlarge
      - m5a.12xlarge
      - m5a.24xlarge
      - m5ad.large
      - m5ad.xlarge
      - m5ad.2xlarge
      - m5ad.4xlarge
      - m5ad.12xlarge
      - m5ad.24xlarge
      - m5d.large
      - m5d.xlarge
      - m5d.2xlarge
      - m5d.4xlarge
      - m5d.12xlarge
      - m5d.24xlarge
      - p2.xlarge
      - p2.8xlarge
      - p2.16xlarge
      - p3.2xlarge
      - p3.8xlarge
      - p3.16xlarge
      - p3dn.24xlarge
      - r3.large
      - r3.xlarge
      - r3.2xlarge
      - r3.4xlarge
      - r3.8xlarge
      - r4.large
      - r4.xlarge
      - r4.2xlarge
      - r4.4xlarge
      - r4.8xlarge
      - r4.16xlarge
      - r5.large
      - r5.xlarge
      - r5.2xlarge
      - r5.4xlarge
      - r5.12xlarge
      - r5.24xlarge
      - r5a.large
      - r5a.xlarge
      - r5a.2xlarge
      - r5a.4xlarge
      - r5a.12xlarge
      - r5a.24xlarge
      - r5ad.large
      - r5ad.xlarge
      - r5ad.2xlarge
      - r5ad.4xlarge
      - r5ad.12xlarge
      - r5ad.24xlarge
      - r5d.large
      - r5d.xlarge
      - r5d.2xlarge
      - r5d.4xlarge
      - r5d.12xlarge
      - r5d.24xlarge
      - t1.micro
      - t2.nano
      - t2.micro
      - t2.small
      - t2.medium
      - t2.large
      - t2.xlarge
      - t2.2xlarge
      - t3.nano
      - t3.micro
      - t3.small
      - t3.medium
      - t3.large
      - t3.xlarge
      - t3.2xlarge
      - t3a.nano
      - t3a.micro
      - t3a.small
      - t3a.medium
      - t3a.large
      - t3a.xlarge
      - t3a.2xlarge
      - x1.16xlarge
      - x1.32xlarge
      - x1e.xlarge
      - x1e.2xlarge
      - x1e.4xlarge
      - x1e.8xlarge
      - x1e.16xlarge
      - x1e.32xlarge
      - z1d.large
      - z1d.xlarge
      - z1d.2xlarge
      - z1d.3xlarge
      - z1d.6xlarge
      - z1d.12xlarge

  NodeAutoScalingGroupMinSize:
    Description: Minimum size of Node Group ASG.
    Type: Number
    Default: 1

  NodeAutoScalingGroupMaxSize:
    Description: Maximum size of Node Group ASG. Set to at least 1 greater than NodeAutoScalingGroupDesiredCapacity.
    Type: Number
    Default: 1

  NodeAutoScalingGroupDesiredCapacity:
    Description: Desired capacity of Node Group ASG.
    Type: Number
    Default: 1

  NodeVolumeSize:
    Description: Node volume size
    Type: Number
    Default: 20

  ClusterName:
    Description: The cluster name provided when the cluster was created. If it is incorrect, nodes will not be able to join the cluster.
    Type: String

  BootstrapArguments:
    Description: Arguments to pass to the bootstrap script. See files/bootstrap.sh in https://github.com/awslabs/amazon-eks-ami
    Type: String
    Default: ""

  NodeGroupName:
    Description: Unique identifier for the Node Group.
    Type: String

  ClusterControlPlaneSecurityGroup:
    Description: The security group of the cluster control plane.
    Type: AWS::EC2::SecurityGroup::Id

  VpcId:
    Description: The VPC of the worker instances
    Type: AWS::EC2::VPC::Id

  Subnets:
    Description: The subnets where workers can be created.
    Type: List<AWS::EC2::Subnet::Id>

  EnvName:
    Type: String
    Default: yoda-close-nw
    Description: Component application identifier used by this sequential deployment
  
  OperatorSgId:
    Description: Operator security group id 
    Type: String

Metadata:

  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: EKS Cluster
        Parameters:
          - ClusterName
          - ClusterControlPlaneSecurityGroup
      - Label:
          default: Worker Node Configuration
        Parameters:
          - NodeGroupName
          - NodeAutoScalingGroupMinSize
          - NodeAutoScalingGroupDesiredCapacity
          - NodeAutoScalingGroupMaxSize
          - NodeInstanceType
          - NodeImageId
          - NodeVolumeSize
          - KeyName
          - BootstrapArguments
      - Label:
          default: Worker Network Configuration
        Parameters:
          - VpcId
          - Subnets

Resources:

  NodeInstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Path: "/"
      Roles:
        - !Ref NodeInstanceRole

  NodeInstanceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${EnvName}-${ClusterName}-worker-role
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: sts:AssumeRole
      Path: "/"
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonEKSWorkerNodePolicy
        - arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy
        - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly

  NodeSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for all nodes in the cluster
      VpcId: !Ref VpcId
      Tags:
        - Key: !Sub kubernetes.io/cluster/${ClusterName}
          Value: owned
        - Key: Name
          Value: !Sub ${EnvName}-${ClusterName}-worker-sg

  NodeSecurityGroupIngress:
    Type: AWS::EC2::SecurityGroupIngress
    DependsOn: NodeSecurityGroup
    Properties:
      Description: Allow node to communicate with each other
      GroupId: !Ref NodeSecurityGroup
      SourceSecurityGroupId: !Ref NodeSecurityGroup
      IpProtocol: -1
      FromPort: 0
      ToPort: 65535

  NodeSecurityGroupFromControlPlaneIngress:
    Type: AWS::EC2::SecurityGroupIngress
    DependsOn: NodeSecurityGroup
    Properties:
      Description: Allow worker Kubelets and pods to receive communication from the cluster control plane
      GroupId: !Ref NodeSecurityGroup
      SourceSecurityGroupId: !Ref ClusterControlPlaneSecurityGroup
      IpProtocol: tcp
      FromPort: 1025
      ToPort: 65535

  ControlPlaneEgressToNodeSecurityGroup:
    Type: AWS::EC2::SecurityGroupEgress
    DependsOn: NodeSecurityGroup
    Properties:
      Description: Allow the cluster control plane to communicate with worker Kubelet and pods
      GroupId: !Ref ClusterControlPlaneSecurityGroup
      DestinationSecurityGroupId: !Ref NodeSecurityGroup
      IpProtocol: tcp
      FromPort: 1025
      ToPort: 65535

  NodeSecurityGroupFromControlPlaneOn443Ingress:
    Type: AWS::EC2::SecurityGroupIngress
    DependsOn: NodeSecurityGroup
    Properties:
      Description: Allow pods running extension API servers on port 443 to receive communication from cluster control plane
      GroupId: !Ref NodeSecurityGroup
      SourceSecurityGroupId: !Ref ClusterControlPlaneSecurityGroup
      IpProtocol: tcp
      FromPort: 443
      ToPort: 443

  NodeSecurityGroupFromOperatorOn22Ingress:
    Type: AWS::EC2::SecurityGroupIngress
    DependsOn: NodeSecurityGroup
    Properties:
      Description: Allow nodes to communicate with the operator on SSH
      GroupId: !Ref NodeSecurityGroup
      SourceSecurityGroupId: !Ref OperatorSgId
      IpProtocol: tcp
      FromPort: 22
      ToPort: 22

  NodeSecurityGroupFromOperatorOn80Ingress:
    Type: AWS::EC2::SecurityGroupIngress
    DependsOn: NodeSecurityGroup
    Properties:
      Description: Allow nodes to communicate with the operator on HTTP
      GroupId: !Ref NodeSecurityGroup
      SourceSecurityGroupId: !Ref OperatorSgId
      IpProtocol: tcp
      FromPort: 80
      ToPort: 80

  ControlPlaneEgressToNodeSecurityGroupOn443:
    Type: AWS::EC2::SecurityGroupEgress
    DependsOn: NodeSecurityGroup
    Properties:
      Description: Allow the cluster control plane to communicate with pods running extension API servers on port 443
      GroupId: !Ref ClusterControlPlaneSecurityGroup
      DestinationSecurityGroupId: !Ref NodeSecurityGroup
      IpProtocol: tcp
      FromPort: 443
      ToPort: 443

  ClusterControlPlaneSecurityGroupIngress:
    Type: AWS::EC2::SecurityGroupIngress
    DependsOn: NodeSecurityGroup
    Properties:
      Description: Allow pods to communicate with the cluster API Server
      GroupId: !Ref ClusterControlPlaneSecurityGroup
      SourceSecurityGroupId: !Ref NodeSecurityGroup
      IpProtocol: tcp
      ToPort: 443
      FromPort: 443

  NodeGroup:
    Type: AWS::AutoScaling::AutoScalingGroup
    Properties:
      DesiredCapacity: !Ref NodeAutoScalingGroupDesiredCapacity
      LaunchConfigurationName: !Ref NodeLaunchConfig
      MinSize: !Ref NodeAutoScalingGroupMinSize
      MaxSize: !Ref NodeAutoScalingGroupMaxSize
      VPCZoneIdentifier: !Ref Subnets
      Tags:
        - Key: Name
          Value: !Sub ${ClusterName}-${NodeGroupName}-Node
          PropagateAtLaunch: true
        - Key: !Sub kubernetes.io/cluster/${ClusterName}
          Value: owned
          PropagateAtLaunch: true
    UpdatePolicy:
      AutoScalingRollingUpdate:
        MaxBatchSize: 1
        MinInstancesInService: !Ref NodeAutoScalingGroupDesiredCapacity
        PauseTime: PT5M

  NodeLaunchConfig:
    Type: AWS::AutoScaling::LaunchConfiguration
    Properties:
      AssociatePublicIpAddress: false
      IamInstanceProfile: !Ref NodeInstanceProfile
      ImageId: !Ref NodeImageId
      InstanceType: !Ref NodeInstanceType
      KeyName: !Ref KeyName
      SecurityGroups:
        - !Ref NodeSecurityGroup
      BlockDeviceMappings:
        - DeviceName: /dev/xvda
          Ebs:
            VolumeSize: !Ref NodeVolumeSize
            VolumeType: gp2
            DeleteOnTermination: true
            Encrypted: true
      UserData:
        Fn::Base64:
          !Sub |
            #!/bin/bash
            set -o xtrace
            /etc/eks/bootstrap.sh ${ClusterName} ${BootstrapArguments}
            /opt/aws/bin/cfn-signal --exit-code $? \
                     --stack  ${AWS::StackName} \
                     --resource NodeGroup  \
                     --region ${AWS::Region}

Outputs:

  NodeInstanceRole:
    Description: The node instance role
    Value: !GetAtt NodeInstanceRole.Arn

  NodeSecurityGroup:
    Description: The security group for the node group
    Value: !Ref NodeSecurityGroup
```
</div></details>

#### 起動後のセットアップ
Worker Nodeを起動しただけでは、EKSからはNodeとして認識されるものの`NotReady`の状態となります。
そのため`aws-auth`というConfigMap(Kubernetesにおける設定ファイルのような概念)を作成・適用する必要があります。
また、VPC内にのOperatorからkubectlを打てるように、Operatorへ紐付けたIAMロールも`aws-auth`に追加する必要があります。

さらに環境変数等設定せずにkubectlを打つためには、`${HOME}/.kube/config`以下に設定が記載されたファイルを保持する必要があります。
参考スクリプトでは、scpでクライアントPCから転送します。クライアントのssh_configへと追記を行うようにしていますが、こいじるのが嫌な場合は適宜修正してください。

<details><summary>`create_cluster.sh`</summary><div>

```sh
#!/usr/bin/env bash

set -o pipefail  # Stop pipe when error occurred
set -o nounset   # Check undefined variables
set -o errexit   # Exit when error occurred
set -o xtrace    # Print debug information

################################################################################
### Variables ##################################################################
################################################################################

ENV_NAME=${ENV_NAME:-"yoda-eks-closed"}
STACK_NAME_BASE=${STACK_NAME_BASE:-$ENV_NAME}
CLUSTER_NAME=${CLUSTER_NAME:-"yoda-k8s-1"}
K8S_VERSION=${K8S_VERSION:-"1.13"}
KEY_NAME=${KEY_NAME:-"yoda-key"}
NODE_INSTANCE_TYPE=${NODE_INSTANCE_TYPE:-"t3.small"}
OPERATOR_AMI_ID=${OPERATOR_AMI_ID:-"ami-03dc8213c70f79358"}
YOUR_COMPUTER_IPADDR=${YOUR_COMPUTER_IPADDR:-$(curl inet-ip.info)}


################################################################################
### 1 Infrastructure ###########################################################
################################################################################

STACK_NAME=${STACK_NAME_BASE}-infrastructure
TEMPLATE=${TEMPLATE_1:-"file://templates/1-infrastructure.yaml"}

aws cloudformation create-stack \
  --stack-name ${STACK_NAME} \
  --template-body ${TEMPLATE} \
  --parameters \
  ParameterKey=EnvName,ParameterValue=${ENV_NAME} \
  ParameterKey=YourComputerIPAddress,ParameterValue=${YOUR_COMPUTER_IPADDR} \
  ParameterKey=KeyName,ParameterValue=${KEY_NAME} \
  ParameterKey=AmiId,ParameterValue=${OPERATOR_AMI_ID} \
  --capabilities CAPABILITY_NAMED_IAM

aws cloudformation wait stack-create-complete --stack-name ${STACK_NAME}

################################################################################
### 2-1 Control Plane SG #######################################################
################################################################################

STACK_NAME=${STACK_NAME_BASE}-${CLUSTER_NAME}-cplane-sg
TEMPLATE=${TEMPLATE_2_1:-"file://templates/2-1-controlplane-sg.yaml"}

K8S_VPC_ID=$(aws ec2 describe-vpcs \
             --filters Name=tag:Name,Values=${ENV_NAME}-k8s-vpc \
             --query "Vpcs[*].VpcId" \
             --output text)

OPERATOR_SG_ID=$(aws ec2 describe-security-groups \
                 --filter Name=group-name,Values=${ENV_NAME}-k8s-operator-sg \
                 --query "SecurityGroups[*].GroupId" \
                 --output text)

aws cloudformation create-stack \
  --stack-name ${STACK_NAME} \
  --template-body ${TEMPLATE} \
  --parameters \
  ParameterKey=EnvName,ParameterValue=${ENV_NAME} \
  ParameterKey=ClusterName,ParameterValue=${CLUSTER_NAME} \
  ParameterKey=K8sVpcId,ParameterValue=${K8S_VPC_ID} \
  ParameterKey=OperatorSgId,ParameterValue=${OPERATOR_SG_ID}

aws cloudformation wait stack-create-complete --stack-name ${STACK_NAME}

################################################################################
### 2-2 EKS ####################################################################
################################################################################

EKS_SERVICE_ROLE_ARN=$(aws iam get-role \
                       --role-name ${ENV_NAME}-eks-service-role \
                       --query "Role.Arn" \
                       --output text)

SUBNET_IDS=$(aws ec2 describe-subnets \
             --filter "Name=tag:Name,Values=${ENV_NAME}-k8s-worker*" \
             --query "Subnets[*].SubnetId" \
             --output text |
             tr '\t' ',')

CPLANE_SG_ID=$(aws ec2 describe-security-groups \
             --filter Name=group-name,Values=${ENV_NAME}-${CLUSTER_NAME}-controlplane-sg \
             --query "SecurityGroups[*].GroupId" \
             --output text)

aws eks create-cluster \
  --name ${CLUSTER_NAME} \
  --role-arn ${EKS_SERVICE_ROLE_ARN} \
  --resources-vpc-config \
    subnetIds=${SUBNET_IDS},securityGroupIds=${CPLANE_SG_ID},endpointPublicAccess=true,endpointPrivateAccess=true \
  --kubernetes-version=${K8S_VERSION}

aws eks wait cluster-active --name ${CLUSTER_NAME}

# Download kubeconfig to /root/.kube
aws eks update-kubeconfig --name ${CLUSTER_NAME}
kubectl get svc  # Check Auth

################################################################################
### 2-3 Worker Nodes ###########################################################
################################################################################

STACK_NAME=${STACK_NAME_BASE}-${CLUSTER_NAME}-workers
TEMPLATE=${TEMPLATE_2_3:-"file://templates/2-3-workers.yaml"}

K8S_ENDPOINT=$(aws eks describe-cluster \
               --name ${CLUSTER_NAME} \
               --query "cluster.endpoint" \
               --output text)

K8S_CA=$(aws eks describe-cluster \
         --name ${CLUSTER_NAME} \
         --query "cluster.certificateAuthority.data" \
         --output text)

MAIN_SUBNET_ID=$(aws ec2 describe-subnets \
                 --filter "Name=tag:Name,Values=${ENV_NAME}-k8s-worker-subnet-1" \
                 --query "Subnets[*].SubnetId" \
                 --output text)

aws cloudformation create-stack \
  --stack-name ${STACK_NAME} \
  --template-body ${TEMPLATE} \
  --parameters \
  ParameterKey=KeyName,ParameterValue=${KEY_NAME} \
  ParameterKey=NodeInstanceType,ParameterValue=${NODE_INSTANCE_TYPE} \
  ParameterKey=ClusterName,ParameterValue=${CLUSTER_NAME} \
  ParameterKey=BootstrapArguments,ParameterValue="--apiserver-endpoint ${K8S_ENDPOINT} --b64-cluster-ca ${K8S_CA}" \
  ParameterKey=NodeGroupName,ParameterValue=${CLUSTER_NAME}-worker \
  ParameterKey=ClusterControlPlaneSecurityGroup,ParameterValue=${CPLANE_SG_ID} \
  ParameterKey=VpcId,ParameterValue=${K8S_VPC_ID} \
  ParameterKey=Subnets,ParameterValue=${MAIN_SUBNET_ID} \
  ParameterKey=EnvName,ParameterValue=${ENV_NAME} \
  ParameterKey=OperatorSgId,ParameterValue=${OPERATOR_SG_ID} \
  --capabilities CAPABILITY_NAMED_IAM

aws cloudformation wait stack-create-complete --stack-name ${STACK_NAME}

# Apply AWS Authenticator ConfigMap
WORKER_ROLE_ARN=$(aws iam get-role \
                  --role-name ${ENV_NAME}-${CLUSTER_NAME}-worker-role \
                  --query "Role.Arn" \
                  --output text)

K8S_ADMIN_ROLE_ARN=$(aws iam get-role \
                     --role-name ${ENV_NAME}-admin-role \
                     --query "Role.Arn" \
                     --output text)


cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
  name: aws-auth
  namespace: kube-system
data:
  mapRoles: |
    - rolearn: ${WORKER_ROLE_ARN}
      username: system:node:{{EC2PrivateDNSName}}
      groups:
        - system:bootstrappers
        - system:nodes
    - rolearn: ${K8S_ADMIN_ROLE_ARN}
      username: system:node:{{EC2PrivateDNSName}}
      groups:
        - system:masters
EOF

################################################################################
### 3 Configuration ############################################################
################################################################################

# Assemble .ssh/config
BASTION_IP=$(aws ec2 describe-instances \
             --filter Name=tag:Name,Values=${ENV_NAME}-bastion \
             --query "Reservations[*].Instances[*].PublicIpAddress" \
             --output text)
OPERATOR_IP=$(aws ec2 describe-instances \
              --filter Name=tag:Name,Values=${ENV_NAME}-operator \
              --query "Reservations[*].Instances[*].PrivateIpAddress" \
              --output text)

cat <<EOF > ~/.ssh/config
Host *
  IdentitiesOnly yes
  ServerAliveInterval 120
  ServerAliveCountMax 10
  ForwardAgent yes
  User ec2-user
  IdentityFile ~/.ssh/${KEY_NAME}.pem
  TCPKeepAlive yes
Host bastion
  HostName ${BASTION_IP}
Host operator
  HostName ${OPERATOR_IP}
  Localforward 0.0.0.0:8765 localhost:8765
  ProxyCommand ssh -W %h:%p bastion
EOF

# Create .kube and transport config file to bastion
ssh -oStrictHostKeyChecking=no bastion \
  mkdir /home/ec2-user/.kube
scp  -oStrictHostKeyChecking=no ~/.kube/config bastion:/home/ec2-user/.kube/

# Create .kube and transport config file to operator
ssh -oStrictHostKeyChecking=no operator \
  mkdir /home/ec2-user/.kube
scp  -oStrictHostKeyChecking=no ~/.kube/config operator:/home/ec2-user/.kube/

# Enable SSH Agent (To log-in to worker nodes from this container)
eval `ssh-agent`
ssh-add ~/.ssh/${KEY_NAME}.pem

echo "Creation Completed."
```
</div></details>

先にあげたテンプレートを作業ディレクトリ(仮に`/templetes`とします）に

- `1-infrastructure.yaml`
- `2-1-controlplane-sg.yaml` 
- `2-3-workers.yaml`
 
と配置して、スクリプトを走らせると通しで環境作成がされます（`Variables`は適宜変更ください）。


## 運用について

### VPCエンドポイントについて
構築時に必要になるVPCエンドポイントですが、それ以降は必要にならないので、セキュリティ面で気になる場合は外してしまっても問題ありません。
ただし、AutoScalingが組まれるので、VPCエンドポイントをそのままにしておけば

- AutoScalingの値を調整するだけでノード増減が可能
- EC2の障害発生時も自動で新しいノードに挿しかわる

ため、つけておくと何かと便利です。
またS3, ECRはVPCエンドポイントポリシーとリソースのポリシーの両方を持ち合わせているため、内部からの流出経路となりうるバケットやリポジトリに対し、**強固な制限** が可能です（e.g. 他のアカウントのアクセスキーを持ち込まれてもエンドポイントポリシーで拒否する、リソースのポリシーで特定VPCエンドポイントからの操作のみ受け付けるようにする、など）。

### コンテナの持ち込み
いくつか手段がありますが、

- ECRに直接Push
- `docer export`を利用し、`tar`で固めたイメージをS3経由で転送

が簡便です。双方上述の通りリソースポリシー、VPCエンドポイントポリシーを持つので持ち込み限定のリポジトリやバケットの作成が可能です。
前者が簡便ですが、後者の方がログや他の資材の持ち込みも可能、と言った長短があります。

### EKSの困ったところ
#### 自動アップデート
https://docs.aws.amazon.com/ja_jp/eks/latest/userguide/platform-versions.html
にあるように、EKSの対応するkubernetesの、マイナーバージョンがアップデートが行われた場合**自動でアップデート**されます。そのため **EKSの上でSparkアプリケーションをを動かしていたのですがある日突然停止** するなんてことも……
（[こちら](https://stackoverflow.com/questions/57643079/kubernetes-watchconnectionmanager-exec-failure-http-403)の事象だったので一部jarを差し替えてことなきを得ました）
レアケースだとは思いますが、気をつけておいた方がいいポイントです。

#### PodとPrivate IPの1:1対応
こちらはよく言われていることですが、EKSではPodに対しVPC内でワークするPrivateIPを割り当てます。そのため

- VPCのレンジを広めに
- NodeのEC2のサイズは大きめ（EC2に割り当て可能なENIの上限はインスタンスサイズで決まるため、それぞれの上限数は[こちら](https://github.com/awslabs/amazon-eks-ami/blob/master/files/eni-max-pods.txt)）

にしておかないと追加で新しいPrivate IPが払い出せない=新しいPodの追加ができない、ことになるため難儀します。

------

慣れるまでは大変でしたが、現在はコンテナの便利さを十分に受けれてるんでないかなーと。KubernetesやEKSユーザーの助けになれば幸いです。

[^1]: [1月の第三者認証対応](https://aws.amazon.com/about-aws/whats-new/2019/01/amazon-eks-achieves-iso-and-pci-compliance/)や、[6月のECR VPCEndpoint対応](https://aws.amazon.com/about-aws/whats-new/2019/06/amazon-eks-now-supports-kubernetes113-ecr-privatelink-kubernetes-pod-security/)など、一気にEKSが利用しやすい環境が整った2019年でした！
[^2]: 今年のKubeConでManaged Nodeが発表されたため、正確には全てを管理するサービスとなっています。
[^3]: [Cloud Formation Roadmap](https://github.com/aws-cloudformation/aws-cloudformation-coverage-roadmap/projects/1)によると現在検討中らしい。。。