Skip to content

Instantly share code, notes, and snippets.

@andreialecu
Last active February 27, 2024 12:13
Show Gist options
  • Save andreialecu/df36776c0745641c6cad9737ba25474d to your computer and use it in GitHub Desktop.
Save andreialecu/df36776c0745641c6cad9737ba25474d to your computer and use it in GitHub Desktop.
aws-cdk terraform
import * as cdk from "aws-cdk-lib";
import { TerraformSupport } from "./terraform-support";
import * as azureApp from "@cdk-cloudformation/tf-azuread-application";
export class AzureApplicationStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props: cdk.StackProps) {
super(scope, id, props);
// First deploy step 1, then change to 2 and deploy again
let step = 1;
// Step 1, deploy this:
const terraform = new TerraformSupport(this, "TerraformConstruct", {
activateTypes: ["TF::AzureAD::Application"],
});
if (step === 2) {
const app = new azureApp.CfnApplication(this, "AzureApplication", {
displayName: "Azure Test App",
signInAudience: "AzureADMultipleOrgs",
});
new cdk.CfnOutput(this, "AzureAppId", {
value: app.attrApplicationId,
});
}
}
}
import { Construct } from "constructs";
import * as cdk from "aws-cdk-lib";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as iam from "aws-cdk-lib/aws-iam";
import * as lambda from "aws-cdk-lib/aws-lambda";
import { aws_cloudformation as cloudformation } from "aws-cdk-lib";
import fs = require("fs");
export class TerraformSupport extends Construct {
executionRole: cdk.aws_iam.Role;
constructor(
scope: Construct,
id: string,
{ activateTypes }: { activateTypes: string[] }
) {
super(scope, id);
const stateS3Bucket = new s3.Bucket(this, "StateS3Bucket", {
bucketName: `cfntf-${cdk.Stack.of(this).region}-${
cdk.Stack.of(this).account
}`,
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
});
const executorLambdaServiceRole = new iam.Role(
this,
"ExecutorLambdaServiceRole",
{
assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
inlinePolicies: {
CoreFunctionality: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
actions: [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
],
resources: ["*"],
}),
new iam.PolicyStatement({
actions: ["secretsmanager:GetSecretValue"],
resources: [
cdk.Arn.format(
{
service: "secretsmanager",
resource: "secret",
arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME,
resourceName: "terraform/*",
},
cdk.Stack.of(this)
),
],
}),
new iam.PolicyStatement({
actions: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
resources: [stateS3Bucket.arnForObjects("*")],
}),
],
}),
},
}
);
const executorLambdaFunction = new lambda.Function(
this,
"ExecutorLambdaFunction",
{
functionName: "cfntf-executor",
code: lambda.Code.fromAsset(
// fromAsset requires a directory, so we create a temporary one
fs.mkdtempSync("cdktfsupp"),
{
assetHashType: cdk.AssetHashType.CUSTOM,
assetHash: "v1.1-tf-1.7.4",
bundling: {
image: lambda.Runtime.PYTHON_3_8.bundlingImage,
command: [
"bash",
"-c",
[
"curl -o /asset-output/terraform.zip https://releases.hashicorp.com/terraform/1.7.4/terraform_1.7.4_linux_arm64.zip",
"unzip -o /asset-output/terraform.zip -d /asset-output",
"rm /asset-output/terraform.zip",
"curl -o /asset-output/index.py https://raw.githubusercontent.com/iann0036/cfn-tf-custom-types/755fbfc7eb6f30f3d7dfca700bad00ba19ee7ee3/executor_lambda/index.py",
"ls -la /asset-output",
].join(" && "),
],
},
}
),
handler: "index.handler",
runtime: lambda.Runtime.PYTHON_3_8,
architecture: lambda.Architecture.ARM_64,
role: executorLambdaServiceRole,
environment: {
BUCKET: stateS3Bucket.bucketName,
},
memorySize: 1024,
timeout: cdk.Duration.minutes(15),
retryAttempts: 1,
logRetention: cdk.aws_logs.RetentionDays.TWO_WEEKS,
}
);
this.executionRole = new iam.Role(this, "ExecutionRole", {
assumedBy: new iam.ServicePrincipal(
"resources.cloudformation.amazonaws.com"
),
});
this.executionRole.addToPolicy(
new iam.PolicyStatement({
actions: ["lambda:InvokeFunction"],
resources: [executorLambdaFunction.functionArn],
})
);
this.executionRole.addToPolicy(
new iam.PolicyStatement({
actions: ["s3:DeleteObject", "s3:GetObject", "s3:ListBucket"],
resources: [stateS3Bucket.arnForObjects("*")],
})
);
activateTypes.forEach((typeName) => {
const cfnTypeActivation = new cloudformation.CfnTypeActivation(
this,
"CfActivate_" + typeName.replace("::", ""),
{
autoUpdate: true,
publisherId: "e1238fdd31aee1839e14fb3fb2dac9db154dae29",
type: "RESOURCE",
typeName,
executionRoleArn: this.executionRole.roleArn,
}
);
});
}
}
@andreialecu
Copy link
Author

andreialecu commented Feb 25, 2024

Usage:

Deployment will need to be done in two steps, because we first need to activate the TF::* CloudFormation extension that we need to use.

See the step variable and logic in azure-app.stack.ts

You will then need to create a terraform/azuread secret. You can use this as a template:

    // you may use the azure cli to create a service principal
    // az ad sp create-for-rbac --name AwsCdkTFIntegration
    // the command will print some credentials you can use
    new secretsManager.Secret(this, "TFAzureAdSecret", {
      secretName: "terraform/azuread",
      removalPolicy: cdk.RemovalPolicy.RETAIN,
      description:
        "See: https://github.com/iann0036/cfn-tf-custom-types/blob/docs/docs/azuread.md",
      secretObjectValue: {
        // leave the placeholders as is and go into the AWS console to update the values
        client_id: cdk.SecretValue.unsafePlainText("service principal appId"),
        tenant_id: cdk.SecretValue.unsafePlainText("service principal tenant"),
        client_secret: cdk.SecretValue.unsafePlainText("service principal password"),
      },
    });

Note that you may need to grant the following permissions to the service principal, as per:
https://github.com/iann0036/cfn-tf-custom-types/blob/docs/resources/azuread/TF-AzureAD-Application/docs/README.md

-> NOTE: If you're authenticating using a Service Principal then it must have permissions to both Read and write owned by applications and Sign in and read user profile within the Windows Azure Active Directory API.

You can do that via the CLI using these commands (you can read what the identifiers represent here):

az ad app permission add --id {appId} --api 00000003-0000-0000-c000-000000000000 --api-permissions df021288-bdef-4463-88db-98f22de89214=Role 1bfefb4e-e0b5-418b-a88f-73c46d2cc8e9=Role

az ad app permission grant --id {appId} --api 00000003-0000-0000-c000-00000000000

Afterwards, set step=2, deploy again, and you should see a new Azure application in your account.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment