AWS CDKを使ってTrend Micro Cloud One Container SecurityをEKSにセットアップしてみた その２

# 背景

[前回記事](https://qiita.com/nst-zama/items/954ce559b2de46e946ed)の続きです。未読の方はそちらからお願いします。

## 前回のおさらい

前回記事でやったことを軽くまとめておきます。

1. [Cloud One Container Security](https://www.trendmicro.com/ja_jp/business/products/hybrid-cloud/cloud-one-container-image-security.html)の監視対象となるAWS EKSクラスタの作成
    - AWS CDKを用いて以下のような環境を作成した
        1. VPC(public subnet × 2, private subnet × 2)
        1. EKSクラスタ
        1. RDS(EKSクラスタ上のPodやJobなどが利用する想定)
        1. RDSの接続情報が格納されたAWS Secrets Manager上のSecret
        1. AWS Secrets ManagerのSecretをk8s上のSecretとしてマウントできるExternal Secrets Controller
1. 上記EKSクラスタをCloud One Container Securityの監視対象化
    - [公式ガイドライン](https://cloudone.trendmicro.com/docs/container-security/cluster-add/#install-the-policy-based-deployment-controller)に記載された手順をAWS CDKで実装して、作成したEKSクラスタにPolicy Based Deployment Controllerをインストールした
        1. Cloud Oneの管理コンソール上に表示されたAPI KeyをAWS Systems Managerのパラメータストアに格納(Systems Managerを選択できなかった理由は前回記事を参照)
        1. Helm Chart経由でPolicy Based Deployment Controllerをインストール
        1. Cloud Oneの管理コンソール上からクラスタが監視対象になっていることを確認


# EKSクラスタの更新 - 作成したRDSにサンプルデータをインストールする

前回作ったクラスタがせっかくCloud One Container Securityの管理下に入ったので、動作確認を兼ねてクラスタ上にリソースを追加してみましょう。

EKSクラスタと同じVPCに作成したRDSに何か初期データを投入したいと思います。前回解説した通り、RDSのインスタンスへはEKSクラスタからしかアクセスできないよう制限をかけていますので、RDSにデータを投入するDocker Imageを作成してKubernetesのJobとしてEKSクラスタ上で実行することにします。

また、投入するデータに関してはGitHub上に公開されているaws-samplesのデータを使わせて頂きます。

![rds-initialize.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782759/79d38ca0-9dd9-e5b8-64c6-af610acf8418.png)

## AWS CDKによるKubernetes Jobのapply

普通に`kubectl`経由でmanifestをapplyしても良いのですが、せっかくなのでこの辺りもAWS CDK経由でやりましょう。

`kubectl`でやる場合は以下のような手順になるかと思いますが、

1. Dockerfileを書いてDocker Imageをビルドする
1. Docker Registry(例えばAWS Elastic Container Registry)にpushする
1. pushしたImageを参照するmanifestファイルを書く
1. 作成したファイルを`kubectl`経由でapplyする

AWS CDKでやる場合は必要なソース群を一括で管理し、`cdk deploy`コマンド一発で上記の手順を一括実行するよう設定できます。

### DockerImageAssetの作成

CDKで自作のDocker Imageを管理するAssetを作成します。

とは言っても特に難しいことはなく、`docker build`でビルドする時のディレクトリ構成、つまり`Dockerfile`が直下に置かれ、そこから参照されるスクリプトなどが配置されたディレクトリを作成します。

![スクリーンショット 2021-08-08 16.13.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782759/f526cf93-b9a9-f39a-a5b1-3fcf5341d9c4.png)

```bash:./docker/rds-initializer/scripts/import_data.sh
#!/bin/bash

# RDS(Postgres)の認証情報が以下にマウントされる
DBINFO_DIR=/etc/secret
DBINFO_JSON=$DBINFO_DIR/rds-db-info

# 認証情報はJSONで格納されているのでjqでパースして環境変数に設定する
export PGHOST=`cat $DBINFO_JSON | jq -r .host`
export PGPORT=`cat $DBINFO_JSON | jq -r .port`
export PGUSER=`cat $DBINFO_JSON | jq -r .username`
export PGPASSWORD=`cat $DBINFO_JSON | jq -r .password`
export PGDATABASE=postgres


# AWSの公式サンプルデータをCloneする
git clone https://github.com/aws-samples/aws-database-migration-samples/

# CloneしたSQLを実行する
cd aws-database-migration-samples/PostgreSQL/sampledb/v1/
psql -f ./install-postgresql.sql

```

```dockerfile:./docker/rds-initializer/scripts/Dockerfile
FROM debian:stable-slim

RUN apt-get update && apt-get install -y jq && apt-get install -y git && apt-get install -y postgresql-client

RUN mkdir /tmp/workspace

COPY ./scripts/import_data.sh /tmp/workspace/import_data.sh

WORKDIR /tmp/workspace

ENTRYPOINT ["sh", "import_data.sh"]

```

ディレクトリができたら、`@aws-cdk/aws-ecr-assets`の`DockerImageAssert`にディレクトリを指定するだけでOKです。

```typescript
import {DockerImageAsset} from '@aws-cdk/aws-ecr-assets';

//中略

const rdsInitializerImage = new DockerImageAsset(this, 'rds-initializer', {
    directory: path.join(__dirname, '../docker/rds-initializer')
});
```

これだけで、`cdk deploy`コマンド実行時に`docker build`を実行しCDK用のECRレポジトリである`aws-cdk/assets`に`push`してくれます。

![スクリーンショット 2021-08-08 16.23.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782759/960ca450-11f8-3f16-9b1b-759469199f7a.png)

### CDKでKuberbetes JobのManifestを作成

前回記事でも特に何の前置きもなくhelm chartのインストールやexternal secretのマニフェストをAWS CDKの`Construct`として定義してしまいましたが、改めて、AWS CDKの`@aws-cdk/aws-eks`ではクラスタに適用するマニフェストを`KubernetesManifest`として定義することができます。

今回は、先ほど例示した`DockerAsset`と作成されたImageを参照するKubernetes Jobのマニフェストを定義するカスタム`Construct`を作成しました。

```typescript:./lib/manifest-rds-initialize-job.ts
import * as path from 'path';

import * as cdk from '@aws-cdk/core';
import * as eks from '@aws-cdk/aws-eks';
import {DockerImageAsset} from '@aws-cdk/aws-ecr-assets';

/**
 * RdsInitializeJobに指定するProperty
 */ 
export interface RdsInitializeJobProps{
    /** マニフェストを適用するEKSクラスタ*/
    cluster: eks.ICluster,
    /** RDSの認証情報が格納されているSecretの情報*/
    rdsCredentialSecret: {
        name: string,
        key: string
    }
}

/**
 * RDSの初期化Jobを作成するConstruct
 */ 
export class RdsInitializeJob extends cdk.Construct {
    /** 作成されるマニフェストのConstructNode(依存性の定義に使用)*/
    readonly manifestNode: cdk.ConstructNode
    
    /**
     * コンストラクタ
     */ 
    constructor(scope: cdk.Construct, id: string, props: RdsInitializeJobProps){
        super(scope, id);
        
        const {cluster, rdsCredentialSecret} = props;
        
        /**
         * jobで使用するDockerイメージのAsset
         */ 
        const rdsInitializerImage = new DockerImageAsset(this, 'rds-initializer', {
            directory: path.join(__dirname, '../docker/rds-initializer')
        });
        
        /**
         * RDS初期化ジョブのマニフェスト
         */ 
        const manifest = new eks.KubernetesManifest(this, 'rds-initialize-job', {
          cluster,
          manifest:[
            {
              apiVersion: "batch/v1",
              kind: "Job",
              metadata:{
                name: "rds-initialize-job"
              },
              spec:{
                template:{
                  spec:{
                    containers: [
                      {
                        name: "rds-initialize",
                        image: rdsInitializerImage.imageUri, //DockerAsset化Imageをこのように参照できる
                        volumeMounts:[
                          {
                            name: "rdsdbinfo",
                            mountPath: "/etc/secret"
                          }
                        ]
                      }
                    ],
                    volumes: [
                      { //external secretで連携したRDSの認証情報
                        name: "rdsdbinfo",
                        secret: {
                          secretName: rdsCredentialSecret.name,
                          items: [
                            {
                              key: rdsCredentialSecret.key,
                              path: 'rds-db-info'
                            }
                          ]
                        }
                      }
                    ],
                    restartPolicy: "Never"
                  }
                }
              }
            }
          ]
        });
        
        this.manifestNode = manifest.node;
    }
}
```

作成した`Construct`を`Stack`側で呼び出します。

```typescript:./lib/cdk-container-security-sample-stack.ts
import * as cdk from '@aws-cdk/core';
import * as ec2 from '@aws-cdk/aws-ec2';
import * as eks from '@aws-cdk/aws-eks';
import * as rds from '@aws-cdk/aws-rds';

import {PolicyBasedDeploymentController} from './helm-policybased-deployment-controller';
//追加
import {RdsInitializeJob} from './minifest-rds-initialize-job';

//中略

export class CdkContainerSecuritySampleStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    //だいぶ中略

    if(postgresRdsSecretInfo){
      //中略

      // rdsの初期化Jobの適用
      const rdsInitializeJob = new RdsInitializeJob(this, 'rds-initialize', {
        cluster,
        rdsCredentialSecret: {
          name: 'postgres-rds-secret',
          key: "dbcredential"
        }
      });
      // このマニフェストがpostgresRdsSecretManifestの後に適用されるよう依存関係を明示
      rdsInitializeJob.manifestNode.addDependency(postgresRdsSecretManifest);
    }
  }

  //中略
}

```

### 動作確認

この状態で再度、

```bash
$ yarn cdk deploy --parameters CloudOneApiKey=/cloudone/apikey/samplecluster
```

を実行します。

実行後しばらく待つと、RDSへのデータ投入が完了します。

![スクリーンショット 2021-08-08 12.27.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782759/dfe7a41a-8511-c8b3-32cf-16ba5ff4a951.png)
![スクリーンショット 2021-08-08 12.32.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782759/fc30e119-8643-06f3-ad10-1b98be12b7ef.png)

そして肝心のCloud Consoleを覗くと...

![スクリーンショット 2021-08-08 12.30.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782759/8bcecd01-58c8-d49d-383b-47a79f3da26e.png)


きちんとイベントとして記録されていることがわかります。


# Deep Security Smart Checkのインストール

[Deep Security Smart Check](https://cloudone.trendmicro.com/docs/container-security/sc-about/)とは、Trend Micro社が提供するContainer Image Scannerです。Container Securityと連動して、例えばスキャン未実施のImageや一定以上の脆弱性があるImageがクラスタにデプロイされることを阻止することができます。

![スクリーンショット 2021-08-09 9.58.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782759/ad51b15f-486e-8d22-60c8-2d8c06fb7a32.png)
画像は公式の[製品紹介ページ](https://www.trendmicro.com/ja_jp/business/products/hybrid-cloud/cloud-one-container-image-security.html)より

前回のPolicy based deployment controllerと同様、[公式ガイド](https://cloudone.trendmicro.com/docs/container-security/sc-integrate/)の手順をAWS CDKで実施します。

## API KeyをSystems Managerのパラメータストアに登録する

公式ガイドの手順に従って、Cloud OneのコンソールからContainer Security > Add Scannerを選択すると、前回同様API Keyが発行されます。

![スクリーンショット 2021-08-09 9.10.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782759/48c1935d-cbc5-e0e9-0c64-1d6dae16fc41.png)

前回解説した通り、このAPI KeyをCDKのソースや設定ファイルなどにハードコードするわけにはいきません。本来はSecrets Managerを使いたいところですが、これも前回解説した通り2021年8月現在でちょっと使いにくいので、代わりにSystems Managerのパラメータストアを利用します。

![スクリーンショット 2021-08-09 10.12.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782759/1fabdb3d-69cd-ddf7-8ba3-22a4171dc245.png)

今回は`/cloudone/apikey/smartcheck`という名前で登録しました。

## SmartCheckをEKSクラスタにインストール

前回同様、AWS CDK経由で用意されている`helm`チャートをインストールします。

### Constructの作成

`Stack`の中に直接定義しても良いのですが、専用のConstructを作成して分離しておきます。

```typescript:./lib/helm-smartcheck.ts
import * as cdk from '@aws-cdk/core';
import * as eks from '@aws-cdk/aws-eks';

/**
 * SmartCheckのプロパティ
 */ 
export interface SmartCheckProps{
    /** インストール対象のEKSクラスタ*/
    cluster: eks.ICluster,
    /** Cloud Oneが発行したAPI Key*/
    apiKey: cdk.SecretValue
}

/**
 * Trend Micro Cloud One Container SecurityのDeepSecurity SmartCheckをインストールするHelmを適用するConstruct
 * @see https://cloudone.trendmicro.com/docs/container-security/sc-integrate/
 * @see https://github.com/deep-security/smartcheck-helm
 */ 
export class SmartCheck extends cdk.Construct{
    constructor(scope: cdk.Construct, id: string, props: SmartCheckProps){
        super(scope, id);
        
        const {cluster, apiKey} = props;
        new eks.HelmChart(this, "smartcheck-deployment", {
            cluster,
            release: "deepsecurity-smartcheck",
            chart: "https://github.com/deep-security/smartcheck-helm/archive/master.tar.gz",
            values: {
                cloudOne: {
                    apiKey
                },
                auth: {
                    /**
                     * 管理コンソールのパスワードを作成する際のシード
                     * ここではStack IDを使用する
                     */ 
                    secretSeed: cdk.Stack.of(this).stackId
                }
            } 
        })
    }
}
```

前回のpolicy-based-deployment-controllerと同じといえば同じですが、一点だけ注意ください。

[smartcheck-helmのGitHubレポジトリ](https://github.com/deep-security/smartcheck-helm)を確認すると以下のようなサンプルがあります。

```yaml:smartcheck-helmのGitHubレポジトリより引用
## activationCode is the product activation code.
##
## Default value: (none)
# activationCode: YOUR-CODE-HERE

# While it's still possible to use activationCode to activate Deep Security Smart Check,
# we strongly encourage you to enroll your Deep Security Smart Check installation with a valid Cloud One Container Security API key.
# You can find more detail on how to get a API key in https://cloudone.trendmicro.com/docs/container-security/api-key-create/
cloudOne:
 apiKey: YOUR-CLOUD-ONE-CONTAINER-SECURITY-API-KEY

auth:
  ## secretSeed is used as part of the password generation process for
  ## all auto-generated internal passwords, ensuring that each installation of
  ## Deep Security Smart Check has different passwords.
  ##
  ## Default value: {must be provided by the installer}
  secretSeed: YOUR-SECRET-HERE
```

`apiKey`の他に`secretSeed`という、SmartCheckの管理画面のパスワード生成に使用するシードが必須となっています。今回はStackのIDを使用することにしました。

```typescript
                auth: {
                    /**
                     * 管理コンソールのパスワードを作成する際のシード
                     * ここではStack IDを使用する
                     */ 
                    secretSeed: cdk.Stack.of(this).stackId
                }
```

### Stack側から呼び出し

作成したConstructをStack側から呼び出します。前述の通り、パラメータストアにAPI Keyを保存しているので以下のようになります。

```typescript:./lib/cdk-container-security-sample-stack.ts
import * as cdk from '@aws-cdk/core';
import * as ec2 from '@aws-cdk/aws-ec2';
import * as eks from '@aws-cdk/aws-eks';
import * as rds from '@aws-cdk/aws-rds';

import {PolicyBasedDeploymentController} from './helm-policybased-deployment-controller';
//追加
import {SmartCheck} from './helm-smartcheck';

//中略

export class CdkContainerSecuritySampleStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    //だいぶ中略

    //Cloud One Container Securityの導入
    new PolicyBasedDeploymentController(this, "cloudone", {
      cluster,
      apiKey: cdk.SecretValue.cfnParameter(new cdk.CfnParameter(this, 'CloudOneApiKey', {type: "AWS::SSM::Parameter::Value<String>", noEcho: true}))
    });
    
    //DeepSecurity SmartCheckの導入
    new SmartCheck(this, "smartcheck", {
      cluster,
      apiKey: cdk.SecretValue.cfnParameter(new cdk.CfnParameter(this, 'SmartCheckApiKey', {type: "AWS::SSM::Parameter::Value<String>", noEcho: true}))
    });  }

  //中略
}
```

この変更によってパラメータが１つ増え、deployコマンドは以下のようになります。

```bash
$ yarn run cdk deploy --parameters CloudOneApiKey=/cloudone/apikey/samplecluster --parameters SmartCheckApiKey=/cloudone/apikey/smartcheck
```

## 動作確認

Smart Checkは自身のクラスタ上に作成されます。管理コンソールのUIと初期のID/Passwordの取得方法は[公式ガイド](https://cloudone.trendmicro.com/docs/container-security/sc-install/)を参照ください。

### 管理コンソールへのログインと設定

![スクリーンショット 2021-08-09 11.06.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782759/9d55f5e1-6ef8-ebe0-e260-2452a4b8e58c.png)

![スクリーンショット 2021-08-09 11.10.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782759/042cc714-b87c-348e-a061-4f9dcbd19e95.png)


EKSと同じアカウント/リージョンにあるECRがスキャン対象であれば、`Registry ID`の設定は不要です。また、EKSのセットアップ時にECRを参照するロールもInstance Roleに付与されているので、`Authentication Method`は`Instance Role`でOKです。

### ECRのスキャン

設定後しばらく待つとECR内にあるImageのScanが開始されます。
以下は私のレジストリをScan中の結果ですが、雑に管理しているECRなので結構Critaicalが出ていますね。。

![スクリーンショット 2021-08-09 11.25.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782759/ffb36b65-abdf-9e92-b9c2-4ec1eb6a6262.png)

### Cloud Oneコンソールの確認

Cloud Oneコンソール > Container Security > Scannersを確認するとちゃんと認識されています。

![スクリーンショット 2021-08-09 11.28.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782759/e347e871-dfb3-0028-f4ad-003b1fbf687f.png)

Scannerが認識されたので、Scan関連のPolicy(未スキャンのイメージはデプロイをブロックするなど)を設定できるようになりました。

![スクリーンショット 2021-08-09 11.29.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1782759/0d97bcc7-4e94-64aa-9bd8-e70e189f871c.png)

## 後片付け

前回も書きましたが、使い終わったら後片付けをしましょう。EKSを稼働しているとそれなりの利用料がかかります。

繰り返しになりますが、Infrastructure as Codeの良いところは作った環境を躊躇なく削除できるところです。もう一度動作検証したくなったらもう一度`depoly`すれば良いだけです。

```bash
$ yarn cdk destroy #作成した環境を削除する
```

# まとめ

## Infrastructure as Codeとセキュリティ

今回の記事ではAWS EKSに対するTrend Micro Cloud One Container SecurityをAWS CDKを用いたInfrastructure as Codeで実現する方法を紹介しました。

AWSもCloud Oneも素晴らしいサービスです。そういった大前提の中で、我々エンジニアにとってのセキュリティリスクは、実は我々エンジニアの人為ミスにあるのではないでしょうか。

- 単純に知識がなく、適切な設定ができない
- 納期や時間に追われて設定をミスしてしまう
- PoCや技術スパイク中に「とりあえず」設定した雑な設定をうっかり本番環境に適用してしまう

などなどの問題をAWS CDKをはじめとしたInfrastructure as Codeツールが解決します。

自動化は効率化や納期短縮のためにやるのではなく、ガバナンスやセキュリティを含めた品質向上のためにやるのです。

## AWS CDKとKubernetes

今回紹介した通り、AWS CDKを用いてAWS EKSのクラスタを構築するだけでなく、`helm chart`のインストールや`manifest`の定義をすることができます。それだけでなく、`@aws-cdk/aws-ecr-assets`などを活用して、Docker Imageの管理までAWS CDKの中で管理することができます。

言い換えると、<b>インフラ層からアプリケーション層まで１つのソースベースで一括管理できる</b>ということです。

こういった構造は、アジャイルやリーンスタートアップのような<b>少数精鋭チームが自律的に判断し、臨機応変に活動する</b>形態に於いて非常に有用です。

## マインドや文化を支えるには技術も必要

アジャイルやリーンスタートアップ、CI/CDやDevOpsなどの近年流行の概念はツール論や技術論だけではなく文化やマインドセットが占める割合が多い概念です。しかし、文化やマインドセットを支えるのは技術であることも事実です。

「失敗を恐れずにチャレンジしよう」という号令をかけるには、その裏で失敗した時に最悪の事態を招かないようリスク削減を行うための技術投資が必要です。その中にはもちろん<b>セキュリティ</b>も含まれます。

今回紹介したようにAWSやCloud Oneのような素晴らしいサービスの力を借りつつ、<b>安全にチャレンジができる</b>よう技術を磨いていきましょう。

# Appendix

## 今回のソースコードについて

ソースコード全体をGitHubに[公開](https://github.com/nri-st-cloud/cdk-container-security-sample)しました。自由にお使いください。