pulumiのapplyを理解する

# applyとは

pulumiを使っていると必ずapplyを使う場面が出てくるのですが、私はこのapplyが最初なんなのかよくわかりませんでした。ドキュメント読んだり使ったりしてなんとなくわかったのでまとめてみます。

# pulumiの実行順序

pulumiは以下のようなステップを経て最終的なインフラの変更反映が行われます。

1. ソースコード(typescript/golang/python)の評価
2. ソースコードで定義されたリソース(awsのec2とか)の依存関係を解決
3. リソースごとの依存関係を表すツリー(state)を作成
4. app.pulumi.comにアップロードされたstateとの差分から最終的なインフラの変更操作を導き出す
5. リソースのプロバイダー(awsとかgcpとか）に対して変更を反映する

たぶんterraformと同じような感じだと思います。

# 問題

プログラミング言語でインフラを定義した時に問題になるのが「リソースが生成された後の値をコードで使うにはどうしたらいいのか？」ということです。例としてawsでs3とiam roleを以下のように定義します。

```typescript
// s3
const bucket = new aws.s3.Bucket("my-bucket");
// s3のバケット名(ハッシュが末尾に付きます my-bucket-xyz123 のような感じ)
const bucketName = bucket.bucket;
// s3への権限を持つiam role
const role = new aws.iam.Role("my-role", {
  assumeRolePolicy: JSON.stringify({
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": ["s3:ListBucket"],
            "Resource": [`arn:aws:s3:::${bucket.bucket}`]
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject"
            ],
            "Resource": [`arn:aws:s3:::${bucket.bucket}/*`]
        }
    ]
  })
});
```

上記を実行すると以下のエラーが発生します
>Plan apply failed: Error creating IAM Role my-role-8a4d5da: MalformedPolicyDocument: Has prohibited field Resource


なんだかよくわかりませんね :thinking: 
`console.log`で該当のjsonを出力してみます。


```json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::[object Object]"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject"
      ],
      "Resource": [
        "arn:aws:s3:::[object Object]/*"
      ]
    }
  ]
}
```

バケット名がobjectになっています :eyes:
これはリソース生成後のバケット名を**typescriptが評価される時に文字列として扱おうとしている**ためです。

前述のpulumiの実行順序では、まずtypescriptが評価され、その後にリソースの依存関係を構築します。このためリソース反映の結果を加工して扱おうとすると、リソース変更が反映される前にtypescript上で評価されてしまうため、うまく動きません。

# pulumi.Output<T>

pulumiではリソースの反映結果を`Output<T>型`として扱います。これは平たく言うと`Promise`であり、リソース作成の**実行結果ではありません**。pulumiはOutputをリソース同士で参照して依存関係を構築します。

以下はec2インスタンスにkeypairを割り当てる例です

```typescript
// keypair
const keyPair = new aws.ec2.KeyPair("my-ec2-keypair", {
  publicKey: "YOUR_PUBLIC_KEY"
});
// ec2
const instance = new aws.ec2.Instance("my-ec2-instance", {
  instanceType: "t3.nano",
  keyName: keyPair.keyName,
  // 他にも必須パラメータがありますが、省略
});
```
これはkeypairをOutput型のまま参照しているので、エラーにならずに依存関係が構築されます。エラーになるのはtypescript上でリソースの反映結果を加工する場合です。

# apply

本題のapplyです。リソースの反映結果をtypescript上で加工して扱う場合にapplyを使います。
applyはリソース生成の実行結果を表すOutputを実行後に加工するメソッドで、戻り値はまたOutputになります。

```typescript
const bucket = new aws.s3.Bucket("my-bucket");
const bucketName = bucket.bucket;
const role = new aws.iam.Role("my-role", {
  assumeRolePolicy: bucketName.apply(realBucketName=>{
    // stringを返却するとOutput<string>型になる
    return JSON.stringify({
      "Version": "2012-10-17",
      "Statement": [
          {
              "Effect": "Allow",
              "Action": ["s3:ListBucket"],
              "Resource": [`arn:aws:s3:::${realBucketName}`]
          },
          {
              "Effect": "Allow",
              "Action": [
                  "s3:PutObject",
                  "s3:GetObject"
              ],
              "Resource": [`arn:aws:s3:::${realBucketName}/*`]
          }
      ]
    })
  })
});
```

これでポリシーの文字列が遅延評価されるようになり、s3バケット作成とiam role作成の間に実行されます :smiley: 

# pulumi.all

複数のapplyを扱うにはallを使います。配列でOutputを渡します。

```typescript
const bucket1 = new aws.s3.Bucket("my-bucket-1");
const bucketName1 = bucket1.bucket;
const bucket2 = new aws.s3.Bucket("my-bucket-2");
const bucketName2 = bucket2.bucket;
const role = new aws.iam.Role("my-role", {
  assumeRolePolicy: pulumi.all([bucketName1, bucketName2]).apply(([realBucketName1, realBucketName2]) => {
    return JSON.stringify({
      "Version": "2012-10-17",
      "Statement": [
          {
              "Effect": "Allow",
              "Action": ["s3:ListBucket"],
              "Resource": [
                  `arn:aws:s3:::${realBucketName1}`,
                  `arn:aws:s3:::${realBucketName2}`
              ]
          },
          {
              "Effect": "Allow",
              "Action": [
                  "s3:PutObject",
                  "s3:GetObject"
              ],
              "Resource": [
                  `arn:aws:s3:::${realBucketName1}/*`,
                  `arn:aws:s3:::${realBucketName2}/*`
              ]
          }
      ]
    })
  })
});
```

# pulumi.interpolate

前述のコードははっきり言って読みにくい :sweat: のですが、簡単な文字列加工であれば`pulumi.interpolate`を使います。

```typescript
// s3
const bucket = new aws.s3.Bucket("my-bucket");
const bucketName = bucket.bucket;
const role = new aws.iam.Role("my-role", {
  assumeRolePolicy: JSON.stringify({
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": ["s3:ListBucket"],
            "Resource": [pulumi.interpolate `arn:aws:s3:::${bucketNanme}`]
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:PutObject",
                "s3:GetObject"
            ],
            "Resource": [pulumi.interpolate `arn:aws:s3:::${bucketName}/*`]
        }
    ]
  })
});
```

簡潔に表現できました :smiley: 他に配列用の `pulumi.concat` があります。