DEV Community

Cover image for Deploy on AWS with CircleCI - OpenID Connect
Krzysztof Łuczak
Krzysztof Łuczak

Posted on

Deploy on AWS with CircleCI - OpenID Connect

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

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 {
  required_version = "1.1.4"

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

provider "aws" {
  region = "eu-central-1"
}
Enter fullscreen mode Exit fullscreen mode

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.

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"
}
Enter fullscreen mode Exit fullscreen mode

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/*"
Enter fullscreen mode Exit fullscreen mode

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.

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"
}
Enter fullscreen mode Exit fullscreen mode

CircleCI configuration

.circleci/config.yml

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
Enter fullscreen mode Exit fullscreen mode

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

circleci@a094c7852509:~$ echo $CIRCLE_OIDC_TOKEN

eyJraWQiOiJhSzlqNmRZQm8zS0R3NGdmV3k4eGRNTFU1UThPeWNzcW44S.......
Enter fullscreen mode Exit fullscreen mode

Documentation

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

Discussion (0)