Deploy on AWS with CircleCI - OpenID Connect
Tags: circleci, aws, terraform, devops
Published: 2022-04-24 14:50:11

One of the first things that we do during deployment configuration is creating credentials for CI/CD tool.
For AWS, the simplest way is to create **IAM User** with programmatic keys. They are placed into secrets or environment variables inside CI/CD tool as: `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY`. It is called **long-term credentials**, because there is not expiration time by default. We have to rotate them yourself.

The other, more secure, way is to use **short-term credentials** which invalidates automatically after specific time.
In this article I will present example solution using **OpenID Connect* protocol, CircleCI as deployment platform and AWS as target. I will not dive into protocol details.


# Main idea overview

This is simplified diagram that shows how the whole process looks like.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0plb3x33m7zenn320rhv.png)

We need 2 things to authorize in AWS:
- OIDC Token
- AWS IAM Role

OIDC Token will be generated by CircleCI, but IAM Role needs to be created by us.

# Terraform - create IAM Role & OIDC Provider

### main.tf - quite common. Description no needed
```terraform
terraform {
  required_version = "1.1.4"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "4.8.0"
    }
  }
}

provider "aws" {
  region = "eu-central-1"
}
```


### variables.tf

We need 3 values to configure our IAM Role & OIDC Provider resources in AWS. `circleci_org_id` and `circleci_thumbprint` are used to create OIDC Provider, and they are rather static for our purposes (we have multiple repositories/CircleCI projects in the same CircleCI organization).
`circleci_project_id` allows us to limit IAM Role to specific projects. We want to authorize to AWS from specific (one or multiple) projects, not the whole organization.

```terraform
variable "circleci_org_id" {
  default = "CHANGE_ME"
  description = "CircleCI Organization ID"
}

variable "circleci_thumbprint" {
  # https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc_verify-thumbprint.html
  default = "CHANGE_ME"
  description = "Fingerprint of CircleCI servers"
}

variable "circleci_project_id" {
  default = "CHANGE_ME"
  description = "CircleCI Project ID"
}
``` 

### oidc.tf

The most important part. There we have OIDC Provider and IAM Role with federated principal and 2 conditions. They are compared with values from OIDC Token. Second condition contains schema:
```
"org/${var.circleci_org_id}/project/${var.circleci_project_id}/user/*"
```
It means, that all users that have permissions to specific CircleCI project in specific CircleCI organization are able to successfully run this pipeline. This whole configuration works with Github platform connected to CircleCI, so Github manage permissions.

At end we attach other permissions to role. 

```terraform
resource "aws_iam_openid_connect_provider" "default" {
  url             = "https://oidc.circleci.com/org/${var.circleci_org_id}"
  client_id_list  = [var.circleci_org_id]
  thumbprint_list = [var.circleci_thumbprint]
}

###########################################################


data "aws_caller_identity" "current" {}

data "aws_iam_policy_document" "circleci" {
  statement {
    actions = ["sts:AssumeRoleWithWebIdentity", "sts:TagSession"]

    principals {
      type = "Federated"
      identifiers = [
        "${aws_iam_openid_connect_provider.default.arn}"
      ]
    }

    condition {
      test     = "StringEquals"
      variable = "oidc.circleci.com/org/${var.circleci_org_id}:aud"
      values   = [var.circleci_org_id]
    }

    condition {
      test     = "ForAnyValue:StringLike"
      variable = "oidc.circleci.com/org/${var.circleci_org_id}:sub"
      values   = ["org/${var.circleci_org_id}/project/${var.circleci_project_id}/user/*"]
    }
  }
}

resource "aws_iam_role" "circleci" {
  name                 = "circleci"
  path                 = "/"
  assume_role_policy   = data.aws_iam_policy_document.circleci.json
}

resource "aws_iam_role_policy_attachment" "attach_1" {
  role       = aws_iam_role.circleci.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}
```

# CircleCI configuration

.circleci/config.yml
```yaml
version: 2.1

orbs:
  aws-cli: circleci/aws-cli@3.0.0


commands:
  aws-oidc-setup:
    description: Setup AWS auth using OIDC token
    parameters:
      aws-role-arn:
        type: string
    steps:
      - run:
          name: Get short-term credentials
          command: |
            STS=($(aws sts assume-role-with-web-identity --role-arn << parameters.aws-role-arn >> --role-session-name "${CIRCLE_BRANCH}-${CIRCLE_BUILD_NUM}" --web-identity-token "${CIRCLE_OIDC_TOKEN}" --duration-seconds 900 --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' --output text))
            echo "export AWS_ACCESS_KEY_ID=${STS[0]}" >> $BASH_ENV
            echo "export AWS_SECRET_ACCESS_KEY=${STS[1]}" >> $BASH_ENV
            echo "export AWS_SESSION_TOKEN=${STS[2]}" >> $BASH_ENV
      - run:
          name: Verify AWS credentials
          command: aws sts get-caller-identity

jobs:

  test:
    executor: aws-cli/default
    steps:
      - checkout
      - aws-cli/install
      - aws-oidc-setup:
          aws-role-arn: "arn:aws:iam::XXXXXXXXXXXX:role/circleci"
      - run:
          name: Test AWS connection
          command: aws s3 ls


workflows:
  build_and_deploy:
    jobs:
      - test:
          context:
            - example-context
```

Here we have configuration with our custom command used to authorize in AWS. There are few important things:
- `parameters.aws-role-arn` -> previously created IAM Role
- `role-session-name` -> it could be anything. It will shows up as `user` field in AWS CloudTrail
- `CIRCLE_OIDC_TOKEN` -> it is **OIDC Token**. It is added by CircleCI **only** if job uses **any** context. That's why I added `example-context` in workflows. This context can be empty also.
- `duration-seconds` - time that token is valid

You can check what is inside `CIRCLE_OIDC_TOKEN` using SSH in job, and decode it here: [jwt.io](https://jwt.io/)
```
circleci@a094c7852509:~$ echo $CIRCLE_OIDC_TOKEN

eyJraWQiOiJhSzlqNmRZQm8zS0R3NGdmV3k4eGRNTFU1UThPeWNzcW44S.......
```

# Documentation

- https://circleci.com/docs/2.0/openid-connect-tokens/
- https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html


It's my first article, so feedback is welcome :)